-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2abfd9c
Showing
14 changed files
with
1,050 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
__pycache__ | ||
.venv/ | ||
dist/ | ||
*.egg-info |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
build: | ||
@python3 -m build | ||
|
||
clean: | ||
rm -rf dist *.egg-info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Playlist-sync | ||
|
||
`playlist-sync` is a little command line tool to download and sync playlists from Deezer or Spotify to predefined folders. It reads playlists links and target folders from a JSON file. | ||
|
||
It uses [`deemix`](https://pypi.org/project/deemix/) under the hood to actually download the playlists. | ||
|
||
## What you will need | ||
- Python >= 3.8 with pip (untested on earlier versions of Python) | ||
- A Deeezer account. Since `deemix` downloads songs from Deezer, it uses your Deezer account to access Deezer servers and download music. So even if you only want to download Spotify playlists, you will **need** to have a Deezer account. Note that to download 320kbps MP3 or FLAC, you will need a Deezer Premium account. A free Deezer account only allow to download 128kbps MP3. | ||
- A Spotify account if you want to download playlists from Spotify. | ||
|
||
## Installation | ||
Playlist-sync can be installed with `pip` from [PyPI](https://pypi.org/project/playlist-sync/): | ||
``` | ||
pip install playlist-sync | ||
``` | ||
The pip package adds the `playlist-sync` command to the command line. | ||
|
||
## How to setup and use | ||
Playlist-sync relies on two files, `config.json` and `playlists.json`, which must exist in the current working directory. `config.json` contains some general settings (Deezer ARL, Spotify API token, bitrate...), and `playlists.json` contains the links to your playlists as well as the target folders where you want them to be downloaded. | ||
|
||
`playlist-sync` can create templates for these two files so you only need to fill them. In your music library folder (where you want your playlists to be downloaded), run: | ||
``` | ||
playlist-sync init | ||
``` | ||
|
||
It will create the 2 json files. Fill them both as explained in the wiki, [here](https://github.com/lilianmallardeau/playlist-sync/wiki/The-config.json-file) and [here](https://github.com/lilianmallardeau/playlist-sync/wiki/The-playlists.json-file). | ||
Once you've filled the `config.json` file with your Deezer ARL (and Spotify API client ID and secret if you want to download Spotify playlists) and the `playlists.json` file with your playlists links, to download them all at once in the desired folders, simply run: | ||
``` | ||
playlist-sync sync | ||
``` | ||
|
||
|
||
## How to install and use easily on Windows | ||
1. If you don't have it installed already, download and install [Python](https://www.python.org). During installation, make sure to choose to update the PATH environment variable. | ||
2. Open the command prompt (search for "cmd" in the search bar) and type `pip install playlist-sync` | ||
3. Download the 2 scripts in the [`windows_scripts`](https://github.com/lilianmallardeau/playlist-sync/tree/main/windows_scripts) folder in this repo, and put them in your music library folder | ||
4. Double click on `playlist-sync_init.cmd`. It will create two json files, `config.json` and `playlists.json`, in the same folder. | ||
5. Fill the two json files as described [here](https://github.com/lilianmallardeau/playlist-sync/wiki/The-config.json-file) and [here](https://github.com/lilianmallardeau/playlist-sync/wiki/The-playlists.json-file). | ||
6. To download/update your playlists, simply double click on the `playlist-sync_sync.cmd` file | ||
|
||
|
||
--- | ||
|
||
|
||
## Todo | ||
- Add support for SoundCloud and YouTube playlists, with [youtube-dl](http://ytdl-org.github.io/youtube-dl/) | ||
- Sync Serato/rekordbox crates with downloaded playlists | ||
- Use ISRC numbers to prevent downloading songs from different playlists twice, and make hardlinks between files instead |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
#!/usr/bin/env python3 | ||
import os | ||
import sys | ||
import json | ||
import click | ||
|
||
from .config import Config | ||
from .deemix_config import DeemixConfig | ||
from .playlist import Playlist | ||
|
||
CONFIG_FILE = "config.json" | ||
PLAYLIST_FILE = "playlists.json" | ||
|
||
DEFAULT_PLAYLISTS = [ | ||
{ | ||
"url": "https://deezer.com/playlist/12345678...", | ||
"folder": "PLAYLIST_FOLDER", | ||
"overwrite": False | ||
}, | ||
{ | ||
"url": "https://open.spotify.com/playlist/abcd1234...", | ||
"folder": "PLAYLIST_FOLDER", | ||
"overwrite": False | ||
} | ||
] | ||
|
||
|
||
@click.group() | ||
def cli(): | ||
pass | ||
|
||
@cli.command() | ||
def init(): | ||
config = Config(CONFIG_FILE, initialize=True) | ||
if (not config.exists()) or input(f"{config.config_file} already exists. Overwrite? (y/N) ").lower() == 'y': | ||
config.save() | ||
if (not os.path.isfile(PLAYLIST_FILE)) or input(f"{PLAYLIST_FILE} already exists. Overwrite? (y/N) ").lower() == 'y': | ||
with open(PLAYLIST_FILE, "w") as playlist_file: | ||
json.dump(DEFAULT_PLAYLISTS, playlist_file, indent=4) | ||
|
||
@cli.command() | ||
def sync(): | ||
if not (os.path.isfile(CONFIG_FILE) and os.path.isfile(PLAYLIST_FILE)): | ||
print(f"{CONFIG_FILE} or {PLAYLIST_FILE} doesn't exist. Run `{sys.argv[0]} init` to initialize.") | ||
return | ||
|
||
# Reading config | ||
config = Config(CONFIG_FILE) | ||
deemix_config = DeemixConfig("config", arl=config['arl']) | ||
deemix_config.setSpotifyConfig(config['spotifyClientId'], config['spotifyClientSecret']) | ||
deemix_config.setDefaultBitrate(config['defaultBitrate'], fallback=bool(config['fallbackBitrate'])) | ||
deemix_config.setOverwrite(bool(config['overwriteFiles'])) | ||
deemix_config['saveArtwork'] = config['saveArtwork'] | ||
|
||
# Reading playlists file and syncing each playlist | ||
for playlist in json.load(open(PLAYLIST_FILE)): | ||
playlist_obj = Playlist(playlist['url'], playlist['folder'], deemix_config) | ||
playlist_obj.sync(overwrite_files=bool(playlist['overwrite']) if 'overwrite' in playlist else False) | ||
|
||
deemix_config.clear() | ||
|
||
|
||
if __name__ == '__main__': | ||
cli() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import os | ||
from pathlib import Path | ||
import json | ||
|
||
DEFAULT_CONFIG = { | ||
"arl": "YOUR_DEEZER_ARL", | ||
"spotifyClientId": "", | ||
"spotifyClientSecret": "", | ||
"defaultBitrate": 320, | ||
"fallbackBitrate": True, | ||
"overwriteFiles": False, | ||
"saveArtwork": True | ||
} | ||
|
||
|
||
class Config(): | ||
def __init__(self, config_file, initialize=False) -> None: | ||
self.config_file = config_file | ||
if initialize: | ||
self._config = DEFAULT_CONFIG | ||
else: | ||
self.load() | ||
|
||
def save(self): | ||
json.dump(self._config, open(self.config_file, "w"), indent=4) | ||
|
||
def load(self): | ||
self._config = json.load(open(self.config_file)) | ||
|
||
def exists(self): | ||
return os.path.isfile(self.config_file) | ||
|
||
def __getitem__(self, key): | ||
return self._config[key] | ||
|
||
def __setitem__(self, key, value): | ||
self._config[key] = value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import os | ||
import shutil | ||
from pathlib import Path | ||
import json | ||
|
||
DEFAULT_CONFIG = { | ||
"downloadLocation": os.getcwd(), | ||
"tracknameTemplate": "%artist% - %title%", | ||
"albumTracknameTemplate": "%tracknumber% - %title%", | ||
"playlistTracknameTemplate": "%position% - %artist% - %title%", | ||
"createPlaylistFolder": True, | ||
"playlistNameTemplate": "%playlist%", | ||
"createArtistFolder": False, | ||
"artistNameTemplate": "%artist%", | ||
"createAlbumFolder": True, | ||
"albumNameTemplate": "%artist% - %album%", | ||
"createCDFolder": True, | ||
"createStructurePlaylist": False, | ||
"createSingleFolder": False, | ||
"padTracks": True, | ||
"paddingSize": "0", | ||
"illegalCharacterReplacer": "_", | ||
"queueConcurrency": 3, | ||
"maxBitrate": "3", | ||
"feelingLucky": False, | ||
"fallbackBitrate": True, | ||
"fallbackSearch": False, | ||
"fallbackISRC": False, | ||
"logErrors": True, | ||
"logSearched": False, | ||
"overwriteFile": "n", | ||
"createM3U8File": False, | ||
"playlistFilenameTemplate": "playlist", | ||
"syncedLyrics": False, | ||
"embeddedArtworkSize": 800, | ||
"embeddedArtworkPNG": False, | ||
"localArtworkSize": 1400, | ||
"localArtworkFormat": "jpg", | ||
"saveArtwork": True, | ||
"coverImageTemplate": "cover", | ||
"saveArtworkArtist": False, | ||
"artistImageTemplate": "folder", | ||
"jpegImageQuality": 90, | ||
"dateFormat": "Y-M-D", | ||
"albumVariousArtists": True, | ||
"removeAlbumVersion": False, | ||
"removeDuplicateArtists": True, | ||
"featuredToTitle": "0", | ||
"titleCasing": "nothing", | ||
"artistCasing": "nothing", | ||
"executeCommand": "", | ||
"tags": { | ||
"title": True, | ||
"artist": True, | ||
"artists": True, | ||
"album": True, | ||
"cover": True, | ||
"trackNumber": True, | ||
"trackTotal": False, | ||
"discNumber": True, | ||
"discTotal": False, | ||
"albumArtist": True, | ||
"genre": True, | ||
"year": True, | ||
"date": True, | ||
"explicit": False, | ||
"isrc": True, | ||
"length": True, | ||
"barcode": True, | ||
"bpm": True, | ||
"replayGain": False, | ||
"label": True, | ||
"lyrics": False, | ||
"syncedLyrics": False, | ||
"copyright": False, | ||
"composer": False, | ||
"involvedPeople": False, | ||
"source": False, | ||
"rating": False, | ||
"savePlaylistAsCompilation": False, | ||
"useNullSeparator": False, | ||
"saveID3v1": True, | ||
"multiArtistSeparator": "default", | ||
"singleAlbumArtist": False, | ||
"coverDescriptionUTF8": False | ||
} | ||
} | ||
DEFAULT_SPOTIFY_CONFIG = { | ||
"clientId": "", | ||
"clientSecret": "", | ||
"fallbackSearch": False | ||
} | ||
|
||
class DeemixConfig(): | ||
def __init__(self, config_folder: str = "config", arl: str = None, overwrite: bool = True) -> None: | ||
self.config_folder = Path(config_folder) | ||
if arl: | ||
self.arl = arl | ||
self._config = DEFAULT_CONFIG | ||
self._spotify_config = DEFAULT_SPOTIFY_CONFIG | ||
if not overwrite: | ||
self.load() | ||
|
||
def save(self): | ||
os.makedirs(self.config_folder / "spotify", exist_ok=True) | ||
|
||
with open(self.config_folder / "config.json", "w") as config_file: | ||
json.dump(self._config, config_file, indent=4) | ||
with open(self.config_folder / "spotify" / "config.json", "w") as config_file: | ||
json.dump(self._spotify_config, config_file, indent=4) | ||
with open(self.config_folder / ".arl", "w") as arl_file: | ||
arl_file.write(self.arl) | ||
|
||
def clear(self): | ||
shutil.rmtree(self.config_folder) | ||
|
||
def load(self): | ||
# TODO | ||
raise NotImplementedError() | ||
|
||
def setSpotifyConfig(self, client_id, secret, fallbackSearch=None): | ||
self._spotify_config["clientId"] = str(client_id) | ||
self._spotify_config["clientSecret"] = str(secret) | ||
if fallbackSearch is not None: | ||
self._spotify_config["fallbackSearch"] = bool(fallbackSearch) | ||
|
||
def setDefaultBitrate(self, bitrate, fallback=None): | ||
if bitrate == 128: | ||
self._config['maxBitrate'] = 1 | ||
elif bitrate == 320: | ||
self._config['maxBitrate'] = 3 | ||
elif bitrate == 'flac': | ||
self._config['maxBitrate'] = 9 | ||
else: | ||
raise ValueError(f"`bitrate` must be either 128, 320 or 'flac', got {bitrate}") | ||
|
||
if fallback is not None: | ||
self._config['fallbackBitrate'] = bool(fallback) | ||
|
||
def setOverwrite(self, overwrite: bool): | ||
self._config['overwriteFile'] = 'y' if overwrite else 'n' | ||
|
||
def __getitem__(self, key): | ||
return self._config[key] | ||
|
||
def __setitem__(self, key, value): | ||
self._config[key] = value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import os | ||
from pathlib import Path | ||
|
||
class Playlist(): | ||
def __init__(self, url, folder, deemix_config): | ||
self.url = url | ||
self.folder = Path(folder) | ||
self._deemix_config = deemix_config | ||
|
||
self._deemix_config['downloadLocation'] = str(self.folder.absolute()) | ||
self._deemix_config['createPlaylistFolder'] = False | ||
|
||
def sync(self, overwrite_files=False, suppress_output=False): | ||
self._deemix_config.setOverwrite(overwrite_files) | ||
self._deemix_config.save() | ||
return os.system(f"deemix --portable {self.url}{' > /dev/null' if suppress_output else ''}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[build-system] | ||
requires = [ | ||
"setuptools>=42", | ||
"wheel" | ||
] | ||
build-backend = "setuptools.build_meta" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
certifi==2021.10.8 | ||
charset-normalizer==2.0.12 | ||
click==8.0.4 | ||
deemix==3.6.6 | ||
deezer-py==1.3.7 | ||
idna==3.3 | ||
mutagen==1.45.1 | ||
pycryptodomex==3.14.1 | ||
requests==2.27.1 | ||
six==1.16.0 | ||
spotipy==2.19.0 | ||
urllib3==1.26.8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
[metadata] | ||
name = playlist-sync | ||
version = 0.1.0 | ||
author = Lilian Mallardeau | ||
author_email = [email protected] | ||
description = A little command line tool to download and sync playlists from Deezer or Spotify to predefined folders. | ||
long_description = file: README.md | ||
long_description_content_type = text/markdown | ||
url = https://github.com/lilianmallardeau/playlist-sync | ||
project_urls = | ||
Bug Tracker = https://github.com/lilianmallardeau/playlist-sync/issues | ||
classifiers = | ||
Programming Language :: Python :: 3 | ||
License :: OSI Approved :: GNU General Public License v3 (GPLv3) | ||
Operating System :: OS Independent | ||
|
||
[options] | ||
packages = playlist_sync | ||
python_requires = >=3.8 | ||
install_requires = | ||
click==8.0.4 | ||
deemix==3.6.6 | ||
spotipy==2.19.0 | ||
|
||
[options.entry_points] | ||
console_scripts = | ||
playlist-sync = playlist_sync.__main__:cli |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
@echo off | ||
playlist-sync init | ||
echo Done! Now, fill the config.json and playlists.json files. | ||
pause |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@echo off | ||
playlist-sync sync | ||
echo. | ||
echo Done! | ||
pause |