diff --git a/src/tauon/t_modules/t_bootstrap.py b/src/tauon/t_modules/t_bootstrap.py index c818d74bf..a545c8523 100644 --- a/src/tauon/t_modules/t_bootstrap.py +++ b/src/tauon/t_modules/t_bootstrap.py @@ -13,7 +13,7 @@ #@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 (???) diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index e3fc3fc87..a4da674b0 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -346,37 +346,6 @@ from pylast import Artist, LibreFMNetwork from PIL.ImageFile import ImageFile -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_tls() -> ssl.SSLContext: - """TLS setup (needed for frozen installs)""" - # 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 - return ssl.create_default_context(cafile=get_cert_path()) - -def whicher(target: str) -> 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 - class LoadImageAsset: assets: list[LoadImageAsset] = [] @@ -450,24 +419,6 @@ def render(self, x: int, y: int, colour) -> None: self.rect.y = round(y) SDL_RenderCopy(renderer, self.sdl_texture, None, self.rect) -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 - -def no_padding() -> int: - """This will remove all padding""" - return 0 - class DConsole: """GUI console with logs""" def __init__(self) -> None: @@ -477,61 +428,6 @@ def toggle(self) -> None: """Toggle the GUI console with logs on and off""" self.show ^= True -def uid_gen() -> int: - return random.randrange(1, 100000000) - -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 is 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) - -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 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""" @@ -934,8 +830,6 @@ def __init__(self): self.discord_status = "Standby" self.mouse_unknown = False self.macstyle = prefs.macstyle - if macos or detect_macstyle: - self.macstyle = True self.radio_view = False self.window_size = window_size self.box_over = False @@ -956,44 +850,6 @@ def __init__(self): # self.text_input_active = False self.center_blur_pixel = (0, 0, 0) -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) - class StarStore: """Functions for reading and setting play counts""" def __init__(self) -> None: @@ -1287,87 +1143,6 @@ def test(self, function): return False -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 @@ -1637,38 +1412,6 @@ def light_mode(self): # view_box.off_colour = self.grey(200) -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 - class TrackClass: """This is the fundamental object/data structure of a track""" @@ -1715,20 +1458,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)""" @@ -1743,35734 +1472,36144 @@ def __init__(self) -> None: self.play: bool = False self.force_scan: bool = 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" | "arrow" | "download" | "bubble" | "link": - 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 +class MOD(Structure): + """Access functions from libopenmpt for scanning tracker files""" + _fields_ = [("ctl", c_char_p), ("value", c_char_p)] -def pumper(): - if macos: - return - while pump: - time.sleep(0.005) - SDL_PumpEvents() +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 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 +class PlayerCtl: + """Main class that controls playback (play, pause, stepping, playlists, queue etc). Sends commands to backend.""" -def advance_theme() -> None: - global theme + # C-PC + def __init__(self): - theme += 1 - gui.reload_theme = True + self.running: bool = True + self.prefs: Prefs = prefs + self.install_directory: Path = install_directory -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 + # Database -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.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 -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.gen_codes = gen_codes - cf.update_value("plex-username", prefs.plex_username) - cf.update_value("plex-password", prefs.plex_password) - cf.update_value("plex-servername", prefs.plex_servername) + self.shuffle_pools = {} + self.after_import_flag = False + self.quick_add_target = 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) + self.album_mbid_release_cache = {} + self.album_mbid_release_group_cache = {} + self.mbid_image_url_cache = {} - 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) + # Misc player control - 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) + self.url: str = "" + # self.save_urls = url_saves + self.tag_meta: str = "" + self.found_tags = {} + self.encoder_pause = 0 - cf.update_value("display-language", prefs.ui_lang) - # cf.update_value("decode-search", prefs.diacritic_search) + # Playback - # 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) + 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 - # cf.update_value("gst-output", prefs.gst_output) - # cf.update_value("gst-use-custom-output", prefs.gst_use_custom_output) + self.record_stream = False + self.record_title = "" - cf.update_value("separate-multi-genre", prefs.sep_genre_multi) + # Bass - cf.update_value("tag-editor-name", prefs.tag_editor_name) - cf.update_value("tag-editor-target", prefs.tag_editor_target) + self.bass_devices = [] + self.set_device = 0 - 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) + self.gst_devices = [] # Display names + self.gst_outputs = {} # Display name : (sink, device) - 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) + #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 - 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.playing_time_int = 0 # playing time but with no decimel - 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) + self.windows_progress = None - 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.finish_transition = False + # self.queue_target = 0 + self.start_time_target = 0 - 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.decode_time = 0 + self.download_time = 0 - cf.update_value("force-subpixel-text", prefs.force_subpixel_text) + self.radio_meta_on = "" - 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) + self.radio_scrobble_trip = True + self.radio_scrobble_timer = Timer() - 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) + self.radio_image_bin = None + self.radio_rate_timer = Timer(2) + self.radio_poll_timer = Timer(2) - 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) + self.volume_update_timer = Timer() + self.wake_past_time = 0 - 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) + self.regen_in_progress = False + self.notify_in_progress = False - 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) + self.radio_playlists = radio_playlists + self.radio_playlist_viewing = radio_playlist_viewing + self.tag_history = {} - 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) + self.commit: int | None = None + self.spot_playing = False - cf.update_value("maloja-key", prefs.maloja_key) - cf.update_value("maloja-url", prefs.maloja_url) - cf.update_value("maloja-enable", prefs.maloja_enable) + self.buffering_percent = 0 - cf.update_value("tau-url", prefs.sat_url) + def notify_change(self) -> None: + self.db_inc += 1 + tauon.bg_save() - cf.update_value("lastfm-pull-love", prefs.lastfm_pull_love) + 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 + } - cf.update_value("broadcast-page-port", prefs.metadata_page_port) - cf.update_value("show-current-on-transition", prefs.show_current_on_transition) + 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) - 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 response.status_code == 200: + d = json.loads(response.text) + if "song" in d and "artist" in d["song"] and "title" in d["song"]: + self.tag_meta = d["song"]["artist"] + " - " + d["song"]["title"] - if config_directory.is_dir(): - cf.dump(str(config_directory / "tauon.conf")) - else: - logging.error("Missing config directory") + 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 -def load_prefs(): - cf.reset() - cf.load(str(config_directory / "tauon.conf")) + 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 = "" - 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") + 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"] - cf.br() - cf.add_text("[audio]") + 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() - # 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.") + if self.tag_meta: + radiobox.song_key = self.tag_meta + else: + radiobox.song_key = radiobox.dummy_track.artist + " - " + radiobox.dummy_track.title - 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)") + self.update_tag_history() + if radiobox.loaded_url not in radiobox.websocket_source_urls: + self.radio_image_bin = None + logging.info("NEXT RADIO TRACK") - prefs.pause_fade_time = max(prefs.pause_fade_time, 100) - prefs.pause_fade_time = min(prefs.pause_fade_time, 5000) + try: + get_radio_art() + except Exception: + logging.exception("Get art error") - 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.") + self.notify_update(mpris=False) + if self.mpris: + self.mpris.update(force=True) - 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") + lfm_scrobbler.listen_track(radiobox.dummy_track) + lfm_scrobbler.start_queue() - # 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.") + 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)) - # 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 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") - if prefs.dc_device_setting == "on": - prefs.dc_device = True - elif prefs.dc_device_setting == "off": - prefs.dc_device = False + 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 - 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 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") - 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 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") - 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 image_path is None: + image_path = "" - 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.") + image_path = image_path.replace("/", "\\") + #logging.info(image_path) - cf.br() - cf.add_text("[ui]") + 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)) - 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) + 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") - 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 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 - # 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 track_object.file_ext == "JELY": + return jellyfin.resolve_stream(track_object.url_key) - 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 track_object.file_ext == "KOEL": + return koel.resolve_stream(track_object.url_key) - 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") + if track_object.file_ext == "SUB": + return subsonic.resolve_stream(track_object.url_key) - 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 track_object.file_ext == "TAU": + return tau.resolve_stream(track_object.url_key), None - # 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) + return None, None - 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) + def playing_playlist(self) -> list[int] | None: + return self.multi_playlist[self.active_playlist_playing].playlist_ids - # prefs.force_subpixel_text = cf.sync_add("bool", "force-subpixel-text", prefs.force_subpixel_text, "(Subpixel rendering defaults to off with Flatpak)") + def playing_ready(self) -> bool: + return len(self.track_queue) > 0 - 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 selected_ready(self) -> bool: + return default_playlist and self.selected_in_playlist < len(default_playlist) - 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 render_playlist(self) -> None: + if taskbar_progress and msys and self.windows_progress: + self.windows_progress.update(True) + gui.pl_update = 1 - 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") + def show_selected(self) -> int: + if gui.playlist_view_length < 1: + return 0 - 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") + for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): - 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 i == self.selected_in_playlist: - 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) + 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 - cf.br() - cf.add_text("[tauon_satellite]") - prefs.sat_url = cf.sync_add("string", "tau-url", prefs.sat_url, "Exclude the port") + self.render_playlist() - 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") + return 0 + def get_track(self, track_index: int) -> TrackClass: + """Get track object by track_index""" + return self.master_library[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) + 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 - 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.") + def show_object(self) -> None: + """The track to show in the metadata side panel""" + target_track = None - 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 self.playing_state == 3: + return radiobox.dummy_track - 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 3 > self.playing_state > 0: + target_track = self.playing_object() - 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("/") + 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]) - 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.") + elif self.playing_state == 0 and prefs.meta_persists_stop: + target_track = self.master_library[self.track_queue[self.queue_step]] - 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") + 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]) - 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") + return target_track -def auto_scale() -> None: + def playing_object(self) -> TrackClass | None: - old = prefs.scale_want + if self.playing_state == 3: + return radiobox.dummy_track - 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") + if len(self.track_queue) > 0: + return self.master_library[self.track_queue[self.queue_step]] + return None - prefs.scale_want = round(round(prefs.scale_want / 0.05) * 0.05, 2) + def title_text(self) -> str: + line = "" + track = self.playing_object() + if track: + title = track.title + artist = track.artist - 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 + if not title: + line = clean_string(track.filename) + else: + if artist != "": + line += artist + if title != "": + if line != "": + line += " - " + line += title - if old != prefs.scale_want: - logging.info(f"Using UI scale: {prefs.scale_want}") + if self.playing_state == 3 and not title and not artist: + return self.tag_meta - if prefs.scale_want < 0.5: - prefs.scale_want = 1.0 + return line - 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 + def show(self) -> int | None: + global shift_selection -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 not self.track_queue: + return 0 + return None - if scale_want != prefs.ui_scale or force: + 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 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 + # 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") - if scaled_asset_directory != asset_directory: - logging.info("Rendering icons...") - render_icons(str(svg_directory), str(scaled_asset_directory), scale_want) + global shift_selection - logging.info("Done rendering icons") + if tauon.spot_ctl.coasting: + sptr = tauon.dummy_track.misc.get("spotify-track-url") + if sptr: - diff_ratio = scale_want / prefs.ui_scale - prefs.ui_scale = scale_want - prefs.playlist_row_height = round(22 * prefs.ui_scale) + 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 - # Save user values - column_backup = gui.pl_st - rspw = gui.pref_rspw - grspw = gui.pref_gallery_w + if not self.track_queue: + return 0 - gui.destroy_textures() - gui.rescale() + track_index = self.track_queue[self.queue_step] + if index is not None: + track_index = index - # 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) + # 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) -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 gui.playlist_view_length < 1: + return 0 -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 + 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: -class MOD(Structure): - """Access functions from libopenmpt for scanning tracker files""" - _fields_ = [("ctl", c_char_p), ("value", c_char_p)] + 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 -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), - ] + if select: + self.selected_in_playlist = i -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 playing: + # Make the found track the playing track + self.playlist_playing_position = i + self.active_playlist_playing = self.active_playlist_viewing - tag = tags + 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 - 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") + if not ( + quiet and self.playing_object().length < 15): # or (abs(self.playlist_view_position - i) < vl - 1)): - process_odat(nt, natural_get(tags, None, "TDOR", None)) + # Align to album if in view range (and folder titles are active) + ap = get_album_info(i)[1][0] - frames = tag.getall("POPM") - rating = 0 - if frames: - for frame in frames: - if frame.rating: - rating = frame.rating - nt.misc["POPM"] = frame.rating + 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(nt.comment) > 4 and nt.comment[2] == "+": - nt.comment = "" - if nt.comment[0:3] == "000": - nt.comment = "" + # Move to a random offset --- - 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 = "" + elif i == self.playlist_view_position - 1 and self.playlist_view_position > 1: + self.playlist_view_position -= 1 - 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) + # 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 - 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) + # 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)) - 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 + # 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) - 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 + break - tx = tags.getall("UFID") - if tx: - for item in tx: - if item.owner == "http://musicbrainz.org": - nt.misc["musicbrainz_recordingid"] = item.data.decode() + 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 - tx = tags.getall("TSOP") - if tx: - nt.misc["artist_sort"] = tx[0].text[0] + self.playlist_view_position = max(self.playlist_view_position, 0) - 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 + # if self.playlist_view_position > len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 1: + # logging.info("Run Over") - 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) + if select: + shift_selection = [] - if item.desc == "FMPS_RATING": - nt.misc["FMPS_Rating"] = float(item.text[0]) + self.render_playlist() -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 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) -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 + if prefs.left_panel_mode == "artist list" and gui.lsp and not quiet: + artist_list_box.locate_artist(self.playing_object()) - nt.misc.clear() + 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()) - nt.file_ext = os.path.splitext(os.path.basename(nt.fullpath))[1][1:].upper() + return 0 - if nt.file_ext.lower() in GME_Formats and gme: - 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) + 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 - filepath = nt.fullpath # this is the full file path - filename = nt.filename # this is the name of the file + self.set_volume() - # Get the directory of the file - dir_path = os.path.dirname(filepath) + def set_volume(self, notify: bool = True) -> None: - # 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: - 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.queue_step == 0: + return - 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) is mutagen.wave._WaveID3: - use_id3(audio.tags, nt) + 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 - elif nt.file_ext in ("OPUS", "OGG", "OGA"): + if not len(self.track_queue) > self.queue_step >= 0: + logging.error("There is no previous track?") + return - #logging.info("get opus") - with Opus(nt.fullpath) as audio: - audio.read() + 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 - #logging.info(audio.title) + if tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() - 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.show_current() + self.render_playlist() - 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) + 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 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", "")) + if id not in self.shuffle_pools: + self.update_shuffle_pool(pl.uuid_int) - with Ape(nt.fullpath) as audio: - audio.read() + 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] - # logging.info(audio.title) + if track_id in pool: + pool.remove(track_id) - # 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 - elif nt.file_ext in ("WV", "TTA"): + def play_target_rr(self) -> None: + tauon.thread_manager.ready_playback() + self.playing_length = self.master_library[self.track_queue[self.queue_step]].length - with Ape(nt.fullpath) as audio: - audio.read() + 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 - # logging.info(audio.title) + 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 - 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 tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() - else: - # Use MUTAGEN - try: - if nt.file_ext.lower() in VID_Formats: - scan_ffprobe(nt) - return nt + if update_title: + update_title_do() - try: - audio = mutagen.File(nt.fullpath) - except Exception: - logging.exception("Mutagen scan failed, falling back to FFPROBE") - scan_ffprobe(nt) - return nt + self.deduct_shuffle(self.target_object.index) - nt.samplerate = audio.info.sample_rate - nt.bitrate = audio.info.bitrate // 1000 - nt.length = audio.info.length - nt.size = os.path.getsize(nt.fullpath) + def play_target(self, gapless: bool = False, jump: bool = False) -> None: - 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") + tauon.thread_manager.ready_playback() - if type(audio.tags) == mutagen.mp4.MP4Tags: - tags = audio.tags + #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 - def in_get(key, tags): - if key in tags: - return tags[key][0] - return "" + if tauon.stream_proxy and tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() - 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]) + 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 - 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]) + self.playerCommand = "open" + if jump: # and not prefs.use_jump_crossfade: + self.playerSubCommand = "now" - 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")] + self.playerCommandReady = True + self.playing_state = 1 + self.update_change() + self.deduct_shuffle(target.index) - elif type(audio.tags) == mutagen.id3.ID3: - use_id3(audio.tags, nt) + def update_change(self) -> None: + if update_title: + update_title_do() + self.notify_update() + hit_discord() + self.render_playlist() + if lfm_scrobbler.a_sc: + lfm_scrobbler.a_sc = False + self.a_time = 0 - except Exception: - logging.exception("Failed loading file through Mutagen") - raise + lfm_scrobbler.start_queue() + if (album_mode or not gui.rsp) and (gui.theme_name == "Carbon" or prefs.colour_from_image): + target = self.playing_object() + if target and prefs.colour_from_image and target.parent_folder_path == colours.last_album: + return - # 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) - 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 - # This check won't guarantee that all codepaths above are checked as some return early, but it's better than nothing - # And importantly it does catch openmpt which can actually return such - if math.isinf(nt.length) or math.isnan(nt.length): - logging.error(f"Infinite/NaN found(autocorrected to 0) when scanning tags in file: {vars(nt)}!") - nt.length = 0 - return nt - -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() + album_art_gen.display(target, (0, 0), (50, 50), theme_only=True) - elif "gensokyoradio.net" in radiobox.loaded_url: + def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: + lfm_scrobbler.start_queue() + self.auto_stop = False - response = requests.get("https://gensokyoradio.net/api/station/playing/", timeout=10) + 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 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", "") + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] - 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() + 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 - 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) + global playlist_hold + gui.update_spec = 0 + self.active_playlist_playing = self.active_playlist_viewing + self.track_queue.append(index) + self.queue_step = len(self.track_queue) - 1 + playlist_hold = False + self.play_target(jump=jump) - if "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() + if pl_position is not None: + self.playlist_playing_position = pl_position - # Failure - elif pctl.radio_image_bin: - pctl.radio_image_bin.close() - pctl.radio_image_bin = None + gui.pl_update = 1 - gui.clear_image_cache_next += 1 + def back(self) -> None: + if self.playing_state < 3 and prefs.back_restarts and self.playing_time > 6: + self.seek_time(0) + self.render_playlist() + return -class PlayerCtl: - """Main class that controls playback (play, pause, stepping, playlists, queue etc). Sends commands to backend.""" + 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 - # C-PC - def __init__(self): + if len(self.track_queue) > 0: + self.left_time = self.playing_time + self.left_index = self.track_queue[self.queue_step] - self.running: bool = True - self.prefs: Prefs = prefs - self.install_directory: Path = install_directory + gui.update_spec = 0 + # Move up + if self.random_mode is False and len(self.playing_playlist()) > self.playlist_playing_position > 0: - # Database + if len(self.track_queue) > 0 and self.playing_playlist()[self.playlist_playing_position] != \ + self.track_queue[ + self.queue_step]: - 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 + try: + p = self.playing_playlist().index(self.track_queue[self.queue_step]) + except Exception: + logging.exception("Failed to change playing_playlist") + p = random.randrange(len(self.playing_playlist())) + if p is not None: + self.playlist_playing_position = p - self.gen_codes = gen_codes + 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) - self.shuffle_pools = {} - self.after_import_flag = False - self.quick_add_target = None + 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() - self.album_mbid_release_cache = {} - self.album_mbid_release_group_cache = {} - self.mbid_image_url_cache = {} + if self.active_playlist_viewing == self.active_playlist_playing: + self.show_current(False, True) - # Misc player control + if album_mode: + goto_album(self.playlist_playing_position) + if gui.combo_mode and self.active_playlist_viewing == self.active_playlist_playing: + self.show_current() - self.url: str = "" - # self.save_urls = url_saves - self.tag_meta: str = "" - self.found_tags = {} - self.encoder_pause = 0 + self.render_playlist() + self.notify_update() + notify_song() + lfm_scrobbler.start_queue() + gui.pl_update += 1 - # Playback + def stop(self, block: bool = False, run : bool = False) -> None: - 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.playerCommand = "stop" + if run: + self.playerCommand = "runstop" + if block: + self.playerSubCommand = "return" - self.record_stream = False - self.record_title = "" + self.playerCommandReady = True - # Bass + if tauon.thread_manager.player_lock.locked(): + try: + tauon.thread_manager.player_lock.release() + except RuntimeError as e: + if str(e) == "release unlocked lock": + logging.error("RuntimeError: Attempted to release already unlocked player_lock") + else: + logging.exception("Unknown RuntimeError trying to release player_lock") + except Exception: + logging.exception("Unknown exception trying to release player_lock") - self.bass_devices = [] - self.set_device = 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.gst_devices = [] # Display names - self.gst_outputs = {} # Display name : (sink, device) + 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 - #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 + if tauon.stream_proxy and tauon.stream_proxy.download_running: + tauon.stream_proxy.stop() - self.playing_time_int = 0 # playing time but with no decimel + 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) - self.windows_progress = None + if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: + logging.info("Spotify stop") + tauon.spot_ctl.control("stop") - self.finish_transition = False - # self.queue_target = 0 - self.start_time_target = 0 + self.notify_update() + lfm_scrobbler.start_queue() + return previous_state - self.decode_time = 0 - self.download_time = 0 + def pause(self) -> None: - self.radio_meta_on = "" + 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 - self.radio_scrobble_trip = True - self.radio_scrobble_timer = Timer() + if self.playing_state == 3: + if tauon.spot_ctl.coasting: + if tauon.spot_ctl.paused: + tauon.spot_ctl.control("resume") + else: + tauon.spot_ctl.control("pause") + return - self.radio_image_bin = None - self.radio_rate_timer = Timer(2) - self.radio_poll_timer = Timer(2) + 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 - self.volume_update_timer = Timer() - self.wake_past_time = 0 + 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.regen_in_progress = False - self.notify_in_progress = False + self.playerCommandReady = True - self.radio_playlists = radio_playlists - self.radio_playlist_viewing = radio_playlist_viewing - self.tag_history = {} + self.render_playlist() + self.notify_update() - self.commit: int | None = None - self.spot_playing = False + def pause_only(self) -> None: + if self.playing_state == 1: + self.playerCommand = "pauseon" + self.playing_state = 2 - self.buffering_percent = 0 + self.playerCommandReady = True + self.render_playlist() + self.notify_update() - def notify_change(self) -> None: - self.db_inc += 1 - tauon.bg_save() + def play_pause(self) -> None: + if self.playing_state == 3: + self.stop() + elif self.playing_state > 0: + self.pause() + else: + self.play() - def update_tag_history(self) -> None: - if prefs.auto_rec: - self.tag_history[radiobox.song_key] = { - "title": radiobox.dummy_track.title, - "artist": radiobox.dummy_track.artist, - "album": radiobox.dummy_track.album, - # "image": self.radio_image_bin - } + def 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 - def radio_progress(self) -> None: - if radiobox.loaded_url and "radio.plaza.one" in radiobox.loaded_url and self.radio_poll_timer.get() > 0: - self.radio_poll_timer.force_set(-10) - response = requests.get("https://api.plaza.one/status", timeout=10) + if msys and taskbar_progress and self.windows_progress: + self.windows_progress.update(True) - if response.status_code == 200: - d = json.loads(response.text) - if "song" in d and "artist" in d["song"] and "title" in d["song"]: - self.tag_meta = d["song"]["artist"] + " - " + d["song"]["title"] + if self.mpris is not None: + self.mpris.seek_do(self.playing_time) - 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 + 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): - 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 = "" + if new > self.playing_length - 0.5: + self.advance() + return - 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"] + if new < 0.4: + new = 0 - 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() + self.new_time = new + self.playing_time = new - if self.tag_meta: - radiobox.song_key = self.tag_meta - else: - radiobox.song_key = radiobox.dummy_track.artist + " - " + radiobox.dummy_track.title + self.playerCommand = "seek" + self.playerCommandReady = 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.mpris is not None: + self.mpris.seek_do(self.playing_time) - try: - get_radio_art() - except Exception: - logging.exception("Get art error") + def play(self) -> None: - self.notify_update(mpris=False) - if self.mpris: - self.mpris.update(force=True) + if tauon.spot_ctl.playing: + if self.playing_state == 2: + self.play_pause() + return - lfm_scrobbler.listen_track(radiobox.dummy_track) - lfm_scrobbler.start_queue() + # Unpause if paused + if self.playing_state == 2: + self.playerCommand = "pauseoff" + self.playerCommandReady = True + self.playing_state = 1 + self.notify_update() - 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 stopped + elif self.playing_state == 0: - 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") + if radiobox.loaded_station: + radiobox.start(radiobox.loaded_station) + return - 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 + # 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 - 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") + self.play_target() - 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 the queue is not empty, play? + elif len(self.track_queue) > 0: + self.play_target() - if image_path is None: - image_path = "" + self.render_playlist() - image_path = image_path.replace("/", "\\") - #logging.info(image_path) + 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() - 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)) + elif self.playing_state == 3 and tauon.spot_ctl.coasting: + th = 7 + if self.playing_time > self.playing_length or self.playing_time < 2.5: + th = 1 + if tauon.spot_ctl.update_timer.get() < th: + if not tauon.spot_ctl.paused: + self.playing_time += tauon.spot_ctl.progress_timer.get() + self.decode_time = self.playing_time + tauon.spot_ctl.progress_timer.set() + else: + tauon.spot_ctl.update_timer.set() + tauon.spot_ctl.update() - if self.mpris is not None and mpris is True: - while self.notify_in_progress: - time.sleep(0.01) - self.notify_in_progress = True - shoot = threading.Thread(target=self.notify_update_fire) - shoot.daemon = True - shoot.start() - if prefs.art_bg or (gui.mode == 3 and prefs.mini_mode_mode == 5): - tauon.thread_manager.ready("style") + def 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 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 + 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 track_object.file_ext == "JELY": - return jellyfin.resolve_stream(track_object.url_key) + # 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 track_object.file_ext == "KOEL": - return koel.resolve_stream(track_object.url_key) + gap_extra = 2 # 2 - if track_object.file_ext == "SUB": - return subsonic.resolve_stream(track_object.url_key) + if tauon.spot_ctl.playing or tauon.chrome_mode: + gap_extra = 3 - if track_object.file_ext == "TAU": - return tau.resolve_stream(track_object.url_key), None + if msys and taskbar_progress and self.windows_progress: + self.windows_progress.update(True) - return None, None + if self.commit is not None: + return - def playing_playlist(self) -> list[int] | None: - return self.multi_playlist[self.active_playlist_playing].playlist_ids + 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 - def playing_ready(self) -> bool: - return len(self.track_queue) > 0 + if self.playing_state == 1 and self.decode_time + gap_extra >= self.playing_length and self.decode_time > 0.2: - def selected_ready(self) -> bool: - return default_playlist and self.selected_in_playlist < len(default_playlist) + # Allow some time for spotify playing time to update? + if tauon.spot_ctl.playing and tauon.spot_ctl.start_timer.get() < 3: + return - def render_playlist(self) -> None: - if taskbar_progress and msys and self.windows_progress: - self.windows_progress.update(True) - gui.pl_update = 1 + # Allow some time for backend to provide a length + if self.playing_time < 6 and self.playing_length == 0: + return + if not tauon.spot_ctl.playing and self.a_time < 2: + return - def show_selected(self) -> int: - if gui.playlist_view_length < 1: - return 0 + self.decode_time = 0 - global shift_selection + pp = self.playing_playlist() - for i in range(len(self.multi_playlist[self.active_playlist_viewing].playlist_ids)): + 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 - if i == self.selected_in_playlist: + 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) - if i < self.playlist_view_position: - self.playlist_view_position = i - random.randint(2, int((gui.playlist_view_length / 3) * 2) + int(gui.playlist_view_length / 6)) - logging.debug("Position changed show selected (a)") - elif abs(self.playlist_view_position - i) > gui.playlist_view_length: - self.playlist_view_position = i - logging.debug("Position changed show selected (b)") - if i > 6: - self.playlist_view_position -= 5 - logging.debug("Position changed show selected (c)") - if i > gui.playlist_view_length * 1 and i + (gui.playlist_view_length * 2) < len( - self.multi_playlist[self.active_playlist_viewing].playlist_ids) and i > 10: - self.playlist_view_position = i - random.randint(2, int(gui.playlist_view_length / 3) * 2) - logging.debug("Position changed show selected (d)") - break - self.render_playlist() - return 0 + elif self.repeat_mode is True: - def get_track(self, track_index: int) -> TrackClass: - """Get track object by track_index""" - return self.master_library[track_index] + if self.album_repeat_mode: - 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 self.playlist_playing_position > len(pp) - 1: + self.playlist_playing_position = 0 # Hack fix, race condition bug? - def show_object(self) -> None: - """The track to show in the metadata side panel""" - target_track = None + ti = self.get_track(pp[self.playlist_playing_position]) - if self.playing_state == 3: - return radiobox.dummy_track + i = self.playlist_playing_position - if 3 > self.playing_state > 0: - target_track = self.playing_object() + # 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 - 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]) + # 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) - elif self.playing_state == 0 and prefs.meta_persists_stop: - target_track = self.master_library[self.track_queue[self.queue_step]] + self.selected_in_playlist = i + shift_selection = [i] - 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]) + self.jump(pp[i], i, jump=False) - return target_track + 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): - def playing_object(self) -> TrackClass | None: + logging.info("Repeat follow cursor") - if self.playing_state == 3: - return radiobox.dummy_track + self.playing_time = 0 + self.decode_time = 0 + self.active_playlist_playing = self.active_playlist_viewing + self.playlist_playing_position = self.selected_in_playlist - if len(self.track_queue) > 0: - return self.master_library[self.track_queue[self.queue_step]] - return None + 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() - def title_text(self) -> str: - line = "" - track = self.playing_object() - if track: - title = track.title - artist = track.artist + 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 not title: - line = clean_string(track.filename) - else: - if artist != "": - line += artist - if title != "": - if line != "": - line += " - " - line += title + #self.render_playlist() + lfm_scrobbler.start_queue() - if self.playing_state == 3 and not title and not artist: - return self.tag_meta + # Reload lastfm for rescrobble + if lfm_scrobbler.a_sc: + lfm_scrobbler.a_sc = False + self.a_time = 0 - return line + 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 show(self) -> int | None: - global shift_selection + # not (self.force_queue and not self.pause_queue) and \ - if not self.track_queue: - return 0 - return None + # We can shave it closer + if not self.playing_time + 0.1 >= self.playing_length: + return - def show_current( - self, select: bool = True, playing: bool = True, quiet: bool = False, this_only: bool = False, highlight: bool = False, - index: int | None = None, no_switch: bool = False, folder_list: bool = True, - ) -> int | None: + logging.info("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() - # 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") + gui.update += 1 + gui.pl_update = 1 - global shift_selection + if update_title: + update_title_do() + self.notify_update() + else: + # self.advance(quiet=True, end=True) - if tauon.spot_ctl.coasting: - sptr = tauon.dummy_track.misc.get("spotify-track-url") - if sptr: + 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 - 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 + self.advance(quiet=True, end=True) + self.playing_time = 0 + self.decode_time = 0 - if not self.track_queue: - return 0 + def start_commit(self, commit_id: int, repeat: bool = False) -> None: + self.commit = commit_id + target = self.get_track(commit_id) + self.target_open = target.fullpath + self.target_object = target + self.start_time = target.start_time + self.start_time_target = self.start_time + self.playerCommand = "open" + if repeat: + self.playerSubCommand = "repeat" + self.playerCommandReady = True - track_index = self.track_queue[self.queue_step] - if index is not None: - track_index = index + 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 - # 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) + # Temporary Workaround for UI block causing unwanted dragging + if not dry: + quick_d_timer.set() - if gui.playlist_view_length < 1: - return 0 + if prefs.show_current_on_transition: + quiet = False - 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: + # Trim the history if it gets too long + while len(self.track_queue) > 250: + self.queue_step -= 1 + del self.track_queue[0] - 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 + # 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] - if select: - self.selected_in_playlist = i + # Test to register skip (not currently used for anything) + if not dry and self.playing_state == 1 and 1 < self.left_time < 45: + self.master_library[self.left_index].skips += 1 + #logging.info('skip registered') - if playing: - # Make the found track the playing track - self.playlist_playing_position = i - self.active_playlist_playing = self.active_playlist_viewing + if not dry: + self.playing_time = 0 + self.decode_time = 0 + self.playing_length = 100 + gui.update_spec = 0 - 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 + old = self.queue_step + end_of_playlist = False - if not ( - quiet and self.playing_object().length < 15): # or (abs(self.playlist_view_position - i) < vl - 1)): + # Force queue (middle click on track) + if len(self.force_queue) > 0 and not self.pause_queue: - # Align to album if in view range (and folder titles are active) - ap = get_album_info(i)[1][0] + q = self.force_queue[0] + target_index = q.track_id - if not (quiet and self.playlist_view_position <= i <= self.playlist_view_position + vl) and ( - not abs(i - ap) > vl - 2) and not self.multi_playlist[self.active_playlist_viewing].hide_title: - self.playlist_view_position = ap + if q.type == 1: + # This is an album type - # Move to a random offset --- + 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) - elif i == self.playlist_view_position - 1 and self.playlist_view_position > 1: - self.playlist_view_position -= 1 + if not dry: - # 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 + pl = id_to_pl(q.playlist_id) + if pl is not None: + self.active_playlist_playing = pl - # We know its out of range if above view postion - elif i < self.playlist_view_position: - self.playlist_view_position = i - random.randint(2, int(( - gui.playlist_view_length / 3) * 2) + int(gui.playlist_view_length / 6)) + if target_index not in self.playing_playlist(): + del self.force_queue[0] + self.advance() + return None - # 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 dry: + return target_index - break + 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) - 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 + # Set the flag that we have entered the album + self.force_queue[0].album_stage = 1 - self.playlist_view_position = max(self.playlist_view_position, 0) + # This code is mirrored below ------- + ok_continue = True - # if self.playlist_view_position > len(self.multi_playlist[self.active_playlist_viewing].playlist_ids) - 1: - # logging.info("Run Over") + # 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 select: - shift_selection = [] + # 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.render_playlist() + # ----------- - if album_mode and not quiet: - if highlight: - gui.gallery_animate_highlight_on = goto_album(self.selected_in_playlist) - gallery_select_animate_timer.set() - else: - goto_album(self.selected_in_playlist) - if prefs.left_panel_mode == "artist list" and gui.lsp and not quiet: - artist_list_box.locate_artist(self.playing_object()) + elif q.album_stage == 1: + # We have previously started playing this album - 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()) + # Check to see if we still are: + ok_continue = True - return 0 + if self.get_track(target_index).parent_folder_path != self.playing_object().parent_folder_path: + # Remember to set jumper check this too (leave album if we jump to some other track, i.e. double click)) + ok_continue = False - def 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 + pl = self.multi_playlist[self.active_playlist_playing].playlist_ids - self.set_volume() + # Check next song is in album + if ok_continue: - def set_volume(self, notify: bool = True) -> None: + # 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 (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 + if dry: + return None - self.volume_update_timer.set() - self.playerCommand = "volume" - self.playerCommandReady = True - if notify: - self.notify_update() + del self.force_queue[0] + self.advance() + return None - def revert(self) -> None: - if self.queue_step == 0: - return + # 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 - 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 ok_continue: + # We seem to be still in the album. Step down one and play + if not dry: + self.playlist_playing_position += 1 - if not len(self.track_queue) > self.queue_step >= 0: - logging.error("There is no previous track?") - return + 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 - 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 dry: + return pl[self.playlist_playing_position + 1] + self.track_queue.append(pl[self.playlist_playing_position]) + self.queue_step = len(self.track_queue) - 1 + # self.queue_target = len(self.track_queue) - 1 + if play: + self.play_target(jump=not end) - if tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + if not ok_continue: + # It seems this item has expired, remove it and call advance again - self.show_current() - self.render_playlist() + if dry: + return None - 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 + logging.info("Remove expired album from queue") + del self.force_queue[0] - if id not in self.shuffle_pools: - self.update_shuffle_pool(pl.uuid_int) + if q.auto_stop: + self.auto_stop = True + if prefs.stop_end_queue and not self.force_queue: + self.auto_stop = True - 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 queue_box.scroll_position > 0: + queue_box.scroll_position -= 1 - if track_id in pool: - pool.remove(track_id) + # self.advance() + # return + else: + # This is track type + pl = id_to_pl(q.playlist_id) + if not dry and pl is not None: + self.active_playlist_playing = pl - 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 target_index not in self.playing_playlist(): + if dry: + return None + del self.force_queue[0] + self.advance() + return None - if self.playing_length > 2: - random_start = random.randrange(1, int(self.playing_length) - 45 if self.playing_length > 50 else int( - self.playing_length)) - else: - random_start = 0 + if dry: + return target_index - 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 + 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 - if tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + # Stop if playlist is empty + elif len(self.playing_playlist()) == 0: + if dry: + return None + self.stop() + return 0 - if update_title: - update_title_do() + # 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): - self.deduct_shuffle(self.target_object.index) + if dry: + return default_playlist[self.selected_in_playlist] - def play_target(self, gapless: bool = False, jump: bool = False) -> None: + self.active_playlist_playing = self.active_playlist_viewing + self.playlist_playing_position = self.selected_in_playlist - tauon.thread_manager.ready_playback() + 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) - #logging.info(self.track_queue) - self.playing_time = 0 - self.decode_time = 0 - target = self.master_library[self.track_queue[self.queue_step]] - self.target_open = target.fullpath - self.target_object = target - self.start_time = target.start_time - self.start_time_target = self.start_time - self.playing_length = target.length - self.last_playing_time = 0 - self.commit = None - radiobox.loaded_station = None + # If random, jump to random track + elif (self.random_mode or rr) and len(self.playing_playlist()) > 0 and not ( + self.album_shuffle_mode or prefs.album_shuffle_lock_mode): + # self.queue_step += 1 + new_step = self.queue_step + 1 - if tauon.stream_proxy and tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + if new_step == len(self.track_queue): - 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 + 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.playerCommand = "open" - if jump: # and not prefs.use_jump_crossfade: - self.playerSubCommand = "now" + if ti.index not in pp: + if dry: + return None + logging.info("No tracks to repeat!") + return 0 - self.playerCommandReady = True + matches = [] + for i, p in enumerate(pp): - self.playing_state = 1 - self.update_change() - self.deduct_shuffle(target.index) + if self.get_track(p).parent_folder_path == ti.parent_folder_path: + matches.append((i, p)) - def update_change(self) -> None: - if update_title: - update_title_do() - self.notify_update() - hit_discord() - self.render_playlist() + if matches: + # Avoid a repeat of same track + if len(matches) > 1 and (k, ti.index) in matches: + matches.remove((k, ti.index)) - if lfm_scrobbler.a_sc: - lfm_scrobbler.a_sc = False - self.a_time = 0 + i, p = random.choice(matches) # not used - lfm_scrobbler.start_queue() + if prefs.true_shuffle: - 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 + id = ti.parent_folder_path - album_art_gen.display(target, (0, 0), (50, 50), theme_only=True) + while True: + if id in self.shuffle_pools: - def jump(self, index: int, pl_position: int = None, jump: bool = True) -> None: - lfm_scrobbler.start_queue() - self.auto_stop = False + pool = self.shuffle_pools[id] - 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 pool: + del self.shuffle_pools[id] # Trigger a refill + continue - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + ref = pool.pop() + if dry: + pool.append(ref) + return ref[1] + # ref = random.choice(pool) + # pool.remove(ref) - if self.playing_state == 1 and self.left_time > 5 and self.playing_length - self.left_time > 15: - self.master_library[self.left_index].skips += 1 + if ref[1] not in pp: # Check track still in the live playlist + logging.info("Track not in pool") + continue - 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) + i, p = ref # Find position of reference in playlist + break - if pl_position is not None: - self.playlist_playing_position = pl_position + # Refill the pool + random.shuffle(matches) + self.shuffle_pools[id] = matches + logging.info("Refill folder shuffle pool") - gui.pl_update = 1 + self.playlist_playing_position = i + self.track_queue.append(p) - def back(self) -> None: - if self.playing_state < 3 and prefs.back_restarts and self.playing_time > 6: - self.seek_time(0) - self.render_playlist() - return + else: + # Normal select from playlist - if tauon.spot_ctl.coasting: - tauon.spot_ctl.control("previous") - tauon.spot_ctl.update_timer.set() - self.playing_time = -2 - self.decode_time = -2 - return + if prefs.true_shuffle: + # True shuffle avoids repeats by using a pool - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] + pl = self.multi_playlist[self.active_playlist_playing] + id = pl.uuid_int - gui.update_spec = 0 - # Move up - if self.random_mode is False and len(self.playing_playlist()) > self.playlist_playing_position > 0: + while True: - if len(self.track_queue) > 0 and self.playing_playlist()[self.playlist_playing_position] != \ - self.track_queue[ - self.queue_step]: + if id in self.shuffle_pools: - 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 + pool = self.shuffle_pools[id] - 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) + if not pool: + del self.shuffle_pools[id] # Trigger a refill + continue - 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() + ref = pool.pop() + if dry: + pool.append(ref) + return ref + # ref = random.choice(pool) + # pool.remove(ref) - if self.active_playlist_viewing == self.active_playlist_playing: - self.show_current(False, True) + if ref not in pl.playlist_ids: # Check track still in the live playlist + continue - 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() + random_jump = pl.playlist_ids.index(ref) # Find position of reference in playlist + break - self.render_playlist() - self.notify_update() - notify_song() - lfm_scrobbler.start_queue() - gui.pl_update += 1 + # Refill the pool + self.update_shuffle_pool(pl.uuid_int) - def stop(self, block: bool = False, run : bool = False) -> None: + else: + random_jump = random.randrange(len(self.playing_playlist())) # not used - self.playerCommand = "stop" - if run: - self.playerCommand = "runstop" - if block: - self.playerSubCommand = "return" + self.playlist_playing_position = random_jump + self.track_queue.append(self.playing_playlist()[random_jump]) - self.playerCommandReady = True + 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 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 rr: + if dry: + return None + self.play_target_rr() + elif play: + self.play_target(jump=not end) - self.record_stream = False - if len(self.track_queue) > 0: - self.left_time = self.playing_time - self.left_index = self.track_queue[self.queue_step] - previous_state = self.playing_state - self.playing_time = 0 - self.decode_time = 0 - self.playing_state = 0 - self.render_playlist() - gui.update_spec = 0 - # gui.update_level = True # Allows visualiser to enter decay sequence - gui.update = True - if update_title: - update_title_do() # Update title bar text + # If not random mode, Step down 1 on the playlist + elif self.random_mode is False and len(self.playing_playlist()) > 0: - if tauon.stream_proxy and tauon.stream_proxy.download_running: - tauon.stream_proxy.stop() + # 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 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) + elif prefs.end_setting in ("advance", "cycle"): - if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: - logging.info("Spotify stop") - tauon.spot_ctl.control("stop") + # 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 - self.notify_update() - lfm_scrobbler.start_queue() - return previous_state + else: - def pause(self) -> None: + p = self.active_playlist_playing + for i in range(len(self.multi_playlist)): - 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 + k = (p + i + 1) % len(self.multi_playlist) - if self.playing_state == 3: - if tauon.spot_ctl.coasting: - if tauon.spot_ctl.paused: - tauon.spot_ctl.control("resume") - else: - tauon.spot_ctl.control("pause") - return + # Skip a playlist if empty + if not (self.multi_playlist[k].playlist_ids): + continue - 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 + # Skip a playlist if hidden + if self.multi_playlist[k].hidden and prefs.tabs_on_top: + continue - 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() + # 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 - self.playerCommandReady = True + else: + # Restart current if no other eligible playlist found + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) - self.render_playlist() - self.notify_update() + return None - def pause_only(self) -> None: - if self.playing_state == 1: - self.playerCommand = "pauseon" - self.playing_state = 2 + elif prefs.end_setting == "repeat": + self.playlist_playing_position = -1 + self.advance(end=end, force=True, play=play) + return None - self.playerCommandReady = True - self.render_playlist() - self.notify_update() + gui.update += 3 - def play_pause(self) -> None: - if self.playing_state == 3: - self.stop() - elif self.playing_state > 0: - self.pause() - else: - self.play() + else: + if self.playlist_playing_position > len(self.playing_playlist()) - 1: + if dry: + return None + self.playlist_playing_position = 0 - 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 + 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 msys and taskbar_progress and self.windows_progress: - self.windows_progress.update(True) + if len(self.playing_playlist()) == self.playlist_playing_position + 1: + return None - if self.mpris is not None: - self.mpris.seek_do(self.playing_time) + 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]) - 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): + # 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 new > self.playing_length - 0.5: - self.advance() - return + elif self.random_mode and (self.album_shuffle_mode or prefs.album_shuffle_lock_mode): - if new < 0.4: - new = 0 + # Album shuffle mode + logging.info("Album shuffle mode") - self.new_time = new - self.playing_time = new + po = self.playing_object() - self.playerCommand = "seek" - self.playerCommandReady = True + redraw = False - if self.mpris is not None: - self.mpris.seek_do(self.playing_time) + # Checks + if po is not None and len(self.playing_playlist()) > 0: - def play(self) -> None: + # 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 tauon.spot_ctl.playing: - if self.playing_state == 2: - self.play_pause() - return + 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) - # Unpause if paused - if self.playing_state == 2: - self.playerCommand = "pauseoff" - self.playerCommandReady = True - self.playing_state = 1 - self.notify_update() + else: - # If stopped - elif self.playing_state == 0: + 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) - if radiobox.loaded_station: - radiobox.start(radiobox.loaded_station) - return + random.shuffle(albums) - # 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 + 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() - self.play_target() + else: + logging.error("ADVANCE ERROR - NO CASE!") - # If the queue is not empty, play? - elif len(self.track_queue) > 0: - self.play_target() + if dry: + return None - self.render_playlist() + 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) - 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 album_mode: + # goto_album(self.playlist_playing) - 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() + self.render_playlist() - else: - tauon.spot_ctl.update_timer.set() - tauon.spot_ctl.update() + if tauon.spot_ctl.playing and end_of_playlist: + tauon.spot_ctl.control("stop") - 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] + self.notify_update() + lfm_scrobbler.start_queue() + if play: + notify_song(end_of_playlist, delay=1.3) + return None - 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 + def reset_missing_flags(self) -> None: + for value in self.master_library.values(): + value.found = True + gui.pl_update += 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 +class LastFMapi: + API_SECRET = "6e433964d3ff5e817b7724d16a9cf0cc" + connected = False + API_KEY = "bfdaf6357f1dddd494e5bee1afe38254" + scanning_username = "" - gap_extra = 2 # 2 + network = None + lastfm_network = None + tries = 0 - if tauon.spot_ctl.playing or tauon.chrome_mode: - gap_extra = 3 + scanning_friends = False + scanning_loves = False + scanning_scrobbles = False - if msys and taskbar_progress and self.windows_progress: - self.windows_progress.update(True) + def __init__(self) -> None: + self.sg = None + self.url = None - if self.commit is not None: - return + def get_network(self) -> LibreFMNetwork: + if prefs.use_libre_fm: + return pylast.LibreFMNetwork + return pylast.LastFMNetwork - 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 + 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.playing_state == 1 and self.decode_time + gap_extra >= self.playing_length and self.decode_time > 0.2: + if self.network is None: + self.no_user_connect() - # Allow some time for spotify playing time to update? - if tauon.spot_ctl.playing and tauon.spot_ctl.start_timer.get() < 3: - return + 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) - # Allow some time for backend to provide a length - if self.playing_time < 6 and self.playing_length == 0: - return - if not tauon.spot_ctl.playing and self.a_time < 2: - return + def auth2(self) -> None: - self.decode_time = 0 + # This is step 2 where the user clicks "Done" - pp = self.playing_playlist() + if self.sg is None: + show_message(_("You need to log in first")) + return - 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 + 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 - 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) + except Exception as e: + if "Unauthorized Token" in str(e): + logging.exception("Not authorized") + show_message(_("Error - Not authorized"), mode="error") + else: + logging.exception("Unknown error") + show_message(_("Error"), _("Unknown error."), mode="error") + if not toggle_lfm_auto(mode=1): + toggle_lfm_auto() + def auth3(self) -> None: + """This is used for 'logout'""" + prefs.last_fm_token = None + prefs.last_fm_username = "" + show_message(_("Logout will complete on app restart.")) - elif self.repeat_mode is True: + def connect(self, m_notify: bool = True) -> bool | None: - if self.album_repeat_mode: + if not last_fm_enable: + return False - if self.playlist_playing_position > len(pp) - 1: - self.playlist_playing_position = 0 # Hack fix, race condition bug? + if self.connected is True: + if m_notify: + show_message(_("Already connected to Last.fm")) + return True - ti = self.get_track(pp[self.playlist_playing_position]) + if prefs.last_fm_token is None: + show_message(_("No Last.Fm account registered"), _("Authorise an account in settings"), mode="info") + return None - i = self.playlist_playing_position + logging.info("Attempting to connect to Last.fm network") - # 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 + try: - # We need to backtrack to see where the folder begins - i -= 1 - while i >= 0: - nt = self.get_track(pp[i]) - if ti.parent_folder_path != nt.parent_folder_path: - i += 1 - break - i -= 1 - i = max(i, 0) + self.network = self.get_network()( + api_key=self.API_KEY, api_secret=self.API_SECRET, session_key=prefs.last_fm_token) # , username=lfm_username, password_hash=lfm_hash) - self.selected_in_playlist = i - shift_selection = [i] + self.connected = True + if m_notify: + show_message(_("Connection to Last.fm was successful."), mode="done") - self.jump(pp[i], i, jump=False) + logging.info("Connection to lastfm appears successful") + return True - 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): + 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 - logging.info("Repeat follow cursor") + def toggle(self) -> None: + prefs.scrobble_hold ^= True - self.playing_time = 0 - self.decode_time = 0 - self.active_playlist_playing = self.active_playlist_viewing - self.playlist_playing_position = self.selected_in_playlist + def details_ready(self) -> bool: + if prefs.last_fm_token: + return True + return False - 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() + 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 - 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 + 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.render_playlist() - lfm_scrobbler.start_queue() + 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 - # Reload lastfm for rescrobble - if lfm_scrobbler.a_sc: - lfm_scrobbler.a_sc = False - self.a_time = 0 + 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 - 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 get_all_scrobbles_estimate_time(self) -> float | None: - # not (self.force_queue and not self.pause_queue) and \ + if not self.connected: + self.connect(False) + if not self.connected or not prefs.last_fm_username: + return None - # We can shave it closer - if not self.playing_time + 0.1 >= self.playing_length: - return + user = pylast.User(prefs.last_fm_username, self.network) + total = user.get_playcount() - logging.info("Do transition CUE") - self.playlist_playing_position += 1 - self.queue_step += 1 - self.track_queue.append(pp[self.playlist_playing_position]) - self.playing_state = 1 - self.playing_time = 0 - self.decode_time = 0 - self.playing_length = self.master_library[self.track_queue[self.queue_step]].length - self.start_time = self.master_library[self.track_queue[self.queue_step]].start_time - self.start_time_target = self.start_time - lfm_scrobbler.start_queue() + if total: + return 0.04364 * total + return 0 - gui.update += 1 - gui.pl_update = 1 + def get_all_scrobbles(self) -> None: - if update_title: - update_title_do() - self.notify_update() - else: - # self.advance(quiet=True, end=True) + if not self.connected: + self.connect(False) + if not self.connected or not prefs.last_fm_username: + return - 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 + 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) - self.advance(quiet=True, end=True) - self.playing_time = 0 - self.decode_time = 0 + counts = {} - 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 + # Count up the unique pairs + for track in tracks: + key = (str(track.track.artist), str(track.track.title)) + c = counts.get(key, 0) + counts[key] = c + 1 - def 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 + touched = [] - # Temporary Workaround for UI block causing unwanted dragging - if not dry: - quick_d_timer.set() + # Add counts to matching tracks + for key, value in counts.items(): + artist, title = key + artist = artist.lower() + title = title.lower() - if prefs.show_current_on_transition: - quiet = False + 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 - # Trim the history if it gets too long - while len(self.track_queue) > 250: - self.queue_step -= 1 - del self.track_queue[0] + logging.info(perf_timer.get()) + gui.pl_update += 1 + self.scanning_scrobbles = False + tauon.bg_save() + show_message(_("Scanning scrobbles complete"), mode="done") - # 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 artist_info(self, artist: str): - # Test to register skip (not currently used for anything) - if not dry and self.playing_state == 1 and 1 < self.left_time < 45: - self.master_library[self.left_index].skips += 1 - #logging.info('skip registered') + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return False, "", "" - if not dry: - self.playing_time = 0 - self.decode_time = 0 - self.playing_length = 100 - gui.update_spec = 0 + 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() - old = self.queue_step - end_of_playlist = False + return True, bio, "", mbid, url + except Exception: + logging.exception("last.fm get artist info failed") - # Force queue (middle click on track) - if len(self.force_queue) > 0 and not self.pause_queue: + return False, "", "", "", "" - q = self.force_queue[0] - target_index = q.track_id + def artist_mbid(self, artist: str): - if q.type == 1: - # This is an album type + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return "" - 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) + 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") - if not dry: + return "" - pl = id_to_pl(q.playlist_id) - if pl is not None: - self.active_playlist_playing = pl + 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 - if target_index not in self.playing_playlist(): - del self.force_queue[0] - self.advance() - return None + 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 - if dry: - return target_index + remote_loved = track.get_userloved() - 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 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 - # Set the flag that we have entered the album - self.force_queue[0].album_stage = 1 + if remote_loved is None: + logging.error("Error getting loved status") + return - # This code is mirrored below ------- - ok_continue = True + local_loved = love(set=False, track_id=track_object.index, notify=False, sync=False) - # 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 remote_loved != local_loved: + love(set=True, track_id=track_object.index, notify=False, sync=False) + except Exception: + logging.exception("Failed to pull love") - # 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 + def scrobble(self, track_object: TrackClass, timestamp: float | None = None) -> bool: + if not last_fm_enable: + return True + if prefs.scrobble_hold: + return True + if prefs.auto_lfm: + self.connect(False) - # ----------- + if timestamp is None: + timestamp = int(time.time()) + # lastfm_user = self.network.get_user(self.username) - elif q.album_stage == 1: - # We have previously started playing this album + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) + album_artist = track_object.album_artist - # Check to see if we still are: - ok_continue = True + logging.info("Submitting scrobble...") - 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 + # 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') - pl = self.multi_playlist[self.active_playlist_playing].playlist_ids + # Pull loved status - # Check next song is in album - if ok_continue: + self.sync_pull_love(track_object) - # Check if we are at end of playlist, or already at end of album - if self.playlist_playing_position >= len(pl) - 1 or (self.playlist_playing_position < len( - pl) - 1 and \ - self.get_track(pl[self.playlist_playing_position + 1]).parent_folder_path != self.get_track( - target_index).parent_folder_path): - if dry: - return None + else: + logging.warning("Not sent, incomplete metadata") - del self.force_queue[0] - self.advance() - return None + except Exception as e: + logging.exception("Failed to Scrobble!") + if "retry" in str(e): + logging.warning("Retrying in a couple seconds...") + time.sleep(7) + try: + self.network.scrobble(artist=artist, title=title, timestamp=timestamp) + # logging.info('Scrobbled') + return True + except Exception: + logging.exception("Failed to retry!") - # 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 + # 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) - # if ok_continue: - # We seem to be still in the album. Step down one and play - if not dry: - self.playlist_playing_position += 1 + return False + return True - 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 + def get_bio(self, artist: str) -> str: - if dry: - return pl[self.playlist_playing_position + 1] - self.track_queue.append(pl[self.playlist_playing_position]) - self.queue_step = len(self.track_queue) - 1 - # self.queue_target = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) + if self.lastfm_network is None: + if self.last_fm_only_connect() is False: + return "" - if not ok_continue: - # It seems this item has expired, remove it and call advance again + 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 dry: - return None + def love(self, artist: str, title: str): - logging.info("Remove expired album from queue") - del self.force_queue[0] + 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() - if q.auto_stop: - self.auto_stop = True - if prefs.stop_end_queue and not self.force_queue: - self.auto_stop = True + 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() - if queue_box.scroll_position > 0: - queue_box.scroll_position -= 1 + def clear_friends_love(self) -> None: - # self.advance() - # return + count = 0 + for index, tr in pctl.master_library.items(): + count += len(tr.lfm_friend_likes) + tr.lfm_friend_likes.clear() - 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 + show_message(_("Removed {N} loves.").format(N=count)) - if target_index not in self.playing_playlist(): - if dry: - return None - del self.force_queue[0] - self.advance() - return None + def get_friends_love(self): + if not last_fm_enable: + return + self.scanning_friends = True - if dry: - return target_index + try: + username = prefs.last_fm_username + logging.info(f"Username is {username}") - 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 + if not username: + self.scanning_friends = False + show_message(_("There was an error, try re-log in")) + return - # Stop if playlist is empty - elif len(self.playing_playlist()) == 0: - if dry: - return None - self.stop() - return 0 + if self.network is None: + self.no_user_connect() - # 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): + 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 dry: - return default_playlist[self.selected_in_playlist] + try: + loves = friend.get_loved_tracks(limit=None) + except Exception: + logging.exception("Failed to get_loved_tracks!") - self.active_playlist_playing = self.active_playlist_viewing - self.playlist_playing_position = self.selected_in_playlist + for track in loves: + title = track.track.title.casefold() + artist = track.track.artist.name.casefold() + for index, tr in pctl.master_library.items(): - self.track_queue.append(default_playlist[self.selected_in_playlist]) - self.queue_step = len(self.track_queue) - 1 - if play: - self.play_target(jump=not end) + if 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) - # 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 + except Exception: + logging.exception("There was an error getting friends loves") + show_message(_("There was an error getting friends loves"), "", mode="warning") - if new_step == len(self.track_queue): + self.scanning_friends = False - 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]] + def dl_love(self) -> None: + if not last_fm_enable: + return + username = prefs.last_fm_username + show_message(_("Scanning loved tracks for: {username}").format(username=username), mode="info") + self.scanning_username = username - if ti.index not in pp: - if dry: - return None - logging.info("No tracks to repeat!") - return 0 + if not username: + show_message(_("No username found"), mode="error") + return - matches = [] - for i, p in enumerate(pp): + if len(username) > 25: + logging.error("Aborted due to long username") + return - if self.get_track(p).parent_folder_path == ti.parent_folder_path: - matches.append((i, p)) + self.scanning_loves = True - if matches: - # Avoid a repeat of same track - if len(matches) > 1 and (k, ti.index) in matches: - matches.remove((k, ti.index)) + logging.info("Connect for friend scan") - i, p = random.choice(matches) # not used + try: + if self.network is None: + self.no_user_connect() - if prefs.true_shuffle: + self.network.enable_rate_limit() + logging.info("Get user...") + lastfm_user = self.network.get_user(username) + tracks = lastfm_user.get_loved_tracks(limit=None) - id = ti.parent_folder_path + matches = 0 + updated = 0 - while True: - if id in self.shuffle_pools: + for track in tracks: + title = track.track.title.casefold() + artist = track.track.artist.name.casefold() - pool = self.shuffle_pools[id] + 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" - if not pool: - del self.shuffle_pools[id] # Trigger a refill - continue + star_store.insert(index, star) - ref = pool.pop() - if dry: - pool.append(ref) - return ref[1] - # ref = random.choice(pool) - # pool.remove(ref) + 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 - if ref[1] not in pp: # Check track still in the live playlist - logging.info("Track not in pool") - continue + 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 - i, p = ref # Find position of reference in playlist - break + # logging.info('Updating Now Playing') - # Refill the pool - random.shuffle(matches) - self.shuffle_pools[id] = matches - logging.info("Refill folder shuffle pool") + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) - self.playlist_playing_position = i - self.track_queue.append(p) + 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 - else: - # Normal select from playlist +class ListenBrainz: - if prefs.true_shuffle: - # True shuffle avoids repeats by using a pool + def __init__(self): - pl = self.multi_playlist[self.active_playlist_playing] - id = pl.uuid_int + self.enable = prefs.enable_lb + # self.url = "https://api.listenbrainz.org/1/submit-listens" - while True: + def url(self): + url = prefs.listenbrainz_url + if not url: + url = "https://api.listenbrainz.org/" + if not url.endswith("/"): + url += "/" + return url + "1/submit-listens" - if id in self.shuffle_pools: + def listen_full(self, track_object: TrackClass, time) -> bool: - pool = self.shuffle_pools[id] + if self.enable is False: + return True + if prefs.scrobble_hold is True: + return True + if prefs.lb_token is None: + show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") - if not pool: - del self.shuffle_pools[id] # Trigger a refill - continue + title = track_object.title + album = track_object.album + artist = get_artist_strip_feat(track_object) - ref = pool.pop() - if dry: - pool.append(ref) - return ref - # ref = random.choice(pool) - # pool.remove(ref) + if title == "" or artist == "": + return True - if ref not in pl.playlist_ids: # Check track still in the live playlist - continue + data = {"listen_type": "single", "payload": []} + metadata = {"track_name": title, "artist_name": artist} - random_jump = pl.playlist_ids.index(ref) # Find position of reference in playlist - break + additional = {} - # Refill the pool - self.update_shuffle_pool(pl.uuid_int) + # MusicBrainz Artist IDs + if "musicbrainz_artistids" in track_object.misc: + additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] - else: - random_jump = random.randrange(len(self.playing_playlist())) # not used + # MusicBrainz Release ID + if "musicbrainz_albumid" in track_object.misc: + additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] - self.playlist_playing_position = random_jump - self.track_queue.append(self.playing_playlist()[random_jump]) + # MusicBrainz Recording ID + if "musicbrainz_recordingid" in track_object.misc: + additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] - 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 + # MusicBrainz Track ID + if "musicbrainz_trackid" in track_object.misc: + additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] - if rr: - if dry: - return None - self.play_target_rr() - elif play: - self.play_target(jump=not end) + if additional: + metadata["additional_info"] = additional + # logging.info(additional) + data["payload"].append({"track_metadata": metadata}) + data["payload"][0]["listened_at"] = time - # If not random mode, Step down 1 on the playlist - elif self.random_mode is False and len(self.playing_playlist()) > 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 - # 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 + 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) - elif prefs.end_setting in ("advance", "cycle"): + if title == "" or artist == "": + return - # 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 + data = {"listen_type": "playing_now", "payload": []} + metadata = {"track_name": title, "artist_name": artist} - else: + additional = {} - p = self.active_playlist_playing - for i in range(len(self.multi_playlist)): + # MusicBrainz Artist IDs + if "musicbrainz_artistids" in track_object.misc: + additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] - k = (p + i + 1) % len(self.multi_playlist) + # MusicBrainz Release ID + if "musicbrainz_albumid" in track_object.misc: + additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] - # Skip a playlist if empty - if not (self.multi_playlist[k].playlist_ids): - continue + # MusicBrainz Recording ID + if "musicbrainz_recordingid" in track_object.misc: + additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] - # Skip a playlist if hidden - if self.multi_playlist[k].hidden and prefs.tabs_on_top: - continue + # MusicBrainz Track ID + if "musicbrainz_trackid" in track_object.misc: + additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] - # 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 track_object.track_number: + try: + additional["tracknumber"] = str(int(track_object.track_number)) + except Exception: + logging.exception("Error trying to get track_number") - else: - # Restart current if no other eligible playlist found - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) + if track_object.length: + additional["duration"] = str(int(track_object.length)) - return None + additional["media_player"] = t_title + additional["submission_client"] = t_title + additional["media_player_version"] = str(n_version) - elif prefs.end_setting == "repeat": - self.playlist_playing_position = -1 - self.advance(end=end, force=True, play=play) - return None + metadata["additional_info"] = additional + data["payload"].append({"track_metadata": metadata}) + # data["payload"][0]["listened_at"] = int(time.time()) - gui.update += 3 + 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()) - else: - if self.playlist_playing_position > len(self.playing_playlist()) - 1: - if dry: - return None - self.playlist_playing_position = 0 + def paste_key(self): - 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") + text = copy_from_clipboard() + if text == "": + show_message(_("There is no text in the clipboard"), mode="error") + return - if len(self.playing_playlist()) == self.playlist_playing_position + 1: - return None + if prefs.listenbrainz_url: + prefs.lb_token = text + return - if dry: - return self.playing_playlist()[self.playlist_playing_position + 1] - self.playlist_playing_position += 1 - self.track_queue.append(self.playing_playlist()[self.playlist_playing_position]) + if len(text) == 36 and text[8] == "-": + prefs.lb_token = text + else: + show_message(_("That is not a valid token."), mode="error") - # 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) + def clear_key(self): - elif self.random_mode and (self.album_shuffle_mode or prefs.album_shuffle_lock_mode): + prefs.lb_token = "" + save_prefs() + self.enable = False - # Album shuffle mode - logging.info("Album shuffle mode") +class LastScrob: - po = self.playing_object() + def __init__(self): - redraw = False + self.a_index = -1 + self.a_sc = False + self.a_pt = False + self.queue = [] + self.running = False - # Checks - if po is not None and len(self.playing_playlist()) > 0: + def start_queue(self): - # 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 + self.running = True + mini_t = threading.Thread(target=self.process_queue) + mini_t.daemon = True + mini_t.start() - 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) + def process_queue(self): - else: + time.sleep(0.4) - 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) + while self.queue: - random.shuffle(albums) + try: + tr = self.queue.pop() - 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() + gui.pl_update = 1 + logging.info("Submit Scrobble " + tr[0].artist + " - " + tr[0].title) - else: - logging.error("ADVANCE ERROR - NO CASE!") + success = True - if dry: - return None + if tr[2] == "lfm" and prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): + success = lastfm.scrobble(tr[0], tr[1]) + elif tr[2] == "lb" and lb.enable: + success = lb.listen_full(tr[0], tr[1]) + elif tr[2] == "maloja": + success = maloja_scrobble(tr[0], tr[1]) + elif tr[2] == "air": + success = subsonic.listen(tr[0], submit=True) + elif tr[2] == "koel": + success = koel.listen(tr[0], submit=True) - if 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 not success: + logging.info("Re-queue scrobble") + self.queue.append(tr) + time.sleep(10) + break - # if album_mode: - # goto_album(self.playlist_playing) + except Exception: + logging.exception("SCROBBLE QUEUE ERROR") - self.render_playlist() + if not self.queue: + scrobble_warning_timer.force_set(1000) - if tauon.spot_ctl.playing and end_of_playlist: - tauon.spot_ctl.control("stop") + self.running = False - self.notify_update() - lfm_scrobbler.start_queue() - if play: - notify_song(end_of_playlist, delay=1.3) - return None + def update(self, add_time): - def reset_missing_flags(self) -> None: - for value in self.master_library.values(): - value.found = True - gui.pl_update += 1 + if pctl.queue_step > len(pctl.track_queue) - 1: + logging.info("Queue step error 1") + return -def auto_name_pl(target_pl: int) -> None: - if not pctl.multi_playlist[target_pl].playlist_ids: - return + if self.a_index != pctl.track_queue[pctl.queue_step]: + pctl.a_time = 0 + pctl.b_time = 0 + self.a_index = pctl.track_queue[pctl.queue_step] + self.a_pt = False + self.a_sc = False + if pctl.playing_time == 0 and self.a_sc is True: + logging.info("Reset scrobble timer") + pctl.a_time = 0 + pctl.b_time = 0 + self.a_pt = False + self.a_sc = False - albums = [] - artists = [] - parents = [] + 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() - track = None + 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]) - 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) + 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 - nt = "" - artist = "" + if self.a_sc is False and pctl.master_library[self.a_index].length > 30 and pctl.a_time > 240: + self.a_sc = True + send_full = True - if track: - artist = track.artist - if track.album_artist: - artist = track.album_artist + if send_full: + self.scrob_full_track(pctl.master_library[self.a_index]) - if track and albums and albums[0] and albums.count(albums[0]) == len(albums): - nt = artist + " - " + track.album + def listen_track(self, track_object: TrackClass): + # logging.info("LISTEN") - elif track and artists and artists[0] and artists.count(artists[0]) == len(artists): - nt = artists[0] + if track_object.is_network: + if track_object.file_ext == "SUB": + subsonic.listen(track_object, submit=False) - else: - nt = os.path.basename(commonprefix(parents)) + 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() - pctl.multi_playlist[target_pl].title = nt + if lb.enable: + mini_t = threading.Thread(target=lb.listen_playing, args=([track_object])) + mini_t.daemon = True + mini_t.start() -def get_object(index: int) -> TrackClass: - return pctl.master_library[index] + def scrob_full_track(self, track_object: TrackClass): + # logging.info("SCROBBLE") + track_object.lfm_scrobbles += 1 + gui.pl_update += 1 -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 track_object.is_network: + if track_object.file_ext == "SUB": + self.queue.append((track_object, int(time.time()), "air")) + if track_object.file_ext == "KOEL": + self.queue.append((track_object, int(time.time()), "koel")) -def 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 not prefs.scrobble_hold: + if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): + self.queue.append((track_object, int(time.time()), "lfm")) + if lb.enable: + self.queue.append((track_object, int(time.time()), "lb")) + if prefs.maloja_url and prefs.maloja_enable: + self.queue.append((track_object, int(time.time()), "maloja")) -def g_open_encode_out(a, b, c) -> None: - open_encode_out() +class Strings: -def notify_song_fire(notification, delay, id) -> None: - time.sleep(delay) - notification.show() - if id is None: - return + 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") - time.sleep(8) - if id == gui.notify_main_id: - notification.close() + self.day = _("day") + self.days = _("days") -def notify_song(notify_of_end: bool = False, delay: float = 0.0) -> None: - if not de_notify_support: - return + 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 notify_of_end and prefs.end_setting != "stop": - return + self.web_server_stopped = _("Web server stopped.") - 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 + 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") - track = pctl.playing_object() +class Chunker: - 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 __init__(self): + self.master_count = 0 + self.chunks = {} + self.header = None + self.headers = [] + self.h2 = None - 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") + self.clients = {} - top_line = track.title +class MenuIcon: - if prefs.notify_include_album: - bottom_line = (track.artist + " | " + track.album).strip("| ") - else: - bottom_line = track.artist + 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 - if not track.title: - a, t = filename_to_metadata(clean_string(track.filename)) - if not track.artist: - bottom_line = a - top_line = t +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 - gui.notify_main_id = uid_gen() - id = gui.notify_main_id +class ThreadManager: - if notify_of_end: - bottom_line = "Tauon Music Box" - top_line = (_("End of playlist")) - id = None + def __init__(self): - song_notification.update(top_line, bottom_line, i_path) + 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() - shoot_dl = threading.Thread(target=notify_song_fire, args=([song_notification, delay, id])) - shoot_dl.daemon = True - shoot_dl.start() + self.d: dict = {} -class LastFMapi: - API_SECRET = "6e433964d3ff5e817b7724d16a9cf0cc" - connected = False - API_KEY = "bfdaf6357f1dddd494e5bee1afe38254" - scanning_username = "" + 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 - network = None - lastfm_network = None - tries = 0 + 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() - scanning_friends = False - scanning_loves = False - scanning_scrobbles = False + def check_playback_running(self) -> bool: + if self.playback is None: + return False + return self.playback.is_alive() - def __init__(self) -> None: - self.sg = None - self.url = None +class Menu: + """Right click context menu generator""" - def get_network(self) -> LibreFMNetwork: - if prefs.use_libre_fm: - return pylast.LibreFMNetwork - return pylast.LastFMNetwork + switch = 0 + count = switch + 1 + instances: list[Menu] = [] + active = False - 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 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 - if self.network is None: - self.no_user_connect() + def __init__(self, width: int, show_icons: bool = False) -> None: - 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) + 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 auth2(self) -> None: + 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)) - # This is step 2 where the user clicks "Done" + self.id = Menu.count + self.break_height = round(4 * gui.scale) - if self.sg is None: - show_message(_("You need to log in first")) - return + Menu.count += 1 - 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 + self.sub_number = 0 + self.sub_active = -1 + self.sub_y_postion = 0 + Menu.instances.append(self) - 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") + @staticmethod + def deco(_=_): + return [colours.menu_text, colours.menu_background, None] - if not toggle_lfm_auto(mode=1): - toggle_lfm_auto() + def click(self) -> None: + self.clicked = True + # cheap hack to prevent scroll bar from being activated when closing menu + global click_location + click_location = [0, 0] - def auth3(self) -> None: - """This is used for 'logout'""" - prefs.last_fm_token = None - prefs.last_fm_username = "" - show_message(_("Logout will complete on app restart.")) + def 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 connect(self, m_notify: bool = True) -> bool | None: + def br(self) -> None: + self.items.append(None) - if not last_fm_enable: - return False + 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 self.connected is True: - if m_notify: - show_message(_("Already connected to Last.fm")) - return True + 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) - if prefs.last_fm_token is None: - show_message(_("No Last.Fm account registered"), _("Authorise an account in settings"), mode="info") - return None + def test_item_active(self, item): + if item.show_test is not None: + if item.show_test(1) is False: + return False + return True - logging.info("Attempting to connect to Last.fm network") + 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() - try: + def render_icon(self, x, y, icon, selected, fx): - self.network = self.get_network()( - api_key=self.API_KEY, api_secret=self.API_SECRET, session_key=prefs.last_fm_token) # , username=lfm_username, password_hash=lfm_hash) + if colours.lm: + selected = True - self.connected = True - if m_notify: - show_message(_("Connection to Last.fm was successful."), mode="done") + if icon is not None: - logging.info("Connection to lastfm appears successful") - return True + x += icon.xoff * gui.scale + y += icon.yoff * gui.scale - 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 + colour = None - def toggle(self) -> None: - prefs.scrobble_hold ^= True + if icon.base_asset is None: + # Colourise mode - def details_ready(self) -> bool: - if prefs.last_fm_token: - return True - return False + if icon.colour_callback is not None: # and icon.colour_callback() is not None: + colour = icon.colour_callback() - 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 + elif selected and fx[0] != colours.menu_text_disabled: + colour = icon.colour - 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 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 - 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 colour is None: + # colour = [145, 145, 145, 70] + colour = colours.menu_icons # [255, 255, 255, 35] + # colour = [50, 50, 50, 255] - 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 + icon.asset.render(x, y, colour) - def get_all_scrobbles_estimate_time(self) -> float | None: + 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 not self.connected: - self.connect(False) - if not self.connected or not prefs.last_fm_username: - return None + # 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) - user = pylast.User(prefs.last_fm_username, self.network) - total = user.get_playcount() + def render(self): + if self.active: - if total: - return 0.04364 * total - return 0 + if Menu.switch != self.id: + self.active = False - def get_all_scrobbles(self) -> None: + for menu in Menu.instances: + if menu.active: + break + else: + Menu.active = False - if not self.connected: - self.connect(False) - if not self.connected or not prefs.last_fm_username: - return + return - try: - self.scanning_scrobbles = True - self.network.enable_rate_limit() - user = pylast.User(prefs.last_fm_username, self.network) - # username = user.get_name() - perf_timer.set() - tracks = user.get_recent_tracks(None) + # ytoff = 3 + y_run = round(self.pos[1]) + to_call = None - counts = {} + # 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) - # 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 + x_run = self.pos[0] - touched = [] + for i in range(len(self.items)): + #logging.info(self.items[i]) - # Add counts to matching tracks - for key, value in counts.items(): - artist, title = key - artist = artist.lower() - title = title.lower() + # Draw menu break + if self.items[i] is 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 + 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) - logging.info(perf_timer.get()) - gui.pl_update += 1 - self.scanning_scrobbles = False - tauon.bg_save() - show_message(_("Scanning scrobbles complete"), mode="done") + rect = (x_run, y_run, self.w, self.break_height - 1) + if coll(rect): + self.clicked = False - def artist_info(self, artist: str): + ddt.rect_a((x_run, y_run), (self.w, self.break_height), colours.menu_background) - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return False, "", "" + ddt.rect_a((x_run, y_run + 2 * gui.scale), (self.w, 2 * gui.scale), break_colour) - 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() + # Draw tab + ddt.rect_a((x_run, y_run), (4 * gui.scale, self.break_height), colours.menu_tab) + y_run += self.break_height - return True, bio, "", mbid, url - except Exception: - logging.exception("last.fm get artist info failed") + continue - return False, "", "", "", "" + 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 - def artist_mbid(self, artist: str): + # 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 self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return "" + if fx[2] is not None: + label = fx[2] + else: + label = self.items[i].title - 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") + # Show text as disabled if disable_test() passes + if self.is_item_disabled(self.items[i]): + fx[0] = colours.menu_text_disabled - return "" + # Draw item background, black by default + ddt.rect_a((x_run, y_run), (self.w, self.h), fx[1]) + bg = fx[1] - 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 + # Detect if mouse is over this item + selected = False + rect = (x_run, y_run, self.w, self.h - 1) + fields.add(rect) - 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 + 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) - remote_loved = track.get_userloved() + # Call menu items callback if clicked + if self.clicked: - if track_object.title != track.get_correction() or track_object.artist != track.get_artist().get_correction(): - logging.warning(f"Pylast/lastfm bug workaround. API thought {track_object.artist} - {track_object.title} loved status was: {remote_loved}") - return + if self.items[i].is_sub_menu is False: + to_call = i + if self.items[i].set_ref is not None: + self.reference = self.items[i].set_ref + global mouse_down + mouse_down = False - if remote_loved is None: - logging.error("Error getting loved status") - return + else: + self.clicked = False + self.sub_active = self.items[i].sub_menu_number + self.sub_y_postion = y_run - local_loved = love(set=False, track_id=track_object.index, notify=False, sync=False) + # Draw tab + ddt.rect_a((x_run, y_run), (4 * gui.scale, self.h), colours.menu_tab) - 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") + # 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) - 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 self.show_icons: + x += 25 * gui.scale - if timestamp is None: - timestamp = int(time.time()) + # Draw arrow icon for sub menu + if self.items[i].is_sub_menu is True: - # lastfm_user = self.network.get_user(self.username) + 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) - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) - album_artist = track_object.album_artist + 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) - logging.info("Submitting scrobble...") + # 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) - # 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) + # Render the items label + ddt.text((x_run + x, y_run + ytoff), label, fx[0], self.font, max_w=self.w - (x + 9 * gui.scale), bg=bg) + + # Render the items hint + if self.items[i].hint != None: + + if is_light(bg) or colours.lm: + hint_colour = rgb_add_hls(bg, 0, -0.30, -0.3) else: - self.network.scrobble(artist=artist, title=title, album=album, timestamp=timestamp) - else: - self.network.scrobble(artist=artist, title=title, timestamp=timestamp) - # logging.info('Scrobbled') + hint_colour = rgb_add_hls(bg, 0, 0.15, 0) - # Pull loved status + # 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) - self.sync_pull_love(track_object) + y_run += self.h + if y_run > window_size[1] - self.h: + direc = 1 + if self.pos[0] > window_size[0] // 2: + direc = -1 + x_run += self.w * direc + y_run = self.pos[1] - else: - logging.warning("Not sent, incomplete metadata") + # Render sub menu if active + if self.sub_active > -1 and self.items[i].is_sub_menu and self.sub_active == self.items[i].sub_menu_number: - except Exception as e: - logging.exception("Failed to Scrobble!") - if "retry" in str(e): - logging.warning("Retrying in a couple seconds...") - time.sleep(7) + # 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 - try: - self.network.scrobble(artist=artist, title=title, timestamp=timestamp) - # logging.info('Scrobbled') - return True - except Exception: - logging.exception("Failed to retry!") + 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 - # 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) + fx = self.deco() - return False - return True + minY = window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale + sub_pos[1] = min(sub_pos[1], minY) - def get_bio(self, artist: str) -> str: + xoff = 0 + for i in self.subs[self.sub_active]: + if i.icon is not None: + xoff = 24 * gui.scale + break - if self.lastfm_network is None: - if self.last_fm_only_connect() is False: - return "" + for w in range(len(self.subs[self.sub_active])): - 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 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 love(self, artist: str, title: str): + # 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 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() + # Item background + ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), fx[1]) - 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() + # 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 - def clear_friends_love(self) -> None: + # Call Callback + if self.clicked and not self.is_item_disabled(self.subs[self.sub_active][w]): - count = 0 - for index, tr in pctl.master_library.items(): - count += len(tr.lfm_friend_likes) - tr.lfm_friend_likes.clear() + # 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) - show_message(_("Removed {N} loves.").format(N=count)) + # If callback just need ref + elif self.subs[self.sub_active][w].pass_ref: + self.subs[self.sub_active][w].func(self.reference) - def get_friends_love(self): - if not last_fm_enable: - return - self.scanning_friends = True + else: + self.subs[self.sub_active][w].func() - try: - username = prefs.last_fm_username - logging.info(f"Username is {username}") + if fx[2] is not None: + label = fx[2] + else: + label = self.subs[self.sub_active][w].title - if not username: - self.scanning_friends = False - show_message(_("There was an error, try re-log in")) - return + # Show text as disabled if disable_test() passes + if self.is_item_disabled(self.subs[self.sub_active][w]): + fx[0] = colours.menu_text_disabled - if self.network is None: - self.no_user_connect() + # 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) - 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) + # 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) - try: - loves = friend.get_loved_tracks(limit=None) - except Exception: - logging.exception("Failed to get_loved_tracks!") + # Draw tab + ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (4 * gui.scale, self.h), colours.menu_tab) - for track in loves: - title = track.track.title.casefold() - artist = track.track.artist.name.casefold() - for index, tr in pctl.master_library.items(): + # Render the menu outline + # ddt.rect_a(sub_pos, (sub_w, self.h * len(self.subs[self.sub_active])), colours.grey(40)) - 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) + # Process Click Actions + if to_call is not None: - except Exception: - logging.exception("There was an error getting friends loves") - show_message(_("There was an error getting friends loves"), "", mode="warning") + 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.scanning_friends = False + if self.clicked or key_esc_press or self.close_next_frame: + self.close_next_frame = False + self.active = False + self.clicked = False - def dl_love(self) -> None: - if not last_fm_enable: - return - username = prefs.last_fm_username - show_message(_("Scanning loved tracks for: {username}").format(username=username), mode="info") - self.scanning_username = username + last_click_location[0] = 0 + last_click_location[1] = 0 - if not username: - show_message(_("No username found"), mode="error") - return + for menu in Menu.instances: + if menu.active: + break + else: + Menu.active = False - if len(username) > 25: - logging.error("Aborted due to long username") - return + # Render the menu outline + # ddt.rect_a(self.pos, (self.w, self.h * len(self.items)), colours.grey(40)) - self.scanning_loves = True + def activate(self, in_reference=0, position=None): - logging.info("Connect for friend scan") + Menu.active = True - try: - if self.network is None: - self.no_user_connect() + if position != None: + self.pos = [position[0], position[1]] + else: + self.pos = [copy.deepcopy(mouse_position[0]), copy.deepcopy(mouse_position[1])] - self.network.enable_rate_limit() - logging.info("Get user...") - lastfm_user = self.network.get_user(username) - tracks = lastfm_user.get_loved_tracks(limit=None) + self.reference = in_reference + Menu.switch = self.id + self.sub_active = -1 - matches = 0 - updated = 0 + # 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) - for track in tracks: - title = track.track.title.casefold() - artist = track.track.artist.name.casefold() + # 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 - 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" + # Flip menu up if would intersect with bottom of window + if self.pos[1] + full_h > window_size[1]: + self.pos[1] -= shown_h - star_store.insert(index, star) + # Prevent moving outside top of window + if self.pos[1] < gui.panelY: + self.pos[1] = gui.panelY + self.pos[0] += 5 * gui.scale - 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 + self.active = True - 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 +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 - # logging.info('Updating Now Playing') + def get_file_source(self, track_object: TrackClass): - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) + global album_art_gen - 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 + sources = album_art_gen.get_sources(track_object) -def get_backend_time(path): - pctl.time_to_get = path + if len(sources) == 0: + return False, 0 - pctl.playerCommand = "time" - pctl.playerCommandReady = True + offset = album_art_gen.get_offset(track_object.fullpath, sources) + return sources[offset], offset - while pctl.playerCommand != "done": - time.sleep(0.005) + def worker_render(self): - return pctl.time_to_get + self.lock.acquire() + # time.sleep(0.1) -class ListenBrainz: + if 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() + gui.update += 1 - def __init__(self): + while len(self.queue) > 0: - self.enable = prefs.enable_lb - # self.url = "https://api.listenbrainz.org/1/submit-listens" + source_image = None - def url(self): - url = prefs.listenbrainz_url - if not url: - url = "https://api.listenbrainz.org/" - if not url.endswith("/"): - url += "/" - return url + "1/submit-listens" + if gui.halt_image_rendering: + self.queue.clear() + break - def listen_full(self, track_object: TrackClass, time) -> bool: + self.i += 1 - 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") + try: + # key = self.queue[0] + key = self.queue.pop(0) + except Exception: + logging.exception("thumb queue empty") + break - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) + if key not in self.gall: + order = [1, None, None, None] + self.gall[key] = order + else: + order = self.gall[key] - if title == "" or artist == "": - return True + size = key[1] - data = {"listen_type": "single", "payload": []} - metadata = {"track_name": title, "artist_name": artist} + slow_load = False + cache_load = False - additional = {} + try: - # MusicBrainz Artist IDs - if "musicbrainz_artistids" in track_object.misc: - additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] + 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 - # MusicBrainz Release ID - if "musicbrainz_albumid" in track_object.misc: - additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] + if slow_load: - # MusicBrainz Recording ID - if "musicbrainz_recordingid" in track_object.misc: - additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + source, c_offset = self.get_file_source(key[0]) - # MusicBrainz Track ID - if "musicbrainz_trackid" in track_object.misc: - additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + if source is False: + order[0] = 0 + self.gall[key] = order + # del self.queue[0] + continue - if additional: - metadata["additional_info"] = additional + img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(c_offset) - # logging.info(additional) - data["payload"].append({"track_metadata": metadata}) - data["payload"][0]["listened_at"] = time - - r = requests.post(self.url(), headers={"Authorization": "Token " + prefs.lb_token}, data=json.dumps(data), timeout=10) - if r.status_code != 200: - show_message(_("There was an error submitting data to ListenBrainz"), r.text, mode="warning") - return False - return True - - def listen_playing(self, track_object: TrackClass) -> None: - if self.enable is False: - return - if prefs.scrobble_hold is True: - return - if prefs.lb_token is None: - show_message(_("ListenBrainz is enabled but there is no token."), _("How did this even happen."), mode="error") - title = track_object.title - album = track_object.album - artist = get_artist_strip_feat(track_object) - - if title == "" or artist == "": - return - - data = {"listen_type": "playing_now", "payload": []} - metadata = {"track_name": title, "artist_name": artist} - - additional = {} - - # MusicBrainz Artist IDs - if "musicbrainz_artistids" in track_object.misc: - additional["artist_mbids"] = track_object.misc["musicbrainz_artistids"] - - # MusicBrainz Release ID - if "musicbrainz_albumid" in track_object.misc: - additional["release_mbid"] = track_object.misc["musicbrainz_albumid"] - - # MusicBrainz Recording ID - if "musicbrainz_recordingid" in track_object.misc: - additional["recording_mbid"] = track_object.misc["musicbrainz_recordingid"] + # gall_render_last_timer.set() - # MusicBrainz Track ID - if "musicbrainz_trackid" in track_object.misc: - additional["track_mbid"] = track_object.misc["musicbrainz_trackid"] + 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 - if track_object.track_number: - try: - additional["tracknumber"] = str(int(track_object.track_number)) - except Exception: - logging.exception("Error trying to get track_number") + # 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) - if track_object.length: - additional["duration"] = str(int(track_object.length)) + g = io.BytesIO() + g.seek(0) - additional["media_player"] = t_title - additional["submission_client"] = t_title - additional["media_player_version"] = str(n_version) + if cache_load: + g.write(source_image.read()) - metadata["additional_info"] = additional - data["payload"].append({"track_metadata": metadata}) - # data["payload"][0]["listened_at"] = int(time.time()) + 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 - 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()) + im.save(g, "BMP") - def paste_key(self): + 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) - text = copy_from_clipboard() - if text == "": - show_message(_("There is no text in the clipboard"), mode="error") - return + g.seek(0) - if prefs.listenbrainz_url: - prefs.lb_token = text - return + # source_image.close() - if len(text) == 36 and text[8] == "-": - prefs.lb_token = text - else: - show_message(_("That is not a valid token."), mode="error") + order = [2, g, None, None] + self.gall[key] = order - def clear_key(self): + gui.update += 1 + if source_image: + source_image.close() + source_image = None + # del self.queue[0] - prefs.lb_token = "" - save_prefs() - self.enable = False + time.sleep(0.001) -def get_love(track_object: TrackClass) -> bool: - star = star_store.full_get(track_object.index) - if star is None: - return False + except Exception: + logging.exception("Image load failed on track: " + key[0].fullpath) + order = [0, None, None, None] + self.gall[key] = order + gui.update += 1 + # del self.queue[0] - if "L" in star[1]: - return True - return False + if size < 150: + random.shuffle(self.queue) -def get_love_index(index: int) -> bool: - star = star_store.full_get(index) - if star is None: + if self.i > 0: + self.i = 0 + return True return False - if "L" in star[1]: - return True - return False + def render(self, track: TrackClass, location, size=None, force_offset=None) -> bool | None: + if gallery_load_delay.get() < 0.5: + return None -def get_love_timestamp_index(index: int): - star = star_store.full_get(index) - if star is None: - return 0 - return star[3] + x = round(location[0]) + y = round(location[1]) -def love(set=True, track_id=None, no_delay=False, notify=False, sync=True): - if len(pctl.track_queue) < 1: - return False + # time.sleep(0.1) + if size is None: + size = self.size - if track_id is not None and track_id < 0: - return False + size = round(size) - if track_id is None: - track_id = pctl.track_queue[pctl.queue_step] + # 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 - loved = False - star = star_store.full_get(track_id) + if force_offset is not None: + offset = force_offset - if star is not None: - if "L" in star[1]: - loved = True + key = (track, size, offset) - if set is False: - return loved + if key in self.gall: + #logging.info("old") - # 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 + order = self.gall[key] - if star is None: - star = star_store.new_object() + if order[0] == 0: + # broken + return False - loved ^= True + if order[0] == 1: + # not done yet + return False - 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) + if order[0] == 2: + # finish processing - delay = 0.3 - if no_delay or not sync or not lastfm.details_ready(): - delay = 0 + 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) - star[3] = time.time() - 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") + order[0] = 3 + order[1].close() + order[1] = None + order[2] = c + order[3] = dst + self.gall[(track, size, offset)] = order - if pctl.master_library[track_id].file_ext == "JELY": - jellyfin.favorite(pctl.master_library[track_id]) + if order[0] == 3: + # ready - 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) + 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]) - gui.pl_update = 2 - gui.update += 1 - if sync and pctl.mpris is not None: - pctl.mpris.update(force=True) + if (track, size, offset) in self.key_list: + self.key_list.remove((track, size, offset)) + self.key_list.append((track, size, offset)) -def maloja_get_scrobble_counts(): - if lastfm.scanning_scrobbles is True or not prefs.maloja_url: - 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] - url = prefs.maloja_url - if not url.endswith("/"): - url += "/" - url += "apis/mlj_1/scrobbles" - lastfm.scanning_scrobbles = True - try: - r = requests.get(url, timeout=10) + return True - 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 + 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 - try: - data = json.loads(r.text) - l = data["list"] +class ThumbTracks: + def __init__(self) -> None: + pass - counts = {} + def path(self, track: TrackClass) -> str: + source, offset = tauon.gall_ren.get_file_source(track) - for item in l: - artists = item.get("artists") - title = item.get("title") - if title and artists: - key = (title, tuple(artists)) - c = counts.get(key, 0) - counts[key] = c + 1 + if source is False: # No art + return None - touched = [] + image_name = track.album + track.parent_folder_path + str(offset) + image_name = hashlib.md5(image_name.encode("utf-8", "replace")).hexdigest() - 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") + t_path = os.path.join(e_cache_dir, image_name + ".jpg") - except Exception: - logging.exception("There was an error parsing the data") - show_message(_("There was an error parsing the data"), mode="warning") + if os.path.isfile(t_path): + return t_path - gui.pl_update += 1 - lastfm.scanning_scrobbles = False - tauon.bg_save() + source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) -def maloja_scrobble(track: TrackClass, timestamp: int = int(time.time())) -> bool | None: - url = prefs.maloja_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 - if not track.artist or not track.title: - return None +class Tauon: + """Root class for everything Tauon""" + def __init__(self): - if not url.endswith("/newscrobble"): - if not url.endswith("/"): - url += "/" - url += "apis/mlj_1/newscrobble" + 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() - d = {} - d["artists"] = [track.artist] # let Maloja parse/fix artists - d["title"] = track.title + #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 - if track.album: - d["album"] = track.album - if track.album_artist: - d["albumartists"] = [track.album_artist] # let Maloja parse/fix artists + self.tray_lock = threading.Lock() + self.tray_releases = 0 - d["length"] = int(track.length) - d["time"] = timestamp - d["key"] = prefs.maloja_key + self.play_lock = None + self.update_play_lock = None + self.sleep_lock = None + self.shutdown_lock = None + self.quick_close = False - 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.copied_track = None + self.macos = macos + self.aud: CDLL | None = None -class LastScrob: + self.recorded_songs = [] - def __init__(self): + self.chrome_mode = False + self.web_running = False + self.web_thread = None + self.remote_limited = True + self.enable_librespot = shutil.which("librespot") - self.a_index = -1 - self.a_sc = False - self.a_pt = False - self.queue = [] - self.running = False + #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 - def start_queue(self): + self.gme_formats = GME_Formats - self.running = True - mini_t = threading.Thread(target=self.process_queue) - mini_t.daemon = True - mini_t.start() + self.spot_ctl: SpotCtl = SpotCtl(self) + self.tidal: Tidal = Tidal(self) + self.chrome: Chrome | None = None + self.chrome_menu: Menu | None = None - def process_queue(self): + self.tls_context = tls_context - time.sleep(0.4) + def start_remote(self) -> None: - while self.queue: + if not self.web_running: + self.web_thread = threading.Thread( + target=webserve2, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) + self.web_thread.daemon = True + self.web_thread.start() + self.web_running = True + def download_ffmpeg(self, x): + def go(): + url = "https://github.com/GyanD/codexffmpeg/releases/download/5.0.1/ffmpeg-5.0.1-essentials_build.zip" + sha = "9e00da9100ae1bba22b1385705837392e8abcdfd2efc5768d447890d101451b5" + show_message(_("Starting download...")) try: - tr = self.queue.pop() + 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 - gui.pl_update = 1 - logging.info("Submit Scrobble " + tr[0].artist + " - " + tr[0].title) + 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)) - success = True + except Exception as e: + logging.exception("Download failed") + show_message(_("Download failed"), str(e), mode="error") - 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) + 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()) - if not success: - logging.info("Re-queue scrobble") - self.queue.append(tr) - time.sleep(10) - break + 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()) - except Exception: - logging.exception("SCROBBLE QUEUE ERROR") + exe.close() + show_message(_("FFMPEG fetch complete"), mode="done") - if not self.queue: - scrobble_warning_timer.force_set(1000) + shooter(go) - self.running = False + def set_tray_icons(self, force: bool = False): - def update(self, add_time): + 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 pctl.queue_step > len(pctl.track_queue) - 1: - logging.info("Queue step error 1") - return + 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 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 + 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 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 not user_icon_dir.is_dir(): + os.makedirs(user_icon_dir) - 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]) + 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") - 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 + def get_tray_icon(self, name: str) -> str: + return str(self.cache_directory / "icon-export" / f"{name}.svg") - 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 + 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 send_full: - self.scrob_full_track(pctl.master_library[self.a_index]) + def get_ffmpeg(self) -> str | None: + path = user_directory / "ffmpeg.exe" + if msys and path.is_file(): + return str(path) - def listen_track(self, track_object: TrackClass): - # logging.info("LISTEN") + # macOS + path = install_directory / "ffmpeg" + if path.is_file(): + return str(path) - if track_object.is_network: - if track_object.file_ext == "SUB": - subsonic.listen(track_object, submit=False) + logging.debug(f"Looking for ffmpeg in PATH: {os.environ.get('PATH')}") + path = shutil.which("ffmpeg") + if path: + return path + return None - 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() + def get_ffprobe(self) -> str | None: + path = user_directory / "ffprobe.exe" + if msys and path.is_file(): + return str(path) - if lb.enable: - mini_t = threading.Thread(target=lb.listen_playing, args=([track_object])) - mini_t.daemon = True - mini_t.start() + # macOS + path = install_directory / "ffprobe" + if path.is_file(): + return str(path) - def scrob_full_track(self, track_object: TrackClass): - # logging.info("SCROBBLE") - track_object.lfm_scrobbles += 1 - gui.pl_update += 1 + logging.debug(f"Looking for ffprobe in PATH: {os.environ.get('PATH')}") + path = shutil.which("ffprobe") + if path: + return path + return None - if track_object.is_network: - if track_object.file_ext == "SUB": - self.queue.append((track_object, int(time.time()), "air")) - if track_object.file_ext == "KOEL": - self.queue.append((track_object, int(time.time()), "koel")) + def bg_save(self) -> None: + self.worker_save_state = True + tauon.thread_manager.ready("worker") - if not prefs.scrobble_hold: - if prefs.auto_lfm and (lastfm.connected or lastfm.details_ready()): - self.queue.append((track_object, int(time.time()), "lfm")) - if lb.enable: - self.queue.append((track_object, int(time.time()), "lb")) - if prefs.maloja_url and prefs.maloja_enable: - self.queue.append((track_object, int(time.time()), "maloja")) + def exit(self, reason: str) -> None: + logging.info("Shutting down. Reason: " + reason) + pctl.running = False + self.wake() -class Strings: + def min_to_tray(self) -> None: + SDL_HideWindow(t_window) + gui.mouse_unknown = True - def __init__(self): - self.spotify_likes = _("Spotify Likes") - self.spotify_albums = _("Spotify Albums") - self.spotify_un_liked = _("Track removed from liked tracks") - self.spotify_already_un_liked = _("Track was already un-liked") - self.spotify_already_liked = _("Track is already liked") - self.spotify_like_added = _("Track added to liked tracks") - self.spotify_account_connected = _("Spotify account connected") - self.spotify_not_playing = _("This Spotify account isn't currently playing anything") - self.spotify_error_starting = _("Error starting Spotify") - self.spotify_request_auth = _("Please authorise Spotify in settings!") - self.spotify_need_enable = _("Please authorise and click the enable toggle first!") - self.spotify_import_complete = _("Spotify import complete") + def raise_window(self) -> None: + SDL_ShowWindow(t_window) + SDL_RaiseWindow(t_window) + SDL_RestoreWindow(t_window) + gui.lowered = False + gui.update += 1 - self.day = _("day") - self.days = _("days") + def focus_window(self) -> None: + SDL_RaiseWindow(t_window) - self.scan_chrome = _("Scanning for Chromecasts...") - self.cast_to = _("Cast to: %s") - self.no_chromecasts = _("No Chromecast devices found") - self.stop_cast = _("End Cast") + def get_playing_playlist_id(self) -> int: + return pl_to_id(pctl.active_playlist_playing) - self.web_server_stopped = _("Web server stopped.") + def wake(self) -> None: + SDL_PushEvent(ctypes.byref(self.dummy_event)) - self.menu_open_tauon = _("Open Tauon Music Box") - self.menu_play_pause = _("Play/Pause") - self.menu_next = _("Next Track") - self.menu_previous = _("Previous Track") - self.menu_quit = _("Quit") +class PlexService: -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): + self.connected = False + self.resource = None + self.scanning = False -def pl_to_id(pl: int) -> int: - return pctl.multi_playlist[pl].uuid_int + def connect(self): -class Chunker: + 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 - def __init__(self): - self.master_count = 0 - self.chunks = {} - self.header = None - self.headers = [] - self.h2 = None + 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 - self.clients = {} + 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 -class MenuIcon: + # from plexapi.server import PlexServer + # baseurl = 'http://localhost:32400' + # token = '' - 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 + # self.resource = PlexServer(baseurl, token) -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 + self.connected = True -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] + def resolve_stream(self, location): + logging.info("Get plex stream") + if not self.connected: + self.connect() -def encode_folder_name(track_object: TrackClass) -> str: - folder_name = track_object.artist + " - " + track_object.album + # return self.resource.url(location, True) + return self.resource.library.fetchItem(location).getStreamURL() - if folder_name == " - ": - folder_name = track_object.parent_folder_name + def resolve_thumbnail(self, location): - folder_name = filename_safe(folder_name).strip() + if not self.connected: + self.connect() + if self.connected: + return self.resource.url(location, True) + return None - if not folder_name: - folder_name = str(track_object.index) + def get_albums(self, return_list=False): - 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) + gui.update += 1 + self.scanning = True - return folder_name + if not self.connected: + self.connect() -class ThreadManager: + if not self.connected: + self.scanning = False + return [] - def __init__(self): + playlist = [] - 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() + 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 - self.d: dict = {} + albums = self.resource.library.section("Music").albums() + gui.to_got = 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 + for album in albums: + year = album.year + album_artist = album.parentTitle + album_title = album.title - 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() + parent = (album_artist + " - " + album_title).strip("- ") - def check_playback_running(self) -> bool: - if self.playback is None: - return False - return self.playback.is_alive() + for track in album.tracks(): -class Menu: - """Right click context menu generator""" + if not track.duration: + logging.warning("Skipping track with invalid duration - " + track.title + " - " + track.grandparentTitle) + continue - switch = 0 - count = switch + 1 - instances: list[Menu] = [] - active = False + id = pctl.master_count + replace_existing = False - def rescale(self): - self.vertical_size = round(self.base_v_size * gui.scale) - self.h = self.vertical_size - self.w = self.request_width * gui.scale - if gui.scale == 2: - self.w += 15 + e = existing.get(track.key) + if e is not None: + id = e + replace_existing = True - def __init__(self, width: int, show_icons: bool = False) -> None: + title = track.title + track_artist = track.grandparentTitle + duration = track.duration / 1000 - 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() + 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] - 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)) + nt.is_network = True - self.id = Menu.count - self.break_height = round(4 * gui.scale) + if track.thumb: + nt.art_url_key = track.thumb - Menu.count += 1 + nt.url_key = track.key + nt.date = str(year) - self.sub_number = 0 - self.sub_active = -1 - self.sub_y_postion = 0 - Menu.instances.append(self) + pctl.master_library[id] = nt - @staticmethod - def deco(_=_): - return [colours.menu_text, colours.menu_background, None] + if not replace_existing: + pctl.master_count += 1 - 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] + playlist.append(nt.index) - 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) + gui.to_got += 1 + gui.update += 1 + gui.pl_update += 1 - def br(self) -> None: - self.items.append(None) + self.scanning = False - 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 return_list: + return playlist - 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) + 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 test_item_active(self, item): - if item.show_test is not None: - if item.show_test(1) is False: - return False - return True +class SubsonicService: - def is_item_disabled(self, item): - if item.disable_test is not None: - if item.pass_ref_deco: - return item.disable_test(self.reference) - return item.disable_test() + def __init__(self): + self.scanning = False + self.playlists = prefs.subsonic_playlists - def render_icon(self, x, y, icon, selected, fx): + def r(self, point, p=None, binary: bool = False, get_url: bool = False): + salt = secrets.token_hex(8) + server = prefs.subsonic_server.rstrip("/") + "/" - if colours.lm: - selected = True + params = { + "u": prefs.subsonic_user, + "v": "1.13.0", + "c": t_title, + "f": "json", + } - if icon is not None: + if prefs.subsonic_password_plain: + params["p"] = prefs.subsonic_password + else: + params["t"] = hashlib.md5((prefs.subsonic_password + salt).encode()).hexdigest() + params["s"] = salt - x += icon.xoff * gui.scale - y += icon.yoff * gui.scale + if p: + params.update(p) - colour = None + point = "rest/" + point - if icon.base_asset is None: - # Colourise mode + url = server + point - if icon.colour_callback is not None: # and icon.colour_callback() is not None: - colour = icon.colour_callback() + if get_url: + return url, params - elif selected and fx[0] != colours.menu_text_disabled: - colour = icon.colour + response = requests.get(url, params=params, timeout=10) - 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 binary: + return response.content - if colour is None: - # colour = [145, 145, 145, 70] - colour = colours.menu_icons # [255, 255, 255, 35] - # colour = [50, 50, 50, 255] + d = json.loads(response.text) + # logging.info(d) - icon.asset.render(x, y, colour) + if d["subsonic-response"]["status"] != "ok": + show_message(_("Subsonic Error: ") + response.text, mode="warning") + logging.error("Subsonic Error: " + response.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 + return d - # Pre-rendered mode - if icon.mode_callback is not None: - if icon.mode_callback(): - icon.asset.render(x, y) - else: - icon.base_asset.render(x, y) - elif selected: - icon.asset.render(x, y) - else: - icon.base_asset.render(x, y) + def get_cover(self, track_object: TrackClass): + response = self.r("getCoverArt", p={"id": track_object.art_url_key}, binary=True) + return io.BytesIO(response) - def render(self): - if self.active: + def resolve_stream(self, key): - if Menu.switch != self.id: - self.active = False + p = {"id": key} + if prefs.network_stream_bitrate > 0: + p["maxBitRate"] = prefs.network_stream_bitrate - for menu in Menu.instances: - if menu.active: - break - else: - Menu.active = False + return self.r("stream", p={"id": key}, get_url=True) + # logging.info(response.content) - return + def listen(self, track_object: TrackClass, submit: bool = False): - # ytoff = 3 - y_run = round(self.pos[1]) - to_call = None + 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 - # 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) + def set_rating(self, track_object: TrackClass, rating): - x_run = self.pos[0] + 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 - for i in range(len(self.items)): - #logging.info(self.items[i]) + def set_album_rating(self, track_object: TrackClass, rating): + id = track_object.misc.get("subsonic-folder-id") + if id is not None: + try: + a = self.r("setRating", p={"id": id, "rating": math.ceil(rating / 2)}) + except Exception: + logging.exception("Error connect for set rating on airsonic") + return True - # Draw menu break - if self.items[i] is None: + def get_music3(self, return_list: bool = False): - 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) + self.scanning = True + gui.to_got = 0 - rect = (x_run, y_run, self.w, self.break_height - 1) - if coll(rect): - self.clicked = False + existing = {} - ddt.rect_a((x_run, y_run), (self.w, self.break_height), colours.menu_background) + for track_id, track in pctl.master_library.items(): + if track.is_network and track.file_ext == "SUB": + existing[track.url_key] = track_id - ddt.rect_a((x_run, y_run + 2 * gui.scale), (self.w, 2 * gui.scale), break_colour) + 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 [] - # Draw tab - ddt.rect_a((x_run, y_run), (4 * gui.scale, self.break_height), colours.menu_tab) - y_run += self.break_height + b = a["subsonic-response"]["indexes"]["index"] - continue + folders = [] - 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 letter in b: + artists = letter["artist"] + for artist in artists: + folders.append(( + artist["id"], + artist["name"], + )) - # 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() + playlist = [] - if fx[2] is not None: - label = fx[2] - else: - label = self.items[i].title + songsets = [] + for i in range(len(folders)): + songsets.append([]) + statuses = [0] * len(folders) + dupes = [] - # Show text as disabled if disable_test() passes - if self.is_item_disabled(self.items[i]): - fx[0] = colours.menu_text_disabled + def getsongs(index, folder_id, name: str, inner: bool = False, parent=None): - # Draw item background, black by default - ddt.rect_a((x_run, y_run), (self.w, self.h), fx[1]) - bg = fx[1] + try: + d = self.r("getMusicDirectory", p={"id": folder_id}) + if "child" not in d["subsonic-response"]["directory"]: + if not inner: + statuses[index] = 2 + return - # Detect if mouse is over this item - selected = False - rect = (x_run, y_run, self.w, self.h - 1) - fields.add(rect) + 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 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) + items = d["subsonic-response"]["directory"]["child"] - # Call menu items callback if clicked - if self.clicked: + gui.update = 2 - 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 + for item in items: - else: - self.clicked = False - self.sub_active = self.items[i].sub_menu_number - self.sub_y_postion = y_run + if item["isDir"]: - # Draw tab - ddt.rect_a((x_run, y_run), (4 * gui.scale, self.h), colours.menu_tab) + 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)) - # 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) + getsongs(index, item["id"], item["title"], inner=True, parent=item) + continue - if self.show_icons: - x += 25 * gui.scale + gui.to_got += 1 + song = item + nt = TrackClass() - # Draw arrow icon for sub menu - if self.items[i].is_sub_menu is True: + if parent and "artist" in parent: + nt.album_artist = parent["artist"] - 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 "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 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) + 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 - # 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) + rating = 0 + if "userRating" in song: + rating = int(song["userRating"]) - # 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) + songsets[index].append((nt, name, song["id"], rating)) - # Render the items hint - if self.items[i].hint != None: + if inner: + return + statuses[index] = 2 - 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) + i = -1 + for id, name in folders: + i += 1 + while statuses.count(1) > 3: + time.sleep(0.1) - # 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) + statuses[i] = 1 + t = threading.Thread(target=getsongs, args=([i, id, name])) + t.daemon = True + t.start() - y_run += self.h + while statuses.count(2) != len(statuses): + time.sleep(0.1) - 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] + for sset in songsets: + for nt, name, song_id, rating in sset: - # 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: + id = pctl.master_count - # 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 + replace_existing = False + ex = existing.get(song_id) + if ex is not None: + id = ex + replace_existing = True - 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 + nt.index = id + pctl.master_library[id] = nt + if not replace_existing: + pctl.master_count += 1 - fx = self.deco() + playlist.append(nt.index) - minY = window_size[1] - self.h * len(self.subs[self.sub_active]) - 15 * gui.scale - sub_pos[1] = min(sub_pos[1], minY) + if star_store.get_rating(nt.index) == 0 and rating == 0: + pass + else: + star_store.set_rating(nt.index, rating * 2) - xoff = 0 - for i in self.subs[self.sub_active]: - if i.icon is not None: - xoff = 24 * gui.scale - break + self.scanning = False + if return_list: + return playlist - for w in range(len(self.subs[self.sub_active])): + 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) - 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 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) - # 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() +class KoelService: - # Item background - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (sub_w, self.h), fx[1]) + def __init__(self) -> None: + self.connected: bool = False + self.resource = None + self.scanning: bool = False + self.server: str = "" - # 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 + self.token: str = "" - # Call Callback - if self.clicked and not self.is_item_disabled(self.subs[self.sub_active][w]): + def connect(self) -> None: - # If callback needs args - if self.subs[self.sub_active][w].args is not None: - self.subs[self.sub_active][w].func(self.reference, self.subs[self.sub_active][w].args) - - # If callback just need ref - elif self.subs[self.sub_active][w].pass_ref: - self.subs[self.sub_active][w].func(self.reference) - - else: - self.subs[self.sub_active][w].func() - - if fx[2] is not None: - label = fx[2] - else: - label = self.subs[self.sub_active][w].title - - # Show text as disabled if disable_test() passes - if self.is_item_disabled(self.subs[self.sub_active][w]): - fx[0] = colours.menu_text_disabled + 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 - # 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) + if self.token: + self.connected = True + logging.info("Already authorised") + return - # 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) + password = prefs.koel_password + username = prefs.koel_username + server = prefs.koel_server_url + self.server = server - # Draw tab - ddt.rect_a((sub_pos[0], sub_pos[1] + w * self.h), (4 * gui.scale, self.h), colours.menu_tab) + target = server + "/api/me" - # Render the menu outline - # ddt.rect_a(sub_pos, (sub_w, self.h * len(self.subs[self.sub_active])), colours.grey(40)) + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + body = { + "email": username, + "password": password, + } - # Process Click Actions - if to_call is not 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 - if not self.is_item_disabled(self.items[to_call]): - if self.items[to_call].pass_ref: - self.items[to_call].func(self.reference) - else: - self.items[to_call].func() + if r.status_code == 200: + # logging.info(r.json()) + self.token = r.json()["token"] + if self.token: + logging.info("GOT KOEL TOKEN") + self.connected = True - if self.clicked or key_esc_press or self.close_next_frame: - self.close_next_frame = False - self.active = False - self.clicked = False + else: + logging.info("AUTH ERROR") - last_click_location[0] = 0 - last_click_location[1] = 0 + else: + error = "" + j = r.json() + if "message" in j: + error = j["message"] - for menu in Menu.instances: - if menu.active: - break - else: - Menu.active = False + gui.show_message(_("Could not establish connection/authorisation"), error, mode="error") - # 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 resolve_stream(self, id: str) -> tuple[str, dict[str, str]]: - Menu.active = True + if not self.connected: + self.connect() - if position != None: - self.pos = [position[0], position[1]] + if prefs.network_stream_bitrate > 0: + target = f"{self.server}/api/{id}/play/1/{prefs.network_stream_bitrate}" else: - self.pos = [copy.deepcopy(mouse_position[0]), copy.deepcopy(mouse_position[1])] + target = f"{self.server}/api/{id}/play/0/0" + params = {"jwt-token": self.token } - self.reference = in_reference - Menu.switch = self.id - self.sub_active = -1 + # if prefs.network_stream_bitrate > 0: + # target = f"{self.server}/api/play/{id}/1/{prefs.network_stream_bitrate}" + # else: + #target = f"{self.server}/api/play/{id}/0/0" + #target = f"{self.server}/api/{id}/play" - # 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) + #params = {"token": self.token, } - # 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 + #target = f"{self.server}/api/download/songs" + #params["songs"] = [id,] + logging.info(target) + logging.info(urllib.parse.urlencode(params)) - # Flip menu up if would intersect with bottom of window - if self.pos[1] + full_h > window_size[1]: - self.pos[1] -= shown_h + return target, params - # Prevent moving outside top of window - if self.pos[1] < gui.panelY: - self.pos[1] = gui.panelY - self.pos[0] += 5 * 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", + } - self.active = True + 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") -class GallClass: - def __init__(self, size=250, save_out=True): - self.gall = {} - self.size = size - self.queue = [] - self.key_list = [] - self.save_out = save_out - self.i = 0 - self.lock = threading.Lock() - self.limit = 60 + def get_albums(self, return_list: bool = False) -> list[int] | None: - def get_file_source(self, track_object: TrackClass): + gui.update += 1 + self.scanning = True - global album_art_gen + if not self.connected: + self.connect() - sources = album_art_gen.get_sources(track_object) + if not self.connected: + self.scanning = False + return [] - if len(sources) == 0: - return False, 0 + playlist = [] - offset = album_art_gen.get_offset(track_object.fullpath, sources) - return sources[offset], offset + target = self.server + "/api/data" + headers = { + "Authorization": "Bearer " + self.token, + "Accept": "application/json", + "Content-Type": "application/json", + } - def worker_render(self): + r = requests.get(target, headers=headers, timeout=10) + data = r.json() - self.lock.acquire() - # time.sleep(0.1) + artists = data["artists"] + albums = data["albums"] + songs = data["songs"] - if 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() - gui.update += 1 + artist_ids = {} + for artist in artists: + id = artist["id"] + if id not in artist_ids: + artist_ids[id] = artist["name"] - while len(self.queue) > 0: + 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"] - source_image = None + existing = {} - if gui.halt_image_rendering: - self.queue.clear() - 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 - self.i += 1 + for song in songs: - try: - # key = self.queue[0] - key = self.queue.pop(0) - except Exception: - logging.exception("thumb queue empty") - break + id = pctl.master_count + replace_existing = False - if key not in self.gall: - order = [1, None, None, None] - self.gall[key] = order - else: - order = self.gall[key] + e = existing.get(song["id"]) + if e is not None: + id = e + replace_existing = True - size = key[1] + nt = TrackClass() - slow_load = False - cache_load = False + 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"]) - try: + 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 - 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 + nt.art_url_key = covers.get(song["album_id"], "") + nt.url_key = song["id"] - if slow_load: + nt.is_network = True + nt.file_ext = "KOEL" - source, c_offset = self.get_file_source(key[0]) + pctl.master_library[id] = nt - if source is False: - order[0] = 0 - self.gall[key] = order - # del self.queue[0] - continue + if not replace_existing: + pctl.master_count += 1 - img_name = str(key[2]) + "-" + str(size) + "-" + str(key[0].index) + "-" + str(c_offset) + playlist.append(nt.index) - # gall_render_last_timer.set() + self.scanning = False - 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 + if return_list: + return playlist - # 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) + 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) - g = io.BytesIO() - g.seek(0) +class TauService: + def __init__(self) -> None: + self.processing = False - if cache_load: - g.write(source_image.read()) + def resolve_stream(self, key: str) -> str: + return "http://" + prefs.sat_url + ":7814/api1/file/" + key - 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 + def resolve_picture(self, key: str) -> str: + return "http://" + prefs.sat_url + ":7814/api1/pic/medium/" + key - im.save(g, "BMP") + 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 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) + def get_playlist(self, playlist_name: str | None = None, return_list: bool = False) -> list[int] | None: - g.seek(0) + p = self.get("playlists") - # source_image.close() + if not p or not p["playlists"]: + self.processing = False + return [] - order = [2, g, None, None] - self.gall[key] = order + if playlist_name is None: + playlist_name = text_sat_playlist.text.strip() + if not playlist_name: + show_message(_("No playlist name")) + return [] - gui.update += 1 - if source_image: - source_image.close() - source_image = None - # del self.queue[0] + id = None + name = "" + for pp in p["playlists"]: + if pp["name"].lower() == playlist_name.lower(): + id = pp["id"] + name = pp["name"] - time.sleep(0.001) + if id is None: + show_message(_("Playlist not found on target"), mode="error") + self.processing = False + return [] - 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] + try: + t = self.get("tracklist/" + id) + except Exception: + logging.exception("error getting tracklist") + return [] + at = t["tracks"] - if size < 150: - random.shuffle(self.queue) + exist = {} + for k, v in pctl.master_library.items(): + if v.is_network and v.file_ext == "TAU": + exist[v.url_key] = k - if self.i > 0: - self.i = 0 - return True - return False + playlist = [] + for item in at: + replace_existing = True - def render(self, track: TrackClass, location, size=None, force_offset=None) -> bool | None: - if gallery_load_delay.get() < 0.5: - return None + tid = item["id"] + id = exist.get(str(tid)) + if id is None: + id = pctl.master_count + replace_existing = False - x = round(location[0]) - y = round(location[1]) + 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) - # time.sleep(0.1) - if size is None: - size = self.size + 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) - size = round(size) + nt.url_key = str(tid) + nt.art_url_key = str(tid) - # 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 + nt.is_network = True + nt.file_ext = "TAU" + pctl.master_library[id] = nt - if force_offset is not None: - offset = force_offset + if not replace_existing: + pctl.master_count += 1 + playlist.append(nt.index) - key = (track, size, offset) + if return_list: + self.processing = False + return playlist - if key in self.gall: - #logging.info("old") + 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 - order = self.gall[key] +class STray: - if order[0] == 0: - # broken - return False + def __init__(self) -> None: + self.active = False - if order[0] == 1: - # not done yet - return False + def up(self, systray: SysTrayIcon): + SDL_ShowWindow(t_window) + SDL_RaiseWindow(t_window) + SDL_RestoreWindow(t_window) + gui.lowered = False - if order[0] == 2: - # finish processing + def down(self) -> None: + if self.active: + SDL_HideWindow(t_window) - 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 advance(self, systray: SysTrayIcon) -> None: + pctl.advance() + def back(self, systray: SysTrayIcon) -> None: + pctl.back() - order[0] = 3 - order[1].close() - order[1] = None - order[2] = c - order[3] = dst - self.gall[(track, size, offset)] = order + def pause(self, systray: SysTrayIcon) -> None: + pctl.play_pause() - if order[0] == 3: - # ready + def track_stop(self, systray: SysTrayIcon) -> None: + pctl.stop() - 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 on_quit_callback(self, systray: SysTrayIcon) -> None: + tauon.exit("Exit called from tray.") - if (track, size, offset) in self.key_list: - self.key_list.remove((track, size, offset)) - self.key_list.append((track, size, offset)) - - # Remove old images to conserve RAM usage - if len(self.key_list) > self.limit: - gui.update += 1 - key = self.key_list[0] - # while key in self.queue: - # self.queue.remove(key) - if self.gall[key][2] is not None: - SDL_DestroyTexture(self.gall[key][2]) - del self.gall[key] - del self.key_list[0] + 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 True + def stop(self) -> None: + self.systray.shutdown() + self.active = False - else: - if key not in self.queue: - self.queue.append(key) - if self.lock.locked(): - try: - self.lock.release() - except RuntimeError as e: - if str(e) == "release unlocked lock": - logging.error("RuntimeError: Attempted to release already unlocked lock") - else: - logging.exception("Unknown RuntimeError trying to release lock") - except Exception: - logging.exception("Unknown error trying to release lock") - return False +class GStats: + def __init__(self): -class ThumbTracks: - def __init__(self) -> None: - pass + self.last_db = 0 + self.last_pl = 0 + self.artist_list = [] + self.album_list = [] + self.genre_list = [] + self.genre_dict = {} - def path(self, track: TrackClass) -> str: - source, offset = tauon.gall_ren.get_file_source(track) + def update(self, playlist): - if source is False: # No art - return None + pt = 0 - image_name = track.album + track.parent_folder_path + str(offset) - image_name = hashlib.md5(image_name.encode("utf-8", "replace")).hexdigest() + if pctl.master_count != self.last_db or self.last_pl != playlist: + self.last_db = pctl.master_count + self.last_pl = playlist - t_path = os.path.join(e_cache_dir, image_name + ".jpg") + artists = {} - if os.path.isfile(t_path): - return t_path + for index in pctl.multi_playlist[playlist].playlist_ids: + artist = pctl.master_library[index].artist - source_image = album_art_gen.get_source_raw(0, 0, track, subsource=source) + if artist == "": + artist = "" - 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 + pt = int(star_store.get(index)) + if pt < 30: + continue -class Tauon: - """Root class for everything Tauon""" - def __init__(self): + if artist in artists: + artists[artist] += pt + else: + artists[artist] = pt - 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() + art_list = artists.items() - #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 + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) - self.tray_lock = threading.Lock() - self.tray_releases = 0 + self.artist_list = copy.deepcopy(sorted_list) - self.play_lock = None - self.update_play_lock = None - self.sleep_lock = None - self.shutdown_lock = None - self.quick_close = False + genres = {} + genre_dict = {} - self.copied_track = None - self.macos = macos - self.aud: CDLL | None = None + for index in pctl.multi_playlist[playlist].playlist_ids: + genre_r = pctl.master_library[index].genre - self.recorded_songs = [] + pt = int(star_store.get(index)) - self.chrome_mode = False - self.web_running = False - self.web_thread = None - self.remote_limited = True - self.enable_librespot = shutil.which("librespot") + 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] - #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 + pt = int(pt / len(gn)) - self.gme_formats = GME_Formats + for genre in gn: - self.spot_ctl: SpotCtl = SpotCtl(self) - self.tidal: Tidal = Tidal(self) - self.chrome: Chrome | None = None - self.chrome_menu: Menu | None = None + 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" - self.tls_context = tls_context + genre = genre.title() - def start_remote(self) -> None: + if len(genre) == 3 and genre[2] == "m": + genre = genre.upper() - 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 genre in genres: - 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 + genres[genre] += pt + else: + genres[genre] = pt - 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)) + if genre in genre_dict: + genre_dict[genre].append(index) + else: + genre_dict[genre] = [index] - except Exception as e: - logging.exception("Download failed") - show_message(_("Download failed"), str(e), mode="error") + art_list = genres.items() + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) - 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()) + self.genre_list = copy.deepcopy(sorted_list) + self.genre_dict = genre_dict - 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()) + # logging.info('\n-----------------------\n') - exe.close() - show_message(_("FFMPEG fetch complete"), mode="done") + g_albums = {} - shooter(go) + for index in pctl.multi_playlist[playlist].playlist_ids: + album = pctl.master_library[index].album - def set_tray_icons(self, force: bool = False): + if album == "": + album = "" - 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") + pt = int(star_store.get(index)) - 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 pt < 30: + continue - 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 album in g_albums: + g_albums[album] += pt + else: + g_albums[album] = pt - if not user_icon_dir.is_dir(): - os.makedirs(user_icon_dir) + art_list = g_albums.items() - 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") + sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) - def get_tray_icon(self, name: str) -> str: - return str(self.cache_directory / "icon-export" / f"{name}.svg") + self.album_list = copy.deepcopy(sorted_list) - 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 +class Drawing: - def get_ffmpeg(self) -> str | None: - path = user_directory / "ffmpeg.exe" - if msys and path.is_file(): - return str(path) + 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=""): - # macOS - path = install_directory / "ffmpeg" - if path.is_file(): - return str(path) + if w is None: + w = ddt.get_text_w(text, font) + 18 * gui.scale + if h is None: + h = 22 * gui.scale - logging.debug(f"Looking for ffmpeg in PATH: {os.environ.get('PATH')}") - path = shutil.which("ffmpeg") - if path: - return path - return None + rect = (x, y, w, h) + fields.add(rect) - def get_ffprobe(self) -> str | None: - path = user_directory / "ffprobe.exe" - if msys and path.is_file(): - return str(path) + 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 - # macOS - path = install_directory / "ffprobe" - if path.is_file(): - return str(path) + click = False - logging.debug(f"Looking for ffprobe in PATH: {os.environ.get('PATH')}") - path = shutil.which("ffprobe") - if path: - return path - return None + if press is None: + press = inp.mouse_click - def bg_save(self) -> None: - self.worker_save_state = True - tauon.thread_manager.ready("worker") + if coll(rect): + if tooltip: + tool_tip.test(x + 15 * gui.scale, y - 28 * gui.scale, tooltip) + ddt.rect(rect, background_highlight_colour) - def exit(self, reason: str) -> None: - logging.info("Shutting down. Reason: " + reason) - pctl.running = False - self.wake() + # if background_highlight_colour[3] != 255: + # background_highlight_colour = None - def min_to_tray(self) -> None: - SDL_HideWindow(t_window) - gui.mouse_unknown = 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 - def raise_window(self) -> None: - SDL_ShowWindow(t_window) - SDL_RaiseWindow(t_window) - SDL_RestoreWindow(t_window) - gui.lowered = False - gui.update += 1 +class DropShadow: - def focus_window(self) -> None: - SDL_RaiseWindow(t_window) + def __init__(self): + self.readys = {} + self.underscan = int(15 * gui.scale) + self.radius = 4 + self.grow = 2 * gui.scale + self.opacity = 90 - def get_playing_playlist_id(self) -> int: - return pl_to_id(pctl.active_playlist_playing) + def prepare(self, w, h): + fh = h + self.underscan + fw = w + self.underscan - def wake(self) -> None: - SDL_PushEvent(ctypes.byref(self.dummy_event)) + 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") -def signal_handler(signum, frame): - signal.signal(signum, signal.SIG_IGN) # ignore additional signals - tauon.exit(reason="SIGINT recieved") + im = im.filter(ImageFilter.GaussianBlur(self.radius)) -class PlexService: + g = io.BytesIO() + g.seek(0) + im.save(g, "PNG") + g.seek(0) - def __init__(self): - self.connected = False - self.resource = None - self.scanning = False + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + c = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_SetTextureAlphaMod(c, self.opacity) - def connect(self): + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(c, None, None, tex_w, tex_h) - 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 + dst = SDL_Rect(0, 0) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) - 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 + SDL_FreeSurface(s_image) + g.close() + im.close() - 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 + unit = (dst, c) + self.readys[(w, h)] = unit - # from plexapi.server import PlexServer - # baseurl = 'http://localhost:32400' - # token = '' + def render(self, x, y, w, h): + if (w, h) not in self.readys: + self.prepare(w, h) - # self.resource = PlexServer(baseurl, token) + 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]) - self.connected = True +class LyricsRenMini: - def resolve_stream(self, location): - logging.info("Get plex stream") - if not self.connected: - self.connect() + def __init__(self): + self.index = -1 + self.text = "" - # return self.resource.url(location, True) - return self.resource.library.fetchItem(location).getStreamURL() + self.lyrics_position = 0 - def resolve_thumbnail(self, location): + def generate(self, index, w): + self.text = pctl.master_library[index].lyrics + self.lyrics_position = 0 - if not self.connected: - self.connect() - if self.connected: - return self.resource.url(location, True) - return None + 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) - def get_albums(self, return_list=False): + colour = colours.side_bar_line1 - gui.update += 1 - self.scanning = True + # if key_ctrl_down: + # if mouse_wheel < 0: + # prefs.lyrics_font_size += 1 + # if mouse_wheel > 0: + # prefs.lyrics_font_size -= 1 - if not self.connected: - self.connect() + ddt.text((x, y, 4, w), self.text, colour, prefs.lyrics_font_size, w - (w % 2), colours.side_panel_background) - if not self.connected: - self.scanning = False - return [] +class LyricsRen: - playlist = [] + def __init__(self): - 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 + self.index = -1 + self.text = "" - albums = self.resource.library.section("Music").albums() - gui.to_got = 0 + self.lyrics_position = 0 - for album in albums: - year = album.year - album_artist = album.parentTitle - album_title = album.title + def test_update(self, track_object: TrackClass): - parent = (album_artist + " - " + album_title).strip("- ") + 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 - for track in album.tracks(): + def render(self, x, y, w, h, p): - if not track.duration: - logging.warning("Skipping track with invalid duration - " + track.title + " - " + track.grandparentTitle) - continue + colour = colours.lyrics + if test_lumi(colours.gallery_background) < 0.5: + colour = colours.grey(40) - id = pctl.master_count - replace_existing = False + ddt.text((x, y, 4, w), self.text, colour, 17, w, colours.playlist_panel_background) - e = existing.get(track.key) - if e is not None: - id = e - replace_existing = True +class TimedLyricsToStatic: - title = track.title - track_artist = track.grandparentTitle - duration = track.duration / 1000 + def __init__(self): + self.cache_key = None + self.cache_lyrics = "" - 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(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) - nt.is_network = True + if data is None: + self.cache_lyrics = "" + self.cache_key = track + return "" + text = "" - if track.thumb: - nt.art_url_key = track.thumb + for line in data: + if len(line) < 10: + continue - nt.url_key = track.key - nt.date = str(year) + if line[0] != "[" or line[9] != "]" or ":" not in line or "." not in line: + continue - pctl.master_library[id] = nt + text += line.split("]")[-1].rstrip("\n") + "\n" - if not replace_existing: - pctl.master_count += 1 + self.cache_lyrics = text + self.cache_key = track + return text - playlist.append(nt.index) +class TimedLyricsRen: - gui.to_got += 1 - gui.update += 1 - gui.pl_update += 1 + def __init__(self): - self.scanning = False + self.index = -1 - if return_list: - return playlist + self.scanned = {} + self.ready = False + self.data = [] - 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) + self.scroll_position = 0 -class SubsonicService: + def generate(self, track: TrackClass) -> bool | None: - def __init__(self): - self.scanning = False - self.playlists = prefs.subsonic_playlists + if self.index == track.index: + return self.ready - def r(self, point, p=None, binary: bool = False, get_url: bool = False): - salt = secrets.token_hex(8) - server = prefs.subsonic_server.rstrip("/") + "/" + self.ready = False + self.index = track.index + self.scroll_position = 0 + self.data.clear() - params = { - "u": prefs.subsonic_user, - "v": "1.13.0", - "c": t_title, - "f": "json", - } + data = find_synced_lyric_data(track) + if data is None: + return None - if prefs.subsonic_password_plain: - params["p"] = prefs.subsonic_password - else: - params["t"] = hashlib.md5((prefs.subsonic_password + salt).encode()).hexdigest() - params["s"] = salt + for line in data: + if len(line) < 10: + continue - if p: - params.update(p) + if line[0] != "[" or "]" not in line or ":" not in line or "." not in line: + continue - point = "rest/" + point + try: - url = server + point + text = line.split("]")[-1].rstrip("\n") + t = line - if get_url: - return url, params + while t[0] == "[" and t[9] == "]" and ":" in t and "." in t: - response = requests.get(url, params=params, timeout=10) + a = t.lstrip("[") + t = t.split("]")[1] + "]" - if binary: - return response.content + a = a.split("]")[0] + mm, b = a.split(":") + ss, ms = b.split(".") - d = json.loads(response.text) - # logging.info(d) + s = int(mm) * 60 + int(ss) + if len(ms) == 2: + s += int(ms) / 100 + elif len(ms) == 3: + s += int(ms) / 1000 - if d["subsonic-response"]["status"] != "ok": - show_message(_("Subsonic Error: ") + response.text, mode="warning") - logging.error("Subsonic Error: " + response.text) + self.data.append((s, text)) - return d + if len(t) < 10: + break + except Exception: + logging.exception("Failed generating timed lyrics") + continue - def get_cover(self, track_object: TrackClass): - response = self.r("getCoverArt", p={"id": track_object.art_url_key}, binary=True) - return io.BytesIO(response) + self.data = sorted(self.data, key=lambda x: x[0]) + # logging.info(self.data) - def resolve_stream(self, key): + self.ready = True + return True - p = {"id": key} - if prefs.network_stream_bitrate > 0: - p["maxBitRate"] = prefs.network_stream_bitrate + def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = 0, h: int = 0) -> bool | None: - return self.r("stream", p={"id": key}, get_url=True) - # logging.info(response.content) + if index != self.index: + self.ready = False + self.generate(pctl.master_library[index]) - def listen(self, track_object: TrackClass, submit: bool = False): + if right_click and x and y and coll((x, y, w, h)): + showcase_menu.activate(pctl.master_library[index]) - 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 + if not self.ready: + return False - def set_rating(self, track_object: TrackClass, rating): + 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) - 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 + line_active = -1 + last = -1 - def set_album_rating(self, track_object: TrackClass, rating): - id = track_object.misc.get("subsonic-folder-id") - if id is not None: - try: - a = self.r("setRating", p={"id": id, "rating": math.ceil(rating / 2)}) - except Exception: - logging.exception("Error connect for set rating on airsonic") - return True + highlight = True - def get_music3(self, return_list: bool = False): + 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) - self.scanning = True - gui.to_got = 0 + test_time = get_real_time() - existing = {} + if pctl.track_queue[pctl.queue_step] == index: - for track_id, track in pctl.master_library.items(): - if track.is_network and track.file_ext == "SUB": - existing[track.url_key] = track_id + for i, line in enumerate(self.data): + if line[0] < test_time: + last = i - 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 line[0] > test_time: + pctl.wake_past_time = line[0] + line_active = last + break + else: + line_active = len(self.data) - 1 - b = a["subsonic-response"]["indexes"]["index"] + if pctl.playing_state == 1: + self.scroll_position = (max(0, line_active)) * spacing * -1 - folders = [] + yy = y + self.scroll_position - for letter in b: - artists = letter["artist"] - for artist in artists: - folders.append(( - artist["id"], - artist["name"], - )) + for i, line in enumerate(self.data): - playlist = [] + if 0 < yy < window_size[1]: - songsets = [] - for i in range(len(folders)): - songsets.append([]) - statuses = [0] * len(folders) - dupes = [] + colour = colours.lyrics + if test_lumi(colours.gallery_background) < 0.5: + colour = colours.grey(40) - def getsongs(index, folder_id, name: str, inner: bool = False, parent=None): + if i == line_active and highlight: + colour = [255, 210, 50, 255] + if colours.lm: + colour = [180, 130, 210, 255] - try: - d = self.r("getMusicDirectory", p={"id": folder_id}) - if "child" not in d["subsonic-response"]["directory"]: - if not inner: - statuses[index] = 2 - return + 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 - 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") +class TextBox2: + cursor = True - items = d["subsonic-response"]["directory"]["child"] + def __init__(self) -> None: - gui.update = 2 + self.text: str = "" + self.cursor_position = 0 + self.selection = 0 + self.offset = 0 + self.down_lock = False + self.paste_text = "" - for item in items: + def paste(self) -> None: - if item["isDir"]: + if SDL_HasClipboardText(): + clip = SDL_GetClipboardText().decode("utf-8") + self.paste_text = clip - 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 copy(self) -> None: - getsongs(index, item["id"], item["title"], inner=True, parent=item) - continue + text = self.get_selection() + if not text: + text = self.text + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) - gui.to_got += 1 - song = item - nt = TrackClass() + def set_text(self, text: str) -> None: - if parent and "artist" in parent: - nt.album_artist = parent["artist"] + self.text = text + if self.cursor_position > len(text): + self.cursor_position = 0 + self.selection = 0 + else: + self.selection = self.cursor_position - 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"] + def clear(self) -> None: + self.text = "" + #self.cursor_position = 0 + self.selection = self.cursor_position - 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 + def highlight_all(self) -> None: - rating = 0 - if "userRating" in song: - rating = int(song["userRating"]) + self.selection = len(self.text) + self.cursor_position = 0 - songsets[index].append((nt, name, song["id"], rating)) + 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 inner: - return - statuses[index] = 2 + 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] - i = -1 - for id, name in folders: - i += 1 - while statuses.count(1) > 3: - time.sleep(0.1) + 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):] - statuses[i] = 1 - t = threading.Thread(target=getsongs, args=([i, id, name])) - t.daemon = True - t.start() + else: + return "" - while statuses.count(2) != len(statuses): - time.sleep(0.1) + def draw( + self, x, y, colour, active=True, secret=False, font=13, width=0, click=False, selection_height=18, big=False): - for sset in songsets: - for nt, name, song_id, rating in sset: + # 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 - id = pctl.master_count + SDL_SetRenderTarget(renderer, text_box_canvas) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - replace_existing = False - ex = existing.get(song_id) - if ex is not None: - id = ex - replace_existing = True + text_box_canvas_rect.x = 0 + text_box_canvas_rect.y = 0 + SDL_RenderFillRect(renderer, text_box_canvas_rect) - nt.index = id - pctl.master_library[id] = nt - if not replace_existing: - pctl.master_count += 1 + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - playlist.append(nt.index) + selection_height *= gui.scale - if star_store.get_rating(nt.index) == 0 and rating == 0: - pass - else: - star_store.set_rating(nt.index, rating * 2) + if click is False: + click = inp.mouse_click + if mouse_down: + gui.update = 2 # TODO: more elegant fix - self.scanning = False - if return_list: - return playlist + 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) - 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) + fields.add(rect) - # def get_music2(self, return_list=False): - # - # self.scanning = True - # gui.to_got = 0 - # - # existing = {} - # - # for track_id, track in pctl.master_library.items(): - # if track.is_network and track.file_ext == "SUB": - # existing[track.url_key] = track_id - # - # try: - # a = self.r("getIndexes") - # except Exception: - # show_message(_("Error connecting to Airsonic server"), mode="error") - # self.scanning = False - # return [] - # - # b = a["subsonic-response"]["indexes"]["index"] - # - # folders = [] - # - # for letter in b: - # artists = letter["artist"] - # for artist in artists: - # folders.append(( - # artist["id"], - # artist["name"] - # )) - # - # playlist = [] - # - # def get(folder_id, name): - # - # try: - # d = self.r("getMusicDirectory", p={"id": folder_id}) - # if "child" not in d["subsonic-response"]["directory"]: - # return - # - # except json.decoder.JSONDecodeError: - # logging.error("Error reading Airsonic directory") - # show_message(_("Error reading Airsonic directory!)", mode="warning") - # return - # - # items = d["subsonic-response"]["directory"]["child"] - # - # gui.update = 1 - # - # for item in items: - # - # gui.to_got += 1 - # - # if item["isDir"]: - # get(item["id"], item["title"]) - # continue - # - # song = item - # id = pctl.master_count - # - # replace_existing = False - # ex = existing.get(song["id"]) - # if ex is not None: - # id = ex - # replace_existing = True - # - # nt = TrackClass() - # - # if "title" in song: - # nt.title = song["title"] - # if "artist" in song: - # nt.artist = song["artist"] - # if "album" in song: - # nt.album = song["album"] - # if "track" in song: - # nt.track_number = song["track"] - # if "year" in song: - # nt.date = str(song["year"]) - # if "duration" in song: - # nt.length = song["duration"] - # - # # if "bitRate" in song: - # # nt.bitrate = song["bitRate"] - # - # nt.file_ext = "SUB" - # - # nt.index = id - # - # nt.parent_folder_name = name - # if "path" in song: - # nt.fullpath = song["path"] - # nt.parent_folder_path = os.path.dirname(song["path"]) - # - # if "coverArt" in song: - # nt.art_url_key = song["id"] - # - # nt.url_key = song["id"] - # nt.is_network = True - # - # pctl.master_library[id] = nt - # - # if not replace_existing: - # pctl.master_count += 1 - # - # playlist.append(nt.index) - # - # for id, name in folders: - # get(id, name) - # - # self.scanning = False - # if return_list: - # return playlist - # - # pctl.multi_playlist.append(pl_gen(title="Airsonic Collection", playlist_ids=playlist)) - # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "air" - # switch_playlist(len(pctl.multi_playlist) - 1) - -class KoelService: + # Activate Menu + if coll(rect): + if right_click or level_2_right_click: + field_menu.activate(self) - def __init__(self) -> None: - self.connected: bool = False - self.resource = None - self.scanning: bool = False - self.server: str = "" + if width > 0 and active: - self.token: str = "" + if click and field_menu.active: + # field_menu.click() + click = False - def connect(self) -> None: + # 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:] - 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 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 self.token: - self.connected = True - logging.info("Already authorised") - return + def g2(): + if len(self.text) == 0 or self.cursor_position == 0: + return None + return self.text[len(self.text) - self.cursor_position] - password = prefs.koel_password - username = prefs.koel_username - server = prefs.koel_server_url - self.server = server + 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 - target = server + "/api/me" + # 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() - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - } - body = { - "email": username, - "password": password, - } + # 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 - 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 + # 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 - 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 + # 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() - else: - logging.info("AUTH ERROR") + # 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 - else: - error = "" - j = r.json() - if "message" in j: - error = j["message"] + 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 - gui.show_message(_("Could not establish connection/authorisation"), error, mode="error") + if self.paste_text: + if "http://" in self.text and "http://" in self.paste_text: + self.text = "" + self.paste_text = self.paste_text.rstrip(" ").lstrip(" ") + self.paste_text = self.paste_text.replace("\n", " ").replace("\r", "") - def resolve_stream(self, id: str) -> tuple[str, dict[str, str]]: + 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 not self.connected: - self.connect() + # Paste via ctrl-v + if key_ctrl_down and key_v_press: + clip = SDL_GetClipboardText().decode("utf-8") + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( + self.text) - self.cursor_position:] - if 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 key_ctrl_down and key_c_press: + self.copy() - # 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" + 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() - #params = {"token": self.token, } + if key_ctrl_down and key_a_press: + self.cursor_position = 0 + self.selection = len(self.text) - #target = f"{self.server}/api/download/songs" - #params["songs"] = [id,] - logging.info(target) - logging.info(urllib.parse.urlencode(params)) + # ddt.rect(rect, [255, 50, 50, 80], True) + if coll(rect) and not field_menu.active: + gui.cursor_want = 2 - return target, params + # 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 - 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 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 - 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") + 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 - def get_albums(self, return_list: bool = False) -> list[int] | None: + if cursor_x < self.offset: + self.offset = cursor_x - round(15 * gui.scale) - gui.update += 1 - self.scanning = True + self.offset = max(self.offset, 0) + else: + self.offset = 0 - if not self.connected: - self.connect() + x -= self.offset - if not self.connected: - self.scanning = False - return [] + 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) - playlist = [] + 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 - target = self.server + "/api/data" - headers = { - "Authorization": "Bearer " + self.token, - "Accept": "application/json", - "Content-Type": "application/json", - } + 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: - r = requests.get(target, headers=headers, timeout=10) - data = r.json() + for i in range(len(text)): + post = ddt.get_text_w(text[0:i + 1], font) + # pre_half = int((post - pre) / 2) - artists = data["artists"] - albums = data["albums"] - songs = data["songs"] + if x + pre - 0 <= mouse_position[0] <= x + post + 0: + diff = post - pre - artist_ids = {} - for artist in artists: - id = artist["id"] - if id not in artist_ids: - artist_ids[id] = artist["name"] + if mouse_position[0] >= x + pre + int(diff / 2): + self.selection = len(text) - i - 1 - 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"] + else: + self.selection = len(text) - i - existing = {} + break + pre = post - for track_id, track in pctl.master_library.items(): - if track.is_network and track.file_ext == "KOEL": - existing[track.url_key] = track_id + else: + self.selection = 0 - for song in songs: + text = self.text[0: len(self.text) - self.cursor_position] + if secret: + text = "●" * len(text) + a = ddt.get_text_w(text, font) - id = pctl.master_count - replace_existing = False + text = self.text[0: len(self.text) - self.selection] + if secret: + text = "●" * len(text) + b = ddt.get_text_w(text, font) - e = existing.get(song["id"]) - if e is not None: - id = e - replace_existing = True + top = y + if big: + top -= 12 * gui.scale - nt = TrackClass() + ddt.rect([a, 0, b - a, selection_height], [40, 120, 180, 255]) - 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 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.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 + text = self.text[0: len(self.text) - self.cursor_position] + if secret: + text = "●" * len(text) + space = ddt.get_text_w(text, font) - nt.art_url_key = covers.get(song["album_id"], "") - nt.url_key = song["id"] + if TextBox.cursor and self.selection == self.cursor_position: + # ddt.line(x + space, y + 2, x + space, y + 15, colour) - nt.is_network = True - nt.file_ext = "KOEL" + ddt.rect((0 + space, 0 + 2, 1 * gui.scale, 14 * gui.scale), colour) - pctl.master_library[id] = nt + if click: + self.selection = self.cursor_position - if not replace_existing: - pctl.master_count += 1 + 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 - playlist.append(nt.index) + if active and editline != "" and editline != input_text: + ex = ddt.text((space + round(4 * gui.scale), 0), editline, [240, 230, 230, 255], font) + tw, th = ddt.get_text_wh(editline, font, max_x=2000) + ddt.rect((space + round(4 * gui.scale), th + round(2 * gui.scale), ex, round(1 * gui.scale)), [245, 245, 245, 255]) - self.scanning = False + 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 return_list: - return playlist + animate_monitor_timer.set() - 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) + text_box_canvas_hide_rect.x = 0 + text_box_canvas_hide_rect.y = 0 -class TauService: - def __init__(self) -> None: - self.processing = False + # if self.offset: + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - def resolve_stream(self, key: str) -> str: - return "http://" + prefs.sat_url + ":7814/api1/file/" + key + text_box_canvas_hide_rect.w = round(self.offset) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) - def resolve_picture(self, key: str) -> str: - return "http://" + prefs.sat_url + ":7814/api1/pic/medium/" + key + 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) - 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 + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + SDL_SetRenderTarget(renderer, gui.main_texture) - def get_playlist(self, playlist_name: str | None = None, return_list: bool = False) -> list[int] | None: + 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) - p = self.get("playlists") +class TextBox: + cursor = True - if not p or not p["playlists"]: - self.processing = False - return [] + def __init__(self) -> None: - if playlist_name is None: - playlist_name = text_sat_playlist.text.strip() - if not playlist_name: - show_message(_("No playlist name")) - return [] + self.text = "" + self.cursor_position = 0 + self.selection = 0 + self.down_lock = False - id = None - name = "" - for pp in p["playlists"]: - if pp["name"].lower() == playlist_name.lower(): - id = pp["id"] - name = pp["name"] + def paste(self) -> None: - if id is None: - show_message(_("Playlist not found on target"), mode="error") - self.processing = False - return [] + if SDL_HasClipboardText(): + clip = SDL_GetClipboardText().decode("utf-8") - try: - t = self.get("tracklist/" + id) - except Exception: - logging.exception("error getting tracklist") - return [] - at = t["tracks"] + if "http://" in self.text and "http://" in clip: + self.text = "" - exist = {} - for k, v in pctl.master_library.items(): - if v.is_network and v.file_ext == "TAU": - exist[v.url_key] = k + clip = clip.rstrip(" ").lstrip(" ") + clip = clip.replace("\n", " ").replace("\r", "") - playlist = [] - for item in at: - replace_existing = True + self.eliminate_selection() + self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( + self.text) - self.cursor_position:] - tid = item["id"] - id = exist.get(str(tid)) - if id is None: - id = pctl.master_count - replace_existing = False + def copy(self) -> None: - 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) + text = self.get_selection() + if not text: + text = self.text + if text != "": + SDL_SetClipboardText(text.encode("utf-8")) - 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) + def set_text(self, text): - nt.url_key = str(tid) - nt.art_url_key = str(tid) + self.text = text + self.cursor_position = 0 + self.selection = 0 - nt.is_network = True - nt.file_ext = "TAU" - pctl.master_library[id] = nt + def clear(self) -> None: + self.text = "" - if not replace_existing: - pctl.master_count += 1 - playlist.append(nt.index) + def highlight_all(self) -> None: - if return_list: - self.processing = False - return playlist + self.selection = len(self.text) + self.cursor_position = 0 - 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 + def highlight_none(self) -> None: + self.selection = 0 + self.cursor_position = 0 -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 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 - return None + 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] -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() + return self.text[len(self.text) - self.cursor_position: len(self.text) - self.selection] + if p == 0: + return self.text[0: len(self.text) - max(self.cursor_position, self.selection)] + if p == 2: + return self.text[len(self.text) - min(self.cursor_position, self.selection):] -def jellyfin_get_library_thread() -> None: - pref_box.close() - save_prefs() - if jellyfin.scanning: - inp.mouse_click = False - show_message(_("Job already in progress!")) - return + else: + return "" - jellyfin.scanning = True - shoot_dl = threading.Thread(target=jellyfin.ingest_library) - shoot_dl.daemon = True - shoot_dl.start() + def draw( + self, x: int, y: int, colour: list[int], active: bool = True, secret: bool = False, + font: int = 13, width: int = 0, click: bool = False, selection_height: int = 18, big: bool = False): -def 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 + # 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 - shoot_dl = threading.Thread(target=plex.get_albums) - shoot_dl.daemon = True - shoot_dl.start() + selection_height *= gui.scale -def sub_get_album_thread() -> None: - # if prefs.backend != 1: - # show_message("This feature is currently only available with the BASS backend") - # return + if click is False: + click = inp.mouse_click - pref_box.close() - save_prefs() - if subsonic.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - subsonic.scanning = True + if width > 0 and active: - shoot_dl = threading.Thread(target=subsonic.get_music3) - shoot_dl.daemon = True - shoot_dl.start() + 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) -def koel_get_album_thread() -> None: - # if prefs.backend != 1: - # show_message("This feature is currently only available with the BASS backend") - # return + # Activate Menu + if coll(rect): + if right_click or level_2_right_click: + field_menu.activate(self) - pref_box.close() - save_prefs() - if koel.scanning: - inp.mouse_click = False - show_message(_("Already scanning!")) - return - koel.scanning = True + if click and field_menu.active: + # field_menu.click() + click = False - shoot_dl = threading.Thread(target=koel.get_albums) - shoot_dl.daemon = True - shoot_dl.start() + # 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:] -class STray: + def g(): + if len(self.text) == 0 or self.cursor_position == len(self.text): + return None + return self.text[len(self.text) - self.cursor_position - 1] - def __init__(self) -> None: - self.active = False + def g2(): + if len(self.text) == 0 or self.cursor_position == 0: + return None + return self.text[len(self.text) - self.cursor_position] - def up(self, systray: SysTrayIcon): - SDL_ShowWindow(t_window) - SDL_RaiseWindow(t_window) - SDL_RestoreWindow(t_window) - gui.lowered = False + 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 - def down(self) -> None: - if self.active: - SDL_HideWindow(t_window) + # 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() - def advance(self, systray: SysTrayIcon) -> None: - pctl.advance() + # 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 back(self, systray: SysTrayIcon) -> None: - pctl.back() + # Ctrl + right to move cursor forward a word + elif (key_ctrl_down or key_rctrl_down) and key_right_press: + while g2() == " ": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + while g2() != None and g2() not in " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~": + self.cursor_position -= 1 + if not key_shift_down: + self.selection = self.cursor_position + if g2() == " ": + self.cursor_position += 1 + if not key_shift_down: + self.selection = self.cursor_position + break - def pause(self, systray: SysTrayIcon) -> None: - pctl.play_pause() + # 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() - def track_stop(self, systray: SysTrayIcon) -> None: - pctl.stop() + # 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 - def on_quit_callback(self, systray: SysTrayIcon) -> None: - tauon.exit("Exit called from tray.") + 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 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 + # 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:] - def stop(self) -> None: - self.systray.shutdown() - self.active = False + if key_ctrl_down and key_c_press: + self.copy() -class GStats: - def __init__(self): + 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() - self.last_db = 0 - self.last_pl = 0 - self.artist_list = [] - self.album_list = [] - self.genre_list = [] - self.genre_dict = {} + if key_ctrl_down and key_a_press: + self.cursor_position = 0 + self.selection = len(self.text) - def update(self, playlist): + # ddt.rect_r(rect, [255, 50, 50, 80], True) + if coll(rect) and not field_menu.active: + gui.cursor_want = 2 - pt = 0 + fields.add(rect) - if pctl.master_count != self.last_db or self.last_pl != playlist: - self.last_db = pctl.master_count - self.last_pl = playlist + # 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 - artists = {} + 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 - for index in pctl.multi_playlist[playlist].playlist_ids: - artist = pctl.master_library[index].artist + 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 artist == "": - artist = "" + 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 - pt = int(star_store.get(index)) - if pt < 30: - continue + if mouse_up: + self.down_lock = False + if self.down_lock: + pre = 0 + post = 0 + if mouse_position[0] < x + 1: - if artist in artists: - artists[artist] += pt + self.selection = len(self.text) else: - artists[artist] = pt - art_list = artists.items() + for i in range(len(self.text)): + post = ddt.get_text_w(self.text[0:i + 1], font) + # pre_half = int((post - pre) / 2) - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + if x + pre - 0 <= mouse_position[0] <= x + post + 0: + diff = post - pre - self.artist_list = copy.deepcopy(sorted_list) + if mouse_position[0] >= x + pre + int(diff / 2): + self.selection = len(self.text) - i - 1 - genres = {} - genre_dict = {} + else: + self.selection = len(self.text) - i - for index in pctl.multi_playlist[playlist].playlist_ids: - genre_r = pctl.master_library[index].genre + break + pre = post - pt = int(star_store.get(index)) + else: + self.selection = 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] + 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) - pt = int(pt / len(gn)) + b = ddt.get_text_w(self.text[0: len(self.text) - self.selection], font) - for genre in gn: + # rint((a, b)) - 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" + top = y + if big: + top -= 12 * gui.scale - genre = genre.title() + ddt.rect([x + a, top, b - a, selection_height], [40, 120, 180, 255]) - if len(genre) == 3 and genre[2] == "m": - genre = genre.upper() - - if genre in genres: - - genres[genre] += pt - else: - genres[genre] = pt - - if genre in genre_dict: - genre_dict[genre].append(index) - else: - genre_dict[genre] = [index] + 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) - art_list = genres.items() - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=True) + space = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) - self.genre_list = copy.deepcopy(sorted_list) - self.genre_dict = genre_dict + if TextBox.cursor and self.selection == self.cursor_position: + # ddt.line(x + space, y + 2, x + space, y + 15, colour) - # logging.info('\n-----------------------\n') + 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) - g_albums = {} + if click: + self.selection = self.cursor_position - for index in pctl.multi_playlist[playlist].playlist_ids: - album = pctl.master_library[index].album + else: + if active: + self.text += input_text + if input_text != "": + self.cursor = True - if album == "": - album = "" + while inp.backspace_press and len(self.text) > 0: + self.text = self.text[:-1] + inp.backspace_press -= 1 - pt = int(star_store.get(index)) + if key_ctrl_down and key_v_press: + self.paste() - if pt < 30: - continue + if secret: + space = ddt.text((x, y), "●" * len(self.text), colour, font) + else: + space = ddt.text((x, y), self.text, colour, font) - if album in g_albums: - g_albums[album] += pt + 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: - g_albums[album] = pt + ddt.rect((xx, yy, 1 * gui.scale, 14 * gui.scale), colour) - art_list = g_albums.items() + 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]) - sorted_list = sorted(art_list, key=lambda x: x[1], reverse=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) - self.album_list = copy.deepcopy(sorted_list) + animate_monitor_timer.set() -def do_exit_button() -> None: - if mouse_up or ab_click: - if gui.tray_active and prefs.min_to_tray: - if key_shift_down: - tauon.exit("User clicked X button with shift key") - return - tauon.min_to_tray() - elif gui.sync_progress and not gui.stop_sync: - show_message(_("Stop the sync before exiting!")) - else: - tauon.exit("User clicked X button") +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 = "" -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) +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 - mouse_down = False - inp.mouse_click = False - drag_mode = False + self.blur_texture = None + self.blur_rect = None + self.loaded_bg_type = 0 -def do_minimize_button(): + self.download_in_progress = False + self.downloaded_image = None + self.downloaded_track = None - 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) + self.base64cache = (0, 0, "") + self.processing64on = None - mouse_down = False - inp.mouse_click = False - drag_mode = False + self.bin_cached = (None, None, None) # track, subsource, bin -def draw_window_tools(): - global mouse_down - global drag_mode + self.embed_cached = (None, None) - # 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 + def async_download_image(self, track: TrackClass, subsource: list[tuple[int, str]]) -> None: - 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.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 - h = round(28 * gui.scale) - y = round(1 * gui.scale) - if macstyle: - y = round(9 * gui.scale) + def get_info(self, track_object: TrackClass) -> list[tuple[str, int, int, int, str]]: - 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 + sources = self.get_sources(track_object) + if len(sources) == 0: + return None - xx = 0 - l = prefs.left_window_control - r = not l - focused = window_is_focused() + offset = self.get_offset(track_object.fullpath, sources) - # Close - if r: - xx = window_size[0] - x_width - xx -= round(2 * gui.scale) + o_size = (0, 0) + format = "ERROR" - 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) + 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 - # 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) + 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 - 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 + return [sources[offset][0], len(sources), offset, o_size, format] - # maximize + def get_sources(self, tr: TrackClass) -> list[tuple[int, str]]: - 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) + filepath = tr.fullpath + ext = tr.file_ext - 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() + # Check if source list already exists, if not, make it + if tr.index in self.source_cache: + return self.source_cache[tr.index] - 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) + source_list: list[tuple[int, str]] = [] # istag, - # minimize + # Source type the is first element in list + # 0 = File + # 1 = Embedded in tag + # 2 = Network location - if draw_min_button: + 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 [] - # 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) + # 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") - 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 not tr.is_network: - else: - if r: - xx -= mi_width - if l: - xx += last_width + 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] - 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) + 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]) - # restore + for i in range(len(dirs_in_dir)): + subdirec = os.path.join(direc, dirs_in_dir[i]) + items_in_dir2 = os.listdir(subdirec) - if gui.mode == 3: + 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]) - # 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 + self.source_cache[tr.index] = source_list - 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) + return source_list -def draw_window_border(): - corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) + 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 - corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) - fields.add(corner_rect) + def fast_display(self, index, location, box, source: list[tuple[int, str]], offset) -> int: + """Renders cached image only by given size for faster performance""" - right_rect = (window_size[0] - 3 * gui.scale, 20 * gui.scale, 10, window_size[1] - 40 * gui.scale) - fields.add(right_rect) + found_unit = None + max_h = 0 - # top_rect = (20 * gui.scale, 0, window_size[0] - 40 * gui.scale, 2 * gui.scale) - # fields.add(top_rect) + 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 - left_rect = (0, 10 * gui.scale, 4 * gui.scale, window_size[1] - 50 * gui.scale) - fields.add(left_rect) + if found_unit == None: + return 1 - bottom_rect = (20 * gui.scale, window_size[1] - 4, window_size[0] - 40 * gui.scale, 7 * gui.scale) - fields.add(bottom_rect) + unit = found_unit - 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 + temp_dest.x = round(location[0]) + temp_dest.y = round(location[1]) - colour = colours.window_frame + temp_dest.w = unit.original_size[0] # round(box[0]) + temp_dest.h = unit.original_size[1] # round(box[1]) - 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) + bh = round(box[1]) + bw = round(box[0]) -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 prefs.zoom_art: + temp_dest.w, temp_dest.h = fit_box((unit.original_size[0], unit.original_size[1]), box) + else: - 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 + # 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])) -# --------------------------------------------------------------------------------------------- -# ABSTRACT SDL DRAWING FUNCTIONS ----------------------------------------------------- + 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 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] + # 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] -def coll(r): - return r[0] < mouse_position[0] <= r[0] + r[2] and r[1] <= mouse_position[1] <= r[1] + r[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 -class Drawing: + # render the image + SDL_RenderCopy(renderer, unit.texture, None, temp_dest) + style_overlay.hole_punches.append(temp_dest) - 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=""): + gui.art_drawn_rect = (temp_dest.x, temp_dest.y, temp_dest.w, temp_dest.h) - if w is None: - w = ddt.get_text_w(text, font) + 18 * gui.scale - if h is None: - h = 22 * gui.scale + return 0 - rect = (x, y, w, h) - fields.add(rect) + def open_external(self, track_object: TrackClass) -> int: - 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 + index = track_object.index - click = False + source = self.get_sources(track_object) + if len(source) == 0: + return 0 - if press is None: - press = inp.mouse_click + offset = self.get_offset(track_object.fullpath, source) - if coll(rect): - if tooltip: - tool_tip.test(x + 15 * gui.scale, y - 28 * gui.scale, tooltip) - ddt.rect(rect, background_highlight_colour) + 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 - # if background_highlight_colour[3] != 255: - # background_highlight_colour = None + source_image = io.BytesIO(pic) + im = Image.open(source_image) + source_image.close() - 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 + 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) -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) + if len(pic) > 30: + with open(target, "wb") as w: + w.write(pic) - ddt.prime_font(standard_font, 9, 412) - ddt.prime_font(standard_font, 10, 413) + else: + target = source[offset][1] - 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 system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - 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) + return 0 - standard_font = prefs.linux_font_bold - # if msys: - # standard_font = prefs.linux_font_bold + ", Noto Sans, Sans Bold" + def cycle_offset(self, track_object: TrackClass, reverse: bool = False) -> int: - 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) + 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: - standard_font = prefs.linux_font_condensed - # if msys: - # standard_font = "Noto Sans ExtCond, Sans" - ddt.prime_font(standard_font, 10, 413) - ddt.prime_font(standard_font, 11, 414) - ddt.prime_font(standard_font, 12, 415) - ddt.prime_font(standard_font, 13, 416) + if reverse: + folder_image_offsets[parent_folder] -= 1 + else: + folder_image_offsets[parent_folder] += 1 - 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) + folder_image_offsets[parent_folder] %= len(sources) + return 0 -class DropShadow: + def cycle_offset_reverse(self, track_object: TrackClass) -> None: + self.cycle_offset(track_object, True) - def __init__(self): - self.readys = {} - self.underscan = int(15 * gui.scale) - self.radius = 4 - self.grow = 2 * gui.scale - self.opacity = 90 + def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: - def prepare(self, w, h): - fh = h + self.underscan - fw = w + self.underscan + # Check if folder offset already exsts, if not, make it + parent_folder = os.path.dirname(filepath) - 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 parent_folder in folder_image_offsets: - im = im.filter(ImageFilter.GaussianBlur(self.radius)) + # 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 - g = io.BytesIO() - g.seek(0) - im.save(g, "PNG") - g.seek(0) + return folder_image_offsets[parent_folder] - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - c = SDL_CreateTextureFromSurface(renderer, s_image) - SDL_SetTextureAlphaMod(c, self.opacity) + def get_embed(self, track: TrackClass): - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) - SDL_QueryTexture(c, None, None, tex_w, tex_h) + # cached = self.embed_cached + # if cached[0] == track: + # #logging.info("used cached") + # return cached[1] - dst = SDL_Rect(0, 0) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + filepath = track.fullpath - SDL_FreeSurface(s_image) - g.close() - im.close() + # Use cached file if present + if prefs.precache and tauon.cachement: + path = tauon.cachement.get_file_cached_only(track) + if path: + filepath = path - unit = (dst, c) - self.readys[(w, h)] = unit + pic = None - def render(self, x, y, w, h): - if (w, h) not in self.readys: - self.prepare(w, h) + 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}") - 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]) + if pic is not None and len(pic) < 30: + pic = None -class LyricsRenMini: + 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 - def __init__(self): - self.index = -1 - self.text = "" + elif track.file_ext == "APE": + with Ape(filepath) as tag: + tag.read() + if tag.has_picture and len(tag.picture) > 30: + pic = tag.picture - self.lyrics_position = 0 + 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 - def generate(self, index, w): - self.text = pctl.master_library[index].lyrics - self.lyrics_position = 0 + 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 - 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) + # self.embed_cached = (track, pic) + return pic - colour = colours.side_bar_line1 + def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, track: TrackClass, subsource: list[tuple[int, str]] | None = None): - # if key_ctrl_down: - # if mouse_wheel < 0: - # prefs.lyrics_font_size += 1 - # if mouse_wheel > 0: - # prefs.lyrics_font_size -= 1 + source_image = None - ddt.text((x, y, 4, w), self.text, colour, prefs.lyrics_font_size, w - (w % 2), colours.side_panel_background) + if subsource is None: + subsource = sources[offset] -class LyricsRen: + if subsource[0] == 1: + # Target is a embedded image\\\ + pic = self.get_embed(track) + assert pic + source_image = io.BytesIO(pic) - def __init__(self): + 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 - self.index = -1 - self.text = "" + 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) - self.lyrics_position = 0 + except Exception: + logging.exception("Failed to get source") - def test_update(self, track_object: TrackClass): + else: + source_image = open(subsource[1], "rb") - 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 + return source_image - def render(self, x, y, w, h, p): + def get_base64(self, track: TrackClass, size): - colour = colours.lyrics - if test_lumi(colours.gallery_background) < 0.5: - colour = colours.grey(40) + # 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 - ddt.text((x, y, 4, w), self.text, colour, 17, w, colours.playlist_panel_background) + cahced = self.base64cache + if track == cahced[0] and size == cahced[1]: + return cahced[2] -def find_synced_lyric_data(track: TrackClass) -> list[str] | None: - if track.is_network: - return None + self.processing64on = track - direc = track.parent_folder_path - name = os.path.splitext(track.filename)[0] + ".lrc" + filepath = track.fullpath + sources = self.get_sources(track) - if len(track.lyrics) > 20 and track.lyrics[0] == "[" and ":" in track.lyrics[:20] and "." in track.lyrics[:20]: - return track.lyrics.splitlines() + if len(sources) == 0: + self.processing64on = None + return False - 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 + offset = self.get_offset(filepath, sources) - return data + # Get source IO + source_image = self.get_source_raw(offset, sources, track) -class TimedLyricsToStatic: + if source_image is None: + self.processing64on = None + return "" - def __init__(self): - self.cache_key = None - self.cache_lyrics = "" + 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()) - 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.base64cache = (track, size, sss) + self.processing64on = None + return sss - if data is None: - self.cache_lyrics = "" - self.cache_key = track - return "" - text = "" + 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 - for line in data: - if len(line) < 10: - continue + # 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") - if line[0] != "[" or line[9] != "]" or ":" not in line or "." not in line: - continue + # 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") - text += line.split("]")[-1].rstrip("\n") + "\n" + # Check we've not already attempted a search for this artist + if artist in prefs.failed_background_artists: + return None - self.cache_lyrics = text - self.cache_key = track - return text + # 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 -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) + # Search fanart.tv for background + try: -class TimedLyricsRen: + r = requests.get( + "https://webservice.fanart.tv/v3/music/" \ + + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) - def __init__(self): + artlink = r.json()["artistbackground"][0]["url"] - self.index = -1 + response = urllib.request.urlopen(artlink, context=tls_context) + info = response.info() - self.scanned = {} - self.ready = False - self.data = [] + assert info.get_content_maintype() == "image" - self.scroll_position = 0 + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + t.seek(0, 2) + l = t.tell() + t.seek(0) - def generate(self, track: TrackClass) -> bool | None: + assert l > 1000 - if self.index == track.index: - return self.ready + # 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 - self.ready = False - self.index = track.index - self.scroll_position = 0 - self.data.clear() + except Exception: + logging.exception(f"Failed to find fanart background for: {artist}") + if not gui.artist_info_panel: + artist_info_box.get_data(artist) + path = artist_info_box.get_data(artist, get_img_path=True) + if os.path.isfile(path): + logging.debug("Downloaded background lfm") + return open(path, "rb") - data = find_synced_lyric_data(track) - if data is None: + + prefs.failed_background_artists.append(artist) return None - for line in data: - if len(line) < 10: - continue + def get_blur_im(self, track: TrackClass) -> BytesIO | bool | None: - if line[0] != "[" or "]" not in line or ":" not in line or "." not in line: - continue + 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 - try: + if source_image is None: + filepath = track.fullpath + sources = self.get_sources(track) - text = line.split("]")[-1].rstrip("\n") - t = line + if len(sources) == 0: + return False - while t[0] == "[" and t[9] == "]" and ":" in t and "." in t: + offset = self.get_offset(filepath, sources) - a = t.lstrip("[") - t = t.split("]")[1] + "]" + source_image = self.get_source_raw(offset, sources, track) - a = a.split("]")[0] - mm, b = a.split(":") - ss, ms = b.split(".") + if source_image is None: + return None - s = int(mm) * 60 + int(ss) - if len(ms) == 2: - s += int(ms) / 100 - elif len(ms) == 3: - s += int(ms) / 1000 + im = Image.open(source_image) - self.data.append((s, text)) + ox_size = im.size[0] + oy_size = im.size[1] - if len(t) < 10: - break - except Exception: - logging.exception("Failed generating timed lyrics") - continue + format = im.format + if im.format == "JPEG": + format = "JPG" - self.data = sorted(self.data, key=lambda x: x[0]) - # logging.info(self.data) + #logging.info(im.size) + if im.mode != "RGB": + im = im.convert("RGB") - self.ready = True - return True + ratio = window_size[0] / ox_size + ratio += 0.2 - def render(self, index: int, x: int, y: int, side_panel: bool = False, w: int = 0, h: int = 0) -> bool | None: + if (oy_size * ratio) - ((oy_size * ratio) // 4) < window_size[1]: + logging.info("Adjust bg vertical") + ratio = window_size[1] / (oy_size - (oy_size // 4)) + ratio += 0.2 - if index != self.index: - self.ready = False - self.generate(pctl.master_library[index]) + new_x = round(ox_size * ratio) + new_y = round(oy_size * ratio) - if right_click and x and y and coll((x, y, w, h)): - showcase_menu.activate(pctl.master_library[index]) + im = im.resize((new_x, new_y)) - if not self.ready: - return 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) - 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) + 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) - line_active = -1 - last = -1 + gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 4 * 3)) - highlight = True + im = im.filter(ImageFilter.GaussianBlur(blur)) - if side_panel: - bg = colours.top_panel_background - font_size = 15 - spacing = round(17 * gui.scale) - else: - bg = colours.playlist_panel_background - font_size = 17 - spacing = round(23 * gui.scale) - test_time = get_real_time() + gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 2)) - if pctl.track_queue[pctl.queue_step] == index: + g = io.BytesIO() + g.seek(0) - for i, line in enumerate(self.data): - if line[0] < test_time: - last = i + a_channel = Image.new("L", im.size, 255) # 'L' 8-bit pixels, black and white + im.putalpha(a_channel) - if line[0] > test_time: - pctl.wake_past_time = line[0] - line_active = last - break - else: - line_active = len(self.data) - 1 + im.save(g, "PNG") + g.seek(0) - if pctl.playing_state == 1: - self.scroll_position = (max(0, line_active)) * spacing * -1 + # source_image.close() - yy = y + self.scroll_position + return g - for i, line in enumerate(self.data): + def save_thumb(self, track_object: TrackClass, size: tuple[int, int], save_path: str, png=False, zoom=False): - if 0 < yy < window_size[1]: + filepath = track_object.fullpath + sources = self.get_sources(track_object) - colour = colours.lyrics - if test_lumi(colours.gallery_background) < 0.5: - colour = colours.grey(40) + if len(sources) == 0: + logging.error("Error thumbnailing; no source images found") + return False - if i == line_active and highlight: - colour = [255, 210, 50, 255] - if colours.lm: - colour = [180, 130, 210, 255] + offset = self.get_offset(filepath, sources) + source_image = self.get_source_raw(offset, sources, track_object) - 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 + im = Image.open(source_image) + if im.mode != "RGB": + im = im.convert("RGB") -def draw_internel_link(x, y, text, colour, font): - tweak = font - while tweak > 100: - tweak -= 100 + 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, + )) - if gui.scale == 2: - tweak *= 2 - tweak += 4 - if gui.scale == 1.25: - tweak = round(tweak * 1.25) - tweak += 1 + im = im.resize(size, Image.Resampling.LANCZOS) - sp = ddt.text((x, y), text, colour, font) + 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 - rect = [x - 5 * gui.scale, y - 2 * gui.scale, sp + 11 * gui.scale, 23 * gui.scale] - fields.add(rect) + if png: + im.save(save_path + ".png", "PNG") + else: + im.save(save_path + ".jpg", "JPEG") - 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 + def display(self, track: TrackClass, location, box, fast: bool = False, theme_only: bool = False) -> int | None: + index = track.index + filepath = track.fullpath -def draw_linked_text(location, text, colour, font, force=False, replace=""): - """No hit detect""" - base = "" - link_text = "" - rest = "" - on_base = 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 - 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] + source = self.get_sources(track) - target_link = link_text - if replace: - link_text = replace + if len(source) == 0: + return 1 - left = ddt.get_text_w(base, font) - right = ddt.get_text_w(base + link_text, font) + offset = self.get_offset(filepath, source) - x = location[0] - y = location[1] + 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 - 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 fast: + return self.fast_display(track, location, box, source, offset) - tweak = font - while tweak > 100: - tweak -= 100 + # 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 gui.scale == 2: - tweak *= 2 - tweak += 4 - elif gui.scale != 1: - tweak = round(tweak * gui.scale) - tweak += 2 + 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]) - if system == "Windows": - tweak += 1 + 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 - # 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)) + elif self.download_in_progress: + return 0 - return left, right - left, target_link + 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() -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) + # 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 -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 self.downloaded_track != track: + return None - if click is None: - click = inp.mouse_click + assert self.downloaded_image + source_image = self.downloaded_image - 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 -def pixel_to_logical(x): - return round((x / window_size[0]) * logical_size[0]) + except Exception: + logging.exception("IMAGE NETWORK LOAD ERROR") + raise -class TextBox2: - cursor = True + else: + # source_image = open(source[offset][1], 'rb') + source_image = self.get_source_raw(0, 0, track, source[offset]) - def __init__(self) -> None: + # Generate + g = io.BytesIO() + g.seek(0) + im = Image.open(source_image) + o_size = im.size - self.text: str = "" - self.cursor_position = 0 - self.selection = 0 - self.offset = 0 - self.down_lock = False - self.paste_text = "" + format = im.format - def paste(self) -> None: + try: + if im.format == "JPEG": + format = "JPG" - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText().decode("utf-8") - self.paste_text = clip + 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 - def copy(self) -> None: - text = self.get_selection() - if not text: - text = self.text - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) + if not theme_only: - def set_text(self, text: str) -> None: + 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) - self.text = text - if self.cursor_position > len(text): - self.cursor_position = 0 - self.selection = 0 - else: - self.selection = self.cursor_position + # Processing for "Carbon" theme + if track == pctl.playing_object() and gui.theme_name == "Carbon" and track.parent_folder_path != colours.last_album: - def clear(self) -> None: - self.text = "" - #self.cursor_position = 0 - self.selection = self.cursor_position + # 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] - def highlight_all(self) -> None: + # 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 - self.selection = len(self.text) - self.cursor_position = 0 + h_colour = rgb_to_hls(colour[0], colour[1], colour[2]) - 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 + l = .51 + s = .44 - 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] + 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 - 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):] + colours.bottom_panel_colour = hls_to_rgb(hh, l, s) + colours.last_album = track.parent_folder_path - else: - return "" + # 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 draw( - self, x, y, colour, active=True, secret=False, font=13, width=0, click=False, selection_height=18, big=False): + colours = copy.deepcopy(colours) - # 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 + 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) - SDL_SetRenderTarget(renderer, text_box_canvas) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + min_colour_varience = 75 - text_box_canvas_rect.x = 0 - text_box_canvas_rect.y = 0 - SDL_RenderFillRect(renderer, text_box_canvas_rect) + 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) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + #logging.info(x_colours) + colours.playlist_panel_bg = colours.side_panel_background + colours.playlist_box_background = colours.side_panel_background - selection_height *= gui.scale + 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,) - if click is False: - click = inp.mouse_click - if mouse_down: - gui.update = 2 # TODO: more elegant fix + colours.queue_background = colours.side_panel_background + # Check artist text colour + if contrast_ratio(colours.artist_text, colours.playlist_panel_background) < 1.9: - 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) + black = [25, 25, 25, 255] + white = [220, 220, 220, 255] - fields.add(rect) + con_b = contrast_ratio(black, colours.playlist_panel_background) + con_w = contrast_ratio(white, colours.playlist_panel_background) - # Activate Menu - if coll(rect): - if right_click or level_2_right_click: - field_menu.activate(self) + choice = black + if con_w > con_b: + choice = white - if width > 0 and active: + colours.artist_text = choice + colours.artist_playing = choice - if click and field_menu.active: - # field_menu.click() - click = False + # Check title text colour + if contrast_ratio(colours.title_text, colours.playlist_panel_background) < 1.9: - # 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:] + black = [60, 60, 60, 255] + white = [180, 180, 180, 255] - 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] + con_b = contrast_ratio(black, colours.playlist_panel_background) + con_w = contrast_ratio(white, colours.playlist_panel_background) - def g2(): - if len(self.text) == 0 or self.cursor_position == 0: - return None - return self.text[len(self.text) - self.cursor_position] + choice = black + if con_w > con_b: + choice = white - 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 + colours.title_text = choice + colours.title_playing = choice - # 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() + 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] - # 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 + colours.album_text = colours.title_text + colours.album_playing = colours.title_playing - # 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 + gui.pl_update = 1 - # 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() + prcl = 100 - int(test_lumi(colours.playlist_panel_background) * 100) - # Left and right arrow keys to move cursor - if key_right_press: - if self.cursor_position > 0: - self.cursor_position -= 1 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position + if 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) - 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 + gui.temp_themes[track.album] = copy.deepcopy(colours) + colours = gui.temp_themes[track.album] + gui.theme_temp_current = track.album - if self.paste_text: - if "http://" in self.text and "http://" in self.paste_text: - self.text = "" + if theme_only: + source_image.close() + g.close() + return None - self.paste_text = self.paste_text.rstrip(" ").lstrip(" ") - self.paste_text = self.paste_text.replace("\n", " ").replace("\r", "") + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + #logging.error(IMG_GetError()) - 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 = "" + c = SDL_CreateTextureFromSurface(renderer, s_image) - # 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:] + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) - if key_ctrl_down and key_c_press: - self.copy() + SDL_QueryTexture(c, None, None, tex_w, tex_h) - 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() + dst = SDL_Rect(round(location[0]), round(location[1])) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) - if key_ctrl_down and key_a_press: - self.cursor_position = 0 - self.selection = len(self.text) + # Clean uo + SDL_FreeSurface(s_image) + source_image.close() + g.close() + # if close: + # source_image.close() - # ddt.rect(rect, [255, 50, 50, 80], True) - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 + 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 - # 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.current_wu = unit + self.image_cache.append(unit) - 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 + self.render(unit, location) - width -= round(15 * gui.scale) - t_len = ddt.get_text_w(self.text, font) - if active and editline and editline != input_text: - t_len += ddt.get_text_w(editline, font) - if not click and not self.down_lock: - cursor_x = ddt.get_text_w(self.text[:len(self.text) - self.cursor_position], font) - if self.cursor_position == 0 or cursor_x < self.offset + round( - 15 * gui.scale) or cursor_x > self.offset + width: - if t_len > width: - self.offset = t_len - width + if 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] - if cursor_x < self.offset: - self.offset = cursor_x - round(15 * 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.offset = max(self.offset, 0) - else: - self.offset = 0 + except Exception: + logging.exception("Image load error") + logging.error("-- Associated track: " + track.fullpath) - x -= self.offset + self.current_wu = None + try: + del self.source_cache[index][offset] + except Exception: + logging.exception(" -- Error, no source cache?") - 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) + return 1 - 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 + return 0 - if mouse_up: - self.down_lock = False - if self.down_lock: - pre = 0 - post = 0 - text = self.text - if secret: - text = "●" * len(self.text) - if mouse_position[0] < x + 1: - self.selection = len(text) - else: + def render(self, unit, location) -> None: - for i in range(len(text)): - post = ddt.get_text_w(text[0:i + 1], font) - # pre_half = int((post - pre) / 2) + rect = unit.rect - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre + gui.art_aspect_ratio = unit.actual_size[0] / unit.actual_size[1] - if mouse_position[0] >= x + pre + int(diff / 2): - self.selection = len(text) - i - 1 + rect.x = round(int((unit.request_size[0] - unit.actual_size[0]) / 2) + location[0]) + rect.y = round(int((unit.request_size[1] - unit.actual_size[1]) / 2) + location[1]) - else: - self.selection = len(text) - i + style_overlay.hole_punches.append(rect) - break - pre = post + SDL_RenderCopy(renderer, unit.texture, None, rect) - else: - self.selection = 0 + gui.art_drawn_rect = (rect.x, rect.y, rect.w, rect.h) - text = self.text[0: len(self.text) - self.cursor_position] - if secret: - text = "●" * len(text) - a = ddt.get_text_w(text, font) + def clear_cache(self) -> None: - text = self.text[0: len(self.text) - self.selection] - if secret: - text = "●" * len(text) - b = ddt.get_text_w(text, font) + for unit in self.image_cache: + SDL_DestroyTexture(unit.texture) - top = y - if big: - top -= 12 * gui.scale + self.image_cache.clear() + self.source_cache.clear() + self.current_wu = None + self.downloaded_track = None - ddt.rect([a, 0, b - a, selection_height], [40, 120, 180, 255]) + self.base64cahce = (0, 0, "") + self.processing64on = None + self.bin_cached = (None, None, None) + self.loading_bin = (None, None) + self.embed_cached = (None, None) - 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) + gui.temp_themes.clear() + gui.theme_temp_current = -1 + colours.last_album = "" - text = self.text[0: len(self.text) - self.cursor_position] - if secret: - text = "●" * len(text) - space = ddt.get_text_w(text, font) +class StyleOverlay: + """ + Stage: + 0 - blank + 1 - preparing first + 2 - render first + """ - if TextBox.cursor and self.selection == self.cursor_position: - # ddt.line(x + space, y + 2, x + space, y + 15, colour) + def __init__(self): - ddt.rect((0 + space, 0 + 2, 1 * gui.scale, 14 * gui.scale), colour) + self.min_on_timer = Timer() + self.fade_on_timer = Timer(0) + self.fade_off_timer = Timer() - if click: - self.selection = self.cursor_position + self.stage = 0 - 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 + self.im = None - 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]) + self.a_texture = None + self.a_rect = None - 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.b_texture = None + self.b_rect = None - animate_monitor_timer.set() + self.a_type = 0 + self.b_type = 0 - text_box_canvas_hide_rect.x = 0 - text_box_canvas_hide_rect.y = 0 + self.window_size = None + self.parent_path = None - # if self.offset: - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + self.hole_punches = [] + self.hole_refills = [] - text_box_canvas_hide_rect.w = round(self.offset) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderFillRect(renderer, text_box_canvas_hide_rect) + self.go_to_sleep = False - 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) + self.current_track_album = "none" + self.current_track_id = -1 - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - SDL_SetRenderTarget(renderer, gui.main_texture) + def worker(self) -> None: - 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 self.stage == 0: -class TextBox: - cursor = True + if (gui.mode == 3 and prefs.mini_mode_mode == 5): + pass + elif prefs.bg_showcase_only and not gui.combo_mode: + return - def __init__(self) -> None: + if pctl.playing_ready() and self.min_on_timer.get() > 0: - self.text = "" - self.cursor_position = 0 - self.selection = 0 - self.down_lock = False + track = pctl.playing_object() - def paste(self) -> None: + self.window_size = copy.copy(window_size) + self.parent_path = track.parent_folder_path + self.current_track_id = track.index + self.current_track_album = track.album - if SDL_HasClipboardText(): - clip = SDL_GetClipboardText().decode("utf-8") + try: + self.im = album_art_gen.get_blur_im(track) + except Exception: + logging.exception("Blur blackground error") + raise + #logging.debug(track.fullpath) - if "http://" in self.text and "http://" in clip: - self.text = "" + 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 - clip = clip.rstrip(" ").lstrip(" ") - clip = clip.replace("\n", " ").replace("\r", "") + self.stage = 1 + gui.update += 1 + return - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( - self.text) - self.cursor_position:] + def flush(self): - def copy(self) -> None: + 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 - text = self.get_selection() - if not text: - text = self.text - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) + def display(self) -> None: - def set_text(self, text): + if self.min_on_timer.get() < 0: + return - self.text = text - self.cursor_position = 0 - self.selection = 0 + if self.stage == 1: - def clear(self) -> None: - self.text = "" + wop = rw_from_object(self.im) + s_image = IMG_Load_RW(wop, 0) - def highlight_all(self) -> None: + c = SDL_CreateTextureFromSurface(renderer, s_image) - self.selection = len(self.text) - self.cursor_position = 0 + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) - def highlight_none(self) -> None: - self.selection = 0 - self.cursor_position = 0 + SDL_QueryTexture(c, None, None, tex_w, tex_h) - 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 + dst = SDL_Rect(round(-40, 0)) + dst.w = int(tex_w.contents.value) + dst.h = int(tex_h.contents.value) - 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] + # Clean uo + SDL_FreeSurface(s_image) + self.im.close() - 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):] + # SDL_SetTextureAlphaMod(c, 10) + self.fade_on_timer.set() - else: - return "" + 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 - 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): + self.a_texture = c + self.a_rect = dst + self.a_type = album_art_gen.loaded_bg_type - # 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 + self.stage = 2 + self.radio_meta = None - selection_height *= gui.scale + gui.update += 1 - if click is False: - click = inp.mouse_click + if self.stage == 2: + track = pctl.playing_object() - if width > 0 and active: + 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 - 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) + 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 - # Activate Menu - if coll(rect): - if right_click or level_2_right_click: - field_menu.activate(self) + if gui.mode == 3 and prefs.mini_mode_mode == 5: + pass + elif prefs.bg_showcase_only: + if not gui.combo_mode: + return - if click and field_menu.active: - # field_menu.click() - click = False + t = self.fade_on_timer.get() + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) + SDL_RenderClear(renderer) - # 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:] + if self.a_texture is not None: + if self.window_size != window_size: + self.flush() - 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 self.b_texture is not None: - def g2(): - if len(self.text) == 0 or self.cursor_position == 0: - return None - return self.text[len(self.text) - self.cursor_position] + self.b_rect.y = 0 - self.b_rect.h // 4 + if self.b_type == 1: + self.b_rect.y = 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 + if t < 0.4: - # 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() + SDL_RenderCopy(renderer, self.b_texture, None, self.b_rect) - # 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 + else: + SDL_DestroyTexture(self.b_texture) + self.b_texture = None + self.b_rect = None - # 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 + if self.a_texture is not None: - # 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() + self.a_rect.y = 0 - self.a_rect.h // 4 + if self.a_type == 1: + self.a_rect.y = 0 - # Left and right arrow keys to move cursor - if key_right_press: - if self.cursor_position > 0: - self.cursor_position -= 1 - if not key_shift_down and not key_shiftr_down: - self.selection = self.cursor_position + if t < 0.4: + fade = round(t / 0.4 * 255) + gui.update += 1 - 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 + else: + fade = 255 - # Paste via ctrl-v - if key_ctrl_down and key_v_press: - clip = SDL_GetClipboardText().decode("utf-8") - self.eliminate_selection() - self.text = self.text[0: len(self.text) - self.cursor_position] + clip + self.text[len( - self.text) - self.cursor_position:] + if self.go_to_sleep: + t = self.fade_off_timer.get() + gui.update += 1 - if key_ctrl_down and key_c_press: - self.copy() + 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 key_ctrl_down and key_x_press: - if len(self.get_selection()) > 0: - text = self.get_selection() - if text != "": - SDL_SetClipboardText(text.encode("utf-8")) - self.eliminate_selection() + if 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 key_ctrl_down and key_a_press: - self.cursor_position = 0 - self.selection = len(self.text) + # 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 - # ddt.rect_r(rect, [255, 50, 50, 80], True) - if coll(rect) and not field_menu.active: - gui.cursor_want = 2 + SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) - fields.add(rect) + SDL_SetTextureAlphaMod(self.a_texture, fade) + SDL_RenderCopy(renderer, self.a_texture, None, self.a_rect) - # Delete key to remove text in front of cursor - if key_del: - if self.selection != self.cursor_position: - self.eliminate_selection() - else: - self.text = self.text[0:len(self.text) - self.cursor_position] + self.text[len( - self.text) - self.cursor_position + 1:] - if self.cursor_position > 0: - self.cursor_position -= 1 - self.selection = self.cursor_position + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) - 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 + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + for rect in self.hole_punches: + SDL_RenderFillRect(renderer, rect) - 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) + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - 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 + SDL_SetRenderTarget(renderer, gui.main_texture) + opacity = prefs.art_bg_opacity + if prefs.mini_mode_mode == 5 and gui.mode == 3: + opacity = 255 - if mouse_up: - self.down_lock = False - if self.down_lock: - pre = 0 - post = 0 - if mouse_position[0] < x + 1: + SDL_SetTextureAlphaMod(gui.main_texture_overlay_temp, opacity) + SDL_RenderCopy(renderer, gui.main_texture_overlay_temp, None, None) - self.selection = len(self.text) - else: + SDL_SetRenderTarget(renderer, gui.main_texture) - for i in range(len(self.text)): - post = ddt.get_text_w(self.text[0:i + 1], font) - # pre_half = int((post - pre) / 2) + else: + SDL_SetRenderTarget(renderer, gui.main_texture) - if x + pre - 0 <= mouse_position[0] <= x + post + 0: - diff = post - pre +class ToolTip: - if mouse_position[0] >= x + pre + int(diff / 2): - self.selection = len(self.text) - i - 1 + 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 - else: - self.selection = len(self.text) - i + def test(self, x, y, text): - break - pre = post + if self.text != text or x != self.x or y != self.y: + self.text = text + # self.timer.set() + self.a = False - else: - self.selection = 0 + self.x = x + self.y = y + self.w = ddt.get_text_w(text, self.font) + 20 * 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) + self.called = True - b = ddt.get_text_w(self.text[0: len(self.text) - self.selection], font) + if self.a is False: + self.timer.set() + gui.frame_callback_list.append(TestTimer(self.trigger)) + self.a = True - # rint((a, b)) + def render(self) -> None: - top = y - if big: - top -= 12 * gui.scale + if self.called is True: - ddt.rect([x + a, top, b - a, selection_height], [40, 120, 180, 255]) + if self.timer.get() > self.trigger: - 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) + 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: - ddt.text((x, y), self.text, colour, font) + # gui.update += 1 + pass + else: + self.timer.set() + self.a = False - space = ddt.get_text_w(self.text[0: len(self.text) - self.cursor_position], font) + self.called = False - if TextBox.cursor and self.selection == self.cursor_position: - # ddt.line(x + space, y + 2, x + space, y + 15, colour) +class ToolTip3: - 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) + 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) - if click: - self.selection = self.cursor_position + def set(self, x, y, text, font, rect): - else: - if active: - self.text += input_text - if input_text != "": - self.cursor = True + y -= round(11 * gui.scale) + if self.show == False or self.y != y or x != self.x or self.pl_position != pctl.playlist_view_position: + self.timer.set() - while inp.backspace_press and len(self.text) > 0: - self.text = self.text[:-1] - inp.backspace_press -= 1 + if point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): + self.timer.set() + return - if key_ctrl_down and key_v_press: - self.paste() + if inp.mouse_click: + self.click_exclude_point = copy.copy(mouse_position) + self.timer.set() + return - if secret: - space = ddt.text((x, y), "●" * len(self.text), colour, font) - else: - space = ddt.text((x, y), self.text, colour, font) + 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 - 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 render(self): - 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 not self.show: + return - 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 not point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): + self.click_exclude_point = (0, 0) - animate_monitor_timer.set() + if not coll( + self.rect) or inp.mouse_click or gui.level_2_click or self.pl_position != pctl.playlist_view_position: + self.show = False -def img_slide_update_gall(value, pause: bool = True) -> None: - global album_mode_art_size - gui.halt_image_rendering = True + gui.frame_callback_list.append(TestTimer(0.02)) - album_mode_art_size = value + if self.timer.get() < 0.6: + return - clear_img_cache(False) - if pause: - gallery_load_delay.set() - gui.frame_callback_list.append(TestTimer(0.6)) - gui.halt_image_rendering = False + w = ddt.get_text_w(self.text, 312) + self.height + x = self.x # - int(self.width / 2) + y = self.y + h = self.height - # Update sizes - tauon.gall_ren.size = album_mode_art_size + border = 1 * gui.scale - if album_mode_art_size > 150: - prefs.thin_gallery_borders = False + ddt.rect((x - border, y - border, w + border * 2, h + border * 2), colours.grey(60)) + ddt.rect((x, y, w, h), colours.menu_background) + p = ddt.text( + (x + int(w / 2), y + 3 * gui.scale, 2), self.text, colours.menu_text, 312, bg=colours.menu_background) -def 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 not coll(self.rect): + self.show = False - i = 0 - while len(tauon.gall_ren.queue) > 0: - time.sleep(0.01) - i += 1 - if i > 5 / 0.01: - break +class RenameTrackBox: - for key, value in tauon.gall_ren.gall.items(): - SDL_DestroyTexture(value[2]) - tauon.gall_ren.gall = {} + def __init__(self): - 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) + self.active = False + self.target_track_id = None + self.single_only = False - 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 + def activate(self, track_id): -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) + 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 - 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)) + def disable_test(self, track_id): + if key_shift_down or key_shiftr_down: + single_only = True + else: + single_only = False - 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 not single_only: + for item in default_playlist: + if pctl.master_library[item].parent_folder_path == pctl.master_library[track_id].parent_folder_path: - gui.halt_image_rendering = False - album_art_gen.clear_cache() + if pctl.master_library[item].is_network is True: + return True + return False -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 = "" + def render(self): -class AlbumArt: - def __init__(self): - self.image_types = {"jpg", "JPG", "jpeg", "JPEG", "PNG", "png", "BMP", "bmp", "GIF", "gif", "jxl", "JXL"} - self.art_folder_names = { - "art", "scans", "scan", "booklet", "images", "image", "cover", - "covers", "coverart", "albumart", "gallery", "jacket", "artwork", - "bonus", "bk", "cover artwork", "cover art"} - self.source_cache: dict[int, list[tuple[int, str]]] = {} - self.image_cache: list[ImageObject] = [] - self.current_wu = None + if not self.active: + return - self.blur_texture = None - self.blur_rect = None - self.loaded_bg_type = 0 + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - self.download_in_progress = False - self.downloaded_image = None - self.downloaded_track = None + 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) - self.base64cache = (0, 0, "") - self.processing64on = 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 - self.bin_cached = (None, None, None) # track, subsource, bin + 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 - self.embed_cached = (None, None) + r_todo = [] - def async_download_image(self, track: TrackClass, subsource: list[tuple[int, str]]) -> None: + # 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: - 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 + # Close and display error if any tracks are not single local files + if pctl.master_library[item].is_network is True: + rename_track_box.active = False + show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") + if pctl.master_library[item].is_cue is True: + rename_track_box.active = False + show_message(_("This function does not support renaming CUE Sheet tracks.")) + else: + r_todo.append(item) + else: + r_todo = [self.target_track_id] - def get_info(self, track_object: TrackClass) -> list[tuple[str, int, int, int, str]]: + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Track Renaming"), colours.grey(230), 213) - sources = self.get_sources(track_object) - if len(sources) == 0: - return None + # 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 - offset = self.get_offset(track_object.fullpath, sources) + # 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 - o_size = (0, 0) - format = "ERROR" + ddt.rect_s( + (x + 8 * gui.scale, y + 36 * gui.scale, 300 * gui.scale, 22 * gui.scale), colours.box_text_border, 1 * gui.scale) - 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 + afterline = "" + warn = False + underscore = False - 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 + for item in r_todo: - return [sources[offset][0], len(sources), offset, o_size, format] + 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 - def get_sources(self, tr: TrackClass) -> list[tuple[int, str]]: + if item == self.target_track_id: + afterline = parse_template2(NRN, pctl.master_library[item]) - filepath = tr.fullpath - ext = tr.file_ext + 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) - # Check if source list already exists, if not, make it - if tr.index in self.source_cache: - return self.source_cache[tr.index] + 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) - source_list: list[tuple[int, str]] = [] # istag, + 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) - # Source type the is first element in list - # 0 = File - # 1 = Embedded in tag - # 2 = Network location + 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 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 [] + label = _("Write") + " (" + str(len(r_todo)) + ")" - # Check for embedded image - try: - pic = self.get_embed(tr) - if pic: - source_list.append([1, filepath]) - except Exception: - logging.exception("Failed to get embedded image") + if 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 not tr.is_network: + inp.mouse_click = False + total_todo = len(r_todo) + pre_state = 0 - 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] + for item in r_todo: - 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]) + if pctl.playing_state > 0 and item == pctl.track_queue[pctl.queue_step]: + pre_state = pctl.stop(True) - for i in range(len(dirs_in_dir)): - subdirec = os.path.join(direc, dirs_in_dir[i]) - items_in_dir2 = os.listdir(subdirec) + try: - 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]) + afterline = parse_template2(NRN, pctl.master_library[item], strict=True) - self.source_cache[tr.index] = source_list + oldname = pctl.master_library[item].filename + oldpath = pctl.master_library[item].fullpath - return source_list + logging.info("Renaming...") - 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 + star = star_store.full_get(item) + star_store.remove(item) - def fast_display(self, index, location, box, source: list[tuple[int, str]], offset) -> int: - """Renders cached image only by given size for faster performance""" + oldpath = pctl.master_library[item].fullpath - found_unit = None - max_h = 0 + oldsplit = os.path.split(oldpath) - for unit in self.image_cache: - if unit.source == source[offset][1]: - if unit.actual_size[1] > max_h: - max_h = unit.actual_size[1] - found_unit = unit + if os.path.exists(os.path.join(oldsplit[0], afterline)): + logging.error("A file with that name already exists") + total_todo -= 1 + continue - if found_unit == None: - return 1 + if not afterline: + logging.error("Rename Error") + total_todo -= 1 + continue - unit = found_unit + if "." in afterline and not afterline.split(".")[0]: + logging.error("A file does not have a target filename") + total_todo -= 1 + continue - temp_dest.x = round(location[0]) - temp_dest.y = round(location[1]) + os.rename(pctl.master_library[item].fullpath, os.path.join(oldsplit[0], afterline)) - temp_dest.w = unit.original_size[0] # round(box[0]) - temp_dest.h = unit.original_size[1] # round(box[1]) + pctl.master_library[item].fullpath = os.path.join(oldsplit[0], afterline) + pctl.master_library[item].filename = afterline - bh = round(box[1]) - bw = round(box[0]) + search_string_cache.pop(item, None) + search_dia_string_cache.pop(item, None) - if prefs.zoom_art: - temp_dest.w, temp_dest.h = fit_box((unit.original_size[0], unit.original_size[1]), box) - else: + if star is not None: + star_store.insert(item, star) - # 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])) + except Exception: + logging.exception("Rendering error") + total_todo -= 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])) + rename_track_box.active = False + logging.info("Done") + if pre_state == 1: + pctl.revert() - # 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] + 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() - # 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 +class TransEditBox: - # render the image - SDL_RenderCopy(renderer, unit.texture, None, temp_dest) - style_overlay.hole_punches.append(temp_dest) + def __init__(self): + self.active = False + self.active_field = 1 + self.selected = [] + self.playlist = -1 - gui.art_drawn_rect = (temp_dest.x, temp_dest.y, temp_dest.w, temp_dest.h) + def render(self): - return 0 + if not self.active: + return - def open_external(self, track_object: TrackClass) -> int: + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - index = track_object.index + 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) - source = self.get_sources(track_object) - if len(source) == 0: - return 0 + 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 - offset = self.get_offset(track_object.fullpath, source) + 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 - 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 + select = list(set(shift_selection)) + if not select and pctl.selected_ready(): + select = [pctl.selected_in_playlist] - source_image = io.BytesIO(pic) - im = Image.open(source_image) - source_image.close() + 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] - 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) + #logging.info(select) + if select != self.selected or pctl.active_playlist_viewing != self.playlist: + #logging.info("reset") + self.selected = select + self.playlist = pctl.active_playlist_viewing + edit_album.clear() + edit_artist.clear() + edit_title.clear() + edit_album_artist.clear() - if len(pic) > 30: - with open(target, "wb") as w: - w.write(pic) + if len(select) == 0: + return - else: - target = source[offset][1] + tr = pctl.get_track(default_playlist[select[0]]) + edit_title.set_text(tr.title) - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + if check_equal(artists): + edit_artist.set_text(artists[0]) - return 0 + if check_equal(albums): + edit_album.set_text(albums[0]) - def cycle_offset(self, track_object: TrackClass, reverse: bool = False) -> int: + if check_equal(album_artists): + edit_album_artist.set_text(album_artists[0]) - 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: + x += round(20 * gui.scale) + y += round(18 * gui.scale) - if reverse: - folder_image_offsets[parent_folder] -= 1 - else: - folder_image_offsets[parent_folder] += 1 + ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) - folder_image_offsets[parent_folder] %= len(sources) - return 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 cycle_offset_reverse(self, track_object: TrackClass) -> None: - self.cycle_offset(track_object, True) + y += round(24 * gui.scale) + ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) - def get_offset(self, filepath: str, source: list[tuple[int, str]]) -> int: + y += round(24 * gui.scale) - # Check if folder offset already exsts, if not, make it - parent_folder = os.path.dirname(filepath) + if inp.key_tab_press: + if key_shift_down or key_shiftr_down: + self.active_field -= 1 + else: + self.active_field += 1 - if parent_folder in folder_image_offsets: + 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 - # 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 + 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 - return folder_image_offsets[parent_folder] + 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) - def get_embed(self, track: TrackClass): + 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) - # cached = self.embed_cached - # if cached[0] == track: - # #logging.info("used cached") - # return cached[1] + if inp.key_return_press: - filepath = track.fullpath + 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) - # Use cached file if present - if prefs.precache and tauon.cachement: - path = tauon.cachement.get_file_cached_only(track) - if path: - filepath = path + 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() - pic = None - if track.file_ext == "MP3": - try: - tag = mutagen.id3.ID3(filepath) - frame = tag.getall("APIC") - if frame: - pic = frame[0].data - except Exception: - logging.exception(f"Failed to get tags on file: {filepath}") + ww = ddt.get_text_w(_("WRITE TAGS"), 212) + round(48 * gui.scale) + if gui.write_tag_in_progress: + text = f"{gui.tag_write_count}/{len(select)}" + text = _("WRITE TAGS") + if draw.button(text, (x + w) - ww, y - round(0) * gui.scale): + if changed: + show_message(_("Press enter on fields to apply your changes first!")) + return - if pic is not None and len(pic) < 30: - pic = None + if gui.write_tag_in_progress: + return - 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 + def write_tag_go(): - elif track.file_ext == "APE": - with Ape(filepath) as tag: - tag.read() - if tag.has_picture and len(tag.picture) > 30: - pic = tag.picture - elif track.file_ext == "M4A": - with M4a(filepath) as tag: - tag.read(True) - if tag.has_picture and len(tag.picture) > 30: - pic = tag.picture + for s in select: + tr = pctl.get_track(default_playlist[s]) - 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 + 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 - # self.embed_cached = (track, pic) - return pic + muta = mutagen.File(tr.fullpath, easy=True) - def get_source_raw(self, offset: int, sources: list[tuple[int, str]] | int, track: TrackClass, subsource: list[tuple[int, str]] | None = None): + 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 - source_image = None + 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") - if subsource is None: - subsource = sources[offset] + 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 subsource[0] == 1: - # Target is a embedded image\\\ - pic = self.get_embed(track) - assert pic - source_image = io.BytesIO(pic) +class SubLyricsBox: - 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 + def __init__(self): - 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) + self.active = False + self.target_track = None + self.active_field = 1 - except Exception: - logging.exception("Failed to get source") + def activate(self, track: TrackClass): - else: - source_image = open(subsource[1], "rb") + self.active = True + gui.box_over = True + self.target_track = track - return source_image + sub_lyrics_a.text = prefs.lyrics_subs.get(self.target_track.artist, "") + sub_lyrics_b.text = prefs.lyrics_subs.get(self.target_track.title, "") - def get_base64(self, track: TrackClass, size): + 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 - # 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 + def render(self): - cahced = self.base64cache - if track == cahced[0] and size == cahced[1]: - return cahced[2] + if not self.active: + return - self.processing64on = track + if gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False - filepath = track.fullpath - sources = self.get_sources(track) + 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(sources) == 0: - self.processing64on = None - return 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) + ddt.text_background_colour = colours.box_background - offset = self.get_offset(filepath, sources) + 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 - # Get source IO - source_image = self.get_source_raw(offset, sources, track) + if sub_lyrics_a.text and sub_lyrics_a.text != self.target_track.artist: + prefs.lyrics_subs[self.target_track.artist] = sub_lyrics_a.text + elif self.target_track.artist in prefs.lyrics_subs: + del prefs.lyrics_subs[self.target_track.artist] - if source_image is None: - self.processing64on = None - return "" + 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] - 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()) + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Substitute Lyric Search"), colours.grey(230), 213) - self.base64cache = (track, size, sss) - self.processing64on = None - return sss + y += round(35 * gui.scale) + x += round(23 * gui.scale) - 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 + 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) - # 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") + 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 - # 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") + sub_lyrics_a.draw( + xx + round(4 * gui.scale), y, colours.box_input_text, self.active_field == 1, + width=rect1[2] - 8 * gui.scale) - # Check we've not already attempted a search for this artist - if artist in prefs.failed_background_artists: - return None + y += round(28 * gui.scale) - # 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 + 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) - # Search fanart.tv for background - try: + 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) - r = requests.get( - "https://webservice.fanart.tv/v3/music/" \ - + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) +class ExportPlaylistBox: - artlink = r.json()["artistbackground"][0]["url"] + def __init__(self): - response = urllib.request.urlopen(artlink, context=tls_context) - info = response.info() + 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, + } - assert info.get_content_maintype() == "image" + def activate(self, playlist): - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - t.seek(0, 2) - l = t.tell() - t.seek(0) + self.active = True + gui.box_over = True + self.id = pl_to_id(playlist) - assert l > 1000 + # 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] - # 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 + def render(self) -> None: + if not self.active: + return - 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") + w = 500 * gui.scale + h = 220 * gui.scale + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background - prefs.failed_background_artists.append(artist) - return None + 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 - def get_blur_im(self, track: TrackClass) -> BytesIO | bool | None: + current = prefs.playlist_exports.get(self.id) + if not current: + current = copy.copy(self.default) - 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 + ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Export Playlist"), colours.grey(230), 213) - if source_image is None: - filepath = track.fullpath - sources = self.get_sources(track) + x += round(15 * gui.scale) + y += round(25 * gui.scale) - if len(sources) == 0: - return False + ddt.text((x, y + 8 * gui.scale), _("Save directory"), colours.grey(230), 11) + y += round(30 * gui.scale) - offset = self.get_offset(filepath, sources) + 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 - source_image = self.get_source_raw(offset, sources, track) + 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) - if source_image is None: - return None + 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)) - im = Image.open(source_image) + prefs.playlist_exports[self.id] = current - ox_size = im.size[0] - oy_size = im.size[1] + if draw.button(_("Export"), x, y, press=gui.level_2_click): + self.run_export(current, self.id, warnings=True) - format = im.format - if im.format == "JPEG": - format = "JPG" + 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) - #logging.info(im.size) - if im.mode != "RGB": - im = im.convert("RGB") + if warnings and target != 1: + show_message(_("Playlist exported"), target, mode="done") - ratio = window_size[0] / ox_size - ratio += 0.2 +class SearchOverlay: - 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 __init__(self): - new_x = round(ox_size * ratio) - new_y = round(oy_size * ratio) + self.active = False + self.search_text = TextBox() - im = im.resize((new_x, new_y)) + 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 - 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) + def clear(self): + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + self.on = 0 + self.all_folders = False - if (ox_size < 500 or prefs.art_bg_always_blur) or gui.mode == 3: - blur = prefs.art_bg_blur - if prefs.mini_mode_mode == 5 and gui.mode == 3: - blur = 160 - pix = im.getpixel((new_x // 2, new_y // 4 * 3)) - pixel_sum = sum(pix) / (255 * 3) - if pixel_sum > 0.6: - enhancer = ImageEnhance.Brightness(im) - deduct = 1 - ((pixel_sum - 0.6) * 1.5) - im = enhancer.enhance(deduct) - logging.info(deduct) + def click_artist(self, name, get_list=False, search_lists=None): - gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 4 * 3)) + playlist = [] - im = im.filter(ImageFilter.GaussianBlur(blur)) + 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) - gui.center_blur_pixel = im.getpixel((new_x // 2, new_y // 2)) + if get_list: + return playlist - g = io.BytesIO() - g.seek(0) + pctl.multi_playlist.append(pl_gen( + title=_("Artist: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - a_channel = Image.new("L", im.size, 255) # 'L' 8-bit pixels, black and white - im.putalpha(a_channel) + 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 + "\"" - im.save(g, "PNG") - g.seek(0) + inp.key_return_press = False - # source_image.close() + def click_year(self, name, get_list: bool = False): - return g + 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 save_thumb(self, track_object: TrackClass, size: tuple[int, int], save_path: str, png=False, zoom=False): + if get_list: + return playlist - filepath = track_object.fullpath - sources = self.get_sources(track_object) + pctl.multi_playlist.append(pl_gen( + title=_("Year: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - if len(sources) == 0: - logging.error("Error thumbnailing; no source images found") - return False + if gui.combo_mode: + exit_combo() - offset = self.get_offset(filepath, sources) - source_image = self.get_source_raw(offset, sources, track_object) + switch_playlist(len(pctl.multi_playlist) - 1) - im = Image.open(source_image) - if im.mode != "RGB": - im = im.convert("RGB") + inp.key_return_press = False - 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 click_composer(self, name: str, get_list: bool = False): - im = im.resize(size, Image.Resampling.LANCZOS) + 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 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 get_list: + return playlist - if png: - im.save(save_path + ".png", "PNG") - else: - im.save(save_path + ".jpg", "JPEG") + pctl.multi_playlist.append(pl_gen( + title=_("Composer: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - def display(self, track: TrackClass, location, box, fast: bool = False, theme_only: bool = False) -> int | None: - index = track.index - filepath = track.fullpath + if gui.combo_mode: + exit_combo() - 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 + switch_playlist(len(pctl.multi_playlist) - 1) - source = self.get_sources(track) + inp.key_return_press = False - if len(source) == 0: - return 1 + def click_meta(self, name: str, get_list: bool = False, search_lists=None): - offset = self.get_offset(filepath, source) + if search_lists is None: + search_lists = [] + for pl in pctl.multi_playlist: + search_lists.append(pl.playlist_ids) - 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 + 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 fast: - return self.fast_display(track, location, box, source, offset) + if get_list: + return playlist - # 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 + pctl.multi_playlist.append(pl_gen( + title=os.path.basename(name).upper(), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - 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]) + if gui.combo_mode: + exit_combo() - 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 + switch_playlist(len(pctl.multi_playlist) - 1) - elif self.download_in_progress: - return 0 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "p\"" + name + "\"" - 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() + inp.key_return_press = False - # 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 + def click_genre(self, name: str, get_list: bool = False, search_lists=None): - if self.downloaded_track != track: - return None + playlist = [] - assert self.downloaded_image - source_image = self.downloaded_image + 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 - except Exception: - logging.exception("IMAGE NETWORK LOAD ERROR") - raise + 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) - else: - # source_image = open(source[offset][1], 'rb') - source_image = self.get_source_raw(0, 0, track, source[offset]) + if get_list: + return playlist - # Generate - g = io.BytesIO() - g.seek(0) - im = Image.open(source_image) - o_size = im.size + pctl.multi_playlist.append(pl_gen( + title=_("Genre: ") + name, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - format = im.format + if gui.combo_mode: + exit_combo() - try: - if im.format == "JPEG": - format = "JPG" + switch_playlist(len(pctl.multi_playlist) - 1) - 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 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 - if not theme_only: + def click_album(self, index): - 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) + pctl.jump(index) + if gui.combo_mode: + exit_combo() - # Processing for "Carbon" theme - if track == pctl.playing_object() and gui.theme_name == "Carbon" and track.parent_folder_path != colours.last_album: + pctl.show_current() - # 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] + inp.key_return_press = False - # 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 + def render(self): + global input_text + if self.active is False: - h_colour = rgb_to_hls(colour[0], colour[1], colour[2]) + # 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: - l = .51 - s = .44 + # 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 - 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 + activate_search_overlay() + self.old_mouse = copy.deepcopy(mouse_position) - colours.bottom_panel_colour = hls_to_rgb(hh, l, s) - colours.last_album = track.parent_folder_path + if self.active: - # 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 + x = 0 + y = 0 + w = window_size[0] + h = window_size[1] - colours = copy.deepcopy(colours) + if keymaps.test("add-to-queue"): + input_text = "" - 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 inp.backspace_press: + # self.searched_text = "" + # self.results.clear() - min_colour_varience = 75 + if len(self.search_text.text) - inp.backspace_press < 1: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" + return - 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) + 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 - #logging.info(x_colours) - colours.playlist_panel_bg = colours.side_panel_background - colours.playlist_box_background = colours.side_panel_background + if gui.level_2_click and mouse_position[0] > 350 * gui.scale: + self.active = False + self.search_text.text = "" - 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,) + mouse_change = False + if not point_proximity_test(self.old_mouse, mouse_position, 25): + mouse_change = True + # mouse_change = True - colours.queue_background = colours.side_panel_background - # Check artist text colour - if contrast_ratio(colours.artist_text, colours.playlist_panel_background) < 1.9: + ddt.rect((x, y, w, h), [3, 3, 3, 235]) + ddt.text_background_colour = [12, 12, 12, 255] - black = [25, 25, 25, 255] - white = [220, 220, 220, 255] - con_b = contrast_ratio(black, colours.playlist_panel_background) - con_w = contrast_ratio(white, colours.playlist_panel_background) + input_text_x = 80 * gui.scale + highlight_x = 30 * gui.scale + thumbnail_rx = 100 * gui.scale + text_lx = 120 * gui.scale - choice = black - if con_w > con_b: - choice = white + s_font = 15 + s_b_font = 214 + b_font = 215 - colours.artist_text = choice - colours.artist_playing = choice + 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 - # Check title text colour - if contrast_ratio(colours.title_text, colours.playlist_panel_background) < 1.9: + #album_art_size_s = 0 * gui.scale - black = [60, 60, 60, 255] - white = [180, 180, 180, 255] + # Search active animation + if self.sip: + x = round(15 * gui.scale) + y = x + s = round(7 * gui.scale) + g = round(4 * gui.scale) - con_b = contrast_ratio(black, colours.playlist_panel_background) - con_w = contrast_ratio(white, colours.playlist_panel_background) + t = self.animate_timer.get() + if abs(t - self.last_animate_time) > 0.3: + self.animate_timer.set() + t = 0 - choice = black - if con_w > con_b: - choice = white + self.last_animate_time = t - colours.title_text = choice - colours.title_playing = choice + 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 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] + ddt.rect((x, y, s, s), colour) + x += g + s - colours.album_text = colours.title_text - colours.album_playing = colours.title_playing + gui.update += 1 - gui.pl_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]) - prcl = 100 - int(test_lumi(colours.playlist_panel_background) * 100) + # 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]) - 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) + 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) - gui.temp_themes[track.album] = copy.deepcopy(colours) - colours = gui.temp_themes[track.album] - gui.theme_temp_current = track.album + 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 theme_only: - source_image.close() - g.close() - return None + if input_text or key_backspace_press: + self.input_timer.set() - wop = rw_from_object(g) - s_image = IMG_Load_RW(wop, 0) - #logging.error(IMG_GetError()) + 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") - c = SDL_CreateTextureFromSurface(renderer, s_image) + if self.input_timer.get() < 10: + gui.frame_callback_list.append(TestTimer(0.1)) - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) + yy = 110 * gui.scale - SDL_QueryTexture(c, None, None, tex_w, tex_h) + if key_down_press: - dst = SDL_Rect(round(location[0]), round(location[1])) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + 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) - # Clean uo - SDL_FreeSurface(s_image) - source_image.close() - g.close() - # if close: - # source_image.close() + if key_up_press: - 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.force_select > -1: + self.force_select -= 1 + self.force_select = max(self.force_select, 0) - self.current_wu = unit - self.image_cache.append(unit) + if self.force_select < self.on + 4: + self.on = self.force_select - 4 + self.on = max(self.on, 0) - self.render(unit, location) + self.old_mouse = copy.deepcopy(mouse_position) - 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] + 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 - # temp fix - global move_on_title - global playlist_hold - global quick_drag - quick_drag = False - move_on_title = False - playlist_hold = False + enter = False - except Exception: - logging.exception("Image load error") - logging.error("-- Associated track: " + track.fullpath) + if self.delay_enter and not self.sip and self.search_text.text == self.searched_text: + enter = True + self.delay_enter = False - self.current_wu = None - try: - del self.source_cache[index][offset] - except Exception: - logging.exception(" -- Error, no source cache?") + 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 - return 1 + inp.key_return_press = False - return 0 + bar_colour = [140, 80, 240, 255] + track_in_bar_colour = [244, 209, 66, 255] - def render(self, unit, location) -> None: + self.on = max(self.on, 0) + self.on = min(len(self.results) - 1, self.on) - rect = unit.rect + full_count = 0 - gui.art_aspect_ratio = unit.actual_size[0] / unit.actual_size[1] + sec = False - 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]) + p = -1 - style_overlay.hole_punches.append(rect) + if self.on > 4: + p += self.on - 4 + p = self.on - 1 + clear = False - SDL_RenderCopy(renderer, unit.texture, None, rect) + for i, item in enumerate(self.results): - gui.art_drawn_rect = (rect.x, rect.y, rect.w, rect.h) + p += 1 - def clear_cache(self) -> None: + if p > len(self.results) - 1: + break - for unit in self.image_cache: - SDL_DestroyTexture(unit.texture) + item: list[int] = self.results[p] - self.image_cache.clear() - self.source_cache.clear() - self.current_wu = None - self.downloaded_track = None + fade = 1 + selected = self.on + if self.force_select > -1: + selected = self.force_select - self.base64cahce = (0, 0, "") - self.processing64on = None - self.bin_cached = (None, None, None) - self.loading_bin = (None, None) - self.embed_cached = (None, None) + #logging.info(selected) - gui.temp_themes.clear() - gui.theme_temp_current = -1 - colours.last_album = "" + if selected != p: + fade = 0.8 -class StyleOverlay: - """ - Stage: - 0 - blank - 1 - preparing first - 2 - render first - """ + start = yy - def __init__(self): + n = item[0] - self.min_on_timer = Timer() - self.fade_on_timer = Timer(0) - self.fade_off_timer = Timer() + 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) - self.stage = 0 + pad = round(4 * gui.scale) + height = round(25 * gui.scale) + if n in (1, 11): + height = round(50 * gui.scale) + album_art_size = height - self.im = None - self.a_texture = None - self.a_rect = None + # 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) - self.b_texture = None - self.b_rect = None + # 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) - self.a_type = 0 - self.b_type = 0 + # 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") - self.window_size = None - self.parent_path = 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.hole_punches = [] - self.hole_refills = [] + if n in (1,): # Two line album + track = pctl.master_library[item[2]] + artist = track.album_artist + if not artist: + artist = track.artist - self.go_to_sleep = False + xx = ddt.text((text_lx, yy + pad + round(5 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], s_b_font) - self.current_track_album = "none" - self.current_track_id = -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) - def worker(self) -> None: - if self.stage == 0: + yy += height + pad + pad - if (gui.mode == 3 and prefs.mini_mode_mode == 5): - pass - elif prefs.bg_showcase_only and not gui.combo_mode: - return + 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 pctl.playing_ready() and self.min_on_timer.get() > 0: + if gui.level_2_click: + if key_ctrl_down: + extend = True + else: + go = True + clear = True - track = pctl.playing_object() - self.window_size = copy.copy(window_size) - self.parent_path = track.parent_folder_path - self.current_track_id = track.index - self.current_track_album = track.album + if level_2_right_click: + show = True + clear = True - try: - self.im = album_art_gen.get_blur_im(track) - except Exception: - logging.exception("Blur blackground error") - raise - #logging.debug(track.fullpath) + if enter and key_shift_down and fade == 1: + show = True + clear = True - 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 + elif enter and fade == 1: + if key_shift_down or key_shiftr_down: + show = True + clear = True + else: + go = True + clear = True - self.stage = 1 - gui.update += 1 - return + 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() - def flush(self): + 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) - 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 + 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() - def display(self) -> None: + 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 self.min_on_timer.get() < 0: - return + # ---- - if self.stage == 1: + # --- + if i > 40: + break + if yy > window_size[1] - (100 * gui.scale): + break - wop = rw_from_object(self.im) - s_image = IMG_Load_RW(wop, 0) + continue - c = SDL_CreateTextureFromSurface(renderer, s_image) + if clear: + self.active = False + self.search_text.text = "" + self.results.clear() + self.searched_text = "" - tex_w = pointer(c_int(0)) - tex_h = pointer(c_int(0)) +class MessageBox: - SDL_QueryTexture(c, None, None, tex_w, tex_h) + def __init__(self): + pass - dst = SDL_Rect(round(-40, 0)) - dst.w = int(tex_w.contents.value) - dst.h = int(tex_h.contents.value) + def get_rect(self): - # Clean uo - SDL_FreeSurface(s_image) - self.im.close() + 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) - # SDL_SetTextureAlphaMod(c, 10) - self.fade_on_timer.set() + w = max(w, 210 * 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 + h = round(60 * gui.scale) + if gui.message_subtext2: + h += round(15 * gui.scale) - self.a_texture = c - self.a_rect = dst - self.a_type = album_art_gen.loaded_bg_type + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) - self.stage = 2 - self.radio_meta = None + return x, y, w, h - gui.update += 1 + def render(self): - if self.stage == 2: - track = pctl.playing_object() + 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 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 + if not key_focused and message_box_min_timer.get() > 0.4: + gui.message_box = False + gui.update += 1 + inp.key_return_press = False - 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 + x, y, w, h = self.get_rect() - if gui.mode == 3 and prefs.mini_mode_mode == 5: - pass - elif prefs.bg_showcase_only: - if not gui.combo_mode: - return + 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) - 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.message_box_bg - if self.a_texture is not None: - if self.window_size != window_size: - self.flush() + 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 self.b_texture is not None: + 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) - self.b_rect.y = 0 - self.b_rect.h // 4 - if self.b_type == 1: - self.b_rect.y = 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 t < 0.4: + else: + ddt.text((x + 62 * gui.scale, y + 20 * gui.scale), gui.message_text, colours.message_box_text, 15) - SDL_RenderCopy(renderer, self.b_texture, None, self.b_rect) +class NagBox: + def __init__(self): + self.wiggle_timer = Timer(10) - else: - SDL_DestroyTexture(self.b_texture) - self.b_texture = None - self.b_rect = None + 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) - if self.a_texture is not None: + # 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 - self.a_rect.y = 0 - self.a_rect.h // 4 - if self.a_type == 1: - self.a_rect.y = 0 + 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 t < 0.4: - fade = round(t / 0.4 * 255) - gui.update += 1 + # 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 - else: - fade = 255 + ddt.text_background_colour = colours.message_box_bg - if self.go_to_sleep: - t = self.fade_off_timer.get() - gui.update += 1 + 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) - 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 + 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) - 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) + heart_notify_icon.render(x + round(425 * gui.scale), y + round(80 * gui.scale), [255, 90, 90, 255]) - # 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 += round(30 * gui.scale) + ddt.text((x, y), _("New supporter bonuses!"), colours.message_box_text, 212) - SDL_SetRenderTarget(renderer, gui.main_texture_overlay_temp) + y += round(20 * gui.scale) - SDL_SetTextureAlphaMod(self.a_texture, fade) - SDL_RenderCopy(renderer, self.a_texture, None, self.a_rect) + 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) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE) + y += round(20 * gui.scale) + ddt.text((x, y), _("Your support means a lot! Love you!"), colours.message_box_text, 12) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - for rect in self.hole_punches: - SDL_RenderFillRect(renderer, rect) + y += round(30 * gui.scale) - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + 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 - SDL_SetRenderTarget(renderer, gui.main_texture) - opacity = prefs.art_bg_opacity - if prefs.mini_mode_mode == 5 and gui.mode == 3: - opacity = 255 +class PowerTag: - SDL_SetTextureAlphaMod(gui.main_texture_overlay_temp, opacity) - SDL_RenderCopy(renderer, gui.main_texture_overlay_temp, None, None) + def __init__(self): + self.name = "BLANK" + self.path = "" + self.position = 0 + self.colour = None - SDL_SetRenderTarget(renderer, gui.main_texture) + self.peak_x = 0 + self.ani_timer = Timer() + self.ani_timer.force_set(10) - else: - SDL_SetRenderTarget(renderer, gui.main_texture) +class Over: + def __init__(self): -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 + global window_size - 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 + self.init2done = False - while ddt.get_text_w(line, font) > px: + 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) - line = line[:-1] - if len(line) < 2: - break + # self.tab_width = round(115 * gui.scale) + self.w = 100 + self.h = 100 - return line + self.box_x = 100 + self.box_y = 100 + self.item_x_offset = round(25 * gui.scale) -def right_trunc(line: str, font: str, px: int, dots: bool = True) -> str: - if ddt.get_text_w(line, font) < px + 10: - return line + self.current_path = os.path.expanduser("~") + self.view_offset = 0 + self.ext_ratio = {} + self.last_db_size = -1 - 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(" ") + self.enabled = False + self.click = False + self.right_click = False + self.scroll = 0 + self.lock = False - 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 + self.drives = [] -# 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 + self.temp_lastfm_user = "" + self.temp_lastfm_pass = "" + self.lastfm_input_box = 0 -def fix_encoding(index, mode, enc): - global default_playlist - global enc_field + 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], + ] - todo = [] + 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 - 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]) + self.ani_cred = 0 + self.cred_page = 0 + self.ani_fade_on_timer = Timer(force=10) + self.ani_fade_off_timer = Timer(force=10) - for q in range(len(todo)): + self.device_scroll_bar_position = 0 - # 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]) + 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 - 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 + self.account_text_field = -1 - 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 + self.themes = [] + self.view_supporters = False + self.key_box = TextBox2() + self.key_box_focused = False - if enc_field == "All" or enc_field == "Title": - line = pctl.master_library[todo[q]].title - line = line.encode("Latin-1", "ignore") - line = line.decode(enc, "ignore") - pctl.master_library[todo[q]].title = line + def theme(self, x0, y0, w0, h0): - if old_star != None: - star_store.insert(todo[q], old_star) + global album_mode_art_size + global update_layout - # 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] + y = y0 + 13 * gui.scale + x = x0 + 25 * gui.scale -def transfer_tracks(index, mode, to): - todo = [] + ddt.text_background_colour = colours.box_background + ddt.text((x, y), _("Theme"), colours.box_text_label, 12) - 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 += 25 * gui.scale - pctl.multi_playlist[to].playlist_ids += todo + self.toggle_square(x, y, toggle_auto_bg, _("Use album art as background")) -def prep_gal(): - global albums - albums = [] + y += 23 * gui.scale - folder = "" + 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 - for index in default_playlist: + 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 folder != pctl.master_library[index].parent_folder_name: - albums.append([index, 0]) - folder = pctl.master_library[index].parent_folder_name + #y += 23 * gui.scale + self.toggle_square(x + 120 * gui.scale, y, toggle_auto_bg_blur, _("Blur")) -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() + y += 23 * gui.scale + self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_showcase, _("Showcase only")) -def load_m3u(path: str) -> None: - name = os.path.basename(path)[:-4] - playlist = [] - stations = [] + 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")) - location_dict = {} - titles = {} + y += 25 * gui.scale - if not os.path.isfile(path): - return + self.toggle_square(x, y, toggle_auto_theme, _("Auto-theme from album art")) - with Path(path).open(encoding="utf-8") as file: - lines = file.readlines() + y += 55 * gui.scale - for i, line in enumerate(lines): - line = line.strip("\r\n").strip() - if not line.startswith("#"): # line.startswith("http"): + square = round(8 * gui.scale) + border = round(4 * gui.scale) + outer_border = round(2 * gui.scale) - # 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() + # theme_files = get_themes() + xx = x + yy = y + hover_name = None + for c, theme_name, theme_number in self.themes: - if line.startswith("http"): - radio: dict[str, int | str] = {} - radio["stream_url"] = line + 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) - if line_title: - radio["title"] = line_title - else: - radio["title"] = os.path.splitext(os.path.basename(path))[0].strip() + rect = [xx, yy, border * 2 + square * 2, border * 2 + square * 2] + ddt.rect(rect, [5, 5, 5, 255]) - stations.append(radio) + 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 - 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) + c1 = c.playlist_panel_background + c2 = c.artist_playing + c3 = c.title_playing + c4 = c.bottom_panel_colour - # 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 theme_name == "Carbon": + c1 = c.title_playing + c2 = c.playlist_panel_background + c3 = c.top_panel_background - # 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 theme_name == "Lavender Light": + c1 = c.tab_background_active - if playlist: - pctl.multi_playlist.append( - pl_gen(title=name, playlist_ids=playlist)) - if stations: - add_stations(stations, name) + if theme_name == "Neon Love": + c2 = c.artist_text + c4 = [118, 85, 194, 255] + c1 = c4 - gui.update = 1 + if theme_name == "Sky": + c2 = c.artist_text -def read_pls(lines: list[str], path: str, followed: bool = False) -> None: - ids = [] - urls = {} - titles = {} + if theme_name == "Sunken": + c2 = c.title_text + c3 = c.artist_text + c4 = [59, 115, 109, 255] + c1 = c4 - 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() + 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: - 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() + # tl + rect = [xx + border, yy + border, square, square] + ddt.rect(rect, c1) - 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] + # tr + rect = [xx + border + square, yy + border, square, square] + ddt.rect(rect, c2) - 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)) + # bl + rect = [xx + border, yy + border + square, square, square] + ddt.rect(rect, c3) -def load_pls(path: str) -> None: - if os.path.isfile(path): - f = open(path) - lines = f.readlines() - read_pls(lines, path) - f.close() - -def load_xspf(path: str) -> None: - global to_got + # br + rect = [xx + border + square, yy + border + square, square, square] + ddt.rect(rect, c4) - name = os.path.basename(path)[:-5] - # tauon.log("Importing XSPF playlist: " + path, title=True) - logging.info("Importing XSPF playlist: " + path) + yy += round(27 * gui.scale) + if yy > y + 40 * gui.scale: + yy = y + xx += round(27 * gui.scale) - try: - parser = ET.XMLParser(encoding="utf-8") - e = ET.parse(path, parser).getroot() + 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 - a = [] - b = {} - info = "" + 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) - for top in e: + def rg(self, x0, y0, w0, h0): + y = y0 + 55 * gui.scale + x = x0 + 130 * gui.scale - 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.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): + self.rg_view = False - 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 + y = y0 + round(15 * gui.scale) + x = x0 + round(50 * gui.scale) - b["info"] = info - b["name"] = name - a.append(copy.deepcopy(b)) - b = {} + ddt.text((x, y), _("ReplayGain"), colours.box_text_label, 14) + y += round(25 * gui.scale) - except Exception: - logging.exception("Error importing/parsing XSPF playlist") - show_message(_("Error importing XSPF playlist."), _("Sorry about that."), mode="warning") - return + 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")) - # 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, y), _("Will only have effect if ReplayGain metadata is present."), colours.box_text_label, 12) + y += round(26 * gui.scale) - stations.append(radio) + ddt.text((x, y), _("Pre-amp"), colours.box_text_label, 14) + y += round(26 * gui.scale) - if gui.auto_play_import: - gui.auto_play_import = False - radiobox.start(radio) + sw = round(170 * gui.scale) + sh = round(2 * gui.scale) - del a[i] - if stations: - add_stations(stations, os.path.basename(path)) - playlist = [] - missing = 0 + slider = (x, y, sw, sh) - if len(a) > 5000: - to_got = "xspfl" + gh = round(14 * gui.scale) + gw = round(8 * gui.scale) + grip = [0, y - (gh // 2), gw, gh] - # 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 + grip[0] = x - for track in a: - found = False + bp = prefs.replay_preamp + 15 - # Check if we already have a track with full file path in database - if not found and "location" in track: + grip[0] += (bp / 30 * sw) - location = track["location"] - if location in location_dict: - playlist.append(location_dict[location]) - if not os.path.isfile(location): - missing += 1 - found = True + m1 = (x, y, sh, sh * 2) + m2 = ((x + sw // 2), y, sh, sh * 2) + m3 = ((x + sw), y, sh, sh * 2) - if found is True: - continue + if coll(grow_rect(slider, 15)) and mouse_down: + bp = (mouse_position[0] - x) / sw * 30 + gui.update += 1 - # 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 + bp = round(bp) + bp = max(bp, 0) + bp = min(bp, 30) + prefs.replay_preamp = bp - 15 - # 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 + # grip[0] += (bp / 30 * sw) - if (not found and "location" in track) or "title" in track: - nt = TrackClass() - nt.index = pctl.master_count - nt.found = False + 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) - 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) + text = f"{prefs.replay_preamp} dB" + if prefs.replay_preamp > 0: + text = "+" + text - pctl.master_library[pctl.master_count] = nt - playlist.append(pctl.master_count) - pctl.master_count += 1 - if nt.found: - continue + 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) - 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"]) + 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 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 + def eq(self, x0, y0, w0, h0): - # tauon.log("Finished importing XSPF") + y = y0 + 55 * gui.scale + x = x0 + 130 * gui.scale -class ToolTip: + if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): + self.eq_view = False - 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 + base_dis = 160 * gui.scale + center = base_dis // 2 + width = 25 * gui.scale - def test(self, x, y, text): + range = 12 - if self.text != text or x != self.x or y != self.y: - self.text = text - # self.timer.set() - self.a = False + self.toggle_square(x - 90 * gui.scale, y - 35 * gui.scale, toggle_eq, _("Enable")) - self.x = x - self.y = y - self.w = ddt.get_text_w(text, self.font) + 20 * gui.scale + 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) - self.called = True + for i, q in enumerate(prefs.eq): - if self.a is False: - self.timer.set() - gui.frame_callback_list.append(TestTimer(self.trigger)) - self.a = True + bar = [x, y, width, base_dis] - def render(self) -> None: + ddt.rect(bar, [255, 255, 255, 20]) - if self.called is True: + bar[0] -= 2 * gui.scale + bar[1] -= 10 * gui.scale + bar[2] += 4 * gui.scale + bar[3] += 20 * gui.scale - if self.timer.get() > self.trigger: + if coll(bar): - 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 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 - self.called = False + prefs.eq[i] = target -def ex_tool_tip(x, y, text1_width, text, font): - text2_width = ddt.get_text_w(text, font) - if text2_width == text1_width: - return + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True - y -= 10 * gui.scale + if self.right_click: + prefs.eq[i] = 0 + pctl.playerCommand = "seteq" + pctl.playerCommandReady = True - w = ddt.get_text_w(text, 312) + 24 * gui.scale - h = 24 * gui.scale + start = (q / range) * center - x -= int(w / 2) + bar = [x, y + center, width, start] - 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) + ddt.rect(bar, [100, 200, 100, 255]) -class ToolTip3: + x += round(29 * gui.scale) - def __init__(self) -> None: - self.x = 0 - self.y = 0 - self.text = "" - self.font = None - self.show = False - self.width = 0 - self.height = 24 * gui.scale - self.timer = Timer() - self.pl_position = 0 - self.click_exclude_point = (0, 0) + def audio(self, x0, y0, w0, h0): - def set(self, x, y, text, font, rect): + global mouse_down - 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.text_background_colour = colours.box_background + y = y0 + 40 * gui.scale + x = x0 + 20 * gui.scale - if point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): - self.timer.set() + if self.eq_view: + self.eq(x0, y0, w0, h0) return - if inp.mouse_click: - self.click_exclude_point = copy.copy(mouse_position) - self.timer.set() + if self.rg_view: + self.rg(x0, y0, w0, h0) return - self.x = x - self.y = y - self.text = text - self.font = font - self.show = True - self.rect = rect - self.pl_position = pctl.playlist_view_position + colour = colours.box_sub_text - def render(self): + # 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 not self.show: - return + elif prefs.backend == 4: - if not point_proximity_test(self.click_exclude_point, mouse_position, 20 * gui.scale): - self.click_exclude_point = (0, 0) + y = y0 + round(20 * gui.scale) + x = x0 + 20 * gui.scale - 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 + x += round(2 * gui.scale) - gui.frame_callback_list.append(TestTimer(0.02)) + 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 self.timer.get() < 0.6: - return + y += round(40 * gui.scale) + if self.button(x, y, _("ReplayGain")): + mouse_down = False + self.rg_view = True - w = ddt.get_text_w(self.text, 312) + self.height - x = self.x # - int(self.width / 2) - y = self.y - h = self.height + 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__() - border = 1 * gui.scale + 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) - 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) + 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 not coll(self.rect): - self.show = False + # if prefs.device_buffer > 100: + # prefs.pa_fast_seek = True + # else: + # prefs.pa_fast_seek = False -def close_all_menus(): - for menu in Menu.instances: - menu.active = False - Menu.active = 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) -def menu_standard_or_grey(bool: bool): - if bool: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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")) - return [line_colour, colours.menu_background, None] + 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") -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.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 -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() + 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) -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() + i = 0 + reload = False + for name in prefs.phazor_devices: -def enable_folder_list(): - if prefs.left_panel_mode != "folder view": - gui.last_left_panel_mode = prefs.left_panel_mode - prefs.left_panel_mode = "folder view" - gui.lsp = True - gui.update_layout() + if i < self.device_scroll_bar_position: + continue + if y > self.box_y + self.h - 40 * gui.scale: + break -def lsp_menu_test_queue(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "queue" + rect = (x, y + 4 * gui.scale, 245 * gui.scale, 13) -def lsp_menu_test_playlist(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "playlist" + if self.click and coll(rect): + prefs.phazor_device_selected = name + reload = True -def lsp_menu_test_tree(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "folder view" + line = trunc_line(name, 10, 245 * gui.scale) -def lsp_menu_test_artist(): - if not gui.lsp: - return False - return prefs.left_panel_mode == "artist list" + fields.add(rect) -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 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 -class RenameTrackBox: + if reload: + pctl.playerCommand = "set-device" + pctl.playerCommandReady = True - def __init__(self): + def reload_device(self, _): + pctl.playerCommand = "reload" + pctl.playerCommandReady = True - self.active = False - self.target_track_id = None - self.single_only = False + def toggle_lyrics_view(self): + self.lyrics_panel ^= True - def activate(self, track_id): + def lyrics(self, x0, y0, w0, h0): + x = x0 + 25 * gui.scale + y = y0 - 10 * gui.scale + y += 30 * 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 + ddt.text_background_colour = colours.box_background - def disable_test(self, track_id): - if key_shift_down or key_shiftr_down: - single_only = True - else: - single_only = False + # 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 - 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: + self.toggle_square(x, y, toggle_guitar_chords, _("Enable chord lyrics")) - if pctl.master_library[item].is_network is True: - return True - return False + y += 40 * gui.scale + ddt.text((x, y), _("Sources:"), colours.box_text_label, 11) + y += 23 * gui.scale - def render(self): + 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) - if not self.active: - return + 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 - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + def view2(self, x0, y0, w0, h0): - 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) + x = x0 + 25 * gui.scale + y = y0 + 20 * gui.scale - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) ddt.text_background_colour = colours.box_background - if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): - rename_track_box.active = False - - r_todo = [] - - # Find matching folder tracks in playlist - if not self.single_only: - for item in default_playlist: - if pctl.master_library[item].parent_folder_path == pctl.master_library[ - self.target_track_id].parent_folder_path: + ddt.text((x, y), _("Metadata side panel"), colours.box_text_label, 12) - # 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] + 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() - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Track Renaming"), colours.grey(230), 213) + global album_mode_art_size + global update_layout + y += 35 * gui.scale + ddt.text((x, y), _("Gallery"), colours.box_text_label, 12) - # 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 + 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")) - # 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 + y += 30 * gui.scale - ddt.rect_s( - (x + 8 * gui.scale, y + 36 * gui.scale, 300 * gui.scale, 22 * gui.scale), colours.box_text_border, 1 * gui.scale) + # y += 25 * gui.scale - afterline = "" - warn = False - underscore = False + x -= 80 * gui.scale + x += ddt.get_text_w(_("Thumbnail size"), 312) + # x += 20 * gui.scale - for item in r_todo: + if album_mode_art_size < 160: + self.toggle_square(x + 235 * gui.scale, y + 2 * gui.scale, toggle_gallery_thin, _("Prefer thinner padding")) - 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.text((x, y), _("Gallery art size"), colours.grey(220), 11) - if item == self.target_track_id: - afterline = parse_template2(NRN, pctl.master_library[item]) + 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) - 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) + def funcs(self, x0, y0, w0, h0): - 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 = x0 + 25 * gui.scale + y = y0 - 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) + ddt.text_background_colour = colours.box_background - 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 self.func_page == 0: - label = _("Write") + " (" + str(len(r_todo)) + ")" + y += 23 * gui.scale - 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: + self.toggle_square( + x, y, toggle_enable_web, _("Enable Listen Along"), subtitle=_("Start server for remote web playback")) - inp.mouse_click = False - total_todo = len(r_todo) - pre_state = 0 + if toggle_enable_web(1): - for item in r_todo: + 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 pctl.playing_state > 0 and item == pctl.track_queue[pctl.queue_step]: - pre_state = pctl.stop(True) + if coll(link_rect2): + if not self.click: + gui.cursor_want = 3 - try: + if self.click: + webbrowser.open(link_pa2[2], new=2, autoraise=True) - afterline = parse_template2(NRN, pctl.master_library[item], strict=True) + y += 38 * gui.scale - oldname = pctl.master_library[item].filename - oldpath = pctl.master_library[item].fullpath + 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) - logging.info("Renaming...") + y += 38 * gui.scale - star = star_store.full_get(item) - star_store.remove(item) + self.toggle_square( + x, y, toggle_auto_artist_dl, + _("Auto fetch artist data"), + subtitle=_("Downloads data in background when artist panel is open")) - oldpath = pctl.master_library[item].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")) - oldsplit = os.path.split(oldpath) + y += 38 * gui.scale + self.toggle_square( + x, y, toggle_top_tabs, _("Tabs in top panel"), + subtitle=_("Uncheck to disable the tab pin function")) - if os.path.exists(os.path.join(oldsplit[0], afterline)): - logging.error("A file with that name already exists") - total_todo -= 1 - continue + y += 45 * gui.scale + # y += 30 * gui.scale - if not afterline: - logging.error("Rename Error") - total_todo -= 1 - continue + 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 - if "." in afterline and not afterline.split(".")[0]: - logging.error("A file does not have a target filename") - total_todo -= 1 - continue + ww = max(wa, wc) - os.rename(pctl.master_library[item].fullpath, os.path.join(oldsplit[0], afterline)) + 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) - pctl.master_library[item].fullpath = os.path.join(oldsplit[0], afterline) - pctl.master_library[item].filename = afterline + self.button(x + wa + round(20 * gui.scale), y, _("Open data folder"), open_data_directory, ww) - search_string_cache.pop(item, None) - search_dia_string_cache.pop(item, None) + 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 - if star is not None: - star_store.insert(item, star) + 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")) - except Exception: - logging.exception("Rendering error") - total_todo -= 1 + y += 28 * gui.scale - rename_track_box.active = False - logging.info("Done") - if pre_state == 1: - pctl.revert() + x = x0 + self.item_x_offset - 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() + ddt.text((x, y), _("End of playlist action"), colours.box_text_label, 12) -class TransEditBox: + 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 - def __init__(self): - self.active = False - self.active_field = 1 - self.selected = [] - self.playlist = -1 + 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")) - def render(self): + 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")) - if not self.active: - return + y += 38 * gui.scale + if not msys: + self.toggle_square(x, y, toggle_use_tray, _("Show icon in system tray")) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + y += 25 * gui.scale + self.toggle_square(x + round(10 * gui.scale), y, toggle_min_tray, _("Close to tray")) - 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 += 25 * gui.scale + self.toggle_square(x + round(10 * gui.scale), y, toggle_text_tray, _("Show title text")) - 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 + 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")) - 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 + else: + self.toggle_square(x, y, toggle_min_tray, _("Close to tray")) - select = list(set(shift_selection)) - if not select and pctl.selected_ready(): - select = [pctl.selected_in_playlist] - titles = [pctl.get_track(default_playlist[s]).title for s in select] - artists = [pctl.get_track(default_playlist[s]).artist for s in select] - albums = [pctl.get_track(default_playlist[s]).album for s in select] - album_artists = [pctl.get_track(default_playlist[s]).album_artist for s in select] - #logging.info(select) - if select != self.selected or pctl.active_playlist_viewing != self.playlist: - #logging.info("reset") - self.selected = select - self.playlist = pctl.active_playlist_viewing - edit_album.clear() - edit_artist.clear() - edit_title.clear() - edit_album_artist.clear() + 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 - if len(select) == 0: - return + 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 - tr = pctl.get_track(default_playlist[select[0]]) - edit_title.set_text(tr.title) + 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") - if check_equal(artists): - edit_artist.set_text(artists[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")) - if check_equal(albums): - edit_album.set_text(albums[0]) + 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 check_equal(album_artists): - edit_album_artist.set_text(album_artists[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() - x += round(20 * gui.scale) - y += round(18 * gui.scale) + 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")) - ddt.text((x, y), _("Simple tag editor"), colours.box_title_text, 215) + 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 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") + 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() - y += round(24 * gui.scale) - ddt.text((x, y), _("Number of tracks selected: {N}").format(N=len(select)), colours.box_title_text, 313) + if old and not prefs.discord_enable: + if prefs.discord_active: + prefs.disconnect_discord = True - y += round(24 * gui.scale) + 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 inp.key_tab_press: - if key_shift_down or key_shiftr_down: - self.active_field -= 1 - else: - self.active_field += 1 + # Switcher + pages = 5 + x = x0 + round(18 * gui.scale) + y = (y0 + h0) - round(29 * gui.scale) + ww = 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 + 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 - 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 + # self.button(x, y, _("Open keymap file"), open_keymap_file, width=wc) - 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) + def button(self, x, y, text, plug=None, width=0, bg=None): - 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) + w = width + if w == 0: + w = ddt.get_text_w(text, 211) + round(10 * gui.scale) - if inp.key_return_press: + h = round(20 * gui.scale) + border_size = round(2 * 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) + 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.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 bg is None: + bg = colours.box_background + real_bg = bg + hit = False - 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 + ddt.rect(rect2, colours.box_check_border) + ddt.rect(rect, bg) - if gui.write_tag_in_progress: - return + 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) - def write_tag_go(): + 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) - for s in select: - tr = pctl.get_track(default_playlist[s]) + bg_colour = colours.box_button_background + real_bg = bg_colour - 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 + ddt.rect(rect, bg_colour) + fields.add(rect) + hit = False - muta = mutagen.File(tr.fullpath, easy=True) + 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) - 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 + 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 - 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 toggle_square(self, x, y, function, text: str , click: bool = False, subtitle: str = "") -> bool: - 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) + x = round(x) + y = round(y) -class SubLyricsBox: + border = round(2 * gui.scale) + gap = round(2 * gui.scale) + inner_square = round(6 * gui.scale) - def __init__(self): + full_w = border * 2 + gap * 2 + inner_square - self.active = False - self.target_track = None - self.active_field = 1 + 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) - def activate(self, track: TrackClass): + 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.active = True - gui.box_over = True - self.target_track = track + # 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)) - sub_lyrics_a.text = prefs.lyrics_subs.get(self.target_track.artist, "") - sub_lyrics_b.text = prefs.lyrics_subs.get(self.target_track.title, "") + # Check if box clicked + clicked = False + if (self.click or click) and coll(hit_rect): + clicked = 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 + # There are two mode, function type, and passthrough bool type + active = False + if type(function) is bool: + active = function + else: + active = function(1) - def render(self): + if clicked: + if type(function) is bool: + active ^= True + else: + function() + active = function(1) - if not self.active: - return + # 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) - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + return active - 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) + def last_fm_box(self, x0, y0, w0, h0): + + x = x0 + round(20 * gui.scale) + y = y0 + round(15 * gui.scale) - ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) - ddt.rect_a((x, y), (w, h), colours.box_background) ddt.text_background_colour = colours.box_background - if key_esc_press or ((inp.mouse_click or right_click or level_2_right_click) and not coll((x, y, w, h))): - self.active = False - gui.box_over = False + 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 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] + y += 28 * gui.scale - 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.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")) - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Substitute Lyric Search"), colours.grey(230), 213) + y += 28 * gui.scale - y += round(35 * gui.scale) - x += round(23 * 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")) - 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) + # if self.button2(x, y, "Discogs", width=84*gui.scale): + # self.account_view = 3 - 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 + y += 28 * 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.button2(x, y, "fanart.tv", width=84 * gui.scale): + self.account_view = 4 - y += round(28 * gui.scale) + y += 28 * gui.scale + y += 28 * gui.scale - xx = x - xx += ddt.text( - (x + round(0 * gui.scale), y + round(0 * gui.scale)), _("Substitute"), colours.box_text_label, 212) - xx += round(6 * gui.scale) - ddt.text((xx, y + round(0 * gui.scale)), self.target_track.title, colours.box_sub_text, 312) + y += 15 * gui.scale - 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 key_shift_down and self.button2(x + round(95 * gui.scale), y, "koel", width=84 * gui.scale): + self.account_view = 6 -class ExportPlaylistBox: + if self.button2(x, y, "Jellyfin", width=84 * gui.scale): + self.account_view = 10 - def __init__(self): + if self.button2(x + round(95 * gui.scale), y, "TIDAL", width=84 * gui.scale): + self.account_view = 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, - } + y += 28 * gui.scale - def activate(self, playlist): + if self.button2(x, y, "Airsonic", width=84 * gui.scale): + self.account_view = 7 - self.active = True - gui.box_over = True - self.id = pl_to_id(playlist) + if self.button2(x + round(95 * gui.scale), y, "PLEX", width=84 * gui.scale): + self.account_view = 5 - # 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] + y += 28 * gui.scale - def render(self) -> None: - if not self.active: - return + if self.button2(x, y, "Spotify", width=84 * gui.scale): + self.account_view = 8 - 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) + if self.button2(x + round(95 * gui.scale), y, "Satellite", width=84 * gui.scale): + self.account_view = 11 - 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 self.account_view in (9, 2): + self.toggle_square( + x0 + 230 * gui.scale, y + 2 * gui.scale, toggle_scrobble_mark, + _("Show threshold marker")) - 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 + x = x0 + 230 * gui.scale + y = y0 + round(20 * gui.scale) - current = prefs.playlist_exports.get(self.id) - if not current: - current = copy.copy(self.default) + if self.account_view == 12: + ddt.text((x, y), "TIDAL", colours.box_sub_text, 213) - ddt.text((x + 10 * gui.scale, y + 8 * gui.scale), _("Export Playlist"), colours.grey(230), 213) + y += round(30 * gui.scale) - x += round(15 * gui.scale) - y += round(25 * 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) - ddt.text((x, y + 8 * gui.scale), _("Save directory"), colours.grey(230), 11) - y += round(30 * gui.scale) + 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) - rect1 = (x, y, round(450 * gui.scale), round(16 * gui.scale)) - fields.add(rect1) - # ddt.rect(rect1, [40, 40, 40, 255], True) - ddt.bordered_rect(rect1, colours.box_background, colours.box_text_border, round(1 * gui.scale)) - self.directory_text_box.text = current["path"] - self.directory_text_box.draw( - x + round(4 * gui.scale), y, colours.box_input_text, True, - width=rect1[2] - 8 * gui.scale, click=gui.level_2_click) - current["path"] = self.directory_text_box.text + y += round(30 * gui.scale) + if self.button(x, y, _("Import Tracks")): + show_message(_("Fetching playlist...")) + shooter(tauon.tidal.fav_tracks) - 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) + if self.account_view == 11: + ddt.text((x, y), "Tauon Satellite", colours.box_sub_text, 213) - 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)) + y += round(30 * gui.scale) - prefs.playlist_exports[self.id] = current + 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() - if draw.button(_("Export"), x, y, press=gui.level_2_click): - self.run_export(current, self.id, warnings=True) + y += round(25 * gui.scale) - 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) + y += round(30 * gui.scale) - if warnings and target != 1: - show_message(_("Playlist exported"), target, mode="done") + 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) -def toggle_repeat() -> None: - gui.update += 1 - pctl.repeat_mode ^= True - if pctl.mpris is not None: - pctl.mpris.update_loop() + 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() + if self.button(x, y, _("Get playlist")): + if tau.processing: + show_message(_("An operation is already running")) + else: + shooter(tau.get_playlist()) -def menu_set_repeat() -> None: - pctl.repeat_mode = True - pctl.album_repeat_mode = False - if pctl.mpris is not None: - pctl.mpris.update_loop() + elif self.account_view == 9: -def menu_album_repeat() -> None: - pctl.repeat_mode = True - pctl.album_repeat_mode = True - if pctl.mpris is not None: - pctl.mpris.update_loop() + 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 toggle_random(): - gui.update += 1 - pctl.random_mode ^= True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 -def toggle_random_on(): - pctl.random_mode = True - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + field_width = round(245 * gui.scale) -def toggle_random_off(): - pctl.random_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + 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() -def menu_shuffle_off(): - pctl.random_mode = False - pctl.album_shuffle_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + 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() -def menu_set_random(): - pctl.random_mode = True - pctl.album_shuffle_mode = False - if pctl.mpris is not None: - pctl.mpris.update_shuffle() + y += round(35 * gui.scale) -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.button(x, y, _("Test connectivity")): -def toggle_shuffle_layout(albums=False): - prefs.shuffle_lock ^= True - if prefs.shuffle_lock: + 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" - gui.shuffle_was_showcase = gui.showcase_mode - gui.shuffle_was_random = pctl.random_mode - gui.shuffle_was_repeat = pctl.repeat_mode + 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") - 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() + y += round(30 * gui.scale) -def toggle_shuffle_layout_albums(): - toggle_shuffle_layout(albums=True) + 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) -def exit_shuffle_layout(_): - return prefs.shuffle_lock + if self.account_view == 8: -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) + ddt.text((x, y), "Spotify", colours.box_sub_text, 213) -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) + prefs.spot_mode = self.toggle_square(x + 80 * gui.scale, y + 2 * gui.scale, prefs.spot_mode, _("Enable")) + y += round(30 * gui.scale) -def artist_info_panel_close(): - gui.artist_info_panel ^= True - gui.update_layout() + if self.button(x, y, _("View setup instructions")): + webbrowser.open("https://github.com/Taiko2k/Tauon/wiki/Spotify", new=2, autoraise=True) -def toggle_bio_size_deco(): - line = _("Make Large Size") - if prefs.bio_large: - line = _("Make Compact Size") + field_width = round(245 * gui.scale) - return [colours.menu_text, colours.menu_background, line] + y += round(26 * gui.scale) -def toggle_bio_size(): - if prefs.bio_large: - prefs.bio_large = False - update_layout_do() - # bio_set_small() + 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() - else: - prefs.bio_large = True - update_layout_do() - # bio_set_large() - # gui.update_layout() + 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() -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 + y += round(27 * gui.scale) -def test_shift(_): - return key_shift_down or key_shiftr_down + 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 test_artist_dl(_): - return not prefs.auto_dl_artist_data + tauon.spot_ctl.auth() -def show_in_playlist(): - if album_mode and window_size[0] < 750 * gui.scale: - toggle_album_mode() + y += round(31 * gui.scale) + prefs.launch_spotify_web = self.toggle_square( + x, y, prefs.launch_spotify_web, + _("Prefer launching web player")) - 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() + y += round(24 * gui.scale) -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]) + old = prefs.launch_spotify_local + prefs.launch_spotify_local = self.toggle_square( + x, y, prefs.launch_spotify_local, + _("Enable local audio playback")) -def open_folder_disable_test(index: int): - track = pctl.master_library[index] - return track.is_network and not os.path.isdir(track.parent_folder_path) + if prefs.launch_spotify_local and not tauon.enable_librespot: + show_message(_("Librespot not installed?")) + prefs.launch_spotify_local = False -def open_folder(index: int): - track = pctl.master_library[index] - if open_folder_disable_test(index): - show_message(_("Can't open folder of a network track.")) - return - 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 self.account_view == 7: -def tag_to_new_playlist(tag_item): - path_stem_to_playlist(tag_item.path, tag_item.name) + ddt.text((x, y), _("Airsonic/Subsonic network streaming"), colours.box_sub_text, 213) -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) + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 -def stem_to_new_playlist(path: str) -> None: - path_stem_to_playlist(path, os.path.basename(path)) + field_width = round(245 * gui.scale) -def move_playing_folder_to_tree_stem(path: str) -> None: - move_playing_folder_to_stem(path, pl_id=tree_view_box.get_pl_id()) + 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 -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 + 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 - track = pctl.playing_object() + 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 - if not track or pctl.playing_state == 0: - show_message(_("No item is currently playing")) - return + y += round(40 * gui.scale) + self.button(x, y, _("Import music to playlist"), sub_get_album_thread) - move_folder = track.parent_folder_path + 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")) - # 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) + if self.account_view == 10: - target_base = path + ddt.text((x, y), _("Jellyfin network streaming"), colours.box_sub_text, 213) - # Determine name for artist folder - artist = track.artist - if track.album_artist: - artist = track.album_artist + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 - # Make filename friendly - artist = filename_safe(artist) - if not artist: - artist = "unknown artist" + field_width = round(245 * gui.scale) - # Sanity checks - if track.is_network: - show_message(_("This track is a networked track."), mode="error") - return + 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 - if not os.path.isdir(move_folder): - show_message(_("The source folder does not exist."), mode="error") - return + 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 - if not os.path.isdir(target_base): - show_message(_("The destination folder does not exist."), mode="error") - return + 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 - if os.path.normpath(target_base) == os.path.normpath(move_folder): - show_message(_("The destination and source folders are the same."), mode="error") - return + y += round(30 * gui.scale) - if len(target_base) < 4: - show_message(_("Safety interupt! The source path seems oddly short."), target_base, mode="error") - return + self.button(x, y, _("Import music to playlist"), jellyfin_get_library_thread) - 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 + 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 directory_size(move_folder) > 3000000000: - show_message(_("Folder size safety limit reached! (3GB)"), move_folder, mode="warning") - return + y += round(35 * gui.scale) + if self.button(x, y, _("Test connectivity")): + jellyfin.test() - # Use target folder if it already is an artist folder - if os.path.basename(target_base).lower() == artist.lower(): - artist_folder = target_base + if self.account_view == 6: - # 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) + ddt.text((x, y), _("koel network streaming"), colours.box_sub_text, 213) - # 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] + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 - # Find insert location - pl = pctl.multi_playlist[id_to_pl(pl_id)].playlist_ids + field_width = round(245 * gui.scale) - matches = [] - insert = 0 + 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 - for i, item in enumerate(pl): - if pctl.get_track(item).fullpath.startswith(target_base): - insert = i + 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 - for i, item in enumerate(pl): - if pctl.get_track(item).fullpath.startswith(artist_folder): - insert = i + 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 - 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 + y += round(40 * gui.scale) - 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") + self.button(x, y, _("Import music to playlist"), koel_get_album_thread) -def move_playing_folder_to_tag(tag_item): - move_playing_folder_to_stem(tag_item.path) + if self.account_view == 5: -def re_import4(id): - p = None - for i, idd in enumerate(default_playlist): - if idd == id: - p = i - break + ddt.text((x, y), _("PLEX network streaming"), colours.box_sub_text, 213) - load_order = LoadClass() + if inp.key_tab_press: + self.account_text_field += 1 + if self.account_text_field > 2: + self.account_text_field = 0 - if p is not None: - load_order.playlist_position = p + field_width = round(245 * gui.scale) - 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") + 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 -def re_import3(stem): - p = None - for i, id in enumerate(default_playlist): - if pctl.get_track(id).fullpath.startswith(stem + "/"): - p = i - break + 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 - load_order = LoadClass() + 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 - if p is not None: - load_order.playlist_position = p + y += round(40 * gui.scale) + self.button(x, y, _("Import music to playlist"), plex_get_album_thread) - 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 self.account_view == 4: -def collapse_tree_deco(): - pl_id = tree_view_box.get_pl_id() + ddt.text((x, y), "fanart.tv", colours.box_sub_text, 213) - 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] + 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 -def collapse_tree(): - tree_view_box.collapse_all() + 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) -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 + 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") -def lock_folder_tree_deco(): - if tree_view_box.lock_pl: - return [colours.menu_text, colours.menu_background, _("Unlock Panel")] - return [colours.menu_text, colours.menu_background, _("Lock Panel")] + # if 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) + # -def finish_current(): - playing_object = pctl.playing_object() - if playing_object is None: - show_message("") + if self.account_view == 1: - 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)) + text = "Last.fm" + if prefs.use_libre_fm: + text = "Libre.fm" -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) + ddt.text((x, y), text, colours.box_sub_text, 213) - 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 + 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) - 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 + y += 25 * gui.scale -def add_album_to_queue_fc(ref): - playing_object = pctl.playing_object() - if playing_object is None: - show_message("") + 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) - queue_item = None + 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")) - 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 + 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) - 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: + else: + self.button(x, y, _("Forget account"), lastfm.auth3) - p = pctl.get_track(ref).parent_folder_path - p = "" - if pctl.playing_ready(): - p = pctl.playing_object().parent_folder_path + x = x0 + 230 * gui.scale + y = y0 + round(130 * gui.scale) - # TODO: fixme for network tracks + # self.toggle_square(x, y, toggle_scrobble_mark, "Show scrobble marker") - for i, item in enumerate(pctl.force_queue): + 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) - 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 + 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) - 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 + # y += 26 * gui.scale + # self.button(x, y, _("Clear local loves"), self.clear_local_loves, width=ww) -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") + y += 26 * gui.scale -def toggle_lyrics_show(a): - return not gui.combo_mode + 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) -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") + 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) - if gui.combo_mode: - colour = colours.menu_text_disabled - return [colour, colours.menu_background, line] + y += 33 * gui.scale -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") + 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 gui.combo_mode or not prefs.show_side_lyrics_art_panel: - colour = colours.menu_text_disabled + y += 25 * gui.scale - return [colour, colours.menu_background, line] + self.toggle_square( + x, y, toggle_scrobble_mark, + _("Show threshold marker")) -def toggle_lyrics_panel_position(): - prefs.lyric_metadata_panel_top ^= True + if self.account_view == 2: -def lyrics_in_side_show(track_object: TrackClass): - if gui.combo_mode or not prefs.show_lyrics_side: - return False - return True + ddt.text((x, y), "ListenBrainz", colours.box_sub_text, 213) -def toggle_side_art(): - prefs.show_side_lyrics_art_panel ^= True + y += 30 * gui.scale + self.button(x, y, _("Paste Token"), lb.paste_key) -def toggle_lyrics_deco(track_object: TrackClass): - colour = colours.menu_text + self.button(x + ddt.get_text_w(_("Paste Token"), 211) + 21 * gui.scale, y, _("Clear"), lb.clear_key) - 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] + y += 35 * gui.scale - if prefs.side_panel_layout == 1: # and prefs.show_side_art: + if prefs.lb_token: + line = prefs.lb_token + ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), line, colours.box_input_text, 212) - 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] + 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 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 coll(link_rect2): + if not self.click: + gui.cursor_want = 3 -def toggle_lyrics(track_object: TrackClass): - if not track_object: - return + if self.click: + webbrowser.open(link_pa2[2], new=2, autoraise=True) - 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: + def clear_local_loves(self): - # Handling for alt panel layout - # if prefs.side_panel_layout == 1 and prefs.show_side_art: - # #prefs.show_side_art = False - # prefs.show_lyrics_side = True - # return + if 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 - 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") + for key, star in star_store.db.items(): + star[1] = star[1].replace("L", "") + star_store.db[key] = star -def get_lyric_fire(track_object: TrackClass, silent: bool = False) -> str | None: - lyrics_ren.lyrics_position = 0 + gui.pl_update += 1 + show_message(_("Cleared all loves"), mode="done") - 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 + def get_scrobble_counts(self): - 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 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 t < -65: - show_message(_("Stop requesting lyrics AAAAAA."), mode="error") + 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.")) - # If the user keeps pressing, lets mess with them haha - lyrics_fetch_timer.force_set(t - 5) + def clear_scrobble_counts(self): - return "later" + for track in pctl.master_library.values(): + track.lfm_scrobbles = 0 - if t > 0: - lyrics_fetch_timer.set() - t = 0 + show_message(_("Cleared all scrobble counts"), mode="done") - lyrics_fetch_timer.force_set(t - 10) + def get_friend_love(self): - if not silent: - show_message(_("Searching...")) + 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 - s_artist = track_object.artist - s_title = track_object.title + 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.")) - 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 get_user_love(self): - logging.info(f"Searching for lyrics: {s_artist} - {s_title}") + 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.")) - found = False - for name in prefs.lyrics_enables: + def codec_config(self, x0, y0, w0, h0): - if name in lyric_sources.keys(): - func = lyric_sources[name] - - 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 = x0 + round(25 * gui.scale) + y = y0 - if not found: - logging.error(f"Could not find lyrics from source {name}") + y += 20 * gui.scale + ddt.text_background_colour = colours.box_background - 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 self.sync_view: -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 + pl = None + if prefs.sync_playlist: + pl = id_to_pl(prefs.sync_playlist) + if pl is None: + prefs.sync_playlist = None - shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object])) - shoot_dl.daemon = True - shoot_dl.start() + 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) - logging.info("..Done") + y += 25 * gui.scale + ww = ddt.text((x, y), _("Path to device music folder: "), colours.box_text_label, 11) + y += 20 * gui.scale -def get_lyric_wiki_silent(track_object: TrackClass): - logging.info("Searching for lyrics...") + 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) - if track_object.artist == "" or track_object.title == "": - return + 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.")) - shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object, True])) - shoot_dl.daemon = True - shoot_dl.start() + power_bar_icon.render(rect[0], rect[1], colour) + y += 30 * gui.scale - logging.info("..Done") + 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 -def test_auto_lyrics(track_object: TrackClass): - if not track_object: - return + text = _("Start Transcode and Sync") + ww = ddt.get_text_w(text, 211) + 25 * gui.scale + if prefs.bypass_transcode: + text = _("Start Sync") - 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) + 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") -def get_bio(track_object: TrackClass): - if track_object.artist != "": - lastfm.get_bio(track_object.artist) + y += 60 * gui.scale -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 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") -def toggle_synced_lyrics(tr): - prefs.prefer_synced_lyrics ^= True + return -def toggle_synced_lyrics_deco(track): - if prefs.prefer_synced_lyrics: - text = _("Show static lyrics") - else: - text = _("Show synced lyrics") - if timed_lyrics_ren.generate(track) and track.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled - if not track.lyrics: - text = _("Show static lyrics") - if not timed_lyrics_ren.generate(track): - text = _("Show synced lyrics") + # ---------- - return [line_colour, colours.menu_background, text] + ddt.text((x, y + 13 * gui.scale), _("Output codec setting:"), colours.box_text_label, 11) -def paste_lyrics_deco(): - if SDL_HasClipboardText(): - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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) - return [line_colour, colours.menu_background, None] + 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 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") + 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 -def chord_lyrics_paste_show_test(_) -> bool: - return gui.combo_mode and prefs.guitar_chords + # 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 copy_lyrics_deco(track_object: TrackClass): - if track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if prefs.transcode_codec != "flac": + y += 35 * gui.scale - return [line_colour, colours.menu_background, None] + prefs.transcode_bitrate = self.slide_control(x, y, _("Bitrate"), "kbs", prefs.transcode_bitrate, 32, 320, 8) -def copy_lyrics(track_object: TrackClass): - copy_to_clipboard(track_object.lyrics) + y -= 1 * gui.scale + x += 280 * gui.scale -def clear_lyrics(track_object: TrackClass): - track_object.lyrics = "" + x = x0 + round(20 * gui.scale) + y = y0 + 215 * gui.scale -def clear_lyrics_deco(track_object: TrackClass): - if track_object.lyrics: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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")) - return [line_colour, colours.menu_background, None] + def devance_theme(self): + global theme -def split_lyrics(track_object: TrackClass): - if track_object.lyrics != "": - track_object.lyrics = track_object.lyrics.replace(". ", ". \n") - else: - pass + theme -= 1 + gui.reload_theme = True + if theme < 0: + theme = len(get_themes()) -def show_sub_search(track_object: TrackClass): - sub_lyrics_box.activate(track_object) + def config_b(self, x0, y0, w0, h0): -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 + global album_mode_art_size + global update_layout -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 + ddt.text_background_colour = colours.box_background + x = x0 + round(25 * gui.scale) + y = y0 + round(20 * gui.scale) - if save_embed_img_disable_test(track_object): - show_message(_("Saving network images not implemented")) - return + # ddt.text((x, y), _("Window"),colours.box_text_label, 12) - try: - pic = album_art_gen.get_embed(track_object) + if system == "Linux": + self.toggle_square(x, y, toggle_notifications, _("Emit track change notifications")) - if not pic: - show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") - return + y += 25 * gui.scale + self.toggle_square(x, y, toggle_borderless, _("Draw own window decorations")) - source_image = io.BytesIO(pic) - im = Image.open(source_image) + # y += 25 * gui.scale + # prefs.save_window_position = self.toggle_square(x, y, prefs.save_window_position, + # _("Restore window position on restart")) - source_image.close() + y += 25 * gui.scale + if not draw_border: + self.toggle_square(x, y, toggle_titlebar_line, _("Show playing in titlebar")) - ext = "." + im.format.lower() - if im.format == "JPEG": - ext = ".jpg" + #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")) - target = os.path.join(folder, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) + 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")) - if len(pic) > 30: - with open(target, "wb") as w: - w.write(pic) + y += 25 * gui.scale + self.toggle_square(x, y, toggle_level_meter, _("Top-panel visualiser")) - open_folder(track_object.index) + y += 25 * gui.scale + if prefs.backend == 4: + self.toggle_square(x, y, toggle_showcase_vis, _("Showcase visualisation")) - except Exception: - logging.exception("Unknown error trying to save an image") - show_message(_("Image save error."), _("A mysterious error occurred"), mode="error") + y += round(30 * gui.scale) + # if not msys: + # y += round(15 * 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) + ddt.text((x, y), _("UI scale for HiDPI displays"), colours.box_text_label, 12) - if info is None: - return [colours.menu_text_disabled, colours.menu_background, None] + y += round(25 * gui.scale) - line_colour = colours.menu_text + sw = round(200 * gui.scale) + sh = round(2 * gui.scale) - return [line_colour, colours.menu_background, None] + slider = (x, y, sw, sh) -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 + gh = round(14 * gui.scale) + gw = round(8 * gui.scale) + grip = [0, y - (gh // 2), gw, gh] -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) + grip[0] = x + grip[0] += ((prefs.scale_want - 0.5) / 3 * sw) -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) + 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 info is None: - return [colours.menu_text_disabled, colours.menu_background, None] + 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 - if info[0] == 1: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + text = str(prefs.scale_want) + if len(text) == 3: + text += "0" + text += "x" - return [line_colour, colours.menu_background, None] + if prefs.x_scale: + text = "auto" -def cycle_image_deco(track_object: TrackClass): - info = album_art_gen.get_info(track_object) + 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 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 + 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) - return [line_colour, colours.menu_background, None] + 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) -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) + y += round(23 * gui.scale) + self.toggle_square(x, y, self.toggle_x_scale, _("Auto scale")) - if info is not None and info[1] > 1: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + if prefs.scale_want != gui.scale: + gui.update += 1 + if not mouse_down: + gui.update_layout() - return [line_colour, colours.menu_background, None] + 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 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) + def toggle_x_scale(self, mode=0): + if mode == 1: + return prefs.x_scale + prefs.x_scale ^= True + auto_scale() + gui.update_layout() -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) + def about(self, x0, y0, w0, h0): -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] + x = x0 + int(w0 * 0.3) - 10 * gui.scale + y = y0 + 85 * gui.scale -def download_art1(tr): - if tr.is_network: - show_message(_("Cannot download art for network tracks.")) - return + ddt.text_background_colour = colours.box_background - # Determine noise of folder ---------------- - siblings = [] - parent = tr.parent_folder_path + icon_rect = (x - 110 * gui.scale, y - 15 * gui.scale, self.about_image.w, self.about_image.h) - 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) + genre = "" + if pctl.playing_object() is not None: + genre = pctl.playing_object().genre.lower() - album_tags = [] - date_tags = [] + 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 = "" - for tr in siblings: - album_tags.append(tr.album) - date_tags.append(tr.date) + if not genre: + self.about_image.render(icon_rect[0], icon_rect[1]) - album_tags = set(album_tags) - date_tags = set(date_tags) + x += 20 * gui.scale + y -= 10 * 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 + self.title_image.render(x - 1, y, alpha_mod(colours.box_sub_text, 240)) - # ------------------------------------------- + credit_pages = 5 - if not os.path.isdir(tr.parent_folder_path): - show_message(_("Directory missing.")) - return + if self.click and coll(icon_rect) and self.ani_cred == 0: + self.ani_cred = 1 + self.ani_fade_on_timer.set() - try: - show_message(_("Looking up MusicBrainz ID...")) + fade = 0 - if "musicbrainz_releasegroupid" not in tr.misc or "musicbrainz_artistids" not in tr.misc or not tr.misc[ - "musicbrainz_artistids"]: + if self.ani_cred == 1: + t = self.ani_fade_on_timer.get() + fade = round(t / 0.7 * 255) + fade = min(fade, 255) - logging.info("MusicBrainz ID lookup...") + 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() - artist = tr.album_artist - if not tr.album: - return - if not artist: - artist = tr.artist + gui.update = 2 - s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=1) + if self.ani_cred == 2: - album_id = s["release-group-list"][0]["id"] - artist_id = s["release-group-list"][0]["artist-credit"][0]["artist"]["id"] + 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 - logging.info("Found release group ID: " + album_id) - logging.info("Found artist ID: " + artist_id) + gui.update = 2 - else: + y += 32 * gui.scale - album_id = tr.misc["musicbrainz_releasegroupid"] - artist_id = tr.misc["musicbrainz_artistids"][0] + block_y = y - 10 * gui.scale - logging.info("Using tagged release group ID: " + album_id) - logging.info("Using tagged artist ID: " + artist_id) + if self.cred_page == 0: - if prefs.enable_fanart_cover: - try: - show_message(_("Searching fanart.tv for cover art...")) + 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) - r = requests.get("https://webservice.fanart.tv/v3/music/albums/" \ - + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) + 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) - artlink = r.json()["albums"][album_id]["albumcover"][0]["url"] - id = r.json()["albums"][album_id]["albumcover"][0]["id"] + fields.add(link_rect) - response = urllib.request.urlopen(artlink, context=tls_context) - info = response.info() + 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) - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - t.seek(0, 2) - l = t.tell() - t.seek(0) + elif self.cred_page == 1: - if info.get_content_maintype() == "image" and l > 1000: + y += 15 * gui.scale - 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 + 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) - f = open(filepath, "wb") - f.write(t.read()) - f.close() + 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) - show_message(_("Cover art downloaded from fanart.tv"), mode="done") - # clear_img_cache() - for track_id in default_playlist: - if tr.parent_folder_path == pctl.get_track(track_id).parent_folder_path: - clear_track_image_cache(pctl.get_track(track_id)) - return - except Exception: - logging.exception("Failed to get from fanart.tv") - 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() + 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") - show_message(_("Cover art downloaded from MusicBrainz"), mode="done") - # clear_img_cache() - clear_track_image_cache(tr) + 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") - 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)) + 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") - return + 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") - except Exception: - logging.exception("Matching cover art or ID could not be found.") - show_message(_("Matching cover art or ID could not be found.")) + 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") -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 -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() + 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 remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | None: - """Return amount of removed objects or None""" - index = track_object.index + 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 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) + 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") - removed = 0 - if not dry: - pr = pctl.stop(True) - try: - for item in tracks: + 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") - tr = pctl.get_track(item) + # 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") - if tr.is_cue: - continue + 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 tr.is_network: - continue + 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") - 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") + 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") - 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") + 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") - 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") + 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 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") + 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") - clear_track_image_cache(tr) + 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") - except Exception: - logging.exception("Image remove error") - show_message(_("Image remove error"), mode="error") - return None + 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 dry: - return removed + 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 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() + 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") -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") + 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") -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) + 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") - text = _("Delete Image File") - line_colour = colours.menu_text + 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") - if info is None or track_object.is_network: - return [colours.menu_text_disabled, colours.menu_background, None] + ddt.rect((x, block_y, 369 * gui.scale, 140 * gui.scale), alpha_mod(colours.box_background, fade)) - if info and info[0] == 0: - text = _("Delete Image File") + y = y0 + h0 - round(33 * gui.scale) + x = x0 + w0 - 0 * gui.scale - 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 + w = max(ddt.get_text_w(_("Credits"), 211), ddt.get_text_w(_("Next"), 211)) + x -= w + round(40 * gui.scale) - text = _("Delete Embedded | Folder") - if key_shift_down or key_shiftr_down: - text = _("Delete Embedded | Track") + 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() - return [line_colour, colours.menu_background, text] + def topchart(self, x0, y0, w0, h0): -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") + x = x0 + round(25 * gui.scale) + y = y0 + 20 * gui.scale -def toggle_gimage(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_gimage - prefs.show_gimage ^= True - return None + ddt.text_background_colour = colours.box_background -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.text((x, y), _("Chart Grid Generator"), colours.box_text, 214) - return [line_colour, colours.menu_background, None] + 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 -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) + y += 30 * gui.scale -def append_here(): - global cargo - global default_playlist - default_playlist += cargo + 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) -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") + 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 - if active: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + else: - return [line_colour, colours.menu_background, line] + 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 lightning_move_test(discard): - return gui.lightning_copy and prefs.show_transfer + y += 35 * gui.scale + x += 5 * gui.scale -# 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] + 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 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 + y -= 25 * gui.scale + x += 170 * gui.scale -def re_template_word(word, tr): - if word == "aa" or word == "albumartist": + 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 tr.album_artist: - return tr.album_artist - return tr.artist + x = x0 + 15 * gui.scale + 320 * gui.scale + y = y0 + 100 * gui.scale - if word == "a" or word == "artist": - return tr.artist + # . Limited width. Max 13 chars + if self.button(x, y, _("Randomise BG")): - if word == "t" or word == "title": - return tr.title + r = round(random.random() * 40) + g = round(random.random() * 40) + b = round(random.random() * 40) - 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) + prefs.chart_bg = [r, g, b] - if word == "sn" or word == "singlenumber" or word == "singletracknumber" or word == "s": - return str(tr.track_number) + d = random.randrange(0, 4) - if word == "d" or word == "date" or word == "year": - return str(tr.date) + if d == 1: + c = 5 + round(random.random() * 20) + prefs.chart_bg = [c, c, c] - if word == "b" or "album" in word: - return str(tr.album) + x += 100 * gui.scale + y -= 20 * gui.scale - if word == "g" or word == "genre": - return tr.genre + display_colour = (prefs.chart_bg[0], prefs.chart_bg[1], prefs.chart_bg[2], 255) - if word == "x" or "ext" in word or "file" in word: - return tr.file_ext.lower() - - if word == "ux" or "upper" in word: - return tr.file_ext.upper() - - if word == "c" or "composer" in word: - return tr.composer - - if "comment" in word: - return tr.comment.replace("\n", "").replace("\r", "") + rect = (x, y, 70 * gui.scale, 70 * gui.scale) + ddt.rect(rect, display_colour) - return "" + ddt.rect_s(rect, (50, 50, 50, 255), round(1 * gui.scale)) -def parse_template2(string: str, track_object: TrackClass, strict: bool = False): - temp = "" - out = "" + # x = self.box_x + self.item_x_offset + 200 * gui.scale + # y = self.box_y + 180 * gui.scale - mode = 0 + x = x0 + 260 * gui.scale + y = y0 + 180 * gui.scale - for c in string: + dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) - if mode == 0: + x = x0 + round(110 * gui.scale) + y = y0 + 240 * gui.scale - if c == "<": - mode = 1 + # . 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: - out += c + shoot = threading.Thread(target=gen_chart) + shoot.daemon = True + shoot.start() + gui.generating_chart = True + x += round(95 * gui.scale) + if gui.generating_chart: + ddt.text((x, y + round(1 * gui.scale)), _("Generating..."), colours.box_text_label, 12) else: - if c == ">": - - test = re_template_word(temp, track_object) - if strict: - assert test - out += test + 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 - mode = 0 - temp = "" + line = _("{N} Album chart").format(N=str(count)) - else: + ww = ddt.text((x, y + round(1 * gui.scale)), line, colours.box_text_label, 12) - temp += c + 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) - if "<und" in string: - out = out.replace(" ", "_") + x = x0 + round(20 * gui.scale) + y = y0 + 240 * gui.scale - return parse_template(out, track_object, strict=strict) + # . Limited width. Max 8 chars + if self.button(x, y, _("Return"), width=75 * gui.scale): + self.chart_view = 0 -def parse_template(string, track_object: TrackClass, up_ext: bool = False, strict: bool = False): - set = 0 - underscore = False - output = "" + def stats(self, x0, y0, w0, h0): - 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 + x = x0 + 10 * gui.scale + y = y0 - output = output.rstrip(" -").lstrip(" -") + if self.chart_view == 1: + self.topchart(x0, y0, w0, h0) + return - if underscore: - output = output.replace(" ", "_") + 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 - # Attempt to ensure the output text is filename safe - output = filename_safe(output) + ddt.text_background_colour = colours.box_background + lt_font = 312 + lt_colour = colours.box_text_label - return output + 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) -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] + x1 = x + (8 + 10 + 10) * gui.scale + x2 = x1 + max(w1, w2, w3, w4, w5, w6) + 20 * gui.scale + y1 = y + 50 * gui.scale - if 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) + 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() - rename_playlist_box.y = min(rename_playlist_box.y, round(350 * gui.scale)) + album_names = set() + folder_names = set() + count = 0 - if rename_playlist_box.y < gui.panelY: - rename_playlist_box.y = gui.panelY + 10 * gui.scale + for track_id in default_playlist: + tr = pctl.get_track(track_id) - if gui.radio_view: - rename_text_area.set_text(pctl.radio_playlists[index]["name"]) - else: - rename_text_area.set_text(pctl.multi_playlist[index].title) - rename_text_area.highlight_all() - gui.gen_code_errors = False + if 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 generator: - rename_playlist_box.toggle_edit_gen() + self.stats_pl_albums = count -def edit_generator_box(index: int) -> None: - rename_playlist(index, generator=True) + self.stats_pl_length = 0 + for item in default_playlist: + self.stats_pl_length += pctl.master_library[item].length -def pin_playlist_toggle(pl: int) -> None: - pctl.multi_playlist[pl].hidden ^= True + line = seconds_to_day_hms(self.stats_pl_length, strings.day, strings.days) -def pl_pin_deco(pl: int): - # if pctl.multi_playlist[pl].hidden == True and tab_menu.pos[1] > + 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) - if pctl.multi_playlist[pl].hidden == True: - return [colours.menu_text, colours.menu_background, _("Pin")] - return [colours.menu_text, colours.menu_background, _("Unpin")] + ddt.text((x2, y1), line, colours.box_sub_text, 12) -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 self.stats_timer.get() > 5: + album_names = set() + folder_names = set() + count = 0 -def view_pl_is_locked(_) -> bool: - return pctl.multi_playlist[pctl.active_playlist_viewing].locked + for pl in pctl.multi_playlist: + for track_id in pl.playlist_ids: + tr = pctl.get_track(track_id) -def pl_is_locked(pl: int) -> bool: - if not pctl.multi_playlist: - return False - return pctl.multi_playlist[pl].locked + 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) -def lock_playlist_toggle(pl: int) -> None: - pctl.multi_playlist[pl].locked ^= True + self.total_albums = count -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 + self.stats_timer.set() -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 + 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) - 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") + 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) - 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 + # Ratio bar + if len(pctl.master_library) > 115 * gui.scale: + x = x0 + y = y0 + h0 - 7 * gui.scale - 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() + full_rect = [x, y, w0, 7 * gui.scale] + d = 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 + # 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 -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 + for key, value in self.ext_ratio.items(): - if not direc: - direc = str(user_directory / "playlists") - if not os.path.exists(direc): - os.makedirs(direc) + colour = [200, 200, 200, 255] + if key in format_colours: + colour = format_colours[key] - target = os.path.join(direc, pctl.multi_playlist[pl].title + ".xspf") + 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] - xspf_root = ET.Element("playlist", version="1", xmlns="http://xspf.org/ns/0/") - xspf_tracklist_tag = ET.SubElement(xspf_root, "trackList") + h = int(round(value / len(pctl.master_library) * full_rect[2])) + block_rect = [full_rect[0] + d, full_rect[1], h, full_rect[3]] - 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) + ddt.rect(block_rect, colour) + d += h - 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) + 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) - ET.SubElement(xspf_track_tag, "duration").text = str(int(track.length * 1000)) + if self.click: + gen_codec_pl(key) + except Exception: + logging.exception("Error draw ext bar") - xspf_tree = ET.ElementTree(xspf_root) - ET.indent(xspf_tree, space=' ', level=0) - xspf_tree.write(target, encoding='UTF-8', xml_declaration=True) + def config_v(self, x0, y0, w0, h0): - 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]) + ddt.text_background_colour = colours.box_background - return target + x = x0 + self.item_x_offset + y = y0 + 17 * gui.scale -def reload(): - if album_mode: - reload_albums(quiet=True) + 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) - # tree_view_box.clear_all() - # elif gui.combo_mode: - # reload_albums(quiet=True) - # combo_pl_render.prep() + 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) -def clear_playlist(index: int): - global default_playlist + 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 pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental erasure")) - return + 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) - pctl.multi_playlist[index].last_folder.clear() # clear import folder list # TODO(Martin): This was actually a string not a list wth? + x = x0 + self.item_x_offset - if not pctl.multi_playlist[index].playlist_ids: - logging.info("Playlist is already empty") - return + # y += round(25 * gui.scale) - li = [] - for i, ref in enumerate(pctl.multi_playlist[index].playlist_ids): - li.append((i, ref)) + # self.toggle_square(x, y, star_line_toggle, _('Show playtime lines')) + y += round(15 * gui.scale) - undo.bk_tracks(index, list(reversed(li))) + # 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) - del pctl.multi_playlist[index].playlist_ids[:] - if pctl.active_playlist_viewing == index: - default_playlist = pctl.multi_playlist[index].playlist_ids - reload() - # pctl.playlist_playing = 0 - pctl.multi_playlist[index].position = 0 - if index == pctl.active_playlist_viewing: - pctl.playlist_view_position = 0 + y += round(25 * gui.scale) - gui.pl_update = 1 + 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 -def convert_playlist(pl: int, get_list: bool = False) -> list[list[int]]| None: - global transcode_list + y += round(25 * gui.scale) - if not tauon.test_ffmpeg(): - return None + 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) - paths: list[str] = [] - folders: list[list[int]] = [] + self.toggle_square(x, y, toggle_append_date, _("Show album release year")) + y += round(25 * gui.scale) - 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) + self.toggle_square(x, y, toggle_append_total_time, _("Show album duration")) + y += round(35 * gui.scale) - 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!")) + 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 - folders.append(folder) + 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 - if get_list: - return folders + 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) - transcode_list.extend(folders) -def get_folder_tracks_local(pl_in: int) -> list[int]: - selection = [] - parent = os.path.normpath(pctl.master_library[default_playlist[pl_in]].parent_folder_path) - while pl_in < len(default_playlist) and parent == os.path.normpath( - pctl.master_library[default_playlist[pl_in]].parent_folder_path): - selection.append(pl_in) - pl_in += 1 - return selection + def set_playlist_cycle(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "cycle" else False + prefs.end_setting = "cycle" + # global pl_follow + # pl_follow = False -def test_pl_tab_locked(pl: int) -> bool: - if gui.radio_view: - return False - return pctl.multi_playlist[pl].locked + def set_playlist_advance(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "advance" else False + prefs.end_setting = "advance" + # global pl_follow + # pl_follow = False -def 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") + def set_playlist_stop(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "stop" else False + prefs.end_setting = "stop" -def move_playlist(source, dest): - global default_playlist - if dest > source: - dest += 1 - try: - active = pctl.multi_playlist[pctl.active_playlist_playing] - view = pctl.multi_playlist[pctl.active_playlist_viewing] + def set_playlist_repeat(self, mode=0): + if mode == 1: + return True if prefs.end_setting == "repeat" else False + prefs.end_setting = "repeat" - temp = pctl.multi_playlist[source] - pctl.multi_playlist[source] = "old" - pctl.multi_playlist.insert(dest, temp) - pctl.multi_playlist.remove("old") + def small_preset(self): - 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") + prefs.playlist_row_height = round(22 * prefs.ui_scale) + prefs.playlist_font_size = 15 + prefs.tracklist_y_text_offset = 0 + gui.update_layout() -def delete_playlist(index: int, force: bool = False, check_lock: bool = False) -> None: - if gui.radio_view: - del pctl.radio_playlists[index] - if not pctl.radio_playlists: - pctl.radio_playlists = [{"uid": uid_gen(), "name": "Default", "items": []}] - return + def large_preset(self): - global default_playlist + prefs.playlist_row_height = round(27 * prefs.ui_scale) + prefs.playlist_font_size = 15 + gui.update_layout() - if check_lock and pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental deletion")) - return + def slide_control(self, x, y, label, units, value, lower_limit, upper_limit, step=1, callback=None, width=58): - if not force: - if pl_is_locked(index): - show_message(_("Playlist is locked to prevent accidental deletion")) - return + width = round(width * gui.scale) - if gui.rename_playlist_box: - return + 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): - # Set screen to be redrawn - gui.pl_update = 1 - gui.update += 1 + if self.click: + if value > lower_limit: + value -= step + gui.update_layout() + if callback is not None: + callback(value) - # 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 mouse_down: + abg = [230, 120, 20, 255] + else: + abg = [220, 150, 20, 255] - # 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 + if colour_value(colours.box_background) > 300: + abg = colours.box_sub_text - # Take note of the id of the playing playlist - old_playing_id = pctl.multi_playlist[pctl.active_playlist_playing].uuid_int + dec_arrow.render(x + 1 * gui.scale, y, abg) - # Take note of the id of the viewed open playlist - old_view_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + x += 33 * gui.scale - # Delete the requested playlist - del pctl.multi_playlist[index] + 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) - # Re-set the open viewed playlist number by uid - for i, pl in enumerate(pctl.multi_playlist): + x += width - 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 + 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): - # Re-initiate the now viewed playlist - if old_view_id != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int: - default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position - logging.debug("Position reset by playlist delete") - pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected - shift_selection = [pctl.selected_in_playlist] + if self.click: + if value < upper_limit: + value += step + gui.update_layout() + if callback is not None: + callback(value) + if mouse_down: + abg = [230, 120, 20, 255] + else: + abg = [220, 150, 20, 255] - if album_mode: - reload_albums(True) - goto_album(pctl.playlist_view_position) + if colour_value(colours.box_background) > 300: + abg = colours.box_sub_text - # Re-set the playing playlist number by uid - for i, pl in enumerate(pctl.multi_playlist): + inc_arrow.render(x + 1 * gui.scale, y, abg) - 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 + return value - test_show_add_home_music() + # def style_up(self): + # prefs.line_style += 1 + # if prefs.line_style > 5: + # prefs.line_style = 1 - # Cleanup - ids = [] - for p in pctl.multi_playlist: - ids.append(p.uuid_int) + def inside(self): - 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] + return coll((self.box_x, self.box_y, self.w, self.h)) - pctl.db_inc += 1 + def init2(self): -def delete_playlist_force(index: int): - delete_playlist(index, force=True, check_lock=True) + self.init2done = True -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) + def close(self): + self.enabled = False + fader.fall() + if gui.opened_config_file: + reload_config_file() -def delete_playlist_ask(index: int): - print("ark") - if gui.radio_view: - delete_playlist_force(index) - return - gen = pctl.gen_codes.get(pl_to_id(index), "") - if (gen and not gen.startswith("self ")) or len(pctl.multi_playlist[index].playlist_ids) < 2: - delete_playlist(index) - return + def render(self): - 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") + if self.init2done is False: + self.init2() -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") + if key_esc_press: + self.close() -# 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)) + tab_width = 115 * gui.scale -def re_import2(pl: int) -> None: - paths = pctl.multi_playlist[pl].last_folder + side_width = 115 * gui.scale + header_width = 0 - reduce_paths(paths) + 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 - 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)) + content_width = round(545 * gui.scale) + content_height = round(275 * gui.scale) # 275 + full_width = content_width + full_height = content_height - if paths: - show_message(_("Rescanning folders..."), mode="info") + full_width += side_width + full_height += header_width -def rescan_all_folders(): - for i, p in enumerate(pctl.multi_playlist): - re_import2(i) + x = int(window_size[0] / 2) - int(full_width / 2) + y = int(window_size[1] / 2) - int(full_height / 2) -def s_append(index: int): - paste(playlist_no=index) + self.box_x = x + self.box_y = y + self.w = full_width + self.h = full_height -def append_playlist(index: int): - global cargo - pctl.multi_playlist[index].playlist_ids += cargo + border_colour = colours.box_border - gui.pl_update = 1 - reload() + 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 index_key(index: int): - tr = pctl.master_library[index] - s = str(tr.track_number) - d = str(tr.disc_number) + current_tab = 0 + tab_height = round(24 * gui.scale) # 30 - if "/" in d: - d = d.split("/")[0] + 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) - # 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 top_mode: + xx = x + yy = y + tab_width = 90 * gui.scale - # 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_a((x, y), (full_width, header_width), tab_bg) - # Use the filename if we dont have any metadata to sort by, - # since it could likely have the track number in it - else: - s = tr.filename + for item in self.tabs: - if (not tr.disc_number or tr.disc_number == "0") and tr.is_cue: - s = tr.filename + "-" + s + if self.click and gui.message_box: + gui.message_box = False - # 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" + box = [xx, yy, tab_width, tab_height] + box2 = [xx, yy, tab_width, tab_height - 1] + fields.add(box2) -def sort_tracK_numbers_album_only(pl: int, custom_list=None): - current_folder = "" - albums = [] - if custom_list is None: - playlist = pctl.multi_playlist[pl].playlist_ids - else: - playlist = custom_list + if self.click and coll(box2): + self.tab_active = current_tab + self.lyrics_panel = False - for i in range(len(playlist)): - if i == 0: - albums.append(i) - current_folder = pctl.master_library[playlist[i]].album - elif pctl.master_library[playlist[i]].album != current_folder: - current_folder = pctl.master_library[playlist[i]].album - albums.append(i) + if 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) - i = 0 - while i < len(albums) - 1: - playlist[albums[i]:albums[i + 1]] = sorted(playlist[albums[i]:albums[i + 1]], key=index_key) - i += 1 - if len(albums) > 0: - playlist[albums[i]:] = sorted(playlist[albums[i]:], key=index_key) + if coll(box2): + ddt.rect(box, tab_over) - gui.pl_update += 1 + alpha = 100 + if current_tab == self.tab_active: + alpha = 240 -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 + ddt.text((xx + (tab_width // 2), yy + 4 * gui.scale, 2), item[0], tab_text, 212) - 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) + current_tab += 1 + xx += tab_width + if current_tab == 6: + yy += round(24 * gui.scale) # 30 + xx = x - 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) + else: - gui.pl_update += 1 + ddt.rect_a((x, y), (tab_width, full_height), tab_bg) -def key_filepath(index: int): - track = pctl.master_library[index] - return track.parent_folder_path.lower(), track.filename + for item in self.tabs: -def key_fullpath(index: int): - return pctl.master_library[index].fullpath + 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 -def key_filename(index: int): - track = pctl.master_library[index] - return track.filename + 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) -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 + if self.click and coll(box2): + self.tab_active = current_tab + self.lyrics_panel = False - if use_natsort and False: - target[:] = natsort.os_sorted(target, key=key_fullpath) - else: - target.sort(key=key_filepath) + 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 append_current_playing(index: int): - if tauon.spot_ctl.coasting: - tauon.spot_ctl.append_playing(index) - gui.pl_update = 1 - return + if coll(box2): + ddt.rect(box, tab_over) - 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 + yy = box[1] + 4 * gui.scale -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 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) - seen_files = {} - seen_types = {} + current_tab += 1 - mp3_bitrates = {} - ogg_bitrates = {} - m4a_bitrates = {} + # ddt.line(x + 110, self.box_y + 1, self.box_x + 110, self.box_y + self.h, colours.grey(50)) - are_cue = 0 + self.tabs[self.tab_active][1](x + side_width, y + header_width, content_width, content_height) - for index in pctl.multi_playlist[pl].playlist_ids: - track = pctl.get_track(index) + self.click = False + self.right_click = False - playlist_time += int(track.length) - play_time += star_store.get(index) + ddt.text_background_colour = colours.box_background - if track.is_cue: - are_cue += 1 +class Fields: + def __init__(self): - 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 + self.id = [] + self.last_id = [] - type = track.file_ext - if type == "OGA": - type = "OGG" - seen_types[type] = seen_types.get(type, 0) + 1 + self.field_array = [] + self.force = 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 + def add(self, rect, callback=None): - total_size = sum(seen_files.values()) + self.field_array.append((rect, callback)) - 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" - - 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" + def test(self): - 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) + if self.force: + self.force = False + return True - line += str(perc) + "%" - line += "\n\n" + self.last_id = self.id + #logging.info(len(self.id)) + self.id = [] - 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 + 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: - line += f"{rate} ({perc}%); " + self.id.append(0) # += "0" - if others: - others = round(others, 1) - if others < 0.1: - others = "<0.1" - line += _("Others") + f"({others}%);" - line = line.rstrip("; ") - line += "\n\n" + if self.last_id == self.id: + return False - 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}%); " + return True - if others: - others = round(others, 1) - if others < 0.1: - others = "<0.1" - line += _("Others") + f"({others}%);" - line = line.rstrip("; ") - line += "\n\n" + def clear(self): - # 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" + self.field_array = [] - line += "\n" + f"-------------- {_('Top Artists')} --------------------" + "\n\n" +class TopPanel: + def __init__(self): - ls = stats_gen.artist_list - for i, item in enumerate(ls[:50]): - line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" + self.height = gui.panelY + self.ty = 0 - 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" + self.start_space_left = round(46 * gui.scale) + self.start_space_compact_left = 46 * gui.scale - 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]) + 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 imported_sort(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + self.ini_menu_space = 17 * gui.scale # 17 + self.menu_space = 17 * gui.scale + self.click_buffer = 4 * gui.scale - og = pctl.multi_playlist[pl].playlist_ids - og.sort(key=lambda x: pctl.get_track(x).index) + self.tabs_right_x = 0 # computed for drag and drop code elsewhere (hacky) + self.tabs_left_x = 1 - reload_albums() - tree_view_box.clear_target_pl(pl) + self.prime_tab = gui.saved_prime_tab + self.prime_side = gui.saved_prime_direction # 0=left, 1=right + self.shown_tabs = [] -def imported_sort_folders(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + # --- + self.space_left = 0 + self.tab_text_spaces = [] + self.index_playing = -1 + self.drag_zone_start_x = 300 * gui.scale - og = pctl.multi_playlist[pl].playlist_ids - og.sort(key=lambda x: pctl.get_track(x).index) + 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) - 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 + self.drag_slide_timer = Timer(100) + self.tab_d_click_timer = Timer(10) + self.tab_d_click_ref = None - og.sort(key=lambda x: first_occurrences[pctl.get_track(x).parent_folder_path]) + self.adds = [] - reload_albums() - tree_view_box.clear_target_pl(pl) + def left_overflow_switch_playlist(self, pl): + self.prime_side = 0 + self.prime_tab = pl + switch_playlist(pl) -def standard_sort(pl: int) -> None: - if pl_is_locked(pl): - show_message(_("Playlist is locked")) - return + def right_overflow_switch_playlist(self, pl): + self.prime_side = 1 + self.prime_tab = pl + switch_playlist(pl) - sort_path_pl(pl) - sort_track_2(pl) - reload_albums() - tree_view_box.clear_target_pl(pl) + def render(self): -def year_s(plt): - sorted_temp = sorted(plt, key=lambda x: x[1]) - temp = [] + # C-TD + global quick_drag + global update_layout - for album in sorted_temp: - temp += album[0] - return temp + hh = gui.panelY2 + yy = gui.panelY - hh + self.height = hh -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 = "" + if quick_drag is True: + # gui.pl_update = 1 + gui.update_on_drag = True - p = 0 - while p < len(playlist): + # Draw the background + ddt.rect((0, 0, window_size[0], gui.panelY), colours.top_panel_background) - track = get_object(playlist[p]) + 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) - 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 = [] + maxx = window_size[0] - (gui.panelY + 30 * gui.scale) + title_colour = colours.grey(249) + if colours.lm: + title_colour = colours.grey(30) + title = tr.title + if not title: + title = tr.filename + artist = tr.artist - if track.album_artist: - album_artist = track.album_artist + if pctl.playing_state == 3 and not radiobox.dummy_track.title: + title = pctl.tag_meta + artist = radiobox.loaded_url # pctl.url - if p > len(playlist) - 1: - break + ddt.text_background_colour = colours.top_panel_background - album = [] - on = get_object(playlist[p]).parent_folder_path - album.append(playlist[p]) - t = 1 + 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) - while t + p < len(playlist) - 1 and get_object(playlist[p + t]).parent_folder_path == on: - album.append(playlist[p + t]) - t += 1 + wwx = 0 + if prefs.left_window_control and not gui.compact_bar: + if gui.macstyle: + wwx = 24 + # wwx = round(64 * gui.scale) + if draw_min_button: + wwx += 20 + if draw_max_button: + wwx += 20 + wwx = round(wwx * gui.scale) + else: + wwx = 26 + # wwx = round(90 * gui.scale) + if draw_min_button: + wwx += 35 + if draw_max_button: + wwx += 33 + wwx = round(wwx * gui.scale) - date = get_object(playlist[p]).date + rect = (wwx + 9 * gui.scale, yy + 4 * gui.scale, 34 * gui.scale, 25 * gui.scale) + fields.add(rect) - # 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 coll(rect) and not prefs.shuffle_lock: + if inp.mouse_click: - # 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 gui.combo_mode: + gui.switch_showcase_off = True + else: + gui.lsp ^= True - plt.append((album, date, artist + " " + get_object(playlist[p]).album)) - p += len(album) - #logging.info(album) + update_layout = True + gui.update += 1 + if mouse_down and quick_drag: + gui.lsp = True + update_layout = True + gui.update += 1 - if plt: - pl2 += year_s(plt) - plt = [] + if middle_click: + toggle_left_last() + update_layout = True + gui.update += 1 - if custom_list is not None: - return pl2 + if right_click: + # prefs.artist_list ^= True + lsp_menu.activate(position=(5 * gui.scale, gui.panelY)) + update_layout_do() - # 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) + colour = colours.corner_button # [230, 230, 230, 255] -def pl_toggle_playlist_break(ref): - pctl.multi_playlist[ref].hide_title ^= 1 - gui.pl_update = 1 + if gui.lsp: + colour = colours.corner_button_active + if gui.combo_mode: + colour = colours.corner_button + if coll(rect): + colour = colours.corner_button_active -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 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) - return title + # 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) -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 + if playlist_box.drag: + drag_mode = False - title = gen_unique_pl_title(_("New Playlist")) + # Need to test length + self.tab_text_spaces = [] - top_panel.prime_side = 1 - top_panel.prime_tab = len(pctl.multi_playlist) + 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) - 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 + x = self.start_space_left + wwx + y = yy # self.ty -def append_deco(): - if pctl.playing_state > 0: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + # 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 - text = None - if tauon.spot_ctl.coasting: - text = _("Add Spotify Album") + p_text_len = 180 * gui.scale + right_space_es = p_text_len + offset - return [line_colour, colours.menu_background, text] + x_start = x -def rescan_deco(pl: int): - if pctl.multi_playlist[pl].last_folder: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + 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 - # base = os.path.basename(pctl.multi_playlist[pl].last_folder) + # List all tabs eligible to be shown + #logging.info("-------------") + ready_tabs = [] + show_tabs = [] - return [line_colour, colours.menu_background, None] + 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)) -def regenerate_deco(pl: int): - id = pl_to_id(pl) - value = pctl.gen_codes.get(id) + left_tabs = [] + right_tabs = [] + if prefs.shuffle_lock: + for p in ready_tabs: + left_tabs.append(p) - if value: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + else: + for p in ready_tabs: + if p < self.prime_tab: + left_tabs.append(p) - return [line_colour, colours.menu_background, None] + for p in ready_tabs: + if p > self.prime_tab: + right_tabs.append(p) + left_tabs.reverse() -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 - - current += cha + run = max_w - if inquote and cha != "\"": - q_string += cha + if self.prime_tab in ready_tabs: + size = self.tab_text_spaces[self.prime_tab] + self.tab_extra_width + if size < run: + show_tabs.append(self.prime_tab) + run -= size - if current: - cmds.append(current) - quotes.append(q_string) + 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 - return cmds, quotes, inquote + # 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 -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 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) - urls = [] - playlist = pctl.multi_playlist[pl].playlist_ids + x += 17 * gui.scale + x_start = x - 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 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])) - if warn: - show_message(_("Playlist contains non-Spotify tracks"), mode="error") - return + xx = x + (max_w - run) # + round(6 * gui.scale) + self.tabs_left_x = x_start - 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 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])) -def regenerate_playlist(pl: int = -1, silent: bool = False, id: int | None = None) -> None: - if id is None and pl == -1: - return + if gui.radio_view: + if not mouse_down and pctl.radio_playlist_viewing not in show_tabs and pctl.radio_playlist_viewing in ready_tabs: + if pctl.radio_playlist_viewing < self.prime_tab: + self.prime_side = 0 + elif pctl.radio_playlist_viewing > self.prime_tab: + self.prime_side = 1 + self.prime_tab = pctl.radio_playlist_viewing + gui.update += 1 + elif not mouse_down and pctl.active_playlist_viewing not in show_tabs and pctl.active_playlist_viewing in ready_tabs: + if pctl.active_playlist_viewing < self.prime_tab: + self.prime_side = 0 + elif pctl.active_playlist_viewing > self.prime_tab: + self.prime_side = 1 + self.prime_tab = pctl.active_playlist_viewing + gui.update += 1 - if id is None: - id = pl_to_id(pl) + 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() - if pl == -1: - pl = id_to_pl(id) - if pl is None: - return + # TAB INPUT PROCESSING + target = pctl.multi_playlist + if gui.radio_view: + target = pctl.radio_playlists + for i, tab in enumerate(target): - source_playlist = pctl.multi_playlist[pl].playlist_ids + if not gui.radio_view: + if not prefs.tabs_on_top or prefs.shuffle_lock: + break - string = pctl.gen_codes.get(id) - if not string: - if not silent: - show_message(_("This playlist has no generator")) - return + if len(pctl.multi_playlist) != len(self.tab_text_spaces): + break - cmds, quotes, inquote = parse_generator(string) + if i not in show_tabs: + continue - if inquote: - gui.gen_code_errors = "close" - return + # Determine the tab width + tab_width = self.tab_text_spaces[i] + self.tab_extra_width - playlist = [] - selections = [] - errors = False - selections_searched = 0 + # Save the far right boundary of the tabs (hacky) + self.tabs_right_x = x + tab_width - def is_source_type(code: str | None) -> bool: - return \ - code is None or \ - code == "" or \ - code.startswith(("self", "jelly", "plex", "koel", "tau", "air", "sal")) + # 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) - #logging.info(cmds) - #logging.info(quotes) + # 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: - pctl.regen_in_progress = True + 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) - for i, cm in enumerate(cmds): + # 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() - quote = quotes[i] + # Drag to move playlist + if mouse_up and playlist_box.drag and coll_point(mouse_up_position, f_rect): - if cm.startswith("\"") and (cm.endswith((">", "<"))): - cm_found = False + 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 - for col in column_names: + if i != playlist_box.drag_on: - if quote.lower() == col.lower() or _(quote).lower() == col.lower(): - cm_found = True + # # Reveal the tab in case it has been hidden + # pctl.multi_playlist[playlist_box.drag_on].hidden = False - if cm[-1] == ">": - sort_ass(0, invert=False, custom_list=playlist, custom_name=col) - elif cm[-1] == "<": - sort_ass(0, invert=True, custom_list=playlist, custom_name=col) - break - if cm_found: - continue + 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) - elif cm == "self": - selections.append(pctl.multi_playlist[pl].playlist_ids) + playlist_box.drag = False + gui.update += 1 - elif cm == "auto": - pass + # Delete playlist on wheel click + elif tab_menu.active is False and middle_click: + # delete_playlist(i) + delete_playlist_ask(i) + break - elif cm.startswith("spl\""): - playlist.extend(tauon.spot_ctl.playlist(quote, return_list=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 - elif cm.startswith("tpl\""): - playlist.extend(tauon.tidal.playlist(quote, return_list=True)) + # 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 - elif cm == "tfa": - playlist.extend(tauon.tidal.fav_albums(return_list=True)) + 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 - elif cm == "tft": - playlist.extend(tauon.tidal.fav_tracks(return_list=True)) + 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") - elif cm.startswith("tar\""): - playlist.extend(tauon.tidal.artist(quote, return_list=True)) + 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"]) - elif cm.startswith("tmix\""): - playlist.extend(tauon.tidal.mix(quote, return_list=True)) + radio_view.drag = None - elif cm == "sal": - playlist.extend(tauon.spot_ctl.get_library_albums(return_list=True)) + x += tab_width + self.tab_spacing - elif cm == "slt": - playlist.extend(tauon.spot_ctl.get_library_likes(return_list=True)) + # Test dupelicate tab function + if playlist_box.drag: + rect = (0, x, self.height, window_size[0]) + fields.add(rect) - elif cm == "plex": - if not plex.scanning: - playlist.extend(plex.get_albums(return_list=True)) + 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) - elif cm.startswith("jelly\""): - if not jellyfin.scanning: - playlist.extend(jellyfin.get_playlist(quote, return_list=True)) + else: + if playlist_box.drag_source == 1: + pctl.multi_playlist[playlist_box.drag_on].hidden = False - elif cm == "jelly": - if not jellyfin.scanning: - playlist.extend(jellyfin.ingest_library(return_list=True)) + move_playlist(playlist_box.drag_on, i) + playlist_box.drag = False - elif cm == "koel": - if not koel.scanning: - playlist.extend(koel.get_albums(return_list=True)) + # Need to test length again + # Need to test length + self.tab_text_spaces = [] - elif cm == "tau": - if not tau.processing: - playlist.extend(tau.get_playlist(pctl.multi_playlist[pl].title, return_list=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) - elif cm == "air": - if not subsonic.scanning: - playlist.extend(subsonic.get_music3(return_list=True)) + # Reset X draw position + x = x_start + bar_highlight_size = round(2 * gui.scale) - 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) + # TAB DRAWING + shown = [] + for i, tab in enumerate(target): - temp = [] - for selection in selections: - temp += selection + if not gui.radio_view: + if not prefs.tabs_on_top or prefs.shuffle_lock: + break - playlist += list(OrderedDict.fromkeys(temp)) - selections.clear() + if len(pctl.multi_playlist) != len(self.tab_text_spaces): + break - elif cm == "cue": + # if tab.hidden is True: + # continue - 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 i not in show_tabs: + continue - 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)) + # if window_size[0] - x - (self.tab_text_spaces[i] + self.tab_extra_width) < right_space_es: + # break - 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)) + shown.append(i) - elif cm.startswith("ext"): - value = quote.upper() - if value: - if not selections: - for plist in pctl.multi_playlist: - selections.append(plist.playlist_ids) + tab_width = self.tab_text_spaces[i] + self.tab_extra_width + rect = [x, y, tab_width, self.height] - temp = [] - for selection in selections: - for track in selection: - tr = pctl.get_track(track) - if tr.file_ext == value: - temp.append(track) + # 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 - playlist += list(OrderedDict.fromkeys(temp)) + # 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 - elif cm == "ypa": - playlist = year_sort(0, playlist) + # Draw tab background + ddt.rect(rect, bg) + if playing_hint: + ddt.rect(rect, [255, 255, 255, 7]) - elif cm == "tn": - sort_track_2(0, playlist) + # Determine text colour + if active: + fg = colours.tab_text_active + else: + fg = colours.tab_text - elif cm == "ia>": - playlist = gen_last_imported_folders(0, playlist) + # 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) - elif cm == "ia<": - playlist = gen_last_imported_folders(0, playlist, reverse=True) + # 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 - elif cm == "m>": - playlist = gen_last_modified(0, playlist) + # Drag to move playlist + if tab_hit: + if mouse_down and i != playlist_box.drag_on and playlist_box.drag is True: - elif cm == "m<": - playlist = gen_last_modified(0, playlist, reverse=False) + if key_shift_down: + ddt.rect((x, y + self.height - bar_highlight_size, tab_width, bar_highlight_size), [80, 160, 200, 255]) + elif playlist_box.drag_on < i: + ddt.rect((x + tab_width - bar_highlight_size, y, bar_highlight_size, gui.panelY2), [80, 160, 200, 255]) + else: + ddt.rect((x, y, bar_highlight_size, gui.panelY2), [80, 160, 200, 255]) - elif cm == "ly" or cm == "lyrics": - playlist = gen_lyrics(0, playlist) + 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]) - elif cm == "l" or cm == "love" or cm == "loved": - playlist = gen_love(0, playlist) - - elif cm == "clr": - selections.clear() + 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 - elif cm == "rv" or cm == "reverse": - playlist = gen_reverse(0, playlist) + ddt.text( + (x + tab_width - 3, int(round(ay)), 1), "+" + str(self.adds[k][1]), colours.pluse_colour, 212, bg=bg) + gui.update += 1 - elif cm == "rva": - playlist = gen_folder_reverse(0, playlist) + x += tab_width + self.tab_spacing - elif cm == "rata>": + # 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]) - playlist = gen_folder_top_rating(0, custom_list=playlist) + if mouse_up: + drop_tracks_to_new_playlist(shift_selection) - elif cm == "rat>": + # 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]) - def rat_key(track_id): - return star_store.get_rating(track_id) + if prefs.tabs_on_top and right_overflow: + x += 24 * gui.scale + self.tabs_right_x += 24 * gui.scale - playlist = sorted(playlist, key=rat_key, reverse=True) + # ------------- + # Other input + if mouse_up: + quick_drag = False + playlist_box.drag = False + radio_view.drag = None - elif cm == "rat<": + # 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 rat_key(track_id): - return star_store.get_rating(track_id) + cycle_playlist_pinned(mouse_wheel) - playlist = sorted(playlist, key=rat_key) + 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 - 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 + x += self.ini_menu_space + y += 7 * gui.scale + ddt.text_background_colour = colours.top_panel_background - 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 + # MENU ----------------------------- - 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 + 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) - 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 (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) - elif cm == "norat": - temp = [] - for item in playlist: - if star_store.get_rating(item) == 0: - temp.append(item) - playlist = temp + 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) - elif cm == "d>": - playlist = gen_sort_len(0, custom_list=playlist) + # 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)) + # - elif cm == "d<": - playlist = gen_sort_len(0, custom_list=playlist) - playlist = list(reversed(playlist)) + dl = len(dl_mon.ready) + watching = len(dl_mon.watching) - 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 (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) - 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 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 - elif cm == "path": - sort_path_pl(0, custom_list=playlist) + 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 - elif cm == "pa>": - playlist = gen_folder_top(0, custom_list=playlist) + load_orders.append(copy.deepcopy(load_order)) - elif cm == "pa<": - playlist = gen_folder_top(0, custom_list=playlist) - playlist = gen_folder_reverse(0, playlist) + if len(dl_mon.ready) > 0: + dl_mon.ready.clear() + switch_playlist(pln) - elif cm == "pt>" or cm == "pc>": - playlist = gen_top_100(0, custom_list=playlist) + 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") - elif cm == "pt<" or cm == "pc<": - playlist = gen_top_100(0, custom_list=playlist) - playlist = list(reversed(playlist)) - 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] + 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] - 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] + 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] - 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] + # LAYOUT -------------------------------- + x += self.menu_space + word_length - elif cm[:3] == "pc<": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - for i in reversed(range(len(playlist))): - t_time = star_store.get(playlist[i]) - tr = pctl.get_track(playlist[i]) - if tr.length > 0: - if not value > t_time / tr.length: - del playlist[i] + self.drag_zone_start_x = x - 5 * gui.scale + status = True - elif cm == "y<": - playlist = gen_sort_date(0, False, playlist) + if loading_in_progress: - elif cm == "y>": - playlist = gen_sort_date(0, True, playlist) + bg = colours.status_info_text + if to_got == "xspf": + text = _("Importing XSPF playlist") + elif to_got == "xspfl": + text = _("Importing XSPF playlist...") + elif to_got == "ex": + text = _("Extracting Archive...") + else: + text = _("Importing... ") + str(to_got) # + "/" + str(to_get) + if right_click and coll([x, y, 180 * gui.scale, 18 * gui.scale]): + cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + elif after_scan: + # bg = colours.status_info_text + bg = [100, 200, 100, 255] + text = _("Scanning Tags... {N} remaining").format(N=str(len(after_scan))) + elif move_in_progress: + text = _("File copy in progress...") + bg = colours.status_info_text + elif cm_clean_db and to_get > 0: + per = str(int(to_got / to_get * 100)) + text = _("Cleaning db... ") + per + "%" + bg = [100, 200, 100, 255] + elif to_scan: + text = _("Rescanning Tags... {N} remaining").format(N=str(len(to_scan))) + bg = [100, 200, 100, 255] + elif plex.scanning: + text = _("Accessing PLEX library...") + if gui.to_got: + text += f" {gui.to_got}" + bg = [229, 160, 13, 255] + elif tauon.spot_ctl.launching_spotify: + text = _("Launching Spotify...") + bg = [30, 215, 96, 255] + elif tauon.spot_ctl.preparing_spotify: + text = _("Preparing Spotify Playback...") + bg = [30, 215, 96, 255] + elif tauon.spot_ctl.spotify_com: + text = _("Accessing Spotify library...") + bg = [30, 215, 96, 255] + elif subsonic.scanning: + text = _("Accessing AIRSONIC library...") + if gui.to_got: + text += f" {gui.to_got}" + bg = [58, 194, 224, 255] + elif koel.scanning: + text = _("Accessing KOEL library...") + bg = [111, 98, 190, 255] + elif jellyfin.scanning: + text = _("Accessing JELLYFIN library...") + bg = [90, 170, 240, 255] + elif tauon.chrome_mode: + text = _("Chromecast Mode") + bg = [207, 94, 219, 255] + elif gui.sync_progress and not transcode_list: + text = gui.sync_progress + bg = [100, 200, 100, 255] + if right_click and coll([x, y, 280 * gui.scale, 18 * gui.scale]): + cancel_menu.activate(position=(x + 20 * gui.scale, y + 23 * gui.scale)) + elif transcode_list and gui.tc_cancel: + bg = [150, 150, 150, 255] + text = _("Stopping transcode...") + elif lastfm.scanning_friends or lastfm.scanning_loves: + text = _("Scanning: ") + lastfm.scanning_username + bg = [200, 150, 240, 255] + elif lastfm.scanning_scrobbles: + text = _("Scanning Scrobbles...") + bg = [219, 88, 18, 255] + elif gui.buffering: + text = _("Buffering... ") + text += gui.buffering_text + bg = [18, 180, 180, 255] - elif 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 + 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 - 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 + text = _("{N} listening along").format(N=len(tauon.listen_alongers)) + bg = [40, 190, 235, 255] + else: + status = False - elif cm[:3] == "y<=": - value = cm[3:] - if value and value.isdigit(): - value = int(value) - temp = [] - for item in playlist: - if pctl.master_library[item].date[:4].isdigit() and int( - pctl.master_library[item].date[:4]) <= value: - temp.append(item) - playlist = temp + if status: + x += ddt.text((x, y), text, bg, 311) + # x += ddt.get_text_w(text, 11) + # TODO: list listening 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)) - 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 + w = 100 * gui.scale + x += ddt.text((x, y), _("Transcoding"), bg, 311) + 8 * gui.scale - 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 gui.transcoding_batch_total: - elif cm == "st" or cm == "rt" or cm == "r": - random.shuffle(playlist) + # 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] - elif cm == "sf" or cm == "rf" or cm == "ra" or cm == "sa": - playlist = gen_folder_shuffle(0, custom_list=playlist) + c1 = [40, 40, 40, 255] + c2 = [100, 59, 200, 200] + c3 = [150, 70, 200, 255] - elif cm.startswith("n"): - value = cm[1:] - if value.isdigit(): - playlist = playlist[:int(value)] + if colours.lm: + c1 = [100, 100, 100, 255] + c2 = [170, 140, 255, 255] + c3 = [230, 170, 255, 255] - # SEARCH FOLDER - elif cm.startswith("p\"") and len(cm) > 3: + 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) - 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) + done = round(gui.transcoding_bach_done / gui.transcoding_batch_total * 100) + doing = round(core_use / gui.transcoding_batch_total * 100) - 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) + ddt.rect([x, yy, done, h], c3) + ddt.rect([x + done, yy, doing, h], c2) - found_name = "" + x += w + 8 * gui.scale - for result in search_over.results: - if result[0] == 5: - found_name = result[1] - break + if gui.sync_progress: + text = gui.sync_progress else: - logging.info("No folder search result found") - continue + 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) - search_over.clear() + x += ddt.text((x, y), text, bg, 311) + 8 * gui.scale - playlist += search_over.click_meta(found_name, get_list=True, search_lists=selections) - # SEARCH GENRE - elif (cm.startswith(('g"', 'gm"', 'g="'))) and len(cm) > 3: + 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) - 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) +class BottomBarType1: + def __init__(self): - g_search = quote.lower().replace("-", "") # .replace(" ", "") + self.mode = 0 - 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) + self.seek_time = 0 - found_name = "" + 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 - 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 + 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] - if not found_name: - logging.warning("No genre search result found") - continue + 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) - search_over.clear() + self.buffer_shard = asset_loader(scaled_asset_directory, loaded_asset_dc, "shard.png", True) - playlist += search_over.click_genre(found_name, get_list=True, search_lists=selections) + self.scrob_stick = 0 - # 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) + def update(self): - 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) + 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 - found_name = "" + seek_bar_x = 300 * gui.scale + if window_size[0] < 600 * gui.scale: + seek_bar_x = 250 * gui.scale - for result in search_over.results: - if result[0] == 0: - found_name = result[1] - break - else: - logging.warning("No artist search result found") - continue + self.seek_bar_size[0] = window_size[0] - seek_bar_x + self.seek_bar_position[0] = seek_bar_x - 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) + # if gui.bb_show_art: + # self.seek_bar_position[0] = 300 + gui.panelBY + # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY - elif cm.startswith("ff\""): + # self.seek_bar_position[0] = 0 + # self.seek_bar_size[0] = window_size[0] - 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() + def render(self): - if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - line = str(unidecode(line)) + global volume_store + global clicked + global right_click - if not search_magic(quote.lower(), line): - del playlist[i] + ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) - playlist = list(OrderedDict.fromkeys(playlist)) + ddt.rect_a(self.seek_bar_position, self.seek_bar_size, colours.seek_bar_background) - elif cm.startswith("fx\""): + right_offset = 0 + if gui.display_time_mode >= 2: + right_offset = 22 * 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 window_size[0] < 670 * gui.scale: + right_offset -= 90 * gui.scale + # Scrobble marker - if search_magic(quote.lower(), line): - del playlist[i] + if prefs.scrobble_mark and ( + prefs.auto_lfm or lb.enable or prefs.maloja_enable) and not prefs.scrobble_hold and pctl.playing_length > 0 and 3 > pctl.playing_state > 0: + if pctl.master_library[pctl.track_queue[pctl.queue_step]].length > 240 * 2: + l_target = 240 + else: + l_target = int(pctl.master_library[pctl.track_queue[pctl.queue_step]].length * 0.50) + l_lead = l_target - pctl.a_time + if l_lead > 0 and pctl.master_library[pctl.track_queue[pctl.queue_step]].length > 30: + l_x = self.seek_bar_position[0] + int(math.ceil( + pctl.playing_time * self.seek_bar_size[0] / int(pctl.playing_length))) + l_x += int(math.ceil(self.seek_bar_size[0] / int(pctl.playing_length) * l_lead)) - elif cm.startswith(('find"', 'f"', 'fs"')): + 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]) - 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) + # # 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])) - cooldown = 0 - dones = {} - for selection in selections: - for track_id in selection: - if track_id not in dones: - tr = pctl.get_track(track_id) + # ddt.rect_r(rect, [255, 255, 255, 20]) - 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) + # SEEK BAR------------------ + if pctl.playing_time < 1: + self.seek_time = 0 - else: - line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() + 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() - # if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): - # line = str(unidecode(line)) + fields.add(self.seek_bar_position + self.seek_bar_size) + if coll(self.seek_bar_position + self.seek_bar_size): - if search_magic(quote.lower(), line): - playlist.append(track_id) + if middle_click and pctl.playing_state > 0: + gui.seek_cur_show = True - cooldown += 1 - if cooldown > 300: - time.sleep(0.005) - cooldown = 0 + clicked = True + if mouse_wheel != 0: + pctl.seek_time(pctl.playing_time + (mouse_wheel * 3)) - dones[track_id] = None + if gui.seek_cur_show: + gui.update += 1 - playlist = list(OrderedDict.fromkeys(playlist)) + # fields.add([mouse_position[0] - 1, mouse_position[1] - 1, 1, 1]) + # ddt.rect_r([mouse_position[0] - 1, mouse_position[1] - 1, 1, 1], [255,0,0,180], True) + bargetX = mouse_position[0] + bargetX = min(bargetX, self.seek_bar_position[0] + self.seek_bar_size[0]) + bargetX = max(bargetX, self.seek_bar_position[0]) + bargetX -= self.seek_bar_position[0] + seek = bargetX / self.seek_bar_size[0] + gui.cur_time = get_display_time(pctl.playing_object().length * seek) - 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 + if self.seek_down is True: + if mouse_position[0] == 0: + self.seek_down = False + self.seek_hit = True - if cm.startswith("s\""): - selections_searched += 1 - selections.append(target) - elif cm.startswith("px\""): - playlist[:] = [x for x in playlist if x not in target] + if (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: - else: - errors = True + self.volume_hit = False + self.seek_down = False + self.seek_hit = False - gui.gen_code_errors = errors - if not playlist and not errors: - gui.gen_code_errors = "empty" + 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] - if gui.rename_playlist_box and (not playlist or cmds.count("a") > 1): - pass - else: - source_playlist[:] = playlist[:] + pctl.seek_decimal(seek) + #logging.info(seek) - tree_view_box.clear_target_pl(0, id) - pctl.regen_in_progress = False - gui.pl_update = 1 - reload() - pctl.notify_change() + self.seek_time = pctl.playing_time - #logging.info(cmds) + 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 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") + # 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 spotify_show_test(_): - return prefs.spot_mode + self.buffer_shard.render(x + offset, y, colour) + x += self.buffer_shard.w -def jellyfin_show_test(_): - return prefs.jelly_password and prefs.jelly_username + 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 upload_jellyfin_playlist(pl: TauonPlaylist) -> None: - if jellyfin.scanning: - return - shooter(jellyfin.upload_playlist, [pl]) + if pctl.playing_length > 0: -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 pctl.download_time != 0: -def forget_pl_import_folder(pl: int) -> None: - pctl.multi_playlist[pl].last_folder = [] + if pctl.download_time == -1: + pctl.download_time = pctl.playing_length -def remove_duplicates(pl: int) -> None: - playlist = [] + colour = (255, 255, 255, 10) + if gui.theme_name == "Lavender Light" or gui.theme_name == "Carbon": + colour = (255, 255, 255, 40) - for item in pctl.multi_playlist[pl].playlist_ids: - if item not in playlist: - playlist.append(item) + 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) - 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") + 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) - pctl.multi_playlist[pl].playlist_ids[:] = playlist[:] + if gui.seek_cur_show: -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 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 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 + ddt.rect( + [mouse_position[0], self.seek_bar_position[1], 2, self.seek_bar_size[1]], + [100, 100, 20, 255]) -def auto_sync_thread(pl: int) -> None: - if prefs.transcode_inplace: - show_message(_("Cannot sync when in transcode inplace mode")) - return + else: + gui.seek_cur_show = False - # Find target path - gui.sync_progress = "Starting Sync..." - gui.update += 1 + 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): - 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 + 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 - prefs.sync_target = str(path) + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() - # Get list of folder names on device - logging.info("Getting folder list from device...") - d_folder_names = path.iterdir() - logging.info("Got list") + # 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) - # Get list of folders we want - folders = convert_playlist(pl, get_list=True) - folder_names: list[str] = [] - folder_dict = {} + 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 gui.stop_sync: - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 + 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 - # 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 + step = round(1 * gui.scale) + min_h = round(4 * gui.scale) + spacing = round(5 * gui.scale) - # ------ - # 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 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() - # ------- - # 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}") + for bar in range(8): - gui.update += 1 - # ----- - # Prepare and copy - for i, item in enumerate(todos): - gui.sync_progress = _("Copying files to device") - if gui.stop_sync: - break + 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) - 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 coll(h_rect): + if mouse_down or mouse_up: + gui.update_on_drag = True - if prefs.bypass_transcode or (prefs.smart_bypass and 0 < pctl.get_track(folder_dict[item][0]).bitrate <= 128): - logging.info("Smart bypass...") + 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 - 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 + pctl.set_volume() - (path / item).mkdir() - encode_done = source_parent - else: - show_message(_("One or more folders is missing")) - continue + colour = colours.mode_button_off - else: + 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 - 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") + ddt.rect(rect, colour) + x += spacing - if encode_done.exists(): + # Volume Bar -------------------------------------------------------- + else: + if (inp.mouse_click and coll(( + self.volume_bar_position[0] - right_offset, self.volume_bar_position[1], self.volume_bar_size[0], + self.volume_bar_size[1] + 4))) or \ + self.volume_bar_being_dragged is True: + clicked = True - if (path / item).exists(): - show_message( - _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") - continue + if inp.mouse_click is True or self.volume_bar_being_dragged is True: + gui.update = 2 - (path / item).mkdir() + 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 - for file in encode_done.iterdir(): - file = file.name - logging.info(f"Copy file {file} to {path / item}…") - # gui.sync_progress += "." - gui.update += 1 + time.sleep(0.02) - if (encode_done / file).is_file(): - size = os.path.getsize(encode_done / file) - sync_file_timer.set() - try: - shutil.copyfile(encode_done / file, path / item / file) - except OSError as e: - if str(e).startswith("[Errno 22] Invalid argument: "): - sanitized_file = re.sub(r'[<>:"/\\|?*]', '_', file) - if sanitized_file == file: - logging.exception("Unknown OSError trying to copy file, maybe FS does not support the name?") - else: - shutil.copyfile(encode_done / file, path / item / sanitized_file) - logging.warning(f"Had to rename {file} to {sanitized_file} on the output! Probably a FS limitation!") - else: - logging.exception("Unknown OSError trying to copy file") - except Exception: - logging.exception("Unknown error trying to copy file") + if mouse_down is False: + self.volume_bar_being_dragged = False + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume(True) - 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" + if mouse_down: + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume(False) - logging.info("Finished copying folder") + 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)): - gui.sync_speed = 0 - gui.sync_progress = "" - gui.stop_sync = False - gui.update += 1 - show_message(_("Sync completed"), mode="done") + if pctl.player_volume > 0: + volume_store = pctl.player_volume + pctl.player_volume = 0 + else: + pctl.player_volume = volume_store -def auto_sync(pl: int) -> None: - shoot_dl = threading.Thread(target=auto_sync_thread, args=([pl])) - shoot_dl.daemon = True - shoot_dl.start() + pctl.set_volume() -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) + 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 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] + 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 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) + ddt.rect(gui.volume_bar_rect, colours.volume_bar_fill) -def set_podcast_playlist(pl: int) -> None: - pctl.multi_playlist[pl].persist_time_positioning ^= True + 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): -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] + 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) -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] + gui.show_bottom_title = gui.showed_title ^ True + if not prefs.hide_bottom_title: + gui.show_bottom_title = True -def csv_string(item): - item = str(item) - item.replace("\"", "\"\"") - return f"\"{item}\"" + if gui.show_bottom_title and pctl.playing_state > 0 and window_size[0] > 820 * gui.scale: + line = pctl.title_text() -def export_playlist_albums(pl: int) -> None: - p = pctl.multi_playlist[pl] - name = p.title - playlist = p.playlist_ids + 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 - 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) + # 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) - playtimes[last_folder] = playtimes.get(last_folder, 0) + int(star_store.get(id)) + 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() - filename = f"{user_directory}/{name}.csv" - xport = open(filename, "w") + if pctl.playing_ready() and not gui.fullscreen: - xport.write("Album name;Artist;Release date;Genre;Rating;Playtime;Folder path") + if right_click: + mode_menu.activate() - for id in albums: - track = pctl.get_track(id) - artist = track.album_artist - if not artist: - artist = track.artist + if d_click_timer.get() < 0.3 and inp.mouse_click: + set_mini_mode() + gui.update += 1 + return + d_click_timer.set() - 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)) + # TIME---------------------- - xport.close() - show_message(_("Export complete."), _("Saved as: ") + filename, mode="done") + x = window_size[0] - 57 * gui.scale + y = window_size[1] - 29 * gui.scale -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)) + 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 -def key_rating(index: int): - return star_store.get_rating(index) + 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 key_scrobbles(index: int): - return pctl.get_track(index).lfm_scrobbles + # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) -def key_disc(index: int): - return pctl.get_track(index).disc_number + 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 key_cue(index: int): - return pctl.get_track(index).is_cue + offset1 = 10 * gui.scale -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 system == "Windows": + offset1 += 2 * gui.scale -def add_pl_tag(text): - return f" <{text}>" + offset2 = offset1 + 7 * gui.scale -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.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 custom_list is not None: - return playlist + elif gui.display_time_mode == 3: - 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)) + # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rat>" + track = pctl.playing_object() + if track and track.index != gui.dtm3_index: -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) + 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 - if custom_list is not None: - return playlist + x -= 4 + text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) - 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)) + ddt.text( + (x - 25 * gui.scale, y), text_time, colours.time_playing, + fonts.bottom_panel_time) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>" + offset1 = 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale + offset2 = offset1 + 7 * gui.scale -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 + 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 len(source) < 3: - return [] + # BUTTONS + # bottom buttons - 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)) + if gui.mode == 1: - 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 + # PLAY--- + buttons_x_offset = 0 + compact = False + if window_size[0] < 650 * gui.scale: + compact = True - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r + 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 - sets = sorted(sets, key=best, reverse=True) + if pctl.playing_state == 1: + play_colour = colours.media_buttons_active - playlist = [] + if pctl.auto_stop: + stop_colour = colours.media_buttons_active - for se in sets: - playlist += se + 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] - # 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 + 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")) - 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)) + if right_click: + pctl.show_current(highlight=True) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a pa>" + 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 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 + # PAUSE--- + if compact: + buttons_x_offset = -46 * gui.scale - if len(source) < 3: - return [] + x = (75 * gui.scale) + buttons_x_offset + y = window_size[1] - self.control_line_bottom - 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)) + if not compact or (compact and pctl.playing_state == 1): - def best(folder): - return album_star_store.get_rating(pctl.get_track(folder[0])) + 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")) - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r + # 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) - sets = sorted(sets, key=best, reverse=True) + # 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")) - playlist = [] + ddt.rect_a((x, y + 0), (13 * gui.scale, 13 * gui.scale), stop_colour) + # ddt.rect_r(rect,[255,0,0,255], True) - for se in sets: - playlist += se + if compact: + buttons_x_offset -= 5 * gui.scale - if custom_list is not None: - return playlist + # 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 - 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.forward_button.render( + buttons_x_offset + 240 * gui.scale, 1 + window_size[1] - self.control_line_bottom, forward_colour) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a rata>" + # ddt.rect_r(rect,[255,0,0,255], True) -def gen_lyrics(pl: int, custom_list=None): - playlist = [] + # 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 - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids + 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) - for item in source: - if pctl.master_library[item].lyrics != "": - playlist.append(item) + # menu button - if custom_list is not None: - return playlist + 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 len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("Tracks with lyrics"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + 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) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" + if self.mode == 0 and window_size[0] > 530 * gui.scale: - else: - show_message(_("No tracks with lyrics were found.")) + # shuffle button + x = window_size[0] - 318 * gui.scale - right_offset + y = window_size[1] - 27 * gui.scale -def gen_incomplete(pl: int, custom_list=None) -> list | None: - playlist = [] + rect = (x - 5 * gui.scale, y - 5 * gui.scale, 60 * gui.scale, 25 * gui.scale) + fields.add(rect) - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids + rpbc = colours.mode_button_off + off = True + if (inp.mouse_click or right_click) and coll(rect): - albums = {} - nums = {} - for id in source: - track = pctl.get_track(id) - if track.album and track.track_number: + 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 type(track.track_number) is str and not track.track_number.isdigit(): - continue + 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 - if track.album not in albums: - albums[track.album] = [] - nums[track.album] = [] + # Keep hover highlight on if menu is open + if shuffle_menu.active and not pctl.random_mode: + rpbc = colours.mode_button_over - if track not in albums[track.album]: - albums[track.album].append(track) - nums[track.album].append(int(track.track_number)) + #self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) - 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) + #y += round(3 * gui.scale) + #ddt.rect_a((x, y), (25 * gui.scale, 3 * gui.scale), rpbc) - if custom_list is not None: - return playlist + 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 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)) + #ddt.rect_a((x + 25 * gui.scale, y), (23 * gui.scale, 3 * gui.scale), rpbc) - # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - else: - show_message(_("No incomplete albums were found.")) - return None + #y += round(5 * gui.scale) + #ddt.rect_a((x, y), (48 * gui.scale, 3 * gui.scale), rpbc) -def gen_codec_pl(codec): - playlist = [] + # REPEAT + x = window_size[0] - round(380 * gui.scale) - right_offset + y = window_size[1] - round(27 * gui.scale) - 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) + rpbc = colours.mode_button_off + off = True - if len(playlist) > 0: - pctl.multi_playlist.append( - pl_gen( - title=_("Codec: ") + codec, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + 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 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 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 - a_cache = {} + 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): - def key_import(index: int): + # 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")) - track = pctl.master_library[index] - cached = a_cache.get((track.album, track.parent_folder_name)) - if cached is not None: - return cached + 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 - if track.album: - a_cache[(track.album, track.parent_folder_name)] = index - return index + # Keep hover highlight on if menu is open + if repeat_menu.active and not pctl.repeat_mode: + rpbc = colours.mode_button_over - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_import, reverse=reverse) - sort_track_2(0, playlist) + rpbc = alpha_blend(rpbc, colours.bottom_panel_colour) # bake in alpha in case of overlap - if custom_list is not None: - return playlist + y += round(3 * gui.scale) + w = round(3 * gui.scale) + y = round(y) + x = round(x) -def gen_last_modified(index, custom_list=None, reverse=True): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + ar = x + round(50 * gui.scale) + h = round(5 * gui.scale) - a_cache = {} + 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 key_modified(index: int): + # 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) - track = pctl.master_library[index] - cached = a_cache.get((track.album, track.parent_folder_name)) - if cached is not None: - return cached +class BottomBarType_ao1: + def __init__(self): - if track.album: - a_cache[(track.album, track.parent_folder_name)] = pctl.master_library[index].modified_time - return pctl.master_library[index].modified_time + self.mode = 0 - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=key_modified, reverse=reverse) - sort_track_2(0, playlist) + self.seek_time = 0 - if custom_list is not None: - return playlist + 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 - 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)) + 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] - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a m>" + 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 gen_love(pl: int, custom_list=None): - playlist = [] + self.scrob_stick = 0 - source = custom_list - if source is None: - source = pctl.multi_playlist[pl].playlist_ids + def update(self): - for item in source: - if get_love_index(item): - playlist.append(item) + 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 - playlist.sort(key=lambda x: get_love_timestamp_index(x), reverse=True) + seek_bar_x = 300 * gui.scale + if window_size[0] < 600 * gui.scale: + seek_bar_x = 250 * gui.scale - if custom_list is not None: - return playlist + self.seek_bar_size[0] = window_size[0] - seek_bar_x + self.seek_bar_position[0] = seek_bar_x - 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 gui.bb_show_art: + # self.seek_bar_position[0] = 300 + gui.panelBY + # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY -def gen_comment(pl: int) -> None: - playlist = [] + # self.seek_bar_position[0] = 0 + # self.seek_bar_size[0] = window_size[0] - 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) + def render(self): - 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.")) + global volume_store + global clicked + global right_click -def gen_replay(pl: int) -> None: - playlist = [] + ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) - for item in pctl.multi_playlist[pl].playlist_ids: - if pctl.master_library[item].misc.get("replaygain_track_gain"): - playlist.append(item) + right_offset = 0 + if gui.display_time_mode >= 2: + right_offset = 22 * gui.scale - 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 window_size[0] < 670 * gui.scale: + right_offset -= 90 * gui.scale -def gen_sort_len(index: int, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + # # 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 length(index: int) -> int: + # ddt.rect_r(rect, [255, 255, 255, 20]) - if pctl.master_library[index].length < 1: - return 0 - return int(pctl.master_library[index].length) + # 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): - playlist = copy.deepcopy(source) - playlist = sorted(playlist, key=length, reverse=True) + 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 - if custom_list is not None: - return playlist + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() - # pctl.multi_playlist.append( - # [pctl.multi_playlist[index].title + " <Duration Sorted>", 0, copy.deepcopy(playlist), 0, 1, 0]) + # 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() - 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)) + # Volume Bar 2 ------------------------------------------------ + if True: + x = window_size[0] - right_offset - 120 * gui.scale + y = window_size[1] - round(21 * gui.scale) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a d>" + if gui.compact_bar: + x -= 90 * gui.scale -def gen_folder_duration(pl: int, get_sets: bool = False): - if len(pctl.multi_playlist[pl].playlist_ids) < 3: - return None + 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 - 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)) + 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 - def best(folder): - total_duration = 0 - for item in folder: - total_duration += pctl.master_library[item].length - return total_duration + step = round(1 * gui.scale) + min_h = round(4 * gui.scale) + spacing = round(5 * gui.scale) - if get_sets: - r = [] - for item in sets: - r.append((item, best(item))) - return r + 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 - sets = sorted(sets, key=best, reverse=True) - playlist = [] + pctl.set_volume() - for se in sets: - playlist += se + for bar in range(8): - 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)) + 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 gen_sort_date(index: int, rev: bool = False, custom_list=None): - def g_date(index: int): + if coll(h_rect): + if mouse_down: + gui.update_on_drag = True - if pctl.master_library[index].date != "": - return str(pctl.master_library[index].date) - return "z" + 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 - playlist = [] - lowest = 0 - highest = 0 - first = True + pctl.set_volume() - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + colour = colours.mode_button_off - 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) + 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 - playlist = sorted(playlist, key=g_date, reverse=rev) + ddt.rect(rect, colour) + x += spacing - if custom_list is not None: - return playlist + # TIME---------------------- - 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) + ">" + x = window_size[0] - 57 * gui.scale + y = window_size[1] - 35 * gui.scale - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + line, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + 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 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<" + 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 gen_sort_date_new(index: int): - gen_sort_date(index, True) + colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) -def gen_500_random(index: int): - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + 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) - random.shuffle(playlist) + offset1 = 10 * gui.scale - 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 system == "Windows": + offset1 += 2 * gui.scale - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a st" + offset2 = offset1 + 7 * gui.scale -def gen_folder_shuffle(index, custom_list=None): - folders = [] - dick = {} + 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) - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + elif gui.display_time_mode == 3: - 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) + colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) - random.shuffle(folders) - playlist = [] + track = pctl.playing_object() + if track and track.index != gui.dtm3_index: - for folder in folders: - playlist += dick[folder] + 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 - if custom_list is not None: - return playlist + x -= 4 + text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) - 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)) + ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a ra" + offset1 = 10 * gui.scale + if system == "Windows": + offset1 += 2 * gui.scale + offset2 = offset1 + 7 * gui.scale -def gen_best_random(index: int): - playlist = [] + 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) - for p in pctl.multi_playlist[index].playlist_ids: - time = star_store.get(p) + # BUTTONS + # bottom buttons - if time > 300: - playlist.append(p) + if gui.mode == 1: - random.shuffle(playlist) + # PLAY--- + buttons_x_offset = 0 + compact = False + if window_size[0] < 650 * gui.scale: + compact = True - 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)) + 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 - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>300 rt" + if pctl.playing_state == 1: + play_colour = colours.media_buttons_active -def gen_reverse(index, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + if pctl.auto_stop: + stop_colour = colours.media_buttons_active - playlist = list(reversed(source)) + 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 custom_list is not None: - return playlist + 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")) - 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 right_click: + pctl.show_current(highlight=True) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rv" + 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 gen_folder_reverse(index: int, custom_list=None): - source = custom_list - if source is None: - source = pctl.multi_playlist[index].playlist_ids + # PAUSE--- + if compact: + buttons_x_offset = -46 * 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) + x = (75 * gui.scale) + buttons_x_offset + y = window_size[1] - self.control_line_bottom - folders = list(reversed(folders)) - playlist = [] + if not compact or (compact and pctl.playing_state == 2): - for folder in folders: - playlist += dick[folder] + 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 custom_list is not None: - return playlist + # 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) - 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)) + # 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 - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rva" + self.forward_button.render( + buttons_x_offset + 125 * gui.scale, + 1 + window_size[1] - self.control_line_bottom, forward_colour) -def gen_dupe(index: int) -> None: - playlist = pctl.multi_playlist[index].playlist_ids +class MiniMode: + def __init__(self): + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) - 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)) + 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) -def gen_sort_path(index: int) -> None: - def path(index: int) -> str: - return pctl.master_library[index].fullpath + self.shuffle_fade_timer = Timer(100) + self.repeat_fade_timer = Timer(100) - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=path) + 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 - 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)) + w = window_size[0] + h = window_size[1] -def gen_sort_artist(index: int) -> None: - def artist(index: int) -> str: - return pctl.master_library[index].artist + y1 = w + if w == h: + y1 -= 79 * gui.scale - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=artist) + h1 = h - y1 - 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)) + # Draw background + bg = colours.mini_mode_background + # bg = [250, 250, 250, 255] -def gen_sort_album(index: int) -> None: - def album(index: int) -> None: - return pctl.master_library[index].album + ddt.rect((0, 0, w, h), bg) + ddt.text_background_colour = bg - playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - playlist = sorted(playlist, key=album) + detect_mouse_rect = (3, 3, w - 6, h - 6) + fields.add(detect_mouse_rect) + mouse_in = coll(detect_mouse_rect) - pctl.multi_playlist.append( - pl_gen( - title=pctl.multi_playlist[index].title + add_pl_tag(_("Album Sorted")), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + # Play / Pause when right clicking below art + if right_click: # and mouse_position[1] > y1: + pctl.play_pause() -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" + # Volume change on scroll + if mouse_wheel != 0: + self.volume_timer.set() -def reload_config_file(): - if transcode_list: - show_message(_("Cannot reload while a transcode is in progress!"), mode="error") - return + 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 - load_prefs() - gui.opened_config_file = False + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() - 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() + track = pctl.playing_object() -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 + 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 open_keymap_file(): - target = str(config_directory / "input.txt") + ddt.rect((0, 0, w, w), (0, 0, 0, 45)) + if track is not None: - if not os.path.isfile(target): - show_message(_("Input file missing")) - return + # Render album art + album_art_gen.display(track, (0, 0), (w, w)) - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) - -def open_file(target): - if not os.path.isfile(target): - show_message(_("Input file missing")) - return - - if system == "Windows" or msys: - os.startfile(target) - elif macos: - subprocess.call(["open", target]) - else: - subprocess.call(["xdg-open", target]) + line1c = colours.mini_mode_text_1 + line2c = colours.mini_mode_text_2 -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]) + 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 remove_folder(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) - 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] + 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() - reload() + # Draw title texts + line1 = track.artist + line2 = track.title -def convert_folder(index: int): - global default_playlist - global transcode_list + # Calculate seek bar position + seek_w = int(w * 0.70) - if not tauon.test_ffmpeg(): - return + 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] - folder = [] - if key_shift_down or key_shiftr_down: - track_object = pctl.get_track(index) - if track_object.is_network: - show_message(_("Transcoding tracks from network locations is not supported")) - return - folder = [index] + if w != h or mouse_in_area: - 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") + 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: - return - folder = [index] + ddt.text((w // 2, y1 + 10 * gui.scale, 2), line1, line2c, 514, window_size[0] - 30 * 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: + ddt.text((w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 414, window_size[0] - 30 * gui.scale) - track_object = pctl.get_track(item) - if track_object.file_ext == "SPOT": # track_object.is_network: - show_message(_("Transcoding spotify tracks not possible")) - return + # Test click to seek + if mouse_up and coll(seek_r_hit): - 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") + 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] - return + if click_x < 6 * gui.scale: + click_x = 0 + seek = click_x / seek_r[2] - #logging.info(folder) - transcode_list.append(folder) - tauon.thread_manager.ready("worker") + pctl.seek_decimal(seek) -def transfer(index: int, args) -> None: - global cargo - global default_playlist - old_cargo = copy.deepcopy(cargo) + # Draw progress bar background + ddt.rect(seek_r, [255, 255, 255, 32]) - 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] + # 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 - 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] + if pctl.playing_state != 1: + seek_colour = [210, 40, 100, 255] - elif args[1] == 3: # playlist - cargo += default_playlist - if args[0] == 0: # cut - default_playlist = [] + seek_r[2] = progress_w - elif args[0] == 2: # Drop - if args[1] == 1: # Before + 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 - 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 + ddt.rect(seek_r, seek_colour) - while len(cargo) > 0: - default_playlist.insert(insert, cargo.pop()) + 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) - elif args[1] == 2: # After - insert = pctl.selected_in_playlist + fields.add(left_area) + fields.add(right_area) - while insert < len(default_playlist) and pctl.master_library[default_playlist[insert]].parent_folder_name == \ - pctl.master_library[index].parent_folder_name: - insert += 1 + 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]) - while len(cargo) > 0: - default_playlist.insert(insert, cargo.pop()) - elif args[1] == 3: # End - default_playlist += cargo - # cargo = [] + 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]) - cargo = old_cargo + # Shuffle - reload() + 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) -def temp_copy_folder(ref): - global cargo - cargo = [] - transfer(ref, args=[1, 2]) + 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] -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() + sx = seek_r[0] + seek_w + 12 * gui.scale + sy = seek_r[1] - 2 * gui.scale + self.shuffle.render(sx, sy, colour) -def menu_paste(position): - paste(None, position) -def s_copy(): - # Copy tracks to internal clipboard - # gui.lightning_copy = False - # if key_shift_down: - gui.lightning_copy = True + # 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) - clip = copy_from_clipboard() - if "file://" in clip: - copy_to_clipboard("") + 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] - global cargo - cargo = [] - if default_playlist: - for item in shift_selection: - cargo.append(default_playlist[item]) - if not cargo and -1 < pctl.selected_in_playlist < len(default_playlist): - cargo.append(default_playlist[pctl.selected_in_playlist]) + sx = seek_r[0] - 36 * gui.scale + sy = seek_r[1] - 1 * gui.scale + self.repeat.render(sx, sy, colour) - tauon.copied_track = None - if len(cargo) == 1: - tauon.copied_track = cargo[0] + # sx = seek_r[0] - 39 * gui.scale + # sy = seek_r[1] - 1 * gui.scale -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 + #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 lightning_paste(): - move = True - # if not key_shift_down: - # move = False - move_track = pctl.get_track(cargo[0]) - move_path = move_track.parent_folder_path + # 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() - 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 + # 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() - match_track = pctl.get_track(default_playlist[shift_selection[0]]) - match_path = match_track.parent_folder_path + 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 pctl.playing_state > 0 and move: - if pctl.playing_object().parent_folder_path == move_path: - pctl.stop(True) +class MiniMode2: - p = Path(match_path) - s = list(p.parts) - base = s[0] - c = base - del s[0] + def __init__(self): - 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.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) - to_move = list(set(to_move)) + 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) - for level in s: - upper = c - c = os.path.join(c, level) + def render(self): - t_artist = match_track.artist - ta_artist = match_track.album_artist + w = window_size[0] + h = window_size[1] - t_artist = filename_safe(t_artist) - ta_artist = filename_safe(ta_artist) + x1 = h - if (len(t_artist) > 0 and t_artist in level) or \ - (len(ta_artist) > 0 and ta_artist in level): + # Draw background + ddt.rect((0, 0, w, h), colours.mini_mode_background) + ddt.text_background_colour = colours.mini_mode_background - logging.info("found target artist level") - logging.info(t_artist) - logging.info("Upper folder is: " + upper) + detect_mouse_rect = (2, 2, w - 4, h - 4) + fields.add(detect_mouse_rect) + mouse_in = coll(detect_mouse_rect) - if len(move_path) < 4: - show_message(_("Safety interupt! The source path seems oddly short."), move_path, mode="error") - return + # Play / Pause when right clicking below art + if right_click: # and mouse_position[1] > y1: + pctl.play_pause() - if not os.path.isdir(upper): - show_message(_("The target directory is missing!"), upper, mode="warning") - return + # Volume change on scroll + if mouse_wheel != 0: + self.volume_timer.set() - if not os.path.isdir(move_path): - show_message(_("The source directory is missing!"), move_path, mode="warning") - return + 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 - 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 + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() - if directory_size(move_path) > 3000000000: - show_message(_("Folder size safety limit reached! (3GB)"), move_path, mode="warning") - return + track = pctl.playing_object() - 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 + if track is not None: - artist = move_track.artist - if move_track.album_artist != "": - artist = move_track.album_artist + # Render album art + album_art_gen.display(track, (0, 0), (h, h)) - artist = filename_safe(artist) + text_hit_area = (x1, 0, w, h) - if artist == "": - show_message(_("The track needs to have an artist name.")) - return + 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() - artist_folder = os.path.join(upper, artist) + # Draw title texts + line1 = track.artist + line2 = track.title - logging.info("Target will be: " + artist_folder) + if not line1 and not line2: - if os.path.isdir(artist_folder): - logging.info("The target artist folder already exists") + ddt.text( + (x1 + 15 * gui.scale, 44 * gui.scale), track.filename, colours.grey(150), 315, + window_size[0] - x1 - 30 * gui.scale) else: - logging.info("Need to make artist folder") - os.makedirs(artist_folder) - logging.info("The folder to be moved is: " + move_path) - load_order = LoadClass() - load_order.target = os.path.join(artist_folder, move_track.parent_folder_name) - load_order.playlist = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + # 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: - 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 + ddt.text( + (x1 + 15 * gui.scale, 18 * gui.scale), line2, colours.grey(249), 514, + window_size[0] - x1 - 30 * gui.scale) - load_order.playlist_position = insert + ddt.text( + (x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 514, + window_size[0] - x1 - 30 * gui.scale) - 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] + # 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() - break - else: - show_message(_("Could not find a folder with the artist's name to match level at.")) - 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]) - # for file in os.listdir(artist_folder): - # + if pctl.playing_state > 0: - if album_mode: - prep_gal() - reload_albums(True) + hit_rect = h - 5 * gui.scale, h - 12 * gui.scale, w - h + 5 * gui.scale, 13 * gui.scale - cargo.clear() - gui.lightning_copy = False + if coll(hit_rect) and mouse_up: + p = (mouse_position[0] - h) / (w - h) -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 + 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) - elif "tidal.com/playlist/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.playlist(num) - clip = False + 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) - elif "tidal.com/mix/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.mix(num) - clip = False +class MiniMode3: - elif "tidal.com/browse/track/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.track(num) - clip = False + def __init__(self): - elif "tidal.com/browse/artist/" in clip: - logging.info(clip) - num = clip.split("/")[-1].split("?")[0] - tauon.tidal.artist(num) - clip = False + self.save_position = None + self.was_borderless = True + self.volume_timer = Timer() + self.volume_timer.force_set(100) - 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.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) - 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 + self.shuffle_fade_timer = Timer(100) + self.repeat_fade_timer = Timer(100) - 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 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 - load_orders.append(copy.deepcopy(load_order)) - found = True + w = window_size[0] + h = window_size[1] - if not found: + y1 = w #+ 10 * gui.scale + # if w == h: + # y1 -= 79 * gui.scale - 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) + h1 = h - y1 - gui.pl_update += 1 + # Draw background + bg = colours.mini_mode_background + bg = [0, 0, 0, 0] + # bg = [250, 250, 250, 255] -def s_cut(): - s_copy() - del_selected() + ddt.rect((0, 0, w, h), bg) -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 + style_overlay.display() -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 + 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 -def paste_playlist_coast_album(): - shoot_dl = threading.Thread(target=paste_playlist_coast_fire) - shoot_dl.daemon = True - shoot_dl.start() + detect_mouse_rect = (3, 3, w - 6, h - 6) + fields.add(detect_mouse_rect) + mouse_in = coll(detect_mouse_rect) -def paste_playlist_coast_track(): - shoot_dl = threading.Thread(target=paste_playlist_track_coast_fire) - shoot_dl.daemon = True - shoot_dl.start() + # Play / Pause when right clicking below art + if right_click: # and mouse_position[1] > y1: + pctl.play_pause() -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 + # Volume change on scroll + if mouse_wheel != 0: + self.volume_timer.set() - return [line_colour, colours.menu_background, None] + 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 -def refind_playing(): - # Refind playing index - if pctl.playing_ready(): - for i, n in enumerate(default_playlist): - if pctl.track_queue[pctl.queue_step] == n: - pctl.playlist_playing_position = i - break + pctl.player_volume = int(pctl.player_volume) + pctl.set_volume() -def del_selected(force_delete=False): - global shift_selection + track = pctl.playing_object() - gui.update += 1 - gui.pl_update = 1 + 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) - if not shift_selection: - shift_selection = [pctl.selected_in_playlist] + #ddt.rect((0, 0, w, w), (0, 0, 0, 45)) + if track is not None: - if not default_playlist: - return + # Render album art - li = [] + wid = (w // 2) + round(60 * gui.scale) + ins = (window_size[0] - wid) / 2 + off = round(4 * gui.scale) - for item in reversed(shift_selection): - if item > len(default_playlist) - 1: - return + 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)) - li.append((item, default_playlist[item])) # take note for force delete + line1c = [255, 255, 255, 255] #colours.mini_mode_text_1 + line2c = [255, 255, 255, 255] #colours.mini_mode_text_2 - # 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 + # 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] - del default_playlist[item] + # Double click bottom text to return to full window + text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) - if force_delete: - for item in li: + 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() - 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")) + # Draw title texts + line1 = track.artist + line2 = track.title + key = None + if not line1 and not line2: + if not ddt.alpha_bg: + key = (track.filename, 214, style_overlay.current_track_id) + ddt.text( + (w // 2, y1 + 18 * gui.scale, 2), track.filename, line1c, 214, + window_size[0] - 30 * gui.scale, real_bg=not transit, key=key) + else: - if 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") + 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) - else: - undo.bk_tracks(pctl.active_playlist_viewing, li) + y1 += round(10 * gui.scale) - reload() - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + # Calculate seek bar position + seek_w = int(w * 0.80) - pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist) - 1) + 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] - shift_selection = [pctl.selected_in_playlist] - gui.pl_update += 1 - refind_playing() - pctl.notify_change() + if w != h or mouse_in_area: -def force_del_selected(): - del_selected(force_delete=True) -def test_show(dummy): - return album_mode + # Test click to seek + if mouse_up and coll(seek_r_hit): -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() + 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 last_fm_test(ignore): - if lastfm.connected: - return True - return False + if click_x < 6 * gui.scale: + click_x = 0 + seek = click_x / seek_r[2] -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 + pctl.seek_decimal(seek) -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 + # Draw progress bar background + ddt.rect(seek_r, [255, 255, 255, 32]) -def love_decox(): - global r_menu_index + # 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 love(False, r_menu_index): - return [colours.menu_text, colours.menu_background, _("Un-Love Track")] - return [colours.menu_text, colours.menu_background, _("Love Track")] + if pctl.playing_state != 1: + seek_colour = [210, 40, 100, 255] -def love_index(): - global r_menu_index + seek_r[2] = progress_w - notify = False - if not gui.show_hearts: - notify = True + ddt.rect(seek_r, seek_colour) - # 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() -def toggle_spotify_like_ref(): - tr = pctl.get_track(r_menu_index) - if tr: - shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) - shoot_dl.daemon = True - shoot_dl.start() -def toggle_spotify_like3(): - toggle_spotify_like_active2(pctl.get_track(r_menu_index)) + 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] -def toggle_spotify_like_row_deco(): - tr = pctl.get_track(r_menu_index) - text = _("Spotify Like Track") + # Test click to volume + if (mouse_up or mouse_down) and coll(volume_r_hit): + gui.update_on_drag = True + click_x = mouse_position[0] + click_x = min(click_x, volume_r[0] + volume_r[2]) + click_x = max(click_x, volume_r[0]) + click_x -= volume_r[0] - # if 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 click_x < 6 * gui.scale: + click_x = 0 + volume = click_x / volume_r[2] - return [colours.menu_text, colours.menu_background, text] + pctl.player_volume = int(volume * 100) + pctl.set_volume() -def spot_like_show_test(x): + ddt.rect(volume_r, [255, 255, 255, 32]) - return spotify_show_test and pctl.get_track(r_menu_index).file_ext == "SPTY" + #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 -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 + ddt.rect(volume_r, volume_colour) -def add_to_queue(ref): - pctl.force_queue.append(queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) - queue_timer_set() - if prefs.stop_end_queue: - pctl.auto_stop = False -def add_selected_to_queue(): - gui.pl_update += 1 - if prefs.stop_end_queue: - pctl.auto_stop = False - if gui.album_tab_mode: - add_album_to_queue(default_playlist[get_album_info(pctl.selected_in_playlist)[1][0]], pctl.selected_in_playlist) - queue_timer_set() - else: - pctl.force_queue.append( - queue_item_gen(default_playlist[pctl.selected_in_playlist], - pctl.selected_in_playlist, - pl_to_id(pctl.active_playlist_viewing))) - queue_timer_set() + 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) -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))) + fields.add(left_area) + fields.add(right_area) -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] + 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]) -def split_queue_album(id: int) -> int | None: - item = pctl.force_queue[0] + 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]) - pl = id_to_pl(item.playlist_id) - if pl is None: - return None + # 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) - playlist = pctl.multi_playlist[pl].playlist_ids + 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 = pctl.playlist_playing_position + 1 - parts = [] - album_parent_path = pctl.get_track(item.track_id).parent_folder_path + sx = volume_r[0] + volume_w + 12 * gui.scale + sy = volume_r[1] - 3 * gui.scale + mini_mode.shuffle.render(sx, sy, colour) - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break + # + # 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) - parts.append((playlist[i], i)) - i += 1 + 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] - del pctl.force_queue[0] + sx = volume_r[0] - 39 * gui.scale + sy = volume_r[1] - 1 * gui.scale + mini_mode.repeat.render(sx, sy, colour) - for part in reversed(parts): - pctl.force_queue.insert(0, queue_item_gen(part[0], part[1], item.type)) - return (len(parts)) + # 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 add_to_queue_next(ref: int) -> None: - if pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(None) + # 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() - pctl.force_queue.insert(0, queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) + search_over.render() -# def toggle_queue(mode: int = 0) -> bool: -# if mode == 1: -# return prefs.show_queue -# prefs.show_queue ^= True -# prefs.show_queue ^= True -def delete_track(track_ref): - tr = pctl.get_track(track_ref) - fullpath = tr.fullpath + # Show exit/min buttons when mosue over + tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] + if prefs.left_window_control: + tool_rect[0] = 0 + fields.add(tool_rect) + if coll(tool_rect): + draw_window_tools() - if system == "Windows" or msys: - fullpath = fullpath.replace("/", "\\") - if tr.is_network: - show_message(_("Cannot delete a network track")) - return + # 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 - while track_ref in default_playlist: - default_playlist.remove(track_ref) +class StandardPlaylist: + def __init__(self): + pass - try: - send2trash(fullpath) + def full_render(self): - 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")) + global highlight_left + global highlight_right - except Exception: - try: - os.remove(fullpath) - show_message(_("File deleted"), fullpath, mode="info") - except Exception: - logging.exception("Error deleting file") - show_message(_("Error deleting file"), fullpath, mode="error") - - reload() - refind_playing() - pctl.notify_change() + global playlist_hold + global playlist_hold_position + global shift_selection -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…")] + global click_time + global quick_drag + global mouse_down + global mouse_up + global selection_stage -def activate_trans_editor(): - trans_edit_box.active = True + global r_menu_index + global r_menu_position -def delete_folder(index, force=False): - track = pctl.master_library[index] + left = gui.playlist_left + width = gui.plw - if track.is_network: - show_message(_("Cannot physically delete"), _("One or more tracks is from a network location!"), mode="info") - return + 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 - old = track.parent_folder_path + w = 0 + gui.row_extra = 0 + cv = 0 # update gui.playlist_current_visible_tracks - if len(old) < 5: - show_message(_("This folder path seems short, I don't wanna try delete that"), mode="warning") - return + # Draw the background + SDL_SetRenderTarget(renderer, gui.tracklist_texture) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) - 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 + rect = (left, gui.panelY, width, window_size[1]) + ddt.rect(rect, colours.playlist_panel_background) - protect = ("", "Documents", "Music", "Desktop", "Downloads") + # 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) - 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 + # 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: - if directory_size(old) > 1500000000: - show_message(_("Delete size safety limit reached! (1.5GB)"), old, mode="warning") - return + # Set scroll speed + mx = 4 - try: + if gui.playlist_view_length < 25: + mx = 3 + if gui.playlist_view_length < 10: + mx = 2 + pctl.playlist_view_position -= mouse_wheel * mx - 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 gui.playlist_view_length > 40: + pctl.playlist_view_position -= mouse_wheel - if force: - shutil.rmtree(old) - elif system == "Windows" or msys: - send2trash(old.replace("/", "\\")) - else: - send2trash(old) + #if mouse_wheel: + #logging.debug("Position changed by mouse wheel scroll: " + str(mouse_wheel)) - for i in reversed(range(len(default_playlist))): + 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() - if old == pctl.master_library[default_playlist[i]].parent_folder_path: - del default_playlist[i] + scroll_hide_timer.set() + gui.frame_callback_list.append(TestTimer(0.9)) - 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") + # Show notice if playlist empty + if len(default_playlist) == 0: + colour = alpha_mod(colours.index_text, 200) # colours.playlist_text_missing - if album_mode: - prep_gal() - reload_albums() + top_a = gui.panelY + if gui.artist_info_panel: + top_a += gui.artist_panel_height - 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") + b = window_size[1] - top_a - gui.panelBY + half = int(top_a + (b * 0.60)) - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - gui.pl_update += 1 - pctl.notify_change() + 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 rename_parent(index: int, template: str) -> None: - # template = prefs.rename_folder_template - template = template.strip("/\\") - track = pctl.master_library[index] + 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) - if track.is_network: - show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") - return + ddt.pretty_rect = None + ddt.alpha_bg = False - old = track.parent_folder_path - #logging.info(old) + # Show notice if at end of playlist + elif pctl.playlist_view_position > len(default_playlist) - 1: + colour = alpha_mod(colours.index_text, 200) - new = parse_template2(template, track) + top_a = gui.panelY + if gui.artist_info_panel: + top_a += gui.artist_panel_height - if len(new) < 1: - show_message(_("Rename error."), _("The generated name is too short"), mode="warning") - return + b = window_size[1] - top_a - gui.panelBY + half = int(top_a + (b * 0.17)) - if len(old) < 5: - show_message(_("Rename error."), _("This folder path seems short, I don't wanna try rename that"), mode="warning") - return + 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 - if not os.path.exists(old): - show_message(_("Rename Failed. The original folder is missing."), mode="warning") - return + ddt.text( + (left + int(width / 2) + 10 * gui.scale, half, 2), _("End of Playlist"), + colour, 213) - protect = ("", "Documents", "Music", "Desktop", "Downloads") + ddt.pretty_rect = None + ddt.alpha_bg = False - 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 + # 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) - 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) + # Process Input - pre_state = 0 + # type (0 is track, 1 is fold title), track_position, track_object, box, input_box, + list_items = [] + number = 0 - for key, object in pctl.master_library.items(): + for i in range(gui.playlist_view_length + 1): - if object.fullpath == "": - continue + track_position = i + pctl.playlist_view_position - if old == object.parent_folder_path: + # Make sure the view position is valid + pctl.playlist_view_position = max(pctl.playlist_view_position, 0) - new_fullpath = os.path.join(new_parent_path, object.filename) + # Break if we are at end of playlist + if len(default_playlist) <= track_position or number > gui.playlist_view_length: + break - if os.path.normpath(new_parent_path) == os.path.normpath(old): - show_message(_("The folder already has that name.")) - return + track_object = pctl.get_track(default_playlist[track_position]) + track_id = track_object.index + move_on_title = False - if os.path.exists(new_parent_path): - show_message(_("Rename Failed."), _("A folder with that name already exists"), mode="warning") - return + line_y = gui.playlist_top + gui.playlist_row_height * number - if key == pctl.track_queue[pctl.queue_step] and pctl.playing_state > 0: - pre_state = pctl.stop(True) + track_box = ( + left + highlight_left, line_y, highlight_width, + gui.playlist_row_height - 1) - object.parent_folder_name = new - object.parent_folder_path = new_parent_path - object.fullpath = new_fullpath + input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) + # 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 - # 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("\\/")) + highlight = False + drag_highlight = False - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) + # Shift selection highlight + if (track_position in shift_selection and len(shift_selection) > 1): + highlight = True - 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 + # Tracks have been dropped? + if playlist_hold is True and coll(input_box): + if mouse_up: + move_on_title = True - show_message(_("Folder renamed."), _("Renamed to: {name}").format(name=new), mode="done") + # 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 - if pre_state == 1: - pctl.revert() + # Detect folder title click + if click_title and mouse_position[1] < window_size[1] - gui.panelBY: - tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - pctl.notify_change() + 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 -def rename_folders_disable_test(index: int) -> bool: - return pctl.get_track(index).is_network + 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 -def rename_folders(index: int): - global track_box - global rename_index - global input_text + # 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 - track_box = False - rename_index = index + if album_mode: + goto_album(pctl.playlist_playing_position) - if rename_folders_disable_test(index): - show_message(_("Not applicable for a network track.")) - return + # 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 - gui.rename_folder_box = True - input_text = "" - shift_selection.clear() + 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 - global quick_drag - global playlist_hold - quick_drag = False - playlist_hold = False + # 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: -def move_folder_up(index: int, do: bool = False) -> bool | None: - track = pctl.master_library[index] + quick_drag = True + set_drag_source() - if track.is_network: - show_message(_("Cannot move"), _("One or more tracks is from a network location!"), mode="info") - return None + if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: + playlist_hold = True - 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) + selection_stage = 1 + temp = get_folder_tracks_local(track_position) + pctl.selected_in_playlist = track_position - 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(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 len(os.listdir(parent_folder)) > 1: - return False + else: + shift_selection = copy.copy(temp) - if do is False: - return True + # Should draw drag highlight? - 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 mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: - try: + if len(shift_selection) < 2 and not key_shift_down: + pass + else: + drag_highlight = True - # Rename the track folder to something temporary - os.rename(move_target, os.path.join(parent_folder, "RMTEMP000")) + # Something to do with quick search, I forgot + if pctl.selected_in_playlist > track_position + 1: + gui.row_extra += 1 - # Move the temporary folder up 2 levels - shutil.move(os.path.join(parent_folder, "RMTEMP000"), upper_folder) + list_items.append( + (1, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, False)) + number += 1 - # Delete the old directory that contained the original folder - shutil.rmtree(parent_folder) + if number > gui.playlist_view_length: + break - # Rename the moved folder back to its original name - os.rename(os.path.join(upper_folder, "RMTEMP000"), os.path.join(upper_folder, folder_name)) + # Standard track --------------------------------------------------------------------- + playing = False - except Exception as e: - logging.exception("System Error!") - show_message(_("System Error!"), str(e), mode="error") + highlight = False + drag_highlight = False + line_y = gui.playlist_top + gui.playlist_row_height * number - # 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(): + track_box = ( + left + highlight_left, line_y, highlight_width, + gui.playlist_row_height - 1) - 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("\\/")) + input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) - search_string_cache.pop(object.index, None) - search_dia_string_cache.pop(object.index, None) + # 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 - logging.info(object.fullpath) - logging.info(object.parent_folder_path) + else: + line_hit = False + else: + line_hit = False + line_over = False - if pre_state == 1: - pctl.revert() + # Prevent click if near scroll bar + if scroll_enable and mouse_position[0] < 30: + line_hit = False -def clean_folder(index: int, do: bool = False) -> int | None: - track = pctl.master_library[index] + # 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): - if track.is_network: - show_message(_("Cannot clean"), _("One or more tracks is from a network location!"), mode="info") - return None + pctl.jump(track_id, track_position) - 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": + click_time -= 1.5 + quick_drag = False + mouse_down = False + mouse_up = False + line_hit = False - 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 album_mode: + goto_album(pctl.playlist_playing_position) - 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 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 - 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)) + # 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 - 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 + # 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 - return found + # # 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 -def reset_play_count(index: int): - star_store.remove(index) + # Begin drag single track + if inp.mouse_click and line_hit and not side_drag: + quick_drag = True + set_drag_source() -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 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))): - for track in todo: + if len(shift_selection) > 1 or key_shift_down: + if track_position not in shift_selection: # p_track != playlist_hold_position and - tr = pctl.get_track(track) + if len(shift_selection) == 0: - total_playtime = 0 - flags = "" + 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 - to_del = [] + gui.pl_update = 1 - for key, value in star_store.db.items(): - if key[0].lower() == tr.artist.lower() and tr.artist and key[1].lower().replace( - " ", "") == tr.title.lower().replace( - " ", "") and tr.title: - to_del.append(key) - total_playtime += value[0] - flags = "".join(set(flags + value[1])) - for key in to_del: - del star_store.db[key] + else: + ref = [] + selection_stage = 2 + for item in shift_selection: + ref.append(default_playlist[item]) - 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?") + for item in shift_selection: + default_playlist[item] = "old" -def reload_metadata(input, keep_star: bool = True) -> None: - global todo + for item in shift_selection: + if move_on_title: + default_playlist.insert(track_position, "new") + else: + default_playlist.insert(track_position + 1, "new") - # vacuum_playtimes(index) - # return - todo = [] + 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 isinstance(input, list): - todo = input + pctl.selected_in_playlist = shift_selection[0] + gui.pl_update += 1 - else: - for k in default_playlist: - if pctl.master_library[input].parent_folder_path == pctl.master_library[k].parent_folder_path: - todo.append(pctl.master_library[k]) + reload_albums(True) + pctl.notify_change() - for i in reversed(range(len(todo))): - if todo[i].is_cue: - del todo[i] + # 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 - for track in todo: + # Right click menu activation + if right_click and line_hit and mouse_position[0] > gui.playlist_left + 10: - search_string_cache.pop(track.index, None) - search_dia_string_cache.pop(track.index, None) + 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 - #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) + if track_position not in shift_selection: + pctl.selected_in_playlist = track_position + shift_selection = [pctl.selected_in_playlist] - pctl.master_library[track.index] = tag_scan(track) + if line_over and inp.mouse_click: - # 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) + 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] - pctl.notify_change() + if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: + playlist_hold = True + playlist_hold_position = track_position - gui.pl_update += 1 - tauon.thread_manager.ready("worker") + # 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 -def reload_metadata_selection() -> None: - cargo = [] - for item in shift_selection: - cargo.append(default_playlist[item]) + # Multi Select Highlight + if track_position in shift_selection or track_position == pctl.selected_in_playlist: + highlight = True - for k in cargo: - if pctl.master_library[k].is_cue == False: - to_scan.append(k) - tauon.thread_manager.ready("worker") + 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 -def editor(index: int | None) -> None: - todo = [] - obs = [] + list_items.append( + (0, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, playing)) + number += 1 - 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]) + if number > gui.playlist_view_length: + break + # --------------------------------------------------------------------------------------- - # 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) + # 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 - file_line = "" - for track in todo: - file_line += ' "' - file_line += pctl.master_library[track].fullpath - file_line += '"' + for type, track_position, tr, track_box, input_box, highlight, number, drag_highlight, playing in list_items: - if system == "Windows" or msys: - file_line = file_line.replace("/", "\\") + line_y = gui.playlist_top + gui.playlist_row_height * number - prefix = "" - app = prefs.tag_editor_target + ddt.text_background_colour = colours.playlist_panel_background - if (system == "Windows" or msys) and app: - if app[0] != '"': - app = '"' + app - if app[-1] != '"': - app = app + '"' + if type == 1: - app_switch = "" + # Is type ALBUM TITLE + separator = " - " + if prefs.row_title_separator_type == 1: + separator = " ‒ " + if prefs.row_title_separator_type == 2: + separator = " ⦁ " - ok = False + date = "" + duration = "" - prefix = launch_prefix + line = tr.parent_folder_name - if system == "Linux": - ok = whicher(prefs.tag_editor_target) - else: + # Use folder name if mixed/singles? + if len(default_playlist) > track_position + 1 and pctl.get_track( + default_playlist[track_position + 1]).album != tr.album and \ + pctl.get_track(default_playlist[track_position + 1]).parent_folder_path == tr.parent_folder_path: + line = tr.parent_folder_name + else: - if 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 + if tr.album_artist != "" and tr.album != "": + line = tr.album_artist + separator + tr.album - ok = True + if prefs.left_align_album_artist_title and not True: + album_artist_mode = True + line = tr.album - if not ok: - show_message(_("Tag editor app does not appear to be installed."), mode="warning") + if len(line) < 6 and "CD" in line: + line = tr.album - 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") + 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 + ")" - return + if line.endswith(")"): + b = line.split("(") + if len(b) > 1 and len(b[1]) <= 11: - if "picard" in prefs.tag_editor_target: - app_switch = " --d " + match = year_search.search(b[1]) - line = prefix + app + app_switch + file_line + if match: + line = b[0] + date = "(" + b[1] - show_message( - prefs.tag_editor_name + " launched.", "Fields will be updated once application is closed.", mode="arrow") - gui.update = 1 + elif line.startswith("("): - complete = subprocess.run(shlex.split(line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + b = line.split(")") + if len(b) > 1 and len(b[0]) <= 11: - 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('"') + match = year_search.search(b[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 match: + line = b[1] + date = b[0] + ")" - gui.message_box = False - reload_metadata(obs, keep_star=False) + if "(" in line and year_search.search(line): + date = "" - # Re apply playtime data in case file names change - for item in old_stars: + line = line.replace(" - ", separator) - old_key = item[1] - old_value = item[2] + qq = 0 + d_date = date + title_line = line - if not old_value: # ignore if there was no old playcount metadata - continue + # Calculate folder duration - new_key = star_store.object_key(item[0]) - new_value = star_store.full_get(item[0].index) + q = track_position - if old_key == new_key: - continue + total_time = 0 + while q < len(default_playlist): - if new_value is None: - new_value = [0, "", 0] + if pctl.get_track(default_playlist[q]).parent_folder_path != tr.parent_folder_path: + break - new_value[0] += old_value[0] - new_value[1] = "".join(set(new_value[1] + old_value[1])) + total_time += pctl.get_track(default_playlist[q]).length - if old_key in star_store.db: - del star_store.db[old_key] + q += 1 + qq += 1 - star_store.db[new_key] = new_value + if qq > 1: + duration = " [ " + get_display_time(total_time) + " ]" # Hair space inside brackets for better visual spacing - gui.pl_update = 1 - gui.update = 1 - pctl.notify_change() + if prefs.append_total_time: + date += duration -def launch_editor(index: int): - if snap_mode: - show_message(_("Sorry, this feature isn't (yet) available with Snap.")) - return + ex = left + highlight_left + highlight_width - 7 * gui.scale - if launch_editor_disable_test(index): - show_message(_("Cannot edit tags of a network track.")) - return + height = line_y + gui.playlist_row_height - 19 * gui.scale # gui.pl_title_y_offset - mini_t = threading.Thread(target=editor, args=[index]) - mini_t.daemon = True - mini_t.start() + 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 launch_editor_selection_disable_test(index: int): - for position in shift_selection: - if pctl.get_track(default_playlist[position]).is_network: - return True - return False + light_offset = 0 + if colours.lm: + light_offset = 3 * gui.scale + ex -= light_offset -def launch_editor_selection(index: int): - if launch_editor_selection_disable_test(index): - show_message(_("Cannot edit tags of a network track.")) - return + if qq > 1: + ex += 1 * gui.scale - mini_t = threading.Thread(target=editor, args=[None]) - mini_t.daemon = True - mini_t.start() + ddt.text_background_colour = colours.playlist_panel_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] + if gui.scale == 2: + height += 1 -def launch_editor_disable_test(index: int): - return pctl.get_track(index).is_network + 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) -def show_lyrics_menu(index: int): - global track_box - track_box = False - enter_showcase_view(track_id=r_menu_index) - inp.mouse_click = False -def recode(text, enc): - return text.encode("Latin-1", "ignore").decode(enc, "ignore") + #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) -def intel_moji(index: int): - gui.pl_update += 1 - gui.update += 1 - track = pctl.master_library[index] - lot = [] + if prefs.row_title_format == 2: - for item in default_playlist: + separator = " | " - if track.album == pctl.master_library[item].album and \ - track.parent_folder_name == pctl.master_library[item].parent_folder_name: - lot.append(item) + start_offset = round(15 * gui.scale) + xx = left + highlight_left + start_offset + ww = highlight_width - lot = set(lot) + 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)] - l_artist = track.artist.encode("Latin-1", "ignore") - l_album = track.album.encode("Latin-1", "ignore") - detect = None + 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) - if track.artist not in track.parent_folder_path: - for enc in encodings: - try: - q_artist = l_artist.decode(enc) - if q_artist.strip(" ") in track.parent_folder_path.strip(" "): - detect = enc - break - except Exception: - logging.exception("Error decoding artist") - continue - if detect is None and track.album not in track.parent_folder_path: - for enc in encodings: - try: - q_album = l_album.decode(enc) - if q_album in track.parent_folder_path: - detect = enc - break - except Exception: - logging.exception("Error decoding album") - continue + 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))) - for item in lot: - t_track = pctl.master_library[item] - if detect is None: - for enc in encodings: - test = recode(t_track.artist, enc) - for cha in test: - if cha in j_chars: - detect = enc - logging.info("This looks like Japanese: " + test) - break - if detect is not None: - break - if detect is None: - for enc in encodings: - test = recode(t_track.title, enc) - for cha in test: - if cha in j_chars: - detect = enc - logging.info("This looks like Japanese: " + test) - break - if detect is not None: - break - if detect is not None: - break - if detect is not None: - logging.info("Fix Mojibake: Detected encoding as: " + detect) - for item in lot: - track = pctl.master_library[item] - # key = pctl.master_library[item].title + pctl.master_library[item].filename - key = star_store.full_get(item) - star_store.remove(item) + 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 - 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) + aa = 0 - if key != None: - star_store.insert(item, key) + ft_width = ddt.get_text_w(line, gui.row_font_size + gui.pl_title_font_offset) - search_string_cache.pop(track.index, None) - search_dia_string_cache.pop(track.index, None) + left_align = highlight_width - date_w - 13 * gui.scale - light_offset - else: - show_message(_("Autodetect failed")) + left_align -= star_offset -def sel_to_car(): - global default_playlist - cargo = [] + extra = aa - for item in shift_selection: - cargo.append(default_playlist[item]) + left_align -= extra -def cut_selection(): - sel_to_car() - del_selected() + 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) -def clip_ar_al(index: int): - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album - SDL_SetClipboardText(line.encode("utf-8")) + else: + ddt.text( + (ex - date_w, height, 1), line, + colours.folder_title, + gui.row_font_size + gui.pl_title_font_offset) -def clip_ar(index: int): - if pctl.master_library[index].album_artist != "": - line = pctl.master_library[index].album_artist - else: - line = pctl.master_library[index].artist - SDL_SetClipboardText(line.encode("utf-8")) + # ----- -def clip_title(index: int): - n_track = pctl.master_library[index] + # 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 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 + # 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]) - SDL_SetClipboardText(line.encode("utf-8")) + continue -def lightning_copy(): - s_copy() - gui.lightning_copy = True + # 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) -def toggle_transcode(mode: int = 0) -> bool: - if mode == 1: - return prefs.enable_transcode - prefs.enable_transcode ^= True - return None + 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 toggle_chromecast(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_chromecast - prefs.show_chromecast ^= True - return None -def toggle_transfer(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_transfer - prefs.show_transfer ^= True + # Blue drop line + if drag_highlight: # playlist_hold_position != p_track: - 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 + ddt.rect( + [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, + 3 * gui.scale], [125, 105, 215, 255]) -def transcode_deco(): - if key_shift_down or key_shiftr_down: - return [colours.menu_text, colours.menu_background, _("Transcode Single")] - return [colours.menu_text, colours.menu_background, _("Transcode Folder")] + # Highlight + if highlight: + ddt.rect_a( + (left + highlight_left, line_y), (highlight_width, gui.playlist_row_height), + colours.row_select_highlight) -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")) + ddt.text_background_colour = alpha_blend(colours.row_select_highlight, ddt.text_background_colour) -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 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) -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) + if not gui.set_mode: - 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") + line_render( + tr, track_position, gui.playlist_text_offset + line_y, + playing, 255, left + inset_left, inset_width, 1, line_y) - return (colours.menu_text, colours.menu_background, text) + else: + # NEE --------------------------------------------------------- + n_track = tr + p_track = track_position + this_line_playing = playing -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) + start = 18 * gui.scale - 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 center_mode: + start = inset_left -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 + elif gui.lsp: + start += gui.lspw - shoot_dl = threading.Thread(target=add_to_spotify_library2, args=([album_url])) - shoot_dl.daemon = True - shoot_dl.start() + run = start + end = start + gui.plw -def selection_queue_deco(): - total = 0 - for item in shift_selection: - total += pctl.get_track(default_playlist[item]).length + if center_mode: + end = highlight_width + start - total = get_hms_time(total) + # 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) - text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" + for h, item in enumerate(gui.pl_st): - return [colours.menu_text, colours.menu_background, text] + 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 -def toggle_rym(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_rym - prefs.show_rym ^= True - return None + if run > end - 50 * gui.scale: + break -def toggle_band(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_band - prefs.show_band ^= True - return None + if len(gui.pl_st) == h + 1: + wid -= 6 * gui.scale -def toggle_wiki(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_wiki - prefs.show_wiki ^= True - return None + 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) -# 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 + if item[0] == "Starline": -def toggle_gen(mode: int = 0) -> bool: - if mode == 1: - return prefs.show_gen - prefs.show_gen ^= True - return None + total = star_store.get_by_object(n_track) -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")) + if total > 0 and n_track.length != 0 and wid > 0: + if gui.star_mode == "star": -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...")) + 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) -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) + 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 -def copy_to_clipboard(text: str) -> None: - SDL_SetClipboardText(text.encode(errors="surrogateescape")) + else: -def copy_from_clipboard(): - return SDL_GetClipboardText().decode() + ratio = total / n_track.length + if ratio > 0.55: + star_x = int(ratio * (4 * gui.scale)) + star_x = min(star_x, wid) -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")) + colour = colours.star_line + if playing and colours.star_line_playing is not None: + colour = colours.star_line_playing -def ser_gen_thread(tr): - s_artist = tr.artist - s_title = tr.title + 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) - 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] + 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 - line = genius(s_artist, s_title, return_url=True) + 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) - r = requests.head(line, timeout=10) + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing + elif item[0] == "#": - 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 + 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) -def ser_gen(track_id, get_lyrics=False): - tr = pctl.master_library[track_id] - if len(tr.title) < 1: - return + 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 = "" - show_message(_("Searching...")) + 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 - shoot = threading.Thread(target=ser_gen_thread, args=[tr]) - shoot.daemon = True - shoot.start() + if get_love(n_track): -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) + 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 clip_ar_tr(index: int) -> None: - line = pctl.master_library[index].artist + " - " + pctl.master_library[index].title + 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 - SDL_SetClipboardText(line.encode("utf-8")) + 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 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) + x = run + u + (heart_row_icon.w + spacing) * count -def is_tidal_track(_) -> bool: - return pctl.master_library[r_menu_index].file_ext == "TIDAL" + j = 0 # justify right + if run < start + 100 * gui.scale: + j = 1 # justify left -# def get_track_spot_url_show_test(_): -# if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): -# return True -# return False + display_friend_heart(x, yy, name, j) + count += 1 -def get_track_spot_url(track_id: int) -> None: - track_object = pctl.get_track(track_id) - url = track_object.misc.get("spotify-track-url") - if url: - copy_to_clipboard(url) - show_message(_("Url copied to clipboard"), mode="done") - else: - show_message(_("No results found")) + # if 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 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] + 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) -def get_spot_artist_track(index: int) -> None: - get_artist_spot(pctl.get_track(index)) + text = str(int(ratio)) + if text == "0": + text = "" + colour = colours.index_text + norm_colour = colour + if this_line_playing is True: + colour = colours.index_playing -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) + 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 -def get_spot_album_track(index: int): - get_album_spot_active(pctl.get_track(index)) + if text: + if item[0] in colours.column_colours: + colour = colours.column_colours[item[0]] -# 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)) + if this_line_playing and item[0] in colours.column_colours_playing: + colour = colours.column_colours_playing[item[0]] -# def get_spot_recs_track(index: int): -# get_spot_recs(pctl.get_track(index)) + if run + 6 * gui.scale + wid > end: + wid = end - run - 40 * gui.scale + if center_mode: + wid += 25 * gui.scale -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]) + wid = max(0, wid) - 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] + # # 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) - elif len(track_list) == 1 and artists: - pctl.multi_playlist[pl].title = artists[0] + ddt.text( + (run + 6 * gui.scale, y + y_off), + text, + colour, + font, + max_w=wid) - if tree_view_box.dragging_name: - pctl.multi_playlist[pl].title = tree_view_box.dragging_name + if ddt.was_truncated: + #logging.info(text) + rect = (run, y, wid - 1, gui.playlist_row_height - 1) + gui.heart_fields.append(rect) - pctl.notify_change() + if coll(rect): + columns_tool_tip.set(run - 7 * gui.scale, y, text, font, rect) -def queue_deco(): - if len(pctl.force_queue) > 0: - line_colour = colours.menu_text - else: - line_colour = colours.menu_text_disabled + run += item[1] - return [line_colour, colours.menu_background, None] + # ----------------------------------------------------------------- + # 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 bass_test(_) -> bool: - # return True - return prefs.backend == 1 + # w += 1 + # if w > gui.playlist_view_length: + # break -def gstreamer_test(_) -> bool: - # return True - return prefs.backend == 2 + # 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 -def field_copy(text_field) -> None: - text_field.copy() + 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 field_paste(text_field) -> None: - text_field.paste() + SDL_SetRenderTarget(renderer, gui.main_texture) + SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) -def field_clear(text_field) -> None: - text_field.clear() + if mouse_down is False: + playlist_hold = False -def vis_off() -> None: - gui.vis_want = 0 - gui.update_layout() - # gui.turbo = False + ddt.pretty_rect = None + ddt.alpha_bg = 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 + def cache_render(self): - gui.vis_want = 1 - gui.update_layout() - # if prefs.backend == 2: - # show_message("Visualisers not implemented in GStreamer mode") - # gui.turbo = True + SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) -def spec_on() -> None: - gui.vis_want = 2 - # if prefs.backend == 2: - # show_message("Not implemented") - gui.update_layout() +class ArtBox: -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 + def __init__(self): + pass - gui.vis_want = 3 - if prefs.backend == 2: - show_message(_("Not implemented")) - # gui.turbo = True - prefs.spec2_colour_setting = "custom" - gui.update_layout() + def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border=None): -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.")) + # 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) -def sa_artist() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Artist", 220, False]) - gui.update_layout() + # We need to find the size of the inner square for the artwork + # box = min(w, h) -def sa_album_artist() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Album Artist", 220, False]) - gui.update_layout() + box_w = w + box_h = h -def sa_composer() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Composer", 220, False]) - gui.update_layout() + box_w -= 17 * gui.scale # Inset the square a bit + box_h -= 17 * gui.scale # Inset the square a bit -def sa_title() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Title", 220, False]) - gui.update_layout() + box_x = x + ((w - box_w) // 2) + box_y = y + ((h - box_h) // 2) -def sa_album() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Album", 220, False]) - gui.update_layout() + # And position the square + rect = (box_x, box_y, box_w, box_h) + gui.main_art_box = rect -def sa_comment() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Comment", 300, False]) - gui.update_layout() + # Draw the album art. If side bar is being dragged set quick draw flag + showc = None + result = 1 -def sa_track() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["#", 25, True]) - gui.update_layout() + 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) -def sa_count() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["P", 25, True]) - gui.update_layout() + # Draw faint border on album art + if tight_border: + if result == 0 and gui.art_drawn_rect: + border = gui.art_drawn_rect + ddt.rect_s(gui.art_drawn_rect, colours.art_box, 1 * gui.scale) + elif default_border: + border = default_border + ddt.rect_s(default_border, colours.art_box, 1 * gui.scale) + else: + border = rect + else: + ddt.rect_s(rect, colours.art_box, 1 * gui.scale) + border = rect -def sa_scrobbles() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["S", 25, True]) - gui.update_layout() + fields.add(border) -def sa_time() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Time", 55, True]) - gui.update_layout() + # 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 -def sa_date() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Date", 55, True]) - gui.update_layout() + # Input for album art + if target_track: -def sa_genre() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Genre", 150, False]) - gui.update_layout() + # Cycle images on click -def sa_file() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Filepath", 350, False]) - gui.update_layout() + if coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: -def sa_filename() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Filename", 300, False]) - gui.update_layout() + album_art_gen.cycle_offset(target_track) -def sa_codec() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Codec", 65, True]) - gui.update_layout() + if pctl.mpris: + pctl.mpris.update(force=True) -def sa_bitrate() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Bitrate", 65, True]) - gui.update_layout() + # 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 sa_lyrics() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Lyrics", 50, True]) - gui.update_layout() + # 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: -def sa_cue() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["CUE", 50, True]) - gui.update_layout() + padding = 6 * gui.scale -def sa_star() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Starline", 80, True]) - gui.update_layout() + 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] -def sa_disc() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Disc", 50, True]) - gui.update_layout() + art_metadata_overlay(xw, yh, showc) -def sa_rating() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["Rating", 80, True]) - gui.update_layout() +class ScrollBox: -def sa_love() -> None: - gui.pl_st.insert(set_menu.reference + 1, ["❤", 25, True]) - # gui.pl_st.append(["❤", 25, True]) - gui.update_layout() + def __init__(self): -def key_love(index: int) -> bool: - return get_love_index(index) + 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 key_artist(index: int) -> str: - return pctl.master_library[index].artist.lower() + 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): -def key_album_artist(index: int) -> str: - return pctl.master_library[index].album_artist.lower() + if max_value < 2: + return 0 -def key_composer(index: int) -> str: - return pctl.master_library[index].composer.lower() + if click is None: + click = inp.mouse_click -def key_comment(index: int) -> str: - return pctl.master_library[index].comment + bar_height = round(90 * gui.scale) -def key_title(index: int) -> str: - return pctl.master_library[index].title.lower() + if h > 400 * gui.scale and max_value < 20: + bar_height = round(180 * gui.scale) -def key_album(index: int) -> str: - return pctl.master_library[index].album.lower() + bg = [255, 255, 255, 7] + fg = [255, 255, 255, 30] + fg_h = [255, 255, 255, 40] + fg_off = [255, 255, 255, 15] -def key_duration(index: int) -> int: - return pctl.master_library[index].length + 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 key_date(index: int) -> str: - return pctl.master_library[index].date + ddt.rect((x, y, w, h), bg) -def key_genre(index: int) -> str: - return pctl.master_library[index].genre.lower() + half = bar_height // 2 -def key_t(index: int): - # return str(pctl.master_library[index].track_number) - return index_key(index) + ratio = value / max_value -def key_codec(index: int) -> str: - return pctl.master_library[index].file_ext + mi = y + half + mo = y + h - half + distance = mo - mi + position = int(round(distance * ratio)) -def key_bitrate(index: int) -> int: - return pctl.master_library[index].bitrate + fw = w + extend_field + fx = x - extend_field -def key_hl(index: int) -> int: - if len(pctl.master_library[index].lyrics) > 5: - return 0 - return 1 + if coll((fx, y, fw, h)): -def sort_ass(h, invert=False, custom_list=None, custom_name=""): - global default_playlist + if mouse_down: + gui.update += 1 - if custom_list is None: - if pl_is_locked(pctl.active_playlist_viewing): - show_message(_("Playlist is locked")) - return + if r_click: + p = mouse_position[1] - half - y + p = max(0, p) - name = gui.pl_st[h][0] - playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - else: - name = custom_name - playlist = custom_list + range = h - bar_height + p = min(p, range) - key = None - ns = False + per = p / range - 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 + value = int(round(max_value * per)) - if custom_list is None: - if key is not None: + ratio = value / max_value - if ns: - key = natsort.natsort_keygen(key=key, alg=natsort.PATH) + mi = y + half + mo = y + h - half + distance = mo - mi + position = int(round(distance * ratio)) - playlist.sort(key=key, reverse=invert) + in_bar = False + if coll((x, mi + position - half, w, bar_height)): + in_bar = True + if click: + self.held = True - pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = playlist - default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + # 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 - 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() + if pctl.playlist_view_position < 0: + pctl.playlist_view_position = 0 -def sort_dec(h): - sort_ass(h, True) -def hide_set_bar(): - gui.set_bar = False - gui.update_layout() - gui.pl_update = 1 + elif mouse_down and not self.held: -def show_set_bar(): - gui.set_bar = True - gui.update_layout() - gui.pl_update = 1 + 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 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] + self.d_position = value / max_value -def toggle_dim_albums(mode: int = 0) -> bool: - if mode == 1: - return prefs.dim_art + 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: - prefs.dim_art ^= True - gui.pl_update = 1 - gui.update += 1 + tt = scroll_timer.hit() + if tt > 0.1: + tt = 0 -def toggle_gallery_combine(mode: int = 0) -> bool: - if mode == 1: - return prefs.gallery_combine_disc + flip = -1 + if self.direction_lock: + flip = 1 - prefs.gallery_combine_disc ^= True - reload_albums() + self.d_position = min(max(self.d_position + (((tt * jump_distance) / max_value) * flip), 0), 1) -def toggle_gallery_click(mode: int = 0) -> bool: - if mode == 1: - return prefs.gallery_single_click + else: + self.slide_hold = False - prefs.gallery_single_click ^= True + if (self.held and mouse_up) or not mouse_down: + self.held = False -def toggle_gallery_thin(mode: int = 0) -> bool: - if mode == 1: - return prefs.thin_gallery_borders + if self.held and not window_is_focused(): + self.held = False - prefs.thin_gallery_borders ^= True - gui.update += 1 - update_layout_do() + if self.held: + get_sdl_input.mouse_capture_want = True + new_y = mouse_position[1] + gui.update += 1 -def toggle_gallery_row_space(mode: int = 0) -> bool: - if mode == 1: - return prefs.increase_gallery_row_spacing + offset = new_y - self.source_click_y - prefs.increase_gallery_row_spacing ^= True - gui.update += 1 - update_layout_do() + position = self.source_bar_y + offset -def toggle_galler_text(mode: int = 0) -> bool: - if mode == 1: - return gui.gallery_show_text + position = max(position, 0) + position = min(position, distance) - gui.gallery_show_text ^= True - gui.update += 1 - update_layout_do() + ratio = position / distance + value = int(round(max_value * ratio)) - # Jump to playing album - if album_mode and gui.first_in_grid is not None: + 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 gui.first_in_grid < len(default_playlist): - goto_album(gui.first_in_grid, force=True) + ddt.rect(rect, colour) -def toggle_card_style(mode: int = 0) -> bool: - if mode == 1: - return prefs.use_card_style + if self.slide_hold: + return round(max_value * self.d_position) - prefs.use_card_style ^= True - gui.update += 1 + return value -def toggle_side_panel(mode: int = 0) -> bool: - global update_layout - global album_mode +class RadioBox: - if mode == 1: - return prefs.prefer_side + def __init__(self): - prefs.prefer_side ^= True - update_layout = True + 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() - if album_mode or prefs.prefer_side is True: - gui.rsp = True - else: - gui.rsp = False + self.x = 1 + self.y = 1 + self.w = 1 + self.h = 1 + self.center = False - if prefs.prefer_side: - gui.rspw = gui.pref_rspw + self.scroll_position = 0 + self.scroll = ScrollBox() -def force_album_view(): - toggle_album_mode(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 = "" -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() + 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) -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 + self.song_key = "" -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() + self.drag = None -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() + self.tab = 0 + self.temp_list = [] -def standard_size(): - global album_mode - global window_size - global update_layout + self.hosts = None + self.host = None - global album_mode_art_size + 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)) - album_mode = False - gui.rsp = True - window_size = window_default_size - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + 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 - gui.rspw = 80 + int(window_size[0] * 0.18) - update_layout = True - album_mode_art_size = 130 - # clear_img_cache() + def parse_vorbis_okay(self): + return ( + self.loaded_url not in self.websocket_source_urls) and \ + "radio.plaza.one" not in self.loaded_url and \ + "gensokyoradio.net" not in self.loaded_url -def path_stem_to_playlist(path: str, title: str) -> None: - """Used with gallery power bar""" - playlist = [] + def search_country(self, text): - # 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) + 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") - 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) + def search_tag(self, text): - pctl.multi_playlist.append(pl_gen( - title=os.path.basename(title).upper(), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + text = text.lower() + self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&tag=" + text) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pctl.active_playlist_viewing].title + "\" f\"" + path + "\"" + def search_title(self, text): - switch_playlist(len(pctl.multi_playlist) - 1) + text = text.lower() + self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&name=" + text) -def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> list | int | None: - logging.debug("Postion set by album locate") + def is_m3u(self, url): + return url.lower().endswith(".m3u") or url.lower().endswith(".m3u8") - if core_timer.get() < 0.5: - return None + def extract_stream_m3u(self, url, recursion_limit=5): + if recursion_limit <= 0: + return None + logging.info("Fetching M3U...") - global album_dex + try: + response = requests.get(url, timeout=10) + if response.status_code != 200: + logging.error(f"M3U Fetch error code: {response.status_code}") + return None - # ---- - 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 - # ---- + content = response.text + lines = content.strip().split("\n") - px = 0 - row = 0 - re = 0 + 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) - 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 + return None - # 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]: + except Exception: + logging.exception("Failed to extract M3U") + return None - # 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 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 - else: - # Set the view to the calculated position - gui.album_scroll_px = px - gui.album_scroll_px -= album_v_slide_value + if self.load_connecting: + return - gui.album_scroll_px = max(gui.album_scroll_px, 0 - album_v_slide_value) + if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: + tauon.spot_ctl.control("stop") - if len(album_dex) > 0: - return album_dex[re] - return 0 + try: + self.websocket.close() + logging.info("Websocket closed") + except Exception: + logging.exception("No socket to close?") - gui.update += 1 + 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 = "" -def toggle_album_mode(force_on=False): - global album_mode - global window_size - global update_layout - global album_playlist_width - global old_album_pos + album_art_gen.clear_cache() - gui.gall_tab_enter = False + if not tauon.test_ffmpeg(): + prefs.auto_rec = False + return - if album_mode is True: + self.run_proxy = True + if url.endswith(".ts"): + self.run_proxy = False - 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() + 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 - gui.rsp = True + # pctl.url = url + pctl.url = f"http://127.0.0.1:{7812}" + if not self.run_proxy: + pctl.url = item["stream_url"] + self.loaded_url = None + pctl.tag_meta = "" + pctl.radio_meta_on = "" + pctl.found_tags = {} + self.song_key = "" + pctl.playing_time = 0 + pctl.decode_time = 0 + self.loaded_station = item - gui.rspw = gui.pref_gallery_w + if tauon.stream_proxy.download_running: + tauon.stream_proxy.abort = True - space = window_size[0] - gui.rspw - if gui.lsp: - space -= gui.lspw + self.load_connecting = True + self.load_failed = False - 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() + shoot = threading.Thread(target=self.start2, args=[url]) + shoot.daemon = True + shoot.start() - reload_albums(quiet=True) + def start2(self, url): - # if pctl.active_playlist_playing == pctl.active_playlist_viewing: - # goto_album(pctl.playlist_playing_position) + 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 album_mode: - if pctl.selected_in_playlist < len(pctl.playing_playlist()): - goto_album(pctl.selected_in_playlist) + 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() -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 tauon.update_play_lock is not None: + tauon.update_play_lock() -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) + time.sleep(0.1) + self.load_connecting = False + self.load_failed = False + gui.update += 1 -def switch_playlist(number, cycle=False, quiet=False): - global default_playlist + 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 - global search_index - global shift_selection + def send_heartbeat(ws): + #logging.info(self.ws_interval) + time.sleep(self.ws_interval) + ws.send("{\"op\":9}") + logging.info("Send heatbeat") - # Close any active menus - # for instance in Menu.instances: - # instance.active = False - close_all_menus() - if gui.radio_view: - if cycle: - pctl.radio_playlist_viewing += number - else: - pctl.radio_playlist_viewing = number - if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: - pctl.radio_playlist_viewing = 0 - return + 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() - gui.previous_playlist_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + if d["op"] == 0: + self.ws_interval = d["d"]["heartbeat"] / 1000 + ws.send("{\"op\":9}") - gui.pl_update = 1 - search_index = 0 - gui.column_d_click_on = -1 - gui.search_error = False - if quick_search_mode: - gui.force_search = True + if d["op"] == 1: + try: - # if pl_follow: - # pctl.multi_playlist[pctl.playlist_active][1] = copy.deepcopy(pctl.playlist_playing) + 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 gui.showcase_mode and gui.combo_mode and not quiet: - view_standard() + pctl.found_tags = found_tags + pctl.tag_meta = line - 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 + filename = d["d"]["song"]["albums"][0]["image"] + fulllink = "https://cdn.listen.moe/covers/" + filename - if gall_pl_switch_timer.get() > 240: - gui.gallery_positions.clear() - gall_pl_switch_timer.set() + #logging.info(fulllink) + art_response = requests.get(fulllink, timeout=10) + #logging.info(art_response.status_code) - gui.gallery_positions[gui.previous_playlist_id] = gui.album_scroll_px + 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 cycle: - pctl.active_playlist_viewing += number - else: - pctl.active_playlist_viewing = number - while pctl.active_playlist_viewing > len(pctl.multi_playlist) - 1: - pctl.active_playlist_viewing -= len(pctl.multi_playlist) - while pctl.active_playlist_viewing < 0: - pctl.active_playlist_viewing += len(pctl.multi_playlist) + 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 - 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 on_error(ws, error): + logging.error(error) - id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int + def on_close(ws): + logging.info("### closed ###") - code = pctl.gen_codes.get(id) - if code is not None and check_auto_update_okay(code, pctl.active_playlist_viewing): - gui.regen_single_id = id - tauon.thread_manager.ready("worker") + def 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 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) + th.start_new_thread(run, ()) - if prefs.auto_goto_playing: - pctl.show_current(this_only=True, playing=False, highlight=True, no_switch=True) + # 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() - 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 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 cycle_playlist_pinned(step): - if gui.radio_view: + def delete_radio_entry_after(self, item): + p = radiobox.right_clicked_station_p + del prefs.radio_urls[p + 1:] - 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 + def edit_entry(self, item): + radio = item + self.radio_field_title.text = radio["title"] + self.radio_field.text = radio["stream_url"] - 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 + def browser_get_hosts(self): - 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 + import socket + """ + Get all base urls of all currently available radiobrowser servers -def activate_info_box(): - fader.rise() - pref_box.enabled = True + Returns: + list: a list of strings -def activate_radio_box(): - radiobox.active = True - radiobox.radio_field.clear() - radiobox.radio_field_title.clear() + """ + 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 new_playlist_colour_callback(): - if gui.radio_view: - return [120, 90, 245, 255] - return [237, 80, 221, 255] + # 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 new_playlist_deco(): - if gui.radio_view: - text = _("New Radio List") - else: - text = _("New Playlist") - return [colours.menu_text, colours.menu_background, text] + # sort list of names + hosts.sort() + # add "https://" in front to make it an url + return list(map(lambda x: "https://" + x, hosts)) -def clean_db_show_test(_): - return gui.suggest_clean_db + def search_page(self): -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 + y = self.y + x = self.x + w = self.w + h = self.h -def clean_db_deco(): - return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] + yy = y + round(40 * gui.scale) -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) + 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) - if album_mode: - reload_albums() - gui.pl_update += 1 + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) -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 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: -def show_import_music(_): - return gui.add_music_folder_ready + 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") -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 + 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() -def stt2(sec): - days, rem = divmod(sec, 86400) - hours, rem = divmod(rem, 3600) - min, sec = divmod(rem, 60) + 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) - s_day = str(days) + "d" - if s_day == "0d": - s_day = " " + 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) - s_hours = str(hours) + "h" - if s_hours == "0h" and s_day == " ": - s_hours = " " + 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) - s_min = str(min) + "m" + 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" - return s_day.rjust(3) + " " + s_hours.rjust(3) + " " + s_min.rjust(3) + self.temp_list.append(radio) -def export_database(): - path = str(user_directory / "DatabaseExport.csv") - xport = open(path, "w") + 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) - xport.write("Artist;Title;Album;Album artist;Track number;Type;Duration;Release date;Genre;Playtime;File path") + radio = {} + radio["title"] = "Yggdrasil Radio | Anime & Jpop" + radio["stream_url_unresolved"] = "http://shirayuki.org:9200/" + radio["stream_url"] = "http://shirayuki.org:9200/" + radio["website_url"] = "https://yggdrasilradio.net/" + self.temp_list.append(radio) - for index, track in pctl.master_library.items(): + for station in primary_stations: + self.temp_list.append(station) - xport.write("\n") + 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() - xport.write(csv_string(track.artist) + ",") - xport.write(csv_string(track.title) + ",") - xport.write(csv_string(track.album) + ",") - xport.write(csv_string(track.album_artist) + ",") - xport.write(csv_string(track.track_number) + ",") - type = "File" - if track.is_network: - type = "Network" - elif track.is_cue: - type = "CUE File" - xport.write(type + ",") - xport.write(str(track.length) + ",") - xport.write(csv_string(track.date) + ",") - xport.write(csv_string(track.genre) + ",") - xport.write(str(int(star_store.get_by_object(track))) + ",") - xport.write(csv_string(track.fullpath)) + def search_radio_browser2(self, param): - xport.close() - show_message(_("Export complete."), _("Saved as: ") + path, mode="done") + if not self.hosts: + self.hosts = self.browser_get_hosts() + if not self.host: + self.host = random.choice(self.hosts) -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)) + 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 clean_db() -> None: - global cm_clean_db - prefs.remove_network_tracks = False - cm_clean_db = True - tauon.thread_manager.ready("worker") + def parse_data(self, data): -def clean_db2() -> None: - global cm_clean_db - prefs.remove_network_tracks = True - cm_clean_db = True - tauon.thread_manager.ready("worker") + self.temp_list.clear() -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) + 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 - show_message(_("{N} ratings imported").format(N=str(len(unique))), mode="done") + def render(self) -> None: - gui.pl_update += 1 + if self.edit_mode: + w = round(510 * gui.scale) + h = round(120 * gui.scale) # + sh -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 + self.w = w + self.h = h + # self.x = x + # self.y = y + width = w + if self.center: + x = int(window_size[0] / 2) - int(w / 2) + y = int(window_size[1] / 2) - int(h / 2) + yy = y + self.y = y + self.x = x + else: + yy = self.y + y = self.y + x = self.x + ddt.rect_a((x - 2 * gui.scale, y - 2 * gui.scale), (w + 4 * gui.scale, h + 4 * gui.scale), colours.box_border) + ddt.rect_a((x, y), (w, h), colours.box_background) + ddt.text_background_colour = colours.box_background + if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): + self.active = False - if 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) + 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) - s = str(len(unique)) + " ratings imported" - if len(skipped) > 0: - s += f", {len(skipped)} skipped" - show_message(s, mode="done") + self.saved() + return - gui.pl_update += 1 + 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) -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 + self.w = w + self.h = h + self.x = x + self.y = y -def find_incomplete() -> None: - gen_incomplete(pctl.active_playlist_viewing) + yy = y -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] + 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 cast_search2() -> None: - chrome.rescan() + ddt.text_background_colour = colours.box_background -def cast_search() -> None: + if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): + self.active = False - if tauon.chrome_mode: - pctl.stop() - chrome.end() - else: - if not chrome: - show_message(_("pychromecast not found")) - return - show_message(_("Searching for Chomecasts...")) - shooter(cast_search2) + ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Station Browser"), colours.box_title_text, 213) -def clear_queue() -> None: - pctl.force_queue = [] - gui.pl_update = 1 - pctl.pause_queue = False + # --- + 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 set_mini_mode_A1() -> None: - prefs.mini_mode_mode = 0 - set_mini_mode() + 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 set_mini_mode_B1() -> None: - prefs.mini_mode_mode = 1 - set_mini_mode() + 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 set_mini_mode_A2() -> None: - prefs.mini_mode_mode = 2 - set_mini_mode() + 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 set_mini_mode_C1() -> None: - prefs.mini_mode_mode = 5 - set_mini_mode() + # --- ---------------------------------------------------------------------- + if self.tab == 1: + self.search_page() + elif self.tab == 0: + self.saved() + self.draw_list() + # self.footer() + return -def set_mini_mode_B2() -> None: - prefs.mini_mode_mode = 3 - set_mini_mode() + def saved(self): + y = self.y + x = self.x + w = self.w + h = self.h -def set_mini_mode_D() -> None: - prefs.mini_mode_mode = 4 - set_mini_mode() + yy = y + round(40 * gui.scale) -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 + width = round(370 * gui.scale) -def stop() -> None: - pctl.stop() + 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) -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() + ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) -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) + yy += round(30 * gui.scale) - key = random.choice(list(folders.keys())) - result = folders[key] - pctl.jump(*result) - pctl.show_current() + 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 -def radio_random() -> None: - pctl.advance(rr=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) -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 draw.button(_("Save"), x + width + round(21 * gui.scale), yy - round(20 * gui.scale), press=gui.level_2_click): -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) + 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"] = "" - rect = (x - round(5 * gui.scale), y - round(4 * gui.scale), round(80 * gui.scale), round(16 * gui.scale)) - gui.heart_fields.append(rect) + if self.add_mode: + pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(radio) + self.active = False - if coll(rect) and (inp.mouse_click or (is_level_zero() and not quick_drag)): - gui.pl_update = 2 - pp = mouse_position[0] - x + else: + show_message(_("Could not validate URL. Must start with https:// or http://")) - if pp < 5 * gui.scale: - rat = 0 - elif pp > 70 * gui.scale: - rat = 10 - else: - rat = pp // (star_row_icon.w // 2) + def draw_list(self): - 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) + x = self.x + y = self.y + w = self.w + h = self.h - # bg = colours.grey(40) - bg = [255, 255, 255, 17] - fg = colours.grey(210) + if self.drag: + gui.update_on_drag = True - if gui.tracklist_bg_is_light: - bg = [0, 0, 0, 25] - fg = colours.grey(70) + yy = y + round(100 * gui.scale) + x += round(10 * gui.scale) - 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) + radio_list = prefs.radio_urls + if self.tab == 1: + radio_list = self.temp_list - for ss in range(5): + 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) - xx = x + ss * star_row_icon.w + 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) - 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: + self.scroll_position = max(self.scroll_position, 0) - 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) + p = self.scroll_position * 2 + offset = 0 + to_delete = None + swap = None -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")] + while True: -def bar_love(notify: bool = False) -> None: - shoot_love = threading.Thread(target=love, args=[True, None, False, notify]) - shoot_love.daemon = True - shoot_love.start() + if p > len(radio_list) - 1: + break -def bar_love_notify() -> None: - bar_love(notify=True) + xx = x + offset + item = radio_list[p] -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] + rect = (xx, yy, round(233 * gui.scale), round(40 * gui.scale)) + fields.add(rect) - shoot_love = threading.Thread(target=love, args=[True, track_id, False, notify]) - shoot_love.daemon = True - shoot_love.start() + bg = colours.box_background + text_colour = colours.box_input_text -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 + playing = pctl.playing_state == 3 and self.loaded_url == item["stream_url"] -def toggle_spotify_like_active() -> None: - tr = pctl.playing_object() - if tr: - shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) - shoot_dl.daemon = True - shoot_dl.start() + if playing: + # bg = colours.box_sub_highlight + # ddt.rect(rect, bg, True) -def toggle_spotify_like_active_deco(): - tr = pctl.playing_object() - text = _("Spotify Like Track") + bg = colours.tab_background_active + text_colour = colours.tab_text_active + ddt.rect(rect, bg) - if pctl.playing_state == 0 or not tr or "spotify-track-url" not in tr.misc: - return [colours.menu_text_disabled, colours.menu_background, text] - if "spotify-liked" in tr.misc: - text = _("Un-like Spotify Track") + if 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) - return [colours.menu_text, colours.menu_background, text] + if coll(rect): -def locate_artist() -> None: - track = pctl.playing_object() - if not track: - return + 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) - artist = track.artist - if track.album_artist: - artist = track.album_artist + bg = alpha_blend(bg, colours.box_background) - 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 + boxx = round(32 * gui.scale) + toff = boxx + round(10 * gui.scale) + if item["title"]: + ddt.text( + (xx + toff, yy + round(3 * gui.scale)), item["title"], text_colour, 212, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) + else: + ddt.text( + (xx + toff, yy + round(3 * gui.scale)), item["stream_url"], text_colour, 212, bg=bg, + max_w=rect[2] - (15 * gui.scale + toff)) - if block_starts: + 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)) - next = False - for start in block_starts: + b_rect = (xx + round(4 * gui.scale), yy + round(4 * gui.scale), boxx, boxx) + ddt.rect(b_rect, colours.box_thumb_background) + radio_thumb_gen.draw(item, b_rect[0], b_rect[1], b_rect[2]) - if next: - pctl.selected_in_playlist = start - pctl.playlist_view_position = start - shift_selection.clear() + if offset == 0: + offset = rect[2] + round(4 * gui.scale) + else: + offset = 0 + yy += round(43 * gui.scale) + + if yy > y + 300 * gui.scale: break - if pctl.selected_in_playlist == start: - next = True - continue + p += 1 - else: - pctl.selected_in_playlist = block_starts[0] - pctl.playlist_view_position = block_starts[0] - shift_selection.clear() + # 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) - 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")) + # 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)) - logging.debug("Position changed by artist locate") + # 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 - gui.pl_update += 1 + # if not mouse_down: + # self.drag = None -def activate_search_overlay() -> None: - if cm_clean_db: - show_message(_("Please wait for cleaning process to finish")) - return - search_over.active = True - search_over.delay_enter = False - search_over.search_text.selection = 0 - search_over.search_text.cursor_position = 0 - search_over.spotify_mode = False + def footer(self): -def get_album_spot_url_active() -> None: - tr = pctl.playing_object() - if tr: - url = tauon.spot_ctl.get_album_url_from_local(tr) + y = self.y + x = self.x + round(15 * gui.scale) + w = self.w + h = self.h - if url: - copy_to_clipboard(url) - show_message(_("URL copied to clipboard"), mode="done") - else: - show_message(_("No results found")) + 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")) -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") + 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") - return [colours.menu_text, colours.menu_background, text] + 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 -def goto_playing_extra() -> None: - pctl.show_current(highlight=True) +class RenamePlaylistBox: -def show_spot_playing_deco(): - if not (tauon.spot_ctl.coasting or tauon.spot_ctl.playing): - return [colours.menu_text, colours.menu_background, None] - return [colours.menu_text_disabled, colours.menu_background, None] + def __init__(self): -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] + self.x = 300 + self.y = 300 + self.playlist_index = 0 -def show_spot_playing() -> None: - if pctl.playing_state != 0 and pctl.playing_state != 3 and not tauon.spot_ctl.coasting and not tauon.spot_ctl.playing: - pctl.stop() - tauon.spot_ctl.update(start=True) + self.edit_generator = False -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 toggle_edit_gen(self): -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")) + self.edit_generator ^= True + if self.edit_generator: -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")) + if len(rename_text_area.text) > 0: + pctl.multi_playlist[self.playlist_index].title = rename_text_area.text -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")) + pl = self.playlist_index + id = pl_to_id(pl) -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])) + text = pctl.gen_codes.get(id) + if not text: + text = "" - 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")) + rename_text_area.set_text(text) + rename_text_area.highlight_none() -def spot_import_context() -> None: - shooter(tauon.spot_ctl.import_context) + gui.regen_single = rename_playlist_box.playlist_index + tauon.thread_manager.ready("worker") -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] + else: + rename_text_area.set_text(pctl.multi_playlist[self.playlist_index].title) + rename_text_area.highlight_none() + # rename_text_area.highlight_all() -def get_artist_spot(tr: TrackClass = None) -> None: - if not tr: - tr = pctl.playing_object() - if not tr: - return - url = tauon.spot_ctl.get_artist_url_from_local(tr) - if not url: - show_message(_("No results found")) - return - show_message(_("Fetching...")) - shooter(tauon.spot_ctl.artist_playlist, (url,)) + def render(self): -# 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 gui.level_2_click: + inp.mouse_click = True + gui.level_2_click = False -def toggle_auto_theme(mode: int = 0) -> None: - if mode == 1: - return prefs.colour_from_image + if inp.key_tab_press: + self.toggle_edit_gen() - prefs.colour_from_image ^= True - gui.theme_temp_current = -1 + text_w = ddt.get_text_w(rename_text_area.text, 315) + min_w = max(250 * gui.scale, text_w + 50 * gui.scale) - gui.reload_theme = True + 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 prefs.colour_from_image and prefs.art_bg and not key_shift_down: - # toggle_auto_bg() + # Draw background + ddt.rect(rect, bg) -def toggle_auto_bg(mode: int= 0) -> bool | None: - if mode == 1: - return prefs.art_bg - prefs.art_bg ^= True + # 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 prefs.art_bg: - gui.update = 60 + # Draw accent + rect2 = [self.x, self.y + rect[3] - 4 * gui.scale, min_w, 4 * gui.scale] + ddt.rect(rect2, [255, 255, 255, 60]) - style_overlay.flush() - tauon.thread_manager.ready("style") - # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: - # toggle_auto_theme() - return None + if self.edit_generator: + pl = self.playlist_index + id = pl_to_id(pl) + pctl.gen_codes[id] = rename_text_area.text -def toggle_auto_bg_strong(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.art_bg_stronger == 2 + if input_text or key_backspace_press: + gui.regen_single = rename_playlist_box.playlist_index + tauon.thread_manager.ready("worker") - if prefs.art_bg_stronger == 2: - prefs.art_bg_stronger = 1 - else: - prefs.art_bg_stronger = 2 - gui.update_layout() - return None + # 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 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 + if hint_rect[0] + hint_rect[2] > window_size[0]: + hint_rect[0] = window_size[0] - hint_rect[2] -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 + 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) -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 + text_colour = [150, 150, 150, 255] + title_colour = text_colour + code_colour = [250, 250, 250, 255] + hint_colour = [110, 110, 110, 255] -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 + title_font = 311 + code_font = 311 + hint_font = 310 -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 + # ddt.pretty_rect = hint_rect -def toggle_notifications(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.show_notifications + 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) - prefs.show_notifications ^= True + 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) - if prefs.show_notifications: - if not de_notify_support: - show_message(_("Notifications for this DE not supported"), "", mode="warning") - return None + 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) -# 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 + 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) -def toggle_mini_lyrics(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.show_lyrics_side - prefs.show_lyrics_side ^= True - return None + # 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 toggle_showcase_vis(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.showcase_vis + 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) - prefs.showcase_vis ^= True - gui.update_layout() - return None + 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) -def toggle_level_meter(mode: int = 0) -> bool | None: - if mode == 1: - return gui.vis_want != 0 + # 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) - if gui.vis_want == 0: - gui.vis_want = 1 - else: - gui.vis_want = 0 + 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) - gui.update_layout() - return None + # yy += round(16 * gui.scale) + ddt.text((xx0, yy), _("Sorters"), title_colour, title_font) + yy += round(14 * gui.scale) -# 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() + 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) -def level_meter_special_2(): - gui.level_meter_colour_mode = 2 + 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) -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") + 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) - bg = colours.menu_background + # ddt.pretty_rect = None - return [colours.menu_text, bg, line] + # 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 -def lastfm_colour() -> list[int] | None: - if not prefs.scrobble_hold: - return [250, 50, 50, 255] - return 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 -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 +class PlaylistBox: -def lb_mode() -> bool: - return prefs.enable_lb + def recalc(self): + self.tab_h = round(25 * gui.scale) + self.gap = round(2 * gui.scale) -def get_album_art_url(tr: TrackClass): + self.text_offset = 2 * gui.scale + if gui.scale == 1.25: + self.text_offset = 3 - artist = tr.album_artist - if not tr.album: - return None - if not artist: - artist = tr.artist - if not artist: - return None + def __init__(self): - 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 + self.scroll_on = prefs.old_playlist_box_position + self.drag = False + self.drag_source = 0 + self.drag_on = -1 - if not release_group_id: - release_group_id = tr.misc.get("musicbrainz_releasegroupid") + self.adds = [] - if not release_id: - release_id = tr.misc.get("musicbrainz_albumid") + self.indicate_w = round(2 * gui.scale) - 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 + 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 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 + # if gui.scale == 1.25: + self.tab_h = 0 + self.gap = 0 - base_url = "https://coverartarchive.org/release-group/" - url = f"{base_url}{release_group_id}" + self.text_offset = 2 * gui.scale + self.recalc() - 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") + def draw(self, x, y, w, h): - 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 + global quick_drag - url = image["thumbnails"].get("250") - if url is None: - url = image["thumbnails"].get("small") + # 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 url: - logging.info("got mb image url for discord") - pctl.mbid_image_url_cache[final_id] = url - return url + max_tabs = (h - 10 * gui.scale) // (self.gap + self.tab_h) - pctl.album_mbid_release_cache[(artist, tr.album)] = None - pctl.album_mbid_release_group_cache[(artist, tr.album)] = None + tab_title_colour = [230, 230, 230, 255] - return None + bg_lumi = test_lumi(colours.playlist_box_background) + light_mode = False -def discord_loop() -> None: - prefs.discord_active = True + if bg_lumi < 0.55: + light_mode = True + tab_title_colour = [20, 20, 20, 255] - try: - if not pctl.playing_ready(): - return - asyncio.set_event_loop(asyncio.new_event_loop()) + dark_mode = False + if bg_lumi > 0.8: + dark_mode = True - # logging.info("Attempting to connect to Discord...") - client_id = "954253873160286278" - RPC = Presence(client_id) - RPC.connect() + if light_mode: + indicate_w = round(3 * gui.scale) + else: + indicate_w = round(2 * gui.scale) - logging.info("Discord RPC connection successful.") - time.sleep(1) - start_time = time.time() - idle_time = Timer() + show_scroll = False + tab_start = x + 10 * gui.scale - state = 0 - index = -1 - br = False - gui.discord_status = "Connected" - gui.update += 1 - current_state = 0 + if window_size[0] < 700 * gui.scale: + tab_start = x + 4 * gui.scale - while True: - while True: + if mouse_wheel != 0 and coll((x, y, w, h)): + self.scroll_on -= mouse_wheel - current_index = pctl.playing_object().index - if pctl.playing_state == 3: - current_index = radiobox.song_key + self.scroll_on = min(self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) - 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() + self.scroll_on = max(self.scroll_on, 0) - 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 + if len(pctl.multi_playlist) > max_tabs: + show_scroll = True + else: + self.scroll_on = 0 - break + if show_scroll: + tab_start += 15 * gui.scale - if current_state == 0 and idle_time.get() > 13: - logging.info("Pause discord RPC...") - gui.discord_status = "Idle" - RPC.clear(pid) - # RPC.close() + if colours.lm: + w -= round(6 * gui.scale) + tab_width = w - tab_start # - 0 * gui.scale - 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) + # 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 prefs.disconnect_discord: - continue + draw_pin_indicator = False # prefs.tabs_on_top - time.sleep(2) + # 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 prefs.disconnect_discord: - RPC.clear(pid) - RPC.close() - prefs.disconnect_discord = False - gui.discord_status = "Not connected" - br = True - break + # Process inputs + delete_pl = None + tab_on = 0 + yy = y + 5 * gui.scale + for i, pl in enumerate(pctl.multi_playlist): - if br: + if tab_on >= max_tabs: break + if i < self.scroll_on: + continue - 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 not pl.hidden and i in tabs_on_top: + # continue - if tr.album: - album = tr.album - else: - album = _("Unknown Album") - if pctl.playing_state == 3: - album = radiobox.loaded_station["title"] + tab_on += 1 - if len(album) == 1: - album += " " + if coll((tab_start, yy - 1, tab_width, (self.tab_h + 1))): + if right_click: + if gui.radio_view: + radio_tab_menu.activate(i, mouse_position) + else: + tab_menu.activate(i, mouse_position) + gui.tab_menu_pl = i - if state == 1: - #logging.info("PLAYING: " + title) - #logging.info(start_time) - url = get_album_art_url(pctl.playing_object()) + if tab_menu.active is False and middle_click: + delete_pl = i + # delete_playlist(i) + # break - 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) + if mouse_up and self.drag and coll_point(mouse_up_position, (tab_start, yy - 1, tab_width, (self.tab_h + 1))): - else: - #logging.info("Discord RPC - Stop") - RPC.update( - pid=pid, - state="Idle", - large_image="tauon-standard") + # 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 - time.sleep(5) + # 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) - if prefs.disconnect_discord: - RPC.clear(pid) - RPC.close() - prefs.disconnect_discord = False - break + gui.update += 1 - 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 + # 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: - finally: - loop = asyncio.get_event_loop() - if not loop.is_closed(): - loop.close() - prefs.discord_active = False + 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 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 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 open_donate_link() -> None: - webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) + # 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 stop_quick_add() -> None: - pctl.quick_add_target = None + 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 show_stop_quick_add(_) -> bool: - return pctl.quick_add_target is not None + # 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 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() + yy += self.tab_h + self.gap -# 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] + # Draw tabs + # delete_pl = None + tab_on = 0 + yy = y + 5 * gui.scale + for i, pl in enumerate(pctl.multi_playlist): -def view_standard_meta() -> None: - # if gui.show_playlist is False: - # gui.show_playlist = True - if album_mode: - toggle_album_mode() + # if yy + self.tab_h > y + h: + # break + if tab_on >= max_tabs: + break + if i < self.scroll_on: + continue - if gui.combo_mode: - exit_combo() + tab_on += 1 - if not gui.rsp: - toggle_side_panel() + name = pl.title + hidden = pl.hidden - global update_layout - update_layout = True - # gui.rspw = 80 + int(window_size[0] * 0.18) + # Background is insivible by default (for hightlighting if selected) + bg = [0, 0, 0, 0] -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() + # 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] -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] + # 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] -# 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 + # 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] -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() + # Get actual bg from blend for text bg + real_bg = alpha_blend(bg, colours.playlist_box_background) -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 + # Draw highlight + ddt.rect((tab_start, yy - round(1 * gui.scale), tab_width, self.tab_h), bg) - if gui.set_mode: - return [tc, colours.menu_background, _("Disable Columns")] - return [tc, colours.menu_background, _("Enable Columns")] + # Draw title text + text_start = 10 * gui.scale + if draw_pin_indicator: + # text_start = 40 * gui.scale + text_start = 32 * gui.scale -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 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 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")] + if not pl.hidden and prefs.tabs_on_top: + cl = [255, 255, 255, 25] -def toggle_playlist_break() -> None: - pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 - gui.pl_update = 1 + if light_mode: + cl = [0, 0, 0, 40] -def transcode_single(item: list[tuple[int, str]], manual_directory: str | None = None, manual_name: str | None = None): - global core_use - global dl_use + xx = tab_start + tab_width - self.lock_icon.w + self.lock_icon.render(xx, yy, cl) - 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 + 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) - t = pctl.master_library[track] + # Is mouse collided with tab? + hit = coll((tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) - path = t.fullpath - cleanup = False + # if not prefs.tabs_on_top: + if i == pctl.active_playlist_playing: - 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 + indicator_colour = colours.title_playing + if colours.lm: + indicator_colour = colours.seek_bar_fill - if not os.path.isfile(path): - show_message(_("Encoding warning: Missing one or more files")) - core_use -= 1 - return + ddt.rect((tab_start + 0 - 2 * gui.scale, yy - round(1 * gui.scale), indicate_w, self.tab_h), indicator_colour) - out_line = encode_track_name(t) + # # 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]) - if not (output / _("output")).exists(): - (output / _("output")).mkdir() - target_out = str(output / _("output") / (str(track) + "." + codec)) + # 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]) - command = tauon.get_ffmpeg() + " " + 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]) - if not t.is_cue: - command += '-i "' - else: - command += "-ss " + str(t.start_time) - command += " -t " + str(t.length) + # 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 - command += ' -i "' + 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 - command += path.replace('"', '\\"') + 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]) - 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("'", "") + '" ' + yy += self.tab_h + self.gap - if codec != "flac": - command += " -b:a " + str(bitrate) + "k -vn " + if delete_pl is not None: + # delete_playlist(delete_pl) + delete_playlist_ask(delete_pl) + gui.update += 1 - command += '"' + target_out.replace('"', '\\"') + '"' + # Create new playlist if drag in blank space after tabs + rect = (x, yy, w - 10 * gui.scale, h - (yy - y)) + fields.add(rect) - # logging.info(shlex.split(command)) - startupinfo = None - if system == "Windows" or msys: - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + if coll(rect): + if quick_drag: + ddt.rect((tab_start, yy, tab_width, self.indicate_w), [80, 160, 200, 255]) + if mouse_up: + drop_tracks_to_new_playlist(shift_selection) - if not msys: - command = shlex.split(command) + if right_click: + extra_tab_menu.activate(pctl.active_playlist_viewing) - subprocess.call(command, stdout=subprocess.PIPE, shell=False, startupinfo=startupinfo) + # 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 - logging.info("FFmpeg finished") - if codec == "opus" and prefs.transcode_opus_as: - codec = "ogg" + 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]) - # logging.info(target_out) +class ArtistList: - 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 __init__(self): - 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) + self.tab_h = round(60 * gui.scale) + self.thumb_size = round(55 * gui.scale) - old_key = star_store.key(track) - old_star = star_store.full_get(track) + self.current_artists = [] + self.current_album_counts = {} + self.current_artist_track_counts = {} - try: - send2trash(pctl.master_library[track].fullpath) - except Exception: - logging.exception("File trash error") + self.thumb_cache = {} - if os.path.isfile(pctl.master_library[track].fullpath): - try: - os.remove(pctl.master_library[track].fullpath) - except Exception: - logging.exception("File delete error") + self.to_fetch = "" + self.to_fetch_mbid_a = "" - pctl.master_library[track].fullpath = new_name - pctl.master_library[track].file_ext = codec.upper() + self.scroll_position = 0 - # Update and merge playtimes - new_key = star_store.key(track) - if old_star and (new_key != old_key): + self.id_to_load = "" - new_star = star_store.full_get(track) - if new_star is None: - new_star = star_store.new_object() + self.d_click_timer = Timer() + self.d_click_ref = -1 - 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])) + self.click_ref = -1 + self.click_highlight_timer = Timer() - if old_key in star_store.db: - del star_store.db[old_key] + self.saves = {} - star_store.db[new_key] = new_star + self.load = False - gui.transcoding_bach_done += 1 - if cleanup: - os.remove(path) - core_use -= 1 - gui.update += 1 + self.shown_letters = [] -def cue_scan(content: str, tn: TrackClass) -> int | None: - # Get length from backend + self.hover_on = "NONE" + self.hover_timer = Timer(10) - lasttime = tn.length + self.sample_tracks = {} - content = content.replace("\r", "") - content = content.split("\n") + def load_img(self, artist): - #logging.info(content) + filepath = artist_info_box.get_data(artist, get_img_path=True) - global added + if filepath and os.path.isfile(filepath): - cued = [] + try: + g = io.BytesIO() + g.seek(0) - LENGTH = 0 - PERFORMER = "" - TITLE = "" - START = 0 - DATE = "" - ALBUM = "" - GENRE = "" - MAIN_PERFORMER = "" + im = Image.open(filepath) - for LINE in content: - if 'TITLE "' in LINE: - ALBUM = LINE[7:len(LINE) - 2] + 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), + )) - if 'PERFORMER "' in LINE: - while LINE[0] != "P": - LINE = LINE[1:] + im.thumbnail((self.thumb_size, self.thumb_size), Image.Resampling.LANCZOS) - MAIN_PERFORMER = LINE[11:len(LINE) - 2] + im.save(g, "PNG") + g.seek(0) - if "REM DATE" in LINE: - DATE = LINE[9:len(LINE) - 1] + 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) - if "REM GENRE" in LINE: - GENRE = LINE[10:len(LINE) - 1] + self.thumb_cache[artist] = [texture, sdl_rect] + except Exception: + logging.exception("Artist thumbnail processing error") + self.thumb_cache[artist] = None - if "TRACK " in LINE: - break + elif artist in prefs.failed_artists: + self.thumb_cache[artist] = None + elif not self.to_fetch: - 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 + if prefs.auto_dl_artist_data: + self.to_fetch = artist + tauon.thread_manager.ready("worker") - START = int(LINE[pos - 2:pos]) + (int(LINE[pos - 5:pos - 3]) * 60) - LENGTH = int(lasttime) - START - lasttime = START + else: + self.thumb_cache[artist] = None - 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 + def worker(self): - elif 'TITLE "' in LINE: + if self.load: - 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 + if after_scan: + return - elif "TRACK " in LINE: + self.prep() + self.load = False + return - pos = 0 - while LINE[pos] != "K": - pos += 1 - if pos > 15: - return 1 - TN = LINE[pos + 2:pos + 4] + if self.to_fetch: - TN = int(TN) + if get_lfm_wait_timer.get() < 2: + return - # try: - # bitrate = audio.info.bitrate - # except Exception: - # logging.exception("Failed to set audio bitrate") - # bitrate = 0 + artist = self.to_fetch + f_artist = filename_safe(artist) + filename = f_artist + "-lfm.png" + filename2 = f_artist + "-lfm.txt" + filename3 = f_artist + "-ftv.jpg" + filename4 = f_artist + "-dcg.jpg" + filepath = os.path.join(a_cache_dir, filename) + filepath2 = os.path.join(a_cache_dir, filename2) + filepath3 = os.path.join(a_cache_dir, filename3) + filepath4 = os.path.join(a_cache_dir, filename4) + got_image = False + try: + # Lookup artist info on last.fm + logging.info("lastfm lookup artist: " + artist) + mbid = lastfm.artist_mbid(artist) + get_lfm_wait_timer.set() + # if data[0] is not False: + # #cover_link = data[2] + # text = data[1] + # + # if not os.path.exists(filepath2): + # f = open(filepath2, 'w', encoding='utf-8') + # f.write(text) + # f.close() - if PERFORMER == "": - PERFORMER = MAIN_PERFORMER + if mbid and prefs.enable_fanart_artist: + save_fanart_artist_thumb(mbid, filepath3, preview=True) + got_image = True - nt = copy.deepcopy(tn) + except Exception: + logging.exception("Failed to find image from fanart.tv") - nt.cue_sheet = "" - nt.is_embed_cue = True + if not got_image and verify_discogs(): + try: + save_discogs_artist_thumb(artist, filepath4) + except Exception: + logging.exception("Failed to find image from discogs") - nt.index = pctl.master_count - # nt.fullpath = filepath.replace('\\', '/') - # nt.filename = filename - # nt.parent_folder_path = os.path.dirname(filepath.replace('\\', '/')) - # nt.parent_folder_name = os.path.splitext(os.path.basename(filepath))[0] - # nt.file_ext = os.path.splitext(os.path.basename(filepath))[1][1:].upper() - if MAIN_PERFORMER: - nt.album_artist = MAIN_PERFORMER - if PERFORMER: - nt.artist = PERFORMER - if GENRE: - nt.genre = GENRE - nt.title = TITLE - nt.length = LENGTH - # nt.bitrate = source_track.bitrate - if ALBUM: - nt.album = ALBUM - if DATE: - nt.date = DATE.replace('"', "") - nt.track_number = TN - nt.start_time = START - nt.is_cue = True - nt.size = 0 # source_track.size - # nt.samplerate = source_track.samplerate - if TN == 1: - nt.size = os.path.getsize(nt.fullpath) + if 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) - pctl.master_library[pctl.master_count] = nt + self.to_fetch = "" - cued.append(pctl.master_count) - # loaded_paths_cache[filepath.replace('\\', '/')] = pctl.master_count - # added.append(pctl.master_count) + def prep(self): + self.scroll_position = 0 - pctl.master_count += 1 - LENGTH = 0 - PERFORMER = "" - TITLE = "" - START = 0 - TN = 0 + curren_pl_no = id_to_pl(self.id_to_load) + if curren_pl_no is None: + return + current_pl = pctl.multi_playlist[curren_pl_no] - added += reversed(cued) + all = [] + artist_parents = {} + counts = {} + play_time = {} + filtered = 0 + b = 0 - # cue_list.append(filepath) + try: -def get_album_from_first_track(track_position, track_id=None, pl_number=None, pl_id: int | None = None): - if pl_number is None: + for item in current_pl.playlist_ids: + b += 1 + if b % 100 == 0: + time.sleep(0.001) - if pl_id: - pl_number = id_to_pl(pl_id) - else: - pl_number = pctl.active_playlist_viewing + track = pctl.get_track(item) - playlist = pctl.multi_playlist[pl_number].playlist_ids + 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) - if track_id is None: - track_id = playlist[track_position] + artists = [x.strip() for x in artists.split(";")] - if playlist[track_position] != track_id: - return [] + pp = 0 + if prefs.artist_list_sort_mode == "play": + pp = star_store.get(item) - tracks = [] - album_parent_path = pctl.get_track(track_id).parent_folder_path + for artist in artists: - i = track_position + if artist: - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break + # Add play time + if prefs.artist_list_sort_mode == "play": + p = play_time.get(artist, 0) + play_time[artist] = p + pp - tracks.append(playlist[i]) - i += 1 + # Get a sample track for fallback art + if artist not in self.sample_tracks: + self.sample_tracks[artist] = track - return tracks + # 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 -class SearchOverlay: + 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 __init__(self): + current_album_counts = artist_parents - self.active = False - self.search_text = TextBox() + 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 ")) - 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 + except Exception: + logging.exception("Album scan failure") + time.sleep(4) + return - def clear(self): - self.search_text.text = "" - self.results.clear() - self.searched_text = "" - self.on = 0 - self.all_folders = False + # Artist-list, album-counts, scroll-position, playlist-length, number ignored + save = [all, current_album_counts, 0, len(current_pl.playlist_ids), counts, filtered] - def click_artist(self, name, get_list=False, search_lists=None): + # 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 - playlist = [] + 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 - if search_lists is None: - search_lists = [] - for pl in pctl.multi_playlist: - search_lists.append(pl.playlist_ids) + self.saves[current_pl.uuid_int] = save + gui.update += 1 - 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) + def locate_artist_letter(self, text): - if get_list: - return playlist + if not text or prefs.artist_list_sort_mode != "alpha": + return - pctl.multi_playlist.append(pl_gen( - title=_("Artist: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + 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 - 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 + "\"" + 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 - inp.key_return_press = False + def locate_artist(self, track: TrackClass): - def click_year(self, name, get_list: bool = False): + 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 - 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) + 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 get_list: - return playlist + def draw_card_text_only(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): - pctl.multi_playlist.append(pl_gen( - title=_("Year: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + album_mode = False + for albums in self.current_album_counts.values(): + if len(albums) > 1: + album_mode = True + break - if gui.combo_mode: - exit_combo() + 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)) - switch_playlist(len(pctl.multi_playlist) - 1) + if gui.preview_artist_loading == artist: + # . Max 20 chars. Alt: Downloading image, Loading image + text = _("Downloading data...") - inp.key_return_press = 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) - def click_composer(self, name: str, get_list: bool = False): + def draw_card_with_thumbnail(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): - 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 artist not in self.thumb_cache: + self.load_img(artist) - if get_list: - return playlist + 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) - pctl.multi_playlist.append(pl_gen( - title=_("Composer: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + 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 gui.combo_mode: - exit_combo() + 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)) - switch_playlist(len(pctl.multi_playlist) - 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) - inp.key_return_press = False + ddt.rect(tab_rect, back_colour_2) + bg = back_colour_2 - def click_meta(self, name: str, get_list: bool = False, search_lists=None): + ddt.rect(rect, back_colour) + ddt.rect(rect, border_colour) - if search_lists is None: - search_lists = [] - for pl in pctl.multi_playlist: - search_lists.append(pl.playlist_ids) + fields.add(rect) + if coll(rect) and is_level_zero(True): + self.hover_any = True - 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) + hover_delay = 0.5 + if gui.compact_artist_list: + hover_delay = 2 - if get_list: - return playlist + 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() - pctl.multi_playlist.append(pl_gen( - title=os.path.basename(name).upper(), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + if path: + set_artist_preview(path, artist, round(thumb_x + self.thumb_size), round(y)) - if gui.combo_mode: - exit_combo() + if inp.mouse_click: + self.hover_timer.force_set(-2) + gui.delay_frame(2 + hover_delay) - switch_playlist(len(pctl.multi_playlist) - 1) + 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) - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "p\"" + name + "\"" + 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) - inp.key_return_press = False + # Draw labels + if not thin_mode or (coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY): - def click_genre(self, name: str, get_list: bool = False, search_lists=None): + album_mode = False + for albums in self.current_album_counts.values(): + if len(albums) > 1: + album_mode = True + break - playlist = [] + 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 search_lists is None: - search_lists = [] - for pl in pctl.multi_playlist: - search_lists.append(pl.playlist_ids) + if gui.preview_artist_loading == artist: + # . Max 20 chars. Alt: Downloading image, Loading image + text = _("Downloading data...") - include_multi = False - if name.endswith("+") or not prefs.sep_genre_multi: - name = name.rstrip("+") - include_multi = 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) - 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) + def draw_card(self, artist, x, y, w): - if get_list: - return playlist + 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) - pctl.multi_playlist.append(pl_gen( - title=_("Genre: ") + name, - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + fields.add(area) - if gui.combo_mode: - exit_combo() + light_mode = False + line1_colour = [235, 235, 235, 255] + line2_colour = [255, 255, 255, 120] + fade_max = 50 - switch_playlist(len(pctl.multi_playlist) - 1) + thin_mode = False + if gui.compact_artist_list: + thin_mode = True + line2_colour = [115, 115, 115, 255] - 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 + "\"" + 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] - inp.key_return_press = False + # Fade on click + bg = colours.side_panel_background + if not thin_mode: - def click_album(self, index): + 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: - pctl.jump(index) - if gui.combo_mode: - exit_combo() + fade = 0 + t = self.click_highlight_timer.get() + if self.click_ref == artist and (t < 2.2 or artist_list_menu.active): - pctl.show_current() + if t < 1.9 or artist_list_menu.active: + fade = fade_max + else: + fade = fade_max - round((t - 1.9) / 0.3 * fade_max) - inp.key_return_press = False + gui.update += 1 + ddt.rect(area, [50, 50, 50, fade]) - def render(self): - global input_text - if self.active is False: + bg = alpha_blend([50, 50, 50, fade], colours.side_panel_background) - # 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: + 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) - # 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 + 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 - activate_search_overlay() - self.old_mouse = copy.deepcopy(mouse_position) + double_click = False + if self.d_click_timer.get() < 0.4 and self.d_click_ref == artist: + double_click = True - if self.active: + self.click_highlight_timer.set() - x = 0 - y = 0 - w = window_size[0] - h = window_size[1] + 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 keymaps.test("add-to-queue"): - input_text = "" - if inp.backspace_press: - # self.searched_text = "" - # self.results.clear() + blocks = [] + current_block = [] - if len(self.search_text.text) - inp.backspace_press < 1: - self.active = False - self.search_text.text = "" - self.results.clear() - self.searched_text = "" - return + in_artist = False + this_artist = artist.casefold() + last_ref = None + on = 0 - 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(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 gui.level_2_click and mouse_position[0] > 350 * gui.scale: - self.active = False - self.search_text.text = "" + 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 - mouse_change = False - if not point_proximity_test(self.old_mouse, mouse_position, 25): - mouse_change = True - # mouse_change = True + if current_block: + blocks.append(current_block) + current_block = [] - ddt.rect((x, y, w, h), [3, 3, 3, 235]) - ddt.text_background_colour = [12, 12, 12, 255] + #logging.info(blocks) + # return + # block_starts = [] + # current = False + # for i in range(len(default_playlist)): + # track = pctl.get_track(default_playlist[i]) + # if current is False: + # if track.artist == artist or track.album_artist == artist or ( + # 'artists' in track.misc and artist in track.misc['artists']): + # block_starts.append(i) + # current = True + # else: + # if track.artist != artist and track.album_artist != artist or ( + # 'artists' in track.misc and artist in track.misc['artists']): + # current = False + # + # if not block_starts: + # logging.info("No matching artists found in playlist") + # return - input_text_x = 80 * gui.scale - highlight_x = 30 * gui.scale - thumbnail_rx = 100 * gui.scale - text_lx = 120 * gui.scale + if not blocks: + return - s_font = 15 - s_b_font = 214 - b_font = 215 + #select = block_starts[0] - 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 + # 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] - #album_art_size_s = 0 * gui.scale + gui.pl_update += 1 - # Search active animation - if self.sip: - x = round(15 * gui.scale) - y = x - s = round(7 * gui.scale) - g = round(4 * gui.scale) + self.click_highlight_timer.set() - t = self.animate_timer.get() - if abs(t - self.last_animate_time) > 0.3: - self.animate_timer.set() - t = 0 + select = blocks[0][0] - self.last_animate_time = t + if double_click: + # Stat first artist track in playlist - for item in range(4): - a = 100 - if round(t * 14) % 4 == item: - a = 255 - if self.spotify_mode: - colour = (145, 245, 78, a) + 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: - colour = (140, 100, 255, a) - - ddt.rect((x, y, s, s), colour) - x += g + s - - gui.update += 1 + 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 - # 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]) + 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))) - # 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]) + 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) - 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 middle_click: + self.click_ref = artist + self.click_highlight_timer.set() + create_artist_pl(artist) - 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 right_click: + self.click_ref = artist + self.click_highlight_timer.set() - if input_text or key_backspace_press: - self.input_timer.set() + artist_list_menu.activate(in_reference=artist) - 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") + def render(self, x, y, w, h): - if self.input_timer.get() < 10: - gui.frame_callback_list.append(TestTimer(0.1)) + if prefs.artist_list_style == 1: + self.tab_h = round(60 * gui.scale) + else: + self.tab_h = round(22 * gui.scale) - yy = 110 * gui.scale + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - if key_down_press: + # use parent playlst is set + if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: - 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) + # test if parent still exists + new = id_to_pl(pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id) + if new is None or not pctl.multi_playlist[pctl.active_playlist_viewing].title.startswith("Artist:"): + pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id = "" + else: + viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id - if key_up_press: + if viewing_pl_id in self.saves: + self.current_artists = self.saves[viewing_pl_id][0] + self.current_album_counts = self.saves[viewing_pl_id][1] + self.current_artist_track_counts = self.saves[viewing_pl_id][4] + self.scroll_position = self.saves[viewing_pl_id][2] - if self.force_select > -1: - self.force_select -= 1 - self.force_select = max(self.force_select, 0) + 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 - if self.force_select < self.on + 4: - self.on = self.force_select - 4 - self.on = max(self.on, 0) + else: - self.old_mouse = copy.deepcopy(mouse_position) + # 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") - 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 + area = (x, y, w, h) + area2 = (x + 1, y, w - 3, h) - enter = False + ddt.rect(area, colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background - if self.delay_enter and not self.sip and self.search_text.text == self.searched_text: - enter = True - self.delay_enter = False + 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) - 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 + range = (h // self.tab_h) - 1 - inp.key_return_press = False + whole_rage = math.floor(h // self.tab_h) - bar_colour = [140, 80, 240, 255] - track_in_bar_colour = [244, 209, 66, 255] + if range > 4 and self.scroll_position > len(self.current_artists) - range: + self.scroll_position = len(self.current_artists) - range - self.on = max(self.on, 0) - self.on = min(len(self.results) - 1, self.on) + if len(self.current_artists) <= whole_rage: + self.scroll_position = 0 - full_count = 0 + fields.add(area2) + scroll_x = x + w - 18 * gui.scale + if colours.lm: + scroll_x = x + w - 22 * gui.scale + if (coll(area2) or artist_list_scroll.held) and not pref_box.enabled: + scroll_width = 15 * gui.scale + inset = 0 + if gui.compact_artist_list: + pass + # scroll_width = round(6 * gui.scale) + # scroll_x += round(9 * gui.scale) + else: + self.scroll_position = artist_list_scroll.draw( + scroll_x, y + 1, scroll_width, h, self.scroll_position, + len(self.current_artists) - range, r_click=right_click, + jump_distance=35, extend_field=6 * gui.scale) - sec = False + if not self.current_artists: + text = _("No artists in playlist") - p = -1 + 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...") - if self.on > 4: - p += self.on - 4 - p = self.on - 1 - clear = False + ddt.text( + (x + w // 2, y + (h // 7), 2), text, alpha_mod(colours.side_bar_line2, 100), 212, + max_w=w - 17 * gui.scale) - for i, item in enumerate(self.results): + yy = y + 12 * gui.scale - p += 1 + i = int(self.scroll_position) - if p > len(self.results) - 1: - break + if viewing_pl_id in self.saves: + self.saves[viewing_pl_id][2] = self.scroll_position - item: list[int] = self.results[p] + prefetch_mode = False + prefetch_distance = 22 - fade = 1 - selected = self.on - if self.force_select > -1: - selected = self.force_select + self.shown_letters.clear() - #logging.info(selected) + self.hover_any = False - if selected != p: - fade = 0.8 + for i, artist in enumerate(self.current_artists[i:], start=i): - start = yy + if not prefetch_mode: + self.draw_card(artist, x, round(yy), w) - n = item[0] + yy += self.tab_h - 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) + if yy - y > h - 24 * gui.scale: + prefetch_mode = True + continue - pad = round(4 * gui.scale) - height = round(25 * gui.scale) - if n in (1, 11): - height = round(50 * gui.scale) - album_art_size = height + if prefetch_mode: + if prefs.artist_list_style == 2: + break + prefetch_distance -= 1 + if prefetch_distance < 1: + break + if artist not in self.thumb_cache: + self.load_img(artist) + break + if not self.hover_any: + gui.preview_artist = "" + self.hover_timer.force_set(10) + artist_preview_render.show = False + self.hover_on = False - # 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) +class TreeView: - # 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) + def __init__(self): - # 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") + self.trees = {} # Per playlist tree + self.rows = [] # For display (parsed from tree) + self.rows_id = "" - # 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.opens = {} # Folders clicks to show per playlist - if n in (1,): # Two line album - track = pctl.master_library[item[2]] - artist = track.album_artist - if not artist: - artist = track.artist + self.scroll_positions = {} - xx = ddt.text((text_lx, yy + pad + round(5 * gui.scale)), item[1], [255, 255, 255, int(255 * fade)], s_b_font) + # Recursive gen_rows vars + self.count = 0 + self.depth = 0 - 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.background_processing = False + self.d_click_timer = Timer(100) + self.d_click_id = "" + self.menu_selected = "" + self.folder_colour_cache = {} + self.dragging_name = "" - yy += height + pad + pad + self.force_opens = [] + self.click_drag_source = None - 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 + self.tooltip_on = "" + self.tooltip_timer = Timer(10) - if gui.level_2_click: - if key_ctrl_down: - extend = True - else: - go = True - clear = True + self.lock_pl = None + # self.bold_colours = ColourGenCache(0.6, 0.7) - if level_2_right_click: - show = True - clear = True + def clear_all(self): + self.rows_id = "" + self.trees.clear() - if enter and key_shift_down and fade == 1: - show = True - clear = True + def collapse_all(self): + pl_id = pl_to_id(pctl.active_playlist_viewing) - elif enter and fade == 1: - if key_shift_down or key_shiftr_down: - show = True - clear = True - else: - go = True - clear = True + if self.lock_pl: + pl_id = self.lock_pl - 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() + opens = self.opens.get(pl_id) + if opens is None: + opens = [] + self.opens[pl_id] = opens - 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) + opens.clear() + self.rows_id = "" - 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() + def clear_target_pl(self, pl_number, pl_id=None): - 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 pl_id is None: + pl_id = pl_to_id(pl_number) - # ---- + if gui.lsp and prefs.left_panel_mode == "folder view": - # --- - if i > 40: - break - if yy > window_size[1] - (100 * gui.scale): - break - - continue + 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] - if clear: - self.active = False - self.search_text.text = "" - self.results.clear() - self.searched_text = "" + def show_track(self, track: TrackClass) -> None: -class MessageBox: + if track is None: + return - def __init__(self): - pass + # 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 - def get_rect(self): + tree = self.trees.get(pl_id) + if not tree: + return - 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) + scroll_position = self.scroll_positions.get(pl_id) + if scroll_position is None: + scroll_position = 0 - w = max(w, 210 * gui.scale) + # Clear all opened folders + opens.clear() - h = round(60 * gui.scale) - if gui.message_subtext2: - h += round(15 * gui.scale) + # Set every folder in path as opened + path = "" + crumbs = track.parent_folder_path.split("/")[1:] + for c in crumbs: + path += "/" + c + opens.append(path) - x = int(window_size[0] / 2) - int(w / 2) - y = int(window_size[1] / 2) - int(h / 2) + # Regenerate row display + self.gen_rows(tree, opens) - return x, y, w, h + # 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 render(self): + scroll_position = i - 5 + scroll_position = max(scroll_position, 0) + break - 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): + 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) - if not key_focused and message_box_min_timer.get() > 0.4: - gui.message_box = False - gui.update += 1 - inp.key_return_press = False + self.scroll_positions[pl_id] = scroll_position - x, y, w, h = self.get_rect() + gui.update_layout() + gui.update += 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) + def get_pl_id(self): + if self.lock_pl: + return self.lock_pl + return pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - ddt.text_background_colour = colours.message_box_bg + def render(self, x, y, w, h): - 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 + global quick_drag - 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) + pl_id = self.get_pl_id() - if gui.message_subtext2: - ddt.text((x + 63 * gui.scale, y + (9 + 42) * gui.scale), gui.message_subtext2, colours.message_box_text, - 12) + tree = self.trees.get(pl_id) - else: - ddt.text((x + 62 * gui.scale, y + 20 * gui.scale), gui.message_text, colours.message_box_text, 15) + # 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() -class NagBox: - def __init__(self): - self.wiggle_timer = Timer(10) + self.playlist_id_on = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - 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) + opens = self.opens.get(pl_id) + if opens is None: + opens = [] + self.opens[pl_id] = opens - # 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 + scroll_position = self.scroll_positions.get(pl_id) + if scroll_position is None: + scroll_position = 0 - 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) + area = (x, y, w, h) + fields.add(area) + ddt.rect(area, colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background - # 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 + 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 - ddt.text_background_colour = colours.message_box_bg + # 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 - 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) + 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) - 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) + else: + return - heart_notify_icon.render(x + round(425 * gui.scale), y + round(80 * gui.scale), [255, 90, 90, 255]) + 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 - y += round(30 * gui.scale) - ddt.text((x, y), _("New supporter bonuses!"), colours.message_box_text, 212) + yy = y + round(11 * gui.scale) + xx = x + round(22 * gui.scale) - y += round(20 * gui.scale) + spacing = round(21 * gui.scale) + max_scroll = len(self.rows) - (h // round(22 * 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) + mouse_in = coll(area) - y += round(20 * gui.scale) - ddt.text((x, y), _("Your support means a lot! Love you!"), colours.message_box_text, 12) + # 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) - y += round(30 * gui.scale) + focused = is_level_zero() - 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 + # 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) -def worker3(): - while True: - # time.sleep(0.04) + self.scroll_positions[pl_id] = scroll_position - # if tauon.thread_manager.exit_worker3: - # tauon.thread_manager.exit_worker3 = False - # return - # time.sleep(1) + # Draw folder rows + playing_track = pctl.playing_object() + max_w = w - round(45 * gui.scale) - tauon.gall_ren.worker_render() + light_mode = test_lumi(colours.side_panel_background) < 0.5 + semilight_mode = test_lumi(colours.side_panel_background) < 0.8 -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() + for i, item in enumerate(self.rows): - 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 + if i < scroll_position: + continue -def worker2(): - while True: - worker2_lock.acquire() + if yy > y + h - spacing: + break - if search_over.search_text.text and not (len(search_over.search_text.text) == 1 and ord(search_over.search_text.text[0]) < 128): + target = item[1] + "/" + item[0] - 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 + 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) - elif True: - # perf_timer.set() + # text_colour = [255, 255, 255, 100] + text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.35, -0.15) - temp_results = [] + box_colour = [200, 100, 50, 255] - search_over.searched_text = search_over.search_text.text + if semilight_mode: + text_colour = [255, 255, 255, 180] - artists = {} - albums = {} - genres = {} - metas = {} - composers = {} - years = {} + if light_mode: + text_colour = [0, 0, 0, 200] - tracks = set() + full_folder_path = item[1] + "/" + item[0] - br = 0 + # Hold highlight while menu open + if (folder_tree_menu.active or folder_tree_stem_menu.active) and full_folder_path == self.menu_selected: + text_colour = [255, 255, 255, 170] + if semilight_mode: + text_colour = (255, 255, 255, 255) + if light_mode: + text_colour = [0, 0, 0, 255] - if search_over.searched_text in ("the", "and"): - continue + # 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] - search_over.sip = True - gui.update += 1 + # 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] - o_text = search_over.search_text.text.lower().replace("-", "") + 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)) - dia_mode = False - if all([ord(c) < 128 for c in o_text]): - dia_mode = True + if mouse_in and not tree_view_scroll.held: - artist_mode = False - if o_text.startswith("artist "): - o_text = o_text[7:] - artist_mode = True + if middle_click: + stem_to_new_playlist(full_folder_path) - album_mode = False - if o_text.startswith("album "): - o_text = o_text[6:] - album_mode = True + elif right_click: - composer_mode = False - if o_text.startswith("composer "): - o_text = o_text[9:] - composer_mode = True + if item[3]: - year_mode = False - if o_text.startswith("year "): - o_text = o_text[5:] - year_mode = True + 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 - 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 + elif inp.mouse_click: + # quick_drag = True - s_text = o_text + if not self.click_drag_source: + self.click_drag_source = item + set_drag_source() - searched = set() + elif mouse_up and self.click_drag_source == item: + # Click tree level folder to open/close branch - for playlist in pctl.multi_playlist: + 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 "<" in playlist.title: - # #logging.info("Skipping search on derivative playlist: " + playlist.title) - # continue + if item[3]: - for track in playlist.playlist_ids: + # Locate the first track of folder in playlist + track_id = None + for p, id in enumerate(default_playlist): + if msys: + if pctl.get_track(id).fullpath.startswith(target.lstrip("/")): + track_id = id + break + elif pctl.get_track(id).fullpath.startswith(target): + track_id = id + break + else: # Fallback to folder name if full-path not found (hack for networked items) + for p, id in enumerate(default_playlist): + if pctl.get_track(id).parent_folder_name == item[0]: + track_id = id + break - if track in searched: - continue - searched.add(track) + if track_id is not None: + # Single click base folder to locate in playlist + if self.d_click_timer.get() > 0.5 or self.d_click_id != target: + pctl.show_current(select=True, index=track_id, no_switch=True, highlight=True, folder_list=False) + self.d_click_timer.set() + self.d_click_id = target + # Double click base folder to play + else: + pctl.jump(track_id) - 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 + # Regenerate display rows after clicking + self.gen_rows(tree, opens) - 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 + # 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] - t = pctl.master_library[track] + # 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 - 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 mouse_in: + tw = ddt.get_text_w(item[0], font) - 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 self.tooltip_on != item: + self.tooltip_on = item + self.tooltip_timer.set() + gui.frame_callback_list.append(TestTimer(0.6)) - 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 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) - if dia_mode: - title = unidecode(title) + # # 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) - artist = unidecode(artist) - album_artist = unidecode(album_artist) - composer = unidecode(composer) - album = unidecode(album) - filename = unidecode(filename) - sartist = unidecode(sartist) + 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) - if cache_string is None: - search_dia_string_cache[ - track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem + # 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) - stem = os.path.dirname(t.parent_folder_path) + 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 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()): + yy += spacing - if stem in metas: - metas[stem] += 2 - else: - temp_results.append([5, stem, track, playlist.uuid_int, 0]) - metas[stem] = 2 + 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 s_text in genre: + self.dragging_name = self.click_drag_source[0] + logging.info(self.dragging_name) - if "/" in genre or "," in genre or ";" in genre: + if "/" in self.dragging_name: + self.dragging_name = os.path.basename(self.dragging_name) - for split in genre.replace(";", "/").replace(",", "/").split("/"): - if s_text in split: + 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 - 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 self.dragging_name and not quick_drag: + self.dragging_name = "" + if not mouse_down: + self.click_drag_source = None - if s_text in composer: + def gen_row(self, tree_point, path, opens): - 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 + for item in tree_point: + p = path + "/" + item[1] + self.count += 1 + enter_level = False + if len(tree_point) > 1 or path in self.force_opens: # Ignore levels that are only a single folder wide - if s_text in date: + 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 - year = get_year_from_string(date) - if year: + # 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])]) - if year in years: - years[year] += 1 - else: - temp_results.append([7, year, track, playlist.uuid_int, 0]) - years[year] = 1000 + # 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 search_magic(s_text, title + artist + filename + album + sartist + album_artist): + # 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]) - if "artists" in t.misc and t.misc["artists"]: - for a in t.misc["artists"]: - if search_magic(s_text, a.lower()): + self.depth += 1 + enter_level = True - value = 1 - if a.lower().startswith(s_text): - value = 5 + self.gen_row(item[0], p, opens) - # Add artist - if a in artists: - artists[a] += value - else: - temp_results.append([0, a, track, playlist.uuid_int, 0]) - artists[a] = value + if enter_level: + self.depth -= 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 + def gen_rows(self, tree, opens): + self.count = 0 + self.depth = 0 + self.rows.clear() + self.force_opens.clear() - elif search_magic(s_text, artist + sartist): + self.gen_row(tree, "", opens) - value = 1 - if artist.startswith(s_text): - value = 10 + gui.update_layout() + gui.update += 1 - # 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 + def gen_tree(self, pl_id): + pl_no = id_to_pl(pl_id) + if pl_no is None: + return - 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 + playlist = pctl.multi_playlist[pl_no].playlist_ids + # Generate list of all unique folder paths + paths = [] + z = 5000 + for p in playlist: - elif search_magic(s_text, album_artist): + 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) - # Add album artist - value = 1 - if t.album_artist.startswith(s_text): - value = 5 + # 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 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 + self.trees[pl_id] = tree + self.rows_id = "" + self.background_processing = False + gui.update += 1 + tauon.wake() - 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 +class QueueBox: - if s_text in album: + def recalc(self): + self.tab_h = 34 * gui.scale + def __init__(self): - value = 1 - if s_text == album: - value = 3 + 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() - 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 + 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)) - if search_magic(s_text, artist + sartist) or search_magic(s_text, album): + 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")) - 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 + queue_menu.add(MenuItem(_("↳ Except for This"), self.clear_queue_crop, show_test=self.except_for_this_show_test)) - elif search_magic_any(s_text, artist + sartist) and search_magic_any(s_text, album): + 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) - 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 + def except_for_this_show_test(self, _): + return self.queue_remove_show(_) and test_shift(_) - if s_text in title: + def make_as_playlist(self): - if t not in tracks: + if pctl.force_queue: + playlist = [] + for item in pctl.force_queue: - value = 50 - if s_text == title: - value = 200 + if item.type == 0: + playlist.append(item.track_id) + else: - temp_results.append([2, t.title, track, playlist.uuid_int, value]) + pl = id_to_pl(item.playlist_id) + if pl is None: + logging.info("Lost the target playlist") + continue - tracks.add(t) + pp = pctl.multi_playlist[pl].playlist_ids - elif t not in tracks: - temp_results.append([2, t.title, track, playlist.uuid_int, 1]) + i = item.position # = pctl.playlist_playing_position + 1 - tracks.add(t) + parts = [] + album_parent_path = pctl.get_track(item.track_id).parent_folder_path - br += 1 - if br > 800: - time.sleep(0.005) # Throttle thread - br = 0 - if search_over.searched_text != search_over.search_text.text: - break + while i < len(pp): + if pctl.get_track(pp[i]).parent_folder_path != album_parent_path: + break - search_over.sip = False - search_over.on = 0 - gui.update += 1 + parts.append((pp[i], i)) + i += 1 - # Remove results not matching any filter keyword + for part in parts: + playlist.append(part[0]) - if artist_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 0: - del temp_results[i] + pctl.multi_playlist.append( + pl_gen( + title=_("Queued Tracks"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - elif album_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 1: - del temp_results[i] + def drop_tracks_insert(self, insert_position): - elif composer_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 6: - del temp_results[i] + global quick_drag - elif year_mode: - for i in reversed(range(len(temp_results))): - if temp_results[i][0] != 7: - del temp_results[i] + if not shift_selection: + return - # 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 + # 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) - 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) + playlist_index = pctl.active_playlist_viewing + playlist_id = pl_to_id(pctl.active_playlist_viewing) - 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 + main_track_position = shift_selection[0] + main_track_id = default_playlist[main_track_position] + quick_drag = False - search_over.on = 0 - search_over.force_select = 0 - #logging.info(perf_timer.get()) + if len(shift_selection) > 1: -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 + # 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 - loaded_paths_cache = {} - loaded_cue_cache = {} - added = [] + if len(shift_selection) == 1: + pctl.force_queue.insert(insert_position, queue_item_gen(main_track_id, main_track_position, playlist_id)) + else: + # Add each track + for position in reversed(shift_selection): + pctl.force_queue.insert( + insert_position, queue_item_gen(default_playlist[position], position, playlist_id)) - def get_quoted_from_line(line): + def clear_queue_crop(self): - # Extract quoted or unquoted string from a line - # e.g., 'FILE "01 - Track01.wav" WAVE' or 'TITLE Track01' or "PERFORMER 'Artist Name'" + save = False + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + save = item + break - parts = line.split(None, 1) - if len(parts) < 2: - return "" + clear_queue() + if save: + pctl.force_queue.append(save) - content = parts[1].strip() + def play_now(self): - 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] + 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 - def add_from_cue(path): + del pctl.force_queue[queue_index] + # [trackid, position, pl_id, type, album_stage, uid_gen(), auto_stop] - global added + if pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(None) - if not msys: # Windows terminal doesn't like unicode - logging.info("Reading CUE file: " + path) + target_track_id = queue_item.track_id - try: + pl = id_to_pl(queue_item.playlist_id) + if pl is not None: + pctl.active_playlist_playing = pl - 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 + if target_track_id not in pctl.playing_playlist(): + pctl.advance() + return - f.close() + pctl.jump(target_track_id, queue_item.position) - # 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 + if queue_item.type == 1: # is album type + queue_item.album_stage = 1 # set as partway playing + pctl.force_queue.insert(0, queue_item) - files = 0 - files_with_subtracks = 0 - subtrack_count = 0 - for line in content: - if line.startswith("FILE "): - files += 1 - if subtrack_count > 2: # A hack way to avoid non-compliant EAC CUE sheet - files_with_subtracks += 1 - subtrack_count = 0 - elif line.strip().startswith("TRACK "): - subtrack_count += 1 - if subtrack_count > 2: - files_with_subtracks += 1 + def toggle_auto_stop(self) -> None: - if files == 1: - pass - elif files_with_subtracks > 1: - pass - else: - return 1 + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + item.auto_stop ^= True + break - cue_performer = "" - cue_date = "" - cue_album = "" - cue_genre = "" - cue_main_performer = "" - cue_songwriter = "" - cue_disc = 0 - cue_disc_total = 0 + def toggle_auto_stop_deco(self): - cd = [] - cds = [] + enabled = False + for item in pctl.force_queue: + if item.uuid_int == self.right_click_id: + if item.auto_stop: + enabled = True + break - file_name = "" - file_path = "" + if enabled: + return [colours.menu_text, colours.menu_background, _("Cancel Auto-Stop")] + return [colours.menu_text, colours.menu_background, _("Auto-Stop")] - in_header = True + def queue_remove_show(self, id: int) -> bool: - i = -1 - while True: - i += 1 + if self.right_click_id is not None: + return True + return False - if i > len(content) - 1: - break + def right_remove_item(self) -> None: - line = content[i].strip() + if self.right_click_id is None: + show_message(_("Eh?")) - if in_header: - if line.startswith("REM "): - line = line[4:] + 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")) - 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) + def toggle_pause(self) -> None: + pctl.pause_queue ^= True - if line.startswith("FILE "): - in_header = False - else: - continue + 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: - if line.startswith("FILE "): + # text_colour = [230, 230, 230, 255] + bg = colours.queue_background - if cd: - cds.append(cd) - cd = [] + # if fq[i].type == 0: - file_name = get_quoted_from_line(line) - file_path = os.path.join(os.path.dirname(path), file_name) + rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) - 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 draw_back: + ddt.rect(rect, colours.queue_card_background) + bg = colours.queue_card_background - if line.startswith("TRACK "): - line = line[6:] - if line.endswith("AUDIO"): - line = line[:-5] + 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] - 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 + tauon.gall_ren.render(track, (rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale), round(28 * gui.scale)) - 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 + ddt.rect((rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale, 26, 26), [0, 0, 0, 6]) - 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) + line = track.album + if fqo.type == 0: + line = track.title - while True: - i += 1 - if i > len(content) - 1 or content[i].startswith("FILE ") or content[i].strip().startswith( - "TRACK"): - break + if not line: + line = clean_string(track.filename) - line = content[i] - line = line.strip() + line2y = yy + 14 * gui.scale - 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 + artist_line = track.artist + if fqo.type == 1 and track.album_artist: + artist_line = track.album_artist - i -= 1 - cd.append(nt) + if fqo.type == 0 and not artist_line: + line2y -= 7 * gui.scale - if cd: - cds.append(cd) + 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) - for cdn, cd in enumerate(cds): + ddt.text( + (rect[0] + (40 * gui.scale), line2y), line, text_colour2, 211, + max_w=rect[2] - 60 * gui.scale, bg=bg) - last_end = None - end_track = TrackClass() - end_track.fullpath = cd[-1].fullpath - tag_scan(end_track) + 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]) - # 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] + 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]) - # Update with proper length - for track in reversed(cd): + def draw(self, x: int, y: int, w: int, h: int): - if last_end == None: - last_end = end_track.length + yy = y - 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 + yy += round(4 * gui.scale) - # 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 + sep_colour = alpha_blend([255, 255, 255, 11], colours.queue_background) - if cue_disc: - track.disc_number = cue_disc - elif len(cds) == 0: - track.disc_number = "" - else: - track.disc_number = str(cdn) + 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) - if cue_disc_total: - track.disc_total = cue_disc_total - elif len(cds) == 0: - track.disc_total = "" - else: - track.disc_total = str(len(cds)) + yy += round(3 * gui.scale) + box_rect = (x, yy - 6 * gui.scale, w, h) + ddt.rect(box_rect, colours.queue_background) + ddt.text_background_colour = colours.queue_background - # 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_paths_cache[track.fullpath] = track.index - added.append(track.index) + 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) - except Exception: - logging.exception("Internal error processing CUE file") + # if y < gui.panelY * 2: + # ddt.rect((x, y - 3 * gui.scale, w, 30 * gui.scale), colours.queue_background, True) - def add_file(path, force_scan: bool = False) -> int | None: - # bm.get("add file start") - global DA_Formats - global to_got + 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) - if not os.path.isfile(path): - logging.error("File to import missing") - return 0 + qb_right_click = 0 - if os.path.splitext(path)[1][1:] in {"CUE", "cue"}: - add_from_cue(path) - return 0 + if coll(box_rect): + # Update scroll position + self.scroll_position += mouse_wheel * -1 + self.scroll_position = max(self.scroll_position, 0) - if path.lower().endswith(".xspf"): - logging.info("Found XSPF file at: " + path) - load_xspf(path) - return 0 + if right_click: + qb_right_click = 1 - if path.lower().endswith(".m3u") or path.lower().endswith(".m3u8"): - load_m3u(path) - return 0 + # 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 path.endswith(".pls"): - load_pls(path) - return 0 + line = _("Up Next:") + if pctl.force_queue: + # line = "Queue" + ddt.text((x + (10 * gui.scale), yy + 2 * gui.scale), line, text_colour, 211) - 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") + yy += 7 * gui.scale - 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 len(pctl.force_queue) < 3: + self.scroll_position = 0 - 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") + # Draw square dots to indicate view has been scrolled down + if self.scroll_position > 0: + ds = 3 * gui.scale + gp = 4 * gui.scale - return 1 + 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]) - 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") + # 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]) - return 1 + yy += 6 * gui.scale - 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") + yy += 10 * gui.scale - 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") + i = 0 - to_got = b - gets(target_dir) - quick_import_done.append(target_dir) - # gets(target_dir) + # 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 - return 1 + # End drag if mouse not in correct state for it + if not mouse_down and not mouse_up: + self.dragging = None - to_got += 1 - gui.update = 1 + if not queue_menu.active: + self.right_click_id = None - path = path.replace("\\", "/") + fq = self.fq - if path in loaded_paths_cache: - de = loaded_paths_cache[path] + list_top = yy - if pctl.master_library[de].fullpath in cue_list: - logging.info("File has an associated .cue file... Skipping") - return None + i = self.scroll_position - if pctl.master_library[de].file_ext.lower() in GME_Formats: - # Skip cache for subtrack formats - pass - else: - added.append(de) - return None + # Limit scroll distance + if i > len(fq): + self.scroll_position = len(fq) + i = self.scroll_position - time.sleep(0.002) + showed_indicator = False + list_extends = False + x1 = x + 13 * gui.scale # highlight position + w1 = w - 28 * gui.scale - 10 * gui.scale - # audio = auto.File(path) + while i < len(fq) + 1: - nt = TrackClass() + # Stop drawing if past window + if yy > window_size[1] - gui.panelBY - gui.panelY - (50 * gui.scale): + list_extends = True + break - nt.index = pctl.master_count - set_path(nt, path) + # 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 commit_track(nt): - pctl.master_library[pctl.master_count] = nt - added.append(pctl.master_count) + if self.dragging is not None and coll(h_rect) and mouse_up: - if prefs.auto_sort or force_scan: - tag_scan(nt) - else: - after_scan.append(nt) - tauon.thread_manager.ready("worker") + ob = None + for u in reversed(range(len(pctl.force_queue))): - pctl.master_count += 1 + if pctl.force_queue[u].uuid_int == self.dragging: + ob = pctl.force_queue[u] + pctl.force_queue[u] = None + break - # nt = tag_scan(nt) - if nt.cue_sheet != "": - tag_scan(nt) - cue_scan(nt.cue_sheet, nt) - del nt + else: + self.dragging = None - elif nt.file_ext.lower() in GME_Formats and gme: + if self.dragging: + pctl.force_queue.insert(i, ob) + self.dragging = None - 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) + 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 - gme.gme_delete(emu) + # 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 - else: + inp.mouse_click = False + self.draw(x, y, w, h) + return - commit_track(nt) + if i > len(fq) - 1: + break - # bm.get("fill entry") - if gui.auto_play_import: - pctl.jump(pctl.master_count - 1) - gui.auto_play_import = False + track = pctl.get_track(fq[i].track_id) - # Count the approx number of files to be imported - def pre_get(direc): + rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) - global to_get + if inp.mouse_click and coll(rect): - to_get = 0 - for root, dirs, files in os.walk(direc): - to_get += len(files) - if gui.im_cancel: - return - gui.update = 3 + self.dragging = fq[i].uuid_int + self.drag_start_y = mouse_position[1] + self.drag_start_top = yy - def gets(direc, force_scan=False): + if d_click_timer.get() < 1: - global DA_Formats + if self.d_click_ref == fq[i].uuid_int: - if os.path.basename(direc) == "__MACOSX": - return + pl = id_to_pl(fq[i].uuid_int) + if pl is not None: + switch_playlist(pl) - 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") + 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 - return - except Exception: - logging.exception("Unknown error accessing one or more files") - return + d_click_timer.set() - 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 + if self.dragging and coll(h_rect): + yy += self.tab_h + yy += 4 * gui.scale - 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 qb_right_click and coll(rect): + self.right_click_id = fq[i].uuid_int + qb_right_click = 2 - if os.path.splitext(items_in_dir[q])[1][1:].lower() in DA_Formats: + if middle_click and coll(rect): + pctl.force_queue.remove(fq[i]) + gui.pl_update += 1 - if len(items_in_dir[q]) > 2 and items_in_dir[q][0:2] == "._": - continue + if fq[i].uuid_int == self.dragging: + # ddt.rect_r(rect, [22, 22, 22, 255], True) + pass + else: - add_file(os.path.join(direc, items_in_dir[q]).replace("\\", "/"), force_scan) + db = False + if fq[i].uuid_int == self.right_click_id: + db = True - 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.draw_card(x, y, w, h, yy, track, fq[i], db) - if gui.im_cancel: - return + # Drag tracks from main playlist and insert ------------ + if quick_drag: - 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 + if x < mouse_position[0] < x + w: + y1 = yy - 4 * gui.scale + y2 = y1 + h1 = self.tab_h // 2 + if i == 0: + # Extend up if first element + y1 -= 5 * gui.scale + h1 += 10 * gui.scale - #logging.info(pctl.master_library) + insert_position = None - global transcode_list - global transcode_state - global album_art_gen - global cm_clean_db - global to_got - global to_get - global move_in_progress + 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 - active_timer = Timer() - while True: + if mouse_up: + insert_position = i - if not after_scan: - time.sleep(0.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 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 mouse_up: + insert_position = i + 1 - if after_scan: - i = 0 - while after_scan: - i += 1 + if insert_position is not None: + self.drop_tracks_insert(insert_position) - if i > 123: - break + # ----------------------------------------- + yy += self.tab_h + yy += 4 * gui.scale - tag_scan(after_scan[0]) + i += 1 - gui.update = 2 - gui.pl_update = 1 - # time.sleep(0.001) - if pctl.running: - del after_scan[0] - else: - break + # 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 - album_artist_dict.clear() + yy += 15 * gui.scale + if fq: + ddt.rect((x, yy, w, 3 * gui.scale), sep_colour) + yy += 11 * gui.scale - artist_list_box.worker() + # Calculate total queue duration + duration = 0 + tracks = 0 - # 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 + 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 - # Update smart playlists - if gui.regen_single > -1: - target = gui.regen_single - gui.regen_single = -1 - regenerate_playlist(target, silent=True) + album_parent_path = pctl.get_track(item.track_id).parent_folder_path - if pctl.after_import_flag and not after_scan and not search_over.active and not loading_in_progress: - pctl.after_import_flag = False + playing_track = pctl.playing_object() - 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") + 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 - tree_view_box.clear_all() + 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 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 + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break - # Folder moving - if len(move_jobs) > 0: - gui.update += 1 - move_in_progress = True - job = move_jobs[0] - del move_jobs[0] + duration += pctl.get_track(playlist[i]).length + tracks += 1 + i += 1 - 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 + # 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) - 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 + if self.dragging: - show_message(_("Folder move complete."), _("Folder name: {name}").format(name=job[3]), mode="done") + fqo = None + for item in fq: + if item.uuid_int == self.dragging: + fqo = item + break 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 + self.dragging = False - # Clean database - if cm_clean_db is True: - items_removed = 0 + 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) - # old_db = copy.deepcopy(pctl.master_library) - to_got = 0 - to_get = len(pctl.master_library) - search_over.results.clear() + # 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)) - keys = set(pctl.master_library.keys()) - for index in keys: - time.sleep(0.0001) - track = pctl.master_library[index] - to_got += 1 + # 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) - if to_got % 100 == 0: - gui.update = 1 +class MetaBox: - if not prefs.remove_network_tracks and track.file_ext == "SPTY": + def l_panel(self, x, y, w, h, track, top_border=True): - for playlist in pctl.multi_playlist: - if index in playlist.playlist_ids: - break - else: - pctl.purge_track(index) - items_removed += 1 + if not track: + return - continue + 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] - 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): + rect = (x, y, w, h) - if track.is_network and track.file_ext == "SPTY": - continue + 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) - pctl.purge_track(index) - items_removed += 1 + ddt.text_background_colour = colours.gallery_background - 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() + insert = round(9 * gui.scale) + border = round(2 * gui.scale) - gui.update = 1 - gui.pl_update = 1 - pctl.notify_change() + compact_mode = False + if w < h * 1.9: + compact_mode = True - search_dia_string_cache.clear() - search_string_cache.clear() - search_over.results.clear() + art_rect = [x + insert - 2 * gui.scale, y + insert, h - insert * 2 + 1 * gui.scale, + h - insert * 2 + 1 * gui.scale] - pctl.notify_change() + if compact_mode: + art_rect[0] = x + round(w / 2 - art_rect[2] / 2) - round(1 * gui.scale) # - border - # FOLDER ENC - if transcode_list: + border_rect = ( + art_rect[0] - border, art_rect[1] - border, art_rect[2] + (border * 2), art_rect[3] + (border * 2)) - try: - transcode_state = "" - gui.update += 1 + 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) - folder_items = transcode_list[0] + 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])) - ref_track_object = pctl.master_library[folder_items[0]] - ref_album = ref_track_object.album + 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) - # Generate a folder name based on artist and album of first track in batch - folder_name = encode_folder_name(ref_track_object) + 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 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((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) - logging.info("Transcoding folder: " + folder_name) + gui.showed_title = True - # Remove any existing matching folder - if (prefs.encoder_output / folder_name).is_dir(): - shutil.rmtree(prefs.encoder_output / folder_name) + def lyrics(self, x, y, w, h, track: TrackClass): - # Create new empty folder to output tracks to - (prefs.encoder_output / folder_name).mkdir(parents=True) + ddt.rect((x, y, w, h), colours.side_panel_background) + ddt.text_background_colour = colours.side_panel_background - 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() + if not track: + return - cache_dir = tmp_cache_dir() - if not os.path.isdir(cache_dir): - os.makedirs(cache_dir) + # 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 prefs.transcode_codec in ("opus", "ogg", "flac", "mp3"): - global core_use - cores = os.cpu_count() + # 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() - total = len(folder_items) - gui.transcoding_batch_total = total - gui.transcoding_bach_done = 0 - dones = [] + gui.update += 1 - 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() + tw, th = ddt.get_text_wh(track.lyrics + "\n", 15, w - 50 * gui.scale, True) - 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 + oth = th - else: - logging.error("Codec error") + th -= h + th += 25 * gui.scale # Empty space buffer at end - 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")) + if lyrics_ren_mini.lyrics_position * -1 > th: + lyrics_ren_mini.lyrics_position = th * -1 + if oth > h: + lyric_side_bottom_pulse.pulse() - #logging.info(transcode_list[0]) + scroll_w = 15 * gui.scale + if gui.maximized: + scroll_w = 17 * gui.scale - del transcode_list[0] - transcode_state = "" - gui.update += 1 + 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 - 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] + margin = 10 * gui.scale + if colours.lm: + margin += 1 * gui.scale - 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() + 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) - 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 + ddt.rect((x, y + h - 1, w, 1), colours.side_panel_background) - if loaderCommandReady is True: - for order in load_orders: - if order.stage == 1: - if loaderCommand == LC_Folder: - to_get = 0 - to_got = 0 - loaded_paths_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_paths_cache, loaded_cue_cache = cache_paths() - add_file(order.target) + 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 gui.im_cancel: - gui.im_cancel = False - to_get = 0 - to_got = 0 - load_orders.clear() - added = [] - loaderCommand = LC_Done - loaderCommandReady = False - break + def draw(self, x, y, w, h, track=None): - loaderCommand = LC_Done - #logging.info("LOAD ORDER") - order.tracks = added + ddt.rect((x, y, w, h), colours.side_panel_background) - # 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 track: + return - added = [] - order.stage = 2 - loaderCommandReady = False - #logging.info("DONE LOADING") - break + # 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) -def get_album_info(position, pl: int | None = None): + 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 - playlist = default_playlist - if pl is not None: - playlist = pctl.multi_playlist[pl].playlist_ids + if h < 15: + return - global album_info_cache_key + # Check for lyrics if auto setting + test_auto_lyrics(track) - 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()) + # # 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) - if position in album_info_cache: - return album_info_cache[position] + # Draw standard metadata + if len(pctl.track_queue) > 0: - 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) + 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 - end = len(playlist) - start = 0 + ddt.text_background_colour = colours.side_panel_background - for i, p in enumerate(reversed(dex)): - if p <= position: - start = p - break - end = p + 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 - album = list(range(start, end)) + title = "" + album = "" + artist = "" + ext = "" + date = "" + genre = "" - playing = 0 - select = False + margin = x + 10 * gui.scale + if colours.lm: + margin += 2 * gui.scale - if pctl.selected_in_playlist in album: - select = True + text_width = w - 25 * gui.scale + tr = None - if len(pctl.track_queue) > 0 and p < len(playlist): - if pctl.track_queue[pctl.queue_step] in playlist[start:end]: - playing = 1 + # if pctl.playing_state < 3: - album_info_cache[position] = playing, album, select - return playing, album, select + 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: -def get_folder_list(index: int): - playlist = [] + 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]) - 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)) + 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]) -def gal_jump_select(up=False, num=1): + if tr is None: + tr = pctl.playing_object() + if tr is None: + return - old_selected = pctl.selected_in_playlist - old_num = num + title = tr.title + album = tr.album + artist = tr.artist + ext = tr.file_ext + if ext == "JELY": + ext = "Jellyfin" + if "container" in tr.misc: + ext = tr.misc.get("container", "") + " | Jellyfin" + if tr.lyrics: + ext += "," + date = tr.date + genre = tr.genre - if not default_playlist: - return + if not title and not artist: + title = pctl.tag_meta - on = pctl.selected_in_playlist - if on > len(default_playlist) - 1: - on = 0 - pctl.selected_in_playlist = 0 + if h > 58 * gui.scale: - if up is False: + block_y = y + 7 * gui.scale - 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 not prefs.show_side_art: + block_y += 3 * gui.scale - if on > len(default_playlist) - 1: - pctl.selected_in_playlist = old_selected - return + 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) - pctl.selected_in_playlist = on - num -= 1 - else: + gui.showed_title = True - if num > 1: - if pctl.selected_in_playlist > len(default_playlist) - 1: - pctl.selected_in_playlist = old_selected - return + if h > 140 * gui.scale: - alb = get_album_info(pctl.selected_in_playlist) - if alb[1][0] in album_dex[:num]: - pctl.selected_in_playlist = old_selected - return + 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) - while num > 0: - alb = get_album_info(pctl.selected_in_playlist) + if not genre == date == "": + line = date + if genre != "": + if line != "": + line += " | " + line += genre - if alb[1][0] > -1: - on = alb[1][0] - 1 + ddt.text( + (margin, block_y + 20 * gui.scale), line, colours.side_bar_line2, + fonts.side_panel_line2, max_w=text_width) - pctl.selected_in_playlist = max(get_album_info(on)[1][0], 0) - num -= 1 + 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) -class PowerTag: + if tr and tr.lyrics: + if draw_internel_link( + margin + sp + 6 * gui.scale, block_y + 40 * gui.scale, "Lyrics", colours.side_bar_line2, fonts.side_panel_line2): + prefs.show_lyrics_showcase = True + enter_showcase_view(track_id=tr.index) + +class PictureRender: def __init__(self): - self.name = "BLANK" + self.show = False self.path = "" - self.position = 0 - self.colour = None - self.peak_x = 0 - self.ani_timer = Timer() - self.ani_timer.force_set(10) + self.image_data = None + self.texture = None + self.sdl_rect = None + self.size = (0, 0) -def gen_power2(): - tags = {} # [tag name]: (first position, number of times we saw it) - tag_list = [] + def load(self, path, box_size=None): - last = "a" - noise = 0 + if not os.path.isfile(path): + logging.warning("NO PICTURE FILE TO LOAD") + return - def key(tag): - return tags[tag][1] + g = io.BytesIO() + g.seek(0) - for position in album_dex: + im = Image.open(path) + if box_size is not None: + im.thumbnail(box_size, Image.Resampling.LANCZOS) - index = default_playlist[position] - track = pctl.get_track(index) + 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] - crumbs = track.parent_folder_path.split("/") + def draw(self, x, y): - for i, b in enumerate(crumbs): + if self.show is False: + return - if i > 0 and (track.artist in b and track.artist): - tag = crumbs[i - 1] + if self.image_data is not None: + if self.texture is not None: + SDL_DestroyTexture(self.texture) - if tag != last: - noise += 1 - last = tag + # Convert raw image to sdl texture + #logging.info("Create Texture") + wop = rw_from_object(self.image_data) + s_image = IMG_Load_RW(wop, 0) + self.texture = SDL_CreateTextureFromSurface(renderer, s_image) + SDL_FreeSurface(s_image) + tex_w = pointer(c_int(0)) + tex_h = pointer(c_int(0)) + SDL_QueryTexture(self.texture, None, None, tex_w, tex_h) + self.sdl_rect = SDL_Rect(round(x), round(y)) + self.sdl_rect.w = int(tex_w.contents.value) + self.sdl_rect.h = int(tex_h.contents.value) + self.image_data = None - if tag in tags: - tags[tag][1] += 1 - else: - tags[tag] = [position, 1, "/".join(crumbs[:i])] - tag_list.append(tag) - break + 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 noise > len(album_dex) / 2: - #logging.info("Playlist is too noisy for power bar.") - return [] +class ArtistInfoBox: - tag_list_sort = sorted(tag_list, key=key, reverse=True) + def __init__(self): + self.artist_on = None + self.min_rq_timer = Timer() + self.min_rq_timer.force_set(10) - max_tags = round((window_size[1] - gui.panelY - gui.panelBY - 10) // 30 * gui.scale) + self.text = "" - tag_list_sort = tag_list_sort[:max_tags] + self.status = "" - for i in reversed(range(len(tag_list))): - if tag_list[i] not in tag_list_sort: - del tag_list[i] + self.scroll_y = 0 - h = [] + self.process_text_artist = "" + self.processed_text = "" + self.th = 0 + self.w = 0 + self.lock = False - for tag in tag_list: + self.mini_box = asset_loader(scaled_asset_directory, loaded_asset_dc, "mini-box.png", True) - if tags[tag][1] > 2: - t = PowerTag() - t.path = tags[tag][2] - t.name = tag.upper() - t.position = tags[tag][0] - h.append(t) + def manual_dl(self): - cc = random.random() - cj = 0.03 - if len(h) < 5: - cj = 0.11 + track = pctl.playing_object() + if track is None or not track.artist: + show_message(_("No artist name found"), mode="warning") + return - cj = 0.5 / max(len(h), 2) + # Check if the artist has changed + self.artist_on = track.artist - for item in h: - item.colour = hsl_to_rgb(cc, 0.8, 0.7) - cc += cj + if not self.lock and self.artist_on: + self.lock = True + # self.min_rq_timer.set() - return h + self.scroll_y = 0 + self.status = _("Looking up...") + self.process_text_artist = "" -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 + shoot_dl = threading.Thread(target=self.get_data, args=([self.artist_on, False, True])) + shoot_dl.daemon = True + shoot_dl.start() - if cm_clean_db: - # Doing reload while things are being removed may cause crash - return None + def draw(self, x, y, w, h): - dex = [] - current_folder = "" - current_album = "" - current_artist = "" - current_date = "" - current_title = "" + if gui.artist_panel_height > 300 and w < 500 * gui.scale: + bio_set_small() - 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 w < 300 * gui.scale: + gui.artist_info_panel = False + gui.update_layout() + return - playlist = pctl.multi_playlist[target_pl_no].playlist_ids + track = pctl.playing_object() + if track is None: + return - for i in range(len(playlist)): - tr = pctl.master_library[playlist[i]] + # Check if the artist has changed + artist = track.artist + wait = False - 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 + # Activate menu + if right_click and coll((x, y, w, h)): + artist_info_menu.activate(in_reference=artist) - if return_playlist > -1 or custom_list: - return dex + background = colours.artist_bio_background + text_colour = colours.artist_bio_text + ddt.rect((x + 10, y + 5, w - 15, h - 5), background) - album_dex = dex - album_info_cache.clear() - gui.update += 2 - gui.pl_update = 1 - update_layout = True + if artist != self.artist_on: - if not quiet: - goto_album(pctl.playlist_playing_position) + if artist == "": + return - # Generate POWER BAR - gui.power_bar = gen_power2() - gui.pt = 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 -def star_line_toggle(mode: int= 0) -> bool | None: - if mode == 1: - return gui.star_mode == "line" + 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 gui.star_mode == "line": - gui.star_mode = "none" - else: - gui.star_mode = "line" + if not wait and not self.lock: + self.lock = True + # self.min_rq_timer.set() - gui.show_ratings = False + self.scroll_y = 0 + self.status = _("Loading...") - gui.update += 1 - gui.pl_update = 1 - return None + shoot_dl = threading.Thread(target=self.get_data, args=([artist])) + shoot_dl.daemon = True + shoot_dl.start() -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 + if self.process_text_artist != self.artist_on: + self.process_text_artist = self.artist_on - else: - if mode == 1: - return gui.star_mode == "star" + text = self.text + lic = "" + link = "" - if gui.star_mode == "star": - gui.star_mode = "none" - else: - gui.star_mode = "star" + if "<a" in text: + text, ex = text.split('<a href="', 1) - # gui.show_ratings = False - gui.update += 1 - gui.pl_update = 1 - return None + link, ex = ex.split('">', 1) -def heart_toggle(mode: int = 0) -> bool | None: - if mode == 1: - return gui.show_hearts + lic = ex.split("</a>. ", 1)[1] - gui.show_hearts ^= True - # gui.show_ratings = False + text += "\n" - gui.update += 1 - gui.pl_update = 1 - return None + 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")) -def album_rating_toggle(mode: int = 0) -> bool | None: - if mode == 1: - return gui.show_album_ratings + self.processed_text = text + self.w = -1 # trigger text recalc - gui.show_album_ratings ^= True + if self.status == "Ready": - gui.update += 1 - gui.pl_update = 1 - return None + # 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 rating_toggle(mode: int = 0) -> bool | None: - if mode == 1: - return gui.show_ratings + text_max_w = w - (round(55 * gui.scale) + p_off) - gui.show_ratings ^= True + 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 - 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.")) + scroll_max = self.th - (h - 26) - gui.update += 1 - gui.pl_update = 1 - return None + 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) -def toggle_titlebar_line(mode: int = 0) -> bool | None: - global update_title - if mode == 1: - return update_title + right = x + w - 25 * gui.scale - line = window_title - SDL_SetWindowTitle(t_window, line) - update_title ^= True - if update_title: - update_title_do() - return None + 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 toggle_meta_persists_stop(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.meta_persists_stop - prefs.meta_persists_stop ^= True - return None + 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) -def toggle_side_panel_layout(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.side_panel_layout == 1 + yy = y + 12 + for item in self.urls: - if prefs.side_panel_layout == 1: - prefs.side_panel_layout = 0 - else: - prefs.side_panel_layout = 1 - return None + rect = (right - 2, yy - 2, 16, 16) -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 + 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]) -def scale1(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.ui_scale == 1: - return True - return False + 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) - prefs.ui_scale = 1 - pref_box.large_preset() + yy += 19 * gui.scale - if prefs.ui_scale != gui.scale: - show_message(_("Change will be applied on restart.")) - return None + else: + ddt.text((x + w // 2, y + h // 2 - 7 * gui.scale, 2), self.status, [255, 255, 255, 60], 313, bg=background) -def scale125(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.ui_scale == 1.25: - return True - return False - return None + def get_data(self, artist: str, get_img_path: bool = False, force_dl: bool = False) -> str | None: - prefs.ui_scale = 1.25 - pref_box.large_preset() + if not get_img_path: + logging.info("Load Bio Data") - if prefs.ui_scale != gui.scale: - show_message(_("Change will be applied on restart.")) - return None + if artist is None and not get_img_path: + self.artist_on = artist + self.lock = False + return "" -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 + f_artist = filename_safe(artist) -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 + 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 toggle_min_tray(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.min_to_tray - prefs.min_to_tray ^= True - return None + 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"), + ] -def scale2(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.ui_scale == 2: - return True - return False + if get_img_path: + for path in image_paths: + if os.path.isfile(path): + return path + return "" - prefs.ui_scale = 2 - pref_box.large_preset() + # Check for cache + box_size = ( + round(gui.artist_panel_height - 20 * gui.scale) * 2, round(gui.artist_panel_height - 20 * gui.scale)) + try: - if prefs.ui_scale != gui.scale: - show_message(_("Change will be applied on restart.")) - return None + if os.path.isfile(text_filepath): + logging.info("Load cached bio and image") -def toggle_borderless(mode: int = 0) -> bool | None: - global draw_border - global update_layout + artist_picture_render.show = False - if mode == 1: - return draw_border + 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 - update_layout = True - draw_border ^= True + 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 - if draw_border: - SDL_SetWindowBordered(t_window, False) - else: - SDL_SetWindowBordered(t_window, True) - return None + return "" -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 + 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 toggle_scroll(mode: int = 0) -> bool | None: - global scroll_enable - global 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") - if mode == 1: - if scroll_enable: - return False - return True + 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) - scroll_enable ^= True - gui.pl_update = 1 - update_layout = True - return None + 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) -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 + 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 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 + # 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 -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 + self.status = "Ready" + gui.update = 2 -def toggle_true_shuffle(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.true_shuffle - prefs.true_shuffle ^= True - return None + # 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 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 + except Exception: + logging.exception("Failed to load bio") + self.status = _("Load Failed") - prefs.enable_web ^= True + self.artist_on = artist + self.processed_text = "" + self.process_text_artist = "" + self.min_rq_timer.set() + self.lock = False + gui.update = 2 + return "" - if prefs.enable_web and not gui.web_running: - webThread = threading.Thread( - target=webserve, args=[pctl, prefs, gui, album_art_gen, str(install_directory), strings, tauon]) - webThread.daemon = True - webThread.start() - show_message(_("Web server starting"), _("External connections will be accepted."), mode="done") +class RadioThumbGen: + def __init__(self): + self.cache = {} + self.requests = [] + self.size = 100 - elif prefs.enable_web is False: - if tauon.radio_server is not None: - tauon.radio_server.shutdown() - gui.web_running = False + def loader(self): - time.sleep(0.25) - return None + 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"]) -def toggle_scrobble_mark(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.scrobble_mark - prefs.scrobble_mark ^= True - return None + 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 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 + 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 -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 + 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() -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 + im = im.resize((size, size), Image.Resampling.LANCZOS) + g = io.BytesIO() + g.seek(0) + im.save(g, "PNG") + g.seek(0) + wop = rw_from_object(g) + s_image = IMG_Load_RW(wop, 0) + self.cache[key] = [2, None, None, s_image] + gui.update += 1 -def 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 draw(self, station, x, y, w): + if not station.get("title"): + return 0 + key = (station["title"], w) -def toggle_dl_mon(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.monitor_downloads - prefs.monitor_downloads ^= True - return None + 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 -def toggle_music_ex(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.extract_to_music - prefs.extract_to_music ^= True - return None +class RadioView: + def __init__(self): + self.add_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "add-station.png", True) + self.search_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "station-search.png", True) + self.save_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "save-station.png", True) + self.menu_icon = asset_loader(scaled_asset_directory, loaded_asset_dc, "radio-menu.png", True) + self.drag = None + self.click_point = (0, 0) -def 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 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) -def toggle_top_tabs(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.tabs_on_top - prefs.tabs_on_top ^= True - return None + # 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) -def toggle_guitar_chords(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.guitar_chords - prefs.guitar_chords ^= True - return None + # 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 toggle_auto_lyrics(mode: int = 0) -> bool | None: -# if mode == 1: -# return prefs.auto_lyrics -# prefs.auto_lyrics ^= True + if coll(rect): + colour = b_colour + if inp.mouse_click: + add_station() + else: + colour = a_colour -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 + self.add_icon.render(rect[0] + round(4 * gui.scale), rect[1] + round(4 * gui.scale), colour) -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 + y += round(33 * gui.scale) + rect = (x, y, round(25 * gui.scale), round(25 * gui.scale)) + fields.add(rect) -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 + 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) -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 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"] -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 + 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) -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 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"]) -def toggle_transcode_inplace(mode: int = 0) -> bool | None: - if mode == 1: - if prefs.transcode_inplace: - return True - return False + self.save_icon.render(rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), colour) - if gui.sync_progress: - prefs.transcode_inplace = False - return None + x = round(30 * gui.scale) + y = gui.panelY + round(30 * gui.scale) + yy = y - 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 + 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] -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 + w = round(400 * gui.scale) + h = round(55 * gui.scale) + gap = round(7 * gui.scale) -def toggle_sbt(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.prefer_bottom_title - prefs.prefer_bottom_title ^= True - return None + mm = (window_size[1] - (gui.panelBY + yy + h + round(15 * gui.scale))) // (h + gap) + 1 -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 + 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 -def toggle_use_title(mode: int = 0) -> bool | None: - if mode == 1: - return prefs.use_title - prefs.use_title ^= True - return None + pctl.radio_playlists[pctl.radio_playlist_viewing]["scroll"] = scroll + insert = 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 + 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]) -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 + 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 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 + 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) -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 + 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) -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 + bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) -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 + 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 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 + self.menu_icon.render(x + (w - round(75 * gui.scale)), yy + round(26 * gui.scale), colour) -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 + # 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) -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 + yy += round(h + gap) -def reload_backend() -> None: - gui.backend_reloading = True - logging.info("Reload backend...") - wait = 0 - pre_state = pctl.stop(True) + 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) - 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") + 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) - pctl.playerCommand = "unload" - pctl.playerCommandReady = True + 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]) - wait = 0 - while pctl.playerCommand != "done": - time.sleep(0.01) - wait += 1 - if wait > 200: - break + yy = window_size[1] / 3 - boxx / 2 + yy += boxx + round(30 * gui.scale) - tauon.thread_manager.ready_playback() - - if pre_state == 1: - pctl.revert() - gui.backend_reloading = False + 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) -def gen_chart() -> None: - try: + if self.drag: + gui.update_on_drag = True - topchart = t_topchart.TopChart(tauon, album_art_gen) + 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"]) - tracks = [] + radios[radios.index("New")] = self.drag + self.drag = None + gui.update += 1 - source_tracks = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids +class Showcase: - 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) + def __init__(self): - for item in dex: - tracks.append(pctl.get_track(source_tracks[item])) + self.lastfm_artist = None + self.artist_mode = False - 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)) + def render(self): - path = topchart.generate( - tracks, prefs.chart_bg, prefs.chart_rows, prefs.chart_columns, prefs.chart_text, - prefs.chart_font, prefs.chart_tile, cascade) + global right_click - 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 + box = int(window_size[1] * 0.4 + 120 * gui.scale) + box = min(window_size[0] // 2, box) - gui.generating_chart = False + hide_art = False + if window_size[0] < 900 * gui.scale: + hide_art = True - if path: - open_file(path) - else: - show_message(_("There was an error generating the chart"), _("Sorry!"), mode="error") - return + x = int(window_size[0] * 0.15) + y = int((window_size[1] / 2) - (box / 2)) - 10 * gui.scale - show_message(_("Chart generated"), mode="done") + if hide_art: + box = 45 * gui.scale + elif window_size[1] / window_size[0] > 0.7: + x = int(window_size[0] * 0.07) -class Over: - def __init__(self): + 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) - global window_size + t1 = colours.grey(250) - self.init2done = False + 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] - 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) + if test_lumi(colours.playlist_panel_background) < 0.7: + light_mode = True + t1 = colours.grey(30) + gui.vis_4_colour = [40, 40, 40, 255] - # self.tab_width = round(115 * gui.scale) - self.w = 100 - self.h = 100 + ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), colours.playlist_panel_background) - self.box_x = 100 - self.box_y = 100 - self.item_x_offset = round(25 * gui.scale) + if prefs.bg_showcase_only and prefs.art_bg: + style_overlay.display() - self.current_path = os.path.expanduser("~") - self.view_offset = 0 - self.ext_ratio = {} - self.last_db_size = -1 + # 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 - self.enabled = False - self.click = False - self.right_click = False - self.scroll = 0 - self.lock = False + 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 - self.drives = [] + if prefs.bg_showcase_only and prefs.art_bg: + ddt.alpha_bg = True + ddt.force_gray = True - self.temp_lastfm_user = "" - self.temp_lastfm_pass = "" - self.lastfm_input_box = 0 + # 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() - 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], - ] + # ddt.force_gray = True - 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 + if pctl.playing_state == 3 and not radiobox.dummy_track.title: - self.ani_cred = 0 - self.cred_page = 0 - self.ani_fade_on_timer = Timer(force=10) - self.ani_fade_off_timer = Timer(force=10) + 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) - self.device_scroll_bar_position = 0 + y = int(window_size[1] / 2) - 60 - gui.scale + ddt.text((x, y, 2), pctl.tag_meta, colours.side_bar_line1, 216, w) - 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 + else: - self.account_text_field = -1 + if len(pctl.track_queue) < 1: + ddt.alpha_bg = False + return - self.themes = [] - self.view_supporters = False - self.key_box = TextBox2() - self.key_box_focused = False + # if draw.button("Return", 20, gui.panelY + 5, bg=colours.grey(30)): + # pass - def theme(self, x0, y0, w0, h0): + if prefs.bg_showcase_only and prefs.art_bg: + ddt.alpha_bg = True + ddt.force_gray = True - global album_mode_art_size - global update_layout + 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 - y = y0 + 13 * gui.scale - x = x0 + 25 * gui.scale + if gui.force_showcase_index >= 0: + index = gui.force_showcase_index + track = pctl.master_library[index] + else: - ddt.text_background_colour = colours.box_background - ddt.text((x, y), _("Theme"), colours.box_text_label, 12) + if pctl.playing_state == 3: + track = radiobox.dummy_track + else: + index = pctl.track_queue[pctl.queue_step] + track = pctl.master_library[index] - y += 25 * gui.scale + if not hide_art: - self.toggle_square(x, y, toggle_auto_bg, _("Use album art as background")) + # 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) - y += 23 * gui.scale + # Draw album art in box + album_art_gen.display(track, (x, y), (box, box)) - 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 + # 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 - 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")) + # Check for lyrics if auto setting + test_auto_lyrics(track) - #y += 23 * gui.scale - self.toggle_square(x + 120 * gui.scale, y, toggle_auto_bg_blur, _("Blur")) + gui.draw_vis4_top = False - y += 23 * gui.scale - self.toggle_square(x + 10 * gui.scale, y, toggle_auto_bg_showcase, _("Showcase only")) + 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) - 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")) + gcx = x + box + int(window_size[0] * 0.15) + 10 * gui.scale + gcx -= 100 * gui.scale - y += 25 * gui.scale + timed_ready = False + if True and prefs.show_lyrics_showcase: + timed_ready = timed_lyrics_ren.generate(track) - self.toggle_square(x, y, toggle_auto_theme, _("Auto-theme from album art")) + if timed_ready and track.lyrics: - y += 55 * gui.scale + # 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 - square = round(8 * gui.scale) - border = round(4 * gui.scale) - outer_border = round(2 * gui.scale) + timed_ready = prefs.prefer_synced_lyrics - # theme_files = get_themes() - xx = x - yy = y - hover_name = None - for c, theme_name, theme_number in self.themes: + 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 + elif True and prefs.show_lyrics_showcase and timed_ready: + w = window_size[0] - (x + box) - round(30 * gui.scale) + timed_lyrics_ren.render(track.index, gcx, y, w=w) + elif track.lyrics == "" or not prefs.show_lyrics_showcase: + w = window_size[0] - (x + box) - round(30 * gui.scale) + x = int(x + box + (window_size[0] - x - box) / 2) - if 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) + if hide_art: + x = window_size[0] // 2 - rect = [xx, yy, border * 2 + square * 2, border * 2 + square * 2] - ddt.rect(rect, [5, 5, 5, 255]) + # x = int((window_size[0]) / 2) + y = int(window_size[1] / 2) - round(60 * gui.scale) - 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 + if prefs.showcase_vis and prefs.backend == 1: + y -= round(30 * gui.scale) - c1 = c.playlist_panel_background - c2 = c.artist_playing - c3 = c.title_playing - c4 = c.bottom_panel_colour + 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) - if theme_name == "Carbon": - c1 = c.title_playing - c2 = c.playlist_panel_background - c3 = c.top_panel_background + y += round(48 * gui.scale) - if theme_name == "Lavender Light": - c1 = c.tab_background_active + 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) - if theme_name == "Neon Love": - c2 = c.artist_text - c4 = [118, 85, 194, 255] - c1 = c4 + 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 theme_name == "Sky": - c2 = c.artist_text + gui.spec4_rec.x = x - (gui.spec4_rec.w // 2) + gui.spec4_rec.y = y + round(50 * gui.scale) - if theme_name == "Sunken": - c2 = c.title_text - c3 = c.artist_text - c4 = [59, 115, 109, 255] - c1 = c4 + if prefs.showcase_vis and window_size[1] > 369 and not search_over.active and not ( + tauon.spot_ctl.coasting or tauon.spot_ctl.playing): - if c2 == c3 and colour_value(c1) < 200: - rect = [(xx + border + square) - (square // 2), (yy + border + square) - (square // 2), square, square] - ddt.rect(rect, c2) + 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 - # tl - rect = [xx + border, yy + border, square, square] - ddt.rect(rect, c1) + 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 - # tr - rect = [xx + border + square, yy + border, square, square] - ddt.rect(rect, c2) + lyrics_ren.test_update(track) + tw, th = ddt.get_text_wh(lyrics_ren.text + "\n", 17, w, True) - # bl - rect = [xx + border, yy + border + square, square, square] - ddt.rect(rect, c3) + 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) - # br - rect = [xx + border + square, yy + border + square, square, square] - ddt.rect(rect, c4) + 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 - yy += round(27 * gui.scale) - if yy > y + 40 * gui.scale: - yy = y - xx += round(27 * gui.scale) + def render_vis(self, top=False): - 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 + SDL_SetRenderTarget(renderer, gui.spec4_tex) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) + SDL_RenderClear(renderer) - 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) + bx = 0 + by = 50 * gui.scale - def rg(self, x0, y0, w0, h0): - y = y0 + 55 * gui.scale - x = x0 + 130 * gui.scale + if gui.vis_4_colour is not None: + SDL_SetRenderDrawColor( + renderer, gui.vis_4_colour[0], gui.vis_4_colour[1], gui.vis_4_colour[2], gui.vis_4_colour[3]) - if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): - self.rg_view = False + 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 - y = y0 + round(15 * gui.scale) - x = x0 + round(50 * gui.scale) + for i in range(len(gui.spec4_array)): + gui.spec4_array[i] -= 0.1 + gui.spec4_array[i] = max(gui.spec4_array[i], 0) - ddt.text((x, y), _("ReplayGain"), colours.box_text_label, 14) - y += round(25 * gui.scale) + if not top and (pctl.playing_state == 1 or pctl.playing_state == 3): + gui.update = 2 - 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")) + slide = 0.7 + for i, bar in enumerate(gui.spec4_array): - 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) + # We wont draw higher bars that may not move + if i > 40: + break - ddt.text((x, y), _("Pre-amp"), colours.box_text_label, 14) - y += round(26 * gui.scale) + # 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 - sw = round(170 * gui.scale) - sh = round(2 * gui.scale) + # 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)))) - slider = (x, y, sw, sh) + # 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) - gh = round(14 * gui.scale) - gw = round(8 * gui.scale) - grip = [0, y - (gh // 2), gw, gh] + SDL_RenderFillRect(renderer, gui.bar4) - grip[0] = x + # Set distance between bars + bx += 8 * gui.scale - bp = prefs.replay_preamp + 15 + if top: + SDL_SetRenderTarget(renderer, None) + else: + SDL_SetRenderTarget(renderer, gui.main_texture) - grip[0] += (bp / 30 * sw) + # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) + SDL_RenderCopy(renderer, gui.spec4_tex, None, gui.spec4_rec) - m1 = (x, y, sh, sh * 2) - m2 = ((x + sw // 2), y, sh, sh * 2) - m3 = ((x + sw), y, sh, sh * 2) +class ColourPulse2: + """Animates colour between two colours""" + def __init__(self): - if coll(grow_rect(slider, 15)) and mouse_down: - bp = (mouse_position[0] - x) / sw * 30 - gui.update += 1 + self.timer = Timer() + self.in_timer = Timer() + self.out_timer = Timer() + self.out_timer.start = 0 + self.active = False - bp = round(bp) - bp = max(bp, 0) - bp = min(bp, 30) - prefs.replay_preamp = bp - 15 + def get(self, hit, on, off, low_hls, high_hls): - # grip[0] += (bp / 30 * sw) + 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] - 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) + ani_time = 0.15 - text = f"{prefs.replay_preamp} dB" - if prefs.replay_preamp > 0: - text = "+" + text + if hit is True and self.active is False: + self.active = True + self.in_timer.set() - 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) + out_time = self.out_timer.get() + if out_time < ani_time: + self.in_timer.force_set(ani_time - out_time) - 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) + elif hit is False and self.active is True: + self.active = False + self.out_timer.set() - def eq(self, x0, y0, w0, h0): + in_time = self.in_timer.get() + if in_time < ani_time: + self.out_timer.force_set(ani_time - in_time) - y = y0 + 55 * gui.scale - x = x0 + 130 * gui.scale + 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 - if self.button(x - 110 * gui.scale, y + 180 * gui.scale, _("Return"), width=75 * gui.scale): - self.eq_view = False + return colour_slide(low_hls, high_hls, pro, 1) - base_dis = 160 * gui.scale - center = base_dis // 2 - width = 25 * gui.scale +class ViewBox: - range = 12 + def __init__(self, reload=False): + self.x = 0 + self.y = gui.panelY + self.w = 52 * gui.scale + self.h = 260 * gui.scale # 257 + self.active = False - self.toggle_square(x - 90 * gui.scale, y - 35 * gui.scale, toggle_eq, _("Enable")) + self.border = 3 * gui.scale - 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) + 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) - for i, q in enumerate(prefs.eq): + # _ .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) - bar = [x, y, width, base_dis] + self.on_colour = [255, 190, 50, 255] + self.over_colour = [255, 190, 50, 255] + self.off_colour = colours.grey(40) - ddt.rect(bar, [255, 255, 255, 20]) + if not reload: + gui.combo_was_album = False - bar[0] -= 2 * gui.scale - bar[1] -= 10 * gui.scale - bar[2] += 4 * gui.scale - bar[3] += 20 * gui.scale + def activate(self, x): + self.x = x + self.active = True + self.clicked = False - if coll(bar): + 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) - 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 + 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 - prefs.eq[i] = target + self.col_force_off = False - pctl.playerCommand = "seteq" - pctl.playerCommandReady = True + # gui.level_2_click = False + gui.update = 2 - if self.right_click: - prefs.eq[i] = 0 - pctl.playerCommand = "seteq" - pctl.playerCommandReady = True + def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): - start = (q / range) * center + on = test() + rect = [x - 8 * gui.scale, + y - 8 * gui.scale, + asset.w + 16 * gui.scale, + asset.h + 16 * gui.scale] + fields.add(rect) - bar = [x, y + center, width, start] + if on: + colour = self.on_colour - ddt.rect(bar, [100, 200, 100, 255]) + else: + colour = self.off_colour - x += round(29 * gui.scale) + fun = None + col = False + if coll(rect): - def audio(self, x0, y0, w0, h0): + tool_tip.test(x + asset.w + 10 * gui.scale, y - 15 * gui.scale, name) - global mouse_down + col = True + if gui.level_2_click: + fun = test + if colour_get is None: + colour = self.over_colour - ddt.text_background_colour = colours.box_background - y = y0 + 40 * gui.scale - x = x0 + 20 * gui.scale + colour = colour_get.get(col, on, not on and not animate, low, high) - if self.eq_view: - self.eq(x0, y0, w0, h0) - return + # if "+" in name: + # + # colour = cctest.get(col, on, [0, 0.2, 0.0], [0, 0.8, 0.8]) - if self.rg_view: - self.rg(x0, y0, w0, h0) - return + # if not on and not animate: + # colour = self.off_colour - colour = colours.box_sub_text + asset.render(x, y, colour) - # 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) + return fun - elif prefs.backend == 4: + def tracks(self, hit=False): - y = y0 + round(20 * gui.scale) - x = x0 + 20 * gui.scale + if hit is False: + return album_mode is False and \ + gui.combo_mode is False and \ + gui.rsp is False - x += round(2 * gui.scale) + 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.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")) + view_tracks() - y += round(40 * gui.scale) - if self.button(x, y, _("ReplayGain")): - mouse_down = False - self.rg_view = True + def side(self, hit=False): - 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__() + 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 - 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) + view_standard_meta() - 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) + def gallery1(self, hit: bool = False) -> bool | None: - # if prefs.device_buffer > 100: - # prefs.pa_fast_seek = True - # else: - # prefs.pa_fast_seek = False + if hit is False: + return album_mode is True # and gui.show_playlist is True - 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 album_mode and not gui.combo_mode: + gui.hide_tracklist_in_gallery ^= True + gui.rspw = gui.pref_gallery_w + gui.update_layout() + # x_menu.active = False + x_menu.close_next_frame = True + # Menu.active = False + return None - if 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")) + if x_menu.active: + x_menu.close_next_frame = True - 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") + force_album_view() - 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 + def radio(self, hit=False): - 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) + if hit is False: + return gui.radio_view - i = 0 - reload = False - for name in prefs.phazor_devices: + if not gui.radio_view: + enter_radio_view() + else: + exit_combo(restore=True) - if i < self.device_scroll_bar_position: - continue - if y > self.box_y + self.h - 40 * gui.scale: - break + if x_menu.active: + x_menu.close_next_frame = True - rect = (x, y + 4 * gui.scale, 245 * gui.scale, 13) + def lyrics(self, hit=False): - if self.click and coll(rect): - prefs.phazor_device_selected = name - reload = True + if hit is False: + return gui.showcase_mode - line = trunc_line(name, 10, 245 * gui.scale) + if not gui.showcase_mode: + if gui.radio_view: + gui.was_radio = True + enter_showcase_view() - fields.add(rect) + elif gui.was_radio: + enter_radio_view() + else: + exit_combo(restore=True) + if x_menu.active: + x_menu.close_next_frame = True - 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 + def col(self, hit=False): - if reload: - pctl.playerCommand = "set-device" - pctl.playerCommandReady = True + if hit is False: + return gui.set_mode - def reload_device(self, _): - pctl.playerCommand = "reload" - pctl.playerCommandReady = True + if not gui.set_mode: + if gui.combo_mode: + exit_combo() - def toggle_lyrics_view(self): - self.lyrics_panel ^= True + if album_mode and gui.plw < 550 * gui.scale: + toggle_album_mode() - def lyrics(self, x0, y0, w0, h0): - x = x0 + 25 * gui.scale - y = y0 - 10 * gui.scale - y += 30 * gui.scale + toggle_library_mode() - ddt.text_background_colour = colours.box_background + def artist_info(self, hit=False): - # 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 + if hit is False: + return gui.artist_info_panel - self.toggle_square(x, y, toggle_guitar_chords, _("Enable chord lyrics")) + gui.artist_info_panel ^= True + gui.update_layout() - y += 40 * gui.scale - ddt.text((x, y), _("Sources:"), colours.box_text_label, 11) - y += 23 * gui.scale + def render(self): - 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) + if prefs.shuffle_lock: + self.active = False + self.clicked = False + return - 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 + if not self.active: + return - def view2(self, x0, y0, w0, h0): + # rect = [self.x, self.y, self.w, self.h] + # if x_menu.clicked or inp.mouse_click: + if self.clicked: + gui.level_2_click = True + self.clicked = False - x = x0 + 25 * gui.scale - y = y0 + 20 * gui.scale + x = self.x - 40 * gui.scale - ddt.text_background_colour = colours.box_background + vr = [x, gui.panelY, self.w, self.h] + # vr = [x, gui.panelY, 52 * gui.scale, 220 * gui.scale] - ddt.text((x, y), _("Metadata side panel"), colours.box_text_label, 12) + 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) - 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() + x += 7 * gui.scale + y = gui.panelY + 14 * gui.scale - global album_mode_art_size - global update_layout - y += 35 * gui.scale - ddt.text((x, y), _("Gallery"), colours.box_text_label, 12) + func = None - 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")) + # 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) - y += 30 * gui.scale + # if colours.lm: + # low = (0, 0.5, 0) - # y += 25 * gui.scale + # ---- + #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] - x -= 80 * gui.scale - x += ddt.get_text_w(_("Thumbnail size"), 312) - # x += 20 * gui.scale + 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 - 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) + y += 40 * gui.scale - 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) + high = [76, 137, 229, 255] # (.6, .6, .75) + if colours.lm: + # high = (.6, .80, .85) + high = [63, 63, 63, 255] - def funcs(self, x0, y0, w0, h0): - - x = x0 + 25 * gui.scale - y = y0 - 10 * gui.scale + 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 - ddt.text_background_colour = colours.box_background + # --- - if self.func_page == 0: + y += 40 * gui.scale - y += 23 * gui.scale + high = [76, 229, 229, 255] + if colours.lm: + # high = (.5, .7, .65) + high = [63, 63, 63, 255] - self.toggle_square( - x, y, toggle_enable_web, _("Enable Listen Along"), subtitle=_("Start server for remote web playback")) + 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 - 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) + y += 45 * gui.scale - if coll(link_rect2): - if not self.click: - gui.cursor_want = 3 + high = [107, 76, 229, 255] + if colours.lm: + # high = (.7, .75, .75) + high = [63, 63, 63, 255] - if self.click: - webbrowser.open(link_pa2[2], new=2, autoraise=True) + test = self.button( + x + 4 * gui.scale, y, self.lyrics_img, self.lyrics, self.lyrics_colour, + _("Showcase + Lyrics"), low=low, high=high) + if test is not None: + func = test - y += 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 += 40 * gui.scale - y += 38 * gui.scale + high = [92, 86, 255, 255] + if colours.lm: + # high = (.7, .75, .75) + high = [63, 63, 63, 255] - self.toggle_square( - x, y, toggle_auto_artist_dl, - _("Auto fetch artist data"), - subtitle=_("Downloads data in background when artist panel is open")) + test = self.button( + x + 3 * gui.scale, y, self.radio_img, self.radio, self.radio_colour, _("Radio"), low=low, high=high) + if test is not None: + func = test - y += 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 += 45 * gui.scale - # y += 30 * gui.scale + high = [229, 205, 76, 255] + if colours.lm: + # high = (.9, .75, .65) + high = [63, 63, 63, 255] - 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 + 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 - 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) + # 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 - self.button(x + wa + round(20 * gui.scale), y, _("Open data folder"), open_data_directory, ww) + if func is not None: + func(True) - 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 + if gui.level_2_click and coll(vr): + x_menu.clicked = False - 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")) + gui.level_2_click = False + if not x_menu.active: + self.active = False - y += 28 * gui.scale +class DLMon: - x = x0 + self.item_x_offset + def __init__(self): - ddt.text((x, y), _("End of playlist action"), colours.box_text_label, 12) + self.ticker = Timer() + self.ticker.force_set(8) - 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.watching = {} + self.ready = set() + self.done = set() - 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")) + def scan(self): - 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")) + if len(self.watching) == 0: + if self.ticker.get() < 10: + return + elif self.ticker.get() < 2: + return - y += 38 * gui.scale - if not msys: - self.toggle_square(x, y, toggle_use_tray, _("Show icon in system tray")) + self.ticker.set() - y += 25 * gui.scale - self.toggle_square(x + round(10 * gui.scale), y, toggle_min_tray, _("Close to tray")) + for downloads in download_directories: - y += 25 * gui.scale - self.toggle_square(x + round(10 * gui.scale), y, toggle_text_tray, _("Show title text")) + for item in os.listdir(downloads): - 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")) + path = os.path.join(downloads, item) - else: - self.toggle_square(x, y, toggle_min_tray, _("Close to tray")) + if path in self.done: + continue + if path in self.ready and not os.path.exists(path): + del self.ready[path] + continue + if path in self.watching and not os.path.exists(path): + del self.watching[path] + continue - 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 + # 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 - 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 + min_age = (time.time() - stamp) / 60 + ext = os.path.splitext(path)[1][1:].lower() - 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") + if msys and "TauonMusicBox" in path: + continue - 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")) + 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] - 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") + # 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 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() + if os.path.exists(target_dir): + pass + #logging.info("Target folder for archive already exists") - 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")) + 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.") - 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") + 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: - 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 + # 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: - hit_discord() + self.done.add(path) - if old and not prefs.discord_enable: - if prefs.discord_active: - prefs.disconnect_discord = True + 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 - 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 len(self.watching) > 0: + gui.update += 1 - # Switcher - pages = 5 - x = x0 + round(18 * gui.scale) - y = (y0 + h0) - round(29 * gui.scale) - ww = round(40 * gui.scale) +class Fader: - 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 + def __init__(self): - # self.button(x, y, _("Open keymap file"), open_keymap_file, width=wc) + self.total_timer = Timer() + self.timer = Timer() + self.ani_duration = 0.3 + self.state = 0 # 0 = Want off, 1 = Want fade on + self.a = 0 # The fade progress (0-1) - def button(self, x, y, text, plug=None, width=0, bg=None): + def render(self): - w = width - if w == 0: - w = ddt.get_text_w(text, 211) + round(10 * gui.scale) + 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) - h = round(20 * gui.scale) - border_size = round(2 * gui.scale) + rect = [0, 0, window_size[0], window_size[1]] + ddt.rect(rect, [0, 0, 0, int(110 * self.a)]) - 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 not (self.a == 0 or self.a == 1): + gui.update += 1 - if bg is None: - bg = colours.box_background + def rise(self): - real_bg = bg - hit = False + self.state = 1 + self.timer.hit() + self.total_timer.set() - ddt.rect(rect2, colours.box_check_border) - ddt.rect(rect, bg) + def fall(self): - 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) + self.state = 0 + self.timer.hit() + self.total_timer.set() - return hit +class EdgePulse: - 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) + def __init__(self): - bg_colour = colours.box_button_background - real_bg = bg_colour + self.timer = Timer() + self.timer.force_set(10) + self.ani_duration = 0.5 - ddt.rect(rect, bg_colour) - fields.add(rect) - hit = False + 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 - 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) + def pulse(self): + self.timer.set() - 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 +class EdgePulse2: - def toggle_square(self, x, y, function, text: str , click: bool = False, subtitle: str = "") -> bool: + def __init__(self): - x = round(x) - y = round(y) + self.timer = Timer() + self.timer.force_set(10) + self.ani_duration = 0.22 - border = round(2 * gui.scale) - gap = round(2 * gui.scale) - inner_square = round(6 * gui.scale) + def render(self, x, y, w, h, bottom=False) -> bool | None: - full_w = border * 2 + gap * 2 + inner_square + time = self.timer.get() + if time < self.ani_duration: - 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 bottom: + if mouse_wheel > 0: + self.timer.force_set(10) + return None + elif mouse_wheel < 0: + self.timer.force_set(10) + return None - 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 + alpha = 30 - int(25 * (time / self.ani_duration)) + h_off = (h // 5) * (time / self.ani_duration) * 4 - # There are two mode, function type, and passthrough bool type - active = False - if type(function) is bool: - active = function - else: - active = function(1) + if colours.lm: + colour = (0, 0, 0, alpha) + else: + colour = (255, 255, 255, alpha) - if clicked: - if type(function) is bool: - active ^= True + if not bottom: + ddt.rect((x, y, w, h - h_off), colour) else: - function() - active = function(1) + ddt.rect((x, y - (h - h_off), w, h - h_off), colour) + gui.update = 2 + return True + return False - # 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) + def pulse(self): + self.timer.set() - return active +class Undo: - def last_fm_box(self, x0, y0, w0, h0): + def __init__(self): - x = x0 + round(20 * gui.scale) - y = y0 + round(15 * gui.scale) + self.e = [] - ddt.text_background_colour = colours.box_background + def undo(self): - 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 not self.e: + show_message(_("There are no more steps to undo.")) + return - y += 28 * gui.scale + job = self.e.pop() - 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 job[0] == "playlist": + pctl.multi_playlist.append(job[1]) + switch_playlist(len(pctl.multi_playlist) - 1) + elif job[0] == "tracks": - y += 28 * gui.scale + uid = job[1] + li = job[2] - 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")) + 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 - # if self.button2(x, y, "Discogs", width=84*gui.scale): - # self.account_view = 3 + for i, ref in reversed(li): - y += 28 * gui.scale + if i > len(pl): + logging.error("restore track error - playlist not correct length") + continue + pl.insert(i, ref) - if self.button2(x, y, "fanart.tv", width=84 * gui.scale): - self.account_view = 4 + 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 - y += 28 * gui.scale - y += 28 * gui.scale + gui.pl_update = 1 - y += 15 * gui.scale + def bk_playlist(self, pl_index: int) -> None: - if key_shift_down and self.button2(x + round(95 * gui.scale), y, "koel", width=84 * gui.scale): - self.account_view = 6 + self.e.append(("playlist", pctl.multi_playlist[pl_index])) - if self.button2(x, y, "Jellyfin", width=84 * gui.scale): - self.account_view = 10 + def bk_tracks(self, pl_index: int, indis) -> None: - if self.button2(x + round(95 * gui.scale), y, "TIDAL", width=84 * gui.scale): - self.account_view = 12 + uid = pctl.multi_playlist[pl_index].uuid_int + self.e.append(("tracks", uid, indis)) - y += 28 * gui.scale + 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 self.button2(x, y, "Airsonic", width=84 * gui.scale): - self.account_view = 7 +class GetSDLInput: - if self.button2(x + round(95 * gui.scale), y, "PLEX", width=84 * gui.scale): - self.account_view = 5 + def __init__(self): + self.i_y = pointer(c_int(0)) + self.i_x = pointer(c_int(0)) - y += 28 * gui.scale + self.mouse_capture_want = False + self.mouse_capture = False - if self.button2(x, y, "Spotify", width=84 * gui.scale): - self.account_view = 8 + 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]) - if self.button2(x + round(95 * gui.scale), y, "Satellite", width=84 * gui.scale): - self.account_view = 11 + 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 - if self.account_view in (9, 2): - self.toggle_square( - x0 + 230 * gui.scale, y + 2 * gui.scale, toggle_scrobble_mark, - _("Show threshold marker")) +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) - x = x0 + 230 * gui.scale - y = y0 + round(20 * gui.scale) + 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 - if self.account_view == 12: - ddt.text((x, y), "TIDAL", colours.box_sub_text, 213) + self.taskbar.SetProgressValue(self.window_id, perc, 100) - y += round(30 * gui.scale) + elif pctl.playing_state == 2 and self.updated_state != 2: + self.updated_state = 2 + self.taskbar.SetProgressState(self.window_id, 0x8) - 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) + 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 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) +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), + ] - y += round(30 * gui.scale) - if self.button(x, y, _("Import Tracks")): - show_message(_("Fetching playlist...")) - shooter(tauon.tidal.fav_tracks) +def get_cert_path() -> str: + if pyinstaller_mode: + return os.path.join(sys._MEIPASS, "certifi", "cacert.pem") + # Running as script + return certifi.where() - if self.account_view == 11: - ddt.text((x, y), "Tauon Satellite", colours.box_sub_text, 213) +def setup_tls() -> ssl.SSLContext: + """TLS setup (needed for frozen installs)""" + # 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 - y += round(30 * gui.scale) + # Create default TLS context + return ssl.create_default_context(cafile=get_cert_path()) - 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() +def whicher(target: str) -> 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 - y += round(25 * gui.scale) +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] - y += round(30 * gui.scale) + 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 - 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) +def no_padding() -> int: + """This will remove all padding""" + return 0 - y += round(25 * gui.scale) +def uid_gen() -> int: + return random.randrange(1, 100000000) - if self.button(x, y, _("Get playlist")): - if tau.processing: - show_message(_("An operation is already running")) - else: - shooter(tau.get_playlist()) +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 - elif self.account_view == 9: + Creates a default playlist when called without parameters + """ + if playlist_ids is None: + playlist_ids = [] - 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") + notify_change() - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 + #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) - field_width = round(245 * gui.scale) +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 - 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() + #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) - 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() +def open_uri(uri:str) -> None: + logging.info("OPEN URI") + load_order = LoadClass() - y += round(35 * gui.scale) + 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) - if self.button(x, y, _("Test connectivity")): + load_order.target = str(urllib.parse.unquote(uri)).replace("file:///", "/").replace("\r", "") - 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 gui.auto_play_import is False: + load_order.play = True + gui.auto_play_import = True - 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") + load_orders.append(copy.deepcopy(load_order)) + gui.update += 1 - y += round(30 * gui.scale) +def toast(text: str) -> None: + gui.mode_toast_text = text + toast_mode_timer.set() + gui.frame_callback_list.append(TestTimer(1.5)) - 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) +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 self.account_view == 8: +def get_artist_preview(artist, x, y): + # show_message(_("Loading artist image...")) - ddt.text((x, y), "Spotify", colours.box_sub_text, 213) + 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 = "" - prefs.spot_mode = self.toggle_square(x + 80 * gui.scale, y + 2 * gui.scale, prefs.spot_mode, _("Enable")) - y += round(30 * gui.scale) +def set_drag_source(): + gui.drag_source_position = tuple(click_location) + gui.drag_source_position_persist = tuple(click_location) - if self.button(x, y, _("View setup instructions")): - webbrowser.open("https://github.com/Taiko2k/Tauon/wiki/Spotify", new=2, autoraise=True) +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) - field_width = round(245 * gui.scale) + total = 0 + for item in gui.pl_st: + if item[2] is False: + total += item[1] + else: + wid -= item[1] - y += round(26 * gui.scale) + wid = max(75, wid) - 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() + 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 - 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() +def auto_size_columns(): + fixed_n = 0 - y += round(27 * gui.scale) + wid = gui.plw - round(16 * gui.scale) + if gui.tracklist_center_mode: + wid = gui.tracklist_highlight_width - round(16 * 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) + total = wid + for item in gui.pl_st: - tauon.spot_ctl.auth() + if item[2]: + fixed_n += 1 - y += round(31 * gui.scale) - prefs.launch_spotify_web = self.toggle_square( - x, y, prefs.launch_spotify_web, - _("Prefer launching web player")) + if item[0] == "Lyrics": + item[1] = round(50 * gui.scale) + total -= round(50 * gui.scale) - y += round(24 * gui.scale) + if item[0] == "Rating": + item[1] = round(80 * gui.scale) + total -= round(80 * 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 item[0] == "Starline": + item[1] = round(78 * gui.scale) + total -= round(78 * gui.scale) - if prefs.launch_spotify_local and not tauon.enable_librespot: - show_message(_("Librespot not installed?")) - prefs.launch_spotify_local = False + 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 self.account_view == 7: + if item[0] == "P" or item[0] == "S" or item[0] == "#": + item[1] = round(32 * gui.scale) + total -= round(32 * gui.scale) - ddt.text((x, y), _("Airsonic/Subsonic network streaming"), colours.box_sub_text, 213) + if item[0] == "Date": + item[1] = round(55 * gui.scale) + total -= round(55 * gui.scale) - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 + if item[0] == "Bitrate": + item[1] = round(67 * gui.scale) + total -= round(67 * gui.scale) - field_width = round(245 * gui.scale) + if item[0] == "❤": + item[1] = round(27 * gui.scale) + total -= round(27 * 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 + vr = len(gui.pl_st) - fixed_n - 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 + if vr > 0 and total > 50: - 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 + space = round(total / vr) - y += round(40 * gui.scale) - self.button(x, y, _("Import music to playlist"), sub_get_album_thread) + for item in gui.pl_st: + if not item[2]: + item[1] = space - 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")) + gui.pl_update += 1 + update_set() - if self.account_view == 10: +def set_colour(colour): + SDL_SetRenderDrawColor(renderer, colour[0], colour[1], colour[2], colour[3]) - ddt.text((x, y), _("Jellyfin network streaming"), colours.box_sub_text, 213) +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")) - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 + 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]) - field_width = round(245 * gui.scale) + scan_folders(direcs) + themes.sort() + if deco: + return decos + return themes - 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 +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 - 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 +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() - 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 +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" | "arrow" | "download" | "bubble" | "link": + 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 - y += round(30 * gui.scale) +def pumper(): + if macos: + return + while pump: + time.sleep(0.005) + SDL_PumpEvents() - self.button(x, y, _("Import music to playlist"), jellyfin_get_library_thread) +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 - 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() +def advance_theme() -> None: + global theme - y += round(35 * gui.scale) - if self.button(x, y, _("Test connectivity")): - jellyfin.test() + theme += 1 + gui.reload_theme = True - if self.account_view == 6: +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 - ddt.text((x, y), _("koel network streaming"), colours.box_sub_text, 213) +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 "" - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 +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) - field_width = round(245 * gui.scale) + cf.update_value("plex-username", prefs.plex_username) + cf.update_value("plex-password", prefs.plex_password) + cf.update_value("plex-servername", prefs.plex_servername) - 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 + 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) - 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 + 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) - 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 + 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) - y += round(40 * gui.scale) + cf.update_value("display-language", prefs.ui_lang) + # cf.update_value("decode-search", prefs.diacritic_search) - self.button(x, y, _("Import music to playlist"), koel_get_album_thread) + # 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 self.account_view == 5: + # cf.update_value("gst-output", prefs.gst_output) + # cf.update_value("gst-use-custom-output", prefs.gst_use_custom_output) - ddt.text((x, y), _("PLEX network streaming"), colours.box_sub_text, 213) + cf.update_value("separate-multi-genre", prefs.sep_genre_multi) - if inp.key_tab_press: - self.account_text_field += 1 - if self.account_text_field > 2: - self.account_text_field = 0 + cf.update_value("tag-editor-name", prefs.tag_editor_name) + cf.update_value("tag-editor-target", prefs.tag_editor_target) - field_width = round(245 * gui.scale) + 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) - 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 + 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) - 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 + 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) - 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 + 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) - y += round(40 * gui.scale) - self.button(x, y, _("Import music to playlist"), plex_get_album_thread) + 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) - if self.account_view == 4: + 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) - ddt.text((x, y), "fanart.tv", colours.box_sub_text, 213) + cf.update_value("force-subpixel-text", prefs.force_subpixel_text) - 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 + 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) - 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) + 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) - 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") + 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) - # 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) - # + 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 self.account_view == 1: + 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) + + cf.update_value("maloja-key", prefs.maloja_key) + cf.update_value("maloja-url", prefs.maloja_url) + cf.update_value("maloja-enable", prefs.maloja_enable) + + cf.update_value("tau-url", prefs.sat_url) + + 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) + + 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 config_directory.is_dir(): + cf.dump(str(config_directory / "tauon.conf")) + else: + logging.error("Missing config directory") + +def load_prefs(): + cf.reset() + cf.load(str(config_directory / "tauon.conf")) + + 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]") + + # 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)") + + prefs.pause_fade_time = max(prefs.pause_fade_time, 100) + prefs.pause_fade_time = min(prefs.pause_fade_time, 5000) + + 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.") + + 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.") + + # 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.") + + 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.") + + 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.") + + 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) + + 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.") + + cf.br() + cf.add_text("[ui]") + + 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) + + 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") + + 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") + + 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 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) + + 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) + + # 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") + + 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") + + 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") + + 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") + + 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") + + 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 + + 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) + + cf.br() + cf.add_text("[tauon_satellite]") + prefs.sat_url = cf.sync_add("string", "tau-url", prefs.sat_url, "Exclude the port") + + 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") + + + 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.") + + 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) + + 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("/") + + 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("/") + + 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.") + + 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") + + 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") + +def auto_scale() -> None: + + old = prefs.scale_want + + 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") + + prefs.scale_want = round(round(prefs.scale_want / 0.05) * 0.05, 2) + + 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 + + if old != prefs.scale_want: + logging.info(f"Using UI scale: {prefs.scale_want}") + + if prefs.scale_want < 0.5: + prefs.scale_want = 1.0 + + 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 + +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 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 + + if scaled_asset_directory != asset_directory: + logging.info("Rendering icons...") + render_icons(str(svg_directory), str(scaled_asset_directory), scale_want) + + logging.info("Done rendering icons") + + diff_ratio = scale_want / prefs.ui_scale + prefs.ui_scale = scale_want + prefs.playlist_row_height = round(22 * prefs.ui_scale) + + # Save user values + column_backup = gui.pl_st + rspw = gui.pref_rspw + grspw = gui.pref_gallery_w + + gui.destroy_textures() + gui.rescale() + + # 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) + +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 + +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 + +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, "") + + tag = tags + + 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") + + process_odat(nt, natural_get(tags, None, "TDOR", None)) + + 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(nt.comment) > 4 and nt.comment[2] == "+": + nt.comment = "" + if nt.comment[0:3] == "000": + nt.comment = "" + + 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 = "" + + 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) + + 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) + + 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 + + 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 + + tx = tags.getall("UFID") + if tx: + for item in tx: + if item.owner == "http://musicbrainz.org": + nt.misc["musicbrainz_recordingid"] = item.data.decode() + + tx = tags.getall("TSOP") + if tx: + nt.misc["artist_sort"] = tx[0].text[0] + + 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 + + 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) + + if item.desc == "FMPS_RATING": + nt.misc["FMPS_Rating"] = float(item.text[0]) + +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") + +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 + + nt.misc.clear() + + nt.file_ext = os.path.splitext(os.path.basename(nt.fullpath))[1][1:].upper() + + if nt.file_ext.lower() in GME_Formats and gme: + 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) + + filepath = nt.fullpath # this is the full file path + filename = nt.filename # this is the name of the file + + # Get the directory of the file + dir_path = os.path.dirname(filepath) + + # 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) + 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)) + # The function may return infinity if the pattern data is too complex to evaluate + 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() + mpt.openmpt_module_destroy(byref(MOD1)) + del MOD1 + elif nt.file_ext == "FLAC": + with Flac(nt.fullpath) as audio: + audio.read() + + 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 + + elif nt.file_ext == "WAV": + with Wav(nt.fullpath) as audio: + try: + audio.read() + + 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 + + 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) is mutagen.wave._WaveID3: + use_id3(audio.tags, nt) + + elif nt.file_ext in ("OPUS", "OGG", "OGA"): + + #logging.info("get opus") + with Opus(nt.fullpath) as audio: + audio.read() + + #logging.info(audio.title) + + 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) + + 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) + + # # 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", "")) + + with Ape(nt.fullpath) as audio: + audio.read() + + # logging.info(audio.title) + + # 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 + + elif nt.file_ext in ("WV", "TTA"): + + with Ape(nt.fullpath) as audio: + audio.read() - text = "Last.fm" - if prefs.use_libre_fm: - text = "Libre.fm" + # logging.info(audio.title) - ddt.text((x, y), text, colours.box_sub_text, 213) + 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 - 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) + else: + # Use MUTAGEN + try: + if nt.file_ext.lower() in VID_Formats: + scan_ffprobe(nt) + return nt - y += 25 * gui.scale + try: + audio = mutagen.File(nt.fullpath) + except Exception: + logging.exception("Mutagen scan failed, falling back to FFPROBE") + scan_ffprobe(nt) + return nt - 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) + nt.samplerate = audio.info.sample_rate + nt.bitrate = audio.info.bitrate // 1000 + nt.length = audio.info.length + nt.size = os.path.getsize(nt.fullpath) - 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")) + 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") - 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) + if type(audio.tags) == mutagen.mp4.MP4Tags: + tags = audio.tags - else: - self.button(x, y, _("Forget account"), lastfm.auth3) + def in_get(key, tags): + if key in tags: + return tags[key][0] + return "" - x = x0 + 230 * gui.scale - y = y0 + round(130 * 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, toggle_scrobble_mark, "Show scrobble marker") + 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]) - 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) + 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")] - 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) + elif type(audio.tags) == mutagen.id3.ID3: + use_id3(audio.tags, nt) - 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) + except Exception: + logging.exception("Failed loading file through Mutagen") + raise - 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) + # 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) + 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 + # This check won't guarantee that all codepaths above are checked as some return early, but it's better than nothing + # And importantly it does catch openmpt which can actually return such + if math.isinf(nt.length) or math.isnan(nt.length): + logging.error(f"Infinite/NaN found(autocorrected to 0) when scanning tags in file: {vars(nt)}!") + nt.length = 0 + return nt - y += 33 * 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() - 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")) + elif "gensokyoradio.net" in radiobox.loaded_url: - y += 25 * gui.scale + response = requests.get("https://gensokyoradio.net/api/station/playing/", timeout=10) - self.toggle_square( - x, y, toggle_scrobble_mark, - _("Show threshold marker")) + 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", "") - if self.account_view == 2: + 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() - ddt.text((x, y), "ListenBrainz", colours.box_sub_text, 213) + 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) - y += 30 * gui.scale - self.button(x, y, _("Paste Token"), lb.paste_key) + 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() - self.button(x + ddt.get_text_w(_("Paste Token"), 211) + 21 * gui.scale, y, _("Clear"), lb.clear_key) + # Failure + elif pctl.radio_image_bin: + pctl.radio_image_bin.close() + pctl.radio_image_bin = None - y += 35 * gui.scale + gui.clear_image_cache_next += 1 - if prefs.lb_token: - line = prefs.lb_token - ddt.text((x + 0 * gui.scale, y - 0 * gui.scale), line, colours.box_input_text, 212) +def auto_name_pl(target_pl: int) -> None: + if not pctl.multi_playlist[target_pl].playlist_ids: + return - 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) + albums = [] + artists = [] + parents = [] - if coll(link_rect2): - if not self.click: - gui.cursor_want = 3 + track = None - if self.click: - webbrowser.open(link_pa2[2], new=2, autoraise=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) - def clear_local_loves(self): + nt = "" + artist = "" - 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 + if track: + artist = track.artist + if track.album_artist: + artist = track.album_artist - for key, star in star_store.db.items(): - star[1] = star[1].replace("L", "") - star_store.db[key] = star + if track and albums and albums[0] and albums.count(albums[0]) == len(albums): + nt = artist + " - " + track.album - gui.pl_update += 1 - show_message(_("Cleared all loves"), mode="done") + elif track and artists and artists[0] and artists.count(artists[0]) == len(artists): + nt = artists[0] - def get_scrobble_counts(self): + else: + nt = os.path.basename(commonprefix(parents)) - 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 + pctl.multi_playlist[target_pl].title = nt - 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 get_object(index: int) -> TrackClass: + return pctl.master_library[index] - def clear_scrobble_counts(self): +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) - for track in pctl.master_library.values(): - track.lfm_scrobbles = 0 +def open_encode_out() -> None: + if not prefs.encoder_output.exists(): + prefs.encoder_output.mkdir() + if system == "Windows" or msys: + line = r"explorer " + prefs.encoder_output.replace("/", "\\") + subprocess.Popen(line) + else: + if macos: + subprocess.Popen(["open", prefs.encoder_output]) + else: + subprocess.Popen(["xdg-open", prefs.encoder_output]) - show_message(_("Cleared all scrobble counts"), mode="done") +def g_open_encode_out(a, b, c) -> None: + open_encode_out() - def get_friend_love(self): +def notify_song_fire(notification, delay, id) -> None: + time.sleep(delay) + notification.show() + if id is None: + return - 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 + time.sleep(8) + if id == gui.notify_main_id: + notification.close() - 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 notify_song(notify_of_end: bool = False, delay: float = 0.0) -> None: + if not de_notify_support: + return - def get_user_love(self): + if notify_of_end and prefs.end_setting != "stop": + return - 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 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 codec_config(self, x0, y0, w0, h0): + track = pctl.playing_object() - x = x0 + round(25 * gui.scale) - y = y0 + 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 - y += 20 * gui.scale - ddt.text_background_colour = colours.box_background + 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.sync_view: + top_line = track.title - pl = None - if prefs.sync_playlist: - pl = id_to_pl(prefs.sync_playlist) - if pl is None: - prefs.sync_playlist = None + if prefs.notify_include_album: + bottom_line = (track.artist + " | " + track.album).strip("| ") + else: + bottom_line = track.artist - 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) + if not track.title: + a, t = filename_to_metadata(clean_string(track.filename)) + if not track.artist: + bottom_line = a + top_line = t - y += 25 * gui.scale - ww = ddt.text((x, y), _("Path to device music folder: "), colours.box_text_label, 11) - y += 20 * gui.scale + gui.notify_main_id = uid_gen() + id = gui.notify_main_id - 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) + if notify_of_end: + bottom_line = "Tauon Music Box" + top_line = (_("End of playlist")) + id = None - 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.")) + song_notification.update(top_line, bottom_line, i_path) - power_bar_icon.render(rect[0], rect[1], colour) - y += 30 * gui.scale + shoot_dl = threading.Thread(target=notify_song_fire, args=([song_notification, delay, id])) + shoot_dl.daemon = True + shoot_dl.start() - 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 +def get_backend_time(path): + pctl.time_to_get = path - text = _("Start Transcode and Sync") - ww = ddt.get_text_w(text, 211) + 25 * gui.scale - if prefs.bypass_transcode: - text = _("Start Sync") + pctl.playerCommand = "time" + pctl.playerCommandReady = True - 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") + while pctl.playerCommand != "done": + time.sleep(0.005) - y += 60 * gui.scale + return pctl.time_to_get - if self.button(x, y, _("Return"), width=round(75 * gui.scale)): - self.sync_view = False +def get_love(track_object: TrackClass) -> bool: + star = star_store.full_get(track_object.index) + if star is None: + return 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") + if "L" in star[1]: + return True + return False - return +def get_love_index(index: int) -> bool: + star = star_store.full_get(index) + if star is None: + return False - # ---------- + if "L" in star[1]: + return True + return False - ddt.text((x, y + 13 * gui.scale), _("Output codec setting:"), colours.box_text_label, 11) +def get_love_timestamp_index(index: int): + star = star_store.full_get(index) + if star is None: + return 0 + return star[3] - 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) +def love(set=True, track_id=None, no_delay=False, notify=False, sync=True): + if len(pctl.track_queue) < 1: + return False - 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 + if track_id is not None and track_id < 0: + return False - 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 track_id is None: + track_id = pctl.track_queue[pctl.queue_step] - # 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) + loved = False + star = star_store.full_get(track_id) - if prefs.transcode_codec != "flac": - y += 35 * gui.scale + if star is not None: + if "L" in star[1]: + loved = True - prefs.transcode_bitrate = self.slide_control(x, y, _("Bitrate"), "kbs", prefs.transcode_bitrate, 32, 320, 8) + if set is False: + return loved - y -= 1 * gui.scale - x += 280 * gui.scale + # 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 - x = x0 + round(20 * gui.scale) - y = y0 + 215 * gui.scale + if star is None: + star = star_store.new_object() - 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")) + loved ^= True - def devance_theme(self): - global theme + 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) - theme -= 1 - gui.reload_theme = True - if theme < 0: - theme = len(get_themes()) + delay = 0.3 + if no_delay or not sync or not lastfm.details_ready(): + delay = 0 - def config_b(self, x0, y0, w0, h0): + star[3] = time.time() - global album_mode_art_size - global update_layout + 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") - ddt.text_background_colour = colours.box_background - x = x0 + round(25 * gui.scale) - y = y0 + round(20 * gui.scale) + if pctl.master_library[track_id].file_ext == "JELY": + jellyfin.favorite(pctl.master_library[track_id]) - # ddt.text((x, y), _("Window"),colours.box_text_label, 12) + 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 system == "Linux": - self.toggle_square(x, y, toggle_notifications, _("Emit track change notifications")) + gui.pl_update = 2 + gui.update += 1 + if sync and pctl.mpris is not None: + pctl.mpris.update(force=True) - y += 25 * gui.scale - self.toggle_square(x, y, toggle_borderless, _("Draw own window decorations")) +def maloja_get_scrobble_counts(): + if lastfm.scanning_scrobbles is True or not prefs.maloja_url: + return - # y += 25 * gui.scale - # prefs.save_window_position = self.toggle_square(x, y, prefs.save_window_position, - # _("Restore window position on restart")) + 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) - y += 25 * gui.scale - if not draw_border: - self.toggle_square(x, y, toggle_titlebar_line, _("Show playing in titlebar")) + 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 - #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")) + try: + data = json.loads(r.text) + l = data["list"] - 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")) + counts = {} - y += 25 * gui.scale - self.toggle_square(x, y, toggle_level_meter, _("Top-panel visualiser")) + 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 - y += 25 * gui.scale - if prefs.backend == 4: - self.toggle_square(x, y, toggle_showcase_vis, _("Showcase visualisation")) + touched = [] - y += round(30 * gui.scale) - # if not msys: - # y += round(15 * gui.scale) + 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((x, y), _("UI scale for HiDPI displays"), colours.box_text_label, 12) + except Exception: + logging.exception("There was an error parsing the data") + show_message(_("There was an error parsing the data"), mode="warning") - y += round(25 * gui.scale) + gui.pl_update += 1 + lastfm.scanning_scrobbles = False + tauon.bg_save() - sw = round(200 * gui.scale) - sh = round(2 * gui.scale) +def maloja_scrobble(track: TrackClass, timestamp: int = int(time.time())) -> bool | None: + url = prefs.maloja_url - slider = (x, y, sw, sh) + if not track.artist or not track.title: + return None - gh = round(14 * gui.scale) - gw = round(8 * gui.scale) - grip = [0, y - (gh // 2), gw, gh] + if not url.endswith("/newscrobble"): + if not url.endswith("/"): + url += "/" + url += "apis/mlj_1/newscrobble" - grip[0] = x - grip[0] += ((prefs.scale_want - 0.5) / 3 * sw) + d = {} + d["artists"] = [track.artist] # let Maloja parse/fix artists + d["title"] = track.title - 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 track.album: + d["album"] = track.album + if track.album_artist: + d["albumartists"] = [track.album_artist] # let Maloja parse/fix artists - 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 + d["length"] = int(track.length) + d["time"] = timestamp + d["key"] = prefs.maloja_key - text = str(prefs.scale_want) - if len(text) == 3: - text += "0" - text += "x" + try: + r = requests.post(url, json=d, timeout=10) + if r.status_code != 200: + show_message(_("There was an error submitting data to Maloja server"), r.text, mode="warning") + return False + except Exception: + logging.exception("There was an error submitting data to Maloja server") + show_message(_("There was an error submitting data to Maloja server"), mode="warning") + return False + return True - if prefs.x_scale: - text = "auto" +def id_to_pl(id: int): + for i, item in enumerate(pctl.multi_playlist): + if item.uuid_int == id: + return i + return None - 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 +def pl_to_id(pl: int) -> int: + return pctl.multi_playlist[pl].uuid_int - 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) +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] - 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) +def encode_folder_name(track_object: TrackClass) -> str: + folder_name = track_object.artist + " - " + track_object.album - y += round(23 * gui.scale) - self.toggle_square(x, y, self.toggle_x_scale, _("Auto scale")) + if folder_name == " - ": + folder_name = track_object.parent_folder_name - if prefs.scale_want != gui.scale: - gui.update += 1 - if not mouse_down: - gui.update_layout() + folder_name = filename_safe(folder_name).strip() - 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 not folder_name: + folder_name = str(track_object.index) - def toggle_x_scale(self, mode=0): - if mode == 1: - return prefs.x_scale - prefs.x_scale ^= True - auto_scale() - gui.update_layout() + if "cd" not in folder_name.lower() or "disc" not in folder_name.lower(): + if track_object.disc_total not in ("", "0", 0, "1", 1) or ( + str(track_object.disc_number).isdigit() and int(track_object.disc_number) > 1): + folder_name += " CD" + str(track_object.disc_number) - def about(self, x0, y0, w0, h0): + return folder_name - x = x0 + int(w0 * 0.3) - 10 * gui.scale - y = y0 + 85 * gui.scale +def signal_handler(signum, frame): + signal.signal(signum, signal.SIG_IGN) # ignore additional signals + tauon.exit(reason="SIGINT recieved") - ddt.text_background_colour = colours.box_background +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 - icon_rect = (x - 110 * gui.scale, y - 15 * gui.scale, self.about_image.w, self.about_image.h) + return None - genre = "" - if pctl.playing_object() is not None: - genre = pctl.playing_object().genre.lower() +def jellyfin_get_playlists_thread() -> None: + if jellyfin.scanning: + inp.mouse_click = False + show_message(_("Job already in progress!")) + return + jellyfin.scanning = True + shoot_dl = threading.Thread(target=jellyfin.get_playlists) + shoot_dl.daemon = True + shoot_dl.start() - if 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 = "" +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 - if not genre: - self.about_image.render(icon_rect[0], icon_rect[1]) + jellyfin.scanning = True + shoot_dl = threading.Thread(target=jellyfin.ingest_library) + shoot_dl.daemon = True + shoot_dl.start() - x += 20 * gui.scale - y -= 10 * gui.scale +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 - self.title_image.render(x - 1, y, alpha_mod(colours.box_sub_text, 240)) + shoot_dl = threading.Thread(target=plex.get_albums) + shoot_dl.daemon = True + shoot_dl.start() - credit_pages = 5 +def sub_get_album_thread() -> None: + # if prefs.backend != 1: + # show_message("This feature is currently only available with the BASS backend") + # return - if self.click and coll(icon_rect) and self.ani_cred == 0: - self.ani_cred = 1 - self.ani_fade_on_timer.set() + pref_box.close() + save_prefs() + if subsonic.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) + return + subsonic.scanning = True - fade = 0 + shoot_dl = threading.Thread(target=subsonic.get_music3) + shoot_dl.daemon = True + shoot_dl.start() - if self.ani_cred == 1: - t = self.ani_fade_on_timer.get() - fade = round(t / 0.7 * 255) - fade = min(fade, 255) +def koel_get_album_thread() -> None: + # if prefs.backend != 1: + # show_message("This feature is currently only available with the BASS backend") + # return - 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() + pref_box.close() + save_prefs() + if koel.scanning: + inp.mouse_click = False + show_message(_("Already scanning!")) + return + koel.scanning = True - gui.update = 2 + shoot_dl = threading.Thread(target=koel.get_albums) + shoot_dl.daemon = True + shoot_dl.start() - if self.ani_cred == 2: +def do_exit_button() -> None: + if mouse_up or ab_click: + if gui.tray_active and prefs.min_to_tray: + if key_shift_down: + tauon.exit("User clicked X button with shift key") + return + tauon.min_to_tray() + elif gui.sync_progress and not gui.stop_sync: + show_message(_("Stop the sync before exiting!")) + else: + tauon.exit("User clicked X button") - 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 +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) - gui.update = 2 + mouse_down = False + inp.mouse_click = False + drag_mode = False - y += 32 * gui.scale +def do_minimize_button(): - block_y = y - 10 * gui.scale + global mouse_down + global drag_mode + if macos: + # hack + SDL_SetWindowBordered(t_window, True) + SDL_MinimizeWindow(t_window) + SDL_SetWindowBordered(t_window, False) + else: + SDL_MinimizeWindow(t_window) - if self.cred_page == 0: + mouse_down = False + inp.mouse_click = False + drag_mode = False - 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) +def draw_window_tools(): + global mouse_down + global drag_mode - 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) + # 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 - fields.add(link_rect) + 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 - 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) + h = round(28 * gui.scale) + y = round(1 * gui.scale) + if macstyle: + y = round(9 * gui.scale) - elif self.cred_page == 1: + 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 - y += 15 * gui.scale + xx = 0 + l = prefs.left_window_control + r = not l + focused = window_is_focused() - 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) + # Close + if r: + xx = window_size[0] - x_width + xx -= round(2 * gui.scale) - 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) + if macstyle: + xx = window_size[0] - 27 * gui.scale + if l: + xx = round(4 * gui.scale) + rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) + fields.add(rect) + colour = mac_close + if not focused: + colour = (86, 85, 86, 255) + mac_circle.render(xx + 6 * gui.scale, y, colour) + if coll(rect) and not gui.mouse_unknown: + if coll_point(last_click_location, rect): + do_exit_button() + else: + rect = (xx, y, x_width, h) + last_width = x_width + ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_off) + fields.add(rect) + if coll(rect) and not gui.mouse_unknown: + ddt.rect((rect[0], rect[1], rect[2], rect[3]), bg_on) + top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_on) + if coll_point(last_click_location, rect): + do_exit_button() + else: + top_panel.exit_button.render(rect[0] + 8 * gui.scale, rect[1] + 8 * gui.scale, x_off) + # Macstyle restore + if gui.mode == 3: + if macstyle: + if r: + xx -= round(20 * gui.scale) + if l: + xx += round(20 * gui.scale) + rect = (xx + 5, y - 1, 14 * gui.scale, 14 * gui.scale) - 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") + 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 - 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") + # maximize - 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") + 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) - 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") + 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() - 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") + 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) + # minimize - 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") + if draw_min_button: - 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") + # 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) - 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") + 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() - 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") + else: + if r: + xx -= mi_width + if l: + xx += last_width - # 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") + 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) - 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") + # restore - 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") + if gui.mode == 3: - 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") + # 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 - 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") + 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) - 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") +def draw_window_border(): + corner_icon.render(window_size[0] - corner_icon.w, window_size[1] - corner_icon.h, colours.corner_icon) - 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") + corner_rect = (window_size[0] - 20 * gui.scale, window_size[1] - 20 * gui.scale, 20, 20) + fields.add(corner_rect) - 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") + right_rect = (window_size[0] - 3 * gui.scale, 20 * gui.scale, 10, window_size[1] - 40 * gui.scale) + fields.add(right_rect) - 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") + # top_rect = (20 * gui.scale, 0, window_size[0] - 40 * gui.scale, 2 * gui.scale) + # fields.add(top_rect) - 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") + left_rect = (0, 10 * gui.scale, 4 * gui.scale, window_size[1] - 50 * gui.scale) + fields.add(left_rect) - 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") + bottom_rect = (20 * gui.scale, window_size[1] - 4, window_size[0] - 40 * gui.scale, 7 * gui.scale) + fields.add(bottom_rect) - 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") + 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 - 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") + colour = colours.window_frame - y += spacing - ddt.text((x, y), "GTK/PyGObject", colours.box_sub_text, font) - ddt.text((xx, y), "LGPLv2.1+", colours.box_text_label, font) - draw_linked_text2( - xxx, y, "https://gitlab.gnome.org/GNOME/pygobject", colours.box_sub_text, font, click=self.click, replace="gitlab.gnome.org") + ddt.rect((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) - ddt.rect((x, block_y, 369 * gui.scale, 140 * gui.scale), alpha_mod(colours.box_background, fade)) +def bass_player_thread(player): + # logging.basicConfig(filename=user_directory + '/crash.log', level=logging.ERROR, + # format='%(asctime)s %(levelname)s %(name)s %(message)s') - y = y0 + h0 - round(33 * gui.scale) - x = x0 + w0 - 0 * gui.scale + 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 - w = max(ddt.get_text_w(_("Credits"), 211), ddt.get_text_w(_("Next"), 211)) - x -= w + round(40 * gui.scale) +# --------------------------------------------------------------------------------------------- +# ABSTRACT SDL DRAWING FUNCTIONS ----------------------------------------------------- - 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 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] - def topchart(self, x0, y0, w0, h0): +def coll(r): + return r[0] < mouse_position[0] <= r[0] + r[2] and r[1] <= mouse_position[1] <= r[1] + r[3] - x = x0 + round(25 * gui.scale) - y = y0 + 20 * gui.scale +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) - ddt.text_background_colour = colours.box_background + ddt.prime_font(standard_font, 9, 412) + ddt.prime_font(standard_font, 10, 413) - ddt.text((x, y), _("Chart Grid Generator"), colours.box_text, 214) + 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" - 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 + 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) - y += 30 * gui.scale + standard_font = prefs.linux_font_bold + # if msys: + # standard_font = prefs.linux_font_bold + ", Noto Sans, Sans Bold" - 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) + 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) - 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 + 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) - else: + 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) - 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 find_synced_lyric_data(track: TrackClass) -> list[str] | None: + if track.is_network: + return None - y += 35 * gui.scale - x += 5 * gui.scale + direc = track.parent_folder_path + name = os.path.splitext(track.filename)[0] + ".lrc" - 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 + if len(track.lyrics) > 20 and track.lyrics[0] == "[" and ":" in track.lyrics[:20] and "." in track.lyrics[:20]: + return track.lyrics.splitlines() + + 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 - y -= 25 * gui.scale - x += 170 * gui.scale + return data - 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")) +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 = x0 + 15 * gui.scale + 320 * gui.scale - y = y0 + 100 * gui.scale +def draw_internel_link(x, y, text, colour, font): + tweak = font + while tweak > 100: + tweak -= 100 - # . Limited width. Max 13 chars - if self.button(x, y, _("Randomise BG")): + if gui.scale == 2: + tweak *= 2 + tweak += 4 + if gui.scale == 1.25: + tweak = round(tweak * 1.25) + tweak += 1 - r = round(random.random() * 40) - g = round(random.random() * 40) - b = round(random.random() * 40) + sp = ddt.text((x, y), text, colour, font) - prefs.chart_bg = [r, g, b] + rect = [x - 5 * gui.scale, y - 2 * gui.scale, sp + 11 * gui.scale, 23 * gui.scale] + fields.add(rect) - d = random.randrange(0, 4) + if coll(rect): + if not inp.mouse_click: + gui.cursor_want = 3 + ddt.line(x, y + tweak + 2, x + sp, y + tweak + 2, alpha_mod(colour, 180)) + if inp.mouse_click: + return True + return False - if d == 1: - c = 5 + round(random.random() * 20) - prefs.chart_bg = [c, c, c] +def draw_linked_text(location, text, colour, font, force=False, replace=""): + """No hit detect""" + base = "" + link_text = "" + rest = "" + on_base = True - x += 100 * gui.scale - y -= 20 * gui.scale + 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] - display_colour = (prefs.chart_bg[0], prefs.chart_bg[1], prefs.chart_bg[2], 255) + target_link = link_text + if replace: + link_text = replace - rect = (x, y, 70 * gui.scale, 70 * gui.scale) - ddt.rect(rect, display_colour) + left = ddt.get_text_w(base, font) + right = ddt.get_text_w(base + link_text, font) - ddt.rect_s(rect, (50, 50, 50, 255), round(1 * gui.scale)) + x = location[0] + y = location[1] - # x = self.box_x + self.item_x_offset + 200 * gui.scale - # y = self.box_y + 180 * gui.scale + 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) - x = x0 + 260 * gui.scale - y = y0 + 180 * gui.scale + tweak = font + while tweak > 100: + tweak -= 100 - dex = reload_albums(quiet=True, return_playlist=pctl.active_playlist_viewing) + if gui.scale == 2: + tweak *= 2 + tweak += 4 + elif gui.scale != 1: + tweak = round(tweak * gui.scale) + tweak += 2 - x = x0 + round(110 * gui.scale) - y = y0 + 240 * gui.scale + if system == "Windows": + tweak += 1 - # . 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 + # 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)) - x += round(95 * gui.scale) - if gui.generating_chart: - ddt.text((x, y + round(1 * gui.scale)), _("Generating..."), colours.box_text_label, 12) - else: + return left, right - left, target_link - 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 +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) - line = _("{N} Album chart").format(N=str(count)) +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] - ww = ddt.text((x, y + round(1 * gui.scale)), line, colours.box_text_label, 12) + if click is None: + click = inp.mouse_click - 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) + 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 - x = x0 + round(20 * gui.scale) - y = y0 + 240 * gui.scale +def pixel_to_logical(x): + return round((x / window_size[0]) * logical_size[0]) - # . Limited width. Max 8 chars - if self.button(x, y, _("Return"), width=75 * gui.scale): - self.chart_view = 0 +def img_slide_update_gall(value, pause: bool = True) -> None: + global album_mode_art_size + gui.halt_image_rendering = True - def stats(self, x0, y0, w0, h0): + album_mode_art_size = value - x = x0 + 10 * gui.scale - y = y0 + clear_img_cache(False) + if pause: + gallery_load_delay.set() + gui.frame_callback_list.append(TestTimer(0.6)) + gui.halt_image_rendering = False - if self.chart_view == 1: - self.topchart(x0, y0, w0, h0) - return + # Update sizes + tauon.gall_ren.size = album_mode_art_size - 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 + if album_mode_art_size > 150: + prefs.thin_gallery_borders = False - ddt.text_background_colour = colours.box_background - lt_font = 312 - lt_colour = colours.box_text_label +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 = [] - 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) + i = 0 + while len(tauon.gall_ren.queue) > 0: + time.sleep(0.01) + i += 1 + if i > 5 / 0.01: + break - x1 = x + (8 + 10 + 10) * gui.scale - x2 = x1 + max(w1, w2, w3, w4, w5, w6) + 20 * gui.scale - y1 = y + 50 * gui.scale + for key, value in tauon.gall_ren.gall.items(): + SDL_DestroyTexture(value[2]) + tauon.gall_ren.gall = {} - 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() + 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) - album_names = set() - folder_names = set() - count = 0 + 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 - for track_id in default_playlist: - tr = pctl.get_track(track_id) +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) - 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) + 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)) - self.stats_pl_albums = count + 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) - self.stats_pl_length = 0 - for item in default_playlist: - self.stats_pl_length += pctl.master_library[item].length + gui.halt_image_rendering = False + album_art_gen.clear_cache() - line = seconds_to_day_hms(self.stats_pl_length, strings.day, strings.days) +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 - 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) + 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 - ddt.text((x2, y1), line, colours.box_sub_text, 12) + while ddt.get_text_w(line, font) > px: - if self.stats_timer.get() > 5: - album_names = set() - folder_names = set() - count = 0 + line = line[:-1] + if len(line) < 2: + break - for pl in pctl.multi_playlist: - for track_id in pl.playlist_ids: - tr = pctl.get_track(track_id) + return line - 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) +def right_trunc(line: str, font: str, px: int, dots: bool = True) -> str: + if ddt.get_text_w(line, font) < px + 10: + return line - self.total_albums = count + 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(" ") - self.stats_timer.set() + 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 - 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) +# 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 - 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) +def fix_encoding(index, mode, enc): + global default_playlist + global enc_field - # Ratio bar - if len(pctl.master_library) > 115 * gui.scale: - x = x0 - y = y0 + h0 - 7 * gui.scale + todo = [] - full_rect = [x, y, w0, 7 * gui.scale] - d = 0 + 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]) - # Stats - try: - if self.last_db_size != len(pctl.master_library): - self.last_db_size = len(pctl.master_library) - self.ext_ratio = {} - for key, value in pctl.master_library.items(): - if value.file_ext in self.ext_ratio: - self.ext_ratio[value.file_ext] += 1 - else: - self.ext_ratio[value.file_ext] = 1 + for q in range(len(todo)): - for key, value in self.ext_ratio.items(): + # 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]) - colour = [200, 200, 200, 255] - if key in format_colours: - colour = format_colours[key] + 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 - 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] + 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 - h = int(round(value / len(pctl.master_library) * full_rect[2])) - block_rect = [full_rect[0] + d, full_rect[1], h, full_rect[3]] + 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 - ddt.rect(block_rect, colour) - d += h + if old_star != None: + star_store.insert(todo[q], old_star) - block_rect = (block_rect[0], block_rect[1], block_rect[2] - 1, block_rect[3]) - fields.add(block_rect) - if coll(block_rect): - xx = block_rect[0] + int(block_rect[2] / 2) - xx = max(xx, x + 30 * gui.scale) - xx = min(xx, x0 + w0 - 30 * gui.scale) - ddt.text((xx, y0 + h0 - 35 * gui.scale, 2), key, colours.grey_blend_bg(220), 13) + # if key in pctl.star_library: + # newkey = pctl.master_library[todo[q]].title + pctl.master_library[todo[q]].filename + # if newkey not in pctl.star_library: + # pctl.star_library[newkey] = copy.deepcopy(pctl.star_library[key]) + # # del pctl.star_library[key] - if self.click: - gen_codec_pl(key) - except Exception: - logging.exception("Error draw ext bar") +def transfer_tracks(index, mode, to): + todo = [] - def config_v(self, x0, y0, w0, h0): + 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 - ddt.text_background_colour = colours.box_background + pctl.multi_playlist[to].playlist_ids += todo - x = x0 + self.item_x_offset - y = y0 + 17 * gui.scale +def prep_gal(): + global albums + albums = [] - 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) + folder = "" - 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) + for index in default_playlist: - 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 folder != pctl.master_library[index].parent_folder_name: + albums.append([index, 0]) + folder = pctl.master_library[index].parent_folder_name - 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) +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() - x = x0 + self.item_x_offset +def load_m3u(path: str) -> None: + name = os.path.basename(path)[:-4] + playlist = [] + stations = [] - # y += round(25 * gui.scale) + location_dict = {} + titles = {} - # self.toggle_square(x, y, star_line_toggle, _('Show playtime lines')) - y += round(15 * gui.scale) + if not os.path.isfile(path): + return - # 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) + with Path(path).open(encoding="utf-8") as file: + lines = file.readlines() + for i, line in enumerate(lines): + line = line.strip("\r\n").strip() + if not line.startswith("#"): # line.startswith("http"): - y += round(25 * gui.scale) + # 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 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 + if line.startswith("http"): + radio: dict[str, int | str] = {} + radio["stream_url"] = line - y += round(25 * gui.scale) + if line_title: + radio["title"] = line_title + else: + radio["title"] = os.path.splitext(os.path.basename(path))[0].strip() - 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) + stations.append(radio) - self.toggle_square(x, y, toggle_append_date, _("Show album release year")) - y += 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) - self.toggle_square(x, y, toggle_append_total_time, _("Show album duration")) - y += round(35 * gui.scale) + # 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 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 + # 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") - 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 + if playlist: + pctl.multi_playlist.append( + pl_gen(title=name, playlist_ids=playlist)) + if stations: + add_stations(stations, name) - 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) + gui.update = 1 +def read_pls(lines: list[str], path: str, followed: bool = False) -> None: + ids = [] + urls = {} + titles = {} - 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 + 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() - 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 "=" in line and line.startswith("Title"): + # Get number + n = line.split("=")[0][5:] + if n.isdigit(): + if n not in ids: + ids.append(n) + titles[n] = line.split("=", 1)[1].strip() - def set_playlist_stop(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "stop" else False - prefs.end_setting = "stop" + 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] - def set_playlist_repeat(self, mode=0): - if mode == 1: - return True if prefs.end_setting == "repeat" else False - prefs.end_setting = "repeat" + if ".pls" in radio["stream_url"]: + if not followed: + try: + logging.info("Download .pls") + response = requests.get(radio["stream_url"], stream=True, timeout=15) + if int(response.headers["Content-Length"]) < 2000: + read_pls(response.content.decode().splitlines(), path, followed=True) + except Exception: + logging.exception("Failed to retrieve .pls") + else: + stations.append(radio) + if gui.auto_play_import: + gui.auto_play_import = False + radiobox.start(radio) + if stations: + add_stations(stations, os.path.basename(path)) - def small_preset(self): +def load_pls(path: str) -> None: + if os.path.isfile(path): + f = open(path) + lines = f.readlines() + read_pls(lines, path) + f.close() - prefs.playlist_row_height = round(22 * prefs.ui_scale) - prefs.playlist_font_size = 15 - prefs.tracklist_y_text_offset = 0 - gui.update_layout() +def load_xspf(path: str) -> None: + global to_got - def large_preset(self): + name = os.path.basename(path)[:-5] + # tauon.log("Importing XSPF playlist: " + path, title=True) + logging.info("Importing XSPF playlist: " + path) - prefs.playlist_row_height = round(27 * prefs.ui_scale) - prefs.playlist_font_size = 15 - gui.update_layout() + try: + parser = ET.XMLParser(encoding="utf-8") + e = ET.parse(path, parser).getroot() - def slide_control(self, x, y, label, units, value, lower_limit, upper_limit, step=1, callback=None, width=58): + a = [] + b = {} + info = "" - width = round(width * gui.scale) + for top in e: - if label is not None: - ddt.text((x + 55 * gui.scale, y, 1), label, colours.box_text, 312) - x += 65 * gui.scale - y += 1 * gui.scale - rect = (x, y, 33 * gui.scale, 15 * gui.scale) - fields.add(rect) - ddt.rect(rect, colours.box_button_background) - abg = [255, 255, 255, 40] - if coll(rect): + if 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.click: - if value > lower_limit: - value -= step - gui.update_layout() - if callback is not None: - callback(value) + 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 - if mouse_down: - abg = [230, 120, 20, 255] - else: - abg = [220, 150, 20, 255] + b["info"] = info + b["name"] = name + a.append(copy.deepcopy(b)) + b = {} - if colour_value(colours.box_background) > 300: - abg = colours.box_sub_text + except Exception: + logging.exception("Error importing/parsing XSPF playlist") + show_message(_("Error importing XSPF playlist."), _("Sorry about that."), mode="warning") + return - dec_arrow.render(x + 1 * gui.scale, y, abg) + # 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"] - x += 33 * gui.scale + stations.append(radio) - 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) + if gui.auto_play_import: + gui.auto_play_import = False + radiobox.start(radio) - x += width + del a[i] + if stations: + add_stations(stations, os.path.basename(path)) + playlist = [] + missing = 0 - rect = (x, y, 33 * gui.scale, 15 * gui.scale) - fields.add(rect) - ddt.rect(rect, colours.box_button_background) - abg = [255, 255, 255, 40] - if coll(rect): + if len(a) > 5000: + to_got = "xspfl" - 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] + # Generate location dict + location_dict = {} + base_names = {} + r_base_names = {} + titles = {} + for key, value in pctl.master_library.items(): + if value.fullpath != "": + location_dict[value.fullpath] = key + if value.filename != "": + base_names[value.filename] = 0 + r_base_names[key] = value.filename + if value.title != "": + titles[value.title] = 0 - if colour_value(colours.box_background) > 300: - abg = colours.box_sub_text + for track in a: + found = False - inc_arrow.render(x + 1 * gui.scale, y, abg) + # Check if we already have a track with full file path in database + if not found and "location" in track: - return value + location = track["location"] + if location in location_dict: + playlist.append(location_dict[location]) + if not os.path.isfile(location): + missing += 1 + found = True - # def style_up(self): - # prefs.line_style += 1 - # if prefs.line_style > 5: - # prefs.line_style = 1 + if found is True: + continue - def inside(self): + # 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 - return coll((self.box_x, self.box_y, self.w, self.h)) + # 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 - def init2(self): + if (not found and "location" in track) or "title" in track: + nt = TrackClass() + nt.index = pctl.master_count + nt.found = False - self.init2done = True + if "location" in track: + location = track["location"] + set_path(nt, location) + if os.path.isfile(location): + nt.found = True + elif "album" in track: + nt.parent_folder_name = track["album"] + if "artist" in track: + nt.artist = track["artist"] + if "title" in track: + nt.title = track["title"] + if "duration" in track: + nt.length = int(float(track["duration"]) / 1000) + if "album" in track: + nt.album = track["album"] + nt.is_cue = False + if nt.found: + nt = tag_scan(nt) - def close(self): - self.enabled = False - fader.fall() - if gui.opened_config_file: - reload_config_file() + pctl.master_library[pctl.master_count] = nt + playlist.append(pctl.master_count) + pctl.master_count += 1 + if nt.found: + continue - def render(self): + 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 self.init2done is False: - self.init2() + 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 key_esc_press: - self.close() + # tauon.log("Finished importing XSPF") - tab_width = 115 * 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 - side_width = 115 * gui.scale - header_width = 0 + y -= 10 * gui.scale - 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 + w = ddt.get_text_w(text, 312) + 24 * gui.scale + h = 24 * gui.scale - content_width = round(545 * gui.scale) - content_height = round(275 * gui.scale) # 275 - full_width = content_width - full_height = content_height + x -= int(w / 2) - full_width += side_width - full_height += header_width + 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) - x = int(window_size[0] / 2) - int(full_width / 2) - y = int(window_size[1] / 2) - int(full_height / 2) +def close_all_menus(): + for menu in Menu.instances: + menu.active = False + Menu.active = False - self.box_x = x - self.box_y = y - self.w = full_width - self.h = full_height +def menu_standard_or_grey(bool: bool): + if bool: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - border_colour = colours.box_border + return [line_colour, colours.menu_background, None] - 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 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() - current_tab = 0 - tab_height = round(24 * gui.scale) # 30 +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() - 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 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 top_mode: +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() - xx = x - yy = y - tab_width = 90 * gui.scale +def lsp_menu_test_queue(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "queue" - ddt.rect_a((x, y), (full_width, header_width), tab_bg) +def lsp_menu_test_playlist(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "playlist" - for item in self.tabs: +def lsp_menu_test_tree(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "folder view" - if self.click and gui.message_box: - gui.message_box = False +def lsp_menu_test_artist(): + if not gui.lsp: + return False + return prefs.left_panel_mode == "artist list" - box = [xx, yy, tab_width, tab_height] - box2 = [xx, yy, tab_width, tab_height - 1] - fields.add(box2) +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 self.click and coll(box2): - self.tab_active = current_tab - self.lyrics_panel = False +def toggle_repeat() -> None: + gui.update += 1 + pctl.repeat_mode ^= True + if pctl.mpris is not None: + pctl.mpris.update_loop() - 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) +def menu_repeat_off() -> None: + pctl.repeat_mode = False + pctl.album_repeat_mode = False + if pctl.mpris is not None: + pctl.mpris.update_loop() - if coll(box2): - ddt.rect(box, tab_over) +def menu_set_repeat() -> None: + pctl.repeat_mode = True + pctl.album_repeat_mode = False + if pctl.mpris is not None: + pctl.mpris.update_loop() - alpha = 100 - if current_tab == self.tab_active: - alpha = 240 +def menu_album_repeat() -> None: + pctl.repeat_mode = True + pctl.album_repeat_mode = True + if pctl.mpris is not None: + pctl.mpris.update_loop() - ddt.text((xx + (tab_width // 2), yy + 4 * gui.scale, 2), item[0], tab_text, 212) +def toggle_random(): + gui.update += 1 + pctl.random_mode ^= True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - current_tab += 1 - xx += tab_width - if current_tab == 6: - yy += round(24 * gui.scale) # 30 - xx = x +def toggle_random_on(): + pctl.random_mode = True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - else: +def toggle_random_off(): + pctl.random_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - ddt.rect_a((x, y), (tab_width, full_height), tab_bg) +def menu_shuffle_off(): + pctl.random_mode = False + pctl.album_shuffle_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - for item in self.tabs: +def menu_set_random(): + pctl.random_mode = True + pctl.album_shuffle_mode = False + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - 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 +def menu_album_random(): + pctl.random_mode = True + pctl.album_shuffle_mode = True + if pctl.mpris is not None: + pctl.mpris.update_shuffle() - 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) +def toggle_shuffle_layout(albums=False): + prefs.shuffle_lock ^= True + if prefs.shuffle_lock: - if self.click and coll(box2): - self.tab_active = current_tab - self.lyrics_panel = False + gui.shuffle_was_showcase = gui.showcase_mode + gui.shuffle_was_random = pctl.random_mode + gui.shuffle_was_repeat = pctl.repeat_mode - 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 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 coll(box2): - ddt.rect(box, tab_over) +def toggle_shuffle_layout_albums(): + toggle_shuffle_layout(albums=True) - yy = box[1] + 4 * gui.scale +def exit_shuffle_layout(_): + return prefs.shuffle_lock - 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) +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) - current_tab += 1 +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) - # ddt.line(x + 110, self.box_y + 1, self.box_x + 110, self.box_y + self.h, colours.grey(50)) +def artist_info_panel_close(): + gui.artist_info_panel ^= True + gui.update_layout() - self.tabs[self.tab_active][1](x + side_width, y + header_width, content_width, content_height) +def toggle_bio_size_deco(): + line = _("Make Large Size") + if prefs.bio_large: + line = _("Make Compact Size") - self.click = False - self.right_click = False + return [colours.menu_text, colours.menu_background, line] - ddt.text_background_colour = colours.box_background +def toggle_bio_size(): + if prefs.bio_large: + prefs.bio_large = False + update_layout_do() + # bio_set_small() -class Fields: - def __init__(self): + else: + prefs.bio_large = True + update_layout_do() + # bio_set_large() + # gui.update_layout() - self.id = [] - self.last_id = [] +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 - self.field_array = [] - self.force = False +def test_shift(_): + return key_shift_down or key_shiftr_down - def add(self, rect, callback=None): +def test_artist_dl(_): + return not prefs.auto_dl_artist_data - self.field_array.append((rect, callback)) +def show_in_playlist(): + if album_mode and window_size[0] < 750 * gui.scale: + toggle_album_mode() - def test(self): + 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 self.force: - self.force = False - return True +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]) - self.last_id = self.id - #logging.info(len(self.id)) - self.id = [] +def open_folder_disable_test(index: int): + track = pctl.master_library[index] + return track.is_network and not os.path.isdir(track.parent_folder_path) - for 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 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 self.last_id == self.id: - return False + if system == "Windows" or msys: + line = r'explorer /select,"%s"' % ( + track.fullpath.replace("/", "\\")) + subprocess.Popen(line) + else: + line = track.parent_folder_path + line += "/" + if macos: + line = track.fullpath + subprocess.Popen(["open", "-R", line]) + else: + subprocess.Popen(["xdg-open", line]) - return True +def tag_to_new_playlist(tag_item): + path_stem_to_playlist(tag_item.path, tag_item.name) - def clear(self): +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) - self.field_array = [] +def stem_to_new_playlist(path: str) -> None: + path_stem_to_playlist(path, os.path.basename(path)) -def update_playlist_call(): - gui.update + 2 - gui.pl_update = 2 +def move_playing_folder_to_tree_stem(path: str) -> None: + move_playing_folder_to_stem(path, pl_id=tree_view_box.get_pl_id()) -def pl_is_mut(pl: int) -> bool: - id = pl_to_id(pl) - if id is None: - return False - return not (pctl.gen_codes.get(id) and "self" not in pctl.gen_codes[id]) +def 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 -def clear_gen(id: int) -> None: - del pctl.gen_codes[id] - show_message(_("Okay, it's a normal playlist now."), mode="done") + track = pctl.playing_object() -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, ""): + if not track or pctl.playing_state == 0: + show_message(_("No item is currently playing")) return - gui.message_box_confirm_callback = clear_gen - gui.message_box_confirm_reference = (id,) - show_message(_("You added tracks to a generator playlist. Do you want to clear the generator?"), mode="confirm") -class TopPanel: - def __init__(self): + move_folder = track.parent_folder_path - self.height = gui.panelY - self.ty = 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.start_space_left = round(46 * gui.scale) - self.start_space_compact_left = 46 * gui.scale + target_base = path - 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 + # Determine name for artist folder + artist = track.artist + if track.album_artist: + artist = track.album_artist - self.ini_menu_space = 17 * gui.scale # 17 - self.menu_space = 17 * gui.scale - self.click_buffer = 4 * gui.scale + # Make filename friendly + artist = filename_safe(artist) + if not artist: + artist = "unknown artist" - self.tabs_right_x = 0 # computed for drag and drop code elsewhere (hacky) - self.tabs_left_x = 1 + # Sanity checks + if track.is_network: + show_message(_("This track is a networked track."), mode="error") + return - self.prime_tab = gui.saved_prime_tab - self.prime_side = gui.saved_prime_direction # 0=left, 1=right - self.shown_tabs = [] + if not os.path.isdir(move_folder): + show_message(_("The source folder does not exist."), mode="error") + return - # --- - self.space_left = 0 - self.tab_text_spaces = [] - self.index_playing = -1 - self.drag_zone_start_x = 300 * gui.scale + if not os.path.isdir(target_base): + show_message(_("The destination folder does not exist."), mode="error") + return - 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) + if os.path.normpath(target_base) == os.path.normpath(move_folder): + show_message(_("The destination and source folders are the same."), mode="error") + return - self.drag_slide_timer = Timer(100) - self.tab_d_click_timer = Timer(10) - self.tab_d_click_ref = None + if len(target_base) < 4: + show_message(_("Safety interupt! The source path seems oddly short."), target_base, mode="error") + return - self.adds = [] + 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 - def left_overflow_switch_playlist(self, pl): - self.prime_side = 0 - self.prime_tab = pl - switch_playlist(pl) + if directory_size(move_folder) > 3000000000: + show_message(_("Folder size safety limit reached! (3GB)"), move_folder, mode="warning") + return - def right_overflow_switch_playlist(self, pl): - self.prime_side = 1 - self.prime_tab = pl - switch_playlist(pl) + # Use target folder if it already is an artist folder + if os.path.basename(target_base).lower() == artist.lower(): + artist_folder = target_base - def render(self): + # 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 quick_drag - 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 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 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) + 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 +def move_playing_folder_to_tag(tag_item): + move_playing_folder_to_stem(tag_item.path) - 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 re_import4(id): + p = None + for i, idd in enumerate(default_playlist): + if idd == id: + p = i + break - wwx = 0 - if prefs.left_window_control and not gui.compact_bar: - if gui.macstyle: - wwx = 24 - # wwx = round(64 * gui.scale) - if draw_min_button: - wwx += 20 - if draw_max_button: - wwx += 20 - wwx = round(wwx * gui.scale) - else: - wwx = 26 - # wwx = round(90 * gui.scale) - if draw_min_button: - wwx += 35 - if draw_max_button: - wwx += 33 - wwx = round(wwx * gui.scale) + load_order = LoadClass() - rect = (wwx + 9 * gui.scale, yy + 4 * gui.scale, 34 * gui.scale, 25 * gui.scale) - fields.add(rect) + if p is not None: + load_order.playlist_position = p - if coll(rect) and not prefs.shuffle_lock: - if inp.mouse_click: + 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") + +def re_import3(stem): + p = None + for i, id in enumerate(default_playlist): + if pctl.get_track(id).fullpath.startswith(stem + "/"): + p = i + break - if gui.combo_mode: - gui.switch_showcase_off = True - else: - gui.lsp ^= True + load_order = LoadClass() - update_layout = True - gui.update += 1 - if mouse_down and quick_drag: - gui.lsp = True - update_layout = True - gui.update += 1 + if p is not None: + load_order.playlist_position = p - if middle_click: - toggle_left_last() - update_layout = True - gui.update += 1 + 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 right_click: - # prefs.artist_list ^= True - lsp_menu.activate(position=(5 * gui.scale, gui.panelY)) - update_layout_do() +def collapse_tree_deco(): + pl_id = tree_view_box.get_pl_id() - colour = colours.corner_button # [230, 230, 230, 255] + 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.lsp: - colour = colours.corner_button_active - if gui.combo_mode: - colour = colours.corner_button - if coll(rect): - colour = colours.corner_button_active +def collapse_tree(): + tree_view_box.collapse_all() - 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) +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 - # 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) +def lock_folder_tree_deco(): + if tree_view_box.lock_pl: + return [colours.menu_text, colours.menu_background, _("Unlock Panel")] + return [colours.menu_text, colours.menu_background, _("Lock Panel")] - if playlist_box.drag: - drag_mode = False +def finish_current(): + playing_object = pctl.playing_object() + if playing_object is None: + show_message("") - # Need to test length - self.tab_text_spaces = [] + 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.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) +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) - x = self.start_space_left + wwx - y = yy # self.ty + 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 - # 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 + 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 - p_text_len = 180 * gui.scale - right_space_es = p_text_len + offset +def add_album_to_queue_fc(ref): + playing_object = pctl.playing_object() + if playing_object is None: + show_message("") - x_start = x + queue_item = None - 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 + 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 - # List all tabs eligible to be shown - #logging.info("-------------") - ready_tabs = [] - show_tabs = [] + 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: - 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)) + p = pctl.get_track(ref).parent_folder_path + p = "" + if pctl.playing_ready(): + p = pctl.playing_object().parent_folder_path - left_tabs = [] - right_tabs = [] - if prefs.shuffle_lock: - for p in ready_tabs: - left_tabs.append(p) + # TODO: fixme for network tracks - else: - for p in ready_tabs: - if p < self.prime_tab: - left_tabs.append(p) + for i, item in enumerate(pctl.force_queue): - for p in ready_tabs: - if p > self.prime_tab: - right_tabs.append(p) - left_tabs.reverse() + 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 - run = max_w + 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 - 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 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") - 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 +def toggle_lyrics_show(a): + return not gui.combo_mode - # 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 +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 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) + if gui.combo_mode: + colour = colours.menu_text_disabled - x += 17 * gui.scale - x_start = x + return [colour, colours.menu_background, line] - 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 toggle_lyrics_panel_position_deco(): + colour = colours.menu_text + if prefs.lyric_metadata_panel_top: + line = _("Panel Below Lyrics") + else: + line = _("Panel Above Lyrics") - xx = x + (max_w - run) # + round(6 * gui.scale) - self.tabs_left_x = x_start + if gui.combo_mode or not prefs.show_side_lyrics_art_panel: + colour = colours.menu_text_disabled - 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])) + return [colour, colours.menu_background, line] - 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 +def toggle_lyrics_panel_position(): + prefs.lyric_metadata_panel_top ^= True - 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() +def lyrics_in_side_show(track_object: TrackClass): + if gui.combo_mode or not prefs.show_lyrics_side: + return False + return True - # TAB INPUT PROCESSING - target = pctl.multi_playlist - if gui.radio_view: - target = pctl.radio_playlists - for i, tab in enumerate(target): +def toggle_side_art(): + prefs.show_side_lyrics_art_panel ^= True - 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 i not in show_tabs: - continue + if prefs.side_panel_layout == 1: # and prefs.show_side_art: - # Determine the tab width - tab_width = self.tab_text_spaces[i] + self.tab_extra_width + 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] - # Save the far right boundary of the tabs (hacky) - self.tabs_right_x = x + tab_width + 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] - # 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) +def toggle_lyrics(track_object: TrackClass): + if not track_object: + return - # Tab functions - if tab_hit: - if not gui.radio_view: - # Double click to play - if mouse_up and pl_to_id(i) == self.tab_d_click_ref == pl_to_id(pctl.active_playlist_viewing) and \ - self.tab_d_click_timer.get() < 0.25 and point_distance( - last_click_location, mouse_up_position) < 5 * gui.scale: + if 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: - 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) + # 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 - # 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() + 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") - # Drag to move playlist - if mouse_up and playlist_box.drag and coll_point(mouse_up_position, f_rect): +def get_lyric_fire(track_object: TrackClass, silent: bool = False) -> str | None: + lyrics_ren.lyrics_position = 0 - if gui.radio_view: - move_radio_playlist(playlist_box.drag_on, i) - else: - if playlist_box.drag_source == 1: - pctl.multi_playlist[playlist_box.drag_on].hidden = False + if not prefs.lyrics_enables: + if not silent: + show_message( + _("There are no lyric sources enabled."), + _("See 'lyrics settings' under 'functions' tab in settings."), mode="info") + return None - if i != playlist_box.drag_on: + 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.")) - # # Reveal the tab in case it has been hidden - # pctl.multi_playlist[playlist_box.drag_on].hidden = False + if t < -65: + show_message(_("Stop requesting lyrics AAAAAA."), mode="error") - 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) + # If the user keeps pressing, lets mess with them haha + lyrics_fetch_timer.force_set(t - 5) - playlist_box.drag = False - gui.update += 1 + return "later" - # Delete playlist on wheel click - elif tab_menu.active is False and middle_click: - # delete_playlist(i) - delete_playlist_ask(i) - break + if t > 0: + lyrics_fetch_timer.set() + t = 0 - # 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 + lyrics_fetch_timer.force_set(t - 10) - # 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 + if not silent: + show_message(_("Searching...")) - 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 + s_artist = track_object.artist + s_title = track_object.title - 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 s_artist in prefs.lyrics_subs: + s_artist = prefs.lyrics_subs[s_artist] + if s_title in prefs.lyrics_subs: + s_title = prefs.lyrics_subs[s_title] - if mouse_up and radio_view.drag: - pctl.radio_playlists[i]["items"].append(radio_view.drag) - toast(_("Added station to: ") + pctl.radio_playlists[i]["name"]) + logging.info(f"Searching for lyrics: {s_artist} - {s_title}") - radio_view.drag = None + found = False + for name in prefs.lyrics_enables: - x += tab_width + self.tab_spacing + if name in lyric_sources.keys(): + func = lyric_sources[name] - # Test dupelicate tab function - if playlist_box.drag: - rect = (0, x, self.height, window_size[0]) - 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") - 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) + if not found: + logging.error(f"Could not find lyrics from source {name}") - else: - if playlist_box.drag_source == 1: - pctl.multi_playlist[playlist_box.drag_on].hidden = False + 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() - move_playlist(playlist_box.drag_on, i) - playlist_box.drag = 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 - # Need to test length again - # Need to test length - self.tab_text_spaces = [] + shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object])) + shoot_dl.daemon = True + shoot_dl.start() - 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) + logging.info("..Done") - # Reset X draw position - x = x_start - bar_highlight_size = round(2 * gui.scale) +def get_lyric_wiki_silent(track_object: TrackClass): + logging.info("Searching for lyrics...") - # TAB DRAWING - shown = [] - for i, tab in enumerate(target): + if track_object.artist == "" or track_object.title == "": + return - if not gui.radio_view: - if not prefs.tabs_on_top or prefs.shuffle_lock: - break + shoot_dl = threading.Thread(target=get_lyric_fire, args=([track_object, True])) + shoot_dl.daemon = True + shoot_dl.start() - if len(pctl.multi_playlist) != len(self.tab_text_spaces): - break + logging.info("..Done") - # if tab.hidden is True: - # continue +def test_auto_lyrics(track_object: TrackClass): + if not track_object: + return - if i not in show_tabs: - continue + 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) - # if window_size[0] - x - (self.tab_text_spaces[i] + self.tab_extra_width) < right_space_es: - # break +def get_bio(track_object: TrackClass): + if track_object.artist != "": + lastfm.get_bio(track_object.artist) - shown.append(i) +def search_lyrics_deco(track_object: TrackClass): + if not track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - tab_width = self.tab_text_spaces[i] + self.tab_extra_width - rect = [x, y, tab_width, self.height] + return [line_colour, colours.menu_background, None] - # 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 toggle_synced_lyrics(tr): + prefs.prefer_synced_lyrics ^= True - # 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 +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") - # Draw tab background - ddt.rect(rect, bg) - if playing_hint: - ddt.rect(rect, [255, 255, 255, 7]) + return [line_colour, colours.menu_background, text] - # Determine text colour - if active: - fg = colours.tab_text_active - else: - fg = colours.tab_text +def paste_lyrics_deco(): + if SDL_HasClipboardText(): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # 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) + return [line_colour, colours.menu_background, None] - # 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 +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") - # Drag to move playlist - if tab_hit: - if mouse_down and i != playlist_box.drag_on and playlist_box.drag is True: +def chord_lyrics_paste_show_test(_) -> bool: + return gui.combo_mode and prefs.guitar_chords - 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]) +def copy_lyrics_deco(track_object: TrackClass): + if track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - 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]) + return [line_colour, colours.menu_background, None] - 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 +def copy_lyrics(track_object: TrackClass): + copy_to_clipboard(track_object.lyrics) - 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 clear_lyrics(track_object: TrackClass): + track_object.lyrics = "" - x += tab_width + self.tab_spacing +def clear_lyrics_deco(track_object: TrackClass): + if track_object.lyrics: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # 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]) + return [line_colour, colours.menu_background, None] - if mouse_up: - drop_tracks_to_new_playlist(shift_selection) +def split_lyrics(track_object: TrackClass): + if track_object.lyrics != "": + track_object.lyrics = track_object.lyrics.replace(". ", ". \n") + else: + pass - # 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]) +def show_sub_search(track_object: TrackClass): + sub_lyrics_box.activate(track_object) - if prefs.tabs_on_top and right_overflow: - x += 24 * gui.scale - self.tabs_right_x += 24 * 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 - # ------------- - # Other input - if mouse_up: - quick_drag = False - playlist_box.drag = False - radio_view.drag = None +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 - # 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: + if save_embed_img_disable_test(track_object): + show_message(_("Saving network images not implemented")) + return - cycle_playlist_pinned(mouse_wheel) + try: + pic = album_art_gen.get_embed(track_object) - 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 + if not pic: + show_message(_("Image save error."), _("No embedded album art found file."), mode="warning") + return - x += self.ini_menu_space - y += 7 * gui.scale - ddt.text_background_colour = colours.top_panel_background + source_image = io.BytesIO(pic) + im = Image.open(source_image) - # MENU ----------------------------- + source_image.close() - 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) + ext = "." + im.format.lower() + if im.format == "JPEG": + ext = ".jpg" - 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) + target = os.path.join(folder, "embed-" + str(im.height) + "px-" + str(track_object.index) + ext) - 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 len(pic) > 30: + with open(target, "wb") as w: + w.write(pic) - # 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)) - # + open_folder(track_object.index) - dl = len(dl_mon.ready) - watching = len(dl_mon.watching) + except Exception: + logging.exception("Unknown error trying to save an image") + show_message(_("Image save error."), _("A mysterious error occurred"), mode="error") - 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) +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 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 info is None: + return [colours.menu_text_disabled, colours.menu_background, None] - 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 + line_colour = colours.menu_text - load_orders.append(copy.deepcopy(load_order)) + return [line_colour, colours.menu_background, None] - if len(dl_mon.ready) > 0: - dl_mon.ready.clear() - switch_playlist(pln) +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 - 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") +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) +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) - 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] + if info is None: + return [colours.menu_text_disabled, colours.menu_background, None] - 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] + if info[0] == 1: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # LAYOUT -------------------------------- - x += self.menu_space + word_length + return [line_colour, colours.menu_background, None] - self.drag_zone_start_x = x - 5 * gui.scale - status = True +def cycle_image_deco(track_object: TrackClass): + info = album_art_gen.get_info(track_object) - if loading_in_progress: + 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 - 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] + return [line_colour, colours.menu_background, None] + +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) - 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 + if info is not None and info[1] > 1: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - text = _("{N} listening along").format(N=len(tauon.listen_alongers)) - bg = [40, 190, 235, 255] - else: - status = False + return [line_colour, colours.menu_background, None] - if status: - x += ddt.text((x, y), text, bg, 311) - # x += ddt.get_text_w(text, 11) - # TODO: list listening 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)) +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) - w = 100 * gui.scale - x += ddt.text((x, y), _("Transcoding"), bg, 311) + 8 * 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 gui.transcoding_batch_total: +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] - # 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] +def download_art1(tr): + if tr.is_network: + show_message(_("Cannot download art for network tracks.")) + return - c1 = [40, 40, 40, 255] - c2 = [100, 59, 200, 200] - c3 = [150, 70, 200, 255] + # Determine noise of folder ---------------- + siblings = [] + parent = tr.parent_folder_path - if colours.lm: - c1 = [100, 100, 100, 255] - c2 = [170, 140, 255, 255] - c3 = [230, 170, 255, 255] + 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) - 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) + album_tags = [] + date_tags = [] - done = round(gui.transcoding_bach_done / gui.transcoding_batch_total * 100) - doing = round(core_use / gui.transcoding_batch_total * 100) + for tr in siblings: + album_tags.append(tr.album) + date_tags.append(tr.date) - ddt.rect([x, yy, done, h], c3) - ddt.rect([x + done, yy, doing, h], c2) + album_tags = set(album_tags) + date_tags = set(date_tags) - x += w + 8 * 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 - if gui.sync_progress: - text = gui.sync_progress - else: - text = _("{N} Folder Remaining {T}").format(N=str(len(transcode_list)), T=transcode_state) - if len(transcode_list) > 1: - text = _("{N} Folders Remaining {T}").format(N=str(len(transcode_list)), T=transcode_state) + # ------------------------------------------- - x += ddt.text((x, y), text, bg, 311) + 8 * gui.scale + if not os.path.isdir(tr.parent_folder_path): + show_message(_("Directory missing.")) + return + try: + show_message(_("Looking up MusicBrainz ID...")) - 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) + if "musicbrainz_releasegroupid" not in tr.misc or "musicbrainz_artistids" not in tr.misc or not tr.misc[ + "musicbrainz_artistids"]: -class BottomBarType1: - def __init__(self): + logging.info("MusicBrainz ID lookup...") - self.mode = 0 + artist = tr.album_artist + if not tr.album: + return + if not artist: + artist = tr.artist - self.seek_time = 0 + s = musicbrainzngs.search_release_groups(tr.album, artist=artist, limit=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 + album_id = s["release-group-list"][0]["id"] + artist_id = s["release-group-list"][0]["artist-credit"][0]["artist"]["id"] - 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] + logging.info("Found release group ID: " + album_id) + logging.info("Found artist ID: " + artist_id) - 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) + else: - self.buffer_shard = asset_loader(scaled_asset_directory, loaded_asset_dc, "shard.png", True) + album_id = tr.misc["musicbrainz_releasegroupid"] + artist_id = tr.misc["musicbrainz_artistids"][0] - self.scrob_stick = 0 + logging.info("Using tagged release group ID: " + album_id) + logging.info("Using tagged artist ID: " + artist_id) - def update(self): + if prefs.enable_fanart_cover: + try: + show_message(_("Searching fanart.tv for cover art...")) - 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 + r = requests.get("https://webservice.fanart.tv/v3/music/albums/" \ + + artist_id + "?api_key=" + prefs.fatvap, timeout=(4, 10)) - seek_bar_x = 300 * gui.scale - if window_size[0] < 600 * gui.scale: - seek_bar_x = 250 * gui.scale + artlink = r.json()["albums"][album_id]["albumcover"][0]["url"] + id = r.json()["albums"][album_id]["albumcover"][0]["id"] - self.seek_bar_size[0] = window_size[0] - seek_bar_x - self.seek_bar_position[0] = seek_bar_x + response = urllib.request.urlopen(artlink, context=tls_context) + info = response.info() - # if gui.bb_show_art: - # self.seek_bar_position[0] = 300 + gui.panelBY - # self.seek_bar_size[0] = window_size[0] - 300 - gui.panelBY + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + t.seek(0, 2) + l = t.tell() + t.seek(0) - # self.seek_bar_position[0] = 0 - # self.seek_bar_size[0] = window_size[0] + if info.get_content_maintype() == "image" and l > 1000: - def render(self): + if info.get_content_subtype() == "jpeg": + filepath = os.path.join(tr.parent_folder_path, "cover-" + id + ".jpg") + elif info.get_content_subtype() == "png": + filepath = os.path.join(tr.parent_folder_path, "cover-" + id + ".png") + else: + show_message(_("Could not detect downloaded filetype."), mode="error") + return - global volume_store - global clicked - global right_click + f = open(filepath, "wb") + f.write(t.read()) + f.close() - ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) + show_message(_("Cover art downloaded from fanart.tv"), mode="done") + # clear_img_cache() + for track_id in default_playlist: + if tr.parent_folder_path == pctl.get_track(track_id).parent_folder_path: + clear_track_image_cache(pctl.get_track(track_id)) + return + except Exception: + logging.exception("Failed to get from fanart.tv") - ddt.rect_a(self.seek_bar_position, self.seek_bar_size, colours.seek_bar_background) + 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() - right_offset = 0 - if gui.display_time_mode >= 2: - right_offset = 22 * gui.scale + show_message(_("Cover art downloaded from MusicBrainz"), mode="done") + # clear_img_cache() + clear_track_image_cache(tr) - if window_size[0] < 670 * gui.scale: - right_offset -= 90 * gui.scale - # Scrobble marker + 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 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 + return - 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("Matching cover art or ID could not be found.") + show_message(_("Matching cover art or ID could not be found.")) - 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 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 - # # 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 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() - # ddt.rect_r(rect, [255, 255, 255, 20]) +def remove_embed_picture(track_object: TrackClass, dry: bool = True) -> int | None: + """Return amount of removed objects or None""" + index = track_object.index - # SEEK BAR------------------ - if pctl.playing_time < 1: - self.seek_time = 0 + 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) - 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() + removed = 0 + if not dry: + pr = pctl.stop(True) + try: + for item in tracks: - fields.add(self.seek_bar_position + self.seek_bar_size) - if coll(self.seek_bar_position + self.seek_bar_size): + tr = pctl.get_track(item) - if middle_click and pctl.playing_state > 0: - gui.seek_cur_show = True + if tr.is_cue: + continue - clicked = True - if mouse_wheel != 0: - pctl.seek_time(pctl.playing_time + (mouse_wheel * 3)) + if tr.is_network: + continue - if gui.seek_cur_show: - gui.update += 1 + if dry: + removed += 1 + else: + if tr.file_ext == "MP3": + try: + tag = mutagen.id3.ID3(tr.fullpath) + tag.delall("APIC") + remove = True + tag.save(padding=no_padding) + removed += 1 + except Exception: + logging.exception("No MP3 APIC found") - # 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) + 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") - bargetX = mouse_position[0] - bargetX = min(bargetX, self.seek_bar_position[0] + self.seek_bar_size[0]) - bargetX = max(bargetX, self.seek_bar_position[0]) - bargetX -= self.seek_bar_position[0] - seek = bargetX / self.seek_bar_size[0] - gui.cur_time = get_display_time(pctl.playing_object().length * seek) + if 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 self.seek_down is True: - if mouse_position[0] == 0: - self.seek_down = False - self.seek_hit = 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") - 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: + clear_track_image_cache(tr) - self.volume_hit = False - self.seek_down = False - self.seek_hit = False + except Exception: + logging.exception("Image remove error") + show_message(_("Image remove error"), mode="error") + return None - 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] + if dry: + return removed - pctl.seek_decimal(seek) - #logging.info(seek) + 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() - self.seek_time = pctl.playing_time +def delete_file_image(track_object: TrackClass): + try: + showc = album_art_gen.get_info(track_object) + if showc is not None and showc[0] == 0: + source = album_art_gen.get_sources(track_object)[showc[2]][1] + os.remove(source) + # clear_img_cache() + clear_track_image_cache(track_object) + logging.info("Deleted file: " + source) + except Exception: + logging.exception("Failed to delete file") + show_message(_("Something went wrong"), mode="error") - if 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 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) - # 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] + text = _("Delete Image File") + line_colour = colours.menu_text - self.buffer_shard.render(x + offset, y, colour) - x += self.buffer_shard.w + if info is None or track_object.is_network: + return [colours.menu_text_disabled, colours.menu_background, 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) + if info and info[0] == 0: + text = _("Delete Image File") - if pctl.playing_length > 0: + elif info and info[0] == 1: + if pctl.playing_state > 0 and track_object.file_ext in ("MP3", "FLAC", "M4A"): + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - if pctl.download_time != 0: + text = _("Delete Embedded | Folder") + if key_shift_down or key_shiftr_down: + text = _("Delete Embedded | Track") - if pctl.download_time == -1: - pctl.download_time = pctl.playing_length + return [line_colour, colours.menu_background, text] - colour = (255, 255, 255, 10) - if gui.theme_name == "Lavender Light" or gui.theme_name == "Carbon": - colour = (255, 255, 255, 40) +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") + +def toggle_gimage(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_gimage + prefs.show_gimage ^= True + return None + +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 + + return [line_colour, colours.menu_background, None] + +def ser_gimage(track_object: TrackClass): + if track_object.artist and track_object.album: + line = "https://www.google.com/search?tbm=isch&q=" + urllib.parse.quote( + track_object.artist + " " + track_object.album) + webbrowser.open(line, new=2, autoraise=True) + +def append_here(): + global cargo + global default_playlist + default_playlist += cargo + +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") + + if active: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled + + return [line_colour, colours.menu_background, 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 lightning_move_test(discard): + return gui.lightning_copy and prefs.show_transfer - 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 copy_deco(): +# line = "Copy" +# if key_shift_down: +# line = "Copy" #Folder From Library" +# else: +# line = "Copy" +# return [colours.menu_text, colours.menu_background, line] - if gui.seek_cur_show: +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 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 re_template_word(word, tr): + if word == "aa" or word == "albumartist": - ddt.rect( - [mouse_position[0], self.seek_bar_position[1], 2, self.seek_bar_size[1]], - [100, 100, 20, 255]) + if tr.album_artist: + return tr.album_artist + return tr.artist - else: - gui.seek_cur_show = False + if word == "a" or word == "artist": + return tr.artist - 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): + if word == "t" or word == "title": + return tr.title - 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 + 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) - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + if word == "sn" or word == "singlenumber" or word == "singletracknumber" or word == "s": + return str(tr.track_number) - # 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 word == "d" or word == "date" or word == "year": + return str(tr.date) - 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 word == "b" or "album" in word: + return str(tr.album) - 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 + if word == "g" or word == "genre": + return tr.genre - step = round(1 * gui.scale) - min_h = round(4 * gui.scale) - spacing = round(5 * gui.scale) + if word == "x" or "ext" in word or "file" in word: + return tr.file_ext.lower() - 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() + if word == "ux" or "upper" in word: + return tr.file_ext.upper() - for bar in range(8): + if word == "c" or "composer" in word: + return tr.composer - 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 "comment" in word: + return tr.comment.replace("\n", "").replace("\r", "") - if coll(h_rect): - if mouse_down or mouse_up: - gui.update_on_drag = True + 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 +def parse_template2(string: str, track_object: TrackClass, strict: bool = False): + temp = "" + out = "" - pctl.set_volume() + mode = 0 - colour = colours.mode_button_off + for c in string: - 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 mode == 0: - ddt.rect(rect, colour) - x += spacing + if c == "<": + mode = 1 + else: + out += c - # Volume Bar -------------------------------------------------------- else: - if (inp.mouse_click and coll(( - self.volume_bar_position[0] - right_offset, self.volume_bar_position[1], self.volume_bar_size[0], - self.volume_bar_size[1] + 4))) or \ - self.volume_bar_being_dragged is True: - clicked = True - if inp.mouse_click is True or self.volume_bar_being_dragged is True: - gui.update = 2 + if c == ">": - 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 + test = re_template_word(temp, track_object) + if strict: + assert test + out += test - time.sleep(0.02) + mode = 0 + temp = "" - if mouse_down is False: - self.volume_bar_being_dragged = False - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume(True) + else: - if mouse_down: - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume(False) + temp += c - if right_click and coll(( - self.volume_bar_position[0] - 15 * gui.scale, self.volume_bar_position[1] - 10 * gui.scale, - self.volume_bar_size[0] + 30 * gui.scale, - self.volume_bar_size[1] + 20 * gui.scale)): + if "<und" in string: + out = out.replace(" ", "_") - if pctl.player_volume > 0: - volume_store = pctl.player_volume - pctl.player_volume = 0 - else: - pctl.player_volume = volume_store + return parse_template(out, track_object, strict=strict) - pctl.set_volume() +def parse_template(string, track_object: TrackClass, up_ext: bool = False, strict: bool = False): + set = 0 + underscore = False + output = "" - ddt.rect_a( - (self.volume_bar_position[0] - right_offset, self.volume_bar_position[1]), - self.volume_bar_size, colours.volume_bar_background) # 22 + 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 - 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]) + output = output.rstrip(" -").lstrip(" -") - ddt.rect(gui.volume_bar_rect, colours.volume_bar_fill) + if underscore: + output = output.replace(" ", "_") - 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): + # Attempt to ensure the output text is filename safe + output = filename_safe(output) - 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) + return output - gui.show_bottom_title = gui.showed_title ^ True - if not prefs.hide_bottom_title: - gui.show_bottom_title = True +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] - if gui.show_bottom_title and pctl.playing_state > 0 and window_size[0] > 820 * gui.scale: - line = pctl.title_text() + 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 = 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 + rename_playlist_box.y = min(rename_playlist_box.y, round(350 * gui.scale)) - # 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 rename_playlist_box.y < gui.panelY: + rename_playlist_box.y = gui.panelY + 10 * gui.scale - if (inp.mouse_click or right_click) and coll(( - self.seek_bar_position[0] - 10 * gui.scale, self.seek_bar_position[1] + 20 * gui.scale, - window_size[0] - 710 * gui.scale, 30 * gui.scale)): - # if pctl.playing_state == 3: - # copy_to_clipboard(pctl.tag_meta) - # show_message("Copied text to clipboard") - # if input.mouse_click or right_click: - # input.mouse_click = False - # right_click = False - # else: - if inp.mouse_click and pctl.playing_state != 3: - pctl.show_current() + if gui.radio_view: + rename_text_area.set_text(pctl.radio_playlists[index]["name"]) + else: + rename_text_area.set_text(pctl.multi_playlist[index].title) + rename_text_area.highlight_all() + gui.gen_code_errors = False - if pctl.playing_ready() and not gui.fullscreen: + if generator: + rename_playlist_box.toggle_edit_gen() - if right_click: - mode_menu.activate() +def edit_generator_box(index: int) -> None: + rename_playlist(index, generator=True) - if d_click_timer.get() < 0.3 and inp.mouse_click: - set_mini_mode() - gui.update += 1 - return - d_click_timer.set() +def pin_playlist_toggle(pl: int) -> None: + pctl.multi_playlist[pl].hidden ^= True - # TIME---------------------- +def pl_pin_deco(pl: int): + # if pctl.multi_playlist[pl].hidden == True and tab_menu.pos[1] > - x = window_size[0] - 57 * gui.scale - y = window_size[1] - 29 * gui.scale + if pctl.multi_playlist[pl].hidden == True: + return [colours.menu_text, colours.menu_background, _("Pin")] + return [colours.menu_text, colours.menu_background, _("Unpin")] - 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 +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 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 view_pl_is_locked(_) -> bool: + return pctl.multi_playlist[pctl.active_playlist_viewing].locked - # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) +def pl_is_locked(pl: int) -> bool: + if not pctl.multi_playlist: + return False + return pctl.multi_playlist[pl].locked - 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 lock_playlist_toggle(pl: int) -> None: + pctl.multi_playlist[pl].locked ^= True - offset1 = 10 * gui.scale +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 system == "Windows": - offset1 += 2 * gui.scale +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 - offset2 = offset1 + 7 * gui.scale + 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") - 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) + 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 - elif gui.display_time_mode == 3: + 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() - # colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + 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 - track = pctl.playing_object() - if track and track.index != gui.dtm3_index: +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 - 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 + if not direc: + direc = str(user_directory / "playlists") + if not os.path.exists(direc): + os.makedirs(direc) - x -= 4 - text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) + target = os.path.join(direc, pctl.multi_playlist[pl].title + ".xspf") - ddt.text( - (x - 25 * gui.scale, y), text_time, colours.time_playing, - fonts.bottom_panel_time) + xspf_root = ET.Element("playlist", version="1", xmlns="http://xspf.org/ns/0/") + xspf_tracklist_tag = ET.SubElement(xspf_root, "trackList") - offset1 = 10 * gui.scale - if system == "Windows": - offset1 += 2 * gui.scale - offset2 = offset1 + 7 * gui.scale + 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) - 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) + 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) + + ET.SubElement(xspf_track_tag, "duration").text = str(int(track.length * 1000)) - # BUTTONS - # bottom buttons + xspf_tree = ET.ElementTree(xspf_root) + ET.indent(xspf_tree, space=' ', level=0) + xspf_tree.write(target, encoding='UTF-8', xml_declaration=True) - if gui.mode == 1: + 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]) - # PLAY--- - buttons_x_offset = 0 - compact = False - if window_size[0] < 650 * gui.scale: - compact = True + return target - 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 reload(): + if album_mode: + reload_albums(quiet=True) - if pctl.playing_state == 1: - play_colour = colours.media_buttons_active + # tree_view_box.clear_all() + # elif gui.combo_mode: + # reload_albums(quiet=True) + # combo_pl_render.prep() - if pctl.auto_stop: - stop_colour = colours.media_buttons_active +def clear_playlist(index: int): + global default_playlist - 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 pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental erasure")) + return - 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")) + pctl.multi_playlist[index].last_folder.clear() # clear import folder list # TODO(Martin): This was actually a string not a list wth? - if right_click: - pctl.show_current(highlight=True) + if not pctl.multi_playlist[index].playlist_ids: + logging.info("Playlist is already empty") + 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) + li = [] + for i, ref in enumerate(pctl.multi_playlist[index].playlist_ids): + li.append((i, ref)) - # PAUSE--- - if compact: - buttons_x_offset = -46 * gui.scale + undo.bk_tracks(index, list(reversed(li))) - x = (75 * gui.scale) + buttons_x_offset - y = window_size[1] - self.control_line_bottom + del pctl.multi_playlist[index].playlist_ids[:] + if pctl.active_playlist_viewing == index: + default_playlist = pctl.multi_playlist[index].playlist_ids + reload() - if not compact or (compact and pctl.playing_state == 1): + # pctl.playlist_playing = 0 + pctl.multi_playlist[index].position = 0 + if index == pctl.active_playlist_viewing: + pctl.playlist_view_position = 0 - 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")) + gui.pl_update = 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) +def convert_playlist(pl: int, get_list: bool = False) -> list[list[int]]| None: + global transcode_list - # 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 tauon.test_ffmpeg(): + 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) + paths: list[str] = [] + folders: list[list[int]] = [] - if compact: - buttons_x_offset -= 5 * gui.scale + 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) - # 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 + 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.forward_button.render( - buttons_x_offset + 240 * gui.scale, 1 + window_size[1] - self.control_line_bottom, forward_colour) + folders.append(folder) - # ddt.rect_r(rect,[255,0,0,255], True) + if get_list: + return folders - # 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 + transcode_list.extend(folders) - 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) +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 - # menu button +def test_pl_tab_locked(pl: int) -> bool: + if gui.radio_view: + return False + return pctl.multi_playlist[pl].locked - 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 +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") - 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 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] - if self.mode == 0 and window_size[0] > 530 * gui.scale: + temp = pctl.multi_playlist[source] + pctl.multi_playlist[source] = "old" + pctl.multi_playlist.insert(dest, temp) + pctl.multi_playlist.remove("old") - # shuffle button - x = window_size[0] - 318 * gui.scale - right_offset - y = window_size[1] - 27 * gui.scale + 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") - rect = (x - 5 * gui.scale, y - 5 * gui.scale, 60 * gui.scale, 25 * gui.scale) - fields.add(rect) +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 - rpbc = colours.mode_button_off - off = True - if (inp.mouse_click or right_click) and coll(rect): + global default_playlist - 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 check_lock and pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental deletion")) + return - 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 + if not force: + if pl_is_locked(index): + show_message(_("Playlist is locked to prevent accidental deletion")) + return - # Keep hover highlight on if menu is open - if shuffle_menu.active and not pctl.random_mode: - rpbc = colours.mode_button_over + if gui.rename_playlist_box: + return - #self.shuffle_button.render(x + round(1 * gui.scale), y + round(1 * gui.scale), rpbc) + # Set screen to be redrawn + gui.pl_update = 1 + gui.update += 1 - #y += round(3 * gui.scale) - #ddt.rect_a((x, y), (25 * gui.scale, 3 * gui.scale), rpbc) + # 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 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 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 - #ddt.rect_a((x + 25 * gui.scale, y), (23 * gui.scale, 3 * gui.scale), rpbc) + # Take note of the id of the playing playlist + old_playing_id = pctl.multi_playlist[pctl.active_playlist_playing].uuid_int - #y += round(5 * gui.scale) - #ddt.rect_a((x, y), (48 * gui.scale, 3 * gui.scale), rpbc) + # Take note of the id of the viewed open playlist + old_view_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - # REPEAT - x = window_size[0] - round(380 * gui.scale) - right_offset - y = window_size[1] - round(27 * gui.scale) + # Delete the requested playlist + del pctl.multi_playlist[index] - rpbc = colours.mode_button_off - off = True + # Re-set the open viewed playlist number by uid + for i, pl in enumerate(pctl.multi_playlist): - rect = (x - 6 * gui.scale, y - 5 * gui.scale, 61 * gui.scale, 25 * gui.scale) - fields.add(rect) - if (inp.mouse_click or right_click) and coll(rect): + if 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 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 + # Re-initiate the now viewed playlist + if old_view_id != pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int: + default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position + logging.debug("Position reset by playlist delete") + pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected + shift_selection = [pctl.selected_in_playlist] - if 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): + if album_mode: + reload_albums(True) + goto_album(pctl.playlist_view_position) - # 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")) + # Re-set the playing playlist number by uid + for i, pl in enumerate(pctl.multi_playlist): - 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 + 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 - # Keep hover highlight on if menu is open - if repeat_menu.active and not pctl.repeat_mode: - rpbc = colours.mode_button_over + test_show_add_home_music() - rpbc = alpha_blend(rpbc, colours.bottom_panel_colour) # bake in alpha in case of overlap + # Cleanup + ids = [] + for p in pctl.multi_playlist: + ids.append(p.uuid_int) - y += round(3 * gui.scale) - w = round(3 * gui.scale) - y = round(y) - x = round(x) + 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] - ar = x + round(50 * gui.scale) - h = round(5 * gui.scale) + pctl.db_inc += 1 - 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 delete_playlist_force(index: int): + delete_playlist(index, force=True, check_lock=True) - # 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) +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) -class BottomBarType_ao1: - def __init__(self): +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 - self.mode = 0 + 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") - self.seek_time = 0 +def rescan_tags(pl: int) -> None: + for track in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[track].is_cue is False: + to_scan.append(track) + tauon.thread_manager.ready("worker") - 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 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)) - 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 re_import2(pl: int) -> None: + paths = pctl.multi_playlist[pl].last_folder + + reduce_paths(paths) - 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) + 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)) - self.scrob_stick = 0 + if paths: + show_message(_("Rescanning folders..."), mode="info") - def update(self): +def rescan_all_folders(): + for i, p in enumerate(pctl.multi_playlist): + re_import2(i) - 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 s_append(index: int): + paste(playlist_no=index) - seek_bar_x = 300 * gui.scale - if window_size[0] < 600 * gui.scale: - seek_bar_x = 250 * gui.scale +def append_playlist(index: int): + global cargo + pctl.multi_playlist[index].playlist_ids += cargo - self.seek_bar_size[0] = window_size[0] - seek_bar_x - self.seek_bar_position[0] = seek_bar_x + gui.pl_update = 1 + reload() - # 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 index_key(index: int): + tr = pctl.master_library[index] + s = str(tr.track_number) + d = str(tr.disc_number) - # self.seek_bar_position[0] = 0 - # self.seek_bar_size[0] = window_size[0] + if "/" in d: + d = d.split("/")[0] - def render(self): + # 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 = "" - global volume_store - global clicked - global right_click - ddt.rect_a((0, window_size[1] - gui.panelBY), (window_size[0], gui.panelBY), colours.bottom_panel_colour) + # 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 - right_offset = 0 - if gui.display_time_mode >= 2: - right_offset = 22 * gui.scale + # 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 window_size[0] < 670 * gui.scale: - right_offset -= 90 * gui.scale + if (not tr.disc_number or tr.disc_number == "0") and tr.is_cue: + s = tr.filename + "-" + s - # # 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])) + # 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" - # ddt.rect_r(rect, [255, 255, 255, 20]) +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 - # 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): + 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) - 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 + 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 = int(pctl.player_volume) - pctl.set_volume() + gui.pl_update += 1 - # 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() +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 - # Volume Bar 2 ------------------------------------------------ - if True: - x = window_size[0] - right_offset - 120 * gui.scale - y = window_size[1] - round(21 * gui.scale) + 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 gui.compact_bar: - x -= 90 * gui.scale + 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) - 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 + gui.pl_update += 1 - 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 +def key_filepath(index: int): + track = pctl.master_library[index] + return track.parent_folder_path.lower(), track.filename - step = round(1 * gui.scale) - min_h = round(4 * gui.scale) - spacing = round(5 * gui.scale) +def key_fullpath(index: int): + return pctl.master_library[index].fullpath - 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 +def key_filename(index: int): + track = pctl.master_library[index] + return track.filename - pctl.set_volume() +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 - for bar in range(8): + if use_natsort and False: + target[:] = natsort.os_sorted(target, key=key_fullpath) + else: + target.sort(key=key_filepath) - 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 append_current_playing(index: int): + if tauon.spot_ctl.coasting: + tauon.spot_ctl.append_playing(index) + gui.pl_update = 1 + return - if coll(h_rect): - if mouse_down: - gui.update_on_drag = True + 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 - 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 export_stats(pl: int) -> None: + playlist_time = 0 + play_time = 0 + total_size = 0 + tracks_in_playlist = len(pctl.multi_playlist[pl].playlist_ids) - pctl.set_volume() + seen_files = {} + seen_types = {} - colour = colours.mode_button_off + mp3_bitrates = {} + ogg_bitrates = {} + m4a_bitrates = {} - 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 + are_cue = 0 - ddt.rect(rect, colour) - x += spacing + for index in pctl.multi_playlist[pl].playlist_ids: + track = pctl.get_track(index) - # TIME---------------------- + playlist_time += int(track.length) + play_time += star_store.get(index) - x = window_size[0] - 57 * gui.scale - y = window_size[1] - 35 * gui.scale + if track.is_cue: + are_cue += 1 - 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 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 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: + type = track.file_ext + if type == "OGA": + type = "OGG" + seen_types[type] = seen_types.get(type, 0) + 1 - colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + 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 - 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) + total_size = sum(seen_files.values()) - offset1 = 10 * gui.scale + 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 system == "Windows": - offset1 += 2 * gui.scale + 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" - offset2 = offset1 + 7 * 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) - 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) + line += str(perc) + "%" + line += "\n\n" - elif gui.display_time_mode == 3: + 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}%); " - colours.time_sub = alpha_blend([255, 255, 255, 80], colours.bottom_panel_colour) + if others: + others = round(others, 1) + if others < 0.1: + others = "<0.1" + line += _("Others") + f"({others}%);" + line = line.rstrip("; ") + line += "\n\n" - track = pctl.playing_object() - if track and track.index != gui.dtm3_index: + if tracks_in_playlist and ogg_bitrates: + line += _("OGG bitrates (kbps):") + "\n" + rates = sorted(ogg_bitrates, key=ogg_bitrates.get, reverse=True) + others = 0 + for rate in rates: + perc = round((ogg_bitrates.get(rate) / sum(ogg_bitrates.values())) * 100, 1) + if perc < 1: + others += perc + else: + line += f"{rate} ({perc}%); " - gui.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 + if others: + others = round(others, 1) + if others < 0.1: + others = "<0.1" + line += _("Others") + f"({others}%);" + line = line.rstrip("; ") + line += "\n\n" - x -= 4 - text_time = get_display_time(gui.dtm3_cum + pctl.playing_time) + # 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" - ddt.text((x - 25 * gui.scale, y), text_time, colours.time_playing, fonts.bottom_panel_time) + line += "\n" + f"-------------- {_('Top Artists')} --------------------" + "\n\n" - offset1 = 10 * gui.scale - if system == "Windows": - offset1 += 2 * gui.scale - offset2 = offset1 + 7 * gui.scale + ls = stats_gen.artist_list + for i, item in enumerate(ls[:50]): + line += str(i + 1) + ".\t" + stt2(item[1]) + "\t" + item[0] + "\n" - 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) + 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" - # BUTTONS - # bottom buttons + 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 gui.mode == 1: +def imported_sort(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - # PLAY--- - buttons_x_offset = 0 - compact = False - if window_size[0] < 650 * gui.scale: - compact = True + og = pctl.multi_playlist[pl].playlist_ids + og.sort(key=lambda x: pctl.get_track(x).index) - 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 + reload_albums() + tree_view_box.clear_target_pl(pl) - if pctl.playing_state == 1: - play_colour = colours.media_buttons_active +def imported_sort_folders(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + return - if pctl.auto_stop: - stop_colour = colours.media_buttons_active + og = pctl.multi_playlist[pl].playlist_ids + og.sort(key=lambda x: pctl.get_track(x).index) - 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] + 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]) - 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")) + reload_albums() + tree_view_box.clear_target_pl(pl) - if right_click: - pctl.show_current(highlight=True) +def standard_sort(pl: int) -> None: + if pl_is_locked(pl): + show_message(_("Playlist is locked")) + 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) + sort_path_pl(pl) + sort_track_2(pl) + reload_albums() + tree_view_box.clear_target_pl(pl) - # PAUSE--- - if compact: - buttons_x_offset = -46 * gui.scale +def year_s(plt): + sorted_temp = sorted(plt, key=lambda x: x[1]) + temp = [] - x = (75 * gui.scale) + buttons_x_offset - y = window_size[1] - self.control_line_bottom + for album in sorted_temp: + temp += album[0] + return temp - if not compact or (compact and pctl.playing_state == 2): +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 = "" - 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")) + p = 0 + while p < len(playlist): - # 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) + track = get_object(playlist[p]) - # 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")) + 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: - 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) + artist = track.artist + pl2 += year_s(plt) + plt = [] -class MiniMode: - def __init__(self): - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) + if track.album_artist: + album_artist = track.album_artist - 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 p > len(playlist) - 1: + break - self.shuffle_fade_timer = Timer(100) - self.repeat_fade_timer = Timer(100) + album = [] + on = get_object(playlist[p]).parent_folder_path + album.append(playlist[p]) + t = 1 - 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 + while t + p < len(playlist) - 1 and get_object(playlist[p + t]).parent_folder_path == on: + album.append(playlist[p + t]) + t += 1 - w = window_size[0] - h = window_size[1] + date = get_object(playlist[p]).date - y1 = w - if w == h: - y1 -= 79 * 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:] - h1 = h - y1 + # 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] - # Draw background - bg = colours.mini_mode_background - # bg = [250, 250, 250, 255] + plt.append((album, date, artist + " " + get_object(playlist[p]).album)) + p += len(album) + #logging.info(album) - ddt.rect((0, 0, w, h), bg) - ddt.text_background_colour = bg + if plt: + pl2 += year_s(plt) + plt = [] - detect_mouse_rect = (3, 3, w - 6, h - 6) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) + if custom_list is not None: + return pl2 - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() + # 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) - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() +def pl_toggle_playlist_break(ref): + pctl.multi_playlist[ref].hide_title ^= 1 + gui.pl_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 +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 - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + return title - track = pctl.playing_object() +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 - 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) + title = gen_unique_pl_title(_("New Playlist")) - ddt.rect((0, 0, w, w), (0, 0, 0, 45)) - if track is not None: + top_panel.prime_side = 1 + top_panel.prime_tab = len(pctl.multi_playlist) - # Render album art - album_art_gen.display(track, (0, 0), (w, w)) + 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 - line1c = colours.mini_mode_text_1 - line2c = colours.mini_mode_text_2 +def append_deco(): + if pctl.playing_state > 0: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - 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] + text = None + if tauon.spot_ctl.coasting: + text = _("Add 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) + return [line_colour, colours.menu_background, text] - 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() +def rescan_deco(pl: int): + if pctl.multi_playlist[pl].last_folder: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # Draw title texts - line1 = track.artist - line2 = track.title + # base = os.path.basename(pctl.multi_playlist[pl].last_folder) - # Calculate seek bar position - seek_w = int(w * 0.70) + return [line_colour, colours.menu_background, None] - 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 regenerate_deco(pl: int): + id = pl_to_id(pl) + value = pctl.gen_codes.get(id) - if w != h or mouse_in_area: + if value: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - 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: + return [line_colour, colours.menu_background, None] - ddt.text((w // 2, y1 + 10 * gui.scale, 2), line1, line2c, 514, window_size[0] - 30 * 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 - ddt.text((w // 2, y1 + 31 * gui.scale, 2), line2, line1c, 414, window_size[0] - 30 * gui.scale) + current += cha - # Test click to seek - if mouse_up and coll(seek_r_hit): + if inquote and cha != "\"": + q_string += cha - 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 current: + cmds.append(current) + quotes.append(q_string) - if click_x < 6 * gui.scale: - click_x = 0 - seek = click_x / seek_r[2] + return cmds, quotes, inquote - pctl.seek_decimal(seek) +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 - # Draw progress bar background - ddt.rect(seek_r, [255, 255, 255, 32]) + urls = [] + playlist = pctl.multi_playlist[pl].playlist_ids - # 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 + 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 pctl.playing_state != 1: - seek_colour = [210, 40, 100, 255] + if warn: + show_message(_("Playlist contains non-Spotify tracks"), mode="error") + return - seek_r[2] = progress_w + 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 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 +def regenerate_playlist(pl: int = -1, silent: bool = False, id: int | None = None) -> None: + if id is None and pl == -1: + return - ddt.rect(seek_r, seek_colour) + if id is None: + id = pl_to_id(pl) - 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) + if pl == -1: + pl = id_to_pl(id) + if pl is None: + return - fields.add(left_area) - fields.add(right_area) + source_playlist = pctl.multi_playlist[pl].playlist_ids - 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]) + string = pctl.gen_codes.get(id) + if not string: + if not silent: + show_message(_("This playlist has no generator")) + return - 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]) + cmds, quotes, inquote = parse_generator(string) - # Shuffle + if inquote: + gui.gen_code_errors = "close" + return - 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) + playlist = [] + selections = [] + errors = False + selections_searched = 0 - 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] + def is_source_type(code: str | None) -> bool: + return \ + code is None or \ + code == "" or \ + code.startswith(("self", "jelly", "plex", "koel", "tau", "air", "sal")) - sx = seek_r[0] + seek_w + 12 * gui.scale - sy = seek_r[1] - 2 * gui.scale - self.shuffle.render(sx, sy, colour) + #logging.info(cmds) + #logging.info(quotes) + pctl.regen_in_progress = True - # 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) + for i, cm in enumerate(cmds): - 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] + quote = quotes[i] + if cm.startswith("\"") and (cm.endswith((">", "<"))): + cm_found = False - sx = seek_r[0] - 36 * gui.scale - sy = seek_r[1] - 1 * gui.scale - self.repeat.render(sx, sy, colour) + for col in column_names: + if quote.lower() == col.lower() or _(quote).lower() == col.lower(): + cm_found = True - # sx = seek_r[0] - 39 * gui.scale - # sy = seek_r[1] - 1 * gui.scale + 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 - #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) + elif cm == "self": + selections.append(pctl.multi_playlist[pl].playlist_ids) + elif cm == "auto": + pass - # 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() + elif cm.startswith("spl\""): + playlist.extend(tauon.spot_ctl.playlist(quote, return_list=True)) - # 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() + elif cm.startswith("tpl\""): + playlist.extend(tauon.tidal.playlist(quote, return_list=True)) - 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) + elif cm == "tfa": + playlist.extend(tauon.tidal.fav_albums(return_list=True)) -class MiniMode2: + elif cm == "tft": + playlist.extend(tauon.tidal.fav_tracks(return_list=True)) - def __init__(self): + elif cm.startswith("tar\""): + playlist.extend(tauon.tidal.artist(quote, return_list=True)) - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) + elif cm.startswith("tmix\""): + playlist.extend(tauon.tidal.mix(quote, return_list=True)) - 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) + elif cm == "sal": + playlist.extend(tauon.spot_ctl.get_library_albums(return_list=True)) - def render(self): + elif cm == "slt": + playlist.extend(tauon.spot_ctl.get_library_likes(return_list=True)) - w = window_size[0] - h = window_size[1] + elif cm == "plex": + if not plex.scanning: + playlist.extend(plex.get_albums(return_list=True)) - x1 = h + elif cm.startswith("jelly\""): + if not jellyfin.scanning: + playlist.extend(jellyfin.get_playlist(quote, return_list=True)) - # Draw background - ddt.rect((0, 0, w, h), colours.mini_mode_background) - ddt.text_background_colour = colours.mini_mode_background + elif cm == "jelly": + if not jellyfin.scanning: + playlist.extend(jellyfin.ingest_library(return_list=True)) - detect_mouse_rect = (2, 2, w - 4, h - 4) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) + elif cm == "koel": + if not koel.scanning: + playlist.extend(koel.get_albums(return_list=True)) - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() + elif cm == "tau": + if not tau.processing: + playlist.extend(tau.get_playlist(pctl.multi_playlist[pl].title, return_list=True)) - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() + elif cm == "air": + if not subsonic.scanning: + playlist.extend(subsonic.get_music3(return_list=True)) - 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 + 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) - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + temp = [] + for selection in selections: + temp += selection - track = pctl.playing_object() + playlist += list(OrderedDict.fromkeys(temp)) + selections.clear() - if track is not None: + elif cm == "cue": - # Render album art - album_art_gen.display(track, (0, 0), (h, h)) + 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)) - text_hit_area = (x1, 0, w, h) + 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)) - 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() + 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)) - # Draw title texts - line1 = track.artist - line2 = track.title + elif cm.startswith("ext"): + value = quote.upper() + if value: + if not selections: + for plist in pctl.multi_playlist: + selections.append(plist.playlist_ids) - if not line1 and not line2: + temp = [] + for selection in selections: + for track in selection: + tr = pctl.get_track(track) + if tr.file_ext == value: + temp.append(track) - ddt.text( - (x1 + 15 * gui.scale, 44 * gui.scale), track.filename, colours.grey(150), 315, - window_size[0] - x1 - 30 * gui.scale) - else: + playlist += list(OrderedDict.fromkeys(temp)) - # 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: + elif cm == "ypa": + playlist = year_sort(0, playlist) - ddt.text( - (x1 + 15 * gui.scale, 18 * gui.scale), line2, colours.grey(249), 514, - window_size[0] - x1 - 30 * gui.scale) + elif cm == "tn": + sort_track_2(0, playlist) - ddt.text( - (x1 + 15 * gui.scale, 43 * gui.scale), line1, colours.grey(110), 514, - window_size[0] - x1 - 30 * gui.scale) + elif cm == "ia>": + playlist = gen_last_imported_folders(0, 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 - fields.add(tool_rect) - if coll(tool_rect): - draw_window_tools() + elif cm == "ia<": + playlist = gen_last_imported_folders(0, playlist, reverse=True) - # Seek bar - bg_rect = (h, h - round(5 * gui.scale), w - h, round(5 * gui.scale)) - ddt.rect(bg_rect, [255, 255, 255, 18]) + elif cm == "m>": + playlist = gen_last_modified(0, playlist) - if pctl.playing_state > 0: + elif cm == "m<": + playlist = gen_last_modified(0, playlist, reverse=False) - hit_rect = h - 5 * gui.scale, h - 12 * gui.scale, w - h + 5 * gui.scale, 13 * gui.scale + elif cm == "ly" or cm == "lyrics": + playlist = gen_lyrics(0, playlist) - if coll(hit_rect) and mouse_up: - p = (mouse_position[0] - h) / (w - h) + elif cm == "l" or cm == "love" or cm == "loved": + playlist = gen_love(0, playlist) - 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) + elif cm == "clr": + selections.clear() - 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) + elif cm == "rv" or cm == "reverse": + playlist = gen_reverse(0, playlist) -class MiniMode3: + elif cm == "rva": + playlist = gen_folder_reverse(0, playlist) - def __init__(self): + elif cm == "rata>": - self.save_position = None - self.was_borderless = True - self.volume_timer = Timer() - self.volume_timer.force_set(100) + playlist = gen_folder_top_rating(0, custom_list=playlist) - 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) + elif cm == "rat>": - self.shuffle_fade_timer = Timer(100) - self.repeat_fade_timer = Timer(100) + def rat_key(track_id): + return star_store.get_rating(track_id) - 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 + playlist = sorted(playlist, key=rat_key, reverse=True) - w = window_size[0] - h = window_size[1] + elif cm == "rat<": - y1 = w #+ 10 * gui.scale - # if w == h: - # y1 -= 79 * gui.scale + def rat_key(track_id): + return star_store.get_rating(track_id) - h1 = h - y1 + playlist = sorted(playlist, key=rat_key) - # Draw background - bg = colours.mini_mode_background - bg = [0, 0, 0, 0] - # bg = [250, 250, 250, 255] + elif cm[:4] == "rat=": + value = cm[4:] + try: + value = float(value) * 2 + temp = [] + for item in playlist: + if value == star_store.get_rating(item): + temp.append(item) + playlist = temp + except Exception: + logging.exception("Failed to get rating") + errors = True - ddt.rect((0, 0, w, h), bg) + 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 - style_overlay.display() + 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 - 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 + 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 - detect_mouse_rect = (3, 3, w - 6, h - 6) - fields.add(detect_mouse_rect) - mouse_in = coll(detect_mouse_rect) + elif cm == "norat": + temp = [] + for item in playlist: + if star_store.get_rating(item) == 0: + temp.append(item) + playlist = temp - # Play / Pause when right clicking below art - if right_click: # and mouse_position[1] > y1: - pctl.play_pause() + elif cm == "d>": + playlist = gen_sort_len(0, custom_list=playlist) - # Volume change on scroll - if mouse_wheel != 0: - self.volume_timer.set() + elif cm == "d<": + playlist = gen_sort_len(0, custom_list=playlist) + playlist = list(reversed(playlist)) - 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 + 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] - pctl.player_volume = int(pctl.player_volume) - pctl.set_volume() + 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] - track = pctl.playing_object() + elif cm == "path": + sort_path_pl(0, custom_list=playlist) - 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) + elif cm == "pa>": + playlist = gen_folder_top(0, custom_list=playlist) - #ddt.rect((0, 0, w, w), (0, 0, 0, 45)) - if track is not None: + elif cm == "pa<": + playlist = gen_folder_top(0, custom_list=playlist) + playlist = gen_folder_reverse(0, playlist) - # Render album art + elif cm == "pt>" or cm == "pc>": + playlist = gen_top_100(0, custom_list=playlist) - wid = (w // 2) + round(60 * gui.scale) - ins = (window_size[0] - wid) / 2 - off = round(4 * gui.scale) + elif cm == "pt<" or cm == "pc<": + playlist = gen_top_100(0, custom_list=playlist) + playlist = list(reversed(playlist)) - 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)) + 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] - line1c = [255, 255, 255, 255] #colours.mini_mode_text_1 - line2c = [255, 255, 255, 255] #colours.mini_mode_text_2 + 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 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] + 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] - # Double click bottom text to return to full window - text_hit_area = (60 * gui.scale, y1 + 4, 230 * gui.scale, 50 * gui.scale) + elif cm[:3] == "pc<": + value = cm[3:] + if value and value.isdigit(): + value = int(value) + for i in reversed(range(len(playlist))): + t_time = star_store.get(playlist[i]) + tr = pctl.get_track(playlist[i]) + if tr.length > 0: + if not value > t_time / tr.length: + del playlist[i] - if 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() + elif cm == "y<": + playlist = gen_sort_date(0, False, playlist) - # 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: + elif cm == "y>": + playlist = gen_sort_date(0, True, playlist) - 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) + 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 - y1 += round(10 * gui.scale) + 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 - # Calculate seek bar position - seek_w = int(w * 0.80) + 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 - 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] + 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 w != h or mouse_in_area: + 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 + elif cm == "st" or cm == "rt" or cm == "r": + random.shuffle(playlist) - # Test click to seek - if mouse_up and coll(seek_r_hit): + elif cm == "sf" or cm == "rf" or cm == "ra" or cm == "sa": + playlist = gen_folder_shuffle(0, custom_list=playlist) - 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] + elif cm.startswith("n"): + value = cm[1:] + if value.isdigit(): + playlist = playlist[:int(value)] - if click_x < 6 * gui.scale: - click_x = 0 - seek = click_x / seek_r[2] + # SEARCH FOLDER + elif cm.startswith("p\"") and len(cm) > 3: - pctl.seek_decimal(seek) + 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) - # Draw progress bar background - ddt.rect(seek_r, [255, 255, 255, 32]) + 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) - # 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 + found_name = "" - if pctl.playing_state != 1: - seek_colour = [210, 40, 100, 255] + for result in search_over.results: + if result[0] == 5: + found_name = result[1] + break + else: + logging.info("No folder search result found") + continue - seek_r[2] = progress_w + search_over.clear() - ddt.rect(seek_r, seek_colour) + playlist += search_over.click_meta(found_name, get_list=True, search_lists=selections) + # SEARCH GENRE + elif (cm.startswith(('g"', 'gm"', 'g="'))) and len(cm) > 3: + 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) - 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] + g_search = quote.lower().replace("-", "") # .replace(" ", "") - # 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] + 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) - if click_x < 6 * gui.scale: - click_x = 0 - volume = click_x / volume_r[2] + found_name = "" - pctl.player_volume = int(volume * 100) - pctl.set_volume() + 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 - ddt.rect(volume_r, [255, 255, 255, 32]) + if not found_name: + logging.warning("No genre search result found") + continue - #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 + search_over.clear() - ddt.rect(volume_r, volume_colour) + playlist += search_over.click_genre(found_name, get_list=True, search_lists=selections) + # 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) - 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) + 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) - fields.add(left_area) - fields.add(right_area) + found_name = "" - 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]) + for result in search_over.results: + if result[0] == 0: + found_name = result[1] + break + else: + logging.warning("No artist search result found") + continue - 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]) + 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) - # 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) + elif cm.startswith("ff\""): - 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] + 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() - sx = volume_r[0] + volume_w + 12 * gui.scale - sy = volume_r[1] - 3 * gui.scale - mini_mode.shuffle.render(sx, sy, colour) + if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + line = str(unidecode(line)) - # - # 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) + if not search_magic(quote.lower(), line): + del playlist[i] - 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] + playlist = list(OrderedDict.fromkeys(playlist)) - sx = volume_r[0] - 39 * gui.scale - sy = volume_r[1] - 1 * gui.scale - mini_mode.repeat.render(sx, sy, colour) + elif cm.startswith("fx\""): - # 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) + 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)) - # 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 search_magic(quote.lower(), line): + del playlist[i] - search_over.render() + elif cm.startswith(('find"', 'f"', 'fs"')): - # Show exit/min buttons when mosue over - tool_rect = [window_size[0] - 110 * gui.scale, 2, 108 * gui.scale, 45 * gui.scale] - if prefs.left_window_control: - tool_rect[0] = 0 - fields.add(tool_rect) - if coll(tool_rect): - draw_window_tools() + if 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) + cooldown = 0 + dones = {} + for selection in selections: + for track_id in selection: + if track_id not in dones: + tr = pctl.get_track(track_id) - # if 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 + 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) -def set_mini_mode(): - if gui.fullscreen: - return + else: + line = " ".join([tr.title, tr.artist, tr.album, tr.fullpath, tr.composer, tr.comment, tr.album_artist]).lower() - global mouse_down - global mouse_up - global old_window_position - mouse_down = False - mouse_up = False - inp.mouse_click = False + # if prefs.diacritic_search and all([ord(c) < 128 for c in quote]): + # line = str(unidecode(line)) - if gui.maximized: - SDL_RestoreWindow(t_window) - update_layout_do() + if search_magic(quote.lower(), line): + playlist.append(track_id) - if gui.mode < 3: - old_window_position = get_window_position() + cooldown += 1 + if cooldown > 300: + time.sleep(0.005) + cooldown = 0 - if prefs.mini_mode_on_top: - SDL_SetWindowAlwaysOnTop(t_window, True) + dones[track_id] = None - gui.mode = 3 - gui.vis = 0 - gui.turbo = False - gui.draw_vis4_top = False - gui.level_update = False + playlist = list(OrderedDict.fromkeys(playlist)) - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetWindowPosition(t_window, i_x, i_y) - gui.save_position = (i_x.contents.value, i_y.contents.value) - mini_mode.was_borderless = draw_border - SDL_SetWindowBordered(t_window, False) + 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 - size = (350, 429) - if prefs.mini_mode_mode == 1: - size = (330, 330) - if prefs.mini_mode_mode == 2: - size = (420, 499) - if prefs.mini_mode_mode == 3: - size = (430, 430) - if prefs.mini_mode_mode == 4: - size = (330, 80) - if prefs.mini_mode_mode == 5: - size = (350, 545) - style_overlay.flush() - tauon.thread_manager.ready("style") + if cm.startswith("s\""): + selections_searched += 1 + selections.append(target) + elif cm.startswith("px\""): + playlist[:] = [x for x in playlist if x not in target] - if logical_size == window_size: - size = (int(size[0] * gui.scale), int(size[1] * gui.scale)) + else: + errors = True - logical_size[0] = size[0] - logical_size[1] = size[1] + gui.gen_code_errors = errors + if not playlist and not errors: + gui.gen_code_errors = "empty" - SDL_SetWindowMinimumSize(t_window, 100, 100) + if gui.rename_playlist_box and (not playlist or cmds.count("a") > 1): + pass + else: + source_playlist[:] = playlist[:] - SDL_SetWindowResizable(t_window, False) - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + tree_view_box.clear_target_pl(0, id) + pctl.regen_in_progress = False + gui.pl_update = 1 + reload() + pctl.notify_change() - if mini_mode.save_position: - SDL_SetWindowPosition(t_window, mini_mode.save_position[0], mini_mode.save_position[1]) + #logging.info(cmds) - i_x = pointer(c_int(0)) - i_y = pointer(c_int(0)) - SDL_GL_GetDrawableSize(t_window, i_x, i_y) - window_size[0] = i_x.contents.value - window_size[1] = i_y.contents.value +def make_auto_sorting(pl: int) -> None: + pctl.gen_codes[pl_to_id(pl)] = "self a path tn ypa auto" + show_message( + _("OK. This playlist will automatically sort on import from now on"), + _("You remove or edit this behavior by going \"Misc...\" > \"Edit generator...\""), mode="done") - gui.update += 3 +def spotify_show_test(_): + return prefs.spot_mode -def restore_full_mode(): - logging.info("RESTORE FULL") - i_y = pointer(c_int(0)) - i_x = pointer(c_int(0)) - SDL_GetWindowPosition(t_window, i_x, i_y) - mini_mode.save_position = [i_x.contents.value, i_y.contents.value] +def jellyfin_show_test(_): + return prefs.jelly_password and prefs.jelly_username - if not mini_mode.was_borderless: - SDL_SetWindowBordered(t_window, True) +def upload_jellyfin_playlist(pl: TauonPlaylist) -> None: + if jellyfin.scanning: + return + shooter(jellyfin.upload_playlist, [pl]) - logical_size[0] = gui.save_size[0] - logical_size[1] = gui.save_size[1] +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() - SDL_SetWindowPosition(t_window, gui.save_position[0], gui.save_position[1]) +def forget_pl_import_folder(pl: int) -> None: + pctl.multi_playlist[pl].last_folder = [] +def remove_duplicates(pl: int) -> None: + playlist = [] - SDL_SetWindowResizable(t_window, True) - SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - SDL_SetWindowAlwaysOnTop(t_window, False) + for item in pctl.multi_playlist[pl].playlist_ids: + if item not in playlist: + playlist.append(item) - # if macos: - # SDL_SetWindowMinimumSize(t_window, 560, 330) - # else: - SDL_SetWindowMinimumSize(t_window, 560, 330) + 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") - restore_ignore_timer.set() # Hacky + pctl.multi_playlist[pl].playlist_ids[:] = playlist[:] - gui.mode = 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")) - global mouse_down - global mouse_up - mouse_down = False - mouse_up = False - inp.mouse_click = False +def auto_get_sync_targets(): + search_paths = [ + "/run/user/*/gvfs/*/*/[Mm]usic", + "/run/media/*/*/[Mm]usic"] + result_paths = [] + for item in search_paths: + result_paths.extend(glob.glob(item)) + return result_paths - if gui.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 +def auto_sync_thread(pl: int) -> None: + if prefs.transcode_inplace: + show_message(_("Cannot sync when in transcode inplace mode")) + return - #logging.info(window_size) + # Find target path + gui.sync_progress = "Starting Sync..." + gui.update += 1 - 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 + path = Path(sync_target.text.strip().rstrip("/").rstrip("\\").replace("\n", "").replace("\r", "")) + logging.debug(f"sync_path: {path}") + if not path: + show_message(_("No target folder selected")) + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + return + if not path.is_dir(): + show_message(_("Target folder could not be found")) + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + return - gui.update_layout() - if prefs.art_bg: - tauon.thread_manager.ready("style") + prefs.sync_target = str(path) -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 + # Get list of folder names on device + logging.info("Getting folder list from device...") + d_folder_names = path.iterdir() + logging.info("Got list") - 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 + # Get list of folders we want + folders = convert_playlist(pl, get_list=True) + folder_names: list[str] = [] + folder_dict = {} - 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 gui.stop_sync: + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 - artistoffset = 0 - indexLine = "" + # 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 - offset_font_extra = 0 - if gui.row_font_size > 14: - offset_font_extra = 8 + # ------ + # 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) - # In windows (arial?) draws numbers too high (hack fix) - num_y_offset = 0 - # if system == 'Windows': - # num_y_offset = 1 + # ------- + # 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 True or style == 1: + 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 not gui.rsp and not gui.combo_mode: - # width -= 10 * gui.scale + free_space = shutil.disk_usage(path)[2] / 8 / 100000000 # in GB + if free_space < 0.6: + show_message(_("Sync aborted! Low disk space on target device"), mode="warning") + break - dash = False - if n_track.artist and colours.artist_text == colours.title_text: - dash = True + if prefs.bypass_transcode or (prefs.smart_bypass and 0 < pctl.get_track(folder_dict[item][0]).bitrate <= 128): + logging.info("Smart bypass...") - if n_track.title: + 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 - line = track_number_process(n_track.track_number) + (path / item).mkdir() + encode_done = source_parent + else: + show_message(_("One or more folders is missing")) + continue - indexLine = line + else: - 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 += " " + 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") - line = "" + if encode_done.exists(): - if n_track.artist != "" and not dash: - line0 = n_track.artist + if (path / item).exists(): + show_message( + _("Sync warning"), _("One or more folders to sync has the same name. Skipping."), mode="warning") + continue - artistoffset = ddt.text( - (start_x + 27 * gui.scale, y), - line0, - alpha_mod(artistc, album_fade), - gui.row_font_size, - int(width / 2)) + (path / item).mkdir() - line = n_track.title - else: - line += n_track.title - else: - line = \ - os.path.splitext(n_track.filename)[ - 0] + for file in encode_done.iterdir(): + file = file.name + logging.info(f"Copy file {file} to {path / item}…") + # gui.sync_progress += "." + gui.update += 1 - if p_track >= len(default_playlist): - gui.pl_update += 1 - return + 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") - index = default_playlist[p_track] - star_x = 0 - total = star_store.get(index) + 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" - if gui.star_mode == "line" and total > 0 and pctl.master_library[index].length > 0: + logging.info("Finished copying folder") - 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 + gui.sync_speed = 0 + gui.sync_progress = "" + gui.stop_sync = False + gui.update += 1 + show_message(_("Sync completed"), mode="done") - lh = 1 - if gui.scale != 1: - lh = 2 +def auto_sync(pl: int) -> None: + shoot_dl = threading.Thread(target=auto_sync_thread, args=([pl])) + shoot_dl.daemon = True + shoot_dl.start() - colour = colours.star_line - if this_line_playing and colours.star_line_playing is not None: - colour = colours.star_line_playing +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) - 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)) +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] - star_x += 6 * gui.scale +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 gui.show_ratings: - sx = round(width + start_x - round(40 * gui.scale) - offset_font_extra) - sy = round(ry + (gui.playlist_row_height // 2) - round(7 * gui.scale)) - sx -= round(68 * gui.scale) +def set_podcast_playlist(pl: int) -> None: + pctl.multi_playlist[pl].persist_time_positioning ^= True - draw_rating_widget(sx, sy, n_track) +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] - star_x += round(70 * 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] - if gui.star_mode == "star" and total > 0 and pctl.master_library[index].length != 0: - sx = width + start_x - 40 * gui.scale - offset_font_extra - sy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) - # if gui.scale == 1.25: - # sy += 1 - playtime_stars = star_count(total, pctl.master_library[index].length) - 1 +def csv_string(item): + item = str(item) + item.replace("\"", "\"\"") + return f"\"{item}\"" - sx2 = sx - selected_star = -2 - rated_star = -1 +def export_playlist_albums(pl: int) -> None: + p = pctl.multi_playlist[pl] + name = p.title + playlist = p.playlist_ids - # if key_ctrl_down: + 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) - c = 60 - d = 6 + playtimes[last_folder] = playtimes.get(last_folder, 0) + int(star_store.get(id)) - colour = [70, 70, 70, 255] - if colours.lm: - colour = [90, 90, 90, 255] - # colour = alpha_mod(indexc, album_fade) + filename = f"{user_directory}/{name}.csv" + xport = open(filename, "w") - for count in range(8): + xport.write("Album name;Artist;Release date;Genre;Rating;Playtime;Folder path") - if selected_star < count and playtime_stars < count and rated_star < count: - break + for id in albums: + track = pctl.get_track(id) + artist = track.album_artist + if not artist: + artist = track.artist - 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) + 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)) - # 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) + xport.close() + show_message(_("Export complete."), _("Saved as: ") + filename, mode="done") - # if selected_star > -2: - # if selected_star >= count: - # colour = (220, 200, 60, 255) - # else: - # if rated_star >= count: - # colour = (220, 200, 60, 255) +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)) - star_pc_icon.render(sx, sy, colour) +def key_rating(index: int): + return star_store.get_rating(index) - if gui.show_hearts: +def key_scrobbles(index: int): + return pctl.get_track(index).lfm_scrobbles - xxx = star_x +def key_disc(index: int): + return pctl.get_track(index).disc_number - count = 0 - spacing = 6 * gui.scale +def key_cue(index: int): + return pctl.get_track(index).is_cue - yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) - if gui.scale == 1.25: - yy += 1 - if xxx > 0: - xxx += 3 * gui.scale +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 love(False, index): - count = 1 +def add_pl_tag(text): + return f" <{text}>" - x = width + start_x - 52 * gui.scale - offset_font_extra - xxx +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) - f_store.store(display_you_heart, (x, yy)) + if custom_list is not None: + return playlist - star_x += 18 * gui.scale + 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)) - if "spotify-liked" in pctl.master_library[index].misc: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rat>" - x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx +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) - f_store.store(display_spot_heart, (x, yy)) + if custom_list is not None: + return playlist - star_x += heart_row_icon.w + spacing + 2 + 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)) - for name in pctl.master_library[index].lfm_friend_likes: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>" - # Limit to number of hears to display - if gui.star_mode == "none": - if count > 6: - break - elif count > 4: - break +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 - x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx + if len(source) < 3: + return [] - f_store.store(display_friend_heart, (x, yy, name)) + 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)) - count += 1 + 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 - star_x += heart_row_icon.w + spacing + 2 + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - # Draw track number/index - display_queue = False + sets = sorted(sets, key=best, reverse=True) - if pctl.force_queue: + playlist = [] - 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) + for se in sets: + playlist += se - if marks: - display_queue = True + # 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 - if display_queue: + 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)) - 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" + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a pa>" - # 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] +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 album_type: - # li += "🠗" + if len(source) < 3: + return [] - colour = [244, 200, 66, 255] - if colours.lm: - colour = [220, 40, 40, 255] + 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.text( - (start_x + 5 * gui.scale, y, 2), - li, colour, gui.row_font_size + 200 - 1) + def best(folder): + return album_star_store.get_rating(pctl.get_track(folder[0])) - elif len(indexLine) > 2: + if get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - ddt.text( - (start_x + 5 * gui.scale, y, 2), indexLine, - alpha_mod(indexc, album_fade), gui.row_font_size) - else: + sets = sorted(sets, key=best, reverse=True) - ddt.text( - (start_x, y), indexLine, - alpha_mod(indexc, album_fade), gui.row_font_size) + playlist = [] - if dash and n_track.artist and n_track.title: - line = n_track.artist + " - " + n_track.title + for se in sets: + playlist += se - 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) + if custom_list is not None: + return playlist - line = get_display_time(n_track.length) + 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)) - 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) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a rata>" - f_store.recall_all() +def gen_lyrics(pl: int, custom_list=None): + playlist = [] -class StandardPlaylist: - def __init__(self): - pass + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - def full_render(self): + for item in source: + if pctl.master_library[item].lyrics != "": + playlist.append(item) - global highlight_left - global highlight_right + if custom_list is not None: + return playlist - global playlist_hold - global playlist_hold_position - global shift_selection + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("Tracks with lyrics"), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - global click_time - global quick_drag - global mouse_down - global mouse_up - global selection_stage + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" - global r_menu_index - global r_menu_position + else: + show_message(_("No tracks with lyrics were found.")) - left = gui.playlist_left - width = gui.plw +def gen_incomplete(pl: int, custom_list=None) -> list | None: + 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 + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - w = 0 - gui.row_extra = 0 - cv = 0 # update gui.playlist_current_visible_tracks + albums = {} + nums = {} + for id in source: + track = pctl.get_track(id) + if track.album and track.track_number: - # Draw the background - SDL_SetRenderTarget(renderer, gui.tracklist_texture) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) + if type(track.track_number) is str and not track.track_number.isdigit(): + continue - rect = (left, gui.panelY, width, window_size[1]) - ddt.rect(rect, colours.playlist_panel_background) + if track.album not in albums: + albums[track.album] = [] + nums[track.album] = [] - # 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 track not in albums[track.album]: + albums[track.album].append(track) + nums[track.album].append(int(track.track_number)) - # 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: + 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) - # Set scroll speed - mx = 4 + if custom_list is not None: + return playlist - if gui.playlist_view_length < 25: - mx = 3 - if gui.playlist_view_length < 10: - mx = 2 - pctl.playlist_view_position -= mouse_wheel * mx + 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)) - if gui.playlist_view_length > 40: - pctl.playlist_view_position -= mouse_wheel + # pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pl].title + "\" a ly" + else: + show_message(_("No incomplete albums were found.")) + return None - #if mouse_wheel: - #logging.debug("Position changed by mouse wheel scroll: " + str(mouse_wheel)) +def gen_codec_pl(codec): + playlist = [] - 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() + 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) - scroll_hide_timer.set() - gui.frame_callback_list.append(TestTimer(0.9)) + if len(playlist) > 0: + pctl.multi_playlist.append( + pl_gen( + title=_("Codec: ") + codec, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - # Show notice if playlist empty - if len(default_playlist) == 0: - colour = alpha_mod(colours.index_text, 200) # colours.playlist_text_missing +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 - top_a = gui.panelY - if gui.artist_info_panel: - top_a += gui.artist_panel_height + a_cache = {} - b = window_size[1] - top_a - gui.panelBY - half = int(top_a + (b * 0.60)) + def key_import(index: int): - 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 + track = pctl.master_library[index] + cached = a_cache.get((track.album, track.parent_folder_name)) + if cached is not None: + return cached - 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) + if track.album: + a_cache[(track.album, track.parent_folder_name)] = index + return index - ddt.pretty_rect = None - ddt.alpha_bg = False + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=key_import, reverse=reverse) + sort_track_2(0, playlist) - # Show notice if at end of playlist - elif pctl.playlist_view_position > len(default_playlist) - 1: - colour = alpha_mod(colours.index_text, 200) + if custom_list is not None: + return playlist - top_a = gui.panelY - if gui.artist_info_panel: - top_a += gui.artist_panel_height +def gen_last_modified(index, custom_list=None, reverse=True): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - b = window_size[1] - top_a - gui.panelBY - half = int(top_a + (b * 0.17)) + a_cache = {} - 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 + def key_modified(index: int): - ddt.text( - (left + int(width / 2) + 10 * gui.scale, half, 2), _("End of Playlist"), - colour, 213) + track = pctl.master_library[index] + cached = a_cache.get((track.album, track.parent_folder_name)) + if cached is not None: + return cached - ddt.pretty_rect = None - ddt.alpha_bg = False + if track.album: + a_cache[(track.album, track.parent_folder_name)] = pctl.master_library[index].modified_time + return pctl.master_library[index].modified_time - # 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) + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=key_modified, reverse=reverse) + sort_track_2(0, playlist) - # Process Input + if custom_list is not None: + return 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=pctl.multi_playlist[index].title + add_pl_tag(_("File Modified")), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - for i in range(gui.playlist_view_length + 1): + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a m>" - track_position = i + pctl.playlist_view_position +def gen_love(pl: int, custom_list=None): + playlist = [] - # Make sure the view position is valid - pctl.playlist_view_position = max(pctl.playlist_view_position, 0) + source = custom_list + if source is None: + source = pctl.multi_playlist[pl].playlist_ids - # Break if we are at end of playlist - if len(default_playlist) <= track_position or number > gui.playlist_view_length: - break + for item in source: + if get_love_index(item): + playlist.append(item) - track_object = pctl.get_track(default_playlist[track_position]) - track_id = track_object.index - move_on_title = False + playlist.sort(key=lambda x: get_love_timestamp_index(x), reverse=True) - line_y = gui.playlist_top + gui.playlist_row_height * number + if custom_list is not None: + return playlist - track_box = ( - left + highlight_left, line_y, highlight_width, - gui.playlist_row_height - 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=_("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.")) - input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) +def gen_comment(pl: int) -> None: + playlist = [] - # 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 + 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) - highlight = False - drag_highlight = False + 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.")) - # Shift selection highlight - if (track_position in shift_selection and len(shift_selection) > 1): - highlight = True +def gen_replay(pl: int) -> None: + playlist = [] - # Tracks have been dropped? - if playlist_hold is True and coll(input_box): - if mouse_up: - move_on_title = True + for item in pctl.multi_playlist[pl].playlist_ids: + if pctl.master_library[item].misc.get("replaygain_track_gain"): + playlist.append(item) - # 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 + 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.")) - # Detect folder title click - if click_title and mouse_position[1] < window_size[1] - gui.panelBY: +def gen_sort_len(index: int, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].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 + def length(index: int) -> int: - 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 + if pctl.master_library[index].length < 1: + return 0 + return int(pctl.master_library[index].length) - # 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 + playlist = copy.deepcopy(source) + playlist = sorted(playlist, key=length, reverse=True) - if album_mode: - goto_album(pctl.playlist_playing_position) + if custom_list is not None: + return playlist - # 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 + # pctl.multi_playlist.append( + # [pctl.multi_playlist[index].title + " <Duration Sorted>", 0, copy.deepcopy(playlist), 0, 1, 0]) - 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 + 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)) - # 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: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a d>" - quick_drag = True - set_drag_source() +def gen_folder_duration(pl: int, get_sets: bool = False): + if len(pctl.multi_playlist[pl].playlist_ids) < 3: + return None - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True + 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)) - selection_stage = 1 - temp = get_folder_tracks_local(track_position) - pctl.selected_in_playlist = track_position + def best(folder): + total_duration = 0 + for item in folder: + total_duration += pctl.master_library[item].length + return total_duration - 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 get_sets: + r = [] + for item in sets: + r.append((item, best(item))) + return r - else: - shift_selection = copy.copy(temp) + sets = sorted(sets, key=best, reverse=True) + playlist = [] - # Should draw drag highlight? + for se in sets: + playlist += se - if mouse_down and playlist_hold and coll(input_box) and track_position not in shift_selection: + 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)) - if len(shift_selection) < 2 and not key_shift_down: - pass - else: - drag_highlight = True +def gen_sort_date(index: int, rev: bool = False, custom_list=None): + def g_date(index: int): - # Something to do with quick search, I forgot - if pctl.selected_in_playlist > track_position + 1: - gui.row_extra += 1 + if pctl.master_library[index].date != "": + return str(pctl.master_library[index].date) + return "z" - list_items.append( - (1, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, False)) - number += 1 + playlist = [] + lowest = 0 + highest = 0 + first = True - if number > gui.playlist_view_length: - break + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - # Standard track --------------------------------------------------------------------- - playing = False + 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) - highlight = False - drag_highlight = False - line_y = gui.playlist_top + gui.playlist_row_height * number + playlist = sorted(playlist, key=g_date, reverse=rev) - track_box = ( - left + highlight_left, line_y, highlight_width, - gui.playlist_row_height - 1) + if custom_list is not None: + return playlist - input_box = (track_box[0] + 30 * gui.scale, track_box[1] + 1, track_box[2] - 36 * gui.scale, track_box[3]) + 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) + ">" - # 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 + pctl.multi_playlist.append( + pl_gen( + title=pctl.multi_playlist[index].title + line, + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - else: - line_hit = False - else: - line_hit = False - line_over = False + if rev: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a y>" + else: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a y<" - # Prevent click if near scroll bar - if scroll_enable and mouse_position[0] < 30: - line_hit = False +def gen_sort_date_new(index: int): + gen_sort_date(index, True) - # 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): +def gen_500_random(index: int): + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) - pctl.jump(track_id, track_position) + random.shuffle(playlist) - click_time -= 1.5 - quick_drag = False - mouse_down = False - mouse_up = False - line_hit = False + 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 album_mode: - goto_album(pctl.playlist_playing_position) + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a st" - 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 +def gen_folder_shuffle(index, custom_list=None): + folders = [] + dick = {} - # 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 + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - # 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 + 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) - # # 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 + random.shuffle(folders) + playlist = [] - # Begin drag single track - if inp.mouse_click and line_hit and not side_drag: - quick_drag = True - set_drag_source() + for folder in folders: + playlist += dick[folder] - # 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 custom_list is not None: + return playlist - if len(shift_selection) > 1 or key_shift_down: - if track_position not in shift_selection: # p_track != playlist_hold_position and + 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)) - if len(shift_selection) == 0: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a ra" - 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 +def gen_best_random(index: int): + playlist = [] - gui.pl_update = 1 + for p in pctl.multi_playlist[index].playlist_ids: + time = star_store.get(p) + if time > 300: + playlist.append(p) - else: - ref = [] - selection_stage = 2 - for item in shift_selection: - ref.append(default_playlist[item]) + random.shuffle(playlist) - for item in shift_selection: - default_playlist[item] = "old" + 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)) - for item in shift_selection: - if move_on_title: - default_playlist.insert(track_position, "new") - else: - default_playlist.insert(track_position + 1, "new") + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a pt>300 rt" - 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) +def gen_reverse(index, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - pctl.selected_in_playlist = shift_selection[0] - gui.pl_update += 1 + playlist = list(reversed(source)) - reload_albums(True) - pctl.notify_change() + if custom_list is not None: + return playlist - # 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 + 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)) - # Right click menu activation - if right_click and line_hit and mouse_position[0] > gui.playlist_left + 10: + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rv" - 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 gen_folder_reverse(index: int, custom_list=None): + source = custom_list + if source is None: + source = pctl.multi_playlist[index].playlist_ids - if track_position not in shift_selection: - pctl.selected_in_playlist = track_position - shift_selection = [pctl.selected_in_playlist] + 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 line_over and inp.mouse_click: + folders = list(reversed(folders)) + playlist = [] - 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] + for folder in folders: + playlist += dick[folder] - if not pl_is_locked(pctl.active_playlist_viewing) or key_shift_down: - playlist_hold = True - playlist_hold_position = track_position + if custom_list is not None: + return 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 + 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)) - # Multi Select Highlight - if track_position in shift_selection or track_position == pctl.selected_in_playlist: - highlight = True + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[index].title + "\" a rva" - 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 +def gen_dupe(index: int) -> None: + playlist = pctl.multi_playlist[index].playlist_ids - list_items.append( - (0, track_position, track_object, track_box, input_box, highlight, number, drag_highlight, playing)) - number += 1 + 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)) - if number > gui.playlist_view_length: - break - # --------------------------------------------------------------------------------------- +def gen_sort_path(index: int) -> None: + def path(index: int) -> str: + return pctl.master_library[index].fullpath - # 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 + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=path) - for type, track_position, tr, track_box, input_box, highlight, number, drag_highlight, playing in list_items: + 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)) - line_y = gui.playlist_top + gui.playlist_row_height * number +def gen_sort_artist(index: int) -> None: + def artist(index: int) -> str: + return pctl.master_library[index].artist - ddt.text_background_colour = colours.playlist_panel_background + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=artist) - if type == 1: + 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)) - # Is type ALBUM TITLE - separator = " - " - if prefs.row_title_separator_type == 1: - separator = " ‒ " - if prefs.row_title_separator_type == 2: - separator = " ⦁ " +def gen_sort_album(index: int) -> None: + def album(index: int) -> None: + return pctl.master_library[index].album - date = "" - duration = "" + playlist = copy.deepcopy(pctl.multi_playlist[index].playlist_ids) + playlist = sorted(playlist, key=album) - line = tr.parent_folder_name + 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)) - # 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: +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" - if tr.album_artist != "" and tr.album != "": - line = tr.album_artist + separator + tr.album +def reload_config_file(): + if transcode_list: + show_message(_("Cannot reload while a transcode is in progress!"), mode="error") + return - if prefs.left_align_album_artist_title and not True: - album_artist_mode = True - line = tr.album + load_prefs() + gui.opened_config_file = False - if len(line) < 6 and "CD" in line: - line = tr.album + ddt.force_subpixel_text = prefs.force_subpixel_text + ddt.clear_text_cache() + pctl.playerCommand = "reload" + pctl.playerCommandReady = True + show_message(_("Configuration reloaded"), mode="done") + gui.update_layout() - if prefs.append_date and year_search.search(tr.date): - year = d_date_display2(tr) - if not year: - year = d_date_display(tr) - date = "(" + year + ")" +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 line.endswith(")"): - b = line.split("(") - if len(b) > 1 and len(b[1]) <= 11: +def open_keymap_file(): + target = str(config_directory / "input.txt") - match = year_search.search(b[1]) + if not os.path.isfile(target): + show_message(_("Input file missing")) + return - if match: - line = b[0] - date = "(" + b[1] + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - elif line.startswith("("): +def open_file(target): + if not os.path.isfile(target): + show_message(_("Input file missing")) + return - b = line.split(")") - if len(b) > 1 and len(b[0]) <= 11: + if system == "Windows" or msys: + os.startfile(target) + elif macos: + subprocess.call(["open", target]) + else: + subprocess.call(["xdg-open", target]) - match = year_search.search(b[0]) +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]) - if match: - line = b[1] - date = b[0] + ")" +def remove_folder(index: int): + global default_playlist - if "(" in line and year_search.search(line): - date = "" + 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] - line = line.replace(" - ", separator) + reload() - qq = 0 - d_date = date - title_line = line +def convert_folder(index: int): + global default_playlist + global transcode_list - # Calculate folder duration + if not tauon.test_ffmpeg(): + return - q = track_position + 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] - total_time = 0 - while q < len(default_playlist): + 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") - if pctl.get_track(default_playlist[q]).parent_folder_path != tr.parent_folder_path: - break + return + folder = [index] - total_time += pctl.get_track(default_playlist[q]).length + 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: - q += 1 - qq += 1 + 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 qq > 1: - duration = " [ " + get_display_time(total_time) + " ]" # Hair space inside brackets for better visual spacing + 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") - if prefs.append_total_time: - date += duration + return - ex = left + highlight_left + highlight_width - 7 * gui.scale + #logging.info(folder) + transcode_list.append(folder) + tauon.thread_manager.ready("worker") - height = line_y + gui.playlist_row_height - 19 * gui.scale # gui.pl_title_y_offset +def transfer(index: int, args) -> None: + global cargo + global default_playlist + old_cargo = copy.deepcopy(cargo) - 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) + 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] - light_offset = 0 - if colours.lm: - light_offset = 3 * gui.scale - ex -= light_offset + 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] - if qq > 1: - ex += 1 * gui.scale + elif args[1] == 3: # playlist + cargo += default_playlist + if args[0] == 0: # cut + default_playlist = [] - ddt.text_background_colour = colours.playlist_panel_background + elif args[0] == 2: # Drop + if args[1] == 1: # Before - if gui.scale == 2: - height += 1 + 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 - 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) + while len(cargo) > 0: + default_playlist.insert(insert, cargo.pop()) + elif args[1] == 2: # After + insert = pctl.selected_in_playlist - #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) + while insert < len(default_playlist) and pctl.master_library[default_playlist[insert]].parent_folder_name == \ + pctl.master_library[index].parent_folder_name: + insert += 1 + while len(cargo) > 0: + default_playlist.insert(insert, cargo.pop()) + elif args[1] == 3: # End + default_playlist += cargo + # cargo = [] + cargo = old_cargo - if prefs.row_title_format == 2: + reload() - separator = " | " +def temp_copy_folder(ref): + global cargo + cargo = [] + transfer(ref, args=[1, 2]) - start_offset = round(15 * gui.scale) - xx = left + highlight_left + start_offset - ww = highlight_width +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() - 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 menu_paste(position): + paste(None, position) - 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 s_copy(): + # Copy tracks to internal clipboard + # gui.lightning_copy = False + # if key_shift_down: + gui.lightning_copy = True + clip = copy_from_clipboard() + if "file://" in clip: + copy_to_clipboard("") - 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))) + global cargo + cargo = [] + if default_playlist: + for item in shift_selection: + cargo.append(default_playlist[item]) + if not cargo and -1 < pctl.selected_in_playlist < len(default_playlist): + cargo.append(default_playlist[pctl.selected_in_playlist]) + tauon.copied_track = None + if len(cargo) == 1: + tauon.copied_track = cargo[0] - 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 +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 - aa = 0 +def lightning_paste(): + move = True + # if not key_shift_down: + # move = False - ft_width = ddt.get_text_w(line, gui.row_font_size + gui.pl_title_font_offset) + move_track = pctl.get_track(cargo[0]) + move_path = move_track.parent_folder_path - left_align = highlight_width - date_w - 13 * gui.scale - light_offset + 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 - left_align -= star_offset + match_track = pctl.get_track(default_playlist[shift_selection[0]]) + match_path = match_track.parent_folder_path - extra = aa + if pctl.playing_state > 0 and move: + if pctl.playing_object().parent_folder_path == move_path: + pctl.stop(True) - left_align -= extra + p = Path(match_path) + s = list(p.parts) + base = s[0] + c = base + del s[0] - 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) + 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]) - else: - ddt.text( - (ex - date_w, height, 1), line, - colours.folder_title, - gui.row_font_size + gui.pl_title_font_offset) + to_move = list(set(to_move)) - # ----- + for level in s: + upper = c + c = os.path.join(c, level) - # 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) + t_artist = match_track.artist + ta_artist = match_track.album_artist - # 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]) + t_artist = filename_safe(t_artist) + ta_artist = filename_safe(ta_artist) - continue + if (len(t_artist) > 0 and t_artist in level) or \ + (len(ta_artist) > 0 and ta_artist in level): - # 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) + logging.info("found target artist level") + logging.info(t_artist) + logging.info("Upper folder is: " + upper) - 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]) + if len(move_path) < 4: + show_message(_("Safety interupt! The source path seems oddly short."), move_path, mode="error") + return + if not os.path.isdir(upper): + show_message(_("The target directory is missing!"), upper, mode="warning") + return - # Blue drop line - if drag_highlight: # playlist_hold_position != p_track: + if not os.path.isdir(move_path): + show_message(_("The source directory is missing!"), move_path, mode="warning") + return - ddt.rect( - [left + highlight_left, line_y + gui.playlist_row_height - 1 * gui.scale, highlight_width, - 3 * gui.scale], [125, 105, 215, 255]) + 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 - # Highlight - if highlight: - ddt.rect_a( - (left + highlight_left, line_y), (highlight_width, gui.playlist_row_height), - colours.row_select_highlight) + if directory_size(move_path) > 3000000000: + show_message(_("Folder size safety limit reached! (3GB)"), move_path, mode="warning") + return - ddt.text_background_colour = alpha_blend(colours.row_select_highlight, ddt.text_background_colour) + 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 - 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) + artist = move_track.artist + if move_track.album_artist != "": + artist = move_track.album_artist - if not gui.set_mode: + artist = filename_safe(artist) - line_render( - tr, track_position, gui.playlist_text_offset + line_y, - playing, 255, left + inset_left, inset_width, 1, line_y) + if artist == "": + show_message(_("The track needs to have an artist name.")) + return - else: - # NEE --------------------------------------------------------- - n_track = tr - p_track = track_position - this_line_playing = playing + artist_folder = os.path.join(upper, artist) - start = 18 * gui.scale + logging.info("Target will be: " + artist_folder) - if center_mode: - start = inset_left + 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) - elif gui.lsp: - start += gui.lspw + 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 - run = start - end = start + gui.plw + 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 - if center_mode: - end = highlight_width + start + load_order.playlist_position = insert - # 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) + 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] - for h, item in enumerate(gui.pl_st): + break + else: + show_message(_("Could not find a folder with the artist's name to match level at.")) + return - 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 + # for file in os.listdir(artist_folder): + # - if run > end - 50 * gui.scale: - break + if album_mode: + prep_gal() + reload_albums(True) - if len(gui.pl_st) == h + 1: - wid -= 6 * gui.scale + cargo.clear() + gui.lightning_copy = False - 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) +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 - if item[0] == "Starline": + elif "tidal.com/playlist/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.playlist(num) + clip = False - total = star_store.get_by_object(n_track) + elif "tidal.com/mix/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.mix(num) + clip = False - if total > 0 and n_track.length != 0 and wid > 0: - if gui.star_mode == "star": + elif "tidal.com/browse/track/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.track(num) + clip = False - 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 "tidal.com/browse/artist/" in clip: + logging.info(clip) + num = clip.split("/")[-1].split("?")[0] + tauon.tidal.artist(num) + clip = False - 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 + 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 - else: + 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 - ratio = total / n_track.length - if ratio > 0.55: - star_x = int(ratio * (4 * gui.scale)) - star_x = min(star_x, wid) + 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 - colour = colours.star_line - if playing and colours.star_line_playing is not None: - colour = colours.star_line_playing + load_orders.append(copy.deepcopy(load_order)) + found = True - 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) + if not found: - 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 + 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) - 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) + gui.pl_update += 1 - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing - elif item[0] == "#": +def s_cut(): + s_copy() + del_selected() - 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) +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 - 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 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 - 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 +def paste_playlist_coast_album(): + shoot_dl = threading.Thread(target=paste_playlist_coast_fire) + shoot_dl.daemon = True + shoot_dl.start() - if get_love(n_track): +def paste_playlist_coast_track(): + shoot_dl = threading.Thread(target=paste_playlist_track_coast_fire) + shoot_dl.daemon = True + shoot_dl.start() - 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 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 - 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 + return [line_colour, colours.menu_background, None] - 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 refind_playing(): + # Refind playing index + if pctl.playing_ready(): + for i, n in enumerate(default_playlist): + if pctl.track_queue[pctl.queue_step] == n: + pctl.playlist_playing_position = i + break - x = run + u + (heart_row_icon.w + spacing) * count +def del_selected(force_delete=False): + global shift_selection - j = 0 # justify right - if run < start + 100 * gui.scale: - j = 1 # justify left + gui.update += 1 + gui.pl_update = 1 - display_friend_heart(x, yy, name, j) - count += 1 + if not shift_selection: + shift_selection = [pctl.selected_in_playlist] - # 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 not default_playlist: + return + li = [] - 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) + for item in reversed(shift_selection): + if item > len(default_playlist) - 1: + return - text = str(int(ratio)) - if text == "0": - text = "" - colour = colours.index_text - norm_colour = colour - if this_line_playing is True: - colour = colours.index_playing + li.append((item, default_playlist[item])) # take note for force delete - 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 + # 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 - if text: - if item[0] in colours.column_colours: - colour = colours.column_colours[item[0]] + del default_playlist[item] - if this_line_playing and item[0] in colours.column_colours_playing: - colour = colours.column_colours_playing[item[0]] + if force_delete: + for item in li: - if run + 6 * gui.scale + wid > end: - wid = end - run - 40 * gui.scale - if center_mode: - wid += 25 * 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")) - wid = max(0, wid) + 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") - # # 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) + else: + undo.bk_tracks(pctl.active_playlist_viewing, li) - ddt.text( - (run + 6 * gui.scale, y + y_off), - text, - colour, - font, - max_w=wid) + reload() + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) - if ddt.was_truncated: - #logging.info(text) - rect = (run, y, wid - 1, gui.playlist_row_height - 1) - gui.heart_fields.append(rect) + pctl.selected_in_playlist = min(pctl.selected_in_playlist, len(default_playlist) - 1) - if coll(rect): - columns_tool_tip.set(run - 7 * gui.scale, y, text, font, rect) + shift_selection = [pctl.selected_in_playlist] + gui.pl_update += 1 + refind_playing() + pctl.notify_change() - run += item[1] +def force_del_selected(): + del_selected(force_delete=True) - # ----------------------------------------------------------------- - # 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 test_show(dummy): + return album_mode - # w += 1 - # if w > gui.playlist_view_length: - # 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() - # 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 +def last_fm_test(ignore): + if lastfm.connected: + return True + return False - 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 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 - SDL_SetRenderTarget(renderer, gui.main_texture) - SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) +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 mouse_down is False: - playlist_hold = False +def love_decox(): + global r_menu_index - ddt.pretty_rect = None - ddt.alpha_bg = False + if love(False, r_menu_index): + return [colours.menu_text, colours.menu_background, _("Un-Love Track")] + return [colours.menu_text, colours.menu_background, _("Love Track")] - def cache_render(self): +def love_index(): + global r_menu_index - SDL_RenderCopy(renderer, gui.tracklist_texture, None, gui.tracklist_texture_rect) + notify = False + if not gui.show_hearts: + notify = True -class ArtBox: + # 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() - def __init__(self): - pass +def toggle_spotify_like_ref(): + tr = pctl.get_track(r_menu_index) + if tr: + shoot_dl = threading.Thread(target=toggle_spotify_like_active2, args=([tr])) + shoot_dl.daemon = True + shoot_dl.start() - def draw(self, x, y, w, h, target_track=None, tight_border=False, default_border=None): +def toggle_spotify_like3(): + toggle_spotify_like_active2(pctl.get_track(r_menu_index)) - # 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) +def toggle_spotify_like_row_deco(): + tr = pctl.get_track(r_menu_index) + text = _("Spotify Like Track") - # We need to find the size of the inner square for the artwork - # box = min(w, h) + # 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") - box_w = w - box_h = h + return [colours.menu_text, colours.menu_background, text] - box_w -= 17 * gui.scale # Inset the square a bit - box_h -= 17 * gui.scale # Inset the square a bit +def spot_like_show_test(x): - box_x = x + ((w - box_w) // 2) - box_y = y + ((h - box_h) // 2) + return spotify_show_test and pctl.get_track(r_menu_index).file_ext == "SPTY" - # And position the square - rect = (box_x, box_y, box_w, box_h) - gui.main_art_box = rect +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 - # Draw the album art. If side bar is being dragged set quick draw flag - showc = None - result = 1 +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 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) +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() - # Draw faint border on album art - if tight_border: - if result == 0 and gui.art_drawn_rect: - border = gui.art_drawn_rect - ddt.rect_s(gui.art_drawn_rect, colours.art_box, 1 * gui.scale) - elif default_border: - border = default_border - ddt.rect_s(default_border, colours.art_box, 1 * gui.scale) - else: - border = rect - else: - ddt.rect_s(rect, colours.art_box, 1 * gui.scale) - border = rect +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))) - fields.add(border) +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] - # 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 +def split_queue_album(id: int) -> int | None: + item = pctl.force_queue[0] - # Input for album art - if target_track: + pl = id_to_pl(item.playlist_id) + if pl is None: + return None - # Cycle images on click + playlist = pctl.multi_playlist[pl].playlist_ids - if coll(gui.main_art_box) and inp.mouse_click is True and key_focused == 0: + i = pctl.playlist_playing_position + 1 + parts = [] + album_parent_path = pctl.get_track(item.track_id).parent_folder_path - album_art_gen.cycle_offset(target_track) + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break - if pctl.mpris: - pctl.mpris.update(force=True) + parts.append((playlist[i], i)) + i += 1 - # 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) + del pctl.force_queue[0] - # 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: + for part in reversed(parts): + pctl.force_queue.insert(0, queue_item_gen(part[0], part[1], item.type)) + return (len(parts)) - padding = 6 * gui.scale +def add_to_queue_next(ref: int) -> None: + if pctl.force_queue and pctl.force_queue[0].album_stage == 1: + split_queue_album(None) - 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] + pctl.force_queue.insert(0, queue_item_gen(ref, r_menu_position, pl_to_id(pctl.active_playlist_viewing))) - art_metadata_overlay(xw, yh, showc) +# def toggle_queue(mode: int = 0) -> bool: +# if mode == 1: +# return prefs.show_queue +# prefs.show_queue ^= True +# prefs.show_queue ^= True -class ScrollBox: +def delete_track(track_ref): + tr = pctl.get_track(track_ref) + fullpath = tr.fullpath - def __init__(self): + if system == "Windows" or msys: + fullpath = fullpath.replace("/", "\\") - 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 tr.is_network: + show_message(_("Cannot delete a network track")) + return - 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): + while track_ref in default_playlist: + default_playlist.remove(track_ref) - if max_value < 2: - return 0 + try: + send2trash(fullpath) - if click is None: - click = inp.mouse_click + 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")) - bar_height = round(90 * gui.scale) + 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 h > 400 * gui.scale and max_value < 20: - bar_height = round(180 * gui.scale) + reload() + refind_playing() + pctl.notify_change() - bg = [255, 255, 255, 7] - fg = [255, 255, 255, 30] - fg_h = [255, 255, 255, 40] - fg_off = [255, 255, 255, 15] +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…")] - 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 activate_trans_editor(): + trans_edit_box.active = True - ddt.rect((x, y, w, h), bg) +def delete_folder(index, force=False): + track = pctl.master_library[index] - half = bar_height // 2 + if track.is_network: + show_message(_("Cannot physically delete"), _("One or more tracks is from a network location!"), mode="info") + return - ratio = value / max_value + old = track.parent_folder_path - mi = y + half - mo = y + h - half - distance = mo - mi - position = int(round(distance * ratio)) + if len(old) < 5: + show_message(_("This folder path seems short, I don't wanna try delete that"), mode="warning") + return - fw = w + extend_field - fx = x - extend_field + 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 coll((fx, y, fw, h)): + protect = ("", "Documents", "Music", "Desktop", "Downloads") - if mouse_down: - gui.update += 1 + for fo in protect: + if old.strip("\\/") == os.path.join(os.path.expanduser("~"), fo).strip("\\/"): + show_message(_("Woah, careful there!"), _("I don't think we should delete that folder."), mode="warning") + return - if r_click: - p = mouse_position[1] - half - y - p = max(0, p) + if directory_size(old) > 1500000000: + show_message(_("Delete size safety limit reached! (1.5GB)"), old, mode="warning") + return - range = h - bar_height - p = min(p, range) + try: - per = p / range + 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) - value = int(round(max_value * per)) + if force: + shutil.rmtree(old) + elif system == "Windows" or msys: + send2trash(old.replace("/", "\\")) + else: + send2trash(old) + + for i in reversed(range(len(default_playlist))): + + if old == pctl.master_library[default_playlist[i]].parent_folder_path: + del default_playlist[i] + + if not os.path.exists(old): + if force: + show_message(_("Folder deleted."), old, mode="done") + else: + show_message(_("Folder sent to trash."), old, mode="done") + else: + show_message(_("Hmm, its still there"), old, mode="error") - ratio = value / max_value + if album_mode: + prep_gal() + reload_albums() - mi = y + half - mo = y + h - half - distance = mo - mi - position = int(round(distance * ratio)) + 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") - in_bar = False - if coll((x, mi + position - half, w, bar_height)): - in_bar = True - if click: - self.held = True + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + gui.pl_update += 1 + pctl.notify_change() - # 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 rename_parent(index: int, template: str) -> None: + # template = prefs.rename_folder_template + template = template.strip("/\\") + track = pctl.master_library[index] - if pctl.playlist_view_position < 0: - pctl.playlist_view_position = 0 + if track.is_network: + show_message(_("Cannot rename"), _("One or more tracks is from a network location!"), mode="info") + return + old = track.parent_folder_path + #logging.info(old) - elif mouse_down and not self.held: + new = parse_template2(template, track) - if click and not in_bar: - self.slide_hold = True - self.direction_lock = 1 - if mouse_position[1] - y < position: - self.direction_lock = 0 + if len(new) < 1: + show_message(_("Rename error."), _("The generated name is too short"), mode="warning") + return - self.d_position = value / max_value + if len(old) < 5: + show_message(_("Rename error."), _("This folder path seems short, I don't wanna try rename that"), mode="warning") + 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 not os.path.exists(old): + show_message(_("Rename Failed. The original folder is missing."), mode="warning") + return - tt = scroll_timer.hit() - if tt > 0.1: - tt = 0 + protect = ("", "Documents", "Music", "Desktop", "Downloads") - flip = -1 - if self.direction_lock: - flip = 1 + 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 - self.d_position = min(max(self.d_position + (((tt * jump_distance) / max_value) * flip), 0), 1) + 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) - else: - self.slide_hold = False + pre_state = 0 - if (self.held and mouse_up) or not mouse_down: - self.held = False + for key, object in pctl.master_library.items(): - if self.held and not window_is_focused(): - self.held = False + if object.fullpath == "": + continue - if self.held: - get_sdl_input.mouse_capture_want = True - new_y = mouse_position[1] - gui.update += 1 + if old == object.parent_folder_path: - offset = new_y - self.source_click_y + new_fullpath = os.path.join(new_parent_path, object.filename) - position = self.source_bar_y + offset + if os.path.normpath(new_parent_path) == os.path.normpath(old): + show_message(_("The folder already has that name.")) + return - position = max(position, 0) - position = min(position, distance) + if os.path.exists(new_parent_path): + show_message(_("Rename Failed."), _("A folder with that name already exists"), mode="warning") + return - ratio = position / distance - value = int(round(max_value * ratio)) + if key == pctl.track_queue[pctl.queue_step] and pctl.playing_state > 0: + pre_state = pctl.stop(True) - 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 + object.parent_folder_name = new + object.parent_folder_path = new_parent_path + object.fullpath = new_fullpath - ddt.rect(rect, colour) + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) - if self.slide_hold: - return round(max_value * self.d_position) + # 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("\\/")) - return value + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) -class RadioBox: + 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 - def __init__(self): + show_message(_("Folder renamed."), _("Renamed to: {name}").format(name=new), mode="done") - 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() + if pre_state == 1: + pctl.revert() - self.x = 1 - self.y = 1 - self.w = 1 - self.h = 1 - self.center = False + tree_view_box.clear_target_pl(pctl.active_playlist_viewing) + pctl.notify_change() - self.scroll_position = 0 - self.scroll = ScrollBox() +def rename_folders_disable_test(index: int) -> bool: + return pctl.get_track(index).is_network - 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 = "" +def rename_folders(index: int): + global track_box + global rename_index + global input_text - 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) + track_box = False + rename_index = index - self.song_key = "" + if rename_folders_disable_test(index): + show_message(_("Not applicable for a network track.")) + return - self.drag = None + gui.rename_folder_box = True + input_text = "" + shift_selection.clear() - self.tab = 0 - self.temp_list = [] + global quick_drag + global playlist_hold + quick_drag = False + playlist_hold = False - self.hosts = None - self.host = None +def move_folder_up(index: int, do: bool = False) -> bool | None: + track = pctl.master_library[index] - 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)) + if track.is_network: + show_message(_("Cannot move"), _("One or more tracks is from a network location!"), mode="info") + return None - 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 + 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) - 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 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 - def search_country(self, text): + if len(os.listdir(parent_folder)) > 1: + return False - 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 do is False: + return True - def search_tag(self, 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) - text = text.lower() - self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&tag=" + text) + try: - def search_title(self, text): + # Rename the track folder to something temporary + os.rename(move_target, os.path.join(parent_folder, "RMTEMP000")) - text = text.lower() - self.search_radio_browser("/json/stations/search?order=votes&limit=250&reverse=true&name=" + text) + # Move the temporary folder up 2 levels + shutil.move(os.path.join(parent_folder, "RMTEMP000"), upper_folder) - def is_m3u(self, url): - return url.lower().endswith(".m3u") or url.lower().endswith(".m3u8") + # Delete the old directory that contained the original folder + shutil.rmtree(parent_folder) - def extract_stream_m3u(self, url, recursion_limit=5): - if recursion_limit <= 0: - return None - logging.info("Fetching M3U...") + # Rename the moved folder back to its original name + os.rename(os.path.join(upper_folder, "RMTEMP000"), os.path.join(upper_folder, folder_name)) - try: - response = requests.get(url, timeout=10) - if response.status_code != 200: - logging.error(f"M3U Fetch error code: {response.status_code}") - return None + except Exception as e: + logging.exception("System Error!") + show_message(_("System Error!"), str(e), mode="error") - content = response.text - lines = content.strip().split("\n") + # 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(): - 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) + 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("\\/")) - return None + search_string_cache.pop(object.index, None) + search_dia_string_cache.pop(object.index, None) - except Exception: - logging.exception("Failed to extract M3U") - return None + logging.info(object.fullpath) + logging.info(object.parent_folder_path) - def start(self, item): - url = item["stream_url"] - logging.info("Start radio") - logging.info(url) - if self.is_m3u(url): - url = self.extract_stream_m3u(url) - logging.info(f"Extracted URL is: {url}") - if not url: - logging.info("Failed to extract stream from M3U") - return + if pre_state == 1: + pctl.revert() - if self.load_connecting: - return +def clean_folder(index: int, do: bool = False) -> int | None: + track = pctl.master_library[index] - if tauon.spot_ctl.playing or tauon.spot_ctl.coasting: - tauon.spot_ctl.control("stop") + if track.is_network: + show_message(_("Cannot clean"), _("One or more tracks is from a network location!"), mode="info") + return None - try: - self.websocket.close() - logging.info("Websocket closed") - except Exception: - logging.exception("No socket to close?") + 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": - 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 = "" + 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)) - album_art_gen.clear_cache() + 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 not tauon.test_ffmpeg(): - prefs.auto_rec = False - return + 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)) - self.run_proxy = True - if url.endswith(".ts"): - self.run_proxy = False + 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 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 + return found - # 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 +def reset_play_count(index: int): + star_store.remove(index) - if tauon.stream_proxy.download_running: - tauon.stream_proxy.abort = True +def vacuum_playtimes(index: int): + todo = [] + for k in default_playlist: + if pctl.master_library[index].parent_folder_name == pctl.master_library[k].parent_folder_name: + todo.append(k) - self.load_connecting = True - self.load_failed = False + for track in todo: - shoot = threading.Thread(target=self.start2, args=[url]) - shoot.daemon = True - shoot.start() + tr = pctl.get_track(track) - def start2(self, url): + total_playtime = 0 + flags = "" - 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 + to_del = [] - 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() + 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 tauon.update_play_lock is not None: - tauon.update_play_lock() + for key in to_del: + del star_store.db[key] - time.sleep(0.1) - self.load_connecting = False - self.load_failed = False - gui.update += 1 + 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?") - 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 reload_metadata(input, keep_star: bool = True) -> None: + global todo - def send_heartbeat(ws): - #logging.info(self.ws_interval) - time.sleep(self.ws_interval) - ws.send("{\"op\":9}") - logging.info("Send heatbeat") + # vacuum_playtimes(index) + # return + todo = [] - def on_message(ws, message): - logging.info(message) - d = json.loads(message) - if d["op"] == 10: - shoot = threading.Thread(target=send_heartbeat, args=[ws]) - shoot.daemon = True - shoot.start() + if isinstance(input, list): + todo = input - if d["op"] == 0: - self.ws_interval = d["d"]["heartbeat"] / 1000 - ws.send("{\"op\":9}") + 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]) - if d["op"] == 1: - try: + for i in reversed(range(len(todo))): + if todo[i].is_cue: + del todo[i] - 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 + for track in todo: - pctl.found_tags = found_tags - pctl.tag_meta = line + search_string_cache.pop(track.index, None) + search_dia_string_cache.pop(track.index, None) - filename = d["d"]["song"]["albums"][0]["image"] - fulllink = "https://cdn.listen.moe/covers/" + filename + #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) - #logging.info(fulllink) - art_response = requests.get(fulllink, timeout=10) - #logging.info(art_response.status_code) + pctl.master_library[track.index] = tag_scan(track) - 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 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() - 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 + gui.pl_update += 1 + tauon.thread_manager.ready("worker") - def on_error(ws, error): - logging.error(error) +def reload_metadata_selection() -> None: + cargo = [] + for item in shift_selection: + cargo.append(default_playlist[item]) - def on_close(ws): - logging.info("### closed ###") + for k in cargo: + if pctl.master_library[k].is_cue == False: + to_scan.append(k) + tauon.thread_manager.ready("worker") - 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...") +def editor(index: int | None) -> None: + todo = [] + obs = [] - th.start_new_thread(run, ()) + 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]) - # 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() + # 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) - 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] + file_line = "" + for track in todo: + file_line += ' "' + file_line += pctl.master_library[track].fullpath + file_line += '"' - def delete_radio_entry_after(self, item): - p = radiobox.right_clicked_station_p - del prefs.radio_urls[p + 1:] + if system == "Windows" or msys: + file_line = file_line.replace("/", "\\") - def edit_entry(self, item): - radio = item - self.radio_field_title.text = radio["title"] - self.radio_field.text = radio["stream_url"] + prefix = "" + app = prefs.tag_editor_target - def browser_get_hosts(self): + if (system == "Windows" or msys) and app: + if app[0] != '"': + app = '"' + app + if app[-1] != '"': + app = app + '"' - import socket - """ - Get all base urls of all currently available radiobrowser servers + app_switch = "" - Returns: - list: a list of strings + ok = False - """ - 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] + prefix = launch_prefix - # 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") + if system == "Linux": + ok = whicher(prefs.tag_editor_target) + else: - # sort list of names - hosts.sort() - # add "https://" in front to make it an url - return list(map(lambda x: "https://" + x, hosts)) + 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 - def search_page(self): + ok = True - y = self.y - x = self.x - w = self.w - h = self.h + if not ok: + show_message(_("Tag editor app does not appear to be installed."), mode="warning") - yy = y + round(40 * gui.scale) + 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") - 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) + return - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + if "picard" in prefs.tag_editor_target: + app_switch = " --d " - 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: + line = prefix + app + app_switch + file_line - 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") + show_message( + prefs.tag_editor_name + " launched.", "Fields will be updated once application is closed.", mode="arrow") + gui.update = 1 - 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() + complete = subprocess.run(shlex.split(line), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - 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) + 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('"') - 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) + 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.") - 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) + gui.message_box = False + reload_metadata(obs, keep_star=False) - 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" + # Re apply playtime data in case file names change + for item in old_stars: - self.temp_list.append(radio) + old_key = item[1] + old_value = item[2] - 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) + if not old_value: # ignore if there was no old playcount metadata + continue - 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) + new_key = star_store.object_key(item[0]) + new_value = star_store.full_get(item[0].index) - for station in primary_stations: - self.temp_list.append(station) + if old_key == new_key: + continue - 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() + if new_value is None: + new_value = [0, "", 0] - def search_radio_browser2(self, param): + new_value[0] += old_value[0] + new_value[1] = "".join(set(new_value[1] + old_value[1])) - if not self.hosts: - self.hosts = self.browser_get_hosts() - if not self.host: - self.host = random.choice(self.hosts) + if old_key in star_store.db: + del star_store.db[old_key] - 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 + star_store.db[new_key] = new_value - def parse_data(self, data): + gui.pl_update = 1 + gui.update = 1 + pctl.notify_change() - self.temp_list.clear() +def launch_editor(index: int): + if snap_mode: + show_message(_("Sorry, this feature isn't (yet) available with Snap.")) + return - 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 + if launch_editor_disable_test(index): + show_message(_("Cannot edit tags of a network track.")) + return - def render(self) -> None: + mini_t = threading.Thread(target=editor, args=[index]) + mini_t.daemon = True + mini_t.start() - if self.edit_mode: - w = round(510 * gui.scale) - h = round(120 * gui.scale) # + sh +def launch_editor_selection_disable_test(index: int): + for position in shift_selection: + if pctl.get_track(default_playlist[position]).is_network: + return True + return False - 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 launch_editor_selection(index: int): + if launch_editor_selection_disable_test(index): + show_message(_("Cannot edit tags of a network track.")) + return - 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) + mini_t = threading.Thread(target=editor, args=[None]) + mini_t.daemon = True + mini_t.start() - self.saved() - return +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] - 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) +def launch_editor_disable_test(index: int): + return pctl.get_track(index).is_network - self.w = w - self.h = h - self.x = x - self.y = y +def show_lyrics_menu(index: int): + global track_box + track_box = False + enter_showcase_view(track_id=r_menu_index) + inp.mouse_click = False - yy = y +def recode(text, enc): + return text.encode("Latin-1", "ignore").decode(enc, "ignore") - 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 intel_moji(index: int): + gui.pl_update += 1 + gui.update += 1 - ddt.text_background_colour = colours.box_background + track = pctl.master_library[index] - if key_esc_press or (gui.level_2_click and not coll((x, y, w, h))): - self.active = False + lot = [] - ddt.text((x + 10 * gui.scale, yy + 8 * gui.scale), _("Station Browser"), colours.box_title_text, 213) + for item in default_playlist: - # --- - 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 track.album == pctl.master_library[item].album and \ + track.parent_folder_name == pctl.master_library[item].parent_folder_name: + lot.append(item) - 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: + lot = set(lot) - 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") + l_artist = track.artist.encode("Latin-1", "ignore") + l_album = track.album.encode("Latin-1", "ignore") + detect = None - ddt.text((x + 495 * gui.scale, yy + 8 * gui.scale, 1), text, colours.box_title_text, 311) - # if tauon.stream_proxy.s_format: - # ddt.text((x + 425 * gui.scale, yy + 8 * gui.scale,), tauon.stream_proxy.s_format, colours.box_title_text, 311) - # if tauon.stream_proxy.s_bitrate: - # ddt.text((x + 454 * gui.scale, yy + 8 * gui.scale,), tauon.stream_proxy.s_bitrate + "kbps", colours.box_title_text, 311) + if 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 self.tab == 1: - self.search_page() - elif self.tab == 0: - self.saved() - self.draw_list() - # self.footer() - return + 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 - def saved(self): - y = self.y - x = self.x - w = self.w - h = self.h + for item in lot: + t_track = pctl.master_library[item] - yy = y + round(40 * gui.scale) + 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 - width = round(370 * gui.scale) + 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 - 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) + 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) - ddt.rect_s(rect, colours.box_text_border, 1 * gui.scale) + 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) - yy += round(30 * gui.scale) + if key != None: + star_store.insert(item, key) - 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 + search_string_cache.pop(track.index, None) + search_dia_string_cache.pop(track.index, None) - 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) + else: + show_message(_("Autodetect failed")) - if draw.button(_("Save"), x + width + round(21 * gui.scale), yy - round(20 * gui.scale), press=gui.level_2_click): +def sel_to_car(): + global default_playlist + cargo = [] - 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"] = "" + for item in shift_selection: + cargo.append(default_playlist[item]) - if self.add_mode: - pctl.radio_playlists[pctl.radio_playlist_viewing]["items"].append(radio) - self.active = False +def cut_selection(): + sel_to_car() + del_selected() - else: - show_message(_("Could not validate URL. Must start with https:// or http://")) +def clip_ar_al(index: int): + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].album + SDL_SetClipboardText(line.encode("utf-8")) - def draw_list(self): +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")) - x = self.x - y = self.y - w = self.w - h = self.h +def clip_title(index: int): + n_track = pctl.master_library[index] - if self.drag: - gui.update_on_drag = True + 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 - yy = y + round(100 * gui.scale) - x += round(10 * gui.scale) + SDL_SetClipboardText(line.encode("utf-8")) - radio_list = prefs.radio_urls - if self.tab == 1: - radio_list = self.temp_list +def lightning_copy(): + s_copy() + gui.lightning_copy = True - 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) +def toggle_transcode(mode: int = 0) -> bool: + if mode == 1: + return prefs.enable_transcode + prefs.enable_transcode ^= True + return None - 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 toggle_chromecast(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_chromecast + prefs.show_chromecast ^= True + return None - self.scroll_position = max(self.scroll_position, 0) +def toggle_transfer(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_transfer + prefs.show_transfer ^= True - p = self.scroll_position * 2 - offset = 0 - to_delete = None - swap = None + 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 - while 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")] - if p > len(radio_list) - 1: - break +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")) - xx = x + offset - item = radio_list[p] +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] - rect = (xx, yy, round(233 * gui.scale), round(40 * gui.scale)) - fields.add(rect) +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) - bg = colours.box_background - text_colour = colours.box_input_text + album_url = track_object.misc.get("spotify-album-url") + if album_url and album_url in tauon.spot_ctl.cache_saved_albums: + text = _("Un-save Spotify Album") - playing = pctl.playing_state == 3 and self.loaded_url == item["stream_url"] + return (colours.menu_text, colours.menu_background, text) - if playing: - # bg = colours.box_sub_highlight - # ddt.rect(rect, bg, True) +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) - bg = colours.tab_background_active - text_colour = colours.tab_text_active - ddt.rect(rect, bg) + 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 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) +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 - if coll(rect): + shoot_dl = threading.Thread(target=add_to_spotify_library2, args=([album_url])) + shoot_dl.daemon = True + shoot_dl.start() - 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) +def selection_queue_deco(): + total = 0 + for item in shift_selection: + total += pctl.get_track(default_playlist[item]).length - bg = alpha_blend(bg, colours.box_background) + total = get_hms_time(total) - boxx = round(32 * gui.scale) - toff = boxx + round(10 * gui.scale) - if item["title"]: - ddt.text( - (xx + toff, yy + round(3 * gui.scale)), item["title"], text_colour, 212, bg=bg, - max_w=rect[2] - (15 * gui.scale + toff)) - else: - ddt.text( - (xx + toff, yy + round(3 * gui.scale)), item["stream_url"], text_colour, 212, bg=bg, - max_w=rect[2] - (15 * gui.scale + toff)) + text = (_("Queue {N}").format(N=len(shift_selection))) + f" [{total}]" - 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)) + return [colours.menu_text, colours.menu_background, text] - 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]) +def toggle_rym(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_rym + prefs.show_rym ^= True + return None - if offset == 0: - offset = rect[2] + round(4 * gui.scale) - else: - offset = 0 - yy += round(43 * gui.scale) +def toggle_band(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_band + prefs.show_band ^= True + return None - if yy > y + 300 * gui.scale: - break +def toggle_wiki(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_wiki + prefs.show_wiki ^= True + return None - p += 1 +# 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 - # 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) +def toggle_gen(mode: int = 0) -> bool: + if mode == 1: + return prefs.show_gen + prefs.show_gen ^= True + return None - # 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)) +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")) - # 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 +def ser_band(track_id: int) -> None: + tr = pctl.get_track(track_id) + if tr.artist: + shoot_dl = threading.Thread(target=bandcamp_search, args=([tr.artist, ser_band_done])) + shoot_dl.daemon = True + shoot_dl.start() + show_message(_("Searching...")) - # if not mouse_down: - # self.drag = None +def ser_rym(index: int) -> None: + if len(pctl.master_library[index].artist) < 2: + return + line = "https://rateyourmusic.com/search?searchtype=a&searchterm=" + urllib.parse.quote( + pctl.master_library[index].artist) + webbrowser.open(line, new=2, autoraise=True) - def footer(self): +def copy_to_clipboard(text: str) -> None: + SDL_SetClipboardText(text.encode(errors="surrogateescape")) - y = self.y - x = self.x + round(15 * gui.scale) - w = self.w - h = self.h +def copy_from_clipboard(): + return SDL_GetClipboardText().decode() - 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")) +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")) - 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") +def ser_gen_thread(tr): + s_artist = tr.artist + s_title = tr.title - 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 + 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 visit_radio_site_show_test(p): -# return "website_url" in prefs.radio_urls[p] and prefs.radio_urls[p]["website_url"] + line = genius(s_artist, s_title, return_url=True) -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] + r = requests.head(line, timeout=10) -def visit_radio_station_site_deco(item): - return visit_radio_site_deco(item[1]) + 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 visit_radio_site(item): - if "website_url" in item and item["website_url"]: - webbrowser.open(item["website_url"], new=2, autoraise=True) +def ser_gen(track_id, get_lyrics=False): + tr = pctl.master_library[track_id] + if len(tr.title) < 1: + return -def visit_radio_station(item): - visit_radio_site(item[1]) + show_message(_("Searching...")) -def radio_saved_panel_test(_): - return radiobox.tab == 0 + shoot = threading.Thread(target=ser_gen_thread, args=[tr]) + shoot.daemon = True + shoot.start() -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 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) -class RenamePlaylistBox: +def clip_ar_tr(index: int) -> None: + line = pctl.master_library[index].artist + " - " + pctl.master_library[index].title - def __init__(self): + SDL_SetClipboardText(line.encode("utf-8")) - self.x = 300 - self.y = 300 - self.playlist_index = 0 +def tidal_copy_album(index: int) -> None: + t = pctl.master_library.get(index) + if t and t.file_ext == "TIDAL": + id = t.misc.get("tidal_album") + if id: + url = "https://listen.tidal.com/album/" + str(id) + copy_to_clipboard(url) - self.edit_generator = False +def is_tidal_track(_) -> bool: + return pctl.master_library[r_menu_index].file_ext == "TIDAL" - def toggle_edit_gen(self): +# def get_track_spot_url_show_test(_): +# if pctl.get_track(r_menu_index).misc.get("spotify-track-url"): +# return True +# return False - self.edit_generator ^= True - if self.edit_generator: +def get_track_spot_url(track_id: int) -> None: + track_object = pctl.get_track(track_id) + url = track_object.misc.get("spotify-track-url") + if url: + copy_to_clipboard(url) + show_message(_("Url copied to clipboard"), mode="done") + else: + show_message(_("No results found")) - if len(rename_text_area.text) > 0: - pctl.multi_playlist[self.playlist_index].title = rename_text_area.text +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 - pl = self.playlist_index - id = pl_to_id(pl) + return [line_colour, colours.menu_background, None] - text = pctl.gen_codes.get(id) - if not text: - text = "" +def get_spot_artist_track(index: int) -> None: + get_artist_spot(pctl.get_track(index)) - rename_text_area.set_text(text) - rename_text_area.highlight_none() +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) - gui.regen_single = rename_playlist_box.playlist_index - tauon.thread_manager.ready("worker") +def get_spot_album_track(index: int): + get_album_spot_active(pctl.get_track(index)) + +# 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)) - else: - rename_text_area.set_text(pctl.multi_playlist[self.playlist_index].title) - rename_text_area.highlight_none() - # rename_text_area.highlight_all() +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]) - def render(self): + if len(track_list) > 1: + if len(albums) > 0 and albums.count(albums[0]) == len(albums): + track = pctl.get_track(default_playlist[track_list[0]]) + artist = track.artist + if track.album_artist != "": + artist = track.album_artist + pctl.multi_playlist[pl].title = artist + " - " + albums[0][:50] - if gui.level_2_click: - inp.mouse_click = True - gui.level_2_click = False + elif len(track_list) == 1 and artists: + pctl.multi_playlist[pl].title = artists[0] - if inp.key_tab_press: - self.toggle_edit_gen() + if tree_view_box.dragging_name: + pctl.multi_playlist[pl].title = tree_view_box.dragging_name - text_w = ddt.get_text_w(rename_text_area.text, 315) - min_w = max(250 * gui.scale, text_w + 50 * gui.scale) + pctl.notify_change() - 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 +def queue_deco(): + if len(pctl.force_queue) > 0: + line_colour = colours.menu_text + else: + line_colour = colours.menu_text_disabled - # Draw background - ddt.rect(rect, bg) + return [line_colour, colours.menu_background, None] - # 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) +def bass_test(_) -> bool: + # return True + return prefs.backend == 1 - # 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 gstreamer_test(_) -> bool: + # return True + return prefs.backend == 2 - if self.edit_generator: - pl = self.playlist_index - id = pl_to_id(pl) - pctl.gen_codes[id] = rename_text_area.text +def field_copy(text_field) -> None: + text_field.copy() - if input_text or key_backspace_press: - gui.regen_single = rename_playlist_box.playlist_index - tauon.thread_manager.ready("worker") +def field_paste(text_field) -> None: + text_field.paste() - # 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 field_clear(text_field) -> None: + text_field.clear() - if hint_rect[0] + hint_rect[2] > window_size[0]: - hint_rect[0] = window_size[0] - hint_rect[2] +def vis_off() -> None: + gui.vis_want = 0 + gui.update_layout() + # gui.turbo = False - 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) +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 - text_colour = [150, 150, 150, 255] - title_colour = text_colour - code_colour = [250, 250, 250, 255] - hint_colour = [110, 110, 110, 255] + gui.vis_want = 1 + gui.update_layout() + # if prefs.backend == 2: + # show_message("Visualisers not implemented in GStreamer mode") + # gui.turbo = True - title_font = 311 - code_font = 311 - hint_font = 310 +def spec_on() -> None: + gui.vis_want = 2 + # if prefs.backend == 2: + # show_message("Not implemented") + gui.update_layout() - # ddt.pretty_rect = hint_rect +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 - 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) + gui.vis_want = 3 + if prefs.backend == 2: + show_message(_("Not implemented")) + # gui.turbo = True + prefs.spec2_colour_setting = "custom" + gui.update_layout() - 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) +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.")) - 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) +def sa_artist() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Artist", 220, False]) + gui.update_layout() - 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) +def sa_album_artist() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Album Artist", 220, False]) + gui.update_layout() - # 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 sa_composer() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Composer", 220, False]) + gui.update_layout() - 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) +def sa_title() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Title", 220, False]) + gui.update_layout() - 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) +def sa_album() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Album", 220, False]) + gui.update_layout() - # 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) +def sa_comment() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Comment", 300, False]) + gui.update_layout() - 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 sa_track() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["#", 25, True]) + gui.update_layout() - # yy += round(16 * gui.scale) - ddt.text((xx0, yy), _("Sorters"), title_colour, title_font) - yy += round(14 * gui.scale) +def sa_count() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["P", 25, True]) + gui.update_layout() - 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) +def sa_scrobbles() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["S", 25, True]) + gui.update_layout() - 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) +def sa_time() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Time", 55, True]) + gui.update_layout() - 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) +def sa_date() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Date", 55, True]) + gui.update_layout() - # ddt.pretty_rect = None +def sa_genre() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Genre", 150, False]) + gui.update_layout() - # 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 +def sa_file() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Filepath", 350, False]) + gui.update_layout() - 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 sa_filename() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Filename", 300, False]) + gui.update_layout() -class PlaylistBox: +def sa_codec() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Codec", 65, True]) + gui.update_layout() - def recalc(self): - self.tab_h = round(25 * gui.scale) - self.gap = round(2 * gui.scale) +def sa_bitrate() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Bitrate", 65, True]) + gui.update_layout() - self.text_offset = 2 * gui.scale - if gui.scale == 1.25: - self.text_offset = 3 +def sa_lyrics() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Lyrics", 50, True]) + gui.update_layout() - def __init__(self): +def sa_cue() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["CUE", 50, True]) + gui.update_layout() - self.scroll_on = prefs.old_playlist_box_position - self.drag = False - self.drag_source = 0 - self.drag_on = -1 +def sa_star() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Starline", 80, True]) + gui.update_layout() - self.adds = [] +def sa_disc() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Disc", 50, True]) + gui.update_layout() - self.indicate_w = round(2 * gui.scale) +def sa_rating() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["Rating", 80, True]) + gui.update_layout() - 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) +def sa_love() -> None: + gui.pl_st.insert(set_menu.reference + 1, ["❤", 25, True]) + # gui.pl_st.append(["❤", 25, True]) + gui.update_layout() +def key_love(index: int) -> bool: + return get_love_index(index) - # if gui.scale == 1.25: - self.tab_h = 0 - self.gap = 0 +def key_artist(index: int) -> str: + return pctl.master_library[index].artist.lower() - self.text_offset = 2 * gui.scale - self.recalc() +def key_album_artist(index: int) -> str: + return pctl.master_library[index].album_artist.lower() - def draw(self, x, y, w, h): +def key_composer(index: int) -> str: + return pctl.master_library[index].composer.lower() - global quick_drag +def key_comment(index: int) -> str: + return pctl.master_library[index].comment - # 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 +def key_title(index: int) -> str: + return pctl.master_library[index].title.lower() - max_tabs = (h - 10 * gui.scale) // (self.gap + self.tab_h) +def key_album(index: int) -> str: + return pctl.master_library[index].album.lower() - tab_title_colour = [230, 230, 230, 255] +def key_duration(index: int) -> int: + return pctl.master_library[index].length - bg_lumi = test_lumi(colours.playlist_box_background) - light_mode = False +def key_date(index: int) -> str: + return pctl.master_library[index].date - if bg_lumi < 0.55: - light_mode = True - tab_title_colour = [20, 20, 20, 255] +def key_genre(index: int) -> str: + return pctl.master_library[index].genre.lower() - dark_mode = False - if bg_lumi > 0.8: - dark_mode = True +def key_t(index: int): + # return str(pctl.master_library[index].track_number) + return index_key(index) - if light_mode: - indicate_w = round(3 * gui.scale) - else: - indicate_w = round(2 * gui.scale) +def key_codec(index: int) -> str: + return pctl.master_library[index].file_ext - show_scroll = False - tab_start = x + 10 * gui.scale +def key_bitrate(index: int) -> int: + return pctl.master_library[index].bitrate - if window_size[0] < 700 * gui.scale: - tab_start = x + 4 * gui.scale +def key_hl(index: int) -> int: + if len(pctl.master_library[index].lyrics) > 5: + return 0 + return 1 - if mouse_wheel != 0 and coll((x, y, w, h)): - self.scroll_on -= mouse_wheel +def sort_ass(h, invert=False, custom_list=None, custom_name=""): + global default_playlist - self.scroll_on = min(self.scroll_on, len(pctl.multi_playlist) - max_tabs + 1) + if custom_list is None: + if pl_is_locked(pctl.active_playlist_viewing): + show_message(_("Playlist is locked")) + return - self.scroll_on = max(self.scroll_on, 0) + name = gui.pl_st[h][0] + playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + else: + name = custom_name + playlist = custom_list - if len(pctl.multi_playlist) > max_tabs: - show_scroll = True - else: - self.scroll_on = 0 + key = None + ns = False - if show_scroll: - tab_start += 15 * gui.scale + if name == "Filepath": + key = key_filepath + if use_natsort: + key = key_fullpath + ns = True + if name == "Filename": + key = key_filepath # key_filename + if use_natsort: + key = key_fullpath + ns = True + if name == "Artist": + key = key_artist + if name == "Album Artist": + key = key_album_artist + if name == "Title": + key = key_title + if name == "Album": + key = key_album + if name == "Composer": + key = key_composer + if name == "Time": + key = key_duration + if name == "Date": + key = key_date + if name == "Genre": + key = key_genre + if name == "#": + key = key_t + if name == "S": + key = key_scrobbles + if name == "P": + key = key_playcount + if name == "Starline": + key = best + if name == "Rating": + key = key_rating + if name == "Comment": + key = key_comment + if name == "Codec": + key = key_codec + if name == "Bitrate": + key = key_bitrate + if name == "Lyrics": + key = key_hl + if name == "❤": + key = key_love + if name == "Disc": + key = key_disc + if name == "CUE": + key = key_cue - if colours.lm: - w -= round(6 * gui.scale) - tab_width = w - tab_start # - 0 * gui.scale + if custom_list is None: + if key is not None: - # 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 ns: + key = natsort.natsort_keygen(key=key, alg=natsort.PATH) - draw_pin_indicator = False # prefs.tabs_on_top + playlist.sort(key=key, reverse=invert) - # 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 + pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids = playlist + default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - # Process inputs - delete_pl = None - tab_on = 0 - yy = y + 5 * gui.scale - for i, pl in enumerate(pctl.multi_playlist): + pctl.playlist_view_position = 0 + logging.debug("Position changed by sort") + gui.pl_update = 1 - if tab_on >= max_tabs: - break - if i < self.scroll_on: - continue + elif custom_list is not None: + playlist.sort(key=key, reverse=invert) - # if not pl.hidden and i in tabs_on_top: - # continue + reload() - tab_on += 1 +def sort_dec(h): + sort_ass(h, True) - 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 +def hide_set_bar(): + gui.set_bar = False + gui.update_layout() + gui.pl_update = 1 - if tab_menu.active is False and middle_click: - delete_pl = i - # delete_playlist(i) - # break +def show_set_bar(): + gui.set_bar = True + gui.update_layout() + gui.pl_update = 1 - if mouse_up and self.drag and coll_point(mouse_up_position, (tab_start, yy - 1, tab_width, (self.tab_h + 1))): +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 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 +def toggle_dim_albums(mode: int = 0) -> bool: + if mode == 1: + return prefs.dim_art - # 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) + prefs.dim_art ^= True + gui.pl_update = 1 + gui.update += 1 - gui.update += 1 +def toggle_gallery_combine(mode: int = 0) -> bool: + if mode == 1: + return prefs.gallery_combine_disc - # 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: + prefs.gallery_combine_disc ^= True + reload_albums() - 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 toggle_gallery_click(mode: int = 0) -> bool: + if mode == 1: + return prefs.gallery_single_click - 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() + prefs.gallery_single_click ^= True - # 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 toggle_gallery_thin(mode: int = 0) -> bool: + if mode == 1: + return prefs.thin_gallery_borders - 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) + prefs.thin_gallery_borders ^= True + gui.update += 1 + update_layout_do() - # 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 toggle_gallery_row_space(mode: int = 0) -> bool: + if mode == 1: + return prefs.increase_gallery_row_spacing - yy += self.tab_h + self.gap + prefs.increase_gallery_row_spacing ^= True + gui.update += 1 + update_layout_do() - # Draw tabs - # delete_pl = None - tab_on = 0 - yy = y + 5 * gui.scale - for i, pl in enumerate(pctl.multi_playlist): +def toggle_galler_text(mode: int = 0) -> bool: + if mode == 1: + return gui.gallery_show_text - # if yy + self.tab_h > y + h: - # break - if tab_on >= max_tabs: - break - if i < self.scroll_on: - continue + gui.gallery_show_text ^= True + gui.update += 1 + update_layout_do() - tab_on += 1 + # Jump to playing album + if album_mode and gui.first_in_grid is not None: - name = pl.title - hidden = pl.hidden + if gui.first_in_grid < len(default_playlist): + goto_album(gui.first_in_grid, force=True) - # Background is insivible by default (for hightlighting if selected) - bg = [0, 0, 0, 0] +def toggle_card_style(mode: int = 0) -> bool: + if mode == 1: + return prefs.use_card_style - # 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] + prefs.use_card_style ^= True + gui.update += 1 - # 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] +def toggle_side_panel(mode: int = 0) -> bool: + global update_layout + global album_mode - # 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] + if mode == 1: + return prefs.prefer_side - # Get actual bg from blend for text bg - real_bg = alpha_blend(bg, colours.playlist_box_background) + prefs.prefer_side ^= True + update_layout = True - # Draw highlight - ddt.rect((tab_start, yy - round(1 * gui.scale), tab_width, self.tab_h), bg) + if album_mode or prefs.prefer_side is True: + gui.rsp = True + else: + gui.rsp = False - # Draw title text - text_start = 10 * gui.scale - if draw_pin_indicator: - # text_start = 40 * gui.scale - text_start = 32 * gui.scale + if prefs.prefer_side: + gui.rspw = gui.pref_rspw - 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)) +def force_album_view(): + toggle_album_mode(True) - if not pl.hidden and prefs.tabs_on_top: - cl = [255, 255, 255, 25] +def enter_combo(): + if not gui.combo_mode: + gui.combo_was_album = album_mode + gui.showcase_mode = False + gui.radio_view = False + if album_mode: + toggle_album_mode() + if gui.rsp: + gui.rsp = False + gui.combo_mode = True + gui.update_layout() - if light_mode: - cl = [0, 0, 0, 40] +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 - xx = tab_start + tab_width - self.lock_icon.w - self.lock_icon.render(xx, yy, cl) +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() - 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 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() - # Is mouse collided with tab? - hit = coll((tab_start + 50 * gui.scale, yy - 1, tab_width - 50 * gui.scale, (self.tab_h + 1))) +def standard_size(): + global album_mode + global window_size + global update_layout - # if not prefs.tabs_on_top: - if i == pctl.active_playlist_playing: + global album_mode_art_size - indicator_colour = colours.title_playing - if colours.lm: - indicator_colour = colours.seek_bar_fill + album_mode = False + gui.rsp = True + window_size = window_default_size + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - ddt.rect((tab_start + 0 - 2 * gui.scale, yy - round(1 * gui.scale), indicate_w, self.tab_h), indicator_colour) + gui.rspw = 80 + int(window_size[0] * 0.18) + update_layout = True + album_mode_art_size = 130 + # clear_img_cache() - # # 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 path_stem_to_playlist(path: str, title: str) -> None: + """Used with gallery power bar""" + playlist = [] - # 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]) + # 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) - 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]) + 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) - # 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 + pctl.multi_playlist.append(pl_gen( + title=os.path.basename(title).upper(), + playlist_ids=copy.deepcopy(playlist), + hide_title=False)) - 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 + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[pctl.active_playlist_viewing].title + "\" f\"" + path + "\"" - 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]) + switch_playlist(len(pctl.multi_playlist) - 1) - yy += self.tab_h + self.gap +def goto_album(playlist_no: int, down: bool = False, force: bool = False) -> list | int | None: + logging.debug("Postion set by album locate") - if delete_pl is not None: - # delete_playlist(delete_pl) - delete_playlist_ask(delete_pl) - gui.update += 1 + if core_timer.get() < 0.5: + return None - # Create new playlist if drag in blank space after tabs - rect = (x, yy, w - 10 * gui.scale, h - (yy - y)) - fields.add(rect) + global album_dex - 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) + # ---- + 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 right_click: - extra_tab_menu.activate(pctl.active_playlist_viewing) + px = 0 + row = 0 + re = 0 - # 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 + 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 - 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]) + # 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]: -def create_artist_pl(artist: str, replace: bool = False): - source_pl = pctl.active_playlist_viewing - this_pl = pctl.active_playlist_viewing + # 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 - 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 + else: + # Set the view to the calculated position + gui.album_scroll_px = px + gui.album_scroll_px -= album_v_slide_value - playlist = [] + gui.album_scroll_px = max(gui.album_scroll_px, 0 - album_v_slide_value) - 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 len(album_dex) > 0: + return album_dex[re] + return 0 - if replace: - pctl.multi_playlist[this_pl].playlist_ids[:] = playlist[:] - pctl.multi_playlist[this_pl].title = _("Artist: ") + artist - if album_mode: - reload_albums() + gui.update += 1 - # 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_album_mode(force_on=False): + global album_mode + global window_size + global update_layout + global album_playlist_width + global old_album_pos - pctl.gen_codes[pl_to_id(this_pl)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" + gui.gall_tab_enter = False - else: + if album_mode is True: - pctl.multi_playlist.append( - pl_gen( - title=_("Artist: ") + artist, - playlist_ids=playlist, - hide_title=False, - parent=pl_to_id(source_pl))) + 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() - pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" + gui.rsp = True - switch_playlist(len(pctl.multi_playlist) - 1) + gui.rspw = gui.pref_gallery_w -def aa_sort_alpha(): - prefs.artist_list_sort_mode = "alpha" - artist_list_box.saves.clear() + space = window_size[0] - gui.rspw + if gui.lsp: + space -= gui.lspw -def aa_sort_popular(): - prefs.artist_list_sort_mode = "popular" - artist_list_box.saves.clear() + 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() -def aa_sort_play(): - prefs.artist_list_sort_mode = "play" - artist_list_box.saves.clear() + reload_albums(quiet=True) -def toggle_artist_list_style(): - if prefs.artist_list_style == 1: - prefs.artist_list_style = 2 - else: - prefs.artist_list_style = 1 + # if pctl.active_playlist_playing == pctl.active_playlist_viewing: + # goto_album(pctl.playlist_playing_position) -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 album_mode: + if pctl.selected_in_playlist < len(pctl.playing_playlist()): + goto_album(pctl.selected_in_playlist) -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 toggle_gallery_keycontrol(always_exit=False): + if is_level_zero(): + if not album_mode: + toggle_album_mode() + gui.gall_tab_enter = True + gui.album_tab_mode = True + show_in_gal(pctl.selected_in_playlist, silent=True) + elif gui.gall_tab_enter or always_exit: + # Exit gallery and tab mode + toggle_album_mode() + else: + gui.album_tab_mode ^= True + if gui.album_tab_mode: + show_in_gal(pctl.selected_in_playlist, silent=True) -def verify_discogs(): - return len(prefs.discogs_pat) == 40 +def check_auto_update_okay(code, pl=None): + try: + cmds = shlex.split(code) + except Exception: + logging.exception("Malformed generator code!") + return False + return "auto" in cmds or ( + prefs.always_auto_update_playlists and + pctl.active_playlist_playing != pl and + "sf" not in cmds and + "rf" not in cmds and + "ra" not in cmds and + "sa" not in cmds and + "st" not in cmds and + "rt" not in cmds and + "plex" not in cmds and + "jelly" not in cmds and + "koel" not in cmds and + "tau" not in cmds and + "air" not in cmds and + "sal" not in cmds and + "slt" not in cmds and + "spl\"" not in code and + "tpl\"" not in code and + "tar\"" not in code and + "tmix\"" not in code and + "r" not in cmds) -def save_discogs_artist_thumb(artist, filepath): - logging.info("Searching discogs for artist image...") +def switch_playlist(number, cycle=False, quiet=False): + global default_playlist - # Make artist name url safe - artist = artist.replace("/", "").replace("\\", "").replace(":", "") + global search_index + global shift_selection - # 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"] + # Close any active menus + # for instance in Menu.instances: + # instance.active = False + close_all_menus() + if gui.radio_view: + if cycle: + pctl.radio_playlist_viewing += number + else: + pctl.radio_playlist_viewing = number + if pctl.radio_playlist_viewing > len(pctl.radio_playlists) - 1: + pctl.radio_playlist_viewing = 0 + return - # 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"] + gui.previous_playlist_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int - # Respect rate limit - rate_remaining = r.headers["X-Discogs-Ratelimit-Remaining"] - if int(rate_remaining) < 30: - time.sleep(5) + 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 - # 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 pl_follow: + # pctl.multi_playlist[pctl.playlist_active][1] = copy.deepcopy(pctl.playlist_playing) - response = urllib.request.urlopen(url, context=tls_context) - im = Image.open(response) + if gui.showcase_mode and gui.combo_mode and not quiet: + view_standard() - 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 + 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 - im = im.crop((left, upper, right, lower)) - im.save(filepath, "JPEG", quality=90) - im.close() - logging.info("Found artist image from Discogs") + if gall_pl_switch_timer.get() > 240: + gui.gallery_positions.clear() + gall_pl_switch_timer.set() -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") + gui.gallery_positions[gui.previous_playlist_id] = gui.album_scroll_px - response = urllib.request.urlopen(thumblink, timeout=10, context=tls_context) - info = response.info() + if cycle: + pctl.active_playlist_viewing += number + else: + pctl.active_playlist_viewing = number - t = io.BytesIO() - t.seek(0) - t.write(response.read()) - l = 0 - t.seek(0, 2) - l = t.tell() - t.seek(0) + 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 info.get_content_maintype() == "image" and l > 1000: - f = open(filepath, "wb") - f.write(t.read()) - f.close() + default_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids + pctl.playlist_view_position = pctl.multi_playlist[pctl.active_playlist_viewing].position + pctl.selected_in_playlist = pctl.multi_playlist[pctl.active_playlist_viewing].selected + logging.debug("Position changed by playlist change") + shift_selection = [pctl.selected_in_playlist] - if 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") + id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int -class ArtistList: + code = pctl.gen_codes.get(id) + if code is not None and check_auto_update_okay(code, pctl.active_playlist_viewing): + gui.regen_single_id = id + tauon.thread_manager.ready("worker") - def __init__(self): + if album_mode: + reload_albums(True) + if id in gui.gallery_positions: + gui.album_scroll_px = gui.gallery_positions[id] + else: + goto_album(pctl.playlist_view_position) - self.tab_h = round(60 * gui.scale) - self.thumb_size = round(55 * gui.scale) + if prefs.auto_goto_playing: + pctl.show_current(this_only=True, playing=False, highlight=True, no_switch=True) - self.current_artists = [] - self.current_album_counts = {} - self.current_artist_track_counts = {} + if prefs.shuffle_lock: + view_box.lyrics(hit=True) + if pctl.active_playlist_viewing: + pctl.active_playlist_playing = pctl.active_playlist_viewing + random_track() - self.thumb_cache = {} +def cycle_playlist_pinned(step): + if gui.radio_view: - self.to_fetch = "" - self.to_fetch_mbid_a = "" + 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 - self.scroll_position = 0 + 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 - self.id_to_load = "" + 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.d_click_timer = Timer() - self.d_click_ref = -1 +def activate_info_box(): + fader.rise() + pref_box.enabled = True - self.click_ref = -1 - self.click_highlight_timer = Timer() +def activate_radio_box(): + radiobox.active = True + radiobox.radio_field.clear() + radiobox.radio_field_title.clear() - self.saves = {} +def new_playlist_colour_callback(): + if gui.radio_view: + return [120, 90, 245, 255] + return [237, 80, 221, 255] - self.load = False +def new_playlist_deco(): + if gui.radio_view: + text = _("New Radio List") + else: + text = _("New Playlist") + return [colours.menu_text, colours.menu_background, text] - self.shown_letters = [] +def clean_db_show_test(_): + return gui.suggest_clean_db - self.hover_on = "NONE" - self.hover_timer = Timer(10) +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 - self.sample_tracks = {} +def clean_db_deco(): + return [colours.menu_text, [30, 150, 120, 255], _("Clean Database!")] - def load_img(self, artist): +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) - filepath = artist_info_box.get_data(artist, get_img_path=True) + if album_mode: + reload_albums() + gui.pl_update += 1 - if filepath and os.path.isfile(filepath): +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] - try: - g = io.BytesIO() - g.seek(0) +def show_import_music(_): + return gui.add_music_folder_ready - im = Image.open(filepath) +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 - 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 stt2(sec): + days, rem = divmod(sec, 86400) + hours, rem = divmod(rem, 3600) + min, sec = divmod(rem, 60) - im.thumbnail((self.thumb_size, self.thumb_size), Image.Resampling.LANCZOS) + s_day = str(days) + "d" + if s_day == "0d": + s_day = " " - im.save(g, "PNG") - g.seek(0) + s_hours = str(hours) + "h" + if s_hours == "0h" and s_day == " ": + s_hours = " " - 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) + s_min = str(min) + "m" - self.thumb_cache[artist] = [texture, sdl_rect] - except Exception: - logging.exception("Artist thumbnail processing error") - self.thumb_cache[artist] = None + return s_day.rjust(3) + " " + s_hours.rjust(3) + " " + s_min.rjust(3) - elif artist in prefs.failed_artists: - self.thumb_cache[artist] = None - elif not self.to_fetch: +def export_database(): + path = str(user_directory / "DatabaseExport.csv") + xport = open(path, "w") - if prefs.auto_dl_artist_data: - self.to_fetch = artist - tauon.thread_manager.ready("worker") + xport.write("Artist;Title;Album;Album artist;Track number;Type;Duration;Release date;Genre;Playtime;File path") - else: - self.thumb_cache[artist] = None + for index, track in pctl.master_library.items(): - def worker(self): + xport.write("\n") - if self.load: + 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)) - if after_scan: - return + xport.close() + show_message(_("Export complete."), _("Saved as: ") + path, mode="done") - self.prep() - self.load = False - return +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 self.to_fetch: +def clean_db() -> None: + global cm_clean_db + prefs.remove_network_tracks = False + cm_clean_db = True + tauon.thread_manager.ready("worker") - if get_lfm_wait_timer.get() < 2: - return +def clean_db2() -> None: + global cm_clean_db + prefs.remove_network_tracks = True + cm_clean_db = True + tauon.thread_manager.ready("worker") - 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 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 mbid and prefs.enable_fanart_artist: - save_fanart_artist_thumb(mbid, filepath3, preview=True) - got_image = True + show_message(_("{N} ratings imported").format(N=str(len(unique))), mode="done") - except Exception: - logging.exception("Failed to find image from fanart.tv") + gui.pl_update += 1 - 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 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 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) + 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) - self.to_fetch = "" + s = str(len(unique)) + " ratings imported" + if len(skipped) > 0: + s += f", {len(skipped)} skipped" + show_message(s, mode="done") - def prep(self): - self.scroll_position = 0 + gui.pl_update += 1 - 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] +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 - all = [] - artist_parents = {} - counts = {} - play_time = {} - filtered = 0 - b = 0 +def find_incomplete() -> None: + gen_incomplete(pctl.active_playlist_viewing) - try: +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] - for item in current_pl.playlist_ids: - b += 1 - if b % 100 == 0: - time.sleep(0.001) +def cast_search2() -> None: + chrome.rescan() - track = pctl.get_track(item) +def cast_search() -> None: - 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) + if tauon.chrome_mode: + pctl.stop() + chrome.end() + else: + if not chrome: + show_message(_("pychromecast not found")) + return + show_message(_("Searching for Chomecasts...")) + shooter(cast_search2) - artists = [x.strip() for x in artists.split(";")] +def clear_queue() -> None: + pctl.force_queue = [] + gui.pl_update = 1 + pctl.pause_queue = False - pp = 0 - if prefs.artist_list_sort_mode == "play": - pp = star_store.get(item) +def set_mini_mode_A1() -> None: + prefs.mini_mode_mode = 0 + set_mini_mode() - for artist in artists: +def set_mini_mode_B1() -> None: + prefs.mini_mode_mode = 1 + set_mini_mode() - if artist: +def set_mini_mode_A2() -> None: + prefs.mini_mode_mode = 2 + set_mini_mode() - # Add play time - if prefs.artist_list_sort_mode == "play": - p = play_time.get(artist, 0) - play_time[artist] = p + pp +def set_mini_mode_C1() -> None: + prefs.mini_mode_mode = 5 + set_mini_mode() - # Get a sample track for fallback art - if artist not in self.sample_tracks: - self.sample_tracks[artist] = track +def set_mini_mode_B2() -> None: + prefs.mini_mode_mode = 3 + set_mini_mode() - # 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 set_mini_mode_D() -> None: + prefs.mini_mode_mode = 4 + set_mini_mode() - 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 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 - current_album_counts = artist_parents +def stop() -> None: + pctl.stop() - 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 ")) +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() - except Exception: - logging.exception("Album scan failure") - time.sleep(4) - return +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) - # Artist-list, album-counts, scroll-position, playlist-length, number ignored - save = [all, current_album_counts, 0, len(current_pl.playlist_ids), counts, filtered] + key = random.choice(list(folders.keys())) + result = folders[key] + pctl.jump(*result) + pctl.show_current() - # 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 radio_random() -> None: + pctl.advance(rr=True) - 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 +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.saves[current_pl.uuid_int] = save - gui.update += 1 +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) - def locate_artist_letter(self, text): + rect = (x - round(5 * gui.scale), y - round(4 * gui.scale), round(80 * gui.scale), round(16 * gui.scale)) + gui.heart_fields.append(rect) - if not text or prefs.artist_list_sort_mode != "alpha": - return + if coll(rect) and (inp.mouse_click or (is_level_zero() and not quick_drag)): + gui.pl_update = 2 + pp = mouse_position[0] - x - 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 + if pp < 5 * gui.scale: + rat = 0 + elif pp > 70 * gui.scale: + rat = 10 + else: + rat = pp // (star_row_icon.w // 2) - 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 + 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 locate_artist(self, track: TrackClass): + # bg = colours.grey(40) + bg = [255, 255, 255, 17] + fg = colours.grey(210) - 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 + if gui.tracklist_bg_is_light: + bg = [0, 0, 0, 25] + fg = colours.grey(70) - 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 + 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) - def draw_card_text_only(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): + for ss in range(5): - album_mode = False - for albums in self.current_album_counts.values(): - if len(albums) > 1: - album_mode = True - break + xx = x + ss * star_row_icon.w - if not album_mode: - count = self.current_artist_track_counts[artist] - if count > 1: - text = _("{N} tracks").format(N=str(count)) + 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: - text = _("{N} track").format(N=str(count)) + star_row_icon.render(xx, y, fg2) else: - album_count = len(self.current_album_counts[artist]) - if album_count > 1: - text = _("{N} tracks").format(N=str(album_count)) + + 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: - text = _("{N} track").format(N=str(album_count)) + star_row_icon.render(xx, y, fg) - if gui.preview_artist_loading == artist: - # . Max 20 chars. Alt: Downloading image, Loading image - text = _("Downloading data...") +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")] - x_text = round(10 * gui.scale) - artist_font = 313 - count_font = 312 - extra_text_space = 0 - ddt.text( - (x_text, y + round(2 * gui.scale)), artist, line1_colour, artist_font, - extra_text_space + w - x_text - 30 * gui.scale, bg=bg) - # ddt.text((x_text, y + self.tab_h // 2 - 2 * gui.scale), text, line2_colour, count_font, - # extra_text_space + w - x_text - 15 * gui.scale, bg=bg) +def 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 draw_card_with_thumbnail(self, artist, x, y, w, area, thin_mode, line1_colour, line2_colour, light_mode, bg): +def bar_love_notify() -> None: + bar_love(notify=True) - if artist not in self.thumb_cache: - self.load_img(artist) +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] - 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) + shoot_love = threading.Thread(target=love, args=[True, track_id, False, notify]) + shoot_love.daemon = True + shoot_love.start() - 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 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 - 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)) +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() - 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) +def toggle_spotify_like_active_deco(): + tr = pctl.playing_object() + text = _("Spotify Like Track") - ddt.rect(tab_rect, back_colour_2) - bg = back_colour_2 + 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") - ddt.rect(rect, back_colour) - ddt.rect(rect, border_colour) + return [colours.menu_text, colours.menu_background, text] - fields.add(rect) - if coll(rect) and is_level_zero(True): - self.hover_any = True +def locate_artist() -> None: + track = pctl.playing_object() + if not track: + return - hover_delay = 0.5 - if gui.compact_artist_list: - hover_delay = 2 + artist = track.artist + if track.album_artist: + artist = track.album_artist - 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() + block_starts = [] + current = False + for i in range(len(default_playlist)): + track = pctl.get_track(default_playlist[i]) + if current is False: + if track.artist == artist or track.album_artist == artist or ( + "artists" in track.misc and artist in track.misc["artists"]): + block_starts.append(i) + current = True + elif (track.artist != artist and track.album_artist != artist) or ( + "artists" in track.misc and artist in track.misc["artists"]): + current = False - if path: - set_artist_preview(path, artist, round(thumb_x + self.thumb_size), round(y)) + if block_starts: - if inp.mouse_click: - self.hover_timer.force_set(-2) - gui.delay_frame(2 + hover_delay) + next = False + for start in block_starts: - drawn = False - if artist in self.thumb_cache: - thumb = self.thumb_cache[artist] - if thumb is not None: - thumb[1].x = thumb_x - thumb[1].y = round(y) - SDL_RenderCopy(renderer, thumb[0], None, thumb[1]) - drawn = True - if prefs.art_bg: - rect = SDL_Rect(thumb_x, round(y), self.thumb_size, self.thumb_size) - if (rect.y + rect.h) > window_size[1] - gui.panelBY: - diff = (rect.y + rect.h) - (window_size[1] - gui.panelBY) - rect.h -= round(diff) - style_overlay.hole_punches.append(rect) - if not drawn: - track = self.sample_tracks.get(artist) - if track: - tauon.gall_ren.render(track, (round(thumb_x), round(y)), self.thumb_size) + if next: + pctl.selected_in_playlist = start + pctl.playlist_view_position = start + shift_selection.clear() + break - 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) + if pctl.selected_in_playlist == start: + next = True + continue - # Draw labels - if not thin_mode or (coll(area) and is_level_zero() and y + self.tab_h < window_size[1] - gui.panelBY): + else: + pctl.selected_in_playlist = block_starts[0] + pctl.playlist_view_position = block_starts[0] + shift_selection.clear() - album_mode = False - for albums in self.current_album_counts.values(): - if len(albums) > 1: - album_mode = True - break + 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")) - 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)) + logging.debug("Position changed by artist locate") - if gui.preview_artist_loading == artist: - # . Max 20 chars. Alt: Downloading image, Loading image - text = _("Downloading data...") + gui.pl_update += 1 - ddt.text( - (x_text, y + self.tab_h // 2 - 19 * gui.scale), artist, line1_colour, artist_font, - extra_text_space + w - x_text - 30 * gui.scale, bg=bg) - ddt.text( - (x_text, y + self.tab_h // 2 - 2 * gui.scale), text, line2_colour, count_font, - extra_text_space + w - x_text - 15 * gui.scale, bg=bg) +def activate_search_overlay() -> None: + if cm_clean_db: + show_message(_("Please wait for cleaning process to finish")) + return + search_over.active = True + search_over.delay_enter = False + search_over.search_text.selection = 0 + search_over.search_text.cursor_position = 0 + search_over.spotify_mode = False - def draw_card(self, artist, x, y, w): +def get_album_spot_url_active() -> None: + tr = pctl.playing_object() + if tr: + url = tauon.spot_ctl.get_album_url_from_local(tr) - 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) + if url: + copy_to_clipboard(url) + show_message(_("URL copied to clipboard"), mode="done") + else: + show_message(_("No results found")) - fields.add(area) +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") - light_mode = False - line1_colour = [235, 235, 235, 255] - line2_colour = [255, 255, 255, 120] - fade_max = 50 + return [colours.menu_text, colours.menu_background, text] - thin_mode = False - if gui.compact_artist_list: - thin_mode = True - line2_colour = [115, 115, 115, 255] +def goto_playing_extra() -> None: + pctl.show_current(highlight=True) - 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 show_spot_playing_deco(): + if not (tauon.spot_ctl.coasting or tauon.spot_ctl.playing): + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] + +def show_spot_coasting_deco(): + if tauon.spot_ctl.coasting: + return [colours.menu_text, colours.menu_background, None] + return [colours.menu_text_disabled, colours.menu_background, None] + +def show_spot_playing() -> None: + if pctl.playing_state != 0 and pctl.playing_state != 3 and not tauon.spot_ctl.coasting and not tauon.spot_ctl.playing: + pctl.stop() + tauon.spot_ctl.update(start=True) + +def spot_transfer_playback_here() -> None: + tauon.spot_ctl.preparing_spotify = True + if not (tauon.spot_ctl.playing or tauon.spot_ctl.coasting): + tauon.spot_ctl.update(start=True) + pctl.playerCommand = "spotcon" + pctl.playerCommandReady = True + pctl.playing_state = 3 + shooter(tauon.spot_ctl.transfer_to_tauon) - # Fade on click - bg = colours.side_panel_background - if not thin_mode: +def spot_import_albums() -> None: + if not tauon.spot_ctl.spotify_com: + tauon.spot_ctl.spotify_com = True + shoot = threading.Thread(target=tauon.spot_ctl.get_library_albums) + shoot.daemon = True + shoot.start() + else: + show_message(_("Please wait until current job is finished")) - if 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: +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")) - fade = 0 - t = self.click_highlight_timer.get() - if self.click_ref == artist and (t < 2.2 or artist_list_menu.active): +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")) - if t < 1.9 or artist_list_menu.active: - fade = fade_max - else: - fade = fade_max - round((t - 1.9) / 0.3 * fade_max) +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])) - gui.update += 1 - ddt.rect(area, [50, 50, 50, fade]) + 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")) - bg = alpha_blend([50, 50, 50, fade], colours.side_panel_background) +def spot_import_context() -> None: + shooter(tauon.spot_ctl.import_context) - 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) +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") - 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 + return [colours.menu_text, colours.menu_background, text] - double_click = False - if self.d_click_timer.get() < 0.4 and self.d_click_ref == artist: - double_click = True +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,)) - self.click_highlight_timer.set() +# 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 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) +def toggle_auto_theme(mode: int = 0) -> None: + if mode == 1: + return prefs.colour_from_image + prefs.colour_from_image ^= True + gui.theme_temp_current = -1 - blocks = [] - current_block = [] + gui.reload_theme = True - in_artist = False - this_artist = artist.casefold() - last_ref = None - on = 0 + # if prefs.colour_from_image and prefs.art_bg and not key_shift_down: + # toggle_auto_bg() - 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) +def toggle_auto_bg(mode: int= 0) -> bool | None: + if mode == 1: + return prefs.art_bg + prefs.art_bg ^= True - 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 prefs.art_bg: + gui.update = 60 - if current_block: - blocks.append(current_block) - current_block = [] + 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 - #logging.info(blocks) - # return +def toggle_auto_bg_strong(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.art_bg_stronger == 2 - # 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.art_bg_stronger == 2: + prefs.art_bg_stronger = 1 + else: + prefs.art_bg_stronger = 2 + gui.update_layout() + return None - if not blocks: - return +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 - #select = block_starts[0] +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 - # 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] +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 - gui.pl_update += 1 +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 - self.click_highlight_timer.set() +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 - select = blocks[0][0] +def toggle_notifications(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.show_notifications - if double_click: - # Stat first artist track in playlist + prefs.show_notifications ^= True - pctl.jump(default_playlist[select], pl_position=select) - pctl.playlist_view_position = select - pctl.selected_in_playlist = select - shift_selection.clear() - self.d_click_timer.force_set(10) - else: - # Goto next artist section in playlist - c = pctl.selected_in_playlist - next = False - track = pctl.get_track_in_playlist(c, -1) - if track is None: - logging.error("Index out of range!") - pctl.selected_in_playlist = 0 - return - if track.artist.casefold != artist.casefold: - pctl.selected_in_playlist = 0 - pctl.playlist_view_position = 0 - if len(blocks) == 1: - block = blocks[0] - if len(block) > 1: - if c < block[0] or c >= block[-1]: - select = block[0] - toast(_("First of artist's albums ({N} albums)") - .format(N=len(block))) - else: - select = block[-1] - toast(_("Last of artist's albums ({N} albums)") - .format(N=len(block))) - else: - select = None - for bb, block in enumerate(blocks): - for i, al in enumerate(block): - if al <= c: - continue - next = True - if i == 0: - select = al - if len(block) > 1: - toast(_("Start of location {N} of {T} ({Nb} albums)") - .format(N=bb + 1, T=len(blocks), Nb=len(block))) - else: - toast(_("Location {N} of {T}") - .format(N=bb + 1, T=len(blocks))) - break + if prefs.show_notifications: + if not de_notify_support: + show_message(_("Notifications for this DE not supported"), "", mode="warning") + return None - 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 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 - 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 toggle_mini_lyrics(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.show_lyrics_side + prefs.show_lyrics_side ^= True + return None - if middle_click: - self.click_ref = artist - self.click_highlight_timer.set() - create_artist_pl(artist) +def toggle_showcase_vis(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.showcase_vis - if right_click: - self.click_ref = artist - self.click_highlight_timer.set() + prefs.showcase_vis ^= True + gui.update_layout() + return None - artist_list_menu.activate(in_reference=artist) +def toggle_level_meter(mode: int = 0) -> bool | None: + if mode == 1: + return gui.vis_want != 0 - def render(self, x, y, w, h): + if gui.vis_want == 0: + gui.vis_want = 1 + else: + gui.vis_want = 0 - if prefs.artist_list_style == 1: - self.tab_h = round(60 * gui.scale) - else: - self.tab_h = round(22 * gui.scale) + gui.update_layout() + return None - viewing_pl_id = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int +# 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() - # use parent playlst is set - if pctl.multi_playlist[pctl.active_playlist_viewing].parent_playlist_id: +def level_meter_special_2(): + gui.level_meter_colour_mode = 2 - # 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 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") - 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] + bg = colours.menu_background - 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 + return [colours.menu_text, bg, line] - else: +def lastfm_colour() -> list[int] | None: + if not prefs.scrobble_hold: + return [250, 50, 50, 255] + return None - # 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") +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 - area = (x, y, w, h) - area2 = (x + 1, y, w - 3, h) +def lb_mode() -> bool: + return prefs.enable_lb - ddt.rect(area, colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background +def get_album_art_url(tr: TrackClass): - 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) + artist = tr.album_artist + if not tr.album: + return None + if not artist: + artist = tr.artist + if not artist: + return None - range = (h // self.tab_h) - 1 + 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 - whole_rage = math.floor(h // self.tab_h) + if not release_group_id: + release_group_id = tr.misc.get("musicbrainz_releasegroupid") - if range > 4 and self.scroll_position > len(self.current_artists) - range: - self.scroll_position = len(self.current_artists) - range + if not release_id: + release_id = tr.misc.get("musicbrainz_albumid") - if len(self.current_artists) <= whole_rage: - self.scroll_position = 0 + 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 - fields.add(area2) - scroll_x = x + w - 18 * gui.scale - if colours.lm: - scroll_x = x + w - 22 * gui.scale - if (coll(area2) or artist_list_scroll.held) and not pref_box.enabled: - scroll_width = 15 * gui.scale - inset = 0 - if gui.compact_artist_list: - pass - # scroll_width = round(6 * gui.scale) - # scroll_x += round(9 * gui.scale) - else: - self.scroll_position = artist_list_scroll.draw( - scroll_x, y + 1, scroll_width, h, self.scroll_position, - len(self.current_artists) - range, r_click=right_click, - jump_distance=35, extend_field=6 * gui.scale) + if not 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 - if not self.current_artists: - text = _("No artists in playlist") + 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 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...") + base_url = "https://coverartarchive.org/release-group/" + url = f"{base_url}{release_group_id}" - ddt.text( - (x + w // 2, y + (h // 7), 2), text, alpha_mod(colours.side_bar_line2, 100), 212, - max_w=w - 17 * gui.scale) + 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") - yy = y + 12 * gui.scale + if release_id and not image_data: + url = pctl.mbid_image_url_cache.get(release_id) + if url: + return url - i = int(self.scroll_position) + base_url = "https://coverartarchive.org/release/" + url = f"{base_url}{release_id}" - if viewing_pl_id in self.saves: - self.saves[viewing_pl_id][2] = self.scroll_position + 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") - prefetch_mode = False - prefetch_distance = 22 + 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 - self.shown_letters.clear() + url = image["thumbnails"].get("250") + if url is None: + url = image["thumbnails"].get("small") - self.hover_any = False + if url: + logging.info("got mb image url for discord") + pctl.mbid_image_url_cache[final_id] = url + return url - for i, artist in enumerate(self.current_artists[i:], start=i): + pctl.album_mbid_release_cache[(artist, tr.album)] = None + pctl.album_mbid_release_group_cache[(artist, tr.album)] = None - if not prefetch_mode: - self.draw_card(artist, x, round(yy), w) + return None - yy += self.tab_h +def discord_loop() -> None: + prefs.discord_active = True - if yy - y > h - 24 * gui.scale: - prefetch_mode = True - continue + try: + if not pctl.playing_ready(): + return + asyncio.set_event_loop(asyncio.new_event_loop()) - 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 + # logging.info("Attempting to connect to Discord...") + client_id = "954253873160286278" + RPC = Presence(client_id) + RPC.connect() - if not self.hover_any: - gui.preview_artist = "" - self.hover_timer.force_set(10) - artist_preview_render.show = False - self.hover_on = False + logging.info("Discord RPC connection successful.") + time.sleep(1) + start_time = time.time() + idle_time = Timer() -class TreeView: + state = 0 + index = -1 + br = False + gui.discord_status = "Connected" + gui.update += 1 + current_state = 0 - def __init__(self): + while True: + while True: - self.trees = {} # Per playlist tree - self.rows = [] # For display (parsed from tree) - self.rows_id = "" + current_index = pctl.playing_object().index + if pctl.playing_state == 3: + current_index = radiobox.song_key - self.opens = {} # Folders clicks to show per playlist + 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() - self.scroll_positions = {} + 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 - # Recursive gen_rows vars - self.count = 0 - self.depth = 0 + break - self.background_processing = False - self.d_click_timer = Timer(100) - self.d_click_id = "" + if current_state == 0 and idle_time.get() > 13: + logging.info("Pause discord RPC...") + gui.discord_status = "Idle" + RPC.clear(pid) + # RPC.close() - self.menu_selected = "" - self.folder_colour_cache = {} - self.dragging_name = "" + 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) - self.force_opens = [] - self.click_drag_source = None + if not prefs.disconnect_discord: + continue - self.tooltip_on = "" - self.tooltip_timer = Timer(10) + time.sleep(2) - self.lock_pl = None + if prefs.disconnect_discord: + RPC.clear(pid) + RPC.close() + prefs.disconnect_discord = False + gui.discord_status = "Not connected" + br = True + break - # self.bold_colours = ColourGenCache(0.6, 0.7) + if br: + break - def clear_all(self): - self.rows_id = "" - self.trees.clear() + 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 collapse_all(self): - pl_id = pl_to_id(pctl.active_playlist_viewing) + if tr.album: + album = tr.album + else: + album = _("Unknown Album") + if pctl.playing_state == 3: + album = radiobox.loaded_station["title"] - if self.lock_pl: - pl_id = self.lock_pl + if len(album) == 1: + album += " " - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens + if state == 1: + #logging.info("PLAYING: " + title) + #logging.info(start_time) + url = get_album_art_url(pctl.playing_object()) - opens.clear() - self.rows_id = "" + 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) - def clear_target_pl(self, pl_number, pl_id=None): + else: + #logging.info("Discord RPC - Stop") + RPC.update( + pid=pid, + state="Idle", + large_image="tauon-standard") - if pl_id is None: - pl_id = pl_to_id(pl_number) + time.sleep(5) - if gui.lsp and prefs.left_panel_mode == "folder view": + if prefs.disconnect_discord: + RPC.clear(pid) + RPC.close() + prefs.disconnect_discord = False + break - 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] + 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 show_track(self, track: TrackClass) -> None: + finally: + loop = asyncio.get_event_loop() + if not loop.is_closed(): + loop.close() + prefs.discord_active = False - if track is None: - return +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() - # 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 +def open_donate_link() -> None: + webbrowser.open("https://github.com/sponsors/Taiko2k", new=2, autoraise=True) - tree = self.trees.get(pl_id) - if not tree: - return +def stop_quick_add() -> None: + pctl.quick_add_target = None - scroll_position = self.scroll_positions.get(pl_id) - if scroll_position is None: - scroll_position = 0 +def show_stop_quick_add(_) -> bool: + return pctl.quick_add_target is not None - # Clear all opened folders - opens.clear() +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() - # Set every folder in path as opened - path = "" - crumbs = track.parent_folder_path.split("/")[1:] - for c in crumbs: - path += "/" + c - opens.append(path) +# 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] - # Regenerate row display - self.gen_rows(tree, opens) +def view_standard_meta() -> None: + # if gui.show_playlist is False: + # gui.show_playlist = True + if album_mode: + toggle_album_mode() - # 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 gui.combo_mode: + exit_combo() - scroll_position = i - 5 - scroll_position = max(scroll_position, 0) - break + if not gui.rsp: + toggle_side_panel() - 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) + global update_layout + update_layout = True + # gui.rspw = 80 + int(window_size[0] * 0.18) - self.scroll_positions[pl_id] = scroll_position +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() - gui.update_layout() - gui.update += 1 +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 get_pl_id(self): - if self.lock_pl: - return self.lock_pl - return pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int +# 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 render(self, x, y, w, h): +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() - global quick_drag +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 - pl_id = self.get_pl_id() + if gui.set_mode: + return [tc, colours.menu_background, _("Disable Columns")] + return [tc, colours.menu_background, _("Enable Columns")] - tree = self.trees.get(pl_id) +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 - # 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 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")] - self.playlist_id_on = pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int +def toggle_playlist_break() -> None: + pctl.multi_playlist[pctl.active_playlist_viewing].hide_title ^= 1 + gui.pl_update = 1 - opens = self.opens.get(pl_id) - if opens is None: - opens = [] - self.opens[pl_id] = opens +def transcode_single(item: list[tuple[int, str]], manual_directory: str | None = None, manual_name: str | None = None): + global core_use + global dl_use - scroll_position = self.scroll_positions.get(pl_id) - if scroll_position is None: - scroll_position = 0 + 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 - area = (x, y, w, h) - fields.add(area) - ddt.rect(area, colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background + t = pctl.master_library[track] - 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 + path = t.fullpath + cleanup = False - # 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 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 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 not os.path.isfile(path): + show_message(_("Encoding warning: Missing one or more files")) + core_use -= 1 + return - else: - return + out_line = encode_track_name(t) - 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 + if not (output / _("output")).exists(): + (output / _("output")).mkdir() + target_out = str(output / _("output") / (str(track) + "." + codec)) - yy = y + round(11 * gui.scale) - xx = x + round(22 * gui.scale) + command = tauon.get_ffmpeg() + " " - spacing = round(21 * gui.scale) - max_scroll = len(self.rows) - (h // round(22 * gui.scale)) + if not t.is_cue: + command += '-i "' + else: + command += "-ss " + str(t.start_time) + command += " -t " + str(t.length) - mouse_in = coll(area) + command += ' -i "' - # 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) + command += path.replace('"', '\\"') - focused = is_level_zero() + 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("'", "") + '" ' - # 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 codec != "flac": + command += " -b:a " + str(bitrate) + "k -vn " - self.scroll_positions[pl_id] = scroll_position + command += '"' + target_out.replace('"', '\\"') + '"' - # Draw folder rows - playing_track = pctl.playing_object() - max_w = w - round(45 * gui.scale) + # logging.info(shlex.split(command)) + startupinfo = None + if system == "Windows" or msys: + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - light_mode = test_lumi(colours.side_panel_background) < 0.5 - semilight_mode = test_lumi(colours.side_panel_background) < 0.8 + if not msys: + command = shlex.split(command) - for i, item in enumerate(self.rows): + subprocess.call(command, stdout=subprocess.PIPE, shell=False, startupinfo=startupinfo) + + logging.info("FFmpeg finished") + if codec == "opus" and prefs.transcode_opus_as: + codec = "ogg" - if i < scroll_position: - continue + # logging.info(target_out) - if yy > y + h - spacing: - break + 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) - target = item[1] + "/" + item[0] + 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) - 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) + old_key = star_store.key(track) + old_star = star_store.full_get(track) - # text_colour = [255, 255, 255, 100] - text_colour = rgb_add_hls(colours.side_panel_background, 0, 0.35, -0.15) + try: + send2trash(pctl.master_library[track].fullpath) + except Exception: + logging.exception("File trash error") - box_colour = [200, 100, 50, 255] + if os.path.isfile(pctl.master_library[track].fullpath): + try: + os.remove(pctl.master_library[track].fullpath) + except Exception: + logging.exception("File delete error") - if semilight_mode: - text_colour = [255, 255, 255, 180] + pctl.master_library[track].fullpath = new_name + pctl.master_library[track].file_ext = codec.upper() - if light_mode: - text_colour = [0, 0, 0, 200] + # Update and merge playtimes + new_key = star_store.key(track) + if old_star and (new_key != old_key): - full_folder_path = item[1] + "/" + item[0] + new_star = star_store.full_get(track) + if new_star is None: + new_star = star_store.new_object() - # 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] + 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])) - # 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] + if old_key in star_store.db: + del star_store.db[old_key] - # 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] + star_store.db[new_key] = new_star - 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)) + gui.transcoding_bach_done += 1 + if cleanup: + os.remove(path) + core_use -= 1 + gui.update += 1 - if mouse_in and not tree_view_scroll.held: +def cue_scan(content: str, tn: TrackClass) -> int | None: + # Get length from backend - if middle_click: - stem_to_new_playlist(full_folder_path) + lasttime = tn.length - elif right_click: + content = content.replace("\r", "") + content = content.split("\n") - if item[3]: + #logging.info(content) - 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 + global added - elif inp.mouse_click: - # quick_drag = True + cued = [] - if not self.click_drag_source: - self.click_drag_source = item - set_drag_source() + LENGTH = 0 + PERFORMER = "" + TITLE = "" + START = 0 + DATE = "" + ALBUM = "" + GENRE = "" + MAIN_PERFORMER = "" - elif mouse_up and self.click_drag_source == item: - # Click tree level folder to open/close branch + for LINE in content: + if 'TITLE "' in LINE: + ALBUM = LINE[7:len(LINE) - 2] - 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 'PERFORMER "' in LINE: + while LINE[0] != "P": + LINE = LINE[1:] - if item[3]: + MAIN_PERFORMER = LINE[11:len(LINE) - 2] - # Locate the first track of folder in playlist - track_id = None - for p, id in enumerate(default_playlist): - if msys: - if pctl.get_track(id).fullpath.startswith(target.lstrip("/")): - track_id = id - break - elif pctl.get_track(id).fullpath.startswith(target): - track_id = id - break - else: # Fallback to folder name if full-path not found (hack for networked items) - for p, id in enumerate(default_playlist): - if pctl.get_track(id).parent_folder_name == item[0]: - track_id = id - break + if "REM DATE" in LINE: + DATE = LINE[9:len(LINE) - 1] - 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 "REM GENRE" in LINE: + GENRE = LINE[10:len(LINE) - 1] - # Double click base folder to play - else: - pctl.jump(track_id) + if "TRACK " in LINE: + break - # Regenerate display rows after clicking - self.gen_rows(tree, opens) + 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 - # 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] + START = int(LINE[pos - 2:pos]) + (int(LINE[pos - 5:pos - 3]) * 60) + LENGTH = int(lasttime) - START + lasttime = START - # 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 + 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 mouse_in: - tw = ddt.get_text_w(item[0], font) + elif 'TITLE "' in LINE: - if self.tooltip_on != item: - self.tooltip_on = item - self.tooltip_timer.set() - gui.frame_callback_list.append(TestTimer(0.6)) + 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 - 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) + elif "TRACK " in LINE: - # # 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) + pos = 0 + while LINE[pos] != "K": + pos += 1 + if pos > 15: + return 1 + TN = LINE[pos + 2:pos + 4] - 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) + TN = int(TN) - # 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) + # try: + # bitrate = audio.info.bitrate + # except Exception: + # logging.exception("Failed to set audio bitrate") + # bitrate = 0 - 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 PERFORMER == "": + PERFORMER = MAIN_PERFORMER - yy += spacing + nt = copy.deepcopy(tn) - 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 + nt.cue_sheet = "" + nt.is_embed_cue = True - self.dragging_name = self.click_drag_source[0] - logging.info(self.dragging_name) + nt.index = pctl.master_count + # nt.fullpath = filepath.replace('\\', '/') + # nt.filename = filename + # nt.parent_folder_path = os.path.dirname(filepath.replace('\\', '/')) + # nt.parent_folder_name = os.path.splitext(os.path.basename(filepath))[0] + # nt.file_ext = os.path.splitext(os.path.basename(filepath))[1][1:].upper() + if MAIN_PERFORMER: + nt.album_artist = MAIN_PERFORMER + if PERFORMER: + nt.artist = PERFORMER + if GENRE: + nt.genre = GENRE + nt.title = TITLE + nt.length = LENGTH + # nt.bitrate = source_track.bitrate + if ALBUM: + nt.album = ALBUM + if DATE: + nt.date = DATE.replace('"', "") + nt.track_number = TN + nt.start_time = START + nt.is_cue = True + nt.size = 0 # source_track.size + # nt.samplerate = source_track.samplerate + if TN == 1: + nt.size = os.path.getsize(nt.fullpath) - if "/" in self.dragging_name: - self.dragging_name = os.path.basename(self.dragging_name) + pctl.master_library[pctl.master_count] = nt - 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 + cued.append(pctl.master_count) + # loaded_paths_cache[filepath.replace('\\', '/')] = pctl.master_count + # added.append(pctl.master_count) - if self.dragging_name and not quick_drag: - self.dragging_name = "" - if not mouse_down: - self.click_drag_source = None + pctl.master_count += 1 + LENGTH = 0 + PERFORMER = "" + TITLE = "" + START = 0 + TN = 0 - def gen_row(self, tree_point, path, opens): + added += reversed(cued) - 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 + # cue_list.append(filepath) - 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 +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 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])]) + if pl_id: + pl_number = id_to_pl(pl_id) + else: + pl_number = pctl.active_playlist_viewing - # 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 + playlist = pctl.multi_playlist[pl_number].playlist_ids - # 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]) + if track_id is None: + track_id = playlist[track_position] - self.depth += 1 - enter_level = True + if playlist[track_position] != track_id: + return [] - self.gen_row(item[0], p, opens) + tracks = [] + album_parent_path = pctl.get_track(track_id).parent_folder_path - if enter_level: - self.depth -= 1 + i = track_position - def gen_rows(self, tree, opens): - self.count = 0 - self.depth = 0 - self.rows.clear() - self.force_opens.clear() + while i < len(playlist): + if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: + break - self.gen_row(tree, "", opens) + tracks.append(playlist[i]) + i += 1 - gui.update_layout() - gui.update += 1 + return tracks - def gen_tree(self, pl_id): - pl_no = id_to_pl(pl_id) - if pl_no is None: - return +def worker3(): + while True: + # time.sleep(0.04) - playlist = pctl.multi_playlist[pl_no].playlist_ids - # Generate list of all unique folder paths - paths = [] - z = 5000 - for p in playlist: + # if tauon.thread_manager.exit_worker3: + # tauon.thread_manager.exit_worker3 = False + # return + # time.sleep(1) - 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) + tauon.gall_ren.worker_render() - # 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] +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() - self.trees[pl_id] = tree - self.rows_id = "" - self.background_processing = False - gui.update += 1 - tauon.wake() + time.sleep(0.01) + if pctl.playing_state > 0 and pctl.playing_time < 5: + gui.style_worker_timer.set() + if gui.style_worker_timer.get() > 5: + return -def 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 worker2(): + while True: + worker2_lock.acquire() -# 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 search_over.search_text.text and not (len(search_over.search_text.text) == 1 and ord(search_over.search_text.text[0]) < 128): -class QueueBox: + if search_over.spotify_mode: + t = spot_search_rate_timer.get() + if t < 1: + time.sleep(1 - t) + spot_search_rate_timer.set() + logging.info("Spotify search") + search_over.results.clear() + results = tauon.spot_ctl.search(search_over.search_text.text) + if results is not None: + search_over.results = results + else: + search_over.active = False + gui.show_message(_( + "Global search + Tab triggers Spotify search but Spotify is not enabled in settings!"), + mode="warning") + search_over.searched_text = search_over.search_text.text + search_over.sip = False - def recalc(self): - self.tab_h = 34 * gui.scale - def __init__(self): + elif True: + # perf_timer.set() - 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() + temp_results = [] - 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)) + search_over.searched_text = search_over.search_text.text - 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")) + artists = {} + albums = {} + genres = {} + metas = {} + composers = {} + years = {} - queue_menu.add(MenuItem(_("↳ Except for This"), self.clear_queue_crop, show_test=self.except_for_this_show_test)) + tracks = set() - 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) + br = 0 - def except_for_this_show_test(self, _): - return self.queue_remove_show(_) and test_shift(_) + if search_over.searched_text in ("the", "and"): + continue - def make_as_playlist(self): + search_over.sip = True + gui.update += 1 - if pctl.force_queue: - playlist = [] - for item in pctl.force_queue: + o_text = search_over.search_text.text.lower().replace("-", "") - if item.type == 0: - playlist.append(item.track_id) - else: + dia_mode = False + if all([ord(c) < 128 for c in o_text]): + dia_mode = True - pl = id_to_pl(item.playlist_id) - if pl is None: - logging.info("Lost the target playlist") - continue + artist_mode = False + if o_text.startswith("artist "): + o_text = o_text[7:] + artist_mode = True - pp = pctl.multi_playlist[pl].playlist_ids + album_mode = False + if o_text.startswith("album "): + o_text = o_text[6:] + album_mode = True - i = item.position # = pctl.playlist_playing_position + 1 + composer_mode = False + if o_text.startswith("composer "): + o_text = o_text[9:] + composer_mode = True - parts = [] - album_parent_path = pctl.get_track(item.track_id).parent_folder_path + year_mode = False + if o_text.startswith("year "): + o_text = o_text[5:] + year_mode = True - while i < len(pp): - if pctl.get_track(pp[i]).parent_folder_path != album_parent_path: - break + 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 - parts.append((pp[i], i)) - i += 1 + s_text = o_text - for part in parts: - playlist.append(part[0]) + searched = set() - pctl.multi_playlist.append( - pl_gen( - title=_("Queued Tracks"), - playlist_ids=copy.deepcopy(playlist), - hide_title=False)) + for playlist in pctl.multi_playlist: - def drop_tracks_insert(self, insert_position): + # if "<" in playlist.title: + # #logging.info("Skipping search on derivative playlist: " + playlist.title) + # continue - global quick_drag + for track in playlist.playlist_ids: - if not shift_selection: - return + if track in searched: + continue + searched.add(track) - # remove incomplete album from queue - if insert_position == 0 and pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(pctl.force_queue[0].uuid_int) - playlist_index = pctl.active_playlist_viewing - playlist_id = pl_to_id(pctl.active_playlist_viewing) + 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 - main_track_position = shift_selection[0] - main_track_id = default_playlist[main_track_position] - quick_drag = False + 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 len(shift_selection) > 1: + t = pctl.master_library[track] - # 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 + 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 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)) + if cache_string is None: + if not dia_mode: + search_string_cache[ + track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem - def clear_queue_crop(self): + 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 - save = False - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - save = item - break + if dia_mode: + title = unidecode(title) - clear_queue() - if save: - pctl.force_queue.append(save) + artist = unidecode(artist) + album_artist = unidecode(album_artist) + composer = unidecode(composer) + album = unidecode(album) + filename = unidecode(filename) + sartist = unidecode(sartist) - def play_now(self): + if cache_string is None: + search_dia_string_cache[ + track] = title + artist + album_artist + composer + date + album + genre + sartist + filename + stem - 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 + stem = os.path.dirname(t.parent_folder_path) - del pctl.force_queue[queue_index] - # [trackid, position, pl_id, type, album_stage, uid_gen(), auto_stop] + 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 pctl.force_queue and pctl.force_queue[0].album_stage == 1: - split_queue_album(None) + if stem in metas: + metas[stem] += 2 + else: + temp_results.append([5, stem, track, playlist.uuid_int, 0]) + metas[stem] = 2 - target_track_id = queue_item.track_id + if s_text in genre: - pl = id_to_pl(queue_item.playlist_id) - if pl is not None: - pctl.active_playlist_playing = pl + if "/" in genre or "," in genre or ";" in genre: - if target_track_id not in pctl.playing_playlist(): - pctl.advance() - return + for split in genre.replace(";", "/").replace(",", "/").split("/"): + if s_text in split: - pctl.jump(target_track_id, queue_item.position) + 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 queue_item.type == 1: # is album type - queue_item.album_stage = 1 # set as partway playing - pctl.force_queue.insert(0, queue_item) + if s_text in composer: - def toggle_auto_stop(self) -> None: + 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 - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - item.auto_stop ^= True - break + if s_text in date: - def toggle_auto_stop_deco(self): + year = get_year_from_string(date) + if year: - enabled = False - for item in pctl.force_queue: - if item.uuid_int == self.right_click_id: - if item.auto_stop: - enabled = True - break + if year in years: + years[year] += 1 + else: + temp_results.append([7, year, track, playlist.uuid_int, 0]) + years[year] = 1000 - if enabled: - return [colours.menu_text, colours.menu_background, _("Cancel Auto-Stop")] - return [colours.menu_text, colours.menu_background, _("Auto-Stop")] + if search_magic(s_text, title + artist + filename + album + sartist + album_artist): - def queue_remove_show(self, id: int) -> bool: + if "artists" in t.misc and t.misc["artists"]: + for a in t.misc["artists"]: + if search_magic(s_text, a.lower()): - if self.right_click_id is not None: - return True - return False + value = 1 + if a.lower().startswith(s_text): + value = 5 - def right_remove_item(self) -> None: + # Add artist + if a in artists: + artists[a] += value + else: + temp_results.append([0, a, track, playlist.uuid_int, 0]) + artists[a] = value - if self.right_click_id is None: - show_message(_("Eh?")) + 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 - 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")) + elif search_magic(s_text, artist + sartist): - def toggle_pause(self) -> None: - pctl.pause_queue ^= True + value = 1 + if artist.startswith(s_text): + value = 10 - 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: + # 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 - # text_colour = [230, 230, 230, 255] - bg = colours.queue_background + 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 fq[i].type == 0: + elif search_magic(s_text, album_artist): - rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) + # Add album artist + value = 1 + if t.album_artist.startswith(s_text): + value = 5 - if draw_back: - ddt.rect(rect, colours.queue_card_background) - bg = colours.queue_card_background + 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 - 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] + 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 - tauon.gall_ren.render(track, (rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale), round(28 * gui.scale)) + if s_text in album: - ddt.rect((rect[0] + 4 * gui.scale, rect[1] + 4 * gui.scale, 26, 26), [0, 0, 0, 6]) + value = 1 + if s_text == album: + value = 3 - line = track.album - if fqo.type == 0: - line = track.title + 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 not line: - line = clean_string(track.filename) + if search_magic(s_text, artist + sartist) or search_magic(s_text, album): - line2y = yy + 14 * gui.scale + 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 - artist_line = track.artist - if fqo.type == 1 and track.album_artist: - artist_line = track.album_artist + elif search_magic_any(s_text, artist + sartist) and search_magic_any(s_text, album): - if fqo.type == 0 and not artist_line: - line2y -= 7 * gui.scale + 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 - 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 s_text in title: - ddt.text( - (rect[0] + (40 * gui.scale), line2y), line, text_colour2, 211, - max_w=rect[2] - 60 * gui.scale, bg=bg) + if t not in tracks: - 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]) + value = 50 + if s_text == title: + value = 200 - 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]) + temp_results.append([2, t.title, track, playlist.uuid_int, value]) - def draw(self, x: int, y: int, w: int, h: int): + tracks.add(t) - yy = y + elif t not in tracks: + temp_results.append([2, t.title, track, playlist.uuid_int, 1]) - yy += round(4 * gui.scale) + tracks.add(t) - sep_colour = alpha_blend([255, 255, 255, 11], colours.queue_background) + br += 1 + if br > 800: + time.sleep(0.005) # Throttle thread + br = 0 + if search_over.searched_text != search_over.search_text.text: + break - 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) + search_over.sip = False + search_over.on = 0 + gui.update += 1 - yy += round(3 * gui.scale) + # Remove results not matching any filter keyword - box_rect = (x, yy - 6 * gui.scale, w, h) - ddt.rect(box_rect, colours.queue_background) - ddt.text_background_colour = colours.queue_background + if artist_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 0: + del temp_results[i] - 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) + elif album_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 1: + del temp_results[i] - # if y < gui.panelY * 2: - # ddt.rect((x, y - 3 * gui.scale, w, 30 * gui.scale), colours.queue_background, True) + elif composer_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 6: + del temp_results[i] - 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) + elif year_mode: + for i in reversed(range(len(temp_results))): + if temp_results[i][0] != 7: + del temp_results[i] - qb_right_click = 0 + # 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 - if coll(box_rect): - # Update scroll position - self.scroll_position += mouse_wheel * -1 - self.scroll_position = max(self.scroll_position, 0) + 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) - if right_click: - qb_right_click = 1 + 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 - # 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] + search_over.on = 0 + search_over.force_select = 0 + #logging.info(perf_timer.get()) - line = _("Up Next:") - if pctl.force_queue: - # line = "Queue" - ddt.text((x + (10 * gui.scale), yy + 2 * gui.scale), line, text_colour, 211) +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 - yy += 7 * gui.scale + loaded_paths_cache = {} + loaded_cue_cache = {} + added = [] - if len(pctl.force_queue) < 3: - self.scroll_position = 0 + def get_quoted_from_line(line): - # Draw square dots to indicate view has been scrolled down - if self.scroll_position > 0: - ds = 3 * gui.scale - gp = 4 * gui.scale + # Extract quoted or unquoted string from a line + # e.g., 'FILE "01 - Track01.wav" WAVE' or 'TITLE Track01' or "PERFORMER 'Artist Name'" - 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]) + parts = line.split(None, 1) + if len(parts) < 2: + return "" - # 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]) + content = parts[1].strip() - yy += 6 * gui.scale + 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] - yy += 10 * gui.scale + def add_from_cue(path): - i = 0 + global added - # 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 not msys: # Windows terminal doesn't like unicode + logging.info("Reading CUE file: " + path) - # End drag if mouse not in correct state for it - if not mouse_down and not mouse_up: - self.dragging = None + try: - if not queue_menu.active: - self.right_click_id = None + 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") - fq = self.fq + except Exception: + logging.exception("Abort: Can't detect encoding of CUE file") + return 1 - list_top = yy + f.close() - i = self.scroll_position + # 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 - # Limit scroll distance - if i > len(fq): - self.scroll_position = len(fq) - i = self.scroll_position + 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 - showed_indicator = False - list_extends = False - x1 = x + 13 * gui.scale # highlight position - w1 = w - 28 * gui.scale - 10 * gui.scale + if files == 1: + pass + elif files_with_subtracks > 1: + pass + else: + return 1 - while i < len(fq) + 1: + cue_performer = "" + cue_date = "" + cue_album = "" + cue_genre = "" + cue_main_performer = "" + cue_songwriter = "" + cue_disc = 0 + cue_disc_total = 0 - # Stop drawing if past window - if yy > window_size[1] - gui.panelBY - gui.panelY - (50 * gui.scale): - list_extends = True - break + cd = [] + cds = [] - # 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) + file_name = "" + file_path = "" - if self.dragging is not None and coll(h_rect) and mouse_up: + in_header = True - ob = None - for u in reversed(range(len(pctl.force_queue))): + i = -1 + while True: + i += 1 - if pctl.force_queue[u].uuid_int == self.dragging: - ob = pctl.force_queue[u] - pctl.force_queue[u] = None - break + if i > len(content) - 1: + break - else: - self.dragging = None + line = content[i].strip() - if self.dragging: - pctl.force_queue.insert(i, ob) - self.dragging = None + if in_header: + if line.startswith("REM "): + line = line[4:] - 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 + 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 - # 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 + if line.startswith("FILE "): - inp.mouse_click = False - self.draw(x, y, w, h) - return + if cd: + cds.append(cd) + cd = [] - if i > len(fq) - 1: - break + file_name = get_quoted_from_line(line) + file_path = os.path.join(os.path.dirname(path), file_name) - track = pctl.get_track(fq[i].track_id) + 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 - rect = (x + 13 * gui.scale, yy, w - 28 * gui.scale, self.tab_h) + if line.startswith("TRACK "): + line = line[6:] + if line.endswith("AUDIO"): + line = line[:-5] - if inp.mouse_click and coll(rect): + 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 - self.dragging = fq[i].uuid_int - self.drag_start_y = mouse_position[1] - self.drag_start_top = yy + 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 - if d_click_timer.get() < 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) - if self.d_click_ref == fq[i].uuid_int: + while True: + i += 1 + if i > len(content) - 1 or content[i].startswith("FILE ") or content[i].strip().startswith( + "TRACK"): + break - pl = id_to_pl(fq[i].uuid_int) - if pl is not None: - switch_playlist(pl) + line = content[i] + line = line.strip() - 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 + 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 - d_click_timer.set() + i -= 1 + cd.append(nt) - if self.dragging and coll(h_rect): - yy += self.tab_h - yy += 4 * gui.scale + if cd: + cds.append(cd) - if qb_right_click and coll(rect): - self.right_click_id = fq[i].uuid_int - qb_right_click = 2 + for cdn, cd in enumerate(cds): - if middle_click and coll(rect): - pctl.force_queue.remove(fq[i]) - gui.pl_update += 1 + last_end = None + end_track = TrackClass() + end_track.fullpath = cd[-1].fullpath + tag_scan(end_track) - if fq[i].uuid_int == self.dragging: - # ddt.rect_r(rect, [22, 22, 22, 255], True) - pass - else: + # 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] - db = False - if fq[i].uuid_int == self.right_click_id: - db = True + # Update with proper length + for track in reversed(cd): - self.draw_card(x, y, w, h, yy, track, fq[i], db) + if last_end == None: + last_end = end_track.length - # Drag tracks from main playlist and insert ------------ - if quick_drag: + track.length = last_end - track.start_time + track.samplerate = end_track.samplerate + track.bitrate = end_track.bitrate + track.bit_depth = end_track.bit_depth + track.misc["parent-length"] = end_track.length + last_end = track.start_time - if x < mouse_position[0] < x + w: + # 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 - 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 cue_disc: + track.disc_number = cue_disc + elif len(cds) == 0: + track.disc_number = "" + else: + track.disc_number = str(cdn) - insert_position = None + 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 y1 < mouse_position[1] < y1 + h1: - ddt.rect((x1, yy - 2 * gui.scale, w1, 2 * gui.scale), colours.queue_drag_indicator_colour) - showed_indicator = True - if mouse_up: - insert_position = i + # 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_paths_cache[track.fullpath] = track.index + added.append(track.index) - 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 + except Exception: + logging.exception("Internal error processing CUE file") - if mouse_up: - insert_position = i + 1 + def add_file(path, force_scan: bool = False) -> int | None: + # bm.get("add file start") + global DA_Formats + global to_got - if insert_position is not None: - self.drop_tracks_insert(insert_position) + if not os.path.isfile(path): + logging.error("File to import missing") + return 0 - # ----------------------------------------- - yy += self.tab_h - yy += 4 * gui.scale + if os.path.splitext(path)[1][1:] in {"CUE", "cue"}: + add_from_cue(path) + return 0 - i += 1 + if path.lower().endswith(".xspf"): + logging.info("Found XSPF file at: " + path) + load_xspf(path) + return 0 - # 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 path.lower().endswith(".m3u") or path.lower().endswith(".m3u8"): + load_m3u(path) + return 0 - yy += 15 * gui.scale - if fq: - ddt.rect((x, yy, w, 3 * gui.scale), sep_colour) - yy += 11 * gui.scale + if path.endswith(".pls"): + load_pls(path) + return 0 - # Calculate total queue duration - duration = 0 - tracks = 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") - 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 + 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") - album_parent_path = pctl.get_track(item.track_id).parent_folder_path + return 1 - playing_track = pctl.playing_object() + 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 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 + return 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) + 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") - while i < len(playlist): - if pctl.get_track(playlist[i]).parent_folder_path != album_parent_path: - break + 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") - duration += pctl.get_track(playlist[i]).length - tracks += 1 - i += 1 + to_got = b + gets(target_dir) + quick_import_done.append(target_dir) + # gets(target_dir) - # 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) + return 1 + to_got += 1 + gui.update = 1 + path = path.replace("\\", "/") - if self.dragging: + if path in loaded_paths_cache: + de = loaded_paths_cache[path] - fqo = None - for item in fq: - if item.uuid_int == self.dragging: - fqo = item - break + 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: - self.dragging = False + added.append(de) + return None - 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) + time.sleep(0.002) - # 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)) + # audio = auto.File(path) - # 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) + nt = TrackClass() -def art_metadata_overlay(right, bottom, showc): - if not showc: - return + nt.index = pctl.master_count + set_path(nt, path) - padding = 6 * gui.scale + def commit_track(nt): + pctl.master_library[pctl.master_count] = nt + added.append(pctl.master_count) - if not key_shift_down: + if prefs.auto_sort or force_scan: + tag_scan(nt) + else: + after_scan.append(nt) + tauon.thread_manager.ready("worker") - line = "" - if showc[0] == 1: - line += "E " - elif showc[0] == 2: - line += "N " - else: - line += "F " + pctl.master_count += 1 - line += str(showc[2] + 1) + "/" + str(showc[1]) + # nt = tag_scan(nt) + if nt.cue_sheet != "": + tag_scan(nt) + cue_scan(nt.cue_sheet, nt) + del nt - y = bottom - 40 * gui.scale + elif nt.file_ext.lower() in GME_Formats and gme: - 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]) + 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) - else: # Extended metadata + gme.gme_delete(emu) - line = "" - if showc[0] == 1: - line += "Embedded" - elif showc[0] == 2: - line += "Network" else: - line += "File" - y = bottom - 76 * gui.scale - - tag_width = ddt.get_text_w(line, 12) + 12 * gui.scale - ddt.rect_a((right - (tag_width + padding), y), (tag_width, 18 * gui.scale), [8, 8, 8, 255]) - ddt.text(((right) - (6 * gui.scale + padding), y, 1), line, [200, 200, 200, 255], 12, bg=[30, 30, 30, 255]) - - y += 18 * gui.scale - - line = "" - line += showc[4] - line += " " + str(showc[3][0]) + "×" + str(showc[3][1]) + commit_track(nt) - 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]) + # bm.get("fill entry") + if gui.auto_play_import: + pctl.jump(pctl.master_count - 1) + gui.auto_play_import = False - y += 18 * gui.scale + # Count the approx number of files to be imported + def pre_get(direc): - line = "" - line += str(showc[2] + 1) + "/" + str(showc[1]) + global to_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]) + to_get = 0 + for root, dirs, files in os.walk(direc): + to_get += len(files) + if gui.im_cancel: + return + gui.update = 3 -class MetaBox: + def gets(direc, force_scan=False): - def l_panel(self, x, y, w, h, track, top_border=True): + global DA_Formats - if not track: + if os.path.basename(direc) == "__MACOSX": return - border_colour = [255, 255, 255, 30] - line1_colour = [255, 255, 255, 235] - line2_colour = [255, 255, 255, 200] - if test_lumi(colours.gallery_background) < 0.55: - border_colour = [0, 0, 0, 30] - line1_colour = [0, 0, 0, 200] - line2_colour = [0, 0, 0, 230] - - rect = (x, y, w, h) - - ddt.rect(rect, colours.gallery_background) - if top_border: - ddt.rect((x, y, w, round(1 * gui.scale)), border_colour) - else: - ddt.rect((x, y + h - round(1 * gui.scale), w, round(1 * gui.scale)), border_colour) + 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") - ddt.text_background_colour = colours.gallery_background + return + except Exception: + logging.exception("Unknown error accessing one or more files") + return - insert = round(9 * gui.scale) - border = round(2 * gui.scale) + 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 - compact_mode = False - if w < h * 1.9: - compact_mode = True + 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: - art_rect = [x + insert - 2 * gui.scale, y + insert, h - insert * 2 + 1 * gui.scale, - h - insert * 2 + 1 * gui.scale] + if os.path.splitext(items_in_dir[q])[1][1:].lower() in DA_Formats: - if compact_mode: - art_rect[0] = x + round(w / 2 - art_rect[2] / 2) - round(1 * gui.scale) # - border + if len(items_in_dir[q]) > 2 and items_in_dir[q][0:2] == "._": + continue - border_rect = ( - art_rect[0] - border, art_rect[1] - border, art_rect[2] + (border * 2), art_rect[3] + (border * 2)) + add_file(os.path.join(direc, items_in_dir[q]).replace("\\", "/"), force_scan) - 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) + 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("\\", "/")) - 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 gui.im_cancel: + return - 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) + 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 - if not compact_mode: - text_x = border_rect[0] + border_rect[2] + round(10 * gui.scale) - max_w = w - (border_rect[2] + 28 * gui.scale) - yy = y + round(15 * gui.scale) - ddt.text((text_x, yy), track.title, line1_colour, 316, max_w=max_w) - yy += round(20 * gui.scale) - ddt.text((text_x, yy), track.artist, line2_colour, 14, max_w=max_w) - yy += round(30 * gui.scale) - ddt.text((text_x, yy), track.album, line2_colour, 14, max_w=max_w) - yy += round(20 * gui.scale) - ddt.text((text_x, yy), track.date, line2_colour, 14, max_w=max_w) + #logging.info(pctl.master_library) - gui.showed_title = True + global transcode_list + global transcode_state + global album_art_gen + global cm_clean_db + global to_got + global to_get + global move_in_progress - def lyrics(self, x, y, w, h, track: TrackClass): + active_timer = Timer() + while True: - ddt.rect((x, y, w, h), colours.side_panel_background) - ddt.text_background_colour = colours.side_panel_background + if not after_scan: + time.sleep(0.1) - if not track: + 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 - # Test for show lyric menu on right ckick - if coll((x + 10, y, w - 10, h)): - if right_click: # and 3 > pctl.playing_state > 0: - gui.force_showcase_index = -1 - showcase_menu.activate(track) - - # Test for scroll wheel input - if mouse_wheel != 0 and coll((x + 10, y, w - 10, h)): - lyrics_ren_mini.lyrics_position += mouse_wheel * 30 * gui.scale - if lyrics_ren_mini.lyrics_position > 0: - lyrics_ren_mini.lyrics_position = 0 - lyric_side_top_pulse.pulse() - - gui.update += 1 - - tw, th = ddt.get_text_wh(track.lyrics + "\n", 15, w - 50 * gui.scale, True) - - oth = th - - th -= h - th += 25 * gui.scale # Empty space buffer at end - - if lyrics_ren_mini.lyrics_position * -1 > th: - lyrics_ren_mini.lyrics_position = th * -1 - if oth > h: - lyric_side_bottom_pulse.pulse() + if after_scan: + i = 0 + while after_scan: + i += 1 - scroll_w = 15 * gui.scale - if gui.maximized: - scroll_w = 17 * gui.scale + if i > 123: + break - 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 + tag_scan(after_scan[0]) - margin = 10 * gui.scale - if colours.lm: - margin += 1 * gui.scale + gui.update = 2 + gui.pl_update = 1 + # time.sleep(0.001) + if pctl.running: + del after_scan[0] + else: + break - 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) + album_artist_dict.clear() - ddt.rect((x, y + h - 1, w, 1), colours.side_panel_background) + artist_list_box.worker() - 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) + # 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 - def draw(self, x, y, w, h, track=None): + # Update smart playlists + if gui.regen_single > -1: + target = gui.regen_single + gui.regen_single = -1 + regenerate_playlist(target, silent=True) - ddt.rect((x, y, w, h), colours.side_panel_background) + if pctl.after_import_flag and not after_scan and not search_over.active and not loading_in_progress: + pctl.after_import_flag = False - if not track: - return + 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") - # 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) + tree_view_box.clear_all() - 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 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 - if h < 15: - return + # Folder moving + if len(move_jobs) > 0: + gui.update += 1 + move_in_progress = True + job = move_jobs[0] + del move_jobs[0] - # Check for lyrics if auto setting - test_auto_lyrics(track) + 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 - # # 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) + 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 - # Draw standard metadata - if len(pctl.track_queue) > 0: + if job[2] == True: + try: + shutil.rmtree(job[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: + 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 - ddt.text_background_colour = colours.side_panel_background + 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 coll((x + 10, y, w - 10, h)): - # Click area to jump to current track - if inp.mouse_click: - pctl.show_current() - gui.update += 1 + move_in_progress = False + load_orders.append(job[4]) + gui.update += 1 - title = "" - album = "" - artist = "" - ext = "" - date = "" - genre = "" + # Clean database + if cm_clean_db is True: + items_removed = 0 - margin = x + 10 * gui.scale - if colours.lm: - margin += 2 * gui.scale + # old_db = copy.deepcopy(pctl.master_library) + to_got = 0 + to_get = len(pctl.master_library) + search_over.results.clear() - text_width = w - 25 * gui.scale - tr = None + keys = set(pctl.master_library.keys()) + for index in keys: + time.sleep(0.0001) + track = pctl.master_library[index] + to_got += 1 - # if pctl.playing_state < 3: + if to_got % 100 == 0: + gui.update = 1 - if pctl.playing_state == 0 and prefs.meta_persists_stop: - tr = pctl.master_library[pctl.track_queue[pctl.queue_step]] - if pctl.playing_state == 0 and prefs.meta_shows_selected: + if not prefs.remove_network_tracks and track.file_ext == "SPTY": - 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]) + for playlist in pctl.multi_playlist: + if index in playlist.playlist_ids: + break + else: + pctl.purge_track(index) + items_removed += 1 - 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]) + continue - if tr is None: - tr = pctl.playing_object() - if tr is None: - return + 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): - 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 track.is_network and track.file_ext == "SPTY": + continue - if not title and not artist: - title = pctl.tag_meta + pctl.purge_track(index) + items_removed += 1 - if h > 58 * gui.scale: + 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() - block_y = y + 7 * gui.scale + gui.update = 1 + gui.pl_update = 1 + pctl.notify_change() - if not prefs.show_side_art: - block_y += 3 * gui.scale + search_dia_string_cache.clear() + search_string_cache.clear() + search_over.results.clear() - 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) + pctl.notify_change() - gui.showed_title = True + # FOLDER ENC + if transcode_list: - if h > 140 * gui.scale: + try: + transcode_state = "" + gui.update += 1 - 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) + folder_items = transcode_list[0] - if not genre == date == "": - line = date - if genre != "": - if line != "": - line += " | " - line += genre + ref_track_object = pctl.master_library[folder_items[0]] + ref_album = ref_track_object.album - ddt.text( - (margin, block_y + 20 * gui.scale), line, colours.side_bar_line2, - fonts.side_panel_line2, max_w=text_width) + # Generate a folder name based on artist and album of first track in batch + folder_name = encode_folder_name(ref_track_object) - if ext != "": - if ext == "SPTY": - ext = "Spotify" - if ext == "RADIO": - ext = radiobox.playing_title - sp = ddt.text( - (margin, block_y + 40 * gui.scale), ext, colours.side_bar_line2, - fonts.side_panel_line2, max_w=text_width) + # If 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 - 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) + logging.info("Transcoding folder: " + folder_name) -class PictureRender: + # Remove any existing matching folder + if (prefs.encoder_output / folder_name).is_dir(): + shutil.rmtree(prefs.encoder_output / folder_name) - def __init__(self): - self.show = False - self.path = "" + # Create new empty folder to output tracks to + (prefs.encoder_output / folder_name).mkdir(parents=True) - self.image_data = None - self.texture = None - self.sdl_rect = None - self.size = (0, 0) + full_wav_out_p = prefs.encoder_output / "output.wav" + full_target_out_p = prefs.encoder_output / ("output." + prefs.transcode_codec) + if full_wav_out_p.is_file(): + full_wav_out_p.unlink() + if full_target_out_p.is_file(): + full_target_out_p.unlink() - def load(self, path, box_size=None): + cache_dir = tmp_cache_dir() + if not os.path.isdir(cache_dir): + os.makedirs(cache_dir) - if not os.path.isfile(path): - logging.warning("NO PICTURE FILE TO LOAD") - return + if prefs.transcode_codec in ("opus", "ogg", "flac", "mp3"): + global core_use + cores = os.cpu_count() - g = io.BytesIO() - g.seek(0) + total = len(folder_items) + gui.transcoding_batch_total = total + gui.transcoding_bach_done = 0 + dones = [] - im = Image.open(path) - if box_size is not None: - im.thumbnail(box_size, Image.Resampling.LANCZOS) + 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() - 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] + q += 1 + gui.update += 1 + time.sleep(0.05) + if gui.tc_cancel: + while core_use > 0: + time.sleep(1) + break + if q == len(folder_items) and core_use == 0: + gui.update += 1 + break - def draw(self, x, y): + else: + logging.error("Codec error") - if self.show is False: - return + 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")) - if self.image_data is not None: - if self.texture is not None: - SDL_DestroyTexture(self.texture) + #logging.info(transcode_list[0]) - # 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 + del transcode_list[0] + transcode_state = "" + gui.update += 1 - 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) + 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] -class ArtistInfoBox: + 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() - def __init__(self): - self.artist_on = None - self.min_rq_timer = Timer() - self.min_rq_timer.force_set(10) + 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 - self.text = "" + if loaderCommandReady is True: + for order in load_orders: + if order.stage == 1: + if loaderCommand == LC_Folder: + to_get = 0 + to_got = 0 + loaded_paths_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_paths_cache, loaded_cue_cache = cache_paths() + add_file(order.target) - self.status = "" + if gui.im_cancel: + gui.im_cancel = False + to_get = 0 + to_got = 0 + load_orders.clear() + added = [] + loaderCommand = LC_Done + loaderCommandReady = False + break - self.scroll_y = 0 + loaderCommand = LC_Done + #logging.info("LOAD ORDER") + order.tracks = added - self.process_text_artist = "" - self.processed_text = "" - self.th = 0 - self.w = 0 - self.lock = False + # 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] - self.mini_box = asset_loader(scaled_asset_directory, loaded_asset_dc, "mini-box.png", True) + added = [] + order.stage = 2 + loaderCommandReady = False + #logging.info("DONE LOADING") + break - def manual_dl(self): +def get_album_info(position, pl: int | None = None): - track = pctl.playing_object() - if track is None or not track.artist: - show_message(_("No artist name found"), mode="warning") - return + playlist = default_playlist + if pl is not None: + playlist = pctl.multi_playlist[pl].playlist_ids - # Check if the artist has changed - self.artist_on = track.artist + global album_info_cache_key - if not self.lock and self.artist_on: - self.lock = True - # self.min_rq_timer.set() + 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()) - self.scroll_y = 0 - self.status = _("Looking up...") - self.process_text_artist = "" + if position in album_info_cache: + return album_info_cache[position] - shoot_dl = threading.Thread(target=self.get_data, args=([self.artist_on, False, True])) - shoot_dl.daemon = True - shoot_dl.start() + if album_dex and album_mode and (pl is None or pl == pctl.active_playlist_viewing): + dex = album_dex + else: + dex = reload_albums(custom_list=playlist) - def draw(self, x, y, w, h): + end = len(playlist) + start = 0 - if gui.artist_panel_height > 300 and w < 500 * gui.scale: - bio_set_small() + for i, p in enumerate(reversed(dex)): + if p <= position: + start = p + break + end = p - if w < 300 * gui.scale: - gui.artist_info_panel = False - gui.update_layout() - return + album = list(range(start, end)) - track = pctl.playing_object() - if track is None: - return + playing = 0 + select = False - # Check if the artist has changed - artist = track.artist - wait = False + if pctl.selected_in_playlist in album: + select = True - # Activate menu - if right_click and coll((x, y, w, h)): - artist_info_menu.activate(in_reference=artist) + if len(pctl.track_queue) > 0 and p < len(playlist): + if pctl.track_queue[pctl.queue_step] in playlist[start:end]: + playing = 1 - background = colours.artist_bio_background - text_colour = colours.artist_bio_text - ddt.rect((x + 10, y + 5, w - 15, h - 5), background) + album_info_cache[position] = playing, album, select + return playing, album, select - if artist != self.artist_on: +def get_folder_list(index: int): + playlist = [] - if artist == "": - return + 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)) - 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 +def gal_jump_select(up=False, num=1): - if pctl.playing_time < 2: - if os.path.isfile(os.path.join(a_cache_dir, artist + "-lfm.txt")): - pass - else: - self.status = "..." - wait = True + old_selected = pctl.selected_in_playlist + old_num = num - if not wait and not self.lock: - self.lock = True - # self.min_rq_timer.set() + if not default_playlist: + return - self.scroll_y = 0 - self.status = _("Loading...") + on = pctl.selected_in_playlist + if on > len(default_playlist) - 1: + on = 0 + pctl.selected_in_playlist = 0 - shoot_dl = threading.Thread(target=self.get_data, args=([artist])) - shoot_dl.daemon = True - shoot_dl.start() + if up is False: - if self.process_text_artist != self.artist_on: - self.process_text_artist = self.artist_on + 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 - text = self.text - lic = "" - link = "" + if on > len(default_playlist) - 1: + pctl.selected_in_playlist = old_selected + return - if "<a" in text: - text, ex = text.split('<a href="', 1) + pctl.selected_in_playlist = on + num -= 1 + else: - link, ex = ex.split('">', 1) + if num > 1: + if pctl.selected_in_playlist > len(default_playlist) - 1: + pctl.selected_in_playlist = old_selected + return - lic = ex.split("</a>. ", 1)[1] + alb = get_album_info(pctl.selected_in_playlist) + if alb[1][0] in album_dex[:num]: + pctl.selected_in_playlist = old_selected + return - text += "\n" + while num > 0: + alb = get_album_info(pctl.selected_in_playlist) - 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")) + if alb[1][0] > -1: + on = alb[1][0] - 1 - self.processed_text = text - self.w = -1 # trigger text recalc + pctl.selected_in_playlist = max(get_album_info(on)[1][0], 0) + num -= 1 - if self.status == "Ready": +def gen_power2(): + tags = {} # [tag name]: (first position, number of times we saw it) + tag_list = [] - # 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) + last = "a" + noise = 0 - text_max_w = w - (round(55 * gui.scale) + p_off) + def key(tag): + return tags[tag][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 + for position in album_dex: - scroll_max = self.th - (h - 26) + index = default_playlist[position] + track = pctl.get_track(index) - 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) + crumbs = track.parent_folder_path.split("/") - right = x + w - 25 * gui.scale + for i, b in enumerate(crumbs): - 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 + if i > 0 and (track.artist in b and track.artist): + tag = crumbs[i - 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) + if tag != last: + noise += 1 + last = tag - yy = y + 12 - for item in self.urls: + if tag in tags: + tags[tag][1] += 1 + else: + tags[tag] = [position, 1, "/".join(crumbs[:i])] + tag_list.append(tag) + break - rect = (right - 2, yy - 2, 16, 16) + if noise > len(album_dex) / 2: + #logging.info("Playlist is too noisy for power bar.") + return [] - 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]) + tag_list_sort = sorted(tag_list, key=key, reverse=True) - 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) + max_tags = round((window_size[1] - gui.panelY - gui.panelBY - 10) // 30 * gui.scale) - yy += 19 * gui.scale + tag_list_sort = tag_list_sort[:max_tags] - else: - ddt.text((x + w // 2, y + h // 2 - 7 * gui.scale, 2), self.status, [255, 255, 255, 60], 313, bg=background) + for i in reversed(range(len(tag_list))): + if tag_list[i] not in tag_list_sort: + del tag_list[i] - def get_data(self, artist: str, get_img_path: bool = False, force_dl: bool = False) -> str | None: + h = [] - if not get_img_path: - logging.info("Load Bio Data") + for tag in tag_list: - if artist is None and not get_img_path: - self.artist_on = artist - self.lock = False - return "" + if tags[tag][1] > 2: + t = PowerTag() + t.path = tags[tag][2] + t.name = tag.upper() + t.position = tags[tag][0] + h.append(t) - f_artist = filename_safe(artist) + cc = random.random() + cj = 0.03 + if len(h) < 5: + cj = 0.11 - 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) + cj = 0.5 / max(len(h), 2) - 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"), - ] + for item in h: + item.colour = hsl_to_rgb(cc, 0.8, 0.7) + cc += cj - if get_img_path: - for path in image_paths: - if os.path.isfile(path): - return path - return "" + return h - # Check for cache - box_size = ( - round(gui.artist_panel_height - 20 * gui.scale) * 2, round(gui.artist_panel_height - 20 * gui.scale)) - try: +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 os.path.isfile(text_filepath): - logging.info("Load cached bio and image") + if cm_clean_db: + # Doing reload while things are being removed may cause crash + return None - artist_picture_render.show = False + dex = [] + current_folder = "" + current_album = "" + current_artist = "" + current_date = "" + current_title = "" - 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 + 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 - 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 + playlist = pctl.multi_playlist[target_pl_no].playlist_ids - return "" + for i in range(len(playlist)): + tr = pctl.master_library[playlist[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 + 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 - # 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") + 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 - 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) + if return_playlist > -1 or custom_list: + return dex - 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) + album_dex = dex + album_info_cache.clear() + gui.update += 2 + gui.pl_update = 1 + update_layout = True - 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 not quiet: + goto_album(pctl.playlist_playing_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 + # Generate POWER BAR + gui.power_bar = gen_power2() + gui.pt = 0 - self.status = "Ready" - gui.update = 2 +def star_line_toggle(mode: int= 0) -> bool | None: + if mode == 1: + return gui.star_mode == "line" - # 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" + if gui.star_mode == "line": + gui.star_mode = "none" + else: + gui.star_mode = "line" + gui.show_ratings = False - except Exception: - logging.exception("Failed to load bio") - self.status = _("Load Failed") + gui.update += 1 + gui.pl_update = 1 + return None - self.artist_on = artist - self.processed_text = "" - self.process_text_artist = "" - self.min_rq_timer.set() - self.lock = False - gui.update = 2 - return "" +def star_toggle(mode: int = 0) -> bool | None: + if gui.show_ratings: + if mode == 1: + return prefs.rating_playtime_stars + prefs.rating_playtime_stars ^= True -def 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] + else: + if mode == 1: + return gui.star_mode == "star" -class RadioThumbGen: - def __init__(self): - self.cache = {} - self.requests = [] - self.size = 100 + if gui.star_mode == "star": + gui.star_mode = "none" + else: + gui.star_mode = "star" - def loader(self): + # gui.show_ratings = False + gui.update += 1 + gui.pl_update = 1 + return None - 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"]) +def heart_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_hearts - 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") + gui.show_hearts ^= True + # gui.show_ratings = False - 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 + gui.update += 1 + gui.pl_update = 1 + return None - 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() +def album_rating_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_album_ratings - 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 + gui.show_album_ratings ^= True - def draw(self, station, x, y, w): - if not station.get("title"): - return 0 - key = (station["title"], w) + gui.update += 1 + gui.pl_update = 1 + return None - 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 +def rating_toggle(mode: int = 0) -> bool | None: + if mode == 1: + return gui.show_ratings -def station_browse(): - radiobox.active = True - radiobox.edit_mode = False - radiobox.add_mode = False - radiobox.center = True - radiobox.tab = 1 + gui.show_ratings ^= True -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 + 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.")) -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 + gui.update += 1 + gui.pl_update = 1 + return None -def remove_station(item): - index = item[0] - del pctl.radio_playlists[pctl.radio_playlist_viewing]["items"][index] +def toggle_titlebar_line(mode: int = 0) -> bool | None: + global update_title + if mode == 1: + return update_title -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) + line = window_title + SDL_SetWindowTitle(t_window, line) + update_title ^= True + if update_title: + update_title_do() + return None - 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) +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 - # 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) +def toggle_side_panel_layout(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.side_panel_layout == 1 - # 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 prefs.side_panel_layout == 1: + prefs.side_panel_layout = 0 + else: + prefs.side_panel_layout = 1 + return None - if coll(rect): - colour = b_colour - if inp.mouse_click: - add_station() - else: - colour = a_colour +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.add_icon.render(rect[0] + round(4 * gui.scale), rect[1] + round(4 * gui.scale), colour) +def scale1(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 1: + return True + return False - y += round(33 * gui.scale) - rect = (x, y, round(25 * gui.scale), round(25 * gui.scale)) - fields.add(rect) + prefs.ui_scale = 1 + pref_box.large_preset() - if not coll(rect): - colour = a_colour - else: - colour = b_colour - if inp.mouse_click: - station_browse() - self.search_icon.render(rect[0] + round(4 * gui.scale), rect[1] + round(4 * gui.scale), colour) + if prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - 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"] +def scale125(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 1.25: + return True + return False + return None - 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) + prefs.ui_scale = 1.25 + pref_box.large_preset() - 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 prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - self.save_icon.render(rect[0] + round(3 * gui.scale), rect[1] + round(4 * gui.scale), colour) +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 - x = round(30 * gui.scale) - y = gui.panelY + round(30 * gui.scale) - yy = y +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 - 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] +def toggle_min_tray(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.min_to_tray + prefs.min_to_tray ^= True + return None - w = round(400 * gui.scale) - h = round(55 * gui.scale) - gap = round(7 * gui.scale) +def scale2(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.ui_scale == 2: + return True + return False - mm = (window_size[1] - (gui.panelBY + yy + h + round(15 * gui.scale))) // (h + gap) + 1 + prefs.ui_scale = 2 + pref_box.large_preset() - 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 prefs.ui_scale != gui.scale: + show_message(_("Change will be applied on restart.")) + return None - pctl.radio_playlists[pctl.radio_playlist_viewing]["scroll"] = scroll - insert = None +def toggle_borderless(mode: int = 0) -> bool | None: + global draw_border + global update_layout - 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]) + if mode == 1: + return draw_border - 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] + update_layout = True + draw_border ^= 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) + if draw_border: + SDL_SetWindowBordered(t_window, False) + else: + SDL_SetWindowBordered(t_window, True) + return None - 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) +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 - bottom_bar1.play_button.render(x + (w - round(30 * gui.scale)), yy + round(23 * gui.scale), colour) +def toggle_scroll(mode: int = 0) -> bool | None: + global scroll_enable + global update_layout - 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))) + if mode == 1: + if scroll_enable: + return False + return True - self.menu_icon.render(x + (w - round(75 * gui.scale)), yy + round(26 * gui.scale), colour) + scroll_enable ^= True + gui.pl_update = 1 + update_layout = True + return None - # 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) +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 - yy += round(h + gap) +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 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) +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 - 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 toggle_true_shuffle(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.true_shuffle + prefs.true_shuffle ^= True + 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 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 - yy = window_size[1] / 3 - boxx / 2 - yy += boxx + round(30 * gui.scale) +def toggle_enable_web(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.enable_web - 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) + prefs.enable_web ^= True - if self.drag: - gui.update_on_drag = 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") - 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"]) + elif prefs.enable_web is False: + if tauon.radio_server is not None: + tauon.radio_server.shutdown() + gui.web_running = False - radios[radios.index("New")] = self.drag - self.drag = None - gui.update += 1 + time.sleep(0.25) + return None -class Showcase: +def toggle_scrobble_mark(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.scrobble_mark + prefs.scrobble_mark ^= True + return None - def __init__(self): +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 - self.lastfm_artist = None - self.artist_mode = 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 - def render(self): +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 - global right_click +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 - box = int(window_size[1] * 0.4 + 120 * gui.scale) - box = min(window_size[0] // 2, box) +def toggle_dl_mon(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.monitor_downloads + prefs.monitor_downloads ^= True + return None - hide_art = False - if window_size[0] < 900 * gui.scale: - hide_art = True +def toggle_music_ex(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.extract_to_music + prefs.extract_to_music ^= True + return None - x = int(window_size[0] * 0.15) - y = int((window_size[1] / 2) - (box / 2)) - 10 * gui.scale +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 hide_art: - box = 45 * gui.scale - elif window_size[1] / window_size[0] > 0.7: - x = int(window_size[0] * 0.07) +def toggle_top_tabs(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.tabs_on_top + prefs.tabs_on_top ^= True + 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_guitar_chords(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.guitar_chords + prefs.guitar_chords ^= True + return None - t1 = colours.grey(250) +# def toggle_auto_lyrics(mode: int = 0) -> bool | None: +# if mode == 1: +# return prefs.auto_lyrics +# prefs.auto_lyrics ^= True - 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 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 test_lumi(colours.playlist_panel_background) < 0.7: - light_mode = True - t1 = colours.grey(30) - gui.vis_4_colour = [40, 40, 40, 255] +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 - ddt.rect((0, gui.panelY, window_size[0], window_size[1] - gui.panelY), colours.playlist_panel_background) +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 - if prefs.bg_showcase_only and prefs.art_bg: - style_overlay.display() +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 - # 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 +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 - 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 +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.bg_showcase_only and prefs.art_bg: - ddt.alpha_bg = True - ddt.force_gray = True +def toggle_transcode_inplace(mode: int = 0) -> bool | None: + if mode == 1: + if prefs.transcode_inplace: + return True + return False - # 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() + 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 - # ddt.force_gray = True +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 - if pctl.playing_state == 3 and not radiobox.dummy_track.title: +def toggle_sbt(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.prefer_bottom_title + prefs.prefer_bottom_title ^= True + return None - 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) +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 - 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_use_title(mode: int = 0) -> bool | None: + if mode == 1: + return prefs.use_title + prefs.use_title ^= True + return None - else: +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 - if len(pctl.track_queue) < 1: - ddt.alpha_bg = False - return +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 draw.button("Return", 20, gui.panelY + 5, bg=colours.grey(30)): - # pass +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 prefs.bg_showcase_only and prefs.art_bg: - ddt.alpha_bg = True - ddt.force_gray = 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 - 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_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 gui.force_showcase_index >= 0: - index = gui.force_showcase_index - track = pctl.master_library[index] - else: +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 - if pctl.playing_state == 3: - track = radiobox.dummy_track - else: - index = pctl.track_queue[pctl.queue_step] - track = pctl.master_library[index] +def toggle_transition_crossfade(mode: int = 0) -> bool | None: + if mode == 1: + return True if prefs.use_transition_crossfade else False + prefs.use_transition_crossfade ^= True + return None - if not hide_art: +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 - # 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) +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 - # Draw album art in box - album_art_gen.display(track, (x, y), (box, box)) +def reload_backend() -> None: + gui.backend_reloading = True + logging.info("Reload backend...") + wait = 0 + pre_state = pctl.stop(True) - # 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 + 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") - # Check for lyrics if auto setting - test_auto_lyrics(track) + pctl.playerCommand = "unload" + pctl.playerCommandReady = True - gui.draw_vis4_top = False + wait = 0 + while pctl.playerCommand != "done": + time.sleep(0.01) + wait += 1 + if wait > 200: + break - 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) + tauon.thread_manager.ready_playback() - gcx = x + box + int(window_size[0] * 0.15) + 10 * gui.scale - gcx -= 100 * gui.scale + if pre_state == 1: + pctl.revert() + gui.backend_reloading = False - timed_ready = False - if True and prefs.show_lyrics_showcase: - timed_ready = timed_lyrics_ren.generate(track) +def gen_chart() -> None: + try: - if timed_ready and track.lyrics: + topchart = t_topchart.TopChart(tauon, album_art_gen) - # 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 + tracks = [] - timed_ready = prefs.prefer_synced_lyrics + source_tracks = pctl.multi_playlist[pctl.active_playlist_viewing].playlist_ids - 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 - elif True and prefs.show_lyrics_showcase and timed_ready: - w = window_size[0] - (x + box) - round(30 * gui.scale) - timed_lyrics_ren.render(track.index, gcx, y, w=w) - elif track.lyrics == "" or not prefs.show_lyrics_showcase: - w = window_size[0] - (x + box) - round(30 * gui.scale) - x = int(x + box + (window_size[0] - x - box) / 2) + if 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) - if hide_art: - x = window_size[0] // 2 + for item in dex: + tracks.append(pctl.get_track(source_tracks[item])) - # x = int((window_size[0]) / 2) - y = int(window_size[1] / 2) - round(60 * 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)) - if prefs.showcase_vis and prefs.backend == 1: - y -= round(30 * gui.scale) + path = topchart.generate( + tracks, prefs.chart_bg, prefs.chart_rows, prefs.chart_columns, prefs.chart_text, + prefs.chart_font, prefs.chart_tile, cascade) - 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) + 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 - y += round(48 * gui.scale) + gui.generating_chart = False - 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) + if path: + open_file(path) + else: + show_message(_("There was an error generating the chart"), _("Sorry!"), mode="error") + return - 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) + show_message(_("Chart generated"), mode="done") - gui.spec4_rec.x = x - (gui.spec4_rec.w // 2) - gui.spec4_rec.y = y + round(50 * gui.scale) +def update_playlist_call(): + gui.update + 2 + gui.pl_update = 2 - 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 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]) - 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 +def clear_gen(id: int) -> None: + del pctl.gen_codes[id] + show_message(_("Okay, it's a normal playlist now."), mode="done") - 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 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") - lyrics_ren.test_update(track) - tw, th = ddt.get_text_wh(lyrics_ren.text + "\n", 17, w, True) +def set_mini_mode(): + if gui.fullscreen: + return - 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) + global mouse_down + global mouse_up + global old_window_position + mouse_down = False + mouse_up = False + inp.mouse_click = False - 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 + if gui.maximized: + SDL_RestoreWindow(t_window) + update_layout_do() - def render_vis(self, top=False): + if gui.mode < 3: + old_window_position = get_window_position() - SDL_SetRenderTarget(renderer, gui.spec4_tex) - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) - SDL_RenderClear(renderer) + if prefs.mini_mode_on_top: + SDL_SetWindowAlwaysOnTop(t_window, True) - bx = 0 - by = 50 * gui.scale + gui.mode = 3 + gui.vis = 0 + gui.turbo = False + gui.draw_vis4_top = False + gui.level_update = False - 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]) + 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) - 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 + mini_mode.was_borderless = draw_border + SDL_SetWindowBordered(t_window, False) - for i in range(len(gui.spec4_array)): - gui.spec4_array[i] -= 0.1 - gui.spec4_array[i] = max(gui.spec4_array[i], 0) + size = (350, 429) + if prefs.mini_mode_mode == 1: + size = (330, 330) + if prefs.mini_mode_mode == 2: + size = (420, 499) + if prefs.mini_mode_mode == 3: + size = (430, 430) + if prefs.mini_mode_mode == 4: + size = (330, 80) + if prefs.mini_mode_mode == 5: + size = (350, 545) + style_overlay.flush() + tauon.thread_manager.ready("style") - if not top and (pctl.playing_state == 1 or pctl.playing_state == 3): - gui.update = 2 + if logical_size == window_size: + size = (int(size[0] * gui.scale), int(size[1] * gui.scale)) - slide = 0.7 - for i, bar in enumerate(gui.spec4_array): + logical_size[0] = size[0] + logical_size[1] = size[1] - # We wont draw higher bars that may not move - if i > 40: - break + SDL_SetWindowMinimumSize(t_window, 100, 100) - # 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 + SDL_SetWindowResizable(t_window, False) + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) - # 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)))) + if mini_mode.save_position: + SDL_SetWindowPosition(t_window, mini_mode.save_position[0], mini_mode.save_position[1]) - # 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) + 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 - SDL_RenderFillRect(renderer, gui.bar4) + gui.update += 3 - # Set distance between bars - bx += 8 * gui.scale +def restore_full_mode(): + logging.info("RESTORE FULL") + i_y = pointer(c_int(0)) + i_x = pointer(c_int(0)) + SDL_GetWindowPosition(t_window, i_x, i_y) + mini_mode.save_position = [i_x.contents.value, i_y.contents.value] - if top: - SDL_SetRenderTarget(renderer, None) - else: - SDL_SetRenderTarget(renderer, gui.main_texture) + if not mini_mode.was_borderless: + SDL_SetWindowBordered(t_window, True) - # SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) - SDL_RenderCopy(renderer, gui.spec4_tex, None, gui.spec4_rec) + logical_size[0] = gui.save_size[0] + logical_size[1] = gui.save_size[1] -class ColourPulse2: - """Animates colour between two colours""" - def __init__(self): + SDL_SetWindowPosition(t_window, gui.save_position[0], gui.save_position[1]) - 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): + SDL_SetWindowResizable(t_window, True) + SDL_SetWindowSize(t_window, logical_size[0], logical_size[1]) + SDL_SetWindowAlwaysOnTop(t_window, 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 macos: + # SDL_SetWindowMinimumSize(t_window, 560, 330) + # else: + SDL_SetWindowMinimumSize(t_window, 560, 330) - ani_time = 0.15 + restore_ignore_timer.set() # Hacky - if hit is True and self.active is False: - self.active = True - self.in_timer.set() + gui.mode = 1 - out_time = self.out_timer.get() - if out_time < ani_time: - self.in_timer.force_set(ani_time - out_time) + global mouse_down + global mouse_up + mouse_down = False + mouse_up = False + inp.mouse_click = False - elif hit is False and self.active is True: - self.active = False - self.out_timer.set() + 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 - in_time = self.in_timer.get() - if in_time < ani_time: - self.out_timer.force_set(ani_time - in_time) + #logging.info(window_size) - 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 + 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 - return colour_slide(low_hls, high_hls, pro, 1) + gui.update_layout() + if prefs.art_bg: + tauon.thread_manager.ready("style") -class ViewBox: +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 - 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 + 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 - self.border = 3 * gui.scale + 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 - 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) + artistoffset = 0 + indexLine = "" - # _ .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) + offset_font_extra = 0 + if gui.row_font_size > 14: + offset_font_extra = 8 - self.on_colour = [255, 190, 50, 255] - self.over_colour = [255, 190, 50, 255] - self.off_colour = colours.grey(40) + # In windows (arial?) draws numbers too high (hack fix) + num_y_offset = 0 + # if system == 'Windows': + # num_y_offset = 1 - if not reload: - gui.combo_was_album = False + if True or style == 1: - def activate(self, x): - self.x = x - self.active = True - self.clicked = False + # if not gui.rsp and not gui.combo_mode: + # width -= 10 * gui.scale - 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) + dash = False + if n_track.artist and colours.artist_text == colours.title_text: + dash = True - 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 n_track.title: - self.col_force_off = False + line = track_number_process(n_track.track_number) - # gui.level_2_click = False - gui.update = 2 + indexLine = line - def button(self, x, y, asset, test, colour_get=None, name="Unknown", animate=True, low=0, high=0): + 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 += " " - on = test() - rect = [x - 8 * gui.scale, - y - 8 * gui.scale, - asset.w + 16 * gui.scale, - asset.h + 16 * gui.scale] - fields.add(rect) + line = "" - if on: - colour = self.on_colour + if n_track.artist != "" and not dash: + line0 = n_track.artist - else: - colour = self.off_colour + artistoffset = ddt.text( + (start_x + 27 * gui.scale, y), + line0, + alpha_mod(artistc, album_fade), + gui.row_font_size, + int(width / 2)) - fun = None - col = False - if coll(rect): + line = n_track.title + else: + line += n_track.title + else: + line = \ + os.path.splitext(n_track.filename)[ + 0] - tool_tip.test(x + asset.w + 10 * gui.scale, y - 15 * gui.scale, name) + if p_track >= len(default_playlist): + gui.pl_update += 1 + return - col = True - if gui.level_2_click: - fun = test - if colour_get is None: - colour = self.over_colour + index = default_playlist[p_track] + star_x = 0 + total = star_store.get(index) - colour = colour_get.get(col, on, not on and not animate, low, high) + if gui.star_mode == "line" and total > 0 and pctl.master_library[index].length > 0: - # if "+" in name: - # - # colour = cctest.get(col, on, [0, 0.2, 0.0], [0, 0.8, 0.8]) + 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 on and not animate: - # colour = self.off_colour + lh = 1 + if gui.scale != 1: + lh = 2 - asset.render(x, y, colour) + colour = colours.star_line + if this_line_playing and colours.star_line_playing is not None: + colour = colours.star_line_playing - return fun + 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)) - def tracks(self, hit=False): + star_x += 6 * gui.scale - if hit is False: - return album_mode is False and \ - gui.combo_mode is False and \ - gui.rsp is False + 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 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 + draw_rating_widget(sx, sy, n_track) - view_tracks() + star_x += round(70 * gui.scale) - def side(self, hit=False): + if gui.star_mode == "star" and total > 0 and pctl.master_library[index].length != 0: + sx = width + start_x - 40 * gui.scale - offset_font_extra + sy = ry + (gui.playlist_row_height // 2) - (6 * gui.scale) + # if gui.scale == 1.25: + # sy += 1 + playtime_stars = star_count(total, pctl.master_library[index].length) - 1 - 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 + sx2 = sx + selected_star = -2 + rated_star = -1 - view_standard_meta() + # if key_ctrl_down: - def gallery1(self, hit: bool = False) -> bool | None: + c = 60 + d = 6 - if hit is False: - return album_mode is True # and gui.show_playlist is True + colour = [70, 70, 70, 255] + if colours.lm: + colour = [90, 90, 90, 255] + # colour = alpha_mod(indexc, album_fade) - 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 + for count in range(8): - if x_menu.active: - x_menu.close_next_frame = True + if selected_star < count and playtime_stars < count and rated_star < count: + break - force_album_view() + 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) - def radio(self, hit=False): + # if playtime_stars > 4: + # colour = [c + d * count, c + d * count, c + d * count, 255] + # if playtime_stars > 6: # and count < 1: + # colour = [230, 220, 60, 255] + if gui.tracklist_bg_is_light: + colour = alpha_blend([0, 0, 0, 200], ddt.text_background_colour) + else: + colour = alpha_blend([255, 255, 255, 50], ddt.text_background_colour) - if hit is False: - return gui.radio_view + # if selected_star > -2: + # if selected_star >= count: + # colour = (220, 200, 60, 255) + # else: + # if rated_star >= count: + # colour = (220, 200, 60, 255) - if not gui.radio_view: - enter_radio_view() - else: - exit_combo(restore=True) + star_pc_icon.render(sx, sy, colour) - if x_menu.active: - x_menu.close_next_frame = True + if gui.show_hearts: - def lyrics(self, hit=False): + xxx = star_x - if hit is False: - return gui.showcase_mode + count = 0 + spacing = 6 * gui.scale - if not gui.showcase_mode: - if gui.radio_view: - gui.was_radio = True - enter_showcase_view() + yy = ry + (gui.playlist_row_height // 2) - (5 * gui.scale) + if gui.scale == 1.25: + yy += 1 + if xxx > 0: + xxx += 3 * gui.scale - elif gui.was_radio: - enter_radio_view() - else: - exit_combo(restore=True) - if x_menu.active: - x_menu.close_next_frame = True + if love(False, index): + count = 1 - def col(self, hit=False): + x = width + start_x - 52 * gui.scale - offset_font_extra - xxx - if hit is False: - return gui.set_mode + f_store.store(display_you_heart, (x, yy)) - if not gui.set_mode: - if gui.combo_mode: - exit_combo() + star_x += 18 * gui.scale - if album_mode and gui.plw < 550 * gui.scale: - toggle_album_mode() + if "spotify-liked" in pctl.master_library[index].misc: - toggle_library_mode() + x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx - def artist_info(self, hit=False): + f_store.store(display_spot_heart, (x, yy)) - if hit is False: - return gui.artist_info_panel + star_x += heart_row_icon.w + spacing + 2 - gui.artist_info_panel ^= True - gui.update_layout() + for name in pctl.master_library[index].lfm_friend_likes: - def render(self): + # Limit to number of hears to display + if gui.star_mode == "none": + if count > 6: + break + elif count > 4: + break - if prefs.shuffle_lock: - self.active = False - self.clicked = False - return + x = width + start_x - 52 * gui.scale - offset_font_extra - (heart_row_icon.w + spacing) * count - xxx - if not self.active: - return + f_store.store(display_friend_heart, (x, yy, name)) - # 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 + count += 1 - x = self.x - 40 * gui.scale + star_x += heart_row_icon.w + spacing + 2 - vr = [x, gui.panelY, self.w, self.h] - # vr = [x, gui.panelY, 52 * gui.scale, 220 * gui.scale] + # Draw track number/index + display_queue = False - 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) + if pctl.force_queue: - x += 7 * gui.scale - y = gui.panelY + 14 * gui.scale + 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) - func = None + if marks: + display_queue = True - # 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 display_queue: - # if colours.lm: - # low = (0, 0.5, 0) + 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" - # ---- - #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] + # 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] - 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 + # if album_type: + # li += "🠗" - # ---- + colour = [244, 200, 66, 255] + if colours.lm: + colour = [220, 40, 40, 255] - y += 40 * gui.scale + ddt.text( + (start_x + 5 * gui.scale, y, 2), + li, colour, gui.row_font_size + 200 - 1) - high = [76, 137, 229, 255] # (.6, .6, .75) - if colours.lm: - # high = (.6, .80, .85) - high = [63, 63, 63, 255] + elif len(indexLine) > 2: - 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) + ddt.text( + (start_x + 5 * gui.scale, y, 2), indexLine, + alpha_mod(indexc, album_fade), gui.row_font_size) else: - test = self.button( - x, y, self.gallery1_img, self.gallery1, self.gallery1_colour, _("Gallery"), low=low, high=high) - if test is not None: - func = test - - # --- - y += 40 * gui.scale - - high = [76, 229, 229, 255] - if colours.lm: - # high = (.5, .7, .65) - high = [63, 63, 63, 255] + ddt.text( + (start_x, y), indexLine, + alpha_mod(indexc, album_fade), gui.row_font_size) - 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 + if dash and n_track.artist and n_track.title: + line = n_track.artist + " - " + n_track.title - # --- + ddt.text( + (start_x + 33 * gui.scale + artistoffset, y), + line, + alpha_mod(titlec, album_fade), + gui.row_font_size, + width - 71 * gui.scale - artistoffset - star_x - 20 * gui.scale) - y += 45 * gui.scale + line = get_display_time(n_track.length) - high = [107, 76, 229, 255] - if colours.lm: - # high = (.7, .75, .75) - high = [63, 63, 63, 255] + 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) - 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 + f_store.recall_all() - # -- +# def visit_radio_site_show_test(p): +# return "website_url" in prefs.radio_urls[p] and prefs.radio_urls[p]["website_url"] - y += 40 * gui.scale +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] - high = [92, 86, 255, 255] - if colours.lm: - # high = (.7, .75, .75) - high = [63, 63, 63, 255] +def visit_radio_station_site_deco(item): + return visit_radio_site_deco(item[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 +def visit_radio_site(item): + if "website_url" in item and item["website_url"]: + webbrowser.open(item["website_url"], new=2, autoraise=True) - # -- +def visit_radio_station(item): + visit_radio_site(item[1]) - y += 45 * gui.scale +def radio_saved_panel_test(_): + return radiobox.tab == 0 - high = [229, 205, 76, 255] - if colours.lm: - # high = (.9, .75, .65) - high = [63, 63, 63, 255] +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"]) - 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 +def create_artist_pl(artist: str, replace: bool = False): + source_pl = pctl.active_playlist_viewing + this_pl = pctl.active_playlist_viewing - # -- + if pctl.multi_playlist[source_pl].parent_playlist_id: + if pctl.multi_playlist[source_pl].title.startswith("Artist:"): + new = id_to_pl(pctl.multi_playlist[source_pl].parent_playlist_id) + if new is None: + # The original playlist is now gone + pctl.multi_playlist[source_pl].parent_playlist_id = "" + else: + source_pl = new + # replace = True - # 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 + playlist = [] - if func is not None: - func(True) + 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 gui.level_2_click and coll(vr): - x_menu.clicked = False + if replace: + pctl.multi_playlist[this_pl].playlist_ids[:] = playlist[:] + pctl.multi_playlist[this_pl].title = _("Artist: ") + artist + if album_mode: + reload_albums() - gui.level_2_click = False - if not x_menu.active: - self.active = False + # 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) -class DLMon: + pctl.gen_codes[pl_to_id(this_pl)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - def __init__(self): + else: - self.ticker = Timer() - self.ticker.force_set(8) + pctl.multi_playlist.append( + pl_gen( + title=_("Artist: ") + artist, + playlist_ids=playlist, + hide_title=False, + parent=pl_to_id(source_pl))) - self.watching = {} - self.ready = set() - self.done = set() + pctl.gen_codes[pl_to_id(len(pctl.multi_playlist) - 1)] = "s\"" + pctl.multi_playlist[source_pl].title + "\" a\"" + artist + "\"" - def scan(self): + switch_playlist(len(pctl.multi_playlist) - 1) - if len(self.watching) == 0: - if self.ticker.get() < 10: - return - elif self.ticker.get() < 2: - return +def aa_sort_alpha(): + prefs.artist_list_sort_mode = "alpha" + artist_list_box.saves.clear() - self.ticker.set() +def aa_sort_popular(): + prefs.artist_list_sort_mode = "popular" + artist_list_box.saves.clear() - for downloads in download_directories: +def aa_sort_play(): + prefs.artist_list_sort_mode = "play" + artist_list_box.saves.clear() - for item in os.listdir(downloads): +def toggle_artist_list_style(): + if prefs.artist_list_style == 1: + prefs.artist_list_style = 2 + else: + prefs.artist_list_style = 1 - path = os.path.join(downloads, item) +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 path in self.done: - continue +def toggle_artist_list_threshold_deco(): + if prefs.artist_list_threshold == 0: + return [colours.menu_text, colours.menu_background, _("Filter Small Artists")] + save = artist_list_box.saves.get(pctl.multi_playlist[pctl.active_playlist_viewing].uuid_int) + if save and save[5] == 0: + return [colours.menu_text_disabled, colours.menu_background, _("Include All Artists")] + return [colours.menu_text, colours.menu_background, _("Include All Artists")] - if path in self.ready and not os.path.exists(path): - del self.ready[path] - continue +def verify_discogs(): + return len(prefs.discogs_pat) == 40 - if path in self.watching and not os.path.exists(path): - del self.watching[path] - continue +def save_discogs_artist_thumb(artist, filepath): + logging.info("Searching discogs for artist image...") - # 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 + # Make artist name url safe + artist = artist.replace("/", "").replace("\\", "").replace(":", "") - min_age = (time.time() - stamp) / 60 - ext = os.path.splitext(path)[1][1:].lower() + # 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"] - if msys and "TauonMusicBox" in path: - continue + # 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 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] + # Respect rate limit + rate_remaining = r.headers["X-Discogs-Ratelimit-Remaining"] + if int(rate_remaining) < 30: + time.sleep(5) - # 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)) + # 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 os.path.exists(target_dir): - pass - #logging.info("Target folder for archive already exists") + response = urllib.request.urlopen(url, context=tls_context) + im = Image.open(response) - 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.") + width, height = im.size + if width > height: + delta = width - height + left = int(delta / 2) + upper = 0 + right = height + left + lower = height + else: + delta = height - width + left = 0 + upper = int(delta / 2) + right = width + lower = width + upper - elif 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: + im = im.crop((left, upper, right, lower)) + im.save(filepath, "JPEG", quality=90) + im.close() + logging.info("Found artist image from Discogs") - # 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 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 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 + response = urllib.request.urlopen(thumblink, timeout=10, context=tls_context) + info = response.info() - if len(self.watching) > 0: - gui.update += 1 + t = io.BytesIO() + t.seek(0) + t.write(response.read()) + l = 0 + t.seek(0, 2) + l = t.tell() + t.seek(0) -def dismiss_dl(): - dl_mon.ready.clear() - dl_mon.done.update(dl_mon.watching) - dl_mon.watching.clear() + if info.get_content_maintype() == "image" and l > 1000: + f = open(filepath, "wb") + f.write(t.read()) + f.close() -class Fader: + 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 __init__(self): +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")] - self.total_timer = Timer() - self.timer = Timer() - self.ani_duration = 0.3 - self.state = 0 # 0 = Want off, 1 = Want fade on - self.a = 0 # The fade progress (0-1) +# def 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] - def render(self): +def art_metadata_overlay(right, bottom, showc): + if not showc: + return - 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) + padding = 6 * gui.scale - rect = [0, 0, window_size[0], window_size[1]] - ddt.rect(rect, [0, 0, 0, int(110 * self.a)]) + if not key_shift_down: - if not (self.a == 0 or self.a == 1): - gui.update += 1 + line = "" + if showc[0] == 1: + line += "E " + elif showc[0] == 2: + line += "N " + else: + line += "F " - def rise(self): + line += str(showc[2] + 1) + "/" + str(showc[1]) - self.state = 1 - self.timer.hit() - self.total_timer.set() + y = bottom - 40 * gui.scale - def fall(self): + 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]) - self.state = 0 - self.timer.hit() - self.total_timer.set() + else: # Extended metadata -class EdgePulse: + line = "" + if showc[0] == 1: + line += "Embedded" + elif showc[0] == 2: + line += "Network" + else: + line += "File" - def __init__(self): + y = bottom - 76 * 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 + y += 18 * gui.scale - def pulse(self): - self.timer.set() + line = "" + line += showc[4] + line += " " + str(showc[3][0]) + "×" + str(showc[3][1]) -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 += str(showc[2] + 1) + "/" + str(showc[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: +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 bottom: - if mouse_wheel > 0: - self.timer.force_set(10) - return None - elif mouse_wheel < 0: - self.timer.force_set(10) - return None +def station_browse(): + radiobox.active = True + radiobox.edit_mode = False + radiobox.add_mode = False + radiobox.center = True + radiobox.tab = 1 - alpha = 30 - int(25 * (time / self.ani_duration)) - h_off = (h // 5) * (time / self.ani_duration) * 4 +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 - if colours.lm: - colour = (0, 0, 0, alpha) - else: - colour = (255, 255, 255, alpha) +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 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 remove_station(item): + index = item[0] + del pctl.radio_playlists[pctl.radio_playlist_viewing]["items"][index] - def pulse(self): - self.timer.set() +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: @@ -37662,68 +37801,6 @@ def hit_callback(win, point, data): return SDL_HITTEST_NORMAL return SDL_HITTEST_NORMAL -class Undo: - - def __init__(self): - - self.e = [] - - def undo(self): - - if not self.e: - show_message(_("There are no more steps to undo.")) - return - - job = self.e.pop() - - if job[0] == "playlist": - pctl.multi_playlist.append(job[1]) - switch_playlist(len(pctl.multi_playlist) - 1) - elif job[0] == "tracks": - - uid = job[1] - li = job[2] - - for i, playlist in enumerate(pctl.multi_playlist): - if playlist.uuid_int == uid: - pl = playlist.playlist_ids - switch_playlist(i) - break - else: - logging.info("No matching playlist ID to restore tracks to") - return - - for i, ref in reversed(li): - - if i > len(pl): - logging.error("restore track error - playlist not correct length") - continue - pl.insert(i, ref) - - if not pctl.playlist_view_position < i < pctl.playlist_view_position + gui.playlist_view_length: - pctl.playlist_view_position = i - logging.debug("Position changed by undo") - elif job[0] == "ptt": - j, fr, fr_s, fr_scr, so, to_s, to_scr = job - star_store.insert(fr.index, fr_s) - star_store.insert(to.index, to_s) - to.lfm_scrobbles = to_scr - fr.lfm_scrobbles = fr_scr - - gui.pl_update = 1 - - def bk_playlist(self, pl_index: int) -> None: - - self.e.append(("playlist", pctl.multi_playlist[pl_index])) - - def bk_tracks(self, pl_index: int, indis) -> None: - - uid = pctl.multi_playlist[pl_index].uuid_int - self.e.append(("tracks", uid, indis)) - - def bk_playtime_transfer(self, fr, fr_s, fr_scr, so, to_s, to_scr) -> None: - self.e.append(("ptt", fr, fr_s, fr_scr, so, to_s, to_scr)) - def reload_scale(): auto_scale() @@ -38205,29 +38282,6 @@ def update_layout_do(): if prefs.art_bg: tauon.thread_manager.ready("style") -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 - def window_is_focused() -> bool: """Thread safe?""" if SDL_GetWindowFlags(t_window) & SDL_WINDOW_INPUT_FOCUS: @@ -38812,7 +38866,7 @@ def drop_file(target: str): xdpi = 0 detect_macstyle = False -gtk_settings: Settings | None = None +gtk_settings: Gtk.Settings | None = None mac_close = (253, 70, 70, 255) mac_maximize = (254, 176, 36, 255) mac_minimize = (42, 189, 49, 255) @@ -40308,18 +40362,6 @@ def key_callback(event): 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), - ] - try: xcu = ctypes.cdll.LoadLibrary("libXcursor.so") except Exception: @@ -40458,51 +40500,6 @@ def get_xcursor(name: str): rt = 0 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 - - 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() diff --git a/src/tauon/t_modules/t_prefs.py b/src/tauon/t_modules/t_prefs.py index 7b861713a..2b03b227a 100644 --- a/src/tauon/t_modules/t_prefs.py +++ b/src/tauon/t_modules/t_prefs.py @@ -1,6 +1,13 @@ from __future__ import annotations -from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + import gi + gi.require_version("Gtk", "3.0") + from gi.repository import Gtk class Prefs: """Used to hold any kind of settings""" @@ -8,7 +15,7 @@ class Prefs: 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, + gtk_settings: Gtk.Settings | None, discord_allow: bool, flatpak_mode: bool, desktop: str | None, window_opacity: float, scale: float, ) -> None: self.colour_from_image: bool = False