refactor(music)!: make Song an ordinary Title on the generic path

Delete the dead grouped-download block, the planner, and the music service hooks; Song now flows through the one download loop, gets tagged via tag_file, and answers -w with {disc}x{track} keys.

BREAKING CHANGE: GROUP_AUDIO_DOWNLOADS, get_music_track_options and get_music_collection_label are gone. A music service returns one Audio track per quality and lets the framework choose.
This commit is contained in:
imSp4rky
2026-08-18 15:05:30 -06:00
parent a4a385e5aa
commit 68c7d95173
43 changed files with 1082 additions and 2132 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ The special language tokens `orig`, `all`, and `best` are honoured everywhere a
| Flag | Description |
|---|---|
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. For [dated content](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). |
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. For [dated content](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). For a [music release](downloading.md#music-tracks), a token is a track number (`1-5`, `1,3,7`), or `{disc}x{track}` (`2x3`) when the release has more than one disc. |
| `--select-titles` | Interactively select what to download: episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
| `--latest-episode` | Download only the single most recent episode. |
| `--list-titles` | List titles only; do not download. |
+80 -10
View File
@@ -232,6 +232,27 @@ unshackle dl -a EC3,AAC EXAMPLE 81234567
unshackle dl -c 5.1 --noatmos EXAMPLE 0ABC123
```
### Music quality
A music service gives one `Audio` track for each codec and bitrate it offers for a song.
The audio options above choose between them: `-a` picks the codec and `-ab` or `-ab-range`
picks the bitrate. If you set neither, unshackle takes the best track.
`-q` / `--quality` is a video height. It has no effect on a music download.
```shell title="Take the lossless track"
unshackle dl -a FLAC EXAMPLE 81234567
```
```shell title="Take the lossy track at 320 kb/s"
unshackle dl -a AAC -ab 320 EXAMPLE 81234567
```
!!! tip "Two FLAC tracks in one release"
A service can offer both a CD FLAC and a hi-res FLAC. The two have the same codec, so
the bitrate is what separates them. Run `--list` to read the bitrates, then give `-ab`
the one you want.
## Languages
### Video and audio language
@@ -381,15 +402,16 @@ unshackle dl --sub-format srt EXAMPLE 81234567
automatically. This behaviour is governed by the `subtitle.strip_sdh` config option
(default on).
## Selecting episodes
## Selecting episodes and tracks
For series, several flags control which episodes are downloaded. By default, **all**
episodes are downloaded.
For series, several flags control which episodes are downloaded. The same flags select the
tracks of a music release. By default, **all** episodes and **all** tracks are downloaded.
### Wanted ranges
`-w` / `--wanted` accepts flexible season/episode ranges, comma-separated. Prefix a token
with `-` to exclude it.
`-w` / `--wanted` accepts season and episode ranges, comma-separated. Prefix a token
with `-` to exclude it. For a music release, the same option takes track numbers. Read
[music tracks](#music-tracks) below.
=== "Whole seasons"
@@ -440,6 +462,49 @@ with `-` to exclude it.
unshackle dl -w 2026-08-01:2026-08-31,-2026-08-15 EXAMPLE 81234567
```
### Music tracks
`-w` selects the tracks of an album, EP, single, or playlist. Give a track by its number.
A release with more than one disc uses the `{disc}x{track}` form. A number on its own is a
track on disc 1, which is the number the track list shows for a single-disc release.
=== "Tracks"
```shell
# Track 3
unshackle dl -w 3 EXAMPLE 81234567
# Tracks 1 through 5
unshackle dl -w 1-5 EXAMPLE 81234567
# Tracks 1, 3 and 7
unshackle dl -w 1,3,7 EXAMPLE 81234567
```
=== "More than one disc"
```shell
# Disc 2, track 3
unshackle dl -w 2x3 EXAMPLE 81234567
# Disc 2, tracks 1 through 4
unshackle dl -w 2x1-2x4 EXAMPLE 81234567
# Disc 1 track 3, through disc 2 track 2
unshackle dl -w 1x3-2x2 EXAMPLE 81234567
```
=== "Exclusions"
```shell
# Tracks 1 through 8, but not track 4
unshackle dl -w 1-8,-4 EXAMPLE 81234567
```
!!! tip "Read the track list first"
`--list-titles` prints the release with its disc and track numbers, so you can see
which numbers to give `-w`.
### Daily and date-based content
Talk shows, news and sports have no official episode numbering, so unshackle names them
@@ -570,8 +635,8 @@ unshackle dl -o ~/Videos/incoming EXAMPLE 81234567
### Muxing behaviour
By default, tracks are muxed into a single Matroska (`.mkv`) file with `mkvmerge`. These
flags change how the output is assembled:
By default, the tracks of a movie or an episode are muxed into a single Matroska (`.mkv`)
file with `mkvmerge`. These flags change how the output is assembled:
| Flag | Behaviour | Default source |
| --- | --- | --- |
@@ -579,12 +644,17 @@ flags change how the output is assembled:
| `--split-audio` | Write a separate output file per audio codec instead of merging all audio. | config `muxing.merge_audio` (on) |
| `--merge-video` | Mux video tracks that share a height, range, and codec into one file, so only language varies inside a file. | config `muxing.merge_video` (off) |
!!! note "Music is never muxed"
A music track skips the muxer. unshackle keeps the container that the service
delivered, such as `.flac`, `.m4a`, or `.mp3`, and writes the metadata into that file.
Read [Music output files](output-and-naming.md#music-output-files).
### After the download
`--postscript "<command>"` runs your own command once per output file, with unshackle's
metadata substituted into `{variable}` placeholders. It is repeatable, and it replaces the
`post_scripts` config for that run. Nothing runs under `--no-mux`, since no output is
written.
`post_scripts` config for that run. No `success` hook runs under `--no-mux`, because that
run writes no muxed output. A `failure` hook still runs if the download itself fails.
```shell title="Hand each finished file to an uploader"
unshackle dl --postscript "python /opt/upload.py {filepath} --service={service}" EXAMPLE 81234567
@@ -820,7 +890,7 @@ authoritative list.
| `--forced-subs` | `-fs` | Include forced subtitles. |
| `--forced-s-lang` | `-fsl` | Forced subtitle language(s); implies `-fs`. `-` excludes. |
| `--sub-format` | | Output subtitle format. |
| `--wanted` | `-w` | Episode/season range, or an air date. |
| `--wanted` | `-w` | Episode/season range, an air date, or a music track (`1-5`, `2x3`). |
| `--daily` | | Fill missing air dates from TVDB during `--enrich`. |
| `--select-titles` | | Interactively pick episodes or films. |
| `--latest-episode` | | Only the newest episode. |
+42 -4
View File
@@ -6,7 +6,7 @@ If you have not configured `unshackle.yaml` yet, read [Configuration File](../ge
## How an output path is assembled
When a download finishes muxing, unshackle builds the final path from three pieces:
When a download finishes, unshackle builds the final path from three pieces:
```text
<output directory> / <folder template> / <filename template><extension>
@@ -15,7 +15,7 @@ When a download finishes muxing, unshackle builds the final path from three piec
- **Output directory**: `directories.downloads` from your config, or whatever you pass to `-o/--output` on the command line for a single run.
- **Folder template**: an optional per-title subfolder. TV episodes and music tracks are **always** placed in a folder; movies only get one if you define a `movies` folder template, or a single-string `folder` template that applies to all title kinds (see [Folder templates](#folder-templates)).
- **Filename template**: the `output_template` for the title kind (movie, series, or song).
- **Extension**: chosen by the muxer: `.mkv` for video, `.mka` for audio-only, `.mks` for subtitle-only.
- **Extension**: chosen by the muxer for a movie or an episode: `.mkv` for video, `.mka` for audio-only, `.mks` for subtitle-only. A music track is not muxed. It keeps the container the service delivered, such as `.flac`, `.m4a`, or `.mp3`. Read [Music output files](#music-output-files).
!!! example "A typical episode path"
```text
@@ -218,14 +218,52 @@ The per-kind folder keys are `movies`, `series`, `songs`, and `albums`. Any othe
**Fallback behavior:**
- A per-kind folder template wins if present; otherwise the single `folder` string is used; otherwise unshackle falls back to a built-in default.
- Built-in defaults when no folder template is set: movies get no folder at all unless a `movies` folder template (or a single-string `folder` template) exists; series fall back to a folder *derived* from the `series` output template (stripping `{episode}`, `{episode_name}`, and collapsing `{season_episode}` down to `{season}`); music albums fall back to `{artist} - {album} ({year})`.
- Built-in defaults when no folder template is set. Movies get no folder at all unless a `movies` folder template (or a single-string `folder` template) exists. Series fall back to a folder *derived* from the `series` output template (stripping `{episode}`, `{episode_name}`, and collapsing `{season_episode}` down to `{season}`). Music albums fall back to `{artist} - {album} ({year})`. If the song has no year, the folder name is only `{artist} - {album}`.
!!! note "Movies are flat by default"
Episodes and songs are always foldered. Movies are written directly into the output directory *unless* you define a `movies` folder template (or a single-string `folder` template, which folders every kind). Set one if you want each movie in its own directory.
## Music output files
A music track does not go through the muxer. unshackle moves the downloaded audio file to
its final path. It keeps the container that the service delivered. A lossless download
arrives as a `.flac` file. An AAC download arrives as a `.m4a` file. unshackle writes no
`.mkv` file for music, and the [muxing options](#muxing-options) below do not apply.
The `songs` output template names the file. The `albums` folder template names the folder
around it.
```text
/home/user/Downloads/
└─ The Artist - The Album (2024)/
├─ 01. First Track.flac
└─ 02. Second Track.flac
```
### Music metadata
After the file moves, unshackle writes the metadata into the audio file. It uses Vorbis
comments for FLAC and Ogg, ID3 frames for MP3, and MP4 atoms for M4A. The tags hold the
track title, artist, album artist, track and disc numbers, date, genre, ISRC, UPC,
copyright, and record label. When `tag` and `tag_group_name` are set, unshackle also writes the group
name into a `GROUP` tag.
Cover art is embedded when the service gives an artwork URL. unshackle downloads the image
and puts it in the file.
Lyrics are written when the service supplies them: a `LYRICS` Vorbis comment for FLAC, a
`USLT` frame for MP3, and a `©lyr` atom for M4A. Ogg uses the same `LYRICS` field as
FLAC. unshackle does not write synchronized lyrics (`SYLT`).
A metadata fault does not stop the run. The file is already in place, so unshackle writes a
warning and continues.
!!! warning "`--no-mux` leaves music files untagged"
[`--no-mux`](cli-reference.md#output-muxing-files) moves each downloaded track file to the output path without the tagging step. A music file from such a run holds only the metadata that the service put in it.
## Muxing options
Muxing (combining video, audio, subtitle, chapter and attachment tracks into a single Matroska file with `mkvmerge`) is configured under the `muxing` key.
Muxing (combining video, audio, subtitle, chapter and attachment tracks into a single Matroska file with `mkvmerge`) is configured under the `muxing` key. Only movies and episodes are muxed. Read [Music output files](#music-output-files) for what a music download does instead.
```yaml title="unshackle.yaml"
muxing: