mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
feat(titles): add optional Episode.part, selected with -w s1e1.2
Opt-in and additive: an episode without a part behaves exactly as before.
This commit is contained in:
@@ -304,11 +304,14 @@ Common constructor fields:
|
||||
|
||||
```python
|
||||
Episode(id_, service, title, season, number, name=None, year=None,
|
||||
language=None, data=None, description=None, air_date=None)
|
||||
language=None, data=None, description=None, air_date=None,
|
||||
part=None)
|
||||
```
|
||||
|
||||
Pass `air_date` for daily/sports content to name by date instead of
|
||||
`SxxExx`. Return a `Series([...])` of episodes.
|
||||
`SxxExx`. Return a `Series([...])` of episodes. Pass `part` only when the
|
||||
service splits one episode into several videos, see
|
||||
[Split episodes](#split-episodes).
|
||||
|
||||
=== "Song"
|
||||
|
||||
@@ -328,6 +331,52 @@ Common constructor fields:
|
||||
framework path to reuse cached results and gracefully fall back to stale data
|
||||
when the API errors. It also applies the per-service `title_map` config.
|
||||
|
||||
#### Split episodes {#split-episodes}
|
||||
|
||||
Some services split one logical episode into several separately playable videos, for
|
||||
example a variety show that ships episode 1 as *Episode 1 (Part 1)*, *(Part 2)* and
|
||||
*(Part 3)*. `Episode.number` stays an `int`. The optional `part` carries the index.
|
||||
|
||||
```python
|
||||
Episode(..., season=1, number=1, part=1, name="The Reckoning")
|
||||
Episode(..., season=1, number=1, part=2, name="The Reckoning")
|
||||
```
|
||||
|
||||
1. `part` counts from 1, contiguous. `part=0` is rejected, and `-w` can only address
|
||||
parts up to 99.
|
||||
2. Every part of one episode shares the same `season` and `number`.
|
||||
3. Each part is a separate `Episode` with its own unique `id_`.
|
||||
4. Unsplit episode: leave `part` unset. Never pass `part=1` for a whole episode.
|
||||
5. Do not put the part in `name` or `number`. Pass a clean `name` (or `None`) and set
|
||||
`part`.
|
||||
6. `part` is a split *episode*, not a split *season*. A service listing *Season 1 Part 2*
|
||||
describes a group of episodes. Keep numbering those in sequence, the way the shipped
|
||||
services do.
|
||||
|
||||
`part` accepts an `int` or a digit string. A value that is not an integer raises
|
||||
`TypeError`. A value of `0` or less raises `ValueError`.
|
||||
|
||||
!!! warning "The framework never infers a part from the episode name"
|
||||
Rule 6 is the reason. Shipped services already use the word "Part" to mean a *season*
|
||||
part, so name-sniffing would misread every one of them. The part has to arrive
|
||||
structurally, as the `part` argument.
|
||||
|
||||
!!! tip "Clean the part out of the name"
|
||||
`Episode` discards any `name` that starts with `Episode <number>`, so a raw
|
||||
`"Episode 1 (Part 2): The Reckoning"` leaves you with no episode name at all. Strip
|
||||
the prefix before you pass it, or pass `None`.
|
||||
|
||||
Once `part` is set, the filename gains a part token (`S01E01.Part.2`, see
|
||||
[Output & naming](../guide/output-and-naming.md#split-episodes)), `-w S01E01.2` selects a
|
||||
single part while a bare `-w S01E01` selects them all, and the `--list-titles` tree labels
|
||||
the episode `01.2`. The REST API reports it as an extra `part` key on the serialized
|
||||
title, described in [Endpoints](rest-api/endpoints.md#post-apilist-titles).
|
||||
|
||||
!!! note "Opt-in and additive"
|
||||
Leaving `part` unset costs you nothing: filenames, sort order, `-w` matching and the
|
||||
API JSON are identical to a plain episode, so a service that does not split episodes
|
||||
has nothing to adopt.
|
||||
|
||||
### `get_tracks`: required
|
||||
|
||||
Given one title, return a `Tracks` object holding `Video`, `Audio`, and
|
||||
|
||||
@@ -223,6 +223,12 @@ 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`.
|
||||
|
||||
!!! info "`part` on a split episode"
|
||||
A service that splits one episode into several separately playable videos reports each
|
||||
one as its own episode. Those titles share a `season` and `number` and add a `part`
|
||||
integer counting from 1. No other title carries the key, so a client that ignores it
|
||||
is unaffected. See [Split episodes](../creating-a-service.md#split-episodes).
|
||||
|
||||
| Status | Error code | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `200` | - | Titles returned. |
|
||||
@@ -248,9 +254,10 @@ List the video, audio, and subtitle tracks for a title. For series, you can scop
|
||||
| `wanted` | string | no | Episode/season range (e.g. `"S01E01-S01E03"`). |
|
||||
| `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`). |
|
||||
| `profile`, `proxy`, `no_proxy`, `cdm_type` | - | no | As on `list-titles`. |
|
||||
|
||||
When both `season` and `episode` are given, they are combined into a `"{season}x{episode}"` selector.
|
||||
When both `season` and `episode` are given, they are combined into a `"{season}x{episode}"` selector. Adding `part` narrows that to `"{season}x{episode}.{part}"`, one part of a [split episode](../creating-a-service.md#split-episodes); without it you get every part of the episode. `part` is read only when `season` and `episode` are both given, and `wanted` takes precedence over all three. To scope a part by range, put it in `wanted` instead (`"S01E01.2"`).
|
||||
|
||||
=== "Single title / movie: Response `200`"
|
||||
|
||||
@@ -361,7 +368,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. |
|
||||
| `wanted` | string[] | `[]` | Episode/season selectors. Accepts the part form, `"S01E01.2"`. |
|
||||
| `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. |
|
||||
|
||||
@@ -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`). |
|
||||
| `-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. |
|
||||
| `--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. |
|
||||
|
||||
@@ -368,6 +368,45 @@ with `-` to exclude it.
|
||||
unshackle dl -w S01-S05,-S03 EXAMPLE 81234567
|
||||
```
|
||||
|
||||
=== "Split episodes"
|
||||
|
||||
```shell
|
||||
# Part 2 of episode 1 only
|
||||
unshackle dl -w S01E01.2 EXAMPLE 81234567
|
||||
|
||||
# Parts 1 through 3 of episode 1
|
||||
unshackle dl -w S01E01.1-S01E01.3 EXAMPLE 81234567
|
||||
|
||||
# All of season 1 except episode 1 part 2
|
||||
unshackle dl -w S01,-S01E01.2 EXAMPLE 81234567
|
||||
```
|
||||
|
||||
### Split episodes
|
||||
|
||||
A few services split one episode into several separately playable videos. Where a service
|
||||
reports that, add `.N` after the episode to pick one part.
|
||||
|
||||
| Token | Selects |
|
||||
| --- | --- |
|
||||
| `S01E01` | All parts of episode 1. |
|
||||
| `S01E01.2` | Part 2 of episode 1, and nothing else. |
|
||||
| `S01E01.1-S01E01.3` | Parts 1 to 3 of episode 1. |
|
||||
| `S01,-S01E01.2` | All of season 1 except episode 1 part 2. Parts 1 and 3 are kept. |
|
||||
|
||||
A season token such as `S01` covers every episode and every part, so you only need `.N`
|
||||
when you want a part on its own.
|
||||
|
||||
!!! warning "A part range stays inside one episode"
|
||||
`S01E01.1-S01E01.3` is valid. `S01E01.1-S01E02.3` is rejected, because the parser
|
||||
cannot know how many parts episode 1 has and so cannot work out where the range ends.
|
||||
To span episodes, list the parts you want as separate tokens:
|
||||
`-w S01E01.2,S01E02.1`.
|
||||
|
||||
!!! note "A part of an unsplit episode selects nothing"
|
||||
`-w S01E02.2` on an episode that was never split is deliberately empty: it tells you
|
||||
the episode has no parts, rather than quietly handing you the whole episode. Run
|
||||
`--list-titles` to see which episodes carry parts.
|
||||
|
||||
### Other selection flags
|
||||
|
||||
| Flag | Behaviour |
|
||||
|
||||
@@ -126,11 +126,38 @@ Every variable below is valid in both output and folder templates. Values are de
|
||||
| `episode` | Zero-padded episode (`E%02d`) | `E05` |
|
||||
| `season_episode` | Combined | `S01E05` |
|
||||
| `episode_name` | Episode title | `Pilot` |
|
||||
| `part` | Part index of a [split episode](#split-episodes), empty otherwise | `2` |
|
||||
| `date` | ISO air date for daily/dated content | `2024-06-01` |
|
||||
|
||||
!!! note "Daily & sports content"
|
||||
When an episode has an air date, unshackle switches to date-based naming automatically: `season` and `season_episode` become the formatted air date, `episode` and `year` are cleared, and `{date}` holds the ISO date. The date's internal separator (dots or spaces) follows your `series` template style.
|
||||
|
||||
### Split episodes {#split-episodes}
|
||||
|
||||
A few services split one episode into several separately playable videos. Where a service reports that, the part index is folded into `{episode}` and `{season_episode}`, so the stock `series` template names it with no change to your config.
|
||||
|
||||
```text
|
||||
Show.Name.S01E01.Part.2.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
The part token sits immediately after the episode token, before the quality tags, and the group tag stays last. Its separator follows your template's own style, the same way dated content does, so a spaced template gives `S01E01 Part 2` instead.
|
||||
|
||||
The part is left out of the folder name, so every part of an episode lands in the same season folder:
|
||||
|
||||
```text
|
||||
The.Show.S01.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG/
|
||||
├─ The.Show.S01E01.Part.1.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
├─ The.Show.S01E01.Part.2.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
└─ The.Show.S01E02.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
The same holds for a folder template of your own: `{season}` never picks up the part, and `{season_episode}` drops it inside a folder template. An episode with no part is named like any other episode.
|
||||
|
||||
!!! note "What media servers do with the part token"
|
||||
The form above follows scene practice for genuinely split episodes. Kodi stacks the parts back into one playable episode automatically. Jellyfin lists two versions of S01E01, still correct and playable, because its stacking pattern expects the part marker at the end of the name. Plex's scanner is closed-source, so its handling of text after the part token is unverified.
|
||||
|
||||
For custom templates there is also a standalone [`{part}`](../reference/configuration/output.md#output_template) variable. You rarely want it, see the caveat on that page.
|
||||
|
||||
### Audio
|
||||
|
||||
| Variable | Meaning | Example |
|
||||
|
||||
@@ -34,13 +34,23 @@ values, filesystem-unsafe characters (`< > : " / \ | ? *`), empty templates, and
|
||||
folder-kind keys.
|
||||
|
||||
??? example "All valid template variables"
|
||||
`title`, `year`, `season`, `episode`, `season_episode`, `episode_name`, `date`,
|
||||
`quality`, `resolution`, `source`, `tag`, `track_number`, `artist`, `album_artist`,
|
||||
`album`, `disc`, `track_total`, `disc_total`, `release_type`, `genre`, `explicit`,
|
||||
`isrc`, `upc`, `label`, `audio`, `audio_channels`, `audio_full`, `atmos`, `dual`,
|
||||
`multi`, `dubbed`, `video`, `hdr`, `hfr`, `edition`, `repack`, `lang_tag`,
|
||||
`title`, `year`, `season`, `episode`, `season_episode`, `episode_name`, `part`,
|
||||
`date`, `quality`, `resolution`, `source`, `tag`, `track_number`, `artist`,
|
||||
`album_artist`, `album`, `disc`, `track_total`, `disc_total`, `release_type`, `genre`,
|
||||
`explicit`, `isrc`, `upc`, `label`, `audio`, `audio_channels`, `audio_full`, `atmos`,
|
||||
`dual`, `multi`, `dubbed`, `video`, `hdr`, `hfr`, `edition`, `repack`, `lang_tag`,
|
||||
`title_type`.
|
||||
|
||||
!!! warning "`{part?}` is already inside `{season_episode}`"
|
||||
`part` holds the bare part index of a
|
||||
[split episode](../../guide/output-and-naming.md#split-episodes) (`2`) and is empty on
|
||||
every other title. unshackle folds it into `{episode}` and `{season_episode}`, so the
|
||||
shipped `series` template names split episodes correctly with no edit. Putting
|
||||
`{part?}` next to either of them renders the part twice (`S01E01.Part.2.2`). Use the
|
||||
standalone variable only in a template that names neither `{episode}` nor
|
||||
`{season_episode}`, and note that it is always empty in a folder template, since
|
||||
folders do not carry the part.
|
||||
|
||||
## Tagging & naming keys
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|
||||
Reference in New Issue
Block a user