From 65db890b1f5a4bcd4b3bcb76593ec2b7805f0f6f Mon Sep 17 00:00:00 2001 From: sigma67 Date: Thu, 20 Apr 2023 21:52:18 +0200 Subject: [PATCH 1/2] implement cache --- .gitignore | 1 + spotify_to_ytmusic/cache.py | 38 +++++++++++++++++++++++++++++++ spotify_to_ytmusic/controllers.py | 2 +- spotify_to_ytmusic/settings.py | 4 +--- spotify_to_ytmusic/ytmusic.py | 36 +++++++++++++++++------------ tests/test_cli.py | 15 ++++++++---- tests/test_spotipy.py | 5 ++-- 7 files changed, 76 insertions(+), 25 deletions(-) create mode 100644 spotify_to_ytmusic/cache.py diff --git a/.gitignore b/.gitignore index b09f8ea..05e9015 100644 --- a/.gitignore +++ b/.gitignore @@ -109,5 +109,6 @@ venv.bak/ .idea noresults.txt *.ini +*.bin *.txt *.json \ No newline at end of file diff --git a/spotify_to_ytmusic/cache.py b/spotify_to_ytmusic/cache.py new file mode 100644 index 0000000..f5aba69 --- /dev/null +++ b/spotify_to_ytmusic/cache.py @@ -0,0 +1,38 @@ +import bz2 +import json +from pathlib import Path +from typing import Dict + + +class Cache: + _cache: Dict[str, str] + filepath: Path = Path(__file__).parent.joinpath("cache.bin") + + def __init__(self): + self._cache = {} + if self.filepath.is_file(): + self._load(self.filepath) + + def __contains__(self, item): + return item in self._cache + + def __getitem__(self, key): + return self._cache[key] + + def __setitem__(self, key, value): + self._cache[key] = value + + def save(self): + self._save(self.filepath) + + def _load(self, path: Path): + with open(path, "rb") as file: + data = bz2.decompress(file.read()) + + self._cache.update(json.loads(data)) + + def _save(self, path: Path): + byte_data = json.dumps(self._cache) + compressed_byte_data = bz2.compress(byte_data.encode("utf8")) + with open(path, "wb") as file: + file.write(compressed_byte_data) diff --git a/spotify_to_ytmusic/controllers.py b/spotify_to_ytmusic/controllers.py index 8f84880..27a6967 100644 --- a/spotify_to_ytmusic/controllers.py +++ b/spotify_to_ytmusic/controllers.py @@ -39,7 +39,7 @@ def all(args): ) print(playlist_id) except Exception as ex: - print(f"Could not transfer playlist {p['name']}. {str(ex)}") + raise Exception(f"Could not transfer playlist {p['name']}") from ex def create(args): diff --git a/spotify_to_ytmusic/settings.py b/spotify_to_ytmusic/settings.py index 58f984e..a6433d1 100644 --- a/spotify_to_ytmusic/settings.py +++ b/spotify_to_ytmusic/settings.py @@ -12,9 +12,7 @@ def __init__(self, filepath: Optional[Path] = None): if filepath: self.filepath = filepath if not self.filepath.is_file(): - raise FileNotFoundError( - f"No settings.ini not found! Please run \n\n spotify_to_ytmusic setup" - ) + raise FileNotFoundError(f"No settings.ini found! Please run spotify_to_ytmusic setup") self.config.read(self.filepath) def __getitem__(self, key): diff --git a/spotify_to_ytmusic/ytmusic.py b/spotify_to_ytmusic/ytmusic.py index ccd6d5c..a6a9c94 100644 --- a/spotify_to_ytmusic/ytmusic.py +++ b/spotify_to_ytmusic/ytmusic.py @@ -4,6 +4,7 @@ from ytmusicapi import YTMusic +from spotify_to_ytmusic.cache import Cache from spotify_to_ytmusic.match import get_best_fit_song_id from spotify_to_ytmusic.settings import Settings @@ -24,23 +25,28 @@ def search_songs(self, tracks): videoIds = [] songs = list(tracks) notFound = list() - print("Searching YouTube...") - for i, song in enumerate(songs): - name = re.sub(r" \(feat.*\..+\)", "", song["name"]) - query = song["artist"] + " " + name - query = query.replace(" &", "") - result = self.api.search(query) - if len(result) == 0: - notFound.append(query) - else: - targetSong = get_best_fit_song_id(result, song) - if targetSong is None: - notFound.append(query) + cache = Cache() + try: + for i, song in enumerate(songs): + if not i % 10: + print(f"YouTube tracks: {i}/{len(songs)}") + name = re.sub(r" \(feat.*\..+\)", "", song["name"]) + query = song["artist"] + " " + name + query = query.replace(" &", "") + if query in cache: + targetSong = cache[query] else: - videoIds.append(targetSong) + result = self.api.search(query) + if not len(result) or not (targetSong := get_best_fit_song_id(result, song)): + notFound.append(query) + continue + + cache[query] = targetSong + + videoIds.append(targetSong) - if i > 0 and i % 10 == 0: - print(f"YouTube tracks: {i}/{len(songs)}") + finally: + cache.save() with open(path + "noresults_youtube.txt", "w", encoding="utf-8") as f: f.write("\n".join(notFound)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5b1ad9d..2756f8e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import json import shutil import time import unittest @@ -27,16 +28,17 @@ def test_create(self): main() with mock.patch( - "sys.argv", ["", "create", TEST_PLAYLIST, "-n", "test", "-i", "test-playlist", "-d"] + "sys.argv", + ["", "create", TEST_PLAYLIST, "-n", "spotify_to_ytmusic", "-i", "test-playlist", "-d"], ): main() time.sleep(2) - with mock.patch("sys.argv", ["", "update", TEST_PLAYLIST, "test"]): + with mock.patch("sys.argv", ["", "update", TEST_PLAYLIST, "spotify_to_ytmusic"]): main() time.sleep(2) - with mock.patch("sys.argv", ["", "remove", "test"]), mock.patch( + with mock.patch("sys.argv", ["", "remove", "spotify\_to\_ytmusic"]), mock.patch( "sys.stdout", new=StringIO() ) as fakeOutput, mock.patch("builtins.input", side_effect="y"): main() @@ -48,7 +50,12 @@ def test_setup(self): tmp_path = Path(__file__).parent.joinpath("settings.tmp") example_path = Settings.filepath.parent.joinpath("settings.ini.example") shutil.copy(example_path, tmp_path) - with mock.patch("sys.argv", ["", "setup"]), mock.patch("builtins.input", return_value="3"): + with mock.patch("sys.argv", ["", "setup"]), mock.patch( + "builtins.input", return_value="3" + ), mock.patch( + "ytmusicapi.auth.oauth.YTMusicOAuth.get_token_from_code", + return_value=json.loads(Settings()["youtube"]["headers"]), + ): main() assert tmp_path.is_file() tmp_path.unlink() diff --git a/tests/test_spotipy.py b/tests/test_spotipy.py index 81c0190..e5972e0 100644 --- a/tests/test_spotipy.py +++ b/tests/test_spotipy.py @@ -4,12 +4,13 @@ class TestSpotify(unittest.TestCase): - def setUp(self) -> None: self.spotify = Spotify() def test_getSpotifyPlaylist(self): - data = self.spotify.getSpotifyPlaylist("https://open.spotify.com/playlist/03ICMYsVsC4I2SZnERcQJb") + data = self.spotify.getSpotifyPlaylist( + "https://open.spotify.com/playlist/03ICMYsVsC4I2SZnERcQJb" + ) self.assertEqual(len(data), 3) self.assertGreater(len(data["tracks"]), 190) From d17c79304a05aa6af3d861d651031190aa0ffad7 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Mon, 29 May 2023 14:16:54 +0200 Subject: [PATCH 2/2] fix tests --- tests/test_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5316c71..5375134 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -57,7 +57,7 @@ def test_setup(self): example_path = Settings.filepath.parent.joinpath("settings.ini.example") shutil.copy(example_path, tmp_path) with mock.patch("sys.argv", ["", "setup"]), mock.patch( - "builtins.input", return_value="3" + "builtins.input", side_effect=["3", "a", "b", "yes", ""] ), mock.patch( "ytmusicapi.auth.oauth.YTMusicOAuth.get_token_from_code", return_value=json.loads(Settings()["youtube"]["headers"]), @@ -65,8 +65,8 @@ def test_setup(self): main() assert tmp_path.is_file() settings = Settings() - assert settings["spotify"]["client_id"] == "3" - assert settings["spotify"]["client_secret"] == "3" + assert settings["spotify"]["client_id"] == "a" + assert settings["spotify"]["client_secret"] == "b" tmp_path.unlink() with mock.patch("sys.argv", ["", "setup", "--file", example_path.as_posix()]), mock.patch(