trackfetch 2.1.0__tar.gz → 2.2.5__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.
- {trackfetch-2.1.0 → trackfetch-2.2.5}/PKG-INFO +86 -13
- {trackfetch-2.1.0 → trackfetch-2.2.5}/README.md +85 -12
- {trackfetch-2.1.0 → trackfetch-2.2.5}/pyproject.toml +1 -1
- trackfetch-2.2.5/tests/test_config.py +346 -0
- trackfetch-2.2.5/tests/test_inline.py +167 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/tests/test_pipeline.py +5 -5
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.egg-info/PKG-INFO +86 -13
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.egg-info/SOURCES.txt +2 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.py +350 -27
- {trackfetch-2.1.0 → trackfetch-2.2.5}/LICENSE +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/setup.cfg +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/tests/test_spotify.py +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/tests/test_tags_and_files.py +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/tests/test_text.py +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/tests/test_youtube.py +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.egg-info/dependency_links.txt +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.egg-info/entry_points.txt +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.egg-info/requires.txt +0 -0
- {trackfetch-2.1.0 → trackfetch-2.2.5}/trackfetch.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trackfetch
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.5
|
|
4
4
|
Summary: Turn a text file of songs into tagged MP3s: Spotify metadata and cover art, best-match YouTube audio via yt-dlp.
|
|
5
5
|
Author: ByteMe6
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -195,7 +195,7 @@ EOF
|
|
|
195
195
|
trackfetch songs.txt
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
The files land in
|
|
198
|
+
The files land in the current directory.
|
|
199
199
|
|
|
200
200
|
## Playlists from streaming services
|
|
201
201
|
|
|
@@ -213,31 +213,104 @@ trackfetch "My Playlist.txt" -o ~/Music/"My Playlist"
|
|
|
213
213
|
## Usage
|
|
214
214
|
|
|
215
215
|
```text
|
|
216
|
-
trackfetch [-h] [-o OUTPUT] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY]
|
|
216
|
+
trackfetch [-h] [--track ARTIST - TITLE ...] [-o OUTPUT] [--config CONFIG] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY] [--spotify-client-id ID] [--spotify-client-secret SECRET] [INPUT ...]
|
|
217
217
|
```
|
|
218
218
|
|
|
219
219
|
| Option | Default | What it does |
|
|
220
220
|
| --- | --- | --- |
|
|
221
|
-
| `
|
|
222
|
-
|
|
|
223
|
-
| `-
|
|
224
|
-
| `--
|
|
221
|
+
| `INPUT` | required | Text file with one `Artist - Title` per line, or an inline `Artist - Title` track. Can also be set in the [config file](#configuration-file). |
|
|
222
|
+
| `--track` | — | Track to download inline, `Artist - Title`. Can be given several times. |
|
|
223
|
+
| `-o`, `--output` | current directory | Folder to save the files in. It's created if it doesn't exist. |
|
|
224
|
+
| `--config` | auto-detected | Path to a [config file](#configuration-file). |
|
|
225
|
+
| `-f`, `--format` | `m4a` | Audio format: `mp3`, `m4a` or `opus`. See [Audio formats](#audio-formats). |
|
|
226
|
+
| `--title-only` | off | Name files `Title.m4a` instead of `Artist - Title.m4a` |
|
|
225
227
|
| `--delay` | `1.0` | Seconds to wait between songs. Raise it for long lists. |
|
|
228
|
+
| `--spotify-client-id` | `$SPOTIFY_CLIENT_ID` | Spotify API client ID. See [Spotify credentials](#spotify-credentials). |
|
|
229
|
+
| `--spotify-client-secret` | `$SPOTIFY_CLIENT_SECRET` | Spotify API client secret. |
|
|
226
230
|
|
|
227
231
|
```bash
|
|
228
232
|
trackfetch songs.txt -o ~/Music/RoadTrip # save to a specific folder
|
|
229
233
|
trackfetch songs.txt -o /media/usb --title-only # short names for a car stereo
|
|
230
234
|
trackfetch big-list.txt --delay 3 # go easy on rate limits
|
|
231
235
|
trackfetch songs.txt --format opus # original YouTube audio, no re-encode
|
|
236
|
+
trackfetch "Daft Punk - One More Time" # one track, no file needed
|
|
237
|
+
trackfetch "A - B" "C - D" --track "E - F" # a few tracks in one go
|
|
232
238
|
```
|
|
233
239
|
|
|
240
|
+
### Inline tracks
|
|
241
|
+
|
|
242
|
+
Skip the file and name the tracks right on the command line:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
trackfetch "Daft Punk - One More Time"
|
|
246
|
+
trackfetch "Daft Punk - One More Time" "Radiohead - Paranoid Android"
|
|
247
|
+
trackfetch --track "Daft Punk - One More Time" --track "Radiohead - Paranoid Android"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
A file and inline tracks can be mixed: `trackfetch songs.txt --track "Blur - Song 2"`.
|
|
251
|
+
The separator is the same ` - ` as in the file, and the first ` - ` splits the
|
|
252
|
+
artist from the title.
|
|
253
|
+
|
|
254
|
+
### Configuration file
|
|
255
|
+
|
|
256
|
+
Put your defaults in an INI file and forget the flags:
|
|
257
|
+
|
|
258
|
+
```ini
|
|
259
|
+
[trackfetch]
|
|
260
|
+
output = ~/Music/trackfetch
|
|
261
|
+
format = m4a
|
|
262
|
+
title_only = false
|
|
263
|
+
delay = 1.0
|
|
264
|
+
input = ~/songs.txt
|
|
265
|
+
spotify_client_id = your-client-id
|
|
266
|
+
spotify_client_secret = your-client-secret
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Every option above can be set there, under the `[trackfetch]` section. The
|
|
270
|
+
precedence is always the same:
|
|
271
|
+
|
|
272
|
+
1. the command-line flag,
|
|
273
|
+
2. the config file,
|
|
274
|
+
3. the built-in default (the current directory for the output folder).
|
|
275
|
+
|
|
276
|
+
trackfetch looks for the config file in this order: the path given with
|
|
277
|
+
`--config`, the path in `$TRACKFETCH_CONFIG`, `trackfetch.ini` in the current
|
|
278
|
+
directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini` (usually
|
|
279
|
+
`~/.config/trackfetch/trackfetch.ini`) and `~/.trackfetch.ini`.
|
|
280
|
+
|
|
281
|
+
### Spotify credentials
|
|
282
|
+
|
|
283
|
+
trackfetch needs a Spotify client ID and secret to read the metadata. Give
|
|
284
|
+
them in whichever way suits you:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
# in the config file
|
|
288
|
+
[trackfetch]
|
|
289
|
+
spotify_client_id = your-client-id
|
|
290
|
+
spotify_client_secret = your-client-secret
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
# in the environment
|
|
295
|
+
export SPOTIFY_CLIENT_ID='your-client-id'
|
|
296
|
+
export SPOTIFY_CLIENT_SECRET='your-client-secret'
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# on the command line
|
|
301
|
+
trackfetch songs.txt --spotify-client-id your-id --spotify-client-secret your-secret
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The flag wins over the config file, and the config file wins over the
|
|
305
|
+
environment variable.
|
|
306
|
+
|
|
234
307
|
### Audio formats
|
|
235
308
|
|
|
236
309
|
| Format | What you get | Plays on |
|
|
237
310
|
| --- | --- | --- |
|
|
238
|
-
| `
|
|
311
|
+
| `m4a` (default) | YouTube's **original** AAC audio when available, copied without re-encoding | Apple devices, iTunes, most players |
|
|
239
312
|
| `opus` | YouTube's **original** Opus audio, copied without re-encoding. The best quality you can get | most modern players, Android, browsers; not iTunes or older devices |
|
|
240
|
-
| `
|
|
313
|
+
| `mp3` | YouTube's audio **re-encoded** to the best VBR MP3. A second lossy encode, so slightly below the source | everything, including old players and car stereos |
|
|
241
314
|
|
|
242
315
|
All three get the same tags and embedded cover art. `done.txt` doesn't track the format, so use a separate `-o` folder for each format.
|
|
243
316
|
|
|
@@ -254,9 +327,9 @@ The separator is space, hyphen, space (` - `). Lines without it are skipped. See
|
|
|
254
327
|
### Output folder
|
|
255
328
|
|
|
256
329
|
```text
|
|
257
|
-
|
|
258
|
-
├── Daft Punk - One More Time.
|
|
259
|
-
├── Radiohead - Paranoid Android.
|
|
330
|
+
./
|
|
331
|
+
├── Daft Punk - One More Time.m4a
|
|
332
|
+
├── Radiohead - Paranoid Android.m4a
|
|
260
333
|
├── done.txt ← finished songs, skipped on the next run
|
|
261
334
|
└── failed.txt ← "Artist - Title | reason" for each song that failed
|
|
262
335
|
```
|
|
@@ -264,7 +337,7 @@ The separator is space, hyphen, space (` - `). Lines without it are skipped. See
|
|
|
264
337
|
**Retry failures** by stripping the reasons from `failed.txt` and running it again:
|
|
265
338
|
|
|
266
339
|
```bash
|
|
267
|
-
cut -d '|' -f 1
|
|
340
|
+
cut -d '|' -f 1 ./failed.txt > retry.txt
|
|
268
341
|
trackfetch retry.txt
|
|
269
342
|
```
|
|
270
343
|
|
|
@@ -164,7 +164,7 @@ EOF
|
|
|
164
164
|
trackfetch songs.txt
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
The files land in
|
|
167
|
+
The files land in the current directory.
|
|
168
168
|
|
|
169
169
|
## Playlists from streaming services
|
|
170
170
|
|
|
@@ -182,31 +182,104 @@ trackfetch "My Playlist.txt" -o ~/Music/"My Playlist"
|
|
|
182
182
|
## Usage
|
|
183
183
|
|
|
184
184
|
```text
|
|
185
|
-
trackfetch [-h] [-o OUTPUT] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY]
|
|
185
|
+
trackfetch [-h] [--track ARTIST - TITLE ...] [-o OUTPUT] [--config CONFIG] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY] [--spotify-client-id ID] [--spotify-client-secret SECRET] [INPUT ...]
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
| Option | Default | What it does |
|
|
189
189
|
| --- | --- | --- |
|
|
190
|
-
| `
|
|
191
|
-
|
|
|
192
|
-
| `-
|
|
193
|
-
| `--
|
|
190
|
+
| `INPUT` | required | Text file with one `Artist - Title` per line, or an inline `Artist - Title` track. Can also be set in the [config file](#configuration-file). |
|
|
191
|
+
| `--track` | — | Track to download inline, `Artist - Title`. Can be given several times. |
|
|
192
|
+
| `-o`, `--output` | current directory | Folder to save the files in. It's created if it doesn't exist. |
|
|
193
|
+
| `--config` | auto-detected | Path to a [config file](#configuration-file). |
|
|
194
|
+
| `-f`, `--format` | `m4a` | Audio format: `mp3`, `m4a` or `opus`. See [Audio formats](#audio-formats). |
|
|
195
|
+
| `--title-only` | off | Name files `Title.m4a` instead of `Artist - Title.m4a` |
|
|
194
196
|
| `--delay` | `1.0` | Seconds to wait between songs. Raise it for long lists. |
|
|
197
|
+
| `--spotify-client-id` | `$SPOTIFY_CLIENT_ID` | Spotify API client ID. See [Spotify credentials](#spotify-credentials). |
|
|
198
|
+
| `--spotify-client-secret` | `$SPOTIFY_CLIENT_SECRET` | Spotify API client secret. |
|
|
195
199
|
|
|
196
200
|
```bash
|
|
197
201
|
trackfetch songs.txt -o ~/Music/RoadTrip # save to a specific folder
|
|
198
202
|
trackfetch songs.txt -o /media/usb --title-only # short names for a car stereo
|
|
199
203
|
trackfetch big-list.txt --delay 3 # go easy on rate limits
|
|
200
204
|
trackfetch songs.txt --format opus # original YouTube audio, no re-encode
|
|
205
|
+
trackfetch "Daft Punk - One More Time" # one track, no file needed
|
|
206
|
+
trackfetch "A - B" "C - D" --track "E - F" # a few tracks in one go
|
|
201
207
|
```
|
|
202
208
|
|
|
209
|
+
### Inline tracks
|
|
210
|
+
|
|
211
|
+
Skip the file and name the tracks right on the command line:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
trackfetch "Daft Punk - One More Time"
|
|
215
|
+
trackfetch "Daft Punk - One More Time" "Radiohead - Paranoid Android"
|
|
216
|
+
trackfetch --track "Daft Punk - One More Time" --track "Radiohead - Paranoid Android"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
A file and inline tracks can be mixed: `trackfetch songs.txt --track "Blur - Song 2"`.
|
|
220
|
+
The separator is the same ` - ` as in the file, and the first ` - ` splits the
|
|
221
|
+
artist from the title.
|
|
222
|
+
|
|
223
|
+
### Configuration file
|
|
224
|
+
|
|
225
|
+
Put your defaults in an INI file and forget the flags:
|
|
226
|
+
|
|
227
|
+
```ini
|
|
228
|
+
[trackfetch]
|
|
229
|
+
output = ~/Music/trackfetch
|
|
230
|
+
format = m4a
|
|
231
|
+
title_only = false
|
|
232
|
+
delay = 1.0
|
|
233
|
+
input = ~/songs.txt
|
|
234
|
+
spotify_client_id = your-client-id
|
|
235
|
+
spotify_client_secret = your-client-secret
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Every option above can be set there, under the `[trackfetch]` section. The
|
|
239
|
+
precedence is always the same:
|
|
240
|
+
|
|
241
|
+
1. the command-line flag,
|
|
242
|
+
2. the config file,
|
|
243
|
+
3. the built-in default (the current directory for the output folder).
|
|
244
|
+
|
|
245
|
+
trackfetch looks for the config file in this order: the path given with
|
|
246
|
+
`--config`, the path in `$TRACKFETCH_CONFIG`, `trackfetch.ini` in the current
|
|
247
|
+
directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini` (usually
|
|
248
|
+
`~/.config/trackfetch/trackfetch.ini`) and `~/.trackfetch.ini`.
|
|
249
|
+
|
|
250
|
+
### Spotify credentials
|
|
251
|
+
|
|
252
|
+
trackfetch needs a Spotify client ID and secret to read the metadata. Give
|
|
253
|
+
them in whichever way suits you:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# in the config file
|
|
257
|
+
[trackfetch]
|
|
258
|
+
spotify_client_id = your-client-id
|
|
259
|
+
spotify_client_secret = your-client-secret
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# in the environment
|
|
264
|
+
export SPOTIFY_CLIENT_ID='your-client-id'
|
|
265
|
+
export SPOTIFY_CLIENT_SECRET='your-client-secret'
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# on the command line
|
|
270
|
+
trackfetch songs.txt --spotify-client-id your-id --spotify-client-secret your-secret
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The flag wins over the config file, and the config file wins over the
|
|
274
|
+
environment variable.
|
|
275
|
+
|
|
203
276
|
### Audio formats
|
|
204
277
|
|
|
205
278
|
| Format | What you get | Plays on |
|
|
206
279
|
| --- | --- | --- |
|
|
207
|
-
| `
|
|
280
|
+
| `m4a` (default) | YouTube's **original** AAC audio when available, copied without re-encoding | Apple devices, iTunes, most players |
|
|
208
281
|
| `opus` | YouTube's **original** Opus audio, copied without re-encoding. The best quality you can get | most modern players, Android, browsers; not iTunes or older devices |
|
|
209
|
-
| `
|
|
282
|
+
| `mp3` | YouTube's audio **re-encoded** to the best VBR MP3. A second lossy encode, so slightly below the source | everything, including old players and car stereos |
|
|
210
283
|
|
|
211
284
|
All three get the same tags and embedded cover art. `done.txt` doesn't track the format, so use a separate `-o` folder for each format.
|
|
212
285
|
|
|
@@ -223,9 +296,9 @@ The separator is space, hyphen, space (` - `). Lines without it are skipped. See
|
|
|
223
296
|
### Output folder
|
|
224
297
|
|
|
225
298
|
```text
|
|
226
|
-
|
|
227
|
-
├── Daft Punk - One More Time.
|
|
228
|
-
├── Radiohead - Paranoid Android.
|
|
299
|
+
./
|
|
300
|
+
├── Daft Punk - One More Time.m4a
|
|
301
|
+
├── Radiohead - Paranoid Android.m4a
|
|
229
302
|
├── done.txt ← finished songs, skipped on the next run
|
|
230
303
|
└── failed.txt ← "Artist - Title | reason" for each song that failed
|
|
231
304
|
```
|
|
@@ -233,7 +306,7 @@ The separator is space, hyphen, space (` - `). Lines without it are skipped. See
|
|
|
233
306
|
**Retry failures** by stripping the reasons from `failed.txt` and running it again:
|
|
234
307
|
|
|
235
308
|
```bash
|
|
236
|
-
cut -d '|' -f 1
|
|
309
|
+
cut -d '|' -f 1 ./failed.txt > retry.txt
|
|
237
310
|
trackfetch retry.txt
|
|
238
311
|
```
|
|
239
312
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trackfetch"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.5"
|
|
8
8
|
description = "Turn a text file of songs into tagged MP3s: Spotify metadata and cover art, best-match YouTube audio via yt-dlp."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import configparser
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from conftest import FIXTURES, make_track
|
|
9
|
+
|
|
10
|
+
import trackfetch as m
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def pipeline(monkeypatch):
|
|
15
|
+
state = {
|
|
16
|
+
"track": make_track("One More Time", ["Daft Punk"], album="Discovery", track_number=1),
|
|
17
|
+
"video": {"title": "Daft Punk - One More Time (Official Audio)", "id": "abc"},
|
|
18
|
+
"formats": [],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def fake_download_audio(url, temp_dir, audio_format="mp3"):
|
|
22
|
+
state["formats"].append(audio_format)
|
|
23
|
+
path = temp_dir / f"audio.{audio_format}"
|
|
24
|
+
if audio_format == "mp3":
|
|
25
|
+
path.write_bytes(b"\x00" * 64)
|
|
26
|
+
else:
|
|
27
|
+
shutil.copy(FIXTURES / f"silence.{audio_format}", path)
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
def fake_download_cover(url, dest):
|
|
31
|
+
dest.write_bytes(b"\xff\xd8\xff\xe0jpeg")
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(m, "find_spotify_track", lambda sp, a, t: state["track"])
|
|
34
|
+
monkeypatch.setattr(m, "find_youtube_video", lambda a, t: state["video"])
|
|
35
|
+
monkeypatch.setattr(m, "download_audio", fake_download_audio)
|
|
36
|
+
monkeypatch.setattr(m, "download_cover", fake_download_cover)
|
|
37
|
+
monkeypatch.setattr(m, "get_spotify", lambda *a, **k: object())
|
|
38
|
+
monkeypatch.setattr(m.time, "sleep", lambda s: None)
|
|
39
|
+
return state
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_cli(monkeypatch, *args):
|
|
43
|
+
monkeypatch.setattr(sys, "argv", ["trackfetch", *map(str, args)])
|
|
44
|
+
m.main()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_config(path: Path, body: str) -> Path:
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
path.write_text(body, encoding="utf-8")
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestConfigLookup:
|
|
54
|
+
def test_no_config_file_returns_empty_settings(self, tmp_path, monkeypatch):
|
|
55
|
+
monkeypatch.chdir(tmp_path)
|
|
56
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "empty"))
|
|
57
|
+
monkeypatch.delenv("TRACKFETCH_CONFIG", raising=False)
|
|
58
|
+
monkeypatch.setattr(m.Path, "home", classmethod(lambda cls: tmp_path / "home"))
|
|
59
|
+
|
|
60
|
+
settings, path = m.load_config()
|
|
61
|
+
|
|
62
|
+
assert settings == {}
|
|
63
|
+
assert path is None
|
|
64
|
+
|
|
65
|
+
def test_explicit_config_file_is_used(self, tmp_path):
|
|
66
|
+
config = write_config(tmp_path / "custom.ini", "[trackfetch]\noutput = /tmp/x\n")
|
|
67
|
+
|
|
68
|
+
settings, path = m.load_config(config)
|
|
69
|
+
|
|
70
|
+
assert settings["output"] == "/tmp/x"
|
|
71
|
+
assert path == config
|
|
72
|
+
|
|
73
|
+
def test_missing_explicit_config_file_raises(self, tmp_path):
|
|
74
|
+
with pytest.raises(FileNotFoundError):
|
|
75
|
+
m.load_config(tmp_path / "nope.ini")
|
|
76
|
+
|
|
77
|
+
def test_config_in_current_directory(self, tmp_path, monkeypatch):
|
|
78
|
+
monkeypatch.chdir(tmp_path)
|
|
79
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
80
|
+
monkeypatch.delenv("TRACKFETCH_CONFIG", raising=False)
|
|
81
|
+
monkeypatch.setattr(m.Path, "home", classmethod(lambda cls: tmp_path / "home"))
|
|
82
|
+
write_config(tmp_path / m.CONFIG_FILENAME, "[trackfetch]\noutput = /tmp/from-cwd\n")
|
|
83
|
+
|
|
84
|
+
settings, path = m.load_config()
|
|
85
|
+
|
|
86
|
+
assert settings["output"] == "/tmp/from-cwd"
|
|
87
|
+
assert path == tmp_path / m.CONFIG_FILENAME
|
|
88
|
+
|
|
89
|
+
def test_config_in_xdg_directory(self, tmp_path, monkeypatch):
|
|
90
|
+
monkeypatch.chdir(tmp_path)
|
|
91
|
+
monkeypatch.delenv("TRACKFETCH_CONFIG", raising=False)
|
|
92
|
+
monkeypatch.setattr(m.Path, "home", classmethod(lambda cls: tmp_path / "home"))
|
|
93
|
+
xdg = tmp_path / "xdg"
|
|
94
|
+
write_config(xdg / "trackfetch" / m.CONFIG_FILENAME, "[trackfetch]\noutput = /tmp/xdg\n")
|
|
95
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg))
|
|
96
|
+
|
|
97
|
+
settings, path = m.load_config()
|
|
98
|
+
|
|
99
|
+
assert settings["output"] == "/tmp/xdg"
|
|
100
|
+
|
|
101
|
+
def test_env_var_points_to_config(self, tmp_path, monkeypatch):
|
|
102
|
+
config = write_config(tmp_path / "env.ini", "[trackfetch]\noutput = /tmp/env\n")
|
|
103
|
+
monkeypatch.setenv("TRACKFETCH_CONFIG", str(config))
|
|
104
|
+
|
|
105
|
+
settings, path = m.load_config()
|
|
106
|
+
|
|
107
|
+
assert settings["output"] == "/tmp/env"
|
|
108
|
+
|
|
109
|
+
def test_invalid_config_file_raises(self, tmp_path):
|
|
110
|
+
config = write_config(tmp_path / "bad.ini", "not an ini file at all\n")
|
|
111
|
+
|
|
112
|
+
with pytest.raises(configparser.Error):
|
|
113
|
+
m.load_config(config)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestResolveOutput:
|
|
117
|
+
def test_flag_wins_over_config(self, tmp_path):
|
|
118
|
+
settings = {"output": str(tmp_path / "from-config")}
|
|
119
|
+
|
|
120
|
+
result = m.resolve_output(tmp_path / "from-flag", settings)
|
|
121
|
+
|
|
122
|
+
assert result == tmp_path / "from-flag"
|
|
123
|
+
|
|
124
|
+
def test_config_used_when_no_flag(self, tmp_path):
|
|
125
|
+
settings = {"output": str(tmp_path / "from-config")}
|
|
126
|
+
|
|
127
|
+
result = m.resolve_output(None, settings)
|
|
128
|
+
|
|
129
|
+
assert result == tmp_path / "from-config"
|
|
130
|
+
|
|
131
|
+
def test_current_directory_is_the_last_resort(self, tmp_path, monkeypatch):
|
|
132
|
+
monkeypatch.chdir(tmp_path)
|
|
133
|
+
|
|
134
|
+
assert m.resolve_output(None, {}) == tmp_path
|
|
135
|
+
assert m.resolve_output(None, {"output": " "}) == tmp_path
|
|
136
|
+
|
|
137
|
+
def test_tilde_is_expanded(self):
|
|
138
|
+
result = m.resolve_output(None, {"output": "~/Music/x"})
|
|
139
|
+
|
|
140
|
+
assert result == Path.home() / "Music" / "x"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestSpotifyCredentials:
|
|
144
|
+
def test_env_is_used_when_config_is_silent(self, monkeypatch):
|
|
145
|
+
monkeypatch.setenv("SPOTIFY_CLIENT_ID", "env-id")
|
|
146
|
+
monkeypatch.setenv("SPOTIFY_CLIENT_SECRET", "env-secret")
|
|
147
|
+
|
|
148
|
+
client_id, client_secret = m.spotify_credentials()
|
|
149
|
+
|
|
150
|
+
assert (client_id, client_secret) == ("env-id", "env-secret")
|
|
151
|
+
|
|
152
|
+
def test_config_wins_over_env(self, monkeypatch):
|
|
153
|
+
monkeypatch.setenv("SPOTIFY_CLIENT_ID", "env-id")
|
|
154
|
+
monkeypatch.setenv("SPOTIFY_CLIENT_SECRET", "env-secret")
|
|
155
|
+
settings = {
|
|
156
|
+
"spotify_client_id": "config-id",
|
|
157
|
+
"spotify_client_secret": "config-secret",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
parser = argparse.ArgumentParser()
|
|
161
|
+
parser.add_argument("--spotify-client-id", default=None)
|
|
162
|
+
parser.add_argument("--spotify-client-secret", default=None)
|
|
163
|
+
args = parser.parse_args([])
|
|
164
|
+
m.apply_config(args, parser, settings)
|
|
165
|
+
client_id, client_secret = m.spotify_credentials(args)
|
|
166
|
+
|
|
167
|
+
assert (client_id, client_secret) == ("config-id", "config-secret")
|
|
168
|
+
|
|
169
|
+
def test_flag_wins_over_config_and_env(self, monkeypatch):
|
|
170
|
+
monkeypatch.setenv("SPOTIFY_CLIENT_ID", "env-id")
|
|
171
|
+
monkeypatch.setenv("SPOTIFY_CLIENT_SECRET", "env-secret")
|
|
172
|
+
parser = argparse.ArgumentParser()
|
|
173
|
+
parser.add_argument("--spotify-client-id", default=None)
|
|
174
|
+
parser.add_argument("--spotify-client-secret", default=None)
|
|
175
|
+
args = parser.parse_args(
|
|
176
|
+
["--spotify-client-id", "flag-id", "--spotify-client-secret", "flag-secret"]
|
|
177
|
+
)
|
|
178
|
+
settings = {
|
|
179
|
+
"spotify_client_id": "config-id",
|
|
180
|
+
"spotify_client_secret": "config-secret",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
m.apply_config(args, parser, settings)
|
|
184
|
+
|
|
185
|
+
client_id, client_secret = m.spotify_credentials(args)
|
|
186
|
+
|
|
187
|
+
assert (client_id, client_secret) == ("flag-id", "flag-secret")
|
|
188
|
+
|
|
189
|
+
def test_missing_credentials_exit(self, monkeypatch, capsys):
|
|
190
|
+
monkeypatch.delenv("SPOTIFY_CLIENT_ID", raising=False)
|
|
191
|
+
monkeypatch.delenv("SPOTIFY_CLIENT_SECRET", raising=False)
|
|
192
|
+
|
|
193
|
+
with pytest.raises(SystemExit) as exc:
|
|
194
|
+
m.get_spotify()
|
|
195
|
+
|
|
196
|
+
assert exc.value.code == 1
|
|
197
|
+
assert "Spotify credentials not found" in capsys.readouterr().out
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestMainWithConfig:
|
|
201
|
+
def test_config_output_is_used(self, pipeline, monkeypatch, tmp_path):
|
|
202
|
+
songs = tmp_path / "songs.txt"
|
|
203
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
204
|
+
out = tmp_path / "from-config"
|
|
205
|
+
config = write_config(tmp_path / "cfg.ini", f"[trackfetch]\noutput = {out}\n")
|
|
206
|
+
|
|
207
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
208
|
+
|
|
209
|
+
assert (out / "Daft Punk - One More Time.m4a").exists()
|
|
210
|
+
|
|
211
|
+
def test_flag_overrides_config(self, pipeline, monkeypatch, tmp_path):
|
|
212
|
+
songs = tmp_path / "songs.txt"
|
|
213
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
214
|
+
config_out = tmp_path / "from-config"
|
|
215
|
+
flag_out = tmp_path / "from-flag"
|
|
216
|
+
config = write_config(tmp_path / "cfg.ini", f"[trackfetch]\noutput = {config_out}\n")
|
|
217
|
+
|
|
218
|
+
run_cli(monkeypatch, songs, "--config", config, "-o", flag_out)
|
|
219
|
+
|
|
220
|
+
assert (flag_out / "Daft Punk - One More Time.m4a").exists()
|
|
221
|
+
assert not config_out.exists()
|
|
222
|
+
|
|
223
|
+
def test_current_directory_is_used_without_config(self, pipeline, monkeypatch, tmp_path):
|
|
224
|
+
songs = tmp_path / "songs.txt"
|
|
225
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
226
|
+
workdir = tmp_path / "work"
|
|
227
|
+
workdir.mkdir()
|
|
228
|
+
monkeypatch.chdir(workdir)
|
|
229
|
+
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "xdg"))
|
|
230
|
+
monkeypatch.delenv("TRACKFETCH_CONFIG", raising=False)
|
|
231
|
+
monkeypatch.setattr(m.Path, "home", classmethod(lambda cls: tmp_path / "home"))
|
|
232
|
+
|
|
233
|
+
run_cli(monkeypatch, songs)
|
|
234
|
+
|
|
235
|
+
assert (workdir / "Daft Punk - One More Time.m4a").exists()
|
|
236
|
+
|
|
237
|
+
def test_invalid_config_file_exits(self, monkeypatch, tmp_path, capsys):
|
|
238
|
+
songs = tmp_path / "songs.txt"
|
|
239
|
+
songs.write_text("A - B\n", encoding="utf-8")
|
|
240
|
+
config = write_config(tmp_path / "bad.ini", "not an ini file at all\n")
|
|
241
|
+
|
|
242
|
+
with pytest.raises(SystemExit) as exc:
|
|
243
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
244
|
+
|
|
245
|
+
assert exc.value.code == 1
|
|
246
|
+
assert "Invalid config file" in capsys.readouterr().out
|
|
247
|
+
|
|
248
|
+
def test_invalid_value_in_config_exits(self, monkeypatch, tmp_path, capsys):
|
|
249
|
+
songs = tmp_path / "songs.txt"
|
|
250
|
+
songs.write_text("A - B\n", encoding="utf-8")
|
|
251
|
+
config = write_config(tmp_path / "cfg.ini", "[trackfetch]\ndelay = abc\n")
|
|
252
|
+
|
|
253
|
+
with pytest.raises(SystemExit) as exc:
|
|
254
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
255
|
+
|
|
256
|
+
assert exc.value.code == 1
|
|
257
|
+
assert "Invalid value for 'delay' in config file" in capsys.readouterr().out
|
|
258
|
+
|
|
259
|
+
def test_missing_input_file_exits(self, monkeypatch, tmp_path, capsys):
|
|
260
|
+
with pytest.raises(SystemExit) as exc:
|
|
261
|
+
run_cli(monkeypatch)
|
|
262
|
+
|
|
263
|
+
assert exc.value.code == 1
|
|
264
|
+
assert "No input" in capsys.readouterr().out
|
|
265
|
+
|
|
266
|
+
def test_input_can_come_from_the_config(self, pipeline, monkeypatch, tmp_path):
|
|
267
|
+
songs = tmp_path / "songs.txt"
|
|
268
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
269
|
+
out = tmp_path / "o"
|
|
270
|
+
config = write_config(
|
|
271
|
+
tmp_path / "cfg.ini", f"[trackfetch]\ninput = {songs}\noutput = {out}\n"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
run_cli(monkeypatch, "--config", config)
|
|
275
|
+
|
|
276
|
+
assert (out / "Daft Punk - One More Time.m4a").exists()
|
|
277
|
+
|
|
278
|
+
def test_missing_explicit_config_exits(self, monkeypatch, tmp_path, capsys):
|
|
279
|
+
songs = tmp_path / "songs.txt"
|
|
280
|
+
songs.write_text("A - B\n", encoding="utf-8")
|
|
281
|
+
|
|
282
|
+
with pytest.raises(SystemExit) as exc:
|
|
283
|
+
run_cli(monkeypatch, songs, "--config", tmp_path / "nope.ini")
|
|
284
|
+
|
|
285
|
+
assert exc.value.code == 1
|
|
286
|
+
assert "Config file not found" in capsys.readouterr().out
|
|
287
|
+
|
|
288
|
+
def test_format_from_config(self, pipeline, monkeypatch, tmp_path):
|
|
289
|
+
songs = tmp_path / "songs.txt"
|
|
290
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
291
|
+
out = tmp_path / "o"
|
|
292
|
+
config = write_config(
|
|
293
|
+
tmp_path / "cfg.ini", f"[trackfetch]\noutput = {out}\nformat = opus\n"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
297
|
+
|
|
298
|
+
assert (out / "Daft Punk - One More Time.opus").exists()
|
|
299
|
+
assert pipeline["formats"] == ["opus"]
|
|
300
|
+
|
|
301
|
+
def test_unknown_format_in_config_exits(self, monkeypatch, tmp_path, capsys):
|
|
302
|
+
songs = tmp_path / "songs.txt"
|
|
303
|
+
songs.write_text("A - B\n", encoding="utf-8")
|
|
304
|
+
config = write_config(tmp_path / "cfg.ini", "[trackfetch]\nformat = flac\n")
|
|
305
|
+
|
|
306
|
+
with pytest.raises(SystemExit) as exc:
|
|
307
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
308
|
+
|
|
309
|
+
assert exc.value.code == 1
|
|
310
|
+
assert "Unknown value for 'format' in config file" in capsys.readouterr().out
|
|
311
|
+
|
|
312
|
+
def test_title_only_from_config(self, pipeline, monkeypatch, tmp_path):
|
|
313
|
+
songs = tmp_path / "songs.txt"
|
|
314
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
315
|
+
out = tmp_path / "o"
|
|
316
|
+
config = write_config(
|
|
317
|
+
tmp_path / "cfg.ini", f"[trackfetch]\noutput = {out}\ntitle_only = true\n"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
321
|
+
|
|
322
|
+
assert (out / "One More Time.m4a").exists()
|
|
323
|
+
|
|
324
|
+
def test_delay_from_config(self, pipeline, monkeypatch, tmp_path):
|
|
325
|
+
songs = tmp_path / "songs.txt"
|
|
326
|
+
songs.write_text("A - 1\nA - 2\n", encoding="utf-8")
|
|
327
|
+
out = tmp_path / "o"
|
|
328
|
+
config = write_config(tmp_path / "cfg.ini", f"[trackfetch]\noutput = {out}\ndelay = 2.5\n")
|
|
329
|
+
sleeps = []
|
|
330
|
+
monkeypatch.setattr(m.time, "sleep", sleeps.append)
|
|
331
|
+
|
|
332
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
333
|
+
|
|
334
|
+
assert sleeps == [2.5]
|
|
335
|
+
|
|
336
|
+
def test_config_path_is_reported(self, pipeline, monkeypatch, tmp_path, capsys):
|
|
337
|
+
songs = tmp_path / "songs.txt"
|
|
338
|
+
songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
|
|
339
|
+
out = tmp_path / "o"
|
|
340
|
+
config = write_config(tmp_path / "cfg.ini", f"[trackfetch]\noutput = {out}\n")
|
|
341
|
+
|
|
342
|
+
run_cli(monkeypatch, songs, "--config", config)
|
|
343
|
+
log = capsys.readouterr().out
|
|
344
|
+
|
|
345
|
+
assert f"Config: {config}" in log
|
|
346
|
+
assert f"Output: {out}" in log
|