feat(music): pick the song container from the codec in the downloaded file

FFprobe sets the codec; DTS goes to .mka, PCM to .flac or .wav, AC-4/MPEG-H/DTS-UHD to MP4Box. The remux also cuts --sample, --no-mux no longer applies to a Song, and the tagger adds cover fallback, WebP, ARTISTS and ReplayGain. Song tag data now reaches a --remote client.
This commit is contained in:
imSp4rky
2026-11-14 19:58:21 -07:00
parent 703bcf872c
commit 7a5ad5024a
24 changed files with 785 additions and 235 deletions
+3 -3
View File
@@ -370,9 +370,9 @@ Common constructor fields:
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.
A `Song` skips the muxer: the framework reads the codec from the downloaded file
and gives the file its container, so a service does not rename the track path.
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
+6 -8
View File
@@ -202,7 +202,9 @@ Each subclass adds its own required fields on top of `id_` and `service`:
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.
nested `metadata` sub-dict, with the nested keys winning, so either shape works. The keys
that a remote client gets back are `MUSIC_DATA_KEYS` in `unshackle/core/titles/music.py`,
for example `artists` (a list) and `replaygain_track_gain`.
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
@@ -216,13 +218,9 @@ Each subclass adds its own required fields on top of `id_` and `service`:
`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.
Core picks the container of a `Song` from the codec in the downloaded file, so a music
service does not rename the track path. Read
[Music output files](../guide/output-and-naming.md#music-output-files).
The **MUSIC_EXAMPLE** reference service, at `unshackle/services/MUSIC_EXAMPLE/`, shows
the full music surface.
+2 -2
View File
@@ -268,7 +268,7 @@ download. For example, `dovi_tool` only matters if you download Dolby Vision vid
| Tool | Binary looked up | Why it is needed |
| --- | --- | --- |
| Bento4 (`mp4decrypt`) | `mp4decrypt` | Alternative DRM decryptor to shaka-packager |
| MP4Box (GPAC) | `MP4Box` | Muxing DTS:X Profile 2 (DTS-UHD) and MPEG-H audio, and 3D video |
| MP4Box (GPAC) | `MP4Box` | Muxing DTS:X Profile 2 (DTS-UHD) and MPEG-H audio, and 3D video; remuxing AC-4, MPEG-H and DTS-UHD music |
| dovi_tool | `dovi_tool` | Dolby Vision metadata handling |
| HDR10Plus_tool | `hdr10plus_tool` | HDR10+ metadata handling |
| SubtitleEdit | `seconv`, `SubtitleEdit` | Subtitle conversion (the `SeConv` CLI from 5.x) |
@@ -280,7 +280,7 @@ download. For example, `dovi_tool` only matters if you download Dolby Vision vid
| Docker | `docker` | Gluetun VPN proxy support |
| Git | `git` | Fetching and updating remote service repositories |
!!! info "MP4Box is only used for DTS:X Profile 2, MPEG-H and 3D video"
!!! info "MP4Box is only used for DTS:X Profile 2, MPEG-H, AC-4 music and 3D video"
Titles with DTS:X Profile 2 (DTS-UHD) or MPEG-H audio, or a stereoscopic 3D video track, mux to
MP4; everything else muxes to MKV with `mkvmerge`. Install with `apt install gpac`.
+4 -3
View File
@@ -865,8 +865,8 @@ By default, `mkvmerge` muxes the tracks of a movie or an episode into a single M
window, or the whole file when the track is one file.
!!! note "Music is never muxed"
A song skips the muxer. unshackle keeps the container that the service
delivered, such as `.flac`, `.m4a`, or `.mp3`, and writes the metadata into that file.
A song skips the muxer. unshackle remuxes it into the container of its codec,
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
@@ -874,7 +874,8 @@ By default, `mkvmerge` muxes the tracks of a movie or an episode into a single M
`--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. No `success` post-script operates under `--no-mux`, because
that run writes no muxed output. A `failure` post-script still operates if the download fails.
that run writes no muxed output; a song is the exception, because `--no-mux` does not apply to it.
A `failure` post-script still operates if the download fails.
```shell title="Hand each finished file to an uploader"
unshackle dl --postscript "python /opt/upload.py {filepath} --service={service}" EXAMPLE 81234567
+36 -16
View File
@@ -262,22 +262,37 @@ The per-kind folder config keys are `movies`, `series`, `sports`, `songs`, and `
## Music output files
A song does not go through the muxer. Music services deliver audio in a fragmented MP4, which is
not a music file. Its header states a length of zero, so a player stops after the first fragment.
FLAC inside an MP4 is also not a FLAC stream. A rename cannot correct either fault, so unshackle
remuxes each downloaded audio track into the container of its codec, then moves the file to its
final path.
A song does not go through the muxer. Music services deliver audio as a fragmented MP4, a WebM
file or a plain audio file, and every download has a `.mp4` name. A fragmented MP4 header states a
length of zero, so a player stops after the first fragment. A rename cannot correct this, so
unshackle remuxes each downloaded audio track into the container of its codec, then moves the file
to its final path.
FFprobe reads the codec from the downloaded file. The codec that the service declares is used only
when FFprobe cannot read the file.
| Codec | Extension |
|---|---|
| FLAC | `.flac` |
| PCM, integer, 24-bit or less | `.flac` |
| PCM, more than 24-bit or floating point | `.wav` |
| Opus | `.opus` |
| Vorbis | `.ogg` |
| MP3 | `.mp3` |
| AAC, ALAC, and all other codecs | `.m4a` |
| DTS, DTS-HD | `.mka` |
| AAC, ALAC, Dolby Digital, Dolby Digital Plus, and all other codecs | `.m4a` |
unshackle re-encodes FLAC in this step. The audio does not change, but the new file states its
true length. A plain copy cannot, because it keeps the source header and the zero in it.
unshackle encodes FLAC and PCM to FLAC in this step. The audio does not change, but the new file
states its true length. A plain copy cannot, because it keeps the source header and the zero in it.
FLAC cannot hold floating-point PCM, and FFmpeg writes 32-bit FLAC only as an experimental feature,
so that audio goes to `.wav`.
FFmpeg writes DTS into MP4 with the wrong codec entry, so DTS goes to Matroska. AC-4, MPEG-H and
DTS:X Profile 2 (DTS-UHD) go through MP4Box, because FFmpeg cannot write them into MP4. MP4Box
needs [GPAC](../getting-started/installation.md#recommended-and-optional-tools).
With `--sample`, the same step cuts the song to the sample window. If the window starts after the
end of the song, unshackle shows a warning and keeps the full download.
unshackle writes no `.mkv` file for music, and the [muxing options](#muxing-options) below do not
apply.
@@ -295,13 +310,18 @@ around it.
### 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.
comments for FLAC and Ogg, ID3 frames for MP3 and WAV, MP4 atoms for M4A, and Matroska tags for
MKA. The tags hold the track title, artist, album artist, track and disc numbers, date, genre,
ISRC, UPC, copyright, and record label. When the service gives them, unshackle also writes each
artist of a song with several artists into `ARTISTS`, and the ReplayGain values into
`REPLAYGAIN_*` tags. An Opus file gets no `REPLAYGAIN_*` tags, because RFC 7845 advises against
them in Opus files. When `tag` and `tag_group_name` are set, unshackle also writes the group name
into a `GROUP` tag.
unshackle embeds cover art when the service gives an artwork URL. It downloads the image
and puts it in the file.
and puts it in the file. With no artwork URL, it uses the first image attachment of the song. It
converts a WebP image to JPEG. It skips an image larger than 16 MiB, the FLAC limit, with a
warning. An MKA file gets no cover art.
unshackle writes lyrics 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
@@ -314,8 +334,8 @@ the timed lines from that file. The file also comes back to the client from a re
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.
!!! note "`--no-mux` does not change music output"
A song has no mux step, so [`--no-mux`](cli-reference.md#output-muxing-files) does not apply to it. The song gets the same file name, metadata, cover art and lyrics file as in a run without the flag.
## Muxing options
@@ -486,7 +506,7 @@ A few muxing behaviors are automatic and not configurable:
- Descriptive audio gets the visually-impaired flag. SDH subtitles get the hearing-impaired flag. Forced subtitles get the forced flag.
- unshackle writes the chapters from the title's chapter list (see [Chapters](#chapters-naming) below).
The `--no-mux` flag skips muxing entirely and writes the individual track files, each with a track-type suffix, into the same output/folder structure. A track that shaka-packager decrypts has its date set to 1970-01-01; other tracks keep the date of the source file.
The `--no-mux` flag skips muxing entirely and writes the individual track files, each with a track-type suffix, into the same output/folder structure. An audio track in an MP4 container gets the `.m4a` extension. A track that shaka-packager decrypts has its date set to 1970-01-01; other tracks keep the date of the source file.
## Sample files
+2 -1
View File
@@ -328,7 +328,8 @@ credentials and `token=` / `api_key=` query parameters are also masked.
## Limitations
- No `success` post-script operates under `--no-mux` or `--skip-dl`, because neither writes a
muxed output. A `failure` post-script still runs under `--no-mux` if the download itself fails.
muxed output. A song is the exception: `--no-mux` does not apply to it, so its post-scripts
operate. A `failure` post-script still runs under `--no-mux` if the download itself fails.
- `failure` post-scripts only operate in `file` mode.
- There is no webhook sender. Call `curl` from a script instead.
- Script output is not captured. Redirect it inside your own script if you want a log.