spotify-auto-dl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spotify_auto_dl-0.1.0/PKG-INFO +248 -0
- spotify_auto_dl-0.1.0/README.md +234 -0
- spotify_auto_dl-0.1.0/pyproject.toml +26 -0
- spotify_auto_dl-0.1.0/setup.cfg +4 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl/__init__.py +0 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl/config.py +37 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl/downloader.py +117 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl/main.py +117 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl/state.py +39 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl.egg-info/PKG-INFO +248 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl.egg-info/SOURCES.txt +13 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl.egg-info/dependency_links.txt +1 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl.egg-info/entry_points.txt +2 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl.egg-info/requires.txt +7 -0
- spotify_auto_dl-0.1.0/spotify_auto_dl.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spotify-auto-dl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automatically download new releases from your favorite artists using spotdl
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: spotdl>=4.0.0
|
|
8
|
+
Requires-Dist: click>=8.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
12
|
+
Requires-Dist: rich>=13.0.0
|
|
13
|
+
Requires-Dist: spotipy>=2.23.0
|
|
14
|
+
|
|
15
|
+
# Spotify Auto Downloader
|
|
16
|
+
|
|
17
|
+
Automatically download new releases from your favorite Spotify artists to a local drive. Tracks artists you care about, downloads only songs you don't already have, and can run on a schedule to stay up to date automatically.
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
1. You provide a list of Spotify artist URLs in a config file
|
|
22
|
+
2. On each run, the tool fetches each artist's full discography from Spotify
|
|
23
|
+
3. It compares against a local state file to find tracks you don't have yet
|
|
24
|
+
4. New tracks are downloaded as MP3s via [spotdl](https://github.com/spotDL/spotify-downloader)
|
|
25
|
+
5. The state file is updated so those tracks are skipped on future runs
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.10 or higher
|
|
32
|
+
- A [Spotify Developer account](https://developer.spotify.com/dashboard) (free)
|
|
33
|
+
- `ffmpeg` installed on your machine
|
|
34
|
+
|
|
35
|
+
Install ffmpeg on Mac:
|
|
36
|
+
```bash
|
|
37
|
+
brew install ffmpeg
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
**1. Clone the repo**
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/your-username/spotify-auto-downloader.git
|
|
47
|
+
cd spotify-auto-downloader
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. Create and activate a virtual environment**
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**3. Install the package**
|
|
57
|
+
```bash
|
|
58
|
+
pip install .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Spotify API Credentials
|
|
64
|
+
|
|
65
|
+
This tool uses the Spotify API to look up artist discographies. You need a free client ID and secret.
|
|
66
|
+
|
|
67
|
+
1. Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard)
|
|
68
|
+
2. Click **Create App**
|
|
69
|
+
3. Fill in any name and description, set the redirect URI to `http://localhost`
|
|
70
|
+
4. Open the app and copy the **Client ID** and **Client Secret**
|
|
71
|
+
|
|
72
|
+
**4. Set up your credentials**
|
|
73
|
+
```bash
|
|
74
|
+
cp .env.example .env
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Open `.env` and fill in your credentials:
|
|
78
|
+
```
|
|
79
|
+
SPOTIFY_CLIENT_ID=your_client_id_here
|
|
80
|
+
SPOTIFY_CLIENT_SECRET=your_client_secret_here
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
**5. Create your config file**
|
|
88
|
+
```bash
|
|
89
|
+
cp config.yaml.example config.yaml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Open `config.yaml` and edit it:
|
|
93
|
+
```yaml
|
|
94
|
+
download_dir: ~/Music/spotify-auto-dl
|
|
95
|
+
|
|
96
|
+
artists:
|
|
97
|
+
- name: Daniel Caesar
|
|
98
|
+
url: https://open.spotify.com/artist/20wkVLutqVOYrc0kxFs7rA
|
|
99
|
+
|
|
100
|
+
schedule:
|
|
101
|
+
interval_hours: 24
|
|
102
|
+
run_on_start: true
|
|
103
|
+
|
|
104
|
+
output:
|
|
105
|
+
format: "{artist}/{album}/{title}.{output-ext}"
|
|
106
|
+
audio_format: mp3
|
|
107
|
+
bitrate: 320k
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To get an artist URL: open Spotify → artist page → three dots → **Share** → **Copy Artist Link**.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
### Run a one-time sync
|
|
117
|
+
```bash
|
|
118
|
+
spotify-auto-dl sync
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Run continuously on a schedule (daemon mode)
|
|
122
|
+
```bash
|
|
123
|
+
spotify-auto-dl sync --daemon
|
|
124
|
+
```
|
|
125
|
+
Syncs immediately on start (if `run_on_start: true`), then again every `interval_hours` hours. See [Scheduling](#scheduling) for when to use this vs cron.
|
|
126
|
+
|
|
127
|
+
### Add an artist
|
|
128
|
+
```bash
|
|
129
|
+
spotify-auto-dl add-artist "https://open.spotify.com/artist/ARTIST_ID"
|
|
130
|
+
```
|
|
131
|
+
Looks up the artist name from Spotify automatically and adds them to `config.yaml`.
|
|
132
|
+
|
|
133
|
+
### Change the download directory
|
|
134
|
+
```bash
|
|
135
|
+
spotify-auto-dl set-destination /Volumes/MyDrive/Music
|
|
136
|
+
```
|
|
137
|
+
Updates `download_dir` in `config.yaml`. Creates the directory if it doesn't exist.
|
|
138
|
+
|
|
139
|
+
### Use a custom config file path
|
|
140
|
+
```bash
|
|
141
|
+
spotify-auto-dl sync --config /path/to/my-config.yaml
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## File Organization
|
|
147
|
+
|
|
148
|
+
Downloaded files are organized using the `output.format` template in `config.yaml`:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
{download_dir}/
|
|
152
|
+
└── {artist}/
|
|
153
|
+
└── {album}/
|
|
154
|
+
└── {title}.mp3
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For example:
|
|
158
|
+
```
|
|
159
|
+
~/Music/spotify-auto-dl/
|
|
160
|
+
└── Daniel Caesar/
|
|
161
|
+
└── Freudian/
|
|
162
|
+
├── Get You (feat. Kali Uchis).mp3
|
|
163
|
+
└── Best Part (feat. H.E.R.).mp3
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## State File
|
|
169
|
+
|
|
170
|
+
Downloaded tracks are recorded in `~/.spotify-auto-dl/state.json`. This file is how the tool knows what you already have — it prevents re-downloading songs across runs. Do not delete it unless you want to re-download everything.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Scheduling
|
|
175
|
+
|
|
176
|
+
There are two ways to run this tool automatically: **daemon mode** and **cron**. They accomplish the same goal differently.
|
|
177
|
+
|
|
178
|
+
### Daemon vs Cron
|
|
179
|
+
|
|
180
|
+
| | Daemon (`--daemon`) | Cron |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| How it works | Python process that sleeps and loops forever | OS wakes the command at a set time, it runs once and exits |
|
|
183
|
+
| Requires terminal open | Yes | No |
|
|
184
|
+
| Survives reboot | No | Yes |
|
|
185
|
+
| Easy to stop | `Ctrl+C` | `crontab -e` to remove |
|
|
186
|
+
| Best for | Servers, Docker containers | Personal computers |
|
|
187
|
+
|
|
188
|
+
**For a personal Mac, cron is recommended.** The daemon requires a terminal window to stay open and won't restart if you reboot. Cron is managed by the OS and runs reliably in the background.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Setting Up Cron (Recommended for Mac)
|
|
193
|
+
|
|
194
|
+
**Step 1 — open your crontab**
|
|
195
|
+
```bash
|
|
196
|
+
crontab -e
|
|
197
|
+
```
|
|
198
|
+
This opens a text editor. If it opens vim, press `i` to start typing.
|
|
199
|
+
|
|
200
|
+
**Step 2 — add this line** (runs every day at 6am)
|
|
201
|
+
```
|
|
202
|
+
0 6 * * * cd /Users/theoaronow/Documents/Spotify-Auto-Downloader && SPOTIFY_CLIENT_ID=your_id SPOTIFY_CLIENT_SECRET=your_secret /Users/theoaronow/Documents/Spotify-Auto-Downloader/.venv/bin/spotify-auto-dl sync --config /Users/theoaronow/Documents/Spotify-Auto-Downloader/config.yaml >> /Users/theoaronow/.spotify-auto-dl/cron.log 2>&1
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Replace `your_id` and `your_secret` with the values from your `.env` file. The credentials must be inline because cron does not load `.env` files automatically.
|
|
206
|
+
|
|
207
|
+
**Step 3 — save and exit**
|
|
208
|
+
|
|
209
|
+
If using vim: press `Esc`, type `:wq`, press `Enter`.
|
|
210
|
+
|
|
211
|
+
**Step 4 — verify it saved**
|
|
212
|
+
```bash
|
|
213
|
+
crontab -l
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Changing the schedule** — edit the `0 6 * * *` part:
|
|
217
|
+
```
|
|
218
|
+
0 8 * * * every day at 8am
|
|
219
|
+
0 6 * * 1 every Monday at 6am
|
|
220
|
+
0 6,18 * * * every day at 6am and 6pm
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Checking the log after it runs:**
|
|
224
|
+
```bash
|
|
225
|
+
cat ~/.spotify-auto-dl/cron.log
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
**`KeyError: SPOTIFY_CLIENT_ID`**
|
|
233
|
+
Your `.env` file is missing or the variable names are misspelled. Make sure `.env` exists in the directory where you run the command.
|
|
234
|
+
|
|
235
|
+
**`FileNotFoundError: config.yaml`**
|
|
236
|
+
Run the command from the project directory, or use `--config /full/path/to/config.yaml`.
|
|
237
|
+
|
|
238
|
+
**A track shows as new every run**
|
|
239
|
+
The same song can have different Spotify IDs across album versions, deluxe editions, and singles. The tool checks by both ID and title to handle this, but edge cases may occur.
|
|
240
|
+
|
|
241
|
+
**Downloads are slow**
|
|
242
|
+
Normal — each track requires finding a matching audio source on YouTube. The first run is the slowest since it downloads everything. Future runs only download new releases.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Spotify Auto Downloader
|
|
2
|
+
|
|
3
|
+
Automatically download new releases from your favorite Spotify artists to a local drive. Tracks artists you care about, downloads only songs you don't already have, and can run on a schedule to stay up to date automatically.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
1. You provide a list of Spotify artist URLs in a config file
|
|
8
|
+
2. On each run, the tool fetches each artist's full discography from Spotify
|
|
9
|
+
3. It compares against a local state file to find tracks you don't have yet
|
|
10
|
+
4. New tracks are downloaded as MP3s via [spotdl](https://github.com/spotDL/spotify-downloader)
|
|
11
|
+
5. The state file is updated so those tracks are skipped on future runs
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Python 3.10 or higher
|
|
18
|
+
- A [Spotify Developer account](https://developer.spotify.com/dashboard) (free)
|
|
19
|
+
- `ffmpeg` installed on your machine
|
|
20
|
+
|
|
21
|
+
Install ffmpeg on Mac:
|
|
22
|
+
```bash
|
|
23
|
+
brew install ffmpeg
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
**1. Clone the repo**
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/your-username/spotify-auto-downloader.git
|
|
33
|
+
cd spotify-auto-downloader
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**2. Create and activate a virtual environment**
|
|
37
|
+
```bash
|
|
38
|
+
python3 -m venv .venv
|
|
39
|
+
source .venv/bin/activate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**3. Install the package**
|
|
43
|
+
```bash
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Spotify API Credentials
|
|
50
|
+
|
|
51
|
+
This tool uses the Spotify API to look up artist discographies. You need a free client ID and secret.
|
|
52
|
+
|
|
53
|
+
1. Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard)
|
|
54
|
+
2. Click **Create App**
|
|
55
|
+
3. Fill in any name and description, set the redirect URI to `http://localhost`
|
|
56
|
+
4. Open the app and copy the **Client ID** and **Client Secret**
|
|
57
|
+
|
|
58
|
+
**4. Set up your credentials**
|
|
59
|
+
```bash
|
|
60
|
+
cp .env.example .env
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open `.env` and fill in your credentials:
|
|
64
|
+
```
|
|
65
|
+
SPOTIFY_CLIENT_ID=your_client_id_here
|
|
66
|
+
SPOTIFY_CLIENT_SECRET=your_client_secret_here
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
**5. Create your config file**
|
|
74
|
+
```bash
|
|
75
|
+
cp config.yaml.example config.yaml
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Open `config.yaml` and edit it:
|
|
79
|
+
```yaml
|
|
80
|
+
download_dir: ~/Music/spotify-auto-dl
|
|
81
|
+
|
|
82
|
+
artists:
|
|
83
|
+
- name: Daniel Caesar
|
|
84
|
+
url: https://open.spotify.com/artist/20wkVLutqVOYrc0kxFs7rA
|
|
85
|
+
|
|
86
|
+
schedule:
|
|
87
|
+
interval_hours: 24
|
|
88
|
+
run_on_start: true
|
|
89
|
+
|
|
90
|
+
output:
|
|
91
|
+
format: "{artist}/{album}/{title}.{output-ext}"
|
|
92
|
+
audio_format: mp3
|
|
93
|
+
bitrate: 320k
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
To get an artist URL: open Spotify → artist page → three dots → **Share** → **Copy Artist Link**.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
### Run a one-time sync
|
|
103
|
+
```bash
|
|
104
|
+
spotify-auto-dl sync
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Run continuously on a schedule (daemon mode)
|
|
108
|
+
```bash
|
|
109
|
+
spotify-auto-dl sync --daemon
|
|
110
|
+
```
|
|
111
|
+
Syncs immediately on start (if `run_on_start: true`), then again every `interval_hours` hours. See [Scheduling](#scheduling) for when to use this vs cron.
|
|
112
|
+
|
|
113
|
+
### Add an artist
|
|
114
|
+
```bash
|
|
115
|
+
spotify-auto-dl add-artist "https://open.spotify.com/artist/ARTIST_ID"
|
|
116
|
+
```
|
|
117
|
+
Looks up the artist name from Spotify automatically and adds them to `config.yaml`.
|
|
118
|
+
|
|
119
|
+
### Change the download directory
|
|
120
|
+
```bash
|
|
121
|
+
spotify-auto-dl set-destination /Volumes/MyDrive/Music
|
|
122
|
+
```
|
|
123
|
+
Updates `download_dir` in `config.yaml`. Creates the directory if it doesn't exist.
|
|
124
|
+
|
|
125
|
+
### Use a custom config file path
|
|
126
|
+
```bash
|
|
127
|
+
spotify-auto-dl sync --config /path/to/my-config.yaml
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## File Organization
|
|
133
|
+
|
|
134
|
+
Downloaded files are organized using the `output.format` template in `config.yaml`:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
{download_dir}/
|
|
138
|
+
└── {artist}/
|
|
139
|
+
└── {album}/
|
|
140
|
+
└── {title}.mp3
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For example:
|
|
144
|
+
```
|
|
145
|
+
~/Music/spotify-auto-dl/
|
|
146
|
+
└── Daniel Caesar/
|
|
147
|
+
└── Freudian/
|
|
148
|
+
├── Get You (feat. Kali Uchis).mp3
|
|
149
|
+
└── Best Part (feat. H.E.R.).mp3
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## State File
|
|
155
|
+
|
|
156
|
+
Downloaded tracks are recorded in `~/.spotify-auto-dl/state.json`. This file is how the tool knows what you already have — it prevents re-downloading songs across runs. Do not delete it unless you want to re-download everything.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Scheduling
|
|
161
|
+
|
|
162
|
+
There are two ways to run this tool automatically: **daemon mode** and **cron**. They accomplish the same goal differently.
|
|
163
|
+
|
|
164
|
+
### Daemon vs Cron
|
|
165
|
+
|
|
166
|
+
| | Daemon (`--daemon`) | Cron |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| How it works | Python process that sleeps and loops forever | OS wakes the command at a set time, it runs once and exits |
|
|
169
|
+
| Requires terminal open | Yes | No |
|
|
170
|
+
| Survives reboot | No | Yes |
|
|
171
|
+
| Easy to stop | `Ctrl+C` | `crontab -e` to remove |
|
|
172
|
+
| Best for | Servers, Docker containers | Personal computers |
|
|
173
|
+
|
|
174
|
+
**For a personal Mac, cron is recommended.** The daemon requires a terminal window to stay open and won't restart if you reboot. Cron is managed by the OS and runs reliably in the background.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Setting Up Cron (Recommended for Mac)
|
|
179
|
+
|
|
180
|
+
**Step 1 — open your crontab**
|
|
181
|
+
```bash
|
|
182
|
+
crontab -e
|
|
183
|
+
```
|
|
184
|
+
This opens a text editor. If it opens vim, press `i` to start typing.
|
|
185
|
+
|
|
186
|
+
**Step 2 — add this line** (runs every day at 6am)
|
|
187
|
+
```
|
|
188
|
+
0 6 * * * cd /Users/theoaronow/Documents/Spotify-Auto-Downloader && SPOTIFY_CLIENT_ID=your_id SPOTIFY_CLIENT_SECRET=your_secret /Users/theoaronow/Documents/Spotify-Auto-Downloader/.venv/bin/spotify-auto-dl sync --config /Users/theoaronow/Documents/Spotify-Auto-Downloader/config.yaml >> /Users/theoaronow/.spotify-auto-dl/cron.log 2>&1
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Replace `your_id` and `your_secret` with the values from your `.env` file. The credentials must be inline because cron does not load `.env` files automatically.
|
|
192
|
+
|
|
193
|
+
**Step 3 — save and exit**
|
|
194
|
+
|
|
195
|
+
If using vim: press `Esc`, type `:wq`, press `Enter`.
|
|
196
|
+
|
|
197
|
+
**Step 4 — verify it saved**
|
|
198
|
+
```bash
|
|
199
|
+
crontab -l
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Changing the schedule** — edit the `0 6 * * *` part:
|
|
203
|
+
```
|
|
204
|
+
0 8 * * * every day at 8am
|
|
205
|
+
0 6 * * 1 every Monday at 6am
|
|
206
|
+
0 6,18 * * * every day at 6am and 6pm
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Checking the log after it runs:**
|
|
210
|
+
```bash
|
|
211
|
+
cat ~/.spotify-auto-dl/cron.log
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Troubleshooting
|
|
217
|
+
|
|
218
|
+
**`KeyError: SPOTIFY_CLIENT_ID`**
|
|
219
|
+
Your `.env` file is missing or the variable names are misspelled. Make sure `.env` exists in the directory where you run the command.
|
|
220
|
+
|
|
221
|
+
**`FileNotFoundError: config.yaml`**
|
|
222
|
+
Run the command from the project directory, or use `--config /full/path/to/config.yaml`.
|
|
223
|
+
|
|
224
|
+
**A track shows as new every run**
|
|
225
|
+
The same song can have different Spotify IDs across album versions, deluxe editions, and singles. The tool checks by both ID and title to handle this, but edge cases may occur.
|
|
226
|
+
|
|
227
|
+
**Downloads are slow**
|
|
228
|
+
Normal — each track requires finding a matching audio source on YouTube. The first run is the slowest since it downloads everything. Future runs only download new releases.
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spotify-auto-dl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Automatically download new releases from your favorite artists using spotdl"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"spotdl>=4.0.0",
|
|
13
|
+
"click>=8.0.0",
|
|
14
|
+
"pydantic>=2.0.0",
|
|
15
|
+
"pyyaml>=6.0.0",
|
|
16
|
+
"python-dotenv>=1.0.0",
|
|
17
|
+
"rich>=13.0.0",
|
|
18
|
+
"spotipy>=2.23.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
spotify-auto-dl = "spotify_auto_dl.main:cli"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["."]
|
|
26
|
+
include = ["spotify_auto_dl*"]
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pydantic import BaseModel, field_validator, HttpUrl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Artist(BaseModel):
|
|
7
|
+
name: str
|
|
8
|
+
url: HttpUrl
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Schedule(BaseModel):
|
|
12
|
+
interval_hours: int = 24
|
|
13
|
+
run_on_start: bool = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Output(BaseModel):
|
|
17
|
+
format: str = "{artist}/{album}/{title}.{output-ext}"
|
|
18
|
+
audio_format: str = "mp3"
|
|
19
|
+
bitrate: str = "320k"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Config(BaseModel):
|
|
23
|
+
download_dir: Path
|
|
24
|
+
artists: list[Artist]
|
|
25
|
+
schedule: Schedule = Schedule()
|
|
26
|
+
output: Output = Output()
|
|
27
|
+
|
|
28
|
+
@field_validator("download_dir", mode="before")
|
|
29
|
+
@classmethod
|
|
30
|
+
def expand_path(cls, v: str) -> Path:
|
|
31
|
+
return Path(v).expanduser().resolve()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_config(path: Path = Path("config.yaml")) -> Config:
|
|
35
|
+
with open(path) as f:
|
|
36
|
+
raw = yaml.safe_load(f)
|
|
37
|
+
return Config(**raw)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
for _logger in ("spotdl", "yt_dlp", "ytmusicapi", "urllib3", "asyncio"):
|
|
6
|
+
logging.getLogger(_logger).setLevel(logging.CRITICAL)
|
|
7
|
+
|
|
8
|
+
import spotipy
|
|
9
|
+
from spotipy.oauth2 import SpotifyClientCredentials
|
|
10
|
+
from spotdl import Spotdl
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from .config import Config
|
|
14
|
+
from .state import State
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _init_spotify() -> spotipy.Spotify:
|
|
20
|
+
return spotipy.Spotify(auth_manager=SpotifyClientCredentials(
|
|
21
|
+
client_id=os.environ["SPOTIFY_CLIENT_ID"],
|
|
22
|
+
client_secret=os.environ["SPOTIFY_CLIENT_SECRET"],
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _init_spotdl(config: Config) -> Spotdl:
|
|
27
|
+
return Spotdl(
|
|
28
|
+
client_id=os.environ["SPOTIFY_CLIENT_ID"],
|
|
29
|
+
client_secret=os.environ["SPOTIFY_CLIENT_SECRET"],
|
|
30
|
+
downloader_settings={
|
|
31
|
+
"output": str(config.download_dir / config.output.format),
|
|
32
|
+
"format": config.output.audio_format,
|
|
33
|
+
"bitrate": config.output.bitrate,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_discography(sp: spotipy.Spotify, artist_id: str) -> list[dict]:
|
|
39
|
+
albums = []
|
|
40
|
+
results = sp.artist_albums(artist_id, album_type="album,single,compilation", limit=50)
|
|
41
|
+
albums.extend(results["items"])
|
|
42
|
+
while results["next"]:
|
|
43
|
+
results = sp.next(results)
|
|
44
|
+
albums.extend(results["items"])
|
|
45
|
+
|
|
46
|
+
all_tracks = []
|
|
47
|
+
seen_names: set[str] = set()
|
|
48
|
+
for album in albums:
|
|
49
|
+
track_results = sp.album_tracks(album["id"], limit=50)
|
|
50
|
+
tracks = track_results["items"]
|
|
51
|
+
while track_results["next"]:
|
|
52
|
+
track_results = sp.next(track_results)
|
|
53
|
+
tracks.extend(track_results["items"])
|
|
54
|
+
|
|
55
|
+
for track in tracks:
|
|
56
|
+
artist_ids_on_track = [a["id"] for a in track["artists"]]
|
|
57
|
+
if artist_id in artist_ids_on_track:
|
|
58
|
+
name_lower = track["name"].lower()
|
|
59
|
+
if name_lower not in seen_names:
|
|
60
|
+
seen_names.add(name_lower)
|
|
61
|
+
all_tracks.append({
|
|
62
|
+
"id": track["id"],
|
|
63
|
+
"name": track["name"],
|
|
64
|
+
"album": album["name"],
|
|
65
|
+
})
|
|
66
|
+
return all_tracks
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def sync(config: Config, state: State) -> None:
|
|
70
|
+
sp = _init_spotify()
|
|
71
|
+
spotdl_client = _init_spotdl(config)
|
|
72
|
+
|
|
73
|
+
for artist in config.artists:
|
|
74
|
+
console.rule(f"[bold]{artist.name}")
|
|
75
|
+
artist_id = urlparse(str(artist.url)).path.split("/")[-1]
|
|
76
|
+
|
|
77
|
+
with console.status("[bold]Fetching discography...[/]", spinner="dots"):
|
|
78
|
+
all_tracks = _get_discography(sp, artist_id)
|
|
79
|
+
|
|
80
|
+
console.print(f" Found [bold]{len(all_tracks)}[/] total tracks:\n")
|
|
81
|
+
|
|
82
|
+
new_tracks = []
|
|
83
|
+
for track in all_tracks:
|
|
84
|
+
already = state.is_downloaded(track["id"], title=track["name"])
|
|
85
|
+
status = "[dim]already downloaded[/dim]" if already else "[green]new[/green]"
|
|
86
|
+
console.print(f" {track['name']} — [dim]{track['album']}[/dim] [{status}]")
|
|
87
|
+
if not already:
|
|
88
|
+
new_tracks.append(track)
|
|
89
|
+
|
|
90
|
+
if not new_tracks:
|
|
91
|
+
console.print("\n [green]Already up to date.[/]")
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
console.print(f"\n [bold]{len(new_tracks)} new track(s) to download.[/]\n")
|
|
95
|
+
|
|
96
|
+
total = len(new_tracks)
|
|
97
|
+
for i, track in enumerate(new_tracks, 1):
|
|
98
|
+
console.print(f" [{i}/{total}] Downloading: [cyan]{track['name']}[/]")
|
|
99
|
+
track_url = f"https://open.spotify.com/track/{track['id']}"
|
|
100
|
+
songs = spotdl_client.search([track_url])
|
|
101
|
+
if songs:
|
|
102
|
+
_, path = spotdl_client.download(songs[0])
|
|
103
|
+
if path:
|
|
104
|
+
state.mark_downloaded(
|
|
105
|
+
track_id=track["id"],
|
|
106
|
+
title=track["name"],
|
|
107
|
+
artist=artist.name,
|
|
108
|
+
album=track["album"],
|
|
109
|
+
)
|
|
110
|
+
console.print(f" [{i}/{total}] [green]Done:[/] {track['name']}")
|
|
111
|
+
else:
|
|
112
|
+
console.print(f" [{i}/{total}] [red]Failed:[/] {track['name']}")
|
|
113
|
+
else:
|
|
114
|
+
console.print(f" [{i}/{total}] [red]Not found on YouTube:[/] {track['name']}")
|
|
115
|
+
|
|
116
|
+
state.save()
|
|
117
|
+
console.print("\n[bold green]Sync complete.[/]")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import os
|
|
3
|
+
import click
|
|
4
|
+
import yaml
|
|
5
|
+
import spotipy
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from spotipy.oauth2 import SpotifyClientCredentials
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
from .config import load_config
|
|
14
|
+
from .downloader import sync as run_sync
|
|
15
|
+
from .state import State
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def cli():
|
|
20
|
+
"""Automatically download new releases from your favorite Spotify artists."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cli.command()
|
|
24
|
+
@click.option(
|
|
25
|
+
"--config",
|
|
26
|
+
"config_path",
|
|
27
|
+
default="config.yaml",
|
|
28
|
+
type=click.Path(exists=True, path_type=Path),
|
|
29
|
+
help="Path to your config.yaml file.",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--daemon",
|
|
33
|
+
is_flag=True,
|
|
34
|
+
default=False,
|
|
35
|
+
help="Run on a loop using the interval defined in config.",
|
|
36
|
+
)
|
|
37
|
+
def sync(config_path: Path, daemon: bool) -> None:
|
|
38
|
+
"""Sync new tracks for all artists in your config."""
|
|
39
|
+
config = load_config(config_path)
|
|
40
|
+
state = State()
|
|
41
|
+
|
|
42
|
+
if not daemon:
|
|
43
|
+
run_sync(config, state)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if config.schedule.run_on_start:
|
|
47
|
+
run_sync(config, state)
|
|
48
|
+
|
|
49
|
+
interval_seconds = config.schedule.interval_hours * 3600
|
|
50
|
+
click.echo(f"Daemon started. Syncing every {config.schedule.interval_hours}h.")
|
|
51
|
+
while True:
|
|
52
|
+
time.sleep(interval_seconds)
|
|
53
|
+
run_sync(config, state)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@cli.command("add-artist")
|
|
57
|
+
@click.argument("url")
|
|
58
|
+
@click.option(
|
|
59
|
+
"--config",
|
|
60
|
+
"config_path",
|
|
61
|
+
default="config.yaml",
|
|
62
|
+
type=click.Path(path_type=Path),
|
|
63
|
+
help="Path to your config.yaml file.",
|
|
64
|
+
)
|
|
65
|
+
def add_artist(url: str, config_path: Path) -> None:
|
|
66
|
+
"""Add an artist to your config by Spotify URL."""
|
|
67
|
+
artist_id = urlparse(url).path.split("/")[-1]
|
|
68
|
+
|
|
69
|
+
sp = spotipy.Spotify(auth_manager=SpotifyClientCredentials(
|
|
70
|
+
client_id=os.environ["SPOTIFY_CLIENT_ID"],
|
|
71
|
+
client_secret=os.environ["SPOTIFY_CLIENT_SECRET"],
|
|
72
|
+
))
|
|
73
|
+
artist_data = sp.artist(artist_id)
|
|
74
|
+
name = artist_data["name"]
|
|
75
|
+
|
|
76
|
+
with open(config_path) as f:
|
|
77
|
+
config = yaml.safe_load(f)
|
|
78
|
+
|
|
79
|
+
for existing in config.get("artists", []):
|
|
80
|
+
if existing["url"] == url:
|
|
81
|
+
click.echo(f"{name} is already in your config.")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
config.setdefault("artists", []).append({"name": name, "url": url})
|
|
85
|
+
|
|
86
|
+
with open(config_path, "w") as f:
|
|
87
|
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
|
88
|
+
|
|
89
|
+
click.echo(f"Added {name} to {config_path}.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cli.command("set-destination")
|
|
93
|
+
@click.argument("path")
|
|
94
|
+
@click.option(
|
|
95
|
+
"--config",
|
|
96
|
+
"config_path",
|
|
97
|
+
default="config.yaml",
|
|
98
|
+
type=click.Path(path_type=Path),
|
|
99
|
+
help="Path to your config.yaml file.",
|
|
100
|
+
)
|
|
101
|
+
def set_destination(path: str, config_path: Path) -> None:
|
|
102
|
+
"""Set the download directory in your config."""
|
|
103
|
+
destination = Path(path).expanduser().resolve()
|
|
104
|
+
|
|
105
|
+
if not destination.exists():
|
|
106
|
+
click.confirm(f"{destination} does not exist. Create it?", abort=True)
|
|
107
|
+
destination.mkdir(parents=True)
|
|
108
|
+
|
|
109
|
+
with open(config_path) as f:
|
|
110
|
+
config = yaml.safe_load(f)
|
|
111
|
+
|
|
112
|
+
config["download_dir"] = str(destination)
|
|
113
|
+
|
|
114
|
+
with open(config_path, "w") as f:
|
|
115
|
+
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
|
116
|
+
|
|
117
|
+
click.echo(f"Download directory set to {destination}.")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
STATE_FILE = Path("~/.spotify-auto-dl/state.json").expanduser()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class State:
|
|
10
|
+
def __init__(self, path: Path = STATE_FILE):
|
|
11
|
+
self._path = path
|
|
12
|
+
self._data: dict[str, dict] = self._load()
|
|
13
|
+
|
|
14
|
+
def _load(self) -> dict[str, dict]:
|
|
15
|
+
if not self._path.exists():
|
|
16
|
+
return {}
|
|
17
|
+
with open(self._path) as f:
|
|
18
|
+
return json.load(f)
|
|
19
|
+
|
|
20
|
+
def is_downloaded(self, track_id: str, title: str | None = None) -> bool:
|
|
21
|
+
if track_id in self._data:
|
|
22
|
+
return True
|
|
23
|
+
if title is not None:
|
|
24
|
+
normalized = title.lower().strip()
|
|
25
|
+
return any(v["title"].lower().strip() == normalized for v in self._data.values())
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def mark_downloaded(self, track_id: str, title: str, artist: str, album: str) -> None:
|
|
29
|
+
self._data[track_id] = {
|
|
30
|
+
"title": title,
|
|
31
|
+
"artist": artist,
|
|
32
|
+
"album": album,
|
|
33
|
+
"downloaded_at": datetime.now(timezone.utc).isoformat(),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def save(self) -> None:
|
|
37
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
with open(self._path, "w") as f:
|
|
39
|
+
json.dump(self._data, f, indent=2)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spotify-auto-dl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automatically download new releases from your favorite artists using spotdl
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: spotdl>=4.0.0
|
|
8
|
+
Requires-Dist: click>=8.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
12
|
+
Requires-Dist: rich>=13.0.0
|
|
13
|
+
Requires-Dist: spotipy>=2.23.0
|
|
14
|
+
|
|
15
|
+
# Spotify Auto Downloader
|
|
16
|
+
|
|
17
|
+
Automatically download new releases from your favorite Spotify artists to a local drive. Tracks artists you care about, downloads only songs you don't already have, and can run on a schedule to stay up to date automatically.
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
1. You provide a list of Spotify artist URLs in a config file
|
|
22
|
+
2. On each run, the tool fetches each artist's full discography from Spotify
|
|
23
|
+
3. It compares against a local state file to find tracks you don't have yet
|
|
24
|
+
4. New tracks are downloaded as MP3s via [spotdl](https://github.com/spotDL/spotify-downloader)
|
|
25
|
+
5. The state file is updated so those tracks are skipped on future runs
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.10 or higher
|
|
32
|
+
- A [Spotify Developer account](https://developer.spotify.com/dashboard) (free)
|
|
33
|
+
- `ffmpeg` installed on your machine
|
|
34
|
+
|
|
35
|
+
Install ffmpeg on Mac:
|
|
36
|
+
```bash
|
|
37
|
+
brew install ffmpeg
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
**1. Clone the repo**
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/your-username/spotify-auto-downloader.git
|
|
47
|
+
cd spotify-auto-downloader
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. Create and activate a virtual environment**
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv
|
|
53
|
+
source .venv/bin/activate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**3. Install the package**
|
|
57
|
+
```bash
|
|
58
|
+
pip install .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Spotify API Credentials
|
|
64
|
+
|
|
65
|
+
This tool uses the Spotify API to look up artist discographies. You need a free client ID and secret.
|
|
66
|
+
|
|
67
|
+
1. Go to [developer.spotify.com/dashboard](https://developer.spotify.com/dashboard)
|
|
68
|
+
2. Click **Create App**
|
|
69
|
+
3. Fill in any name and description, set the redirect URI to `http://localhost`
|
|
70
|
+
4. Open the app and copy the **Client ID** and **Client Secret**
|
|
71
|
+
|
|
72
|
+
**4. Set up your credentials**
|
|
73
|
+
```bash
|
|
74
|
+
cp .env.example .env
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Open `.env` and fill in your credentials:
|
|
78
|
+
```
|
|
79
|
+
SPOTIFY_CLIENT_ID=your_client_id_here
|
|
80
|
+
SPOTIFY_CLIENT_SECRET=your_client_secret_here
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
**5. Create your config file**
|
|
88
|
+
```bash
|
|
89
|
+
cp config.yaml.example config.yaml
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Open `config.yaml` and edit it:
|
|
93
|
+
```yaml
|
|
94
|
+
download_dir: ~/Music/spotify-auto-dl
|
|
95
|
+
|
|
96
|
+
artists:
|
|
97
|
+
- name: Daniel Caesar
|
|
98
|
+
url: https://open.spotify.com/artist/20wkVLutqVOYrc0kxFs7rA
|
|
99
|
+
|
|
100
|
+
schedule:
|
|
101
|
+
interval_hours: 24
|
|
102
|
+
run_on_start: true
|
|
103
|
+
|
|
104
|
+
output:
|
|
105
|
+
format: "{artist}/{album}/{title}.{output-ext}"
|
|
106
|
+
audio_format: mp3
|
|
107
|
+
bitrate: 320k
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
To get an artist URL: open Spotify → artist page → three dots → **Share** → **Copy Artist Link**.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
### Run a one-time sync
|
|
117
|
+
```bash
|
|
118
|
+
spotify-auto-dl sync
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Run continuously on a schedule (daemon mode)
|
|
122
|
+
```bash
|
|
123
|
+
spotify-auto-dl sync --daemon
|
|
124
|
+
```
|
|
125
|
+
Syncs immediately on start (if `run_on_start: true`), then again every `interval_hours` hours. See [Scheduling](#scheduling) for when to use this vs cron.
|
|
126
|
+
|
|
127
|
+
### Add an artist
|
|
128
|
+
```bash
|
|
129
|
+
spotify-auto-dl add-artist "https://open.spotify.com/artist/ARTIST_ID"
|
|
130
|
+
```
|
|
131
|
+
Looks up the artist name from Spotify automatically and adds them to `config.yaml`.
|
|
132
|
+
|
|
133
|
+
### Change the download directory
|
|
134
|
+
```bash
|
|
135
|
+
spotify-auto-dl set-destination /Volumes/MyDrive/Music
|
|
136
|
+
```
|
|
137
|
+
Updates `download_dir` in `config.yaml`. Creates the directory if it doesn't exist.
|
|
138
|
+
|
|
139
|
+
### Use a custom config file path
|
|
140
|
+
```bash
|
|
141
|
+
spotify-auto-dl sync --config /path/to/my-config.yaml
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## File Organization
|
|
147
|
+
|
|
148
|
+
Downloaded files are organized using the `output.format` template in `config.yaml`:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
{download_dir}/
|
|
152
|
+
└── {artist}/
|
|
153
|
+
└── {album}/
|
|
154
|
+
└── {title}.mp3
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
For example:
|
|
158
|
+
```
|
|
159
|
+
~/Music/spotify-auto-dl/
|
|
160
|
+
└── Daniel Caesar/
|
|
161
|
+
└── Freudian/
|
|
162
|
+
├── Get You (feat. Kali Uchis).mp3
|
|
163
|
+
└── Best Part (feat. H.E.R.).mp3
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## State File
|
|
169
|
+
|
|
170
|
+
Downloaded tracks are recorded in `~/.spotify-auto-dl/state.json`. This file is how the tool knows what you already have — it prevents re-downloading songs across runs. Do not delete it unless you want to re-download everything.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Scheduling
|
|
175
|
+
|
|
176
|
+
There are two ways to run this tool automatically: **daemon mode** and **cron**. They accomplish the same goal differently.
|
|
177
|
+
|
|
178
|
+
### Daemon vs Cron
|
|
179
|
+
|
|
180
|
+
| | Daemon (`--daemon`) | Cron |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| How it works | Python process that sleeps and loops forever | OS wakes the command at a set time, it runs once and exits |
|
|
183
|
+
| Requires terminal open | Yes | No |
|
|
184
|
+
| Survives reboot | No | Yes |
|
|
185
|
+
| Easy to stop | `Ctrl+C` | `crontab -e` to remove |
|
|
186
|
+
| Best for | Servers, Docker containers | Personal computers |
|
|
187
|
+
|
|
188
|
+
**For a personal Mac, cron is recommended.** The daemon requires a terminal window to stay open and won't restart if you reboot. Cron is managed by the OS and runs reliably in the background.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### Setting Up Cron (Recommended for Mac)
|
|
193
|
+
|
|
194
|
+
**Step 1 — open your crontab**
|
|
195
|
+
```bash
|
|
196
|
+
crontab -e
|
|
197
|
+
```
|
|
198
|
+
This opens a text editor. If it opens vim, press `i` to start typing.
|
|
199
|
+
|
|
200
|
+
**Step 2 — add this line** (runs every day at 6am)
|
|
201
|
+
```
|
|
202
|
+
0 6 * * * cd /Users/theoaronow/Documents/Spotify-Auto-Downloader && SPOTIFY_CLIENT_ID=your_id SPOTIFY_CLIENT_SECRET=your_secret /Users/theoaronow/Documents/Spotify-Auto-Downloader/.venv/bin/spotify-auto-dl sync --config /Users/theoaronow/Documents/Spotify-Auto-Downloader/config.yaml >> /Users/theoaronow/.spotify-auto-dl/cron.log 2>&1
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Replace `your_id` and `your_secret` with the values from your `.env` file. The credentials must be inline because cron does not load `.env` files automatically.
|
|
206
|
+
|
|
207
|
+
**Step 3 — save and exit**
|
|
208
|
+
|
|
209
|
+
If using vim: press `Esc`, type `:wq`, press `Enter`.
|
|
210
|
+
|
|
211
|
+
**Step 4 — verify it saved**
|
|
212
|
+
```bash
|
|
213
|
+
crontab -l
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Changing the schedule** — edit the `0 6 * * *` part:
|
|
217
|
+
```
|
|
218
|
+
0 8 * * * every day at 8am
|
|
219
|
+
0 6 * * 1 every Monday at 6am
|
|
220
|
+
0 6,18 * * * every day at 6am and 6pm
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Checking the log after it runs:**
|
|
224
|
+
```bash
|
|
225
|
+
cat ~/.spotify-auto-dl/cron.log
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
**`KeyError: SPOTIFY_CLIENT_ID`**
|
|
233
|
+
Your `.env` file is missing or the variable names are misspelled. Make sure `.env` exists in the directory where you run the command.
|
|
234
|
+
|
|
235
|
+
**`FileNotFoundError: config.yaml`**
|
|
236
|
+
Run the command from the project directory, or use `--config /full/path/to/config.yaml`.
|
|
237
|
+
|
|
238
|
+
**A track shows as new every run**
|
|
239
|
+
The same song can have different Spotify IDs across album versions, deluxe editions, and singles. The tool checks by both ID and title to handle this, but edge cases may occur.
|
|
240
|
+
|
|
241
|
+
**Downloads are slow**
|
|
242
|
+
Normal — each track requires finding a matching audio source on YouTube. The first run is the slowest since it downloads everything. Future runs only download new releases.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
spotify_auto_dl/__init__.py
|
|
4
|
+
spotify_auto_dl/config.py
|
|
5
|
+
spotify_auto_dl/downloader.py
|
|
6
|
+
spotify_auto_dl/main.py
|
|
7
|
+
spotify_auto_dl/state.py
|
|
8
|
+
spotify_auto_dl.egg-info/PKG-INFO
|
|
9
|
+
spotify_auto_dl.egg-info/SOURCES.txt
|
|
10
|
+
spotify_auto_dl.egg-info/dependency_links.txt
|
|
11
|
+
spotify_auto_dl.egg-info/entry_points.txt
|
|
12
|
+
spotify_auto_dl.egg-info/requires.txt
|
|
13
|
+
spotify_auto_dl.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spotify_auto_dl
|