Skip to content

Commit

Permalink
First version!
Browse files Browse the repository at this point in the history
  • Loading branch information
lilianmallardeau committed Mar 16, 2022
0 parents commit 2abfd9c
Show file tree
Hide file tree
Showing 14 changed files with 1,050 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__
.venv/
dist/
*.egg-info
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
build:
@python3 -m build

clean:
rm -rf dist *.egg-info
49 changes: 49 additions & 0 deletions README.md
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 added playlist_sync/__init__.py
Empty file.
64 changes: 64 additions & 0 deletions playlist_sync/__main__.py
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()
37 changes: 37 additions & 0 deletions playlist_sync/config.py
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
147 changes: 147 additions & 0 deletions playlist_sync/deemix_config.py
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
16 changes: 16 additions & 0 deletions playlist_sync/playlist.py
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 ''}")
6 changes: 6 additions & 0 deletions pyproject.toml
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"
12 changes: 12 additions & 0 deletions requirements.txt
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
27 changes: 27 additions & 0 deletions setup.cfg
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
4 changes: 4 additions & 0 deletions windows_scripts/playlist-sync_init.cmd
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
5 changes: 5 additions & 0 deletions windows_scripts/playlist-sync_sync.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@echo off
playlist-sync sync
echo.
echo Done!
pause

0 comments on commit 2abfd9c

Please sign in to comment.