mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 13:05:09 +00:00
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:
@@ -12,10 +12,11 @@ downloading, decryption, muxing, naming) is handled by the unshackle core.
|
||||
the CLI. Service code is deliberately kept separate from the core so it can
|
||||
live in its own private repository.
|
||||
|
||||
Unshackle ships one reference service, **EXAMPLE**, at
|
||||
`unshackle/services/EXAMPLE/`. It is a deliberately exhaustive, non-runnable
|
||||
showcase of every framework feature in a single file. Read it alongside this
|
||||
page; most snippets below are drawn from it.
|
||||
unshackle ships two reference services. **EXAMPLE**, at
|
||||
`unshackle/services/EXAMPLE/`, is a non-runnable showcase of every framework
|
||||
feature in a single file. Read it alongside this page. Most snippets below come
|
||||
from it. **MUSIC_EXAMPLE**, at `unshackle/services/MUSIC_EXAMPLE/`, is the same
|
||||
kind of showcase for a music service. If your platform has songs, read that one too.
|
||||
|
||||
---
|
||||
|
||||
@@ -318,11 +319,20 @@ Common constructor fields:
|
||||
=== "Song"
|
||||
|
||||
```python
|
||||
Song(id_, service, name, artist, album, track, disc, year,
|
||||
language=None, data=None)
|
||||
Song(id_, service, name, artist, album, track, disc=1, year=None,
|
||||
language=None, data=None, ...)
|
||||
```
|
||||
|
||||
Return an `Album([...])` of songs.
|
||||
Return an `Album([...])` of songs. `disc` defaults to 1 and `year` is optional.
|
||||
Optional metadata keywords such as `album_artist`, `genre`, `isrc`, `label`,
|
||||
`lyrics` and `artwork_url` feed the tagger and the naming templates. Read the
|
||||
class for the current list.
|
||||
|
||||
From `get_tracks()`, give one `Audio` track for each codec and bitrate you offer
|
||||
for the song. The framework then selects one with its usual audio options.
|
||||
A `Song` skips the muxer, so restore the real container suffix on the track path
|
||||
in `on_track_downloaded`. The **MUSIC_EXAMPLE** service shows the full music
|
||||
surface.
|
||||
|
||||
!!! note "The ID must be unique and stable"
|
||||
`id_` must be truthy and, if it has a length, at least 4 characters, to
|
||||
@@ -696,5 +706,6 @@ unshackle dl MYSVC https://myservice.com/watch/abc123
|
||||
- [ ] URLs, user agents, and certificates live in `config.yaml`, read via `self.config[...]`.
|
||||
- [ ] Multi-file services import their own modules relatively (`from .helpers import x`), so they work when loaded from a repo.
|
||||
|
||||
Read `unshackle/services/EXAMPLE/` end to end; it annotates every feature
|
||||
touched above in one place.
|
||||
Read `unshackle/services/EXAMPLE/` end to end. It annotates every feature
|
||||
touched above in one place. For a music service, read
|
||||
`unshackle/services/MUSIC_EXAMPLE/` too.
|
||||
|
||||
@@ -223,6 +223,13 @@ Service-specific CLI options may also be passed as additional keys.
|
||||
|
||||
Each serialized title carries `type` (`"episode"`, `"movie"`, or `"other"`), `name`, `id`, `language`, `description`, `date`, and `cover_url`. Episodes and movies add `year`; episodes additionally add `series_title`, `season`, and `number`.
|
||||
|
||||
!!! warning "A song serializes as `"other"`"
|
||||
A music track has no `type` of its own. It is sent as `"other"`, and the response
|
||||
carries none of its music fields: no artist, no album, no track or disc number, and no
|
||||
`year`. `name` holds the track name. To read the disc and track numbers of a release,
|
||||
run the CLI with `--list-titles`. Selecting music tracks over the API still works,
|
||||
through the `wanted` parameter.
|
||||
|
||||
These keys are sent only when the title carries them, so a title without them serializes exactly as before:
|
||||
|
||||
| Key | Type | On | Meaning |
|
||||
@@ -261,7 +268,7 @@ List the video, audio, and subtitle tracks for a title. For series, you can scop
|
||||
| --- | --- | --- | --- |
|
||||
| `service` | string | yes | Service tag. |
|
||||
| `title_id` | string | yes | Title identifier. |
|
||||
| `wanted` | string | no | Episode/season range (e.g. `"S01E01-S01E03"`). |
|
||||
| `wanted` | string / string[] | no | Episode/season range (e.g. `"S01E01-S01E03"`), or a music track selector (e.g. `"1-5"`, `"1,3,7"`, or `"2x3"` for disc 2 track 3). A list of selectors is accepted too. |
|
||||
| `season` | int/string | no | Season number (combined with `episode`). |
|
||||
| `episode` | int/string | no | Episode number (combined with `season`). |
|
||||
| `part` | int/string | no | Part index of a split episode (combined with `season` and `episode`). |
|
||||
@@ -378,7 +385,7 @@ Create a download job. Requires `service` and `title_id`; every other field is a
|
||||
| `range` | string[] | `["SDR"]` | Dynamic range(s). |
|
||||
| `channels` | number | `null` | Audio channel count. |
|
||||
| `no_atmos` | boolean | `false` | Exclude Atmos tracks. |
|
||||
| `wanted` | string[] | `[]` | Episode/season selectors. Accepts the part form, `"S01E01.2"`, and the air-date form, `"2026-08-11"` or `"2026-08-01:2026-08-31"`. |
|
||||
| `wanted` | string / string[] | `[]` | Episode/season selectors, as a list or as one comma-separated string. Accepts the part form, `"S01E01.2"`, and the air-date form, `"2026-08-11"` or `"2026-08-01:2026-08-31"`. For a music release, a selector is a track number, `"1-5"` or `"1,3,7"`, or `"{disc}x{track}"` such as `"2x3"`. |
|
||||
| `latest_episode` | boolean | `false` | Only the newest episode. |
|
||||
| `lang` / `v_lang` / `a_lang` / `s_lang` | string[] | `["orig"]` / `[]` / `[]` / `["all"]` | Language filters. |
|
||||
| `require_subs` | string[] | `[]` | Required subtitle languages. |
|
||||
|
||||
@@ -232,7 +232,7 @@ The request body accepts the full download parameter set. A few of the most usef
|
||||
| `vcodec` | `null` | Video codec, e.g. `"H265"` |
|
||||
| `acodec` | `null` | Audio codec, e.g. `"EC3"` |
|
||||
| `range` | `["SDR"]` | Dynamic range: `SDR`, `HDR10`, `HDR10+`, `DV`, `HLG`, `HYBRID` |
|
||||
| `wanted` | `[]` | Episodes to fetch, e.g. `["S01E01"]`; empty means all |
|
||||
| `wanted` | `[]` | Episodes to fetch, e.g. `["S01E01"]`, or music tracks, e.g. `["1-5"]`. Empty means all |
|
||||
| `lang` | `["orig"]` | Video/audio language(s) |
|
||||
| `s_lang` | `["all"]` | Subtitle language(s) |
|
||||
| `downloads` | `1` | Number of tracks to download concurrently |
|
||||
|
||||
@@ -33,7 +33,8 @@ Track.download → fetch + merge segments (core-driven)
|
||||
│
|
||||
DRM license + decrypt → service license hooks → CDM → decrypt tool
|
||||
│
|
||||
Tracks.mux → mkvmerge → tagged .mkv
|
||||
Tracks.mux → mkvmerge → tagged .mkv (movie / episode)
|
||||
│ a Song skips the muxer and keeps its own container
|
||||
```
|
||||
|
||||
1. **`get_titles()`** returns a `Titles` collection: the movies, episodes, or songs
|
||||
@@ -173,11 +174,45 @@ Each subclass adds its own required fields on top of `id_` and `service`:
|
||||
=== "Song"
|
||||
|
||||
```python
|
||||
Song(id_, service, name, artist, album, track, disc, year,
|
||||
language=None, data=None)
|
||||
Song(id_, service, name, artist, album, track, disc=1, year=None,
|
||||
language=None, data=None, ...)
|
||||
```
|
||||
|
||||
`name`, `artist`, `album`, `track`, `disc`, and `year` are all required and validated.
|
||||
`name`, `artist`, `album`, and `track` are required and validated. `disc` defaults to
|
||||
1, and `year` is optional. A single therefore needs no `disc`, and an undated release
|
||||
needs no `year`.
|
||||
|
||||
A `Song` also takes optional metadata keywords that the tagger and the naming templates
|
||||
read: `album_artist`, `release_type`, `total_tracks`, `total_discs`, `genre`,
|
||||
`explicit`, `isrc`, `upc`, `copyright`, `label`, `lyrics`, and `artwork_url`. Read the
|
||||
class for the current list.
|
||||
|
||||
The tagger also falls back to `Song.data` for fields the constructor did not get, such
|
||||
as the composer or a full release date. It reads the top level of that dict and a
|
||||
nested `metadata` sub-dict, with the nested keys winning, so either shape works.
|
||||
|
||||
From `get_tracks()`, a music service gives **one `Audio` track for each codec and
|
||||
bitrate it offers for a `Song`**. The framework then selects one with its usual audio
|
||||
options, `-a`/`--acodec` and `--abitrate`, and its usual audio sorting. A movie or an
|
||||
episode uses the same options. `-q`/`--quality` sets the video height only and does not
|
||||
apply to music. `Audio` has no `bit_depth` or `sample_rate` field. Therefore `bitrate`
|
||||
separates a hi-res FLAC from a CD FLAC, and the audio sort already ranks it.
|
||||
|
||||
A `Song` is an ordinary `Title` on the one generic download path. unshackle has no
|
||||
music-specific service API and no separate music download loop. The
|
||||
`GROUP_AUDIO_DOWNLOADS` flag, `get_music_track_options` and `get_music_collection_label`
|
||||
no longer exist, and a music service does not need them.
|
||||
|
||||
!!! warning "Restore the container in `on_track_downloaded`"
|
||||
Every track downloads to a temp file named `Audio_<id>.mp4` whatever it holds, and
|
||||
the final move keeps that suffix. A `Song` skips the muxer, so its download **is**
|
||||
the delivered file, and the tagger picks its metadata format from the suffix.
|
||||
Rename the track path to the true container in `on_track_downloaded`, or a FLAC
|
||||
lands as an untaggable `.mp4`. The event fires twice, once before decryption and
|
||||
once at the end, so wait until `track.drm` is clear before you rename.
|
||||
|
||||
The **MUSIC_EXAMPLE** reference service, at `unshackle/services/MUSIC_EXAMPLE/`, shows
|
||||
the full music surface.
|
||||
|
||||
### Title remapping
|
||||
|
||||
@@ -237,8 +272,8 @@ closed-caption descriptors. The relevant enums:
|
||||
[Dolby Vision hybrid](#dolby-vision-and-hybrid)).
|
||||
|
||||
!!! note "Bitrate units"
|
||||
Both `Video` and `Audio` store `bitrate` in **bytes per second** (rounded up), even
|
||||
though the string form displays kb/s.
|
||||
Both `Video` and `Audio` store `bitrate` in **bits per second** (rounded up). The
|
||||
string form displays kb/s, and `--vbitrate` and `--abitrate` match on kb/s.
|
||||
|
||||
### `Audio`
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ keys through your CDM (and any key vaults), then decrypt, mux, and tag the resul
|
||||
| `-sl`, `--s-lang` | Subtitle language(s); defaults to `all`. |
|
||||
| `-v`, `--vcodec` | Video codec, e.g. `-v H.265`. Defaults to any. |
|
||||
| `-r`, `--range` | Dynamic range, e.g. `-r HDR10` or `-r DV`. Defaults to `SDR`. |
|
||||
| `-w`, `--wanted` | Which episodes, e.g. `-w S01` or `-w S01E01-S01E03`. |
|
||||
| `-w`, `--wanted` | Which episodes, e.g. `-w S01` or `-w S01E01-S01E03`. For music, which tracks, e.g. `-w 1-5` or `-w 2x3` for disc 2 track 3. |
|
||||
| `-o`, `--output` | Override the output directory for this run. |
|
||||
| `--list` | List the tracks that would be downloaded, then stop. |
|
||||
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -141,11 +141,16 @@ release fields instead of the season and episode ones. `{season}`, `{episode}` a
|
||||
| `{album}` | Album name |
|
||||
| `{track_number}` `{disc}` | Zero padded. `{disc}` is empty on a single-disc release |
|
||||
| `{track_total}` `{disc_total}` | Zero padded totals, empty when the service did not give them |
|
||||
| `{isrc}` `{upc}` `{label}` `{genre}` `{release_type}` `{explicit}` | As the service reported them, empty when absent |
|
||||
| `{isrc}` `{upc}` `{label}` `{genre}` | As the service reported them, empty when absent |
|
||||
| `{release_type}` | `album`, `single`, `ep`, and so on. A service that reports nothing gives `album` |
|
||||
| `{explicit}` | The word `Explicit` when the track is flagged, otherwise empty |
|
||||
|
||||
In a `file` hook `{title}` is the track name. See [Modes](#modes) for what an album hook
|
||||
puts in `{title}`.
|
||||
|
||||
`{year}` is empty when the service gives the release no year. `{ext}` is the container the
|
||||
track arrived in, such as `.flac`, because a music download is not muxed.
|
||||
|
||||
### Tagging IDs { #tagging-ids }
|
||||
|
||||
`{tmdb}`, `{imdb}` and `{tvdb}` carry the IDs unshackle is tagging the file with, read at
|
||||
@@ -191,9 +196,10 @@ subs = [p for p in sys.argv[i].removeprefix("--subs=").split("\n") if p]
|
||||
about whether the season is complete on disk. Downloading a single episode fires the
|
||||
hook for that season as soon as the episode lands.
|
||||
|
||||
A title that fails takes its group with it. If one episode of S01 fails, the S01 hook
|
||||
does not fire at all, because the run never finished everything it queued for that
|
||||
season.
|
||||
A title that fails stops the whole run. unshackle dispatches the `failure` hook for
|
||||
that title and then returns. It downloads no further title, and no `season` or `run`
|
||||
hook fires after that point. This applies to every title type. If one episode of S01
|
||||
fails, the S01 hook does not fire at all.
|
||||
|
||||
The hook is keyed to the folder, not to the season, so one season landing in several
|
||||
folders (`-q 1080,720` with a quality-dependent folder template) fires it once per folder,
|
||||
@@ -218,11 +224,17 @@ In a `file` hook `{title}` and `{title_raw}` are the track name. In an album hoo
|
||||
the album name, and the per-track variables `{track_number}`, `{disc}` and `{isrc}` are
|
||||
empty, in the same way that `{episode}` is empty in a season hook.
|
||||
|
||||
An album hook obeys the same rule as a season hook. It fires when the last track **this run
|
||||
queued** for that album lands. This does not mean that the album is complete on disk. A
|
||||
one-track download fires the album hook as soon as that track lands. If a track fails, the
|
||||
run stops there, so the album hook never fires, in the same way as an episode and its
|
||||
season.
|
||||
|
||||
## Events { #events }
|
||||
|
||||
`success` runs after the file has moved to its final path. `failure` runs when the download
|
||||
fails, with `{filepath}` empty and `{error}` set. A music `failure` hook describes the
|
||||
release rather than a track, because the whole album stops when one track fails.
|
||||
fails, with `{filepath}` empty and `{error}` set. Each track of an album is its own title.
|
||||
A music `failure` hook therefore describes the track that failed, not the release.
|
||||
|
||||
!!! warning "`failure` only fires in `file` mode"
|
||||
A `failure` entry with `mode: season` or `mode: run` never runs. Leave `mode` at its
|
||||
|
||||
Reference in New Issue
Block a user