trackfetch 2.2.0__tar.gz → 2.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackfetch
3
- Version: 2.2.0
3
+ Version: 2.2.6
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
@@ -213,12 +213,13 @@ trackfetch "My Playlist.txt" -o ~/Music/"My Playlist"
213
213
  ## Usage
214
214
 
215
215
  ```text
216
- trackfetch [-h] [-o OUTPUT] [--config CONFIG] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY] [--spotify-client-id ID] [--spotify-client-secret SECRET] [input]
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
- | `input` | required | Text file with one `Artist - Title` per line. Can also be set in the [config file](#configuration-file). |
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. |
222
223
  | `-o`, `--output` | current directory | Folder to save the files in. It's created if it doesn't exist. |
223
224
  | `--config` | auto-detected | Path to a [config file](#configuration-file). |
224
225
  | `-f`, `--format` | `m4a` | Audio format: `mp3`, `m4a` or `opus`. See [Audio formats](#audio-formats). |
@@ -232,8 +233,24 @@ trackfetch songs.txt -o ~/Music/RoadTrip # save to a specific folder
232
233
  trackfetch songs.txt -o /media/usb --title-only # short names for a car stereo
233
234
  trackfetch big-list.txt --delay 3 # go easy on rate limits
234
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
235
238
  ```
236
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
+
237
254
  ### Configuration file
238
255
 
239
256
  Put your defaults in an INI file and forget the flags:
@@ -249,8 +266,9 @@ spotify_client_id = your-client-id
249
266
  spotify_client_secret = your-client-secret
250
267
  ```
251
268
 
252
- Every option above can be set there, under the `[trackfetch]` section. The
253
- precedence is always the same:
269
+ Every option above can be set there, under the `[trackfetch]` section, except
270
+ `--track`, which only makes sense on the command line. The precedence is
271
+ always the same:
254
272
 
255
273
  1. the command-line flag,
256
274
  2. the config file,
@@ -258,8 +276,8 @@ precedence is always the same:
258
276
 
259
277
  trackfetch looks for the config file in this order: the path given with
260
278
  `--config`, the path in `$TRACKFETCH_CONFIG`, `trackfetch.ini` in the current
261
- directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini` (usually
262
- `~/.config/trackfetch/trackfetch.ini`) and `~/.trackfetch.ini`.
279
+ directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini`,
280
+ `~/.config/trackfetch/trackfetch.ini` and `~/.trackfetch.ini`.
263
281
 
264
282
  ### Spotify credentials
265
283
 
@@ -305,7 +323,8 @@ Daft Punk - One More Time
305
323
  Sufjan Stevens - Mystery of Love - Remastered ← only the first " - " separates artist and title
306
324
  ```
307
325
 
308
- The separator is space, hyphen, space (` - `). Lines without it are skipped. See [`examples/songs.txt`](https://github.com/ByteMe6/trackfetch/blob/master/examples/songs.txt).
326
+ The separator is space, hyphen, space (` - `). Lines without it are skipped,
327
+ and trackfetch prints how many lines it skipped and which ones they were. See [`examples/songs.txt`](https://github.com/ByteMe6/trackfetch/blob/master/examples/songs.txt).
309
328
 
310
329
  ### Output folder
311
330
 
@@ -182,12 +182,13 @@ trackfetch "My Playlist.txt" -o ~/Music/"My Playlist"
182
182
  ## Usage
183
183
 
184
184
  ```text
185
- trackfetch [-h] [-o OUTPUT] [--config CONFIG] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY] [--spotify-client-id ID] [--spotify-client-secret SECRET] [input]
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
- | `input` | required | Text file with one `Artist - Title` per line. Can also be set in the [config file](#configuration-file). |
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. |
191
192
  | `-o`, `--output` | current directory | Folder to save the files in. It's created if it doesn't exist. |
192
193
  | `--config` | auto-detected | Path to a [config file](#configuration-file). |
193
194
  | `-f`, `--format` | `m4a` | Audio format: `mp3`, `m4a` or `opus`. See [Audio formats](#audio-formats). |
@@ -201,8 +202,24 @@ trackfetch songs.txt -o ~/Music/RoadTrip # save to a specific folder
201
202
  trackfetch songs.txt -o /media/usb --title-only # short names for a car stereo
202
203
  trackfetch big-list.txt --delay 3 # go easy on rate limits
203
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
204
207
  ```
205
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
+
206
223
  ### Configuration file
207
224
 
208
225
  Put your defaults in an INI file and forget the flags:
@@ -218,8 +235,9 @@ spotify_client_id = your-client-id
218
235
  spotify_client_secret = your-client-secret
219
236
  ```
220
237
 
221
- Every option above can be set there, under the `[trackfetch]` section. The
222
- precedence is always the same:
238
+ Every option above can be set there, under the `[trackfetch]` section, except
239
+ `--track`, which only makes sense on the command line. The precedence is
240
+ always the same:
223
241
 
224
242
  1. the command-line flag,
225
243
  2. the config file,
@@ -227,8 +245,8 @@ precedence is always the same:
227
245
 
228
246
  trackfetch looks for the config file in this order: the path given with
229
247
  `--config`, the path in `$TRACKFETCH_CONFIG`, `trackfetch.ini` in the current
230
- directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini` (usually
231
- `~/.config/trackfetch/trackfetch.ini`) and `~/.trackfetch.ini`.
248
+ directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini`,
249
+ `~/.config/trackfetch/trackfetch.ini` and `~/.trackfetch.ini`.
232
250
 
233
251
  ### Spotify credentials
234
252
 
@@ -274,7 +292,8 @@ Daft Punk - One More Time
274
292
  Sufjan Stevens - Mystery of Love - Remastered ← only the first " - " separates artist and title
275
293
  ```
276
294
 
277
- The separator is space, hyphen, space (` - `). Lines without it are skipped. See [`examples/songs.txt`](https://github.com/ByteMe6/trackfetch/blob/master/examples/songs.txt).
295
+ The separator is space, hyphen, space (` - `). Lines without it are skipped,
296
+ and trackfetch prints how many lines it skipped and which ones they were. See [`examples/songs.txt`](https://github.com/ByteMe6/trackfetch/blob/master/examples/songs.txt).
278
297
 
279
298
  ### Output folder
280
299
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trackfetch"
7
- version = "2.2.0"
7
+ version = "2.2.6"
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"
@@ -261,7 +261,7 @@ class TestMainWithConfig:
261
261
  run_cli(monkeypatch)
262
262
 
263
263
  assert exc.value.code == 1
264
- assert "No input file" in capsys.readouterr().out
264
+ assert "No input" in capsys.readouterr().out
265
265
 
266
266
  def test_input_can_come_from_the_config(self, pipeline, monkeypatch, tmp_path):
267
267
  songs = tmp_path / "songs.txt"
@@ -0,0 +1,208 @@
1
+ import shutil
2
+ import sys
3
+
4
+ import pytest
5
+ from conftest import FIXTURES, make_track
6
+
7
+ import trackfetch as m
8
+
9
+
10
+ @pytest.fixture
11
+ def pipeline(monkeypatch):
12
+ state = {
13
+ "track": make_track("One More Time", ["Daft Punk"], album="Discovery", track_number=1),
14
+ "video": {"title": "Daft Punk - One More Time (Official Audio)", "id": "abc"},
15
+ "searched": [],
16
+ }
17
+
18
+ def fake_find_spotify(sp, artist, title):
19
+ state["searched"].append((artist, title))
20
+ return state["track"]
21
+
22
+ def fake_download_audio(url, temp_dir, audio_format="mp3"):
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
+ monkeypatch.setattr(m, "find_spotify_track", fake_find_spotify)
31
+ monkeypatch.setattr(m, "find_youtube_video", lambda a, t: state["video"])
32
+ monkeypatch.setattr(m, "download_audio", fake_download_audio)
33
+ monkeypatch.setattr(m, "download_cover", lambda url, dest: dest.write_bytes(b"x"))
34
+ monkeypatch.setattr(m, "get_spotify", lambda *a, **k: object())
35
+ monkeypatch.setattr(m.time, "sleep", lambda s: None)
36
+ return state
37
+
38
+
39
+ def run_cli(monkeypatch, *args):
40
+ monkeypatch.setattr(sys, "argv", ["trackfetch", *map(str, args)])
41
+ m.main()
42
+
43
+
44
+ class TestInlineTracks:
45
+ def test_single_inline_track(self, pipeline, monkeypatch, tmp_path):
46
+ out = tmp_path / "o"
47
+
48
+ run_cli(monkeypatch, "Daft Punk - One More Time", "-o", out)
49
+
50
+ assert pipeline["searched"] == [("Daft Punk", "One More Time")]
51
+ assert (out / "Daft Punk - One More Time.m4a").exists()
52
+
53
+ def test_several_inline_tracks(self, pipeline, monkeypatch, tmp_path):
54
+ out = tmp_path / "o"
55
+
56
+ run_cli(
57
+ monkeypatch,
58
+ "Daft Punk - One More Time",
59
+ "Radiohead - Paranoid Android",
60
+ "-o",
61
+ out,
62
+ )
63
+
64
+ assert [artist for artist, _ in pipeline["searched"]] == [
65
+ "Daft Punk",
66
+ "Radiohead",
67
+ ]
68
+
69
+ def test_track_flag(self, pipeline, monkeypatch, tmp_path):
70
+ out = tmp_path / "o"
71
+
72
+ run_cli(
73
+ monkeypatch,
74
+ "--track",
75
+ "Daft Punk - One More Time",
76
+ "--track",
77
+ "Radiohead - Paranoid Android",
78
+ "-o",
79
+ out,
80
+ )
81
+
82
+ assert len(pipeline["searched"]) == 2
83
+
84
+ def test_track_flag_with_several_values(self, pipeline, monkeypatch, tmp_path):
85
+ out = tmp_path / "o"
86
+
87
+ run_cli(
88
+ monkeypatch,
89
+ "--track",
90
+ "Daft Punk - One More Time",
91
+ "Radiohead - Paranoid Android",
92
+ "-o",
93
+ out,
94
+ )
95
+
96
+ assert len(pipeline["searched"]) == 2
97
+
98
+ def test_file_and_inline_track_together(self, pipeline, monkeypatch, tmp_path):
99
+ songs = tmp_path / "songs.txt"
100
+ songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
101
+ out = tmp_path / "o"
102
+
103
+ run_cli(monkeypatch, songs, "--track", "Radiohead - Paranoid Android", "-o", out)
104
+
105
+ assert len(pipeline["searched"]) == 2
106
+
107
+ def test_inline_track_and_file_in_one_call(self, pipeline, monkeypatch, tmp_path):
108
+ songs = tmp_path / "songs.txt"
109
+ songs.write_text("Daft Punk - One More Time\n", encoding="utf-8")
110
+ out = tmp_path / "o"
111
+
112
+ run_cli(monkeypatch, "Radiohead - Paranoid Android", songs, "-o", out)
113
+
114
+ assert len(pipeline["searched"]) == 2
115
+
116
+ def test_missing_file_exits(self, pipeline, monkeypatch, tmp_path, capsys):
117
+ with pytest.raises(SystemExit) as exc:
118
+ run_cli(monkeypatch, "songs.txt", "-o", tmp_path / "o")
119
+
120
+ assert exc.value.code == 1
121
+ assert "Input file not found" in capsys.readouterr().out
122
+
123
+ def test_unparsable_track_flag_exits(self, pipeline, monkeypatch, tmp_path, capsys):
124
+ with pytest.raises(SystemExit) as exc:
125
+ run_cli(monkeypatch, "--track", "nonsense", "-o", tmp_path / "o")
126
+
127
+ assert exc.value.code == 1
128
+ assert "Could not parse track" in capsys.readouterr().out
129
+
130
+ def test_inline_track_without_artist_exits(self, pipeline, monkeypatch, tmp_path, capsys):
131
+ with pytest.raises(SystemExit) as exc:
132
+ run_cli(monkeypatch, " - One More Time", "-o", tmp_path / "o")
133
+
134
+ assert exc.value.code == 1
135
+ assert "Could not parse track" in capsys.readouterr().out
136
+
137
+ def test_inline_track_without_title_exits(self, pipeline, monkeypatch, tmp_path, capsys):
138
+ with pytest.raises(SystemExit) as exc:
139
+ run_cli(monkeypatch, "Daft Punk - ", "-o", tmp_path / "o")
140
+
141
+ assert exc.value.code == 1
142
+ assert "Could not parse track" in capsys.readouterr().out
143
+
144
+ def test_wrong_separator_hints_at_the_track_format(
145
+ self, pipeline, monkeypatch, tmp_path, capsys
146
+ ):
147
+ with pytest.raises(SystemExit) as exc:
148
+ run_cli(monkeypatch, "Daft Punk -One More Time", "-o", tmp_path / "o")
149
+
150
+ assert exc.value.code == 1
151
+ assert "Artist - Title" in capsys.readouterr().out
152
+
153
+ def test_unparsable_lines_in_a_file_are_reported(
154
+ self, pipeline, monkeypatch, tmp_path, capsys
155
+ ):
156
+ songs = tmp_path / "songs.txt"
157
+ songs.write_text(
158
+ "Daft Punk -One More Time\nRadiohead - Paranoid Android\n",
159
+ encoding="utf-8",
160
+ )
161
+
162
+ run_cli(monkeypatch, songs, "-o", tmp_path / "o")
163
+ log = capsys.readouterr().out
164
+
165
+ assert "WARNING: skipped 1 line(s)" in log
166
+ assert "Daft Punk -One More Time" in log
167
+
168
+ def test_inline_track_uses_the_output_rules(self, pipeline, monkeypatch, tmp_path):
169
+ out = tmp_path / "o"
170
+ config = tmp_path / "cfg.ini"
171
+ config.write_text(f"[trackfetch]\noutput = {out}\n", encoding="utf-8")
172
+
173
+ run_cli(monkeypatch, "Daft Punk - One More Time", "--config", config)
174
+
175
+ assert (out / "Daft Punk - One More Time.m4a").exists()
176
+
177
+
178
+ class TestCollectSources:
179
+ def test_config_input_is_a_file(self, tmp_path):
180
+ songs = tmp_path / "songs.txt"
181
+ songs.write_text("A - B\n", encoding="utf-8")
182
+
183
+ class Args:
184
+ inputs = []
185
+ track = None
186
+
187
+ sources = m.collect_sources(Args(), {"input": str(songs)})
188
+
189
+ assert sources == [songs]
190
+
191
+ def test_missing_config_input_raises(self, tmp_path):
192
+ class Args:
193
+ inputs = []
194
+ track = None
195
+
196
+ with pytest.raises(ValueError) as exc:
197
+ m.collect_sources(Args(), {"input": str(tmp_path / "nope.txt")})
198
+
199
+ assert "Input file not found" in str(exc.value)
200
+
201
+ def test_read_song_file_skips_comments(self, tmp_path):
202
+ songs = tmp_path / "songs.txt"
203
+ songs.write_text("# comment\n\nA - B\nbroken line\nC - D\n", encoding="utf-8")
204
+
205
+ parsed, skipped = m.read_song_file(songs)
206
+
207
+ assert parsed == [("A", "B"), ("C", "D")]
208
+ assert skipped == ["broken line"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackfetch
3
- Version: 2.2.0
3
+ Version: 2.2.6
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
@@ -213,12 +213,13 @@ trackfetch "My Playlist.txt" -o ~/Music/"My Playlist"
213
213
  ## Usage
214
214
 
215
215
  ```text
216
- trackfetch [-h] [-o OUTPUT] [--config CONFIG] [-f {mp3,m4a,opus}] [--title-only] [--delay DELAY] [--spotify-client-id ID] [--spotify-client-secret SECRET] [input]
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
- | `input` | required | Text file with one `Artist - Title` per line. Can also be set in the [config file](#configuration-file). |
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. |
222
223
  | `-o`, `--output` | current directory | Folder to save the files in. It's created if it doesn't exist. |
223
224
  | `--config` | auto-detected | Path to a [config file](#configuration-file). |
224
225
  | `-f`, `--format` | `m4a` | Audio format: `mp3`, `m4a` or `opus`. See [Audio formats](#audio-formats). |
@@ -232,8 +233,24 @@ trackfetch songs.txt -o ~/Music/RoadTrip # save to a specific folder
232
233
  trackfetch songs.txt -o /media/usb --title-only # short names for a car stereo
233
234
  trackfetch big-list.txt --delay 3 # go easy on rate limits
234
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
235
238
  ```
236
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
+
237
254
  ### Configuration file
238
255
 
239
256
  Put your defaults in an INI file and forget the flags:
@@ -249,8 +266,9 @@ spotify_client_id = your-client-id
249
266
  spotify_client_secret = your-client-secret
250
267
  ```
251
268
 
252
- Every option above can be set there, under the `[trackfetch]` section. The
253
- precedence is always the same:
269
+ Every option above can be set there, under the `[trackfetch]` section, except
270
+ `--track`, which only makes sense on the command line. The precedence is
271
+ always the same:
254
272
 
255
273
  1. the command-line flag,
256
274
  2. the config file,
@@ -258,8 +276,8 @@ precedence is always the same:
258
276
 
259
277
  trackfetch looks for the config file in this order: the path given with
260
278
  `--config`, the path in `$TRACKFETCH_CONFIG`, `trackfetch.ini` in the current
261
- directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini` (usually
262
- `~/.config/trackfetch/trackfetch.ini`) and `~/.trackfetch.ini`.
279
+ directory, `$XDG_CONFIG_HOME/trackfetch/trackfetch.ini`,
280
+ `~/.config/trackfetch/trackfetch.ini` and `~/.trackfetch.ini`.
263
281
 
264
282
  ### Spotify credentials
265
283
 
@@ -305,7 +323,8 @@ Daft Punk - One More Time
305
323
  Sufjan Stevens - Mystery of Love - Remastered ← only the first " - " separates artist and title
306
324
  ```
307
325
 
308
- The separator is space, hyphen, space (` - `). Lines without it are skipped. See [`examples/songs.txt`](https://github.com/ByteMe6/trackfetch/blob/master/examples/songs.txt).
326
+ The separator is space, hyphen, space (` - `). Lines without it are skipped,
327
+ and trackfetch prints how many lines it skipped and which ones they were. See [`examples/songs.txt`](https://github.com/ByteMe6/trackfetch/blob/master/examples/songs.txt).
309
328
 
310
329
  ### Output folder
311
330
 
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  trackfetch.py
5
5
  tests/test_config.py
6
+ tests/test_inline.py
6
7
  tests/test_pipeline.py
7
8
  tests/test_spotify.py
8
9
  tests/test_tags_and_files.py
@@ -226,7 +226,7 @@ def download_cover(url: str, destination: Path):
226
226
  url,
227
227
  timeout=30,
228
228
  headers={
229
- "User-Agent": "trackfetch/2.2"
229
+ "User-Agent": "trackfetch/2.2.6"
230
230
  },
231
231
  )
232
232
 
@@ -846,7 +846,7 @@ def apply_config(args, parser, settings):
846
846
  for action in parser._actions:
847
847
  dest = action.dest
848
848
 
849
- if dest in ("help", "version", "config"):
849
+ if dest in ("help", "version", "config", "inputs", "track"):
850
850
  continue
851
851
 
852
852
  if getattr(args, dest, None) is not None:
@@ -874,19 +874,124 @@ def apply_config(args, parser, settings):
874
874
  setattr(args, dest, value)
875
875
 
876
876
 
877
+ def read_song_file(path: Path):
878
+ """Read Artist - Title pairs from a text file.
879
+
880
+ Returns a (songs, skipped) tuple, where skipped holds the lines that
881
+ could not be parsed.
882
+ """
883
+ songs = []
884
+ skipped = []
885
+
886
+ for line in path.read_text(
887
+ encoding="utf-8",
888
+ errors="replace",
889
+ ).splitlines():
890
+
891
+ line = line.strip()
892
+
893
+ if not line or line.startswith("#"):
894
+ continue
895
+
896
+ song = parse_song(line)
897
+
898
+ if song:
899
+ songs.append(song)
900
+
901
+ else:
902
+ skipped.append(line)
903
+
904
+ return songs, skipped
905
+
906
+
907
+ def collect_sources(args, settings):
908
+ """Build the ordered list of inputs: file paths and inline tracks.
909
+
910
+ Every item is either a Path to a text file or an inline
911
+ "Artist - Title" string.
912
+ """
913
+ sources = []
914
+
915
+ config_input = config_str_value(settings, "input")
916
+
917
+ if config_input:
918
+ path = Path(config_input).expanduser()
919
+
920
+ if not path.is_file():
921
+ raise ValueError(f"Input file not found: {path}")
922
+
923
+ sources.append(path)
924
+
925
+ for item in args.inputs:
926
+ path = Path(item).expanduser()
927
+
928
+ if path.is_file():
929
+ sources.append(path)
930
+
931
+ elif " - " in item:
932
+ if not parse_song(item):
933
+ raise ValueError(
934
+ f"Could not parse track: {item} "
935
+ f"(expected 'Artist - Title')"
936
+ )
937
+
938
+ sources.append(item)
939
+
940
+ else:
941
+ raise ValueError(
942
+ f"Input file not found: {item} "
943
+ f"(if this is a track, use the 'Artist - Title' format)"
944
+ )
945
+
946
+ for group in args.track or []:
947
+ for spec in group:
948
+ if not parse_song(spec):
949
+ raise ValueError(
950
+ f"Could not parse track: {spec} "
951
+ f"(expected 'Artist - Title')"
952
+ )
953
+
954
+ sources.append(spec)
955
+
956
+ return sources
957
+
958
+
877
959
  def main():
878
960
  parser = argparse.ArgumentParser(
879
961
  description=(
880
962
  "Download music using Spotify metadata "
881
963
  "and YouTube audio"
882
- )
964
+ ),
965
+ epilog=(
966
+ "A config file with the defaults can be used as well. It is "
967
+ "looked up in --config, $TRACKFETCH_CONFIG, ./trackfetch.ini, "
968
+ "$XDG_CONFIG_HOME/trackfetch/trackfetch.ini, "
969
+ "~/.config/trackfetch/trackfetch.ini and ~/.trackfetch.ini. "
970
+ "A command-line flag wins over the config file, and the config "
971
+ "file wins over the built-in default."
972
+ ),
973
+ formatter_class=argparse.RawDescriptionHelpFormatter,
883
974
  )
884
975
 
885
976
  parser.add_argument(
886
- "input",
887
- type=Path,
888
- nargs="?",
889
- help="Text file with Artist - Title",
977
+ "inputs",
978
+ nargs="*",
979
+ metavar="INPUT",
980
+ help=(
981
+ "Text file with Artist - Title, "
982
+ "or an inline 'Artist - Title' track"
983
+ ),
984
+ )
985
+
986
+ parser.add_argument(
987
+ "--track",
988
+ action="append",
989
+ nargs="+",
990
+ metavar="ARTIST - TITLE",
991
+ help=(
992
+ "Track to download inline, "
993
+ "can be given several times"
994
+ ),
890
995
  )
891
996
 
892
997
  parser.add_argument(
@@ -905,7 +1010,10 @@ def main():
905
1010
  "--config",
906
1011
  type=Path,
907
1012
  default=None,
908
- help="Path to a config file",
1013
+ help=(
1014
+ "Path to a config file "
1015
+ "(default: auto-detected, see below)"
1016
+ ),
909
1017
  )
910
1018
 
911
1019
  parser.add_argument(
@@ -913,21 +1021,21 @@ def main():
913
1021
  "--format",
914
1022
  choices=list(AUDIO_FORMATS),
915
1023
  default=None,
916
- help="Audio format",
1024
+ help="Audio format (default: m4a)",
917
1025
  )
918
1026
 
919
1027
  parser.add_argument(
920
1028
  "--title-only",
921
1029
  action="store_true",
922
1030
  default=None,
923
- help="Use only title in filename",
1031
+ help="Use only title in filename (default: off)",
924
1032
  )
925
1033
 
926
1034
  parser.add_argument(
927
1035
  "--delay",
928
1036
  type=float,
929
1037
  default=None,
930
- help="Delay between songs in seconds",
1038
+ help="Delay between songs in seconds (default: 1.0)",
931
1039
  )
932
1040
 
933
1041
  parser.add_argument(
@@ -974,15 +1082,19 @@ def main():
974
1082
  if args.delay is None:
975
1083
  args.delay = DEFAULT_DELAY
976
1084
 
977
- if args.input is None:
978
- print(
979
- "ERROR: No input file. "
980
- "Pass it as an argument or set 'input' in the config file."
981
- )
1085
+ try:
1086
+ sources = collect_sources(args, settings)
1087
+
1088
+ except ValueError as error:
1089
+ print(f"ERROR: {error}")
982
1090
  sys.exit(1)
983
1091
 
984
- if not args.input.exists():
985
- print(f"ERROR: Input file not found: {args.input}")
1092
+ if not sources:
1093
+ print(
1094
+ "ERROR: No input. "
1095
+ "Pass a file or an inline 'Artist - Title' track, "
1096
+ "or set 'input' in the config file."
1097
+ )
986
1098
  sys.exit(1)
987
1099
 
988
1100
  args.output.mkdir(
@@ -994,27 +1106,41 @@ def main():
994
1106
  failed_file = args.output / "failed.txt"
995
1107
 
996
1108
  songs = []
1109
+ skipped = []
997
1110
 
998
- for line in args.input.read_text(
999
- encoding="utf-8",
1000
- errors="replace",
1001
- ).splitlines():
1111
+ for source in sources:
1112
+ if isinstance(source, Path):
1113
+ file_songs, file_skipped = read_song_file(source)
1002
1114
 
1003
- line = line.strip()
1115
+ songs.extend(file_songs)
1004
1116
 
1005
- if not line or line.startswith("#"):
1006
- continue
1117
+ skipped.extend(
1118
+ (source, line) for line in file_skipped
1119
+ )
1007
1120
 
1008
- song = parse_song(line)
1121
+ else:
1122
+ song = parse_song(source)
1009
1123
 
1010
- if song:
1011
- songs.append(song)
1124
+ if song:
1125
+ songs.append(song)
1012
1126
 
1013
1127
  done = read_done(done_file)
1014
1128
 
1015
1129
  print(f"Found {len(songs)} songs")
1016
1130
  print(f"Already completed: {len(done)}")
1017
1131
 
1132
+ if skipped:
1133
+ print(
1134
+ f"WARNING: skipped {len(skipped)} line(s) "
1135
+ f"that are not 'Artist - Title':"
1136
+ )
1137
+
1138
+ for path, line in skipped[:5]:
1139
+ print(f" {path}: {line}")
1140
+
1141
+ if len(skipped) > 5:
1142
+ print(f" ... and {len(skipped) - 5} more")
1143
+
1018
1144
  if config_path:
1019
1145
  print(f"Config: {config_path}")
1020
1146
 
File without changes
File without changes