diff --git a/run.py b/run.py index 19e1e5e..a3e7744 100644 --- a/run.py +++ b/run.py @@ -1,2 +1,3 @@ from yami.main import entry -entry() \ No newline at end of file + +entry() diff --git a/vlc_.py b/vlc_.py index 574587a..8803237 100644 --- a/vlc_.py +++ b/vlc_.py @@ -1,11 +1,14 @@ import vlc -media=vlc.Media(r"/Users/mith/Music/Music/Media.localized/Music/The Avalanches/Since I Left You (20th Anniversary Deluxe Edition)/1-05 Avalanche Rock.m4a") -player=vlc.MediaPlayer() +media = vlc.Media( + r"/Users/mith/Music/Music/Media.localized/Music/The Avalanches/Since I Left You (20th Anniversary Deluxe Edition)/1-05 Avalanche Rock.m4a" +) +player = vlc.MediaPlayer() player.set_media(media) -inst: vlc.Instance =player.get_instance() +inst: vlc.Instance = player.get_instance() player.play() import time + time.sleep(0.5) print(media.get_meta(1)) print(media.get_meta(0)) @@ -13,4 +16,4 @@ print(media.get_meta(15)) while player.is_playing(): - time.sleep(0.5) \ No newline at end of file + time.sleep(0.5) diff --git a/yami/control.py b/yami/control.py index a95fcdf..75248e8 100644 --- a/yami/control.py +++ b/yami/control.py @@ -4,7 +4,7 @@ import logging import customtkinter as ctk import vlc -from .util import BUTTON_WIDTH, PlayerState +from .util import BUTTON_WIDTH class ControlBar(ctk.CTkFrame): @@ -17,12 +17,11 @@ def __init__( super().__init__(parent, corner_radius=10, fg_color="#121212") # SETUP - self.music_player = parent + self.parent = parent self.pause_icon = parent.pause_icon self.play_icon = parent.play_icon self.prev_icon = parent.prev_icon self.next_icon = parent.next_icon - self.play_next_command = parent.play_next_song self.title_max_chars = 40 # WIDGETS @@ -37,7 +36,7 @@ def __init__( ) self.next_button = ctk.CTkButton( self, - command=self.play_next_command, + command=self.parent.play_next_song, width=BUTTON_WIDTH, text="", corner_radius=10, @@ -48,7 +47,7 @@ def __init__( text="", width=BUTTON_WIDTH, corner_radius=10, - command=self.play_previous, + command=self.parent.play_previous, image=self.prev_icon, ) self.music_title_label = ctk.CTkLabel( @@ -72,57 +71,33 @@ def __init__( self.grid_columnconfigure(4, weight=0) # PLACEMENT - self.music_title_label.grid( - row=0, column=0, sticky="w", padx=5, pady=10 - ) + self.music_title_label.grid(row=0, column=0, sticky="w", padx=5, pady=10) self.playback_label.grid(row=0, column=1, sticky="w", padx=5, pady=10) self.prev_button.grid(row=0, column=2, sticky="nsew", padx=5, pady=10) self.play_button.grid(row=0, column=3, sticky="nsew", padx=5, pady=10) self.next_button.grid(row=0, column=4, sticky="nsew", padx=5, pady=10) + logging.debug("initialized control bar") def play_pause(self, event=None): """Plays Or Pauses The Music""" - if self.music_player.music.get_state() == vlc.State.Playing: - self.music_player.music.pause() - logging.info("paused") + if self.parent.music_list_player.get_state() == vlc.State.Playing: + self.parent.music_list_player.pause() + logging.debug("paused") else: - self.music_player.music.play() - - logging.info("resumed") + self.parent.music_list_player.play() + logging.debug("resumed") self.update_play_button() def update_play_button(self): """Switches Play/Pause Icon""" - if self.music_player.music.get_state()==vlc.State.Playing: + if self.parent.music_list_player.get_state() == vlc.State.Playing: self.play_button.configure(image=self.pause_icon) + logging.debug("updated play button to pause") else: self.play_button.configure(image=self.play_icon) - - def play_previous(self, event=None): - """Play Previous Song/Goto Last Song""" - - if not self.music_player.playlist: - return - - # PLAY FROM END - if self.music_player.playlist_index == 0: - logging.info("playing from end") - self.music_player.playlist_index = ( - len(self.music_player.playlist) - 1 - ) - # PLAY PREVIOUS - else: - self.music_player.playlist_index -= 1 - logging.info("playing previous") - self.music_player.load_and_play_song(self.music_player.playlist_index) - - # UPDATE SELECTION - self.music_player.playlist_frame.song_list.selection_clear(0, tk.END) - self.music_player.playlist_frame.song_list.select_set( - self.music_player.playlist_index - ) + logging.debug("updated play button to play") # TRUNCATOR def set_music_title(self, title, artist): @@ -132,6 +107,7 @@ def set_music_title(self, title, artist): truncated_title = title[: self.title_max_chars - 3] + "..." else: truncated_title = title + logging.debug("truncated title been set to %s", title) self.music_title_label.configure( text=truncated_title + " - " + artist.replace("/", ",") ) diff --git a/yami/cover_art.py b/yami/cover_art.py index 1417bca..6f1899c 100644 --- a/yami/cover_art.py +++ b/yami/cover_art.py @@ -1,6 +1,7 @@ """Frame For Cover Art""" import customtkinter as ctk +import logging class CoverArtFrame(ctk.CTkFrame): @@ -22,3 +23,4 @@ def __init__(self, parent): self.cover_art_label.grid( sticky="nsew", ) + logging.debug("initialized covert art frame") diff --git a/yami/main.py b/yami/main.py index a8e0ea9..1d23ae5 100644 --- a/yami/main.py +++ b/yami/main.py @@ -6,9 +6,12 @@ """add sys args and logs""" logging.getLogger().setLevel(logging.INFO) + + def entry(): app = MusicPlayer() app.mainloop() -if __name__=="__main__": + +if __name__ == "__main__": entry() diff --git a/yami/music.py b/yami/music.py index c9afc25..3efe17b 100644 --- a/yami/music.py +++ b/yami/music.py @@ -14,13 +14,12 @@ import vlc - from .topbar import TopBar from .playlist import PlaylistFrame from .control import ControlBar from .cover_art import CoverArtFrame from .progress import BottomFrame -from .util import GEOMETRY, TITLE, PlayerState, EVENT_INTERVAL +from .util import GEOMETRY, TITLE, PlayerState, EVENT_INTERVAL, make_time_string ctk.set_default_color_theme("yami/data/theme.json") @@ -39,271 +38,154 @@ def __init__(self: ctk.CTk, loop=None): self.title(TITLE) # STATE - self.playlist = [] - #self.STATE = PlayerState.STOPPED + self.playlist = [] self.current_folder = "" - self.playlist_index = 0 + self.loop = loop if loop is not None else asyncio.new_event_loop() - self.downloader = spotdl.Downloader( - spotdl.DownloaderOptions(threads=4) - ) + self.downloader = spotdl.Downloader(spotdl.DownloaderOptions(threads=4)) spotdl.SpotifyClient.init( "5f573c9620494bae87890c0f08a60293", "212476d9b0f3472eaa762d90b19b0ba8", ) - # SETUP PYGAME self.initialize_vlc() - # ICONS + # TKINTER SETUP self.setup_icons() + self.setup_frames() + self.setup_widget_packing() - # FRAMES - self.topbar = TopBar(self) - self.control_bar = ControlBar(self) - self.playlist_frame = PlaylistFrame(self) - self.bottom_frame = BottomFrame(self) - self.cover_art_frame = CoverArtFrame(self) + self.setup_keybindings() - # BINDINGS AND EVENTS - self.setup_bindings() + self.event_manager = self.music_list_player.event_manager() + self.event_manager.event_attach( + vlc.EventType.MediaListPlayerNextItemSet, self.change_info + ) + self.update_loop() + self.after(EVENT_INTERVAL, self.update) - # WIDGET PLACEMENT - self.topbar.pack(side=tk.TOP, fill=tk.X) - self.bottom_frame.pack(side=tk.BOTTOM, fill=tk.X) - self.control_bar.pack(side=tk.BOTTOM, fill=tk.X) - self.playlist_frame.pack(side=tk.RIGHT) - self.cover_art_frame.pack(side=tk.LEFT, padx=10) + def update(self, event=None): + if self.music_list_player.get_state() == vlc.State.Playing: - # UPDATE LOOP - #self.after(EVENT_INTERVAL, self.update) - event_manager=self.music.event_manager() - event_manager.event_attach(vlc.EventType.MediaPlayerEndReached,self.play_next_song) - #event_manager.event_attach(vlc.EventType.MediaPlayerPositionChanged,self.update) - #self.update_loop() - self.after(EVENT_INTERVAL,self.update) - - '''def update(self,*args): - if self.music.get_state()== vlc.State.Opening: - self.after(EVENT_INTERVAL, self.update) - - - if self.music.get_state() == vlc.State.Playing: - song_position = self.get_song_position() - self.song_length=self.music.get_length()//1000 - - curtime=song_position*self.music.get_length()//1000 - #print(curtime,song_position,self.music.get_length()) - - # GETS RATIO OF PROGRESS - #progress = song_position / self.song_length + song_position = self.music.get_position() self.bottom_frame.progress_bar.set(song_position) - minutes = int(curtime // 60) # logic wrong - seconds = int(curtime % 60) - - song_min = int(self.song_length // 60) - song_sec = int(self.song_length % 60) - - time_string = ( - f"{minutes:02d}:{seconds:02d} / {song_min:02d}:{song_sec:02d}" + self.control_bar.playback_label.configure( + text=make_time_string(song_position, self.music.get_length() // 1000) ) - self.control_bar.playback_label.configure(text=time_string) - - #if self.music.get_state()==vlc.State.Ended: # check is still song is not over or not song position less than song length means not yet reached end - # print("hi") - self.after(EVENT_INTERVAL, self.update) - elif self.music.get_state()==vlc.State.Ended: - self.play_next_song() - - elif self.music.get_state() == vlc.State.Paused: - self.after(EVENT_INTERVAL, self.update) - else: - self.bottom_frame.progress_bar.set(0)''' - - - def update(self,event=None): - if self.music.get_state()==vlc.State.Playing: - song_position = self.get_song_position() - self.song_length=self.music.get_length()//1000 # can make this global - curtime=song_position*self.song_length - print(curtime,song_position,self.music.get_length()) - - - self.bottom_frame.progress_bar.set(song_position) + self.after(EVENT_INTERVAL, self.update) - cur_minutes = int(curtime // 60) - cur_seconds = int(curtime % 60) + def load_and_play_song(self, index): - song_min = int(self.song_length // 60) - song_sec = int(self.song_length % 60) + self.music_list_player.pause() + self.music_list_player.play_item_at_index(index) + self.playlist_index = index - time_string = (f"{cur_minutes:02d}:{cur_seconds:02d} / {song_min:02d}:{song_sec:02d}") - self.control_bar.playback_label.configure(text=time_string) - self.after(EVENT_INTERVAL,self.update) + # CHANGE INFO + self.change_info() - def load_and_play_song(self, index): - try: - # MUSIC - #self.music.unload() - #self.music.stop() - self.media=vlc.Media(self.playlist[index]) - self.music.set_media(self.media) - #self.music.load(self.playlist[index]) - self.music.play() - #self.STATE = PlayerState.PLAYING - self.playlist_index = index - - # CHANGE INFO - cover_image = self.get_album_cover() - self.cover_art_frame.cover_art_label.configure( - require_redraw=True, image=cover_image, fg_color="#121212" - ) + logging.info("playing %s", self.get_song_title()) - self.control_bar.set_music_title( # truncates longer titles - self.get_song_title(), - self.get_song_artist(), - ) - self.control_bar.update_play_button() + def change_info(self, event=None): - self.bottom_frame.start_progress_bar( - self.get_song_length(self.playlist[index]), - ) - logging.info( - "playing %s", self.get_song_title() - ) - - - except Exception as e: - logging.exception(e) + self.cover_art_frame.cover_art_label.configure( + require_redraw=True, image=self.get_album_cover(), fg_color="#121212" + ) + self.control_bar.set_music_title( # truncates longer titles + self.get_song_title(), + self.get_song_artist(), + ) + self.control_bar.update_play_button() def play_next_song(self, _event=None): - print(_event) - if not self.playlist: - return - print("hi") - - # PLAY FROM BEGINING - if self.playlist_index >= len(self.playlist) - 1: - self.playlist_index = 0 - else: - self.playlist_index += 1 - self.load_and_play_song(self.playlist_index) + self.music_list_player.next() + self.change_info() + # UPDATE SELECTION + self.playlist_frame.song_list.selection_clear(0, tk.END) + self.playlist_frame.song_list.select_set(self.playlist_index) + def play_previous(self, event=None): + self.music_list_player.previous() + self.change_info() # UPDATE SELECTION self.playlist_frame.song_list.selection_clear(0, tk.END) self.playlist_frame.song_list.select_set(self.playlist_index) - logging.info("playing next song") - def get_song_length(self, file_path): - '''audio = File(file_path) - if audio is not None and audio.info is not None: - return audio.info.length - return 0''' + def get_song_length(self) -> int: + logging.debug("got song length") return self.music.get_length() - def get_song_title(self): + def get_song_title(self) -> str: + media = self.music_list_player.get_media_player().get_media() + logging.debug("got song title") try: - if self.media.is_parsed(): - return self.media.get_meta(0) + if media.is_parsed(): + return media.get_meta(0) else: - self.media.parse() - return self.media.get_meta(0) + media.parse() + return media.get_meta(0) except Exception as e: logging.exception(e) return "" - - - def get_album_cover(self): - """if not file_path.endswith(".mp3"): - return - try: - audio_file = id3.ID3(file_path) - cover_data = None - - # APIC TAG FOR IMAGE DATA - for tag in audio_file.getall("APIC"): - if tag.mime in ("image/jpeg", "image/png"): - cover_data = tag.data - break - if cover_data: - # TEMP STORE COVER - with tempfile.NamedTemporaryFile( - delete=False, suffix=".jpg" - ) as temp_file: - temp_file.write(cover_data) - temp_path = temp_file.name - - return ctk.CTkImage( - self.round_corners(Image.open(temp_path), 20), - size=(250, 250), - ) - return - except Exception as e: - logging.error(e) - return""" - # gives the direct uri to cover jpg + def get_album_cover(self) -> ctk.CTkImage | None: try: - self.media.parse() - print(self.media.get_meta(15)) + media = self.music_list_player.get_media_player().get_media() + media.parse() + logging.debug("got album cover") return ctk.CTkImage( self.round_corners( - Image.open(Path.from_uri(self.media.get_meta(15))), - 20), - size=(250,250) + Image.open( + Path.from_uri(media.get_meta(15)) + ), # gives the direct uri to cover jpg + 20, + ), + size=(250, 250), ) except Exception as e: logging.exception(e) return return - - # ROUNDS ALBUM COVER - def round_corners(self, image, radius): + def get_song_artist(self) -> str: + media = self.music_list_player.get_media_player().get_media() + logging.debug("got song artist") + try: + if media.is_parsed(): + return media.get_meta(1) + else: + media.parse() + return media.get_meta(1) + except Exception as e: + logging.exception(e) + return "" + + def get_song_position(self) -> float: + return self.music.get_position() + + def round_corners(self, image, radius) -> Image.Image: + """Rounds Album Cover""" rounded_mask = Image.new("L", image.size, 0) draw = ImageDraw.Draw(rounded_mask) draw.rounded_rectangle((0, 0) + image.size, radius, fill=255) rounded_image = Image.new("RGBA", image.size) rounded_image.paste(image, (0, 0), mask=rounded_mask) + logging.debug("rounded album cover") return rounded_image - def get_song_artist(self): - try: - if self.media.is_parsed(): - return self.media.get_meta(1) - else: - self.media.parse() - return self.media.get_meta(1) - except exception as e: - logging.exception(e) - return "" - - def get_song_position(self): - return self.music.get_position() - def initialize_vlc(self): - #pygame.init() - #pygame.mixer.init() - self.vlc_instance=vlc.Instance() - self.music=vlc.MediaPlayer() - - #self.music = pygame.mixer.music - - # CREATE USEREVENT WHEN MUSIC ENDS - #pygame.mixer.music.set_endevent(pygame.USEREVENT) - - # AUTOPLAY NEXT SONG AFTER SONG ENDS - '''def check_for_events(self): - #pygame.display.init() - #for event in pygame.event.get(): - # if event.type == pygame.USEREVENT: - # self.play_next_song() - print(self.music.get_state()) - #if self.music.get_state() == vlc.State.Ended: - # self.play_next_song()''' + """initializes and creates + :param `self.music_list_player`: vlc.MediaListPlayer + :param `self.music`: vlc.MediaPlayer + :param `self.vlc_instance`: vlc.Instance + :returns: some vlc attributes + """ + self.music_list_player: vlc.MediaListPlayer = vlc.MediaListPlayer() + self.music: vlc.MediaPlayer = self.music_list_player.get_media_player() + self.vlc_instance: vlc.Instance = self.music_list_player.get_instance() + logging.debug("initialized vlc") def setup_icons(self): self.play_icon = ctk.CTkImage(Image.open("yami/data/play_arrow.png")) @@ -312,18 +194,41 @@ def setup_icons(self): self.next_icon = ctk.CTkImage(Image.open("yami/data/skip_next.png")) self.folder_icon = ctk.CTkImage(Image.open("yami/data/folder.png")) self.music_icon = ctk.CTkImage(Image.open("yami/data/music.png")) - logging.info("icons setup") + logging.debug("icons setup") + + def setup_frames(self): + self.topbar = TopBar(self) + self.control_bar = ControlBar(self) + self.playlist_frame = PlaylistFrame(self) + self.bottom_frame = BottomFrame(self) + self.cover_art_frame = CoverArtFrame(self) + + def setup_keybindings(self): + """ + :param ``: play next + :param ``: play previous + :param ``: play or pause + """ - def setup_bindings(self): self.bind("", self.play_next_song) - self.bind("", self.control_bar.play_previous) + self.bind("", self.play_previous) self.bind("", self.control_bar.play_pause) self.bind("", self.control_bar.play_pause) + self.bind("", self.topbar.choose_folder) + logging.debug("setup keybinds") + + def setup_widget_packing(self): + self.topbar.pack(side=tk.TOP, fill=tk.X) + self.bottom_frame.pack(side=tk.BOTTOM, fill=tk.X) + self.control_bar.pack(side=tk.BOTTOM, fill=tk.X) + self.playlist_frame.pack(side=tk.RIGHT) + self.cover_art_frame.pack(side=tk.LEFT, padx=10) + logging.debug("widgets packed") - '''def update_loop(self): + def update_loop(self): self.loop.call_soon(self.loop.stop) self.loop.run_forever() - self.after(1000, self.update_loop)''' + self.after(1000, self.update_loop) if __name__ == "__main__": diff --git a/yami/playlist.py b/yami/playlist.py index c443057..f75cad0 100644 --- a/yami/playlist.py +++ b/yami/playlist.py @@ -2,6 +2,7 @@ import tkinter as tk import customtkinter as ctk +import logging class PlaylistFrame(ctk.CTkFrame): @@ -34,11 +35,13 @@ def __init__(self, parent): self.song_list.config(yscrollcommand=self.scrollbar.set) self.song_list.bind("", self.play) self.song_list.bind("", self.play) + logging.debug("initialized playlist frame") # SELECTION CALLBACK def play(self, event): try: index = event.widget.curselection()[0] + logging.debug("selected index %s to play", index) self.parent.load_and_play_song(index) - except IndexError: - pass + except Exception as e: + logging.exception(e) diff --git a/yami/topbar.py b/yami/topbar.py index f5e50f2..086aff8 100644 --- a/yami/topbar.py +++ b/yami/topbar.py @@ -12,6 +12,7 @@ import spotdl.utils.formatter import spotdl.utils.search import spotdl +import vlc from .util import SUPPORTED_FORMATS @@ -51,16 +52,15 @@ def __init__(self, parent): # WIDGET PLACEMENT self.open_folder.grid(row=0, column=1, sticky="w", pady=5, padx=10) - self.music_downloader.grid( - row=0, column=2, sticky="w", pady=5, padx=10 - ) + self.music_downloader.grid(row=0, column=2, sticky="w", pady=5, padx=10) self.yami.grid(row=0, column=3, sticky="w", pady=5, padx=10) - - # BINDINGS - self.parent.bind("", self.choose_folder) + logging.debug("initialized topbar") # FOR ADDING SONGS TO PLAYLIST + """TODO MAKE IT SMALLER AND SIMPLER""" + def choose_folder(self, _event=None): + self.parent.current_folder = filedialog.askdirectory( title="Select Music Folder" ) @@ -69,19 +69,23 @@ def choose_folder(self, _event=None): # CLEAR PLAYLIST AND LISTBOX self.parent.playlist_frame.song_list.delete(0, tk.END) - self.parent.playlist.clear() + self.parent.media_list: vlc.MediaList = ( + self.parent.vlc_instance.media_list_new() + ) + + self.parent.music_list_player.set_media_list(self.parent.media_list) # FILTER MUSIC FILES for root, _, files in os.walk(self.parent.current_folder): - music_files = [ - file for file in files if file.endswith(SUPPORTED_FORMATS) - ] + music_files = [file for file in files if file.endswith(SUPPORTED_FORMATS)] for file in music_files: file_path = os.path.join(root, file) - self.parent.playlist.append(file_path) + media = self.parent.vlc_instance.media_new(file_path) + artistname, title = self.get_name_and_title_of_media(media) + self.parent.media_list.add_media(media) self.parent.playlist_frame.song_list.insert( - "end", f"• {Path(file).stem}" + "end", f"• {title} - {artistname}" ) os.chdir(self.parent.current_folder) @@ -94,6 +98,20 @@ def prompt_download(self): if song_url: self.parent.loop.create_task(self.download_song(song_url)) + def get_name_and_title_of_media(self, media): + """gets song artist name and title of song""" + + logging.debug("got song artist name + title of song for playlist") + try: + if media.is_parsed(): + return media.get_meta(1), media.get_meta(0) + else: + media.parse() + return media.get_meta(1), media.get_meta(0) + except Exception as e: + logging.exception(e) + return "" + async def download_song(self, song_url): try: logging.info("searching %s", song_url) diff --git a/yami/util.py b/yami/util.py index 3a6f7ba..0e348cd 100644 --- a/yami/util.py +++ b/yami/util.py @@ -11,3 +11,15 @@ class PlayerState(Enum): PLAYING = 1 PAUSED = 2 STOPPED = 3 + + +def make_time_string(song_position, song_length): + curtime = song_position * song_length + + cur_minutes = int(curtime // 60) + cur_seconds = int(curtime % 60) + + song_min = int(song_length // 60) + song_sec = int(song_length % 60) + + return f"{cur_minutes:02d}:{cur_seconds:02d} / {song_min:02d}:{song_sec:02d}"