diff --git a/pyproject.toml b/pyproject.toml index f0c7bb1..07e659d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ classifiers = [ ] dependencies = [ "ytmusicapi>=1.0", - "spotipy>=2.23.0" + "spotipy>=2.23.0", + "platformdirs>=2.0" ] dynamic = ["version", "readme"] diff --git a/spotify_to_ytmusic/settings.py b/spotify_to_ytmusic/settings.py index 58f984e..a1cbf73 100644 --- a/spotify_to_ytmusic/settings.py +++ b/spotify_to_ytmusic/settings.py @@ -1,20 +1,35 @@ import configparser +import shutil +import warnings from pathlib import Path from typing import Optional +import platformdirs + +CACHE_DIR = Path( + platformdirs.user_cache_dir(appname="spotify_to_ytmusic", appauthor=False, ensure_exists=True) +) +DEFAULT_PATH = CACHE_DIR.joinpath("settings.ini") +EXAMPLE_PATH = Path(__file__).parent.joinpath("settings.ini.example") + class Settings: config: configparser.ConfigParser - filepath: Path = Path(__file__).parent.joinpath("settings.ini") + filepath: Path = DEFAULT_PATH def __init__(self, filepath: Optional[Path] = None): self.config = configparser.ConfigParser(interpolation=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" - ) + try: + # Migration path for pre 0.3.0 + shutil.copy(EXAMPLE_PATH.with_suffix(""), DEFAULT_PATH) + warnings.warn(f"Moved {filepath} to {DEFAULT_PATH}", DeprecationWarning) + except Exception as exc: + raise FileNotFoundError( + f"No settings.ini found! Please run \n\n spotify_to_ytmusic setup" + ) self.config.read(self.filepath) def __getitem__(self, key): diff --git a/spotify_to_ytmusic/setup.py b/spotify_to_ytmusic/setup.py index 7dc315e..1f0896e 100644 --- a/spotify_to_ytmusic/setup.py +++ b/spotify_to_ytmusic/setup.py @@ -6,16 +6,16 @@ import ytmusicapi -from spotify_to_ytmusic.settings import Settings +from spotify_to_ytmusic.settings import DEFAULT_PATH, EXAMPLE_PATH, Settings def setup(file: Optional[Path] = None): if file: - setup_file(file) + shutil.copy(file, DEFAULT_PATH) return - if not Settings.filepath.is_file(): - shutil.copy(Settings.filepath.with_suffix(".ini.example"), Settings.filepath) + if not DEFAULT_PATH.is_file(): + shutil.copy(EXAMPLE_PATH, DEFAULT_PATH) choice = input("Choose which API to set up\n" "(1) Spotify\n" "(2) YouTube\n" "(3) both\n") choices = ["1", "2", "3"] if choice not in choices: @@ -47,9 +47,3 @@ def setup_spotify(): } settings["spotify"].update(credentials) settings.save() - - -def setup_file(file: Path): - if not file: - raise FileNotFoundError(f"{file} not found") - shutil.copy(file, Settings.filepath) diff --git a/spotify_to_ytmusic/spotify.py b/spotify_to_ytmusic/spotify.py index b6a52a1..f546e5f 100644 --- a/spotify_to_ytmusic/spotify.py +++ b/spotify_to_ytmusic/spotify.py @@ -3,9 +3,10 @@ from urllib.parse import urlparse import spotipy +from spotipy import CacheFileHandler from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOAuth -from spotify_to_ytmusic.settings import Settings +from spotify_to_ytmusic.settings import CACHE_DIR, Settings class Spotify: @@ -23,17 +24,20 @@ def __init__(self): ), f"Spotify client_secret not set or invalid: {client_secret}" use_oauth = conf.getboolean("use_oauth") + + cache_handler = CacheFileHandler(cache_path=CACHE_DIR.joinpath("spotipy.cache").as_posix()) if use_oauth: auth = SpotifyOAuth( client_id=client_id, client_secret=client_secret, redirect_uri="http://localhost", scope="user-library-read", + cache_handler=cache_handler, ) self.api = spotipy.Spotify(auth_manager=auth) else: client_credentials_manager = SpotifyClientCredentials( - client_id=client_id, client_secret=client_secret + client_id=client_id, client_secret=client_secret, cache_handler=cache_handler ) self.api = spotipy.Spotify(client_credentials_manager=client_credentials_manager) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5375134..a7a8498 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,18 +1,22 @@ import json -import shutil import time import unittest from io import StringIO -from pathlib import Path from unittest import mock +from spotify_to_ytmusic import settings as settings_module +from spotify_to_ytmusic import setup from spotify_to_ytmusic.main import get_args, main -from spotify_to_ytmusic.settings import Settings +from spotify_to_ytmusic.settings import DEFAULT_PATH, EXAMPLE_PATH, Settings TEST_PLAYLIST = "https://open.spotify.com/playlist/4UzyZJfSQ4584FaWGwepfL" class TestCli(unittest.TestCase): + @classmethod + def setUpClass(cls): + Settings() + def test_get_args(self): args = get_args(["all", "user"]) self.assertEqual(len(vars(args)), 3) @@ -53,24 +57,29 @@ def test_create(self): ) # assert number of lines deleted 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", side_effect=["3", "a", "b", "yes", ""] - ), mock.patch( - "ytmusicapi.auth.oauth.YTMusicOAuth.get_token_from_code", - return_value=json.loads(Settings()["youtube"]["headers"]), + tmp_path = DEFAULT_PATH.with_suffix(".tmp") + with ( + mock.patch("sys.argv", ["", "setup"]), + mock.patch("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"]), + ), + mock.patch.object(setup, "DEFAULT_PATH", tmp_path), + mock.patch.object(settings_module, "DEFAULT_PATH", tmp_path), + mock.patch.object(Settings, "filepath", tmp_path), ): main() assert tmp_path.is_file() - settings = Settings() + settings = Settings() # reload settings 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( - "spotify_to_ytmusic.settings.Settings.filepath", tmp_path + with ( + mock.patch("sys.argv", ["", "setup", "--file", EXAMPLE_PATH.as_posix()]), + mock.patch.object(setup, "DEFAULT_PATH", tmp_path), + mock.patch.object(settings_module, "DEFAULT_PATH", tmp_path), ): main() assert tmp_path.is_file()