diff --git a/README.md b/README.md index 2c27529..cc0cf80 100644 --- a/README.md +++ b/README.md @@ -22,25 +22,34 @@ python3 -m pip install git+https://github.com/Fyssion/playlist-sync.git ## Usage -You'll need to create a `config.py` file. Here's an example of that: +You'll need to get your Spotify API credentials. +Visit the [Spotify developer dashboard][spotify-dashboard] to get them. -```py -# Visit https://developer.spotify.com/dashboard to get your credentials -spotify_client_id = 'spotify client id' -spotify_client_secret = 'spotify client secret' - -sync_from_url = 'url or id of spotify playlist to sync tracks from' -sync_to_id = 'id of youtube music playlist to sync tracks to (found in playlist URL)' -``` - -Then to run the program: +To sync from Spotify to YT Music, use the following command: ```sh # Windows -py -m playlist_sync +py -m playlist_sync spotify-to-yt -f "" -t "" # MacOS/Linux -python3 -m playlist_sync +python3 -m playlist_sync spotify-to-yt -f "" -t "" +``` + +Replace `` and `` with the Spotify playlist URL +and YouTube playlist ID respectively. + +To get the YouTube playlist ID, click the share button on the playlist +page in YouTube Music, and copy the ID after `list=`, as shown below: + +```re +https://music.youtube.com/playlist?list=[THIS_PART_OF_THE_URL]&si=NOT_THIS +``` + +If you want to sync from YT Music to Spotify, modify the command above to look +like this: + +```sh +(py -m | python3 -m) playlist_sync yt-to-spotify -f "" -t "" ``` On the first run, it'll ask you to paste your browser credentials for @@ -54,4 +63,15 @@ terminal. After that, it'll start syncing. If you run into any issues, feel free to open a discussion post and I can try to help. +### Config file + +If you don't want to type your Spotify credentials into the CLI on every run, +you can create a `config.py` file. Here's an example of that: + +```py +spotify_client_id = 'spotify client id' +spotify_client_secret = 'spotify client secret' +``` + [ytmusicapi-browser]: https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers +[spotify-dashboard]: https://developer.spotify.com/dashboard diff --git a/playlist_sync/__main__.py b/playlist_sync/__main__.py index 953ab20..e1c3eec 100644 --- a/playlist_sync/__main__.py +++ b/playlist_sync/__main__.py @@ -4,38 +4,7 @@ from __future__ import annotations -import logging -import pathlib +from .cli import main -import spotipy -import ytmusicapi -from rich.logging import RichHandler -from spotipy.oauth2 import SpotifyOAuth -import config -from playlist_sync.playlist import Playlist -from playlist_sync.services.spotify import Spotify -from playlist_sync.services.ytmusic import YTMusic - -log = logging.getLogger('playlist_sync') - -logging.getLogger().setLevel(logging.INFO) -sh = RichHandler() -sh.setFormatter(logging.Formatter("[%(name)s] %(message)s")) -logging.getLogger().addHandler(sh) - -if not pathlib.Path('./browser.json').is_file(): - ytmusicapi.setup(filepath='browser.json') - -auth_manager = SpotifyOAuth( - client_id=config.spotify_client_id, - client_secret=config.spotify_client_secret, - open_browser=False, - redirect_uri='http://localhost:5000/', - scope='playlist-read-private,playlist-read-collaborative', -) -yt = YTMusic(ytmusicapi.YTMusic('browser.json')) -sp = Spotify(spotipy.Spotify(auth_manager=auth_manager)) - -playlist = Playlist.fetch_from(sp, config.sync_from_url) -playlist.sync_to(yt, config.sync_to_id) +main() diff --git a/playlist_sync/cli/__init__.py b/playlist_sync/cli/__init__.py new file mode 100644 index 0000000..6d29dbe --- /dev/null +++ b/playlist_sync/cli/__init__.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2024-present Fyssion +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations +import argparse +import logging + +from rich.logging import RichHandler + +from . import spotify_to_yt +from . import yt_to_spotify + + +log = logging.getLogger('playlist_sync') + +logging.getLogger().setLevel(logging.INFO) +sh = RichHandler(show_time=False) +sh.setFormatter(logging.Formatter("[%(name)s] %(message)s")) +logging.getLogger().addHandler(sh) + + +def main(): + parser = argparse.ArgumentParser(prog='playlist_sync', description='Sync playlists between streaming services') + parser.set_defaults(callback=None) + parsers = parser.add_subparsers(title='subcommands') + + spotify_to_yt.initialize(parsers) + yt_to_spotify.initialize(parsers) + + args = parser.parse_args() + + if args.callback: + args.callback(args) + else: + parser.print_help() diff --git a/playlist_sync/cli/spotify_to_yt.py b/playlist_sync/cli/spotify_to_yt.py new file mode 100644 index 0000000..8d914e8 --- /dev/null +++ b/playlist_sync/cli/spotify_to_yt.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2024-present Fyssion +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse + +from playlist_sync.playlist import Playlist + +from .utils import add_spotify_auth_to_parser, initialize_spotify, initialize_ytmusic, resolve_spotify_auth_from_args + + +def main(args: argparse.Namespace): + resolve_spotify_auth_from_args(args) + + sp = initialize_spotify(args.client_id, args.client_secret) + yt = initialize_ytmusic() + + playlist = Playlist.fetch_from(sp, args._from) + playlist.sync_to(yt, args.to) + + +def initialize(parsers: argparse._SubParsersAction): + parser = parsers.add_parser( + name='spotify-to-yt', + description='Sync a playlist from Spotify to YT Music', + ) + + parser.set_defaults(callback=main) + parser.add_argument('-f', '--from', required=True, help='spotify url of the playlist to sync from', metavar='FROM_URL', dest='_from') + parser.add_argument('-t', '--to', required=True, help='yt playlist id of the playlist to sync to', metavar='TO_ID') + add_spotify_auth_to_parser(parser) diff --git a/playlist_sync/cli/utils.py b/playlist_sync/cli/utils.py new file mode 100644 index 0000000..b6a6560 --- /dev/null +++ b/playlist_sync/cli/utils.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2024-present Fyssion +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse +import importlib +import logging +import pathlib +import sys + +import spotipy +import ytmusicapi +from spotipy.oauth2 import SpotifyOAuth + +from playlist_sync.services.spotify import Spotify +from playlist_sync.services.ytmusic import YTMusic + + +log = logging.getLogger('playlist_sync') + + +def initialize_spotify(client_id: str, client_secret: str) -> Spotify: + + auth_manager = SpotifyOAuth( + client_id=client_id, + client_secret=client_secret, + open_browser=False, + redirect_uri='http://localhost:5000/', + scope='playlist-read-private,playlist-read-collaborative', + ) + return Spotify(spotipy.Spotify(auth_manager=auth_manager)) + + +def initialize_ytmusic() -> YTMusic: + if not pathlib.Path('./browser.json').is_file(): + ytmusicapi.setup(filepath='browser.json') + + return YTMusic(ytmusicapi.YTMusic('browser.json')) + + +def add_spotify_auth_to_parser(parser: argparse.ArgumentParser): + parser.add_argument('--client-id', help='Spotify Client ID') + parser.add_argument('--client-secret', help='Spotify Client secret') + + +def resolve_spotify_auth_from_args(args: argparse.Namespace): + if args.client_id is None or args.client_secret is None: + # check for a config.py + try: + config = importlib.import_module('config') + except ImportError: + log.error('''Could not find Spotify client ID and secret. + Either provide them as arguments in the CLI or in a config.py file''') + sys.exit(1) + + args.client_id = config.spotify_client_id + args.client_secret = config.spotify_client_secret diff --git a/playlist_sync/cli/yt_to_spotify.py b/playlist_sync/cli/yt_to_spotify.py new file mode 100644 index 0000000..6fbc880 --- /dev/null +++ b/playlist_sync/cli/yt_to_spotify.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2024-present Fyssion +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import argparse + +from playlist_sync.playlist import Playlist + +from .utils import add_spotify_auth_to_parser , initialize_spotify, initialize_ytmusic, resolve_spotify_auth_from_args + + +def main(args: argparse.Namespace): + resolve_spotify_auth_from_args(args) + + sp = initialize_spotify(args.client_id, args.client_secret) + yt = initialize_ytmusic() + + playlist = Playlist.fetch_from(yt, args._from) + playlist.sync_to(sp, args.to) + + +def initialize(parsers: argparse._SubParsersAction): + parser = parsers.add_parser( + name='yt-to-spotify', + description='Sync a playlist from YT Music to Spotify', + ) + + parser.set_defaults(callback=main) + parser.add_argument('-f', '--from', required=True, help='yt playlist id of the playlist to sync from', dest='_from') + parser.add_argument('-t', '--to', required=True, help='spotify url of the playlist to sync to') + add_spotify_auth_to_parser(parser) \ No newline at end of file