Skip to content

Commit

Permalink
Merge branch 'master' into 39-cache-conversions
Browse files Browse the repository at this point in the history
# Conflicts:
#	tests/test_cli.py
  • Loading branch information
sigma67 committed May 29, 2023
2 parents 65db890 + d17c793 commit c652d4c
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 28 deletions.
15 changes: 15 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Setup
For backwards compatibility you can also create your own file and pass it using ``--file settings.ini``.

If you want to transfer private playlists from Spotify (i.e. liked songs), choose "yes" for oAuth authentication, otherwise choose "no".
For oAuth authentication you should set ``http://localhost`` as redirect URI for your app in Spotify's developer dashboard.

Usage
------

Expand All @@ -74,6 +77,18 @@ For migration purposes, it is possible to transfer all public playlists of a use
spotify_to_ytmusic all <spotifyuserid>
Transfer liked tracks of the Spotify user
-----------------------------------------

**You must you oAuth authentication for transferring liked songs.**

.. code-block::
spotify_to_ytmusic liked
This command will open browser where you should give access to your account (if you haven't done that before).
After authorization you will be redirected to localhost, copy link you were redirected to (looks like localhost/?code=...) and paste to command line.

Command line options
---------------------

Expand Down
35 changes: 26 additions & 9 deletions spotify_to_ytmusic/controllers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import time
from datetime import datetime

import spotipy

from spotify_to_ytmusic.setup import setup as setup_func
from spotify_to_ytmusic.spotify import Spotify
from spotify_to_ytmusic.ytmusic import YTMusicTransfer
Expand All @@ -16,6 +18,13 @@ def _get_spotify_playlist(spotify, playlist):
return


def _print_success(name, playlistId):
print(
f"Success: created playlist '{name}' at\n"
f"https://music.youtube.com/playlist?list={playlistId}"
)


def _init():
return Spotify(), YTMusicTransfer()

Expand All @@ -37,29 +46,37 @@ def all(args):
"PUBLIC" if p["public"] else "PRIVATE",
videoIds,
)
print(playlist_id)
_print_success(p["name"], playlist_id)
except Exception as ex:
raise Exception(f"Could not transfer playlist {p['name']}") from ex


def create(args):
spotify, ytmusic = _init()
def _create_ytmusic(args, playlist, ytmusic):
date = ""
if args.date:
date = " " + datetime.today().strftime("%m/%d/%Y")

playlist = _get_spotify_playlist(spotify, args.playlist)
name = args.name + date if args.name else playlist["name"] + date
info = playlist["description"] if (args.info is None) else args.info
videoIds = ytmusic.search_songs(playlist["tracks"])

playlistId = ytmusic.create_playlist(
name, info, "PUBLIC" if args.public else "PRIVATE", videoIds
)
_print_success(name, playlistId)

print(
f"Success: created playlist {name}\n"
f"https://music.youtube.com/playlist?list={playlistId}"
)

def create(args):
spotify, ytmusic = _init()
playlist = _get_spotify_playlist(spotify, args.playlist)
_create_ytmusic(args, playlist, ytmusic)


def liked(args):
spotify, ytmusic = _init()
if not isinstance(spotify.api.auth_manager, spotipy.SpotifyOAuth):
raise Exception("OAuth not configured, please run setup and set OAuth to 'yes'")
playlist = spotify.getLikedPlaylist()
_create_ytmusic(args, playlist, ytmusic)


def update(args):
Expand Down
27 changes: 17 additions & 10 deletions spotify_to_ytmusic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,44 @@ def get_args(args=None):
spotify_playlist = argparse.ArgumentParser(add_help=False)
spotify_playlist.add_argument("playlist", type=str, help="Provide a playlist Spotify link.")

create_parser = subparsers.add_parser(
"create",
help="Create a new playlist on YouTube Music.",
parents=[spotify_playlist],
)
create_parser.set_defaults(func=controllers.create)
create_parser.add_argument(
spotify_playlist_create = argparse.ArgumentParser(add_help=False)
spotify_playlist_create.add_argument(
"-d",
"--date",
action="store_true",
help="Append the current date to the playlist name",
)
create_parser.add_argument(
spotify_playlist_create.add_argument(
"-i",
"--info",
type=str,
help="Provide description information for the YouTube Music Playlist. Default: Spotify playlist description",
)
create_parser.add_argument(
spotify_playlist_create.add_argument(
"-n",
"--name",
type=str,
help="Provide a name for the YouTube Music playlist. Default: Spotify playlist name",
)
create_parser.add_argument(
spotify_playlist_create.add_argument(
"-p",
"--public",
action="store_true",
help="Make created playlist public. Default: private",
)

create_parser = subparsers.add_parser(
"create",
help="Create a new playlist on YouTube Music.",
parents=[spotify_playlist, spotify_playlist_create],
)
create_parser.set_defaults(func=controllers.create)

liked_parser = subparsers.add_parser(
"liked", help="Transfer all liked songs of the user.", parents=[spotify_playlist_create]
)
liked_parser.set_defaults(func=controllers.liked)

update_parser = subparsers.add_parser(
"update",
help="Delete all entries in the provided Google Play Music playlist and update the playlist with entries from the Spotify playlist.",
Expand Down
1 change: 1 addition & 0 deletions spotify_to_ytmusic/settings.ini.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ user_id =
[spotify]
client_id = id_from_developer_console
client_secret = secret_from_developer_console
use_oauth = no
7 changes: 4 additions & 3 deletions spotify_to_ytmusic/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ def setup_youtube():
def setup_spotify():
settings = Settings()
credentials = {
"client_credentials": input(
"Paste your client credentials from the Spotify developer dashboard:"
"client_id": input("Paste your client id from the Spotify developer dashboard:"),
"client_secret": input("Paste your client secret from the Spotify developer dashboard:"),
"use_oauth": input(
"Use OAuth method for authorization to transfer private playlists (yes/no):"
),
"client_secrets": input("Paste your client secret from the Spotify developer dashboard:"),
}
settings["spotify"].update(credentials)
settings.save()
Expand Down
33 changes: 28 additions & 5 deletions spotify_to_ytmusic/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from urllib.parse import urlparse

import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
from spotipy.oauth2 import SpotifyClientCredentials, SpotifyOAuth

from spotify_to_ytmusic.settings import Settings

Expand All @@ -22,10 +22,20 @@ def __init__(self):
string.hexdigits
), f"Spotify client_secret not set or invalid: {client_secret}"

client_credentials_manager = SpotifyClientCredentials(
client_id=client_id, client_secret=client_secret
)
self.api = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
use_oauth = conf.getboolean("use_oauth")
if use_oauth:
auth = SpotifyOAuth(
client_id=client_id,
client_secret=client_secret,
redirect_uri="http://localhost",
scope="user-library-read",
)
self.api = spotipy.Spotify(auth_manager=auth)
else:
client_credentials_manager = SpotifyClientCredentials(
client_id=client_id, client_secret=client_secret
)
self.api = spotipy.Spotify(client_credentials_manager=client_credentials_manager)

def getSpotifyPlaylist(self, url):
playlistId = get_id_from_url(url)
Expand Down Expand Up @@ -64,6 +74,19 @@ def getUserPlaylists(self, user):

return [p for p in pl if p["owner"]["id"] == user and p["tracks"]["total"] > 0]

def getLikedPlaylist(self):
response = self.api.current_user_saved_tracks(limit=50)
tracks = response["items"]
while response["next"] is not None:
response = self.api.current_user_saved_tracks(limit=50, offset=response["offset"] + 50)
tracks.extend(response["items"])

return {
"tracks": build_results(tracks),
"name": "Liked songs (Spotify)",
"description": "Your liked tracks from spotify",
}


def build_results(tracks, album=None):
results = []
Expand Down
11 changes: 10 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ def test_get_args(self):
args = get_args(["setup"])
self.assertEqual(len(vars(args)), 3)

def test_liked(self):
with mock.patch(
"sys.argv", ["", "liked", "-n", "spotify_to_ytmusic", "-d", "-i", "test liked"]
):
main()

def test_create(self):
with mock.patch("sys.argv", ["", "all", "sigmatics"]):
main()
Expand Down Expand Up @@ -51,13 +57,16 @@ 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"]),
):
main()
assert tmp_path.is_file()
settings = 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(
Expand Down

0 comments on commit c652d4c

Please sign in to comment.