From 7bfbbc19bd764573f8ed6372465c10265a48a9b5 Mon Sep 17 00:00:00 2001 From: Yoween Date: Mon, 5 Feb 2024 13:27:01 +0100 Subject: [PATCH] bug fixes and code formatting --- .gitignore | 1 + spotify2ytmusic/backend.py | 80 ++++++++++++++++++++++----------- spotify2ytmusic/gui.py | 90 ++++++++++++-------------------------- 3 files changed, 84 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 24ba072..eb2f5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/ spotify2ytmusic/playlists_backup.json spotify2ytmusic/settings.json spotify2ytmusic/test.py +.idea/ \ No newline at end of file diff --git a/spotify2ytmusic/backend.py b/spotify2ytmusic/backend.py index 229c932..823577a 100644 --- a/spotify2ytmusic/backend.py +++ b/spotify2ytmusic/backend.py @@ -73,9 +73,12 @@ def load_playlists_json(filename: str = "playlists.json", encoding: str = "utf-8 return json.load(open(filename, "r", encoding=encoding)) -def create_playlist(pl_name: str): - """ - Create a YTMusic playlist +def create_playlist(pl_name: str) -> None: + """ Create a YTMusic playlist + + + Args: + `pl_name` (str): The name of the playlist to create. It should be different to "". """ yt = get_ytmusic() @@ -90,7 +93,7 @@ def iter_spotify_liked_albums( """Songs from liked albums on Spotify.""" spotify_pls = load_playlists_json(spotify_playlist_file, spotify_encoding) - if not "albums" in spotify_pls: + if "albums" not in spotify_pls: return None for album in [x["album"] for x in spotify_pls["albums"]]: @@ -103,7 +106,16 @@ def iter_spotify_playlist( spotify_playlist_file: str = "playlists.json", spotify_encoding: str = "utf-8", ) -> Iterator[SongInfo]: - """Songs from a specific album ("Liked Songs" if None)""" + """Songs from a specific album ("Liked Songs" if None) + + Args: + `src_pl_id` (Optional[str], optional): The ID of the source playlist. Defaults to None. + `spotify_playlist_file` (str, optional): The path to the playlists backup files. Defaults to "playlists.json". + `spotify_encoding` (str, optional): Characters encoding. Defaults to "utf-8". + + Yields: + Iterator[SongInfo]: The song's information + """ spotify_pls = load_playlists_json(spotify_playlist_file, spotify_encoding) for src_pl in spotify_pls["playlists"]: @@ -132,14 +144,22 @@ def iter_spotify_playlist( print( f"ERROR: Spotify track seems to be malformed. Track: {src_track!r}" ) - raise (e) + raise e src_track_name = src_track["track"]["name"] yield SongInfo(src_track_name, src_track_artist, src_album_name) def get_playlist_id_by_name(yt: YTMusic, title: str) -> Optional[str]: - """Look up a YTMusic playlist ID by name. Return None if not found.""" + """Look up a YTMusic playlist ID by name. + + Args: + `yt` (YTMusic): _description_ + `title` (str): _description_ + + Returns: + Optional[str]: The playlist ID or None if not found. + """ for pl in yt.get_library_playlists(limit=5000): if pl["title"] == title: return pl["playlistId"] @@ -161,14 +181,14 @@ def lookup_song( The idea is that finding the album and artist and then looking for the exact track match will be more likely to be accurate than searching for the song and artist and relying on the YTMusic yt_search_algorithm to figure things out, especially for short tracks - that might be have many contradictory hits like "Survival by Yes". + that might have many contradictory hits like "Survival by Yes". Args: - yt (YTMusic) - track_name (str): The name of the researched track - artist_name (str): The name of the researched track's artist - album_name (str): The name of the researched track's album - yt_search_algo (int): 0 for exact matching, 1 for extended matching (search past 1st result), 2 for approximate matching (search in videos) + `yt` (YTMusic) + `track_name` (str): The name of the researched track + `artist_name` (str): The name of the researched track's artist + `album_name` (str): The name of the researched track's album + `yt_search_algo` (int): 0 for exact matching, 1 for extended matching (search past 1st result), 2 for approximate matching (search in videos) Raises: ValueError: If no track is found, it returns an error @@ -214,7 +234,7 @@ def lookup_song( for song in songs: # Remove everything in brackets in the song title song_title_without_brackets = re.sub( - r"[\[\(].*?[\]\)]", "", song["title"] + r"[\[(].*?[])]", "", song["title"] ) if ( ( @@ -256,7 +276,7 @@ def lookup_song( print("Found a video") return new_song else: - # Basically we only get here if the song isnt present anywhere on youtube + # Basically we only get here if the song isn't present anywhere on YouTube raise ValueError( f"Did not find {track_name} by {artist_name} from {album_name}" ) @@ -361,22 +381,32 @@ def copy_playlist( @@@ """ yt = get_ytmusic() + pl_name: str = "" + if ytmusic_playlist_id.startswith("+"): pl_name = ytmusic_playlist_id[1:] ytmusic_playlist_id = get_playlist_id_by_name(yt, pl_name) print(f"Looking up playlist '{pl_name}': id={ytmusic_playlist_id}") - if ytmusic_playlist_id is None: - ytmusic_playlist_id = _ytmusic_create_playlist( - yt, title=pl_name, description=pl_name - ) - time.sleep(1) # seems to be needed to avoid missing playlist ID error + + if ytmusic_playlist_id == "": + if pl_name == "": + print("No playlist name or ID provided, creating playlist...") + spotify_pls: dict = load_playlists_json() + for pl in spotify_pls["playlists"]: + if len(pl.keys()) > 3 and pl["id"] == spotify_playlist_id: + pl_name = pl["name"] + + ytmusic_playlist_id = _ytmusic_create_playlist( + yt, title=pl_name, description=pl_name + ) + time.sleep(1) # seems to be needed to avoid missing playlist ID error - # create_playlist returns a dict if there was an error - if isinstance(ytmusic_playlist_id, dict): - print(f"ERROR: Failed to create playlist: {ytmusic_playlist_id}") - sys.exit(1) - print(f"NOTE: Created playlist '{pl_name}' with ID: {ytmusic_playlist_id}") + # create_playlist returns a dict if there was an error + if isinstance(ytmusic_playlist_id, dict): + print(f"ERROR: Failed to create playlist: {ytmusic_playlist_id}") + sys.exit(1) + print(f"NOTE: Created playlist '{pl_name}' with ID: {ytmusic_playlist_id}") copier( iter_spotify_playlist( diff --git a/spotify2ytmusic/gui.py b/spotify2ytmusic/gui.py index c4fcd1b..20d86f0 100644 --- a/spotify2ytmusic/gui.py +++ b/spotify2ytmusic/gui.py @@ -11,7 +11,7 @@ import cli import backend import spotify_backup -from reverse_playlist import reverse_playlist +from typing import Callable def create_label(parent, text, **kwargs) -> tk.Label: @@ -25,7 +25,7 @@ def create_label(parent, text, **kwargs) -> tk.Label: ) -def create_button(parent, text, **kwargs): +def create_button(parent, text, **kwargs) -> tk.Button: return tk.Button( parent, text=text, @@ -116,7 +116,7 @@ def __init__(self) -> None: create_label(self.tab1, text="First, you need to backup your spotify playlists").pack(anchor=tk.CENTER, expand=True) - create_button(self.tab1, text="Backup", command=lambda: self.call_func(spotify_backup.main, self.tab3)).pack( + create_button(self.tab1, text="Backup", command=lambda: self.call_func(func=spotify_backup.main, args=(), next_tab=self.tab3)).pack( anchor=tk.CENTER, expand=True) # # tab2 @@ -133,8 +133,9 @@ def __init__(self) -> None: # tab3 create_label(self.tab3, text="Now, you can load your liked songs.").pack(anchor=tk.CENTER, expand=True) - create_button(self.tab3, text="Load", command=self.call_load_liked_songs).pack( - anchor=tk.CENTER, expand=True) + create_button(self.tab3, text="Load", command=lambda x: self.call_func( + func=backend.copier, args=(backend.iter_spotify_playlist(), None, False, 0.1, self.var_algo.get()), next_tab=self.tab4) + ).pack(anchor=tk.CENTER, expand=True) # tab4 create_label( @@ -143,7 +144,7 @@ def __init__(self) -> None: create_button( self.tab4, text="List", - command=lambda: self.call_func(cli.list_playlists, self.tab5), + command=lambda: self.call_func(func=cli.list_playlists, args=(), next_tab=self.tab5), ).pack(anchor=tk.CENTER, expand=True) # tab5 @@ -155,7 +156,7 @@ def __init__(self) -> None: create_button( self.tab5, text="Copy", - command=lambda: self.call_func(backend.copy_all_playlists, self.tab6), + command=lambda: self.call_func(func=backend.copy_all_playlists, args=(0.1, False, "utf-8", self.var_algo.get()), next_tab=self.tab6), ).pack(anchor=tk.CENTER, expand=True) # tab6 @@ -173,9 +174,9 @@ def __init__(self) -> None: ) self.yt_playlist_id = tk.Entry(self.tab6) self.yt_playlist_id.pack(anchor=tk.CENTER, expand=True) - create_button(self.tab6, text="Copy", command=self.call_copy_playlist).pack( - anchor=tk.CENTER, expand=True - ) + create_button(self.tab6, text="Copy", command=lambda: self.call_func( + func=backend.copy_playlist, args=(self.spotify_playlist_id.get(), self.yt_playlist_id.get()), next_tab=self.tab6) + ).pack(anchor=tk.CENTER, expand=True) # tab7 self.var_scroll = tk.BooleanVar() @@ -222,64 +223,31 @@ def redirector(self, input_str="") -> None: self.logs.config(state=tk.DISABLED) if self.var_scroll.get(): self.logs.see(tk.END) - - def call_load_liked_songs(self): - th = threading.Thread(target=backend.copier, args=(backend.iter_spotify_playlist(), None, False, 0.1, self.var_algo.get())) - th.start() - while th.is_alive(): - self.root.update() - self.tabControl.select(self.tab4) - print() - - def call_func(self, func, next_tab): - th = threading.Thread(target=func) + def call_func(self, func: Callable, args: tuple, next_tab: ttk.Frame) -> None: + th = threading.Thread(target=func, args=args) th.start() while th.is_alive(): self.root.update() self.tabControl.select(next_tab) print() - def call_copy_playlist(self): - spotify_playlist_id = self.spotify_playlist_id.get() - yt_playlist_id = self.yt_playlist_id.get() - - print() - - if spotify_playlist_id == "": - print("Please enter the Spotify playlist ID") - return - if yt_playlist_id == "": - print( - "No Youtube playlist ID, creating one and naming it by the source playlist ID." - ) - backend.create_playlist(spotify_playlist_id) - th = threading.Thread( - target=backend.copy_playlist, args=(spotify_playlist_id, yt_playlist_id) - ) - th.start() - while th.is_alive(): - self.root.update() - - self.tabControl.select(self.tab6) - print() + # def call_reverse(self): + # result = [0] # Shared data structure - def call_reverse(self): - result = [0] # Shared data structure + # def target(): + # result[0] = reverse_playlist( + # replace=True + # ) # Call the function with specific arguments - def target(): - result[0] = reverse_playlist( - replace=True - ) # Call the function with specific arguments + # th = threading.Thread(target=target) + # th.start() + # while th.is_alive(): + # self.root.update() - th = threading.Thread(target=target) - th.start() - while th.is_alive(): - self.root.update() - - if result[0] == 0: # Access the return value - self.tabControl.select(self.tab3) - print() + # if result[0] == 0: # Access the return value + # self.tabControl.select(self.tab3) + # print() def yt_login(self, auto=False) -> None: def run_in_thread(): @@ -324,7 +292,7 @@ def load_write_settings(self, action: int) -> None: exist = True if action == 0: - with open("settings.json", "a+") as f: + with open("settings.json", "a+"): pass with open("settings.json", "r+") as f: value = f.read() @@ -337,9 +305,7 @@ def load_write_settings(self, action: int) -> None: self.var_algo.set(settings["algo_number"]) else: with open("settings.json", "w+") as f: - settings = {} - settings["auto_scroll"] = self.var_scroll.get() - settings["algo_number"] = self.var_algo.get() + settings = {"auto_scroll": self.var_scroll.get(), "algo_number": self.var_algo.get()} json.dump(settings, f) self.algo_label.config(text=f"Algorithm: {texts[self.var_algo.get()]}")