diff --git a/assets/graphics/backwards.png b/assets/graphics/backwards.png new file mode 100644 index 0000000..474efdc Binary files /dev/null and b/assets/graphics/backwards.png differ diff --git a/assets/graphics/forward.png b/assets/graphics/forward.png new file mode 100644 index 0000000..cd6bba2 Binary files /dev/null and b/assets/graphics/forward.png differ diff --git a/assets/graphics/volume.png b/assets/graphics/volume.png new file mode 100644 index 0000000..1ef00dc Binary files /dev/null and b/assets/graphics/volume.png differ diff --git a/menus/downloader.py b/menus/downloader.py index 53d050a..402fe1d 100644 --- a/menus/downloader.py +++ b/menus/downloader.py @@ -161,7 +161,7 @@ class Downloader(arcade.gui.UIView): self.yt_dl_buffer = f"Successfully downloaded {title} to {path}" - def ensure_yt_dlp(): + def ensure_yt_dlp(self): system = platform.system() if system == "Windows": diff --git a/menus/main.py b/menus/main.py index 0a28657..2e242c9 100644 --- a/menus/main.py +++ b/menus/main.py @@ -1,10 +1,10 @@ -import random, asyncio, pypresence, time, copy, json, os, logging, webbrowser +import random, asyncio, pypresence, time, copy, json, os, logging import arcade, pyglet from utils.preload import * from utils.constants import button_style, slider_style, audio_extensions, discord_presence_id, view_modes -from utils.utils import FakePyPresence, UIFocusTextureButton, MusicItem, convert_seconds_to_date -from utils.music_handling import update_last_play_statistics, extract_metadata_and_thumbnail, adjust_volume, truncate_end, convert_timestamp_to_time_ago +from utils.utils import FakePyPresence, UIFocusTextureButton, MusicItem +from utils.music_handling import update_last_play_statistics, extract_metadata_and_thumbnail, adjust_volume, truncate_end from utils.file_watching import watch_directories, watch_files from thefuzz import process, fuzz @@ -13,9 +13,9 @@ from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from arcade.gui.experimental.focus import UIFocusGroup class Main(arcade.gui.UIView): - def __init__(self, pypresence_client: None | FakePyPresence | pypresence.Presence=None, current_mode: str | None=None, current_music_name: str | None=None, - current_length: int | None=None, current_music_player: pyglet.media.Player | None=None, queue: list | None=None, - loaded_sounds: dict | None=None, shuffle: bool=False): + def __init__(self, pypresence_client: None | FakePyPresence | pypresence.Presence=None, current_mode: str | None=None, current_music_artist: str | None=None, + current_music_title: str | None=None, current_music_path: str | None=None, current_length: int | None=None, + current_music_player: pyglet.media.Player | None=None, queue: list | None=None, loaded_sounds: dict | None=None, shuffle: bool=False): super().__init__() self.pypresence_client = pypresence_client @@ -61,23 +61,29 @@ class Main(arcade.gui.UIView): self.file_metadata = {} self.tab_buttons = {} self.music_buttons = {} + self.queue = [] - self.current_music_name = current_music_name - self.current_length = current_length if current_length else 0 + self.current_music_artist = current_music_artist + self.current_music_title = current_music_title self.current_music_player = current_music_player + self.current_music_path = current_music_path + self.current_length = current_length if current_length else 0 + self.shuffle = shuffle + self.volume = self.settings_dict.get("default_volume", 100) + self.current_mode = current_mode or "files" self.current_playlist = None - self.time_to_seek = None - self.tab_observer = None - self.playlist_observer = None - self.should_reload = False self.current_tab = self.tab_options[0] - self.queue = queue if queue else [] - self.loaded_sounds = loaded_sounds if loaded_sounds else {} - self.shuffle = shuffle self.search_term = "" self.highest_score_file = "" - self.volume = self.settings_dict.get("default_volume", 100) + + self.time_to_seek = None + self.should_reload = False + + self.tab_observer = None + self.playlist_observer = None + + self.loaded_sounds = loaded_sounds if loaded_sounds else {} def on_show_view(self): super().on_show_view() @@ -99,7 +105,7 @@ class Main(arcade.gui.UIView): self.current_playlist = list(self.playlist_content.keys())[0] if self.playlist_content else None # Scrollable Sounds - self.scroll_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.8), space_between=15, vertical=False)) + self.scroll_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.85), space_between=15, vertical=False)) self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen self.scroll_area.scroll_speed = -50 @@ -135,34 +141,58 @@ class Main(arcade.gui.UIView): # Controls - self.control_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.1), space_between=10, vertical=False)) + self.now_playing_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.99, 0.075), space_between=10, vertical=False)) - self.current_music_thumbnail_image = self.control_box.add(arcade.gui.UIImage(texture=music_icon, width=self.window.width / 25, height=self.window.height / 15)) + self.current_music_thumbnail_image = self.now_playing_box.add(arcade.gui.UIImage(texture=music_icon, width=self.window.width / 25, height=self.window.height / 15)) - self.current_music_label = self.control_box.add(arcade.gui.UILabel(text=truncate_end(self.current_music_name, int(self.window.width / 30)) if self.current_music_name else "No songs playing", font_name="Roboto", font_size=16)) - self.time_label = self.control_box.add(arcade.gui.UILabel(text="00:00", font_name="Roboto", font_size=16)) + self.now_playing_box.add(arcade.gui.UISpace(width=10)) - self.progressbar = self.control_box.add(arcade.gui.UISlider(style=slider_style, width=self.window.width / 3, height=35)) - self.progressbar.on_change = self.on_progress_change + # Artist - Title + self.current_music_box = self.now_playing_box.add(arcade.gui.UIBoxLayout(space_between=10)) + self.current_music_title_label = self.current_music_box.add(arcade.gui.UILabel(text=truncate_end(self.current_music_title, int(self.window.width / 50)) if self.current_music_title else "No songs playing", font_name="Roboto", font_size=12)) + self.current_music_artist_label = self.current_music_box.add(arcade.gui.UILabel(text=truncate_end(self.current_music_artist, int(self.window.width / 50)) if self.current_music_artist else "No songs playing", font_name="Roboto", font_size=10, text_color=arcade.color.GRAY)) - self.pause_start_button = self.control_box.add(UIFocusTextureButton(texture=pause_icon if not self.current_music_player or self.current_music_player.playing else resume_icon)) + self.now_playing_box.add(arcade.gui.UISpace(width=self.window.width / 16)) + + # Time box with controls, progressbar and time + self.current_time_box = self.now_playing_box.add(arcade.gui.UIBoxLayout(space_between=10)) + + # Controls + self.controls_box = self.current_time_box.add(arcade.gui.UIBoxLayout(space_between=25, vertical=False)) + + self.shuffle_button = self.controls_box.add(UIFocusTextureButton(texture=shuffle_icon)) + self.shuffle_button.on_click = lambda event: self.shuffle_sound() + + self.previous_button = self.controls_box.add(UIFocusTextureButton(texture=backwards_icon)) + self.previous_button.on_click = lambda event: self.previous_track() + + self.pause_start_button = self.controls_box.add(UIFocusTextureButton(texture=pause_icon if not self.current_music_player or self.current_music_player.playing else resume_icon)) self.pause_start_button.on_click = lambda event: self.pause_start() - self.skip_button = self.control_box.add(UIFocusTextureButton(texture=stop_icon)) - self.skip_button.on_click = lambda event: self.skip_sound() + self.next_button = self.controls_box.add(UIFocusTextureButton(texture=forward_icon)) + self.next_button.on_click = lambda event: self.next_track() - self.loop_button = self.control_box.add(UIFocusTextureButton(texture=no_loop_icon if self.current_music_player and self.current_music_player.loop else loop_icon)) + self.loop_button = self.controls_box.add(UIFocusTextureButton(texture=no_loop_icon if self.current_music_player and self.current_music_player.loop else loop_icon)) self.loop_button.on_click = lambda event: self.loop_sound() - self.shuffle_button = self.control_box.add(UIFocusTextureButton(texture=shuffle_icon)) - self.shuffle_button.on_click = lambda event: self.shuffle_sound() + # Time - Progressbar - Full Length + self.progressbar_box = self.current_time_box.add(arcade.gui.UIBoxLayout(vertical=False, space_between=10)) + + self.time_label = self.progressbar_box.add(arcade.gui.UILabel(text="00:00", font_name="Roboto", font_size=13)) + + self.progressbar = self.progressbar_box.add(arcade.gui.UISlider(style=slider_style, width=self.window.width / 3, height=self.window.height / 45)) + self.progressbar.on_change = self.on_progress_change + + self.full_length_label = self.progressbar_box.add(arcade.gui.UILabel(text="00:00", font_name="Roboto", font_size=13)) if self.current_music_player: self.progressbar.max_value = self.current_length self.volume = int(self.current_music_player.volume * 100) - self.volume_label = self.control_box.add(arcade.gui.UILabel(text=f"{self.volume}%", font_name="Roboto", font_size=16)) - self.volume_slider = self.control_box.add(arcade.gui.UISlider(style=slider_style, width=self.window.width / 10, height=35, value=self.volume, max_value=100)) + self.now_playing_box.add(arcade.gui.UISpace(width=self.window.width / 16)) + + self.volume_icon_label = self.now_playing_box.add(arcade.gui.UIImage(texture=volume_icon)) + self.volume_slider = self.now_playing_box.add(arcade.gui.UISlider(style=slider_style, width=self.window.width / 10, height=35, value=self.volume, max_value=100)) self.volume_slider.on_change = self.on_volume_slider_change self.no_music_label = self.anchor.add(arcade.gui.UILabel(text="No music files were found in this directory or playlist.", font_name="Roboto", font_size=24), anchor_x="center", anchor_y="center") @@ -228,6 +258,32 @@ class Main(arcade.gui.UIView): self.load_tabs() self.update_buttons() + def previous_track(self): + if not self.current_music_player is None: + if self.current_mode == "files": + if os.path.basename(self.current_music_path) in self.tab_content[self.current_tab]: + current_idx = self.tab_content[self.current_tab].index(os.path.basename(self.current_music_path)) + self.queue.append(f"{self.current_tab}/{self.tab_content[self.current_tab][current_idx - 1]}") + elif self.current_mode == "playlist": + if os.path.basename(self.current_music_path) in self.playlist_content[self.current_playlist]: + current_idx = self.playlist_content[self.current_playlist].index(os.path.basename(self.current_music_path)) + self.queue.append(f"{self.current_playlist}/{self.playlist_content[self.current_playlist][current_idx - 1]}") + + self.skip_sound() + + def next_track(self): + if not self.current_music_player is None: + if self.current_mode == "files": + if os.path.basename(self.current_music_path) in self.tab_content[self.current_tab]: + current_idx = self.tab_content[self.current_tab].index(os.path.basename(self.current_music_path)) + self.queue.append(f"{self.current_tab}/{self.tab_content[self.current_tab][current_idx + 1]}") + elif self.current_mode == "playlist": + if os.path.basename(self.current_music_path) in self.playlist_content[self.current_playlist]: + current_idx = self.playlist_content[self.current_playlist].index(os.path.basename(self.current_music_path)) + self.queue.append(f"{self.current_playlist}/{self.playlist_content[self.current_playlist][current_idx + 1]}") + + self.skip_sound() + def skip_sound(self): if not self.current_music_player is None: if self.current_music_player.loop: @@ -235,15 +291,18 @@ class Main(arcade.gui.UIView): return if self.settings_dict.get("music_mode", "Streaming") == "Streaming": - del self.loaded_sounds[self.current_music_name] + del self.loaded_sounds[self.current_music_path] self.current_length = 0 - self.current_music_name = None + self.current_music_artist = None + self.current_music_title = None self.current_music_player.delete() self.current_music_player = None + self.current_music_path = None self.progressbar.value = 0 self.current_music_thumbnail_image.texture = music_icon - self.current_music_label.text = "No songs playing" + self.current_music_title_label.text = "No songs playing" + self.full_length_label.text = "00:00" self.time_label.text = "00:00" self.update_buttons() @@ -265,7 +324,7 @@ class Main(arcade.gui.UIView): def view_metadata(self, file_path): from menus.metadata_viewer import MetadataViewer - self.window.show_view(MetadataViewer(self.pypresence_client, "music", self.file_metadata[file_path], file_path, self.current_mode, self.current_music_name, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) + self.window.show_view(MetadataViewer(self.pypresence_client, "music", self.file_metadata[file_path], file_path, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def show_content(self, tab): for music_button in self.music_buttons.values(): @@ -332,7 +391,7 @@ class Main(arcade.gui.UIView): with open("settings.json", "w") as file: file.write(json.dumps(self.settings_dict, indent=4)) - self.window.show_view(Main(self.pypresence_client, self.current_mode, self.current_music_name, # temporarily fixes the issue of bad resolution after deletion with less than 2 rows + self.window.show_view(Main(self.pypresence_client, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, # temporarily fixes the issue of bad resolution after deletion with less than 2 rows self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def load_content(self): @@ -397,7 +456,6 @@ class Main(arcade.gui.UIView): def on_volume_slider_change(self, event): self.volume = int(self.volume_slider.value) - self.volume_label.text = f"{self.volume}%" if not self.current_music_player is None: self.current_music_player.volume = self.volume / 100 @@ -413,10 +471,8 @@ class Main(arcade.gui.UIView): artist, title = self.file_metadata[music_path]["artist"], self.file_metadata[music_path]["title"] - music_name = f"{artist} - {title}" - if self.settings_dict.get("normalize_audio", True): - self.current_music_label.text = "Normalizing audio..." + self.current_music_title_label.text = "Normalizing audio..." self.window.draw(delta_time) # draw before blocking try: adjust_volume(music_path, self.settings_dict.get("normalized_volume", -8)) @@ -425,21 +481,24 @@ class Main(arcade.gui.UIView): update_last_play_statistics(music_path) - if not music_name in self.loaded_sounds: - self.loaded_sounds[music_name] = arcade.Sound(music_path, streaming=self.settings_dict.get("music_mode", "Stream") == "Stream") + if not music_path in self.loaded_sounds: + self.loaded_sounds[music_path] = arcade.Sound(music_path, streaming=self.settings_dict.get("music_mode", "Stream") == "Stream") self.volume = self.settings_dict.get("default_volume", 100) - self.volume_label.text = f"{self.volume}%" self.volume_slider.value = self.volume - self.current_music_player = self.loaded_sounds[music_name].play() + self.current_music_player = self.loaded_sounds[music_path].play() self.current_music_player.volume = self.volume / 100 - self.current_length = self.loaded_sounds[music_name].get_length() + self.current_length = self.loaded_sounds[music_path].get_length() - self.current_music_name = music_name + self.current_music_artist = artist + self.current_music_title = title + self.current_music_title_label.text = title + self.current_music_artist_label.text = artist + self.current_music_path = music_path self.current_music_thumbnail_image.texture = self.file_metadata[music_path]["thumbnail"] - self.current_music_label.text = truncate_end(music_name, int(self.window.width / 30)) self.time_label.text = "00:00" + self.full_length_label.text = "00:00" self.progressbar.max_value = self.current_length self.progressbar.value = 0 @@ -459,6 +518,8 @@ class Main(arcade.gui.UIView): self.progressbar.value = self.current_music_player.time mins, secs = divmod(self.current_music_player.time, 60) self.time_label.text = f"{int(mins):02d}:{int(secs):02d}" + mins, secs = divmod(self.current_length, 60) + self.full_length_label.text = f"{int(mins):02d}:{int(secs):02d}" def on_key_press(self, symbol: int, modifiers: int) -> bool | None: if symbol == arcade.key.SPACE: @@ -512,25 +573,25 @@ class Main(arcade.gui.UIView): from menus.settings import Settings arcade.unschedule(self.update_presence) self.ui.clear() - self.window.show_view(Settings(self.pypresence_client, self.current_mode, self.current_music_name, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) + self.window.show_view(Settings(self.pypresence_client, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def new_tab(self): from menus.new_tab import NewTab arcade.unschedule(self.update_presence) self.ui.clear() - self.window.show_view(NewTab(self.pypresence_client, self.current_mode, self.current_music_name, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) + self.window.show_view(NewTab(self.pypresence_client, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def add_music(self): from menus.add_music import AddMusic arcade.unschedule(self.update_presence) self.ui.clear() - self.window.show_view(AddMusic(self.pypresence_client, self.current_mode, self.current_music_name, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) + self.window.show_view(AddMusic(self.pypresence_client, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def downloader(self): from menus.downloader import Downloader arcade.unschedule(self.update_presence) self.ui.clear() - self.window.show_view(Downloader(self.pypresence_client, self.current_mode, self.current_music_name, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) + self.window.show_view(Downloader(self.pypresence_client, self.current_mode, self.current_music_artist, self.current_music_title, self.current_music_path, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def reload(self): self.load_content() @@ -545,8 +606,8 @@ class Main(arcade.gui.UIView): self.anchor.detect_focusable_widgets() def update_presence(self, _): - if self.current_music_label.text != "No songs playing" and self.current_music_player: - details = f"Listening to {self.current_music_name}" + if self.current_music_title != "No songs playing" and self.current_music_player: + details = f"Listening to {self.current_music_artist} - {self.current_music_title}" if self.current_music_player.playing: mins, secs = divmod(self.current_length, 60) diff --git a/menus/metadata_viewer.py b/menus/metadata_viewer.py index c3d6682..3acce72 100644 --- a/menus/metadata_viewer.py +++ b/menus/metadata_viewer.py @@ -58,8 +58,8 @@ class MetadataViewer(arcade.gui.UIView): albums = ', '.join(list(self.online_metadata[2].keys())) name = f"{self.file_metadata['artist']} - {self.file_metadata['title']} Metadata" metadata_text = f'''File path: {self.file_path} -File Artist: {self.file_metadata['artist']} -MusicBrainz Artists: {', '.join([artist for artist in self.online_metadata[1]])} +File Artist(s): {self.file_metadata['artist']} +MusicBrainz Artist(s): {', '.join([artist for artist in self.online_metadata[1]])} Title: {self.file_metadata['title']} MusicBrainz ID: {self.online_metadata[0]['musicbrainz_id']} ISRC(s): {', '.join(self.online_metadata[0]['isrc-list']) if self.online_metadata[0]['isrc-list'] else "None"} diff --git a/utils/preload.py b/utils/preload.py index a4512e1..e4e33ab 100644 --- a/utils/preload.py +++ b/utils/preload.py @@ -9,9 +9,10 @@ resume_icon = arcade.load_texture("assets/graphics/resume.png") stop_icon = arcade.load_texture("assets/graphics/stop.png") loop_icon = arcade.load_texture("assets/graphics/loop.png") no_loop_icon = arcade.load_texture("assets/graphics/no_loop.png") - shuffle_icon = arcade.load_texture("assets/graphics/shuffle.png") no_shuffle_icon = arcade.load_texture("assets/graphics/no_shuffle.png") +forward_icon = arcade.load_texture("assets/graphics/forward.png") +backwards_icon = arcade.load_texture("assets/graphics/backwards.png") settings_icon = arcade.load_texture("assets/graphics/settings.png") download_icon = arcade.load_texture("assets/graphics/download.png") @@ -21,3 +22,4 @@ playlist_icon = arcade.load_texture("assets/graphics/playlist.png") files_icon = arcade.load_texture("assets/graphics/files.png") music_icon = arcade.load_texture("assets/graphics/music.png") +volume_icon = arcade.load_texture("assets/graphics/volume.png") \ No newline at end of file