I am a student from a developing country. My pocket money is pretty much nonexistent, so I cannot pay for Spotify. However, music is important to my productivity, and there are times when I go to places without an internet connection, so I need to keep local copies of songs for these reasons. Spotify restricts offline listening to premium subscribers, so my only option was to rely on their free API to apply a couple of workarounds.
I then found spotdl. It relies on their free API to get the song list of a Spotify playlist, then uses youtube-dl to query the songs on YouTube, subsequently downloading them. Spotdl supports multi-threading; that is, it can download multiple tracks simultaneously. It also skips the songs that you already have in the selected folder.
The next trouble was syncing the files to my other devices without manual labor. I found Syncthing useful for this. The process was really simple. If the devices are connected to the same Wi-Fi network, you don’t really have to do much to get going. But if not, you have some setup to do yourself that I won’t be explaining here.
If you use Windows 10, then you should install Scoop to manage your packages. If you use Linux, then your default package managers themselves will do an adequate job. MacOS users are on their own, because I don’t have the environment to test the steps I’d be mentioning.
Prerequisites Link to heading
Let us first set up all the prerequisites.
Windows 10 Link to heading
scoop bucket add main
scoop bucket add extras
scoop bucket add versions
scoop install main/ffmpeg
scoop install main/python
scoop install main/syncthing
scoop install main/czkawka
scoop install versions/spotdl-beta
Linux Link to heading
I will only provide the steps for Ubuntu 22.04; other distribution users are asked to do internet searches and install the required packages. I don’t use Ubuntu myself, but it is the most popular choice, so I am choosing it.
sudo mkdir -p /etc/apt/keyrings
sudo curl -L -o /etc/apt/keyrings/syncthing-archive-keyring.gpg https://syncthing.net/release-key.gpg
echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable" | sudo tee /etc/apt/sources.list.d/syncthing.list
sudo add-apt-repository ppa:xtradeb/apps
sudo apt update
sudo apt install ffmpeg python3-pip syncthing czkawka
pip3 install spotdl
Setup Link to heading
czkawka
will be used to discover and delete duplicate songs.
Syncthing Link to heading
You’d want to pick a folder to keep your songs in. I recommend ~/Music/Spotify
. Open a terminal window and start syncthing using the syncthing
command. Your default browser will be opened with a locally hosted instance of Syncthing. Add the folder you picked; make sure you give it an appropriate folder ID (like spotify-sync
). Add your device by clicking on “Add remote device.”. If you want to add an Android device, you can download their Android client from major app stores (Google Play, F-Droid).
I will give special attention to adding an Android device:
- install and open the “Syncthing” app on your phone;
- tap the ‘+’ button;
- scan the QR code that you can find from Actions > Show ID button of the Syncthing web instance;
- wait some time, and then you should be prompted with a request to add the device on the web instance;
- edit the picked folder and register the device in the “Sharing” section;
- you will be notified on your Android to accept the request.
- open the Android app and tap on the new folder entry;
- link the folder to a local folder.
That should be it.
Edit the folder update interval in the web instance to your liking. Personally, I prefer ten minutes (600s).
To test, CWD to your picked folder, then execute spotdl download [playlist_link]
. It will start downloading the songs there. Simultaneously, Syncthing will sync the files on other remote devices as instructed.
Duplicate songs are not kept in check. Continue reading to set that up. I prefer keeping the oldest copy, so my method would be AEO
.
Automating Things Link to heading
For ease of use, I wrote a simple Python script to manage the Spotify folder:
"""
Basic Spotify synchronization manager.
Author(s): eeriemyxi (GitHub)
License: GPL 3.0
"""
import pathlib, subprocess, sys, logging, argparse
SCRIPT_PATH = pathlib.Path(__file__).parent
logging.basicConfig(
level=logging.INFO,
filename=SCRIPT_PATH / "spotify-sync.log",
format="%(asctime)s: %(message)s",
)
logger = logging.getLogger(__name__)
parser = argparse.ArgumentParser(description="Basic Spotify synchronization manager.")
parser.add_argument(
"--duplicate-removal-method",
help="Delete duplicates after syncing. NONE by default. Uses czkawka. "
"See `czkawka dup --help`",
default="NONE",
dest="removal_method",
)
args = parser.parse_args()
args.removal_method = args.removal_method.upper()
logger.info("Script started with arguments: %s.", repr(sys.argv))
with open(SCRIPT_PATH / "playlists-to-sync.txt") as file:
for line_c, line in enumerate(file.readlines(), 1):
line = line.strip()
if line[0] == "#":
sys.stderr.write(f"Skipping line #{line_c}.\n")
logger.warning(f"Skipping line #{line_c}.")
continue
links, folder_name = line.split(";", 1)
links = links.split()
logger.info(f"Downloading to folder {folder_name} from links: {links}.")
playlist_path = SCRIPT_PATH / folder_name
playlist_path.mkdir(parents=True, exist_ok=True)
sync_file = playlist_path / "spotify-sync.spotdl"
if sync_file.exists():
subprocess.run(
["spotdl", "sync", sync_file.name], cwd=playlist_path, shell=True
)
else:
subprocess.run(
["spotdl", "sync", " ".join(links), "--save-file", sync_file.name],
cwd=playlist_path,
shell=True,
)
logger.info(f"Done synching playlists to folder {folder_name}.")
if args.removal_method != "NONE":
logger.info(
"Deleting duplicates with method '%s' in '%s'.",
args.removal_method,
SCRIPT_PATH,
)
out = subprocess.run(
[
"czkawka",
"dup",
"-D",
args.removal_method,
"--directories",
str(SCRIPT_PATH),
],
shell=True,
capture_output=True,
)
logger.info(
"Deletion complete with return code %s; stdout: %s; stderr: %s",
out.returncode,
out.stdout,
out.stderr,
)
Save it in the picked folder as spotify-sync.py
. This file can be downloaded from here.
The script will read a file playlists-to-sync.txt
where each line is like this: link-here.com optional-links.com just-like-this.com;folder name here
. You can ignore a line by adding a #
to the start of the line. Here is a sample file:
https://open.spotify.com/playlist/4O[redacted]84;Liked Songs
# https://open.spotify.com/playlist/37[redacted]8a;Orchestra
The playlists will be synched to the folders named Liked Songs
and Orchestra
in the sample. However the second line is skipped due to the #
character in the file. You can sync multiple playlists in one folder by splitting each link with a space (’ ‘). You can set Liked Songs/Sub Directory Name
as the folder to create empty folder Liked Songs
and inside there then the files will be synced in Sub Directory Name
.
If the folders registered in playlists-to-sync.txt
have songs only available locally, they are deleted unless you add those tracks to the playlists the folders are supposed to backup.
See python3 spotify-sync.py --help
for more information (on duplicating files, it is disabled by default).
Scheduling Link to heading
You can utilize Task Scheduler on Windows 10, and for Linux, Ubuntu comes with Systemd. There are also other tools that can help with the task. I leave the searching to you. I will, however, link one guide on systemd timers. Something more universal and simpler (i.e., less abstracted) could be cron jobs.
I will only guide you through the steps I took on Windows 10. Download Syncthing.xml and Spotify Sync.xml. Import them on Task Scheduler, then edit the Actions section of both tasks to fit your environment. Runner.bat
, used for Spotify Sync.xml
, is provided and explained next.
That file first changes the working directory to the picked folder, then runs python spotify-sync.py --duplicate-removal-method AEO
. Pythonw.exe
is avoided because there are some effortfully unresolved, obscure bugs associated with it, so I migrated to a batch script.
cd C:\Users\myxi\Music\Spotify
python spotify-sync.py --duplicate-removal-method aeo