Skip to content

Commit

Permalink
Add a simple CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
Fyssion committed Oct 17, 2024
1 parent 48d8d7a commit e8f15c0
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 46 deletions.
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<SPOTIFY_URL>" -t "<YT_ID>"

# MacOS/Linux
python3 -m playlist_sync
python3 -m playlist_sync spotify-to-yt -f "<SPOTIFY_URL>" -t "<YT_ID>"
```

Replace `<SPOTIFY_URL>` and `<YT_ID>` 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 "<YT_ID>" -t "<SPOTIFY_URL>"
```

On the first run, it'll ask you to paste your browser credentials for
Expand All @@ -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
35 changes: 2 additions & 33 deletions playlist_sync/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
36 changes: 36 additions & 0 deletions playlist_sync/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2024-present Fyssion <fyssioncodes@gmail.com>
#
# 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()
33 changes: 33 additions & 0 deletions playlist_sync/cli/spotify_to_yt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2024-present Fyssion <fyssioncodes@gmail.com>
#
# 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)
59 changes: 59 additions & 0 deletions playlist_sync/cli/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: 2024-present Fyssion <fyssioncodes@gmail.com>
#
# 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
33 changes: 33 additions & 0 deletions playlist_sync/cli/yt_to_spotify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2024-present Fyssion <fyssioncodes@gmail.com>
#
# 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)

0 comments on commit e8f15c0

Please sign in to comment.