Compare commits

...

375 commits

Author SHA1 Message Date
Nojyto
658f5ef3e1
[BUGFIX] Fix "File name too long" error when a title is used as a folder name (#1479)
Some checks failed
ytld-sub CI (Linux) / test-lint (push) Has been cancelled
ytld-sub CI (Linux) / test-unit (push) Has been cancelled
ytld-sub CI (Windows) / test-unit (push) Has been cancelled
ytld-sub CI (Windows) / test-integration (push) Has been cancelled
ytld-sub CI (Windows) / test-integration-prebuilt-presets (push) Has been cancelled
ytld-sub CI (Windows) / test-e2e (push) Has been cancelled
ytld-sub CI (Linux) / test-integration (push) Has been cancelled
ytld-sub CI (Linux) / test-integration-prebuilt-presets (push) Has been cancelled
ytld-sub CI (Linux) / test-e2e (push) Has been cancelled
ytld-sub Docker GUI Build / version (push) Has been cancelled
ytld-sub Docker GUI Build / build (3.12) (push) Has been cancelled
ytld-sub Docker Ubuntu Build / version (push) Has been cancelled
ytld-sub Docker Ubuntu Build / build (3.12) (push) Has been cancelled
ytld-sub Docker Build / version (push) Has been cancelled
ytld-sub Docker Build / build (3.12) (push) Has been cancelled
ytld-sub Release / version (push) Has been cancelled
ytld-sub CI (Linux) / codecov-upload (push) Has been cancelled
ytld-sub Docker GUI Build / package-arm64 (push) Has been cancelled
ytld-sub Docker GUI Build / package-amd64 (push) Has been cancelled
ytld-sub Docker GUI Build / deploy (push) Has been cancelled
ytld-sub Docker Ubuntu Build / package-arm64 (push) Has been cancelled
ytld-sub Docker Ubuntu Build / package-amd64 (push) Has been cancelled
ytld-sub Docker Ubuntu Build / deploy (push) Has been cancelled
ytld-sub Docker Build / package-arm64 (push) Has been cancelled
ytld-sub Docker Build / package-amd64 (push) Has been cancelled
ytld-sub Docker Build / deploy (push) Has been cancelled
ytld-sub Release / build-linux (push) Has been cancelled
ytld-sub Release / build-windows (push) Has been cancelled
ytld-sub Release / github-release (push) Has been cancelled
ytld-sub Release / pypi-publish (push) Has been cancelled
Some presets put the video title into a folder name (not just the file name). The code only shortened the file name, never the folder name. So a title with multi-byte characters (like bold/fancy Unicode, which take 4 bytes each) could make the folder name go over the OS 255-byte limit and crash with OSError: [Errno 36] File name too long.

This fix shortens every folder in the path too, not just the file name, while keeping the file extension. output_directory is left alone so real folders aren't touched.

Added a test with a title made of 4-byte bold Unicode characters.

Relates to #1092, #1189

-- Thanks @Nojyto for the contribution!
2026-06-23 09:05:10 -07:00
dependabot[bot]
1c4633b815
Bump yt-dlp from 2026.3.17 to 2026.6.9 (#1475)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2026.3.17 to 2026.6.9.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.03.17...2026.06.09)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-version: 2026.6.9
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 04:17:20 -04:00
dependabot[bot]
19b0e621ca
[DEV] Bump ruff from 0.15.12 to 0.15.16 (#1474)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.12 to 0.15.16.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.12...0.15.16)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 13:59:07 -07:00
Jesse Bannon
4421c4eede
[BUGFIX] Fix output sanitization type checking (#1467)
In the inspection output, we check function's return types via issubclass to see if an optimization can be made. Guard against this check in case the return type isn't a class -- it could be a Union[..., ...] from a conditional.
2026-05-10 09:19:27 -07:00
Jesse Bannon
b195f37412
[DEV] Disable soundcloud e2e test in CI (#1448) 2026-05-10 08:59:32 -07:00
dependabot[bot]
108c7cfa14
[DEV] Bump ruff from 0.15.11 to 0.15.12 (#1465)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.11 to 0.15.12.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.11...0.15.12)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 15:50:05 -07:00
dependabot[bot]
772f01a734
[DEV] Bump ruff from 0.15.10 to 0.15.11 (#1462)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.10 to 0.15.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.10...0.15.11)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-19 08:21:50 -07:00
Xavier Salazar
bb96f00ca5
[DOCS] Update inspect usage in first config docs (#1460)
Remove invalid CLI args in example
2026-04-14 22:35:44 -07:00
Jesse Bannon
915d445e21 [DOCS] Fix command arg type 2026-04-13 11:00:29 -07:00
Jesse Bannon
970c74ba45
[FEATURE] inspect subcommand (#1407)
Subscription file syntax is designed to minimize boiler-plate when authoring new subscriptions.
You can now unpack any subscription using the ``inspect`` sub-command to see its boiler-plate *preset format*.

```
ytdl-sub inspect --config /path/to/config.yaml --match "BBC News" /path/to/subscriptions.yaml
```

This can be utilized for numerous purposes including:

* Ensuring your custom preset is getting applied correctly.
* Figuring out which variables set things like file names, metadata, etc.
* Understanding how subscription syntax translates to preset representation.

The default ``--level`` of inspect will fill in defined variables. Using ``--level original`` will present the subscription's raw layout with no fill.
2026-04-13 10:10:58 -07:00
dependabot[bot]
e34b9b6295
[DEV] Bump ruff from 0.15.8 to 0.15.10 (#1458)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.8 to 0.15.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.8...0.15.10)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 09:31:50 -07:00
dependabot[bot]
49e3b5f887
[DEV] Bump ruff from 0.15.7 to 0.15.8 (#1456)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.7 to 0.15.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.7...0.15.8)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 13:50:14 -07:00
dependabot[bot]
5cb9bbea9c
[DEV] Bump ruff from 0.15.6 to 0.15.7 (#1455)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.6 to 0.15.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.6...0.15.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 16:53:47 -07:00
dependabot[bot]
52a1ceaeea
Bump yt-dlp[default] from 2026.3.13 to 2026.3.17 (#1454)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2026.3.13 to 2026.3.17.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.03.13...2026.03.17)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.3.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 21:16:50 -07:00
Jesse Bannon
1156e0070a
[DEV] Make inspect resolution more human readable (#1451)
Removes a lot of the boilerplate output around `concat` and `sanitize`.
2026-03-13 16:27:56 -07:00
dependabot[bot]
efbac43b9f
[DEV] Bump ruff from 0.15.5 to 0.15.6 (#1449)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.5 to 0.15.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.5...0.15.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 13:53:38 -07:00
dependabot[bot]
a6aea037c3
Bump yt-dlp[default] from 2026.3.3 to 2026.3.13 (#1450)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2026.3.3 to 2026.3.13.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.03.03...2026.03.13)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.3.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 13:34:35 -07:00
dependabot[bot]
cb3952de5c
[DEV] Bump pylint from 4.0.1 to 4.0.5 (#1437)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 4.0.1 to 4.0.5.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v4.0.1...v4.0.5)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 4.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 12:36:32 -07:00
dependabot[bot]
0983a594fd
Update pytest requirement from <9.0,>=7.2 to >=7.2,<10.0 (#1380)
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.0...9.0.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 11:39:56 -07:00
Jesse Bannon
57fd6901e4
[DEV] Linter: Use ruff over black and isort (#1445)
Will eventually replace pylint with ruff
2026-03-09 11:39:01 -07:00
Jesse Bannon
a758a8870d
[DOCS] Overhaul configuration file getting started guide (#1172)
Updates the configuration file's docs to be cleaner. Removes sphinx autodocs entirely in favor of our custom-built one.
2026-03-09 10:18:50 -07:00
Ross Patterson
ff66ff5be0
fix(docker): Add default crontab download command (#1321)
* docs(docker): Recommended image DEFAULT_WORKSPACE

* fix(docker): Add default crontab download command

Clarify the `/config/ytdl-sub-configs/cron` script with explanatory comments and a
default `--dry-run` command. Also change the wrapper script to echo commands for easier
debugging. To update an existing script, move the old script aside, restart the
container to regenerate it, and edit the new script.

Also clarify the Automating page in the Getting Started guide docs.

* fix(docker): Unintentional unattended dry runs

[PR feedback](https://github.com/jmbannon/ytdl-sub/pull/1321#discussion_r2315215600)
prompted me to reconsider having a default command at all. We should assume,
unfortunately, that many new users will just skim the docs enough to enable the image's
cron integration but not actually incrementally test their configuration. In those
cases, they'd end up sending dry-run non-download requests for all their subscriptions
every 6 hours for no good reason. There's just no way to provide a default command that
isn't also providing a footgun.

* docs(docker): More open cron schedule generator

From [PR
feedback](https://github.com/jmbannon/ytdl-sub/pull/1321#discussion_r2320521024), this
seems like less of an ad than the previous and the source for the page is itself open source.

* docs(docker): Document image environment footgun

From [PR
feedback](https://github.com/jmbannon/ytdl-sub/pull/1321#discussion_r2320515419).

* docs(automate): Avoid external link 404 responses

* docs(automate): False simultaneous run warning

* docs(automate): Clarify Docker daemon restarts

* docs(automate): Revert run start non-recommended

* docs(automate): Remove footgun manual run command

Addressing this underlying issue requires more thought and should be a separate PR.

* docs(automate): Restore env var footgun in example
2026-03-09 09:27:27 -07:00
dependabot[bot]
64820daf6c
Bump yt-dlp[default] from 2026.2.21 to 2026.3.3 (#1440)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2026.2.21 to 2026.3.3.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.02.21...2026.03.03)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 19:21:53 -08:00
dependabot[bot]
3b5f9cba58
Bump yt-dlp[default] from 2026.2.4 to 2026.2.21 (#1438)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2026.2.4 to 2026.2.21.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.02.04...2026.02.21)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.2.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 15:10:39 -08:00
dependabot[bot]
e300c3d23a
Bump yt-dlp[default] from 2026.1.31 to 2026.2.4 (#1432)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2026.1.31 to 2026.2.4.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.01.31...2026.02.04)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.2.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-04 14:32:42 -08:00
dependabot[bot]
d526545abb
Bump yt-dlp[default] from 2026.1.29 to 2026.1.31 (#1430)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2026.1.29 to 2026.1.31.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2026.01.29...2026.01.31)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.1.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 20:27:33 -08:00
Jesse Bannon
ced547b14a
[FEATURE] Arg to shuffle subscriptions (#1263)
Closes https://github.com/jmbannon/ytdl-sub/issues/1234

Adds `--shuffle` support which shuffles the order of subscriptions when downloading.
2026-02-02 10:23:08 -08:00
Jesse Bannon
c163f9766a
[BACKEND] Configurable resolution level when validating variables (#1415)
Expands variable validation to also include support for partial resolution.

In short, this will partially execute script code until it hits unresolved runtime variables, and store that as the new script representation. This functionality will allow for the upcoming `inspect` command, which will show users their script code in action without having to dry-run.
2026-02-02 09:50:52 -08:00
dependabot[bot]
6b7805c6e1
Bump yt-dlp[default] from 2025.12.8 to 2026.1.29 (#1428)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.12.8 to 2026.1.29.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.12.08...2026.01.29)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2026.1.29
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-29 21:24:07 -08:00
Jesse Bannon
d6eda27371
[BACKEND] Less string casting behind the scenes (#1425)
Simplifies internal scripts by not casting things to String when unnecessary.
2026-01-27 10:15:32 -08:00
Jesse Bannon
d37945db7e
[HOTFIX] Fix StringFormattingException regarding keep_max_files (#1427)
Closes this issue from last release: https://github.com/jmbannon/ytdl-sub/issues/1426
2026-01-26 17:30:44 -08:00
Jesse Bannon
a097c6a476
[BACKEND][v2] More flexible formatter post_process (#1424)
Makes the underlying formatting process better handle native (non-string) types in a more flexible way. 2nd attempt since the last one got reverted due to breaking changes.
2026-01-25 22:35:51 -08:00
Jesse Bannon
97ecae79db
[DEV] More tests around passed-in ytdl-options (#1423) 2026-01-25 09:55:26 -08:00
Jesse Bannon
c1431c8d55
Revert "[BACKEND] More flexible formatter post_process (#1421)" (#1422)
This reverts commit 60cdbad8c7.

Caused a breaking change to ytdl_options
2026-01-24 00:22:18 -08:00
Jesse Bannon
60cdbad8c7
[BACKEND] More flexible formatter post_process (#1421)
Makes the underlying formatting process better handle native (non-string) types in a more flexible way.
2026-01-23 16:36:17 -08:00
Jesse Bannon
264e458c1c
[BACKEND] Cache cycle checks (#1420)
Small speedup to make cycle validation faster.
2026-01-23 10:58:14 -08:00
Jesse Bannon
c4e112e8d5
[BUGFIX] Fix soundcloud not downloading tracks (#1419)
The /tracks URL is currently broken in yt-dlp. Now uses the input URL which will download the entire artists' page instead to get non-album tracks.
2026-01-23 09:23:34 -08:00
Jesse Bannon
97df4dac1d
[BACKEND] Preserve dict formatter value (#1416)
Do not modify the original definition of dict validators.
2026-01-16 13:51:33 -08:00
Jesse Bannon
942c983f60
[BACKEND] Sorted video tags (#1417)
For reproducibility in tests.
2026-01-16 13:42:22 -08:00
Jesse Bannon
530d2f7666
[BACKEND] Partial resolve support for Scripts (#1410)
Adds the mechanism to partially resolve an entire script (aka every underlying variable in ytdl-sub). This will be used in the upcoming `inspect` sub-command where it will dump a subscription's raw contents at multiple levels of resolution.

Potentially useful to optimize script execution as well.
2026-01-12 17:34:34 -08:00
Jesse Bannon
41a5bf60aa
[BACKEND] Remove cast and legacy_bracket_safety usage (#1414)
Removes unused casts and calls to the legacy_bracket_safety function.
2026-01-11 15:47:50 -08:00
dependabot[bot]
3a07d058aa
[DOCS] Update sphinx requirement from <9,>=7 to >=7,<10 (#1411)
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.0.0...v9.1.0)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-version: 9.1.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 12:59:22 -08:00
Jesse Bannon
6b99c31e45
[BACKEND] Simplify script quote generation (#1409)
Internal script -> string generation used triple-quotes redundantly. Try to use them less to make it more human-readable.
2026-01-06 12:58:37 -08:00
Jesse Bannon
b2056bec5d
[BACKEND] Array support for URLs (#1406)
Allows for the `download` plugin to take in a script-based array of URLs. Prebuilt presets are now greatly reduced in size by constructing urls as an array versus 100+ separate URL variables.

Eventually, we can completely remove the limit of the number of URLs in a subscription.
2025-12-31 15:24:44 -08:00
Jesse Bannon
c46de048ca
[FEATURE] %range function (#1405)
Add `%range` function, equivalent to Python's `range`.
2025-12-31 09:06:08 -08:00
Jesse Bannon
869fc0b836
[BUGFIX] Fix custom function lambda cycle detection (#1404)
Cycles detection did not take into account custom functions' variable usage. This is now fixed.
2025-12-31 08:22:26 -08:00
Jesse Bannon
7ac525f875
[BUGFIX] Fix custom function lambda variable dependency issue, part 2 (#1403)
Part two of https://github.com/jmbannon/ytdl-sub/pull/1402 which actually completes the fix.
2025-12-30 23:46:07 -08:00
Jesse Bannon
d373935fff
[BUGFIX] Fix custom function lambda variable dependency issue (#1402)
There was a bug that did not properly check variable dependency when using lambda functions. It could still work depending on order of definitions. This fix should make it work no matter the order.
2025-12-30 19:53:15 -08:00
Jesse Bannon
1abe2a44f5
[BACKEND] Refactor validation, preparation for subscription dissect (#1398)
Completely rewrite how subscription validation is performed. Optimizes it quite a bit while also adding backend support for the upcoming `dissect` sub-command, where you can resolve any subscription into its 'raw' form for easier debugging when making scripting changes.
2025-12-30 17:03:59 -08:00
Jesse Bannon
344753cc63
[BACKEND] Optimize script resolution (#1400)
Speeds up scripting in the backend.
2025-12-29 17:38:34 -08:00
Jesse Bannon
0f96d8e24e
[BACKEND] Disallow override names conflicting with plugin names (#1399)
Disallow override variable names conflicting with plugin names.

May cause backward incompatible issues from `date_range` being an old override variable name for the `Only Recent` preset. Update this variable name to `only_recent_date_range` to resolve. More info in https://ytdl-sub.readthedocs.io/en/latest/deprecation_notices.html
2025-12-26 15:30:37 -08:00
dependabot[bot]
157a0b59c4
Bump yt-dlp[default] from 2025.11.12 to 2025.12.8 (#1396)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.11.12 to 2025.12.8.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.11.12...2025.12.08)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.12.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 15:41:54 -08:00
Jesse Bannon
a5ae69550e
[BACKEND] Validate write permissions on working directory and output directory (#1395)
Closes https://github.com/jmbannon/ytdl-sub/issues/1394
Should help debug user issues like https://github.com/jmbannon/ytdl-sub/issues/1148

Checks permissions on the following:
- working directory
- output directory
- cookiefile (if specified)

And will fail immediately with a verbose message.
2025-12-06 15:54:41 -08:00
Jesse Bannon
8d5d2be6a7
[FEATURE] TV Show Collection multi-url, s00 support (#1391)
Closes https://github.com/jmbannon/ytdl-sub/issues/1251, https://github.com/jmbannon/ytdl-sub/issues/1290
Required for https://github.com/jmbannon/ytdl-sub/issues/1383

Adds both s00 support and multi-url support per season, up to 11 URLs currently. This can and should be extended, but with a more principled approach instead of the current method: AI generating boiler-plate YAML 🙂 

```
   Plex TV Show Collection:
     = Music:
       "~Beyond the Guitar":
         s00_name: "Specials"
         s00_url:
           - "https://www.youtube.com/watch?v=vXzguOdulAI"
           - "https://www.youtube.com/watch?v=IGwYDvaGAz0"
         s01_name: "Videos"
         s01_url:
           - "https://www.youtube.com/c/BeyondTheGuitar"
           - "https://www.youtube.com/@BeyondTheGuitarAcademy"
         s02_name: "Covers"
         s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
```
2025-11-28 10:14:18 -08:00
Jesse Bannon
19f47cd914
[BACKEND] Optimize script interpreter (#1390)
Makes scripting runtime much faster.
2025-11-27 18:43:27 -08:00
Jesse Bannon
24e71ce733
[DOCKER] Make defaults script executable (#1388)
Fixes https://github.com/jmbannon/ytdl-sub/issues/1387 and some potential docker issues by not having this file ran on start.
2025-11-26 20:22:22 -08:00
Colin Davis
63753c5649
[RFC] Add preserve_mtime config (#1382)
Closes https://github.com/jmbannon/ytdl-sub/issues/386

Adds the ability to preserve mtime via Output Options. Usage:

```
output_options:
  preserve_mtime: True
```

Thanks @e1ven for the contribution!
2025-11-17 22:42:13 -08:00
dependabot[bot]
08180c0412
Bump yt-dlp[default] from 2025.10.22 to 2025.11.12 (#1381)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.10.22 to 2025.11.12.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.10.22...2025.11.12)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.11.12
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-12 16:37:17 -08:00
Maxim Borisov
a74c79d014
[DOCKER] Ensure correct cron log file permissions (#1378)
Co-authored-by: bedlamzd <vcs@bedlamzd.dev>
2025-11-09 09:51:57 -08:00
Colin Davis
2c33c3b49b
[DOCKER] Add curl-cffi and yt-dlp-ejs dependencies
Adds recommended deps from yt-dlp README
2025-11-07 17:01:48 -08:00
Snuffy2
e507e6ed46
[DOCKER] Disable pip break-system-packages (#1373)
Fixes `UPDATE_YT_DLP_ON_START` when set to 'nightly' as the pip command is missing --break-system-packages. Simplifies dockerfiles by setting this globally via pip config file.

Thanks @Snuffy2 for the contribution!
2025-11-06 21:08:10 -08:00
Jesse Bannon
ed55f3a3f7
[FEATURE] cli-to-sub argument to convert yt-dlp args to ytdl-sub (#1376)
Example usage:
```
$ ytdl-sub cli-to-sub -S vcodec:h264,res:480,acodec:m4a
ytdl_options:
  format_sort:
  - vcodec:h264
  - res:480
  - acodec:m4a
```
2025-11-06 21:05:18 -08:00
dependabot[bot]
c3ca3c6379
[DEV] Bump isort from 6.1.0 to 7.0.0 (#1359)
Bumps [isort](https://github.com/PyCQA/isort) from 6.1.0 to 7.0.0.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/6.1.0...7.0.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 08:45:00 -07:00
dependabot[bot]
d3e978e517
[DEV] Bump pylint from 3.3.8 to 4.0.1 (#1361)
* Bump pylint from 3.3.8 to 4.0.1

Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.8 to 4.0.1.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.8...v4.0.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* update fixes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Bannon <jbann1994@gmail.com>
2025-10-26 23:57:35 -07:00
dependabot[bot]
a5dd438398
Bump yt-dlp[default] from 2025.10.14 to 2025.10.22 (#1366)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.10.14 to 2025.10.22.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.10.14...2025.10.22)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.10.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 01:28:34 -07:00
Jesse Bannon
7364aac00c
[BUGFIX] Embedded null byte ffmpeg fix (#1363)
Closes https://github.com/jmbannon/ytdl-sub/issues/710

Fixes the notorious embedded null byte error seen during FFMPEG metadata writes when handling special characters.
2025-10-16 08:01:37 -07:00
dependabot[bot]
1b2e34bad4
Bump yt-dlp[default] from 2025.9.26 to 2025.10.14 (#1362)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.9.26 to 2025.10.14.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.09.26...2025.10.14)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.10.14
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 13:39:24 -07:00
Jesse Bannon
863eae68a2
[FEATURE] New parameter for required subtitle languages, and fixes (#1357)
Closes https://github.com/jmbannon/ytdl-sub/issues/1269

Adds the new paramter `languages_required`, which forces an entry to have this set of subtitle files, otherwise it will error.
In addition, better file checking has been added to ensure they exist before attempting to move.
2025-10-11 17:22:13 -07:00
Jesse Bannon
f62b47888f
[FEATURE] Ability to modify webpage_url (#1356)
Closes https://github.com/jmbannon/ytdl-sub/issues/1353

Provides a way to modify webpage_url before actual entry downloads. 
Use-case: remove `#__youtubedl_smuggle` parameter in the URL
2025-10-11 17:21:13 -07:00
dependabot[bot]
e6624ee329
[DEV] Bump isort from 6.0.1 to 6.1.0 (#1351)
Bumps [isort](https://github.com/PyCQA/isort) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/6.0.1...6.1.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-01 21:26:30 -07:00
Jesse Bannon
2deafc9f2d
[DOCKER] Include Deno dependency, other fixes (#1345)
Closes
- Deno requirement: https://github.com/jmbannon/ytdl-sub/issues/1340
  - yt-dlp announcement: https://github.com/yt-dlp/yt-dlp/issues/14404
- Docker cron log double-logging: https://github.com/jmbannon/ytdl-sub/issues/1202 (thanks @timetocode for https://github.com/jmbannon/ytdl-sub/pull/1330 !)
- Missing pip dependency when updating yt-dlp: https://github.com/jmbannon/ytdl-sub/issues/1346
2025-09-27 16:43:34 -07:00
Jesse Bannon
609bec8aaf
[DOCKER] Do not remove pip for headless image (#1347)
Closes https://github.com/jmbannon/ytdl-sub/issues/1346

Keeps pip in the image to be able to update yt-dlp
2025-09-27 15:11:56 -07:00
Jesse Bannon
cc3a36e66a [HOTFIX] Fix CRON_SCHEDULE env var 2025-09-27 14:58:29 -07:00
Jesse Bannon
4f8490d88c
[DOCKER] Env variable to update yt-dlp on start (#1344)
Closes https://github.com/jmbannon/ytdl-sub/issues/1154

For docker images, add the ability to set the variable `UPDATE_YT_DLP_ON_START`, which will update yt-dlp to the specified version if a new one exists.

Supports ``stable``,  ``nightly``, ``master``.
2025-09-27 14:09:57 -07:00
Rob Helgeson
548187abed
bump yt-dlp to 2025.9.26 (#1342)
manual, no dependabot
2025-09-26 21:30:29 -07:00
dependabot[bot]
1d75ea3f06
Bump yt-dlp[default] from 2025.9.5 to 2025.9.23 (#1338)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.9.5 to 2025.9.23.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.09.05...2025.09.23)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.9.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 15:09:32 -07:00
Jesse Bannon
45c155c039
[FEATURE] resolution_assert_ignore_titles to ignore specific low-resolution videos (#1336)
Closes https://github.com/jmbannon/ytdl-sub/issues/1313

Adds `resolution_assert_ignore_titles` variable to be able to ignore specific videos in a subscription where 360p is suspected to be the only option.

Ideally, it would be nice if ytdl-sub could determine whether 360p is actually the only resolution or not automatically, and download if it's true. We need to do more research to determine if this is possible.

In the meantime, the above variable should help manually get around this. Usage:

```
   # use tilda mode to set override variables to the subscription
   "~My Subscription":
     url: "https://youtube.com/@channel"
     resolution_assert_ignore_titles:
       - "This 360p Video Title"
```
2025-09-13 11:32:54 -07:00
dependabot[bot]
0c34f819de
Bump yt-dlp[default] from 2025.8.27 to 2025.9.5 (#1335)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.8.27 to 2025.9.5.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.08.27...2025.09.05)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.9.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-09 08:46:19 -07:00
Ross Patterson
72419a3e98
[DOCS] How to test subscription downloads (#1318)
Revise the "Getting Started" page for testing subscription downloads.
2025-09-06 08:11:06 -07:00
Jesse Bannon
a7cf361821
[DOCKER] Make default paths same as docker defaults (#1332)
Changes documentation and default configs to use the `/tv_shows` default path shown in the docker compose setup.
Also changes working directory and file lock directory to not be in /tmp, which often causes permission issues.
2025-09-05 22:50:47 -07:00
Ross Patterson
b9aefb4bea
[DOCS] Cleanup confusing remnant text (#1333)
I'm not even sure what word I may have been trying for with "software", but it makes no
sense as is and is better without.
2025-09-05 19:39:09 -07:00
Jesse Bannon
2e67a57bc9
[BACKEND] Make resolution print debug-level (#1326)
```
Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False
```
Spams during the initialization phase. Ideally, we should only print this once the subscription has begun downloading. Perhaps via a `print` plugin? Either way, set this to debug log level.
2025-09-01 14:33:46 -07:00
Jesse Bannon
3eb838eb40
[DOCKER] Set default workspace separately for GUI image (#1328)
Another attempt to fix some broken cron setups
2025-09-01 11:26:36 -07:00
Jesse Bannon
c2afe05c11
[DOCKER] Fix & issue in cron (#1325)
Fixes the following issue in some people's cron runs:
```
/bin/sh: 4: /config/.cron_wrapper: Syntax error: "&" unexpected
```
2025-08-31 18:59:59 -07:00
dependabot[bot]
1e0c9928a1
[DEV] Update pytest-rerunfailures requirement from <16,>=14 to >=14,<17 (#1317)
Updates the requirements on [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/14.0...16.0)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  dependency-version: '16.0'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 19:52:02 -07:00
Ross Patterson
321556ecf7
[DEV] docs: Sphinx stale cross refs false success (#1319)
* ci(docs): RTD Sphinx warnings false successes

Use the same options on readthedocs.com that we use locally to fail on all warnings,
mostly to better catch stale cross references.

* docs(usage): Yet another stale Sphinx cross ref

I confirmed the previous RTD CI fix in the previous commit by pushing it to a draft PR
before pushing this change and the RTD action failed with the warning this commit
fixes. After pushing this commit, the RTD action succeeded.

* build(docs): Sphinx stale cross refs false success

I figured out why I kept getting warnings for broken Sphinx cross-refs *after* the
changes that caused them have already been merged, changes I know I ran `$ make docs`
for before pushing. The issue is that by default Sphinx only builds changed files for
faster iterations while editing, but it only catches broken cross-refs when it builds
files. So if changing, for example, a section name in one page that is referenced from
another page that you do *not* change, then the warning will be missed until something
changes that other page, such as a pull or rebase.

I considered adding a separate `./Makefile` target for incremental builds
in the inner loop of making changes. But I opted to just remove the `--write-all` CLI
option locally while editing in the inner loop, because the uncommitted change will
remind me to revert it and run a full rebuild before pushing.
2025-08-29 15:59:06 -07:00
Ross Patterson
746370ef9a
[DOCS] usage: Stale CLI options help output (#1311)
* docs(quick): Stale Sphinx ref link target name

Not sure how I missed this in my local testing, but this should have been a part a
previous PR.

Refs #1304

* docs(usage): Stale CLI options help output

From [merged PR
feedback](https://github.com/jmbannon/ytdl-sub/pull/1305#discussion_r2304503286).

Refs #1305
2025-08-29 10:08:07 -07:00
Jesse Bannon
ccdad4d4cd
[FEATURE] Apply entry throttle protection sleep post-completion (#1316)
Closes https://github.com/jmbannon/ytdl-sub/issues/1294

Applies entry sleep per download *after* the download is fully complete and moved to the output folder.
2025-08-29 09:52:24 -07:00
dependabot[bot]
efd5a88f92
Bump yt-dlp[default] from 2025.8.22 to 2025.8.27 (#1314)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.8.22 to 2025.8.27.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.08.22...2025.08.27)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.8.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 01:00:34 -07:00
Jesse Bannon
df8b4a1df8
[FEATURE] Override variable support for throttle protection ranges (#1315)
Adds support for setting throttle_protection range values using static override variables. `sleep_per_download_s` has additional support for entry variables since it's used per entry.

We can now experiment with scaling this value based on entry attributes, such as duration. Example:

```
throttle_protection:
  sleep_per_download_s:
    min: >-
      {
        %mul(5.5, %pow( duration, 0.4 ))
      }
    max: >-
      {
        %mul(6.5, %pow( duration, 0.6 ))
      }
```
2025-08-28 23:55:57 -07:00
Ross Patterson
adb08ed5f9
[DOCS] start: Clarify subscriptions file tutorial (#1310)
* docs(presets): Stale copied prebuilt comment

* docs(start): How to configure subscriptions

Revise the subscriptions file Getting Started tutorial from the perspective of a new
user. The idea is that the ref docs describe a component as the code sees it, whereas
the how-to/tutorial/getting-started docs provide a narrative description of the
component from the external perspective of a typical new user.

* docs(start): Document subscription override mode

It's debatable whether this belongs here. Having moved onto my own custom presets, the
only use cases for override mode in my subscriptions file are for those series that also
have TVDB metadata. Those ytdl-sub downloads I integrate into my Sonarr library need
per-subscription overrides such as:

- Sonarr manages years 2020-2023 for one series and ytdl-sub fills in
  from there, so set a `sonarr_series_after: "20240101"` override

- each series needs a directory named per Sonarr's configuration with the TVDB ID, the subscription key,
  but also a series name prefix without the TVDB ID for each episode file, so set a
  `sonarr_series_prefix: "Foo Series (2020)"` override

So are there other use cases for override mode that are more common and less niche than
mine?

* docs(docker): Revert example bind mount volumes

Per [PR feedback](https://github.com/jmbannon/ytdl-sub/pull/1310#discussion_r2306067263).
2025-08-28 07:09:05 -07:00
Ross Patterson
283ccc8608
fix(docker): Default config available in WORKDIR (#1303)
Adds better support to run commands via `docker exec ...` by setting the working directory to be ytdl-sub's working directory.

Thanks @rpatterson for the contribution 😊
2025-08-27 20:17:57 -07:00
dependabot[bot]
d1e584d505
Bump yt-dlp[default] from 2025.8.20 to 2025.8.22 (#1306)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.8.20 to 2025.8.22.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.08.20...2025.08.22)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.8.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-27 09:10:28 -07:00
Ross Patterson
895e6ea972
[DOCS] Clarify debugging, support, reporting (#1305)
Reconcile the various debugging hints throughout the documentation and integrate into a
central debugging page. Includes clarification of the roles of ytdl-sub, yt-dlp, and
external services. Also adds cross references to the relevant bits across the docs.
2025-08-27 09:10:01 -07:00
Ross Patterson
abd75f8005
[DOCS] logs: persistent logs config (#1304)
* docs(logs): Mimick argparse option default format

I find it more readable to use punctuation to separate technical information, such as
required vs optional or default values, from narrative description. Follow argparse's
lead, and put such information in parens following the narrative description.

* docs(usage): Consistent structure, clarifications

* docs(config): Clarify persist_logs behavior/opts

Clarify the `persist_logs:` options per [Discord discussion](https://discord.com/channels/994270357957648404/1409161361853780060/1409602529460879431).
2025-08-27 09:04:36 -07:00
Ross Patterson
26922e1787
[DOCKER] Missing cron script stderr output (#1307) 2025-08-27 09:02:45 -07:00
Ross Patterson
28b5a81d19
[DOCS] faq: Clarify re-download date TZ margins (#1298)
* docs(faq): Clarify re-download date TZ margins

From [Discord
discussion](https://discord.com/channels/994270357957648404/1102703969266049174/1408092149492224060).

* docs(faq): Clarify date_range inclusivity

From [PR followup](https://github.com/jmbannon/ytdl-sub/pull/1298#issuecomment-3214598919).
2025-08-24 09:22:57 -07:00
Ross Patterson
a642028d7b
[DOCS] Clarify the = YAML key prefix (#1297)
This is another change that touches the generated docs, so after finishing my writing, I
copied the change to `./docs/source/config_reference/scripting/static_variables.rst`
into the corresponding docstring in
`./src/ytdl_sub/entries/variables/override_variables.py` and formatted it as a
docstring. Then I ran:

    $ REGENERATE_DOCS=1 tox exec -e "py" -- pytest tests/unit/docgen/test_docgen.py

But all that did was revert the changes to
`./docs/source/config_reference/scripting/static_variables.rst`. IOW, it did *not*
update the rST from the changed docstring.
2025-08-24 09:17:38 -07:00
Ross Patterson
8eb5215a0c
[DOCS]: Large file long cross-device renames (#1301)
From [Discord discussion](https://discord.com/channels/994270357957648404/994270357957648408/1408832178623741993).
2025-08-24 07:55:16 -07:00
Jesse Bannon
f729efad52
[BUGFIX] Wrong date_range option in reject print (#1300) 2025-08-22 07:48:32 -07:00
Ross Patterson
a4b910ae74
[DOCS] FAQ: How to force re-downloading an entry (#1292)
* docs(faq): How to force re-downloading an entry

https://discord.com/channels/994270357957648404/994270357957648408/1406876595364560970

* docs(faq): Missing CLI ovrrides to re-download

* docs(faq): Misunderstood re-download CLI options

https://discord.com/channels/994270357957648404/1102703969266049174/1407956781316964372

* docs(faq): Add download missing file entry

* docs(faq): Clarify reasons to re-download files
2025-08-22 07:33:30 -07:00
Ross Patterson
f88b27ffec
[DOCS] Various clarifications (#1289)
* docs(preset): Config file top presets vs base

* docs(throttle): resolution/quality throttle asset

I also added the "wait a few hours and try again" hint, sorry I didn't think of this
when reviewing your PR. While doing that I also reformatted the `%concat(...)` arguments
to be more readable to me. Particularly to keep clear what punctuation is about argument
separation as opposed to punctuation meant to be included in the resulting string. Not a
strong preference if you don't like it.

* docs(various): Minor typo and formatting fixes

* docs(preset): Fix presets vs preset in config file

From [feedback](https://github.com/jmbannon/ytdl-sub/pull/1289#discussion_r2289960678).

* test(throttle): Match modified assertion text

If it were my test, I'd only make assertions on the spirit of error message text
necessary to confirm it's the right error message and not another.

* docs(throttle): More likely override value

From [feedback](https://github.com/jmbannon/ytdl-sub/pull/1289#discussion_r2289976156).
2025-08-22 07:17:32 -07:00
Ross Patterson
60ff645750
[DOCS] Consistent headings, newlines, wraps (#1293)
I've been running into variations in style in the docs I've edited so far. I've been
including changes for consistency in with other commits but in many cases that makes one
part of one doc more consistent with the rest but less consistent with itself. It seems
like a bit of cleanup may be in order.

This change applies the following conventions:

- Section heading heirarchy:
  #. ``=`` with overline for document title, IOW first heading
  #. ``-``
  #. ``~``
  #. ``"``

- Two newlines before ``-`` sections for readability

- A newline between every section of any level and the first line of text

- Wrap paragraph lines at 88 characters to match Python's Black

I've only applied these changes to those ``*.rst`` files that aren't generated.

In the future, I might suggest another bulk change to match [the Sphinx conventions for
section
headings](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections).
2025-08-20 20:20:19 -07:00
dependabot[bot]
e980f60519
Bump yt-dlp[default] from 2025.8.11 to 2025.8.20 (#1295)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.8.11 to 2025.8.20.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.08.11...2025.08.20)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.8.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-20 20:14:56 -07:00
Jesse Bannon
288f106878
[FEATURE] Resolution assert built into throttle protection (#1288)
Closes https://github.com/jmbannon/ytdl-sub/issues/1241

YouTube is upping their throttling by serving 360p videos. Most likely to give us and genAI scraping a bad time on purpose.

Resolution assertion will be enabled by default. It will:

- Require > 360 height (pixels)
- Can configure the width by setting `resolution_assert_height_gte: 1080` to require 1080p, or any desired value
- Can disable resolution assert by setting `enable_resolution_assert: False`
2025-08-19 17:42:56 -07:00
Ross Patterson
e1f81e5274
[DOCS] Add Architecture section and Quick Start (#1287)
* docs(config): Clarify preset ordering priority

Refs #1276

* docs(architecture): WIP Narrative how it works

Much of this is repetition of the reference docs and some of the changes here should
also be applied here. The goal is to repeat here the parts of the reference docs that
may hang up a user when reading "Getting Started" without sending them off to get lost
in the weeds of the reference docs.

Note that this references a `Quick Start` document which is just an empty placeholder
and is to be written in a subsequent change.

Fixes #1279

* docs(terms): Glossary redundant with architecture

Now that the introduction includes an architectural overview, that section introduces
new users to necessary concepts and associated terminology and makes the terminology
section redundant in that part of the docs.

* docs(quick): Add a rote quick start guide

This is what I came up with when I tried to write instructions requiring as little
understanding as possible. Doing so really did reinforce my impression that this just
shouldn't be done, that significant understanding is required for *any* use of ytdl-sub
and even offering a quick start may be irresponsible. I'm still on the fence, thoughts?

That said, I also think I got some good explanations out of this and a better
understanding of what the next bits of the Getting Started should be. This Quick Start
is currently redundant with other parts of the Getting Started pages. I suspect that
I'll end up repeating and/or reorganizing parts of this Quick Start into the next bits
of the Getting Started. To that end I might argue that this change is a WIP and that
merging should wait. But I could also argue that this is an incremental improvement and
can be merged before that other work. Your call.

* docs(start): Various rST/Sphinx markup issues
2025-08-19 00:38:39 -07:00
Ross Patterson
3c45e2217e
[DOCS] Clarify Docker container image install (#1282)
* docs(install): Redundant platform-specific mention

I'm guessing this is a remnant from before it got it's own page.

* docs(docker): Clarify container image install

Capturing my changes thus far while I go off to educate myself about rST/Sphinx refs vs
links. As such, this duplicates a reference link in a way I don't like:

    :ref:`Automating Downloads <guides/getting_started/automating_downloads:docker and
    unraid>`

If you want to fix that or are OK with the duplication, this should be ready to
go. Otherwise, I'll fix and force push once I've learned how to do this correctly.

A matter of opinion in this change, I did away with the tabbed GUI vs headless code
blocks and the separate CPU/GPU passthrough code blocks because I find comments in
example code to be more clear, more readable, and more approachable. This also has the
benefit of putting informative comments in the resulting user's configuration. For
example, imagine a user that learns only later that they require GPU passthrough, their
configuration is already ready to get them started. This comes at the cost some repeated
comments (`environment:` and `deploy:` for GPU passthrough) and the fancy Sphinx
highlighting of relevant lines. I think the trade-off is a net benefit for users.

* docs(sphinx): Fix extlinks LSIO reference usage

* docs(sphinx): Address inline literal warning

Addresses:

    ./docs/source/config_reference/plugins.rst:848: WARNING: Inline literal start-string
    without end-string. [docutils]

* docs(start): Clarify prerequisite tech knowledge
2025-08-17 08:09:21 -07:00
Jesse Bannon
191fa3c1bb
[DOCS] Add warning for auto-generated docs (#1285) 2025-08-17 07:07:25 -07:00
Ross Patterson
7d9b4d9fa5
[DEV] Makefile improvements (#1283)
* build(make): Defensive make settings best practice

I've been bitten by silent failures in make many times and these settings have spared me
that many times. I now use them religiously whenever possible.

* build(docs): Missed Sphinx issues, fail on warn

It would be nice to have these failures in CI. Is there a way to get the ReadTheDocs
integration to use these options?
2025-08-16 12:04:49 -07:00
Ross Patterson
2465acb451
[DOCS] Clarifications for first time users (#1281)
* docs(docker): Clarify where the configuration is

The docs move from installation with Docker straight to modifying the configuration
without detailing where those files are.

* docs(start): Defining comments is discracting

This one is squarely me with my editor's red pen, do what you will. I whole heartedly
agree with this statement about comments and the intention (I assume) to encourage new
users to comment their configurations early and often. I just think this isn't the right
place for it and makes this document less focused and effective.

* docs(start): Less technical YAML key description

* docs(intro): Update and clarify goals/motivation

To help calibrate our collaboration, I went uninhibited with these changes including:

- assumptions about history I don't actually know:
  "this project was one of the early entrants"
- description of the yt-dlp/ytdl-sub division of labor I'm not actually certain about
- max opinion, both about this project and the domain in general
- max cheeky/fun writing tone, with the intention of being personable
- max editor's red pen, cut mercilessly

IOW, review carefully and reject early and often to help me submit future changes that
require less review and revision. To that end, give me more rather than less feedback on
this change in particular.

I include an endorsement of Pinchflat because [they do the same for
ytdl-sub](https://github.com/kieraneglin/pinchflat?tab=readme-ov-file#what-it-does). It
would be my next choice if ytdl-sub couldn't meet my needs or vanquished me.  More
than that link, in other Reddit comments and the like, the Pinchflat author clearly
expresses preference for ytdl-sub as their own "next option".
2025-08-15 09:25:54 -07:00
dependabot[bot]
6b86e5efdf
[DEV] Bump pylint from 3.3.7 to 3.3.8 (#1272)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.7...v3.3.8)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 3.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 19:59:25 -07:00
dependabot[bot]
975c5072a9
Bump yt-dlp[default] from 2025.7.21 to 2025.8.11 (#1271)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.7.21 to 2025.8.11.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.07.21...2025.08.11)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.8.11
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 19:57:15 -07:00
Ross Patterson
47dc861727
[BACKEND]: Less finger-printable request delay in throttle protection (#1273)
Thanks @rpatterson for the fix!
---------

Following [discussion about the previous preset
delay](https://discord.com/channels/994270357957648404/994270357957648408/1405705039649046589),
we weren't able to find a reason for it and there was agreement that the example value
from yt-dlp is more sensible.

While we're at it, also set a default `min:` value that would probably be sensible if it
were to be used in the future.

Finally, capture the reasoning behind the new preset values in comments to make the
reference documentation more helpful to new users in the future.
2025-08-14 19:56:31 -07:00
dependabot[bot]
2f55326981
Bump yt-dlp[default] from 2025.6.30 to 2025.7.21 (#1267)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.6.30 to 2025.7.21.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.06.30...2025.07.21)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.7.21
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 11:04:02 -07:00
Jesse Bannon
00e9ceaf3d
[FEATURE] Filter Duration prebuilt preset (#1259)
Adds the prebuilt preset ``Filter Duration``, which can include/exclude media based on its duration.

Supports the following override variables:

* ``filter_duration_min_s``
* ``filter_duration_max_s``

Usage:

```
      Plex TV Show by Date | Filter Duration:

        = Documentaries:
          "~NOVA PBS":
            url: "https://www.youtube.com/@novapbs"
            filter_duration_min_s: 120  # Only download videos at least 2m long

        = Sports:
          "~Maple Leafs Highlights":
            url: "https://www.youtube.com/@NHL"
            filter_duration_max_s: 180  # Only get highlight videos less than 3m long
```
2025-07-04 13:41:54 -07:00
Jesse Bannon
6c180c9478
[BACKEND] Enable throttle protection by default (#1257)
To protect new users, all prebuilt preset (excluding Soundcloud and Bandcamp) will include throttle protection by default.


> How do I disable?

Set the override variable `enable_throttle_protection: False`.
This can be done on a per-subscription basis and/or in the top __preset__ section to apply to all presets:

```
__preset__:
  overrides:
    enable_throttle_protection: False
```

> What if I already use throttle protection?

Your settings will override whatever values are set in the defaults.

Closes:
- https://github.com/jmbannon/ytdl-sub/issues/1121
- https://github.com/jmbannon/ytdl-sub/issues/1186
2025-07-03 00:34:37 -07:00
Jesse Bannon
a055c9b07a
[BACKEND] Default log level verbose (#1256)
Makes `--log-level verbose` the new default, which now shows yt-dlp logs.
Use `--log-level info` to hide yt-dlp logs.
2025-07-02 23:49:18 -07:00
Jesse Bannon
9871f62b91
[BUILD] Use windows-latest for build (#1254) 2025-07-02 00:58:07 -07:00
dependabot[bot]
ed5443515c
Bump yt-dlp[default] from 2025.6.25 to 2025.6.30 (#1253)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.6.25 to 2025.6.30.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.06.25...2025.06.30)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.6.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 20:52:52 -07:00
dependabot[bot]
d2e2947c75
Bump yt-dlp[default] from 2025.6.9 to 2025.6.25 (#1250)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.6.9 to 2025.6.25.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.06.09...2025.06.25)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.6.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-27 09:18:48 -07:00
Jesse Bannon
fc2da4d525
[FEATURE] square_thumbnail plugin (#1244)
Adds the ability to make thumbnails square, both file and embedded. Usage:

```
my_preset:
  square_thumbnail: True
```

Will soon enable this by default for all music-based presets with a toggle to disable.
Huge thanks to @Kentaro1043 for crafting the ffmpeg command for this!

Closes https://github.com/jmbannon/ytdl-sub/issues/383
2025-06-18 22:37:06 -07:00
Jesse Bannon
f6bce88ebd
[FEATURE] height and width variables (#1243)
Adds `height` and `width` entry variables, representing pixels to know resolution. For audio, defaults to 0.
2025-06-18 20:56:25 -07:00
Jesse Bannon
faf9fd8a10
[DOCKER] Update yt-dlp, fix phantomjs (#1240)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.5.22 to 2025.6.9.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.05.22...2025.06.09)

Also fixes phantomjs in docker builds
2025-06-11 23:25:56 -07:00
Jesse Bannon
cc85e18d43
[DOCS] How to capture from title, GH links (#1236) 2025-06-06 11:00:52 -07:00
Jesse Bannon
a68d5e4539
[FEATURE] Emby prebuilt presets (#1232)
Gives Emby first class support by creating prebuilt presets for each variant, including
- `Emby TV Show by Date`
- `Emby TV Show Collection`
- `Emby Music Videos`

Emby is identical to Jellyfin except in TV Show Collection, where it requires a `season.nfo`. A new plugin `StaticNfoTags` has been created to create those.

Huge thanks to @Kamaroth92 for the initial code for this feature!
2025-06-01 09:56:48 -07:00
Jesse Bannon
0bd97dbe96
[FEATURE] release_date support for date_range (#1231)
Adds the ability to toggle whether to use `upload_date` or `release_date` in the date_range plugin, which is used to cull older videos.

```
date_range:
  type: "upload_date"
```

If using the Only Recent preset, it will be automatically applied based on whether your season/episode ordering uses release date or upload date.

Closes https://github.com/jmbannon/ytdl-sub/issues/1182
2025-06-01 09:28:42 -07:00
Jesse Bannon
66fd7cf41c
[FEATURE] Add scripting print functions (#1230)
Add the ability to print custom messages in override scripts. Adds the functions
- `print`
- `print_if_true`
- `print_if_false`

Will be used to make under-the-hood preset things more verbose.
2025-05-31 12:15:57 -07:00
Jesse Bannon
36e23839f3
[FEATURE] Ability to change season and episode ordering via variables, support for release_date (#1220)
Adds the ability to change season and episode ordering using override variables, like so:

```
       "~Kids Toys Play":
          url: "https://www.youtube.com/@KidsToysPlayChannel"
          tv_show_by_date_season_ordering: "upload-year-month"
          tv_show_by_date_episode_ordering: "upload-day"
```

In addition, adds support for release-date based episode ordering (and culling if using Only Recent).
More info here: https://ytdl-sub.readthedocs.io/en/latest/prebuilt_presets/tv_shows.html

Closes https://github.com/jmbannon/ytdl-sub/issues/1182
2025-05-31 10:26:42 -07:00
Jesse Bannon
bc15fe09bd
[FEATURE] Throttle protection per request (#1229)
Allows adding a sleep inbetween every request during metadata scrape.

```
throttle_protection:
  sleep_per_request_s:
    min: 4
    max: 6
```

Closes https://github.com/jmbannon/ytdl-sub/issues/1222
2025-05-31 09:41:14 -07:00
Jesse Bannon
987b1cd028
[FEATURE] Ability to customize date used for deleting old files (#1221)
Implements https://github.com/jmbannon/ytdl-sub/issues/1182

Adds the ability to change the `keep_files_date_eval` (traditionally upload_date_standardized) to any date, so long as its in the form of YYYY-MM-DD.

Will be able to use this to have episodes get deleted based on their release_date or epoch_date, instead of their upload_date.
2025-05-31 08:52:15 -07:00
dependabot[bot]
c40b032eed
Bump yt-dlp[default] from 2025.4.30 to 2025.5.22 (#1224)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.4.30 to 2025.5.22.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.04.30...2025.05.22)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.5.22
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-22 21:51:22 -07:00
markalex2209
8ea0360b1c
[DOCS] Add faq about disabling parts of presets to address #506 (#1223)
Closes https://github.com/jmbannon/ytdl-sub/issues/506 . Thanks @markalex2209 !
---------

Co-authored-by: Aliaksei Markouski <markalex2209@gmail.com>
2025-05-11 12:34:45 -07:00
dependabot[bot]
d926617d5d
[DEV] Bump pylint from 3.3.6 to 3.3.7 (#1219)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.6 to 3.3.7.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.6...v3.3.7)

---
updated-dependencies:
- dependency-name: pylint
  dependency-version: 3.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 22:00:26 -07:00
Jesse Bannon
a4293398b7
[FEATURE] Add join and strip string functions to scripting (#1218) 2025-05-04 18:19:32 -07:00
Jesse Bannon
dfcbadb69d
[BACKEND] Make kodi_safe nfo overrideable (#1216)
For an upcoming change to make preset selection easier
2025-05-03 21:46:31 -07:00
Jesse Bannon
d5e8157b6c
[FEATURE] Add ability to suppress colors via CLI arg (#1215)
Not all terminals/logs displayed colors correctly. Adds the ability to suppress any color via `--suppress-colors` cli arg.
Thanks @drewski3420 for the contribution 🎉
2025-05-03 19:56:46 -07:00
Jesse Bannon
0c0f05e4d0
[DEV] Get windows testing in better state (#1214) 2025-05-03 19:40:28 -07:00
dependabot[bot]
a1681bf71a
Bump yt-dlp[default] from 2025.3.31 to 2025.4.30 (#1213)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.3.31 to 2025.4.30.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.03.31...2025.04.30)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.4.30
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 16:00:54 -07:00
Jesse Bannon
19d28e9baa
[FEATURE] Throw more actionable error if users supply the wrong type of url for TV show presets (#1210)
Don't let users supply `url` for TV Show Collection presets, or `s01_url` for TV Show by Date presets.
2025-04-17 22:14:37 -07:00
Jesse Bannon
e27a7c5495
[DOCKER] Fix various cron issues (#1204)
Closes
- https://github.com/jmbannon/ytdl-sub/issues/1200
  - Runs in the background now
- https://github.com/jmbannon/ytdl-sub/issues/1201
  - Was not running as user `abc`, now it is
- https://github.com/jmbannon/ytdl-sub/issues/1202
  - Due to reading stdout and tail of hidden log file. Should only tail from hidden log file now

Will leave crontab as-is if no CRON_SCHEDULE is specified
2025-04-02 21:22:12 -07:00
dependabot[bot]
6cf41a06b5
Bump yt-dlp[default] from 2025.3.27 to 2025.3.31 (#1199)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.3.27 to 2025.3.31.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.03.27...2025.03.31)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-version: 2025.3.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 23:54:59 -07:00
dependabot[bot]
2161dc5ac7
Bump yt-dlp[default] from 2025.3.26 to 2025.3.27 (#1196)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.3.26 to 2025.3.27.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.03.26...2025.03.27)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 08:38:10 -07:00
dependabot[bot]
1897f884ab
Bump yt-dlp[default] from 2025.3.25 to 2025.3.26 (#1195)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.3.25 to 2025.3.26.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.03.25...2025.03.26)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 15:39:11 -07:00
dependabot[bot]
6c3fd8d5c7
Bump yt-dlp[default] from 2025.2.19 to 2025.3.25 (#1194)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.2.19 to 2025.3.25.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.02.19...2025.03.25)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 14:43:59 -07:00
dependabot[bot]
e90e95982b
[DEV] Bump pylint from 3.3.4 to 3.3.6 (#1191)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.4 to 3.3.6.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.4...v3.3.6)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 09:07:36 -07:00
curryicecream
d21aebfbaf
[BUILD] Update release workflow to build aarch64 executable correctly (#1190)
Update release action to build aarch64 executable in arm64 runner.
Fixes #1177
2025-03-21 09:05:08 -07:00
dependabot[bot]
665ded7cec
[DEV] Bump isort from 5.13.2 to 6.0.1 (#1180)
Bumps [isort](https://github.com/PyCQA/isort) from 5.13.2 to 6.0.1.
- [Release notes](https://github.com/PyCQA/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyCQA/isort/compare/5.13.2...6.0.1)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-16 00:25:10 -07:00
Jesse Bannon
1264f0c656
[DOCKER] Update Ubuntu-based images (#1185)
- Update Ubuntu-based images to use Python 3.12
- Move hidden files from GUI image's main dir
- Update github workflows to use python 3.12
- Update fixtures
- Minor doc fixes
2025-03-15 21:58:51 -07:00
Jesse Bannon
51c134a036
[DOCKER] Simplify cron (#1184)
Drastically simplifies setting up cron within ytdl-sub. Below is the documentation which is available in readthedocs, and a section on how to migrate from the old way to this.

## New Cron Documentation
Cron is preconfigured in every ytdl-sub docker container. Enable by adding the following ENV variables to your docker setup.

```
services:
  ytdl-sub:
    environment:
    - CRON_SCHEDULE="0 */6 * * *"
    - CRON_RUN_ON_START=false
```  
- CRON_SCHEDULE follows the standard [cron scheduling syntax](https://crontab.guru/#0_*/6_*_*_*). The above value will run the script once every 6 hours.
- CRON_RUN_ON_START toggles whether to run your cron script on container start.

The cron script will reside in the main directory with the file name `cron`. Cron logs should show when viewing the Docker logs.

## Migrating Existing Cron Setup
Containers' cron files will remain intact until you add the `CRON_SCHEDULE` variable. Once you add that with your preferred schedule, it will write that value to /config/crontabs/abc and create a new file `cron` within your working directory. Simply copy everything from your existing `run_cron` script into `cron`.

If you have the ENV variable `DOCKER_MODS` in your docker-compose, remove it. It is now included by default. Not removing this will prevent cron logs from showing in your docker container logs.
2025-03-15 15:47:03 -07:00
Tyler Dakin
bb2bc49a49
Support duplicate chapter timestamps by merging (#1179)
Co-authored-by: Tyler Dakin <tyler@dakin.one>
2025-02-21 08:34:46 -08:00
dependabot[bot]
0ebcfb6a57
Bump yt-dlp[default] from 2025.1.26 to 2025.2.19 (#1178)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.1.26 to 2025.2.19.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.01.26...2025.02.19)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-19 16:41:48 -08:00
marvin8
279ab4a566
Make total delete videos in Total Stats be negative (#1175) 2025-02-16 19:49:05 -08:00
Jesse Bannon
35402438a5
[BACKEND] Use upload-artifact@v4 (#1173)
* [BACKEND] Use upload-artifact@v4

* download too
2025-02-05 10:53:07 -08:00
Sammy Chang
8d992cbcab
[DOCS] Add note to restart Docker container (#1168) 2025-01-31 08:42:15 -08:00
dependabot[bot]
cd58cfe13f
[DEV] Bump pylint from 3.3.3 to 3.3.4 (#1165)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.3...v3.3.4)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 12:05:19 -08:00
dependabot[bot]
d25cde5122
Bump yt-dlp[default] from 2025.1.15 to 2025.1.26 (#1164)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.1.15 to 2025.1.26.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.01.15...2025.01.26)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 21:33:35 -08:00
Jesse Bannon
2b1e74bc8e
[DOCKER] Export SSL certs (#1163)
Fixes a common issue with downloading channel thumbnails and fanart.
2025-01-26 21:07:54 -08:00
dependabot[bot]
f1db01bf2f
Bump yt-dlp[default] from 2025.1.12 to 2025.1.15 (#1162)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2025.1.12 to 2025.1.15.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2025.01.12...2025.01.15)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-17 00:16:44 -08:00
dependabot[bot]
d3ac54deae
Bump yt-dlp[default] from 2024.12.23 to 2025.1.12 (#1158)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2024.12.23 to 2025.1.12.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.12.23...2025.01.12)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-15 14:43:23 -08:00
dependabot[bot]
57c5d3c800
[DEV] Bump pylint from 3.3.2 to 3.3.3 (#1145)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.2...v3.3.3)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-26 19:39:47 -08:00
dependabot[bot]
e0441b5405
Bump yt-dlp[default] from 2024.12.13 to 2024.12.23 (#1144)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2024.12.13 to 2024.12.23.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.12.13...2024.12.23)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-26 19:39:23 -08:00
Jesse Bannon
74c7152cb1
[DOCS] Fix references (#1140) 2024-12-21 09:33:00 -08:00
Jesse Bannon
07d810171d
[DOCS] Rewrite getting started guide (#1129) 2024-12-21 09:13:09 -08:00
dependabot[bot]
ebe9908361
Bump yt-dlp[default] from 2024.12.6 to 2024.12.13 (#1138)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2024.12.6 to 2024.12.13.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.12.06...2024.12.13)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-13 21:35:00 -08:00
dependabot[bot]
d06ad07916
[BACKEND] Bump yt-dlp[default] from 2024.12.3 to 2024.12.6 (#1136)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2024.12.3 to 2024.12.6.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.12.03...2024.12.06)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-08 08:50:58 -08:00
Jesse Bannon
d84310a13c
[BACKEND] Update yt-dlp to 2024.12.3 (#1135) 2024-12-03 18:16:51 -08:00
dependabot[bot]
d83523ffe5
[DEV] Update twine requirement from ~=5.0 to >=5,<7 (#1133)
Updates the requirements on [twine](https://github.com/pypa/twine) to permit the latest version.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/5.0.0...6.0.1)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 23:18:49 -08:00
dependabot[bot]
3bf31a5e07
[DEV] Bump pylint from 3.3.1 to 3.3.2 (#1132)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.1...v3.3.2)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 23:18:33 -08:00
Jesse Bannon
53d84d8c1e
[FEATURE] Toggleable keyword evaluation behavior (#1130)
With the `Filter Keywords` prebuilt preset, can now set keyword evaluation to be either `ANY` or `ALL`. More info here: https://ytdl-sub.readthedocs.io/en/latest/prebuilt_presets/helpers.html#filter-keywords
2024-11-26 08:20:42 -08:00
Jesse Bannon
8c3f8528ad
[BACKEND] More media quality presets + docs (#1127)
Adds common media quality prebuilt presets and documentation on how to use them
2024-11-24 15:28:36 -08:00
dependabot[bot]
a6341971ff
[DEV] Update pytest-rerunfailures requirement from ~=14.0 to >=14,<16 (#1125)
Updates the requirements on [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) to permit the latest version.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/14.0...15.0)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 18:06:52 -08:00
Jesse Bannon
d3295cef86
[BUGFIX] Do not perform nested scripting on file path outputs (#1123)
Fixes a bug where curly braces in file-names causes scripting to think its a variable that does not exist
2024-11-18 00:22:20 -08:00
Gavin Mogan
0f542ac4e5
[DOCKER] Use Gha docker metadata actions to setup all the github actions (#1116) 2024-11-06 09:01:42 -08:00
dependabot[bot]
52038ed999
Bump yt-dlp from 2024.10.22 to 2024.11.4 (#1117)
Bumps [yt-dlp[default]](https://github.com/yt-dlp/yt-dlp) from 2024.10.22 to 2024.11.4.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.10.22...2024.11.04)

---
updated-dependencies:
- dependency-name: yt-dlp[default]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 22:03:25 -08:00
Jesse Bannon
e5451395e0
[DEV] Helper test to reproduce from user logs easier (#1102) 2024-11-03 20:44:02 -08:00
Jesse Bannon
663fb76132
[FEATURE] Multi-url support for YouTube music presets (#1113)
Adds support to specify multiple URLs for the YouTube-based music presets. More info here: https://ytdl-sub.readthedocs.io/en/latest/prebuilt_presets/music.html
2024-10-30 17:46:42 -07:00
Jesse Bannon
6a756cfecf
[BUGFIX] Move unescape parsing to separate function (#1108)
Fixes #1107 
```
FunctionRuntimeException: Runtime error occurred when executing the function %from_json: Invalid control character
```
2024-10-27 15:26:25 -07:00
Jesse Bannon
bf64be8a59
[DOCS] Fix preset key (#1106) 2024-10-27 08:25:26 -07:00
Jesse Bannon
80054aa77b
[FEATURE] Support newlines and tabs in scripting strings (#1105)
Allows the usage of `\n` and `\t` in scripting strings
2024-10-27 08:09:05 -07:00
Jesse Bannon
1bdc65f2e1
[BUGFIX] Custom function ordering (#1104)
Fixes a bug where custom functions would throw an error if they were used out-of-order from their definition
2024-10-26 21:16:17 -07:00
Jesse Bannon
43c10c19e0
[DOCS] Fix docker indents (#1103) 2024-10-26 20:54:57 -07:00
Dan Hand
102e2a9042
[DOCS] Fix documentation for headless cron path (#1100) 2024-10-26 20:39:15 -07:00
Jesse Bannon
88110a816d
[BUGFIX] Fix playlists not downloading correctly
Due to url ytdl-options not materializing
2024-10-25 20:47:18 -07:00
Jesse Bannon
7676fd9488
[BUILD] Use yt-dlp new default group (#1099)
Seen in https://github.com/yt-dlp/yt-dlp/releases/tag/2024.10.22
2024-10-23 21:57:29 -07:00
Jesse Bannon
31e4da9ff7
[DOCS] Music presets + more (#1095) 2024-10-23 21:34:38 -07:00
dependabot[bot]
c8ef766627
Bump yt-dlp from 2024.10.7 to 2024.10.22 (#1096)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.10.7 to 2024.10.22.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.10.07...2024.10.22)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-22 23:37:56 -07:00
dependabot[bot]
18d0d74326
Bump yt-dlp from 2024.9.27 to 2024.10.7 (#1091)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.9.27 to 2024.10.7.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.09.27...2024.10.07)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-08 18:51:12 -07:00
Jesse Bannon
0006c0ea94
[DEV] Disable YouTube tests in GH CLI (#1090) 2024-10-07 23:46:27 -07:00
dependabot[bot]
8547ab569c
[DEV] Update sphinx-rtd-theme requirement from ~=2.0 to >=2,<4 (#1089)
Updates the requirements on [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) to permit the latest version.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/2.0.0...3.0.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 23:17:15 -07:00
dependabot[bot]
78a3e90479
[DEV] Bump black from 24.8.0 to 24.10.0 (#1088)
Bumps [black](https://github.com/psf/black) from 24.8.0 to 24.10.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.8.0...24.10.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 23:16:33 -07:00
Jesse Bannon
1499d7486c
[FEATURE] Override variable support in ytdl_options (#1087)
Adds the ability to use override variables in the [ytdl_options](https://ytdl-sub.readthedocs.io/en/latest/config_reference/plugins.html#ytdl-options) section of the config.
2024-10-05 21:25:50 -07:00
Jesse Bannon
28c29683cb
[FEATURE] Filter Keywords prebuilt preset (#1086)
Adds ``Filter Keywords``, a preset that can include or exclude media with any of the listed keywords. Both keywords and title/description are lower-cased before filtering.

Supports the following override variables:

* ``title_include_keywords``
* ``title_exclude_keywords``
* ``description_include_keywords``
* ``description_exclude_keywords``

For best usage, use the `~` tilda subscription mode to set a subscription's list override variables.
Tilda mode allows override variables to be set directly underneath it.

```
Plex TV Show by Date | Filter Keywords:
  
  = Documentaries:
    "~NOVA PBS":
       url: "https://www.youtube.com/@novapbs"
       title_exclude_keywords:
         - "preview"
         - "trailer"  

    "~To Catch a Smuggler":
       url: "https://www.youtube.com/@NatGeo"
       title_include_keywords:
         - "To Catch a Smuggler"
```
2024-10-05 01:03:52 -07:00
Jesse Bannon
0acf4c9f33
[DOCS] How to download large channels (#1085) 2024-10-04 21:29:23 -07:00
Jesse Bannon
084761fc61
[DEV] Unit test chapters (#1083) 2024-10-03 21:42:19 -07:00
Jesse Bannon
c0ba30b220
[DEV] More OutputOption unit tests (#1082) 2024-10-02 21:37:54 -07:00
Jesse Bannon
ed4a2814b9
[DEV] Remove old prebuilt fixtures (#1081) 2024-10-01 08:21:27 -07:00
Jesse Bannon
97054043c4
[DEV] Refactor and simplify prebuilt preset tests (#1080) 2024-10-01 01:02:51 -07:00
Jesse Bannon
a3f59d5420
[DEV] Separate integration actions (#1079) 2024-09-30 23:55:15 -07:00
Jesse Bannon
648201ed9c
[DEV] OutputOptions unit tests (#1078) 2024-09-30 23:18:33 -07:00
dependabot[bot]
2417bec691
[BACKEND] Bump yt-dlp from 2024.8.6 to 2024.9.27 (#1077)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.8.6 to 2024.9.27.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.08.06...2024.09.27)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 22:07:31 -07:00
Jesse Bannon
481ced34c5
[DEV] Custom ffmpeg unit test (#1076) 2024-09-30 22:04:52 -07:00
Jesse Bannon
01e1f46b86
[DEV] Remaining throttle unit tests (#1075) 2024-09-30 22:04:32 -07:00
Jesse Bannon
1caade46bd
[DEV] Unit test music tags (#1074) 2024-09-29 23:33:00 -07:00
Jesse Bannon
f2ec26d77e
[BUGFIX] Fix variable collision when disabling plugins (#1072)
Fixes https://github.com/jmbannon/ytdl-sub/issues/1071
2024-09-29 22:38:26 -07:00
Jesse Bannon
1b57d79c8f
[BREAKING CHANGE] Remove regex plugin (#1067)
Regex plugin has been removed in favor of scripting. The function [regex_capture_many](https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#regex-capture-many) has been created to replicate the plugin's behavior. See the following converted example:

`regex, now deprecated`
```
    regex:
      from:
        title:
          match:
            - ".*? - (.*)"  # Captures 'Song' from 'Emily Hopkins - Some - Song'
          capture_group_names:
            - "captured_track_title"
          capture_group_defaults:
            - "{title}"
    overrides:
      track_title: "{captured_track_title}"
```

`scripting equivalent`
```
    overrides:
      # Captures 'Song' from 'Emily Hopkins - Some - Song'
      captured_track_title: >-
        {
          %regex_capture_many(
            title,
            [ ".*? - (.*)" ],
            [ title ]
          )
        }
      track_title: "{%array_at(captured_track_title, 1)}"
```

## Motivation:
Regex was a unique plugin that added custom variables based on user input. This made the backend need to support both `overrides` and 'plugin user variables'. Removing `regex` plugin will consolidate this logic into only `overrides`, making it much easier to maintain.
2024-09-29 08:41:09 -07:00
Jesse Bannon
88fa55c9dc
[DEV] Local sphinx packages (#1068) 2024-09-26 19:52:05 -07:00
Jesse Bannon
fd5882051a
[DEV] Unit test filter plugin (#1066) 2024-09-26 11:54:15 -07:00
Jesse Bannon
513d1f5a97
[DEV] Partially unit test throttle protection (#1065) 2024-09-26 10:27:31 -07:00
Jesse Bannon
79ba60021c
[BACKEND] Proper functions with docstrings (#1061)
Reimplement some of the 'hidden' functions, including `%regex_capture_many`, as standard functions with docstrings
2024-09-25 23:24:49 -07:00
Jesse Bannon
f963125398
[DEV] Unit test NFO tags (#1064) 2024-09-25 23:23:16 -07:00
dependabot[bot]
6c3150ac0c
[DEV] Bump pylint from 3.2.7 to 3.3.1 (#1059)
* Bump pylint from 3.2.7 to 3.3.1

Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.7 to 3.3.1.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.7...v3.3.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* R0917

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Bannon <jbann1994@gmail.com>
2024-09-25 22:51:00 -07:00
Jesse Bannon
a37d9ed10e
[DEV] Set ffmpeg/ffprobe paths via ENV variable (#1060) 2024-09-24 22:13:22 -07:00
Jesse Bannon
3fbdd26257
[DEV] Cross platform test fixture helpers (#1058) 2024-09-20 14:58:07 -07:00
Jesse Bannon
6510fd0999
[BACKEND] Add /usr/local/bin/ffmpeg as possible default location (#1056)
For better cross-platform support
2024-09-20 14:22:33 -07:00
Jesse Bannon
10cb82a69a
[BACKEND] %contains_any function, list + dict support for tilda overrides mode (#1054)
Adds the function `%contains_any`, and list support in a tilda override subscription. The end-goal of these features are to more easily add a title exclude list, like so:

```
__preset__:
  filter_exclude:
    - "{%contains_any( %lower(title), exclude_title_strings )}"

...
Jellyfin TV Show by Date:
  ~History Documentaries:
    url: "https://..."
    exclude_title_strings:
      - "trailer"
      - "preview"
```

A proper prebuilt preset or built-in functionality will follow this change.
2024-09-20 09:23:22 -07:00
Parker Moore
759948361f
[BACKEND] get_file_md5_hash: read small blocks instead of the entire file (#1052)
This prevents MemoryError when running on systems with less memory than the file being processed.

Fixes #1051. Thanks @parkr
2024-09-17 20:24:33 -07:00
0xBLERKO
941fa43bb9
[BUGFIX] Find ext when info.json wrong (#1045)
Fixes a bug where YouTube's info.json sometimes say ext: mkv but download is not mkv. Thanks @0xBLERKO for the contribution!
2024-09-08 13:19:05 -07:00
dependabot[bot]
c44b98505e
[BACKEND] Bump yt-dlp from 2024.7.25 to 2024.8.6 (#1034)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.7.25 to 2024.8.6.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.07.25...2024.08.06)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:41:58 -07:00
dependabot[bot]
4e3e270df9
[DEV] Update sphinx requirement from ~=7.0 to >=7,<9 (#1029)
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/master/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.0.0...v8.0.0)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:39:52 -07:00
dependabot[bot]
cf4c7f6d52
[DEV] Bump black from 24.4.2 to 24.8.0 (#1031)
Bumps [black](https://github.com/psf/black) from 24.4.2 to 24.8.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.4.2...24.8.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:38:30 -07:00
dependabot[bot]
221c11ac3e
[DEV] Bump pylint from 3.2.6 to 3.2.7 (#1041)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.6 to 3.2.7.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.6...v3.2.7)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-06 09:38:12 -07:00
dependabot[bot]
e529674b85
[BACKEND] Bump yt-dlp from 2024.7.16 to 2024.7.25 (#1028)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.7.16 to 2024.7.25.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.07.16...2024.07.25)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-25 21:00:15 -07:00
MADMAN-Modding
a0957ffe93
[DOCS] Fix the command syntax on the linux install page for executing the program (#1027) 2024-07-24 21:20:16 -07:00
dependabot[bot]
5a62fc5222
[DEV] Bump pylint from 3.2.5 to 3.2.6 (#1026)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.5 to 3.2.6.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.5...v3.2.6)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 21:48:57 -07:00
dependabot[bot]
bba8d86117
[BACKEND] Bump yt-dlp from 2024.7.9 to 2024.7.16 (#1022)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.7.9 to 2024.7.16.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.07.09...2024.07.16)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-20 21:11:38 -07:00
dependabot[bot]
e08f6e4154
[BACKEND] Bump yt-dlp from 2024.7.2 to 2024.7.9 (#1019)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.7.2 to 2024.7.9.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.07.02...2024.07.09)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-13 00:30:53 -07:00
Jesse Bannon
0ed8aec83d
[BACKEND] Show ffmpeg conversion errors (#1016) 2024-07-04 10:03:34 -07:00
dependabot[bot]
85ee967d39
[BACKEND] Bump yt-dlp from 2024.7.1 to 2024.7.2 (#1015)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.7.1 to 2024.7.2.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.07.01...2024.07.02)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 19:56:28 -07:00
dependabot[bot]
1b3e7a9173
[BACKEND] Bump yt-dlp from 2024.5.27 to 2024.7.1 (#1014)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.5.27 to 2024.7.1.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.05.27...2024.07.01)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 09:25:20 -07:00
dependabot[bot]
f010b574df
[DEV] Bump pylint from 3.2.3 to 3.2.5 (#1012)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.3 to 3.2.5.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.3...v3.2.5)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 09:24:50 -07:00
Joey Babcock
fa13ba30cf
[DOCS] Fixing 404 link to presets (#1008) 2024-06-22 23:30:21 -07:00
dependabot[bot]
f7305ebf8a
[DEV] Bump pylint from 3.2.2 to 3.2.3 (#1001)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.2 to 3.2.3.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.2...v3.2.3)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-06 15:30:36 -07:00
Jesse Bannon
5e9472986a
[FEATURE] Enhance music video presets using specialized map syntax (#808)
Enhances the music video presets by supporting the following syntax:

`subscriptions.yaml`
```
__preset__:
  overrides:
    music_video_directory: "/music_videos"

# Choose between Jellyfin/Kodi/Plex Music Videos preset:
#   - Plex Music Videos:
#   - Jellyfin Music Videos:
#   - Kodi Music Videos:
#
"Plex Music Videos":

  = Pop:  # Sets genre tag to "Pop"
    "Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
    "Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"

  = Rock:
    # Prefixing with '+' puts the subscription into 'map-mode'.
    # Music video presets in map-mode support grouping videos into different
    # categories, which get set on the album field.
    #
    # URLs can either be strings, or maps that can overload title, year, date
    "+ Guns N' Roses":
      Music Videos:
        - "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ"
      Concerts:
        - title: "Live at The Ritz - New York City"
          year: "1988"
          url: "https://www.youtube.com/watch?v=OldpIhHPsbs"
        - title: "Live at The Hollywood Bowl"
          date: "2023-01-11"
          url: "https://www.youtube.com/watch?v=Z7hutGlvq9I"
```
The 'map-mode' (denoted by `+`) lets you specify a map of music video categories. Under each category, a URL can be specified as-is or with additional metadata (title, year/date, url).

Music videos and concerts, especially older ones, are typically uploaded by random users with inconsistencies in their titles. This syntax aims to make it easy to specify individual URLs with the ability to overwrite their title and year, and group them by category.
2024-06-06 11:50:33 -07:00
Jesse Bannon
5866c12104
[BUGFIX] Do not run all branches of if functions in scripts (#999)
Will only run branch script code of `if` statements if the condition evaluates to that branch
2024-06-03 15:02:59 -07:00
Jesse Bannon
5e335b195c
[FEATURE] Allow YAML maps and lists in overrides, convert to script format (#956)
Adds the ability to create map and list-based override variables. 

For example, you can now create lists like this:
```
overrides:
  urls:
    - "https://...1"
    - "https://...2"
```
which is equivalent to:
```
overrides:
  urls: >-
    {
      [
        "https://...1",
        "https://...2",
      ]
    }
```

Likewise, maps can now look like:
```
overrides:
  music_video_category:
    concerts:
      - "https://...1"
      - "https://...2"
    interviews:
      - "https://...3"
```
which is equivalent to:
```
overrides:
  music_video_category: >-
    {
      "concerts": [
        "https://...1",
        "https://...2"
      ],
      "interviews": [
        "https://...3"
      ]
    }
```
2024-06-02 20:02:48 -07:00
Jesse Bannon
6e31ad3600
[BACKEND] Include .info.json files for Music video presets (#996)
Music video presets now include .info.json files by default
2024-06-02 10:57:32 -07:00
Jesse Bannon
30a2ad7a63
[BUGFIX] Prevent corrupt writes to download archive (#983)
Attempts to make writes to the download archive safer (https://github.com/jmbannon/ytdl-sub/issues/982).

`ytdl-sub` will now *copy* the download archive from the working directory to the output directory with a temp name, then perform a *move* to store it with its final expected name. This will drastically lower the window of time where  the process could die mid-write and corrupt it on the next read.
2024-06-02 10:53:19 -07:00
Jesse Bannon
74625c293d
[DEV] Update fixtures (#995)
* [DEV] Update fixtures

* thumbs fixed, longer timeout

* ugh

* fanart
2024-06-02 00:05:44 -07:00
dependabot[bot]
ed969b84d2
[DEV] Bump pylint from 3.2.0 to 3.2.2 (#991)
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 17:04:26 -07:00
dependabot[bot]
87f9378d33
[BACKEND] Bump yt-dlp from 2024.04.09 to 2024.5.27 (#994)
Bumps [yt-dlp](https://github.com/yt-dlp/yt-dlp) from 2024.04.09 to 2024.5.27.
- [Release notes](https://github.com/yt-dlp/yt-dlp/releases)
- [Changelog](https://github.com/yt-dlp/yt-dlp/blob/master/Changelog.md)
- [Commits](https://github.com/yt-dlp/yt-dlp/compare/2024.04.09...2024.05.27)

---
updated-dependencies:
- dependency-name: yt-dlp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-28 19:48:22 -07:00
dependabot[bot]
02db0c41a5
[DEV] Bump pylint from 3.1.1 to 3.2.0 (#988)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 16:17:21 -07:00
dependabot[bot]
8461f5f229
[DEV] Bump pylint from 3.1.0 to 3.1.1 (#987)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.1.0...v3.1.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 19:40:17 -07:00
Jesse Bannon
325d229061
[BUGFIX] Fix usage of ~ in paths (#981)
Fixes path with tildes in them, i.e. `~/videos/youtube`
2024-05-10 19:57:48 -07:00
dependabot[bot]
64d3082a8a
[DEV] Bump pylint from 2.13.5 to 3.1.0 (#972)
* Bump pylint from 2.13.5 to 3.1.0

Bumps [pylint](https://github.com/pylint-dev/pylint) from 2.13.5 to 3.1.0.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v2.13.5...v3.1.0)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* bend knee to pylint

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Bannon <jbann1994@gmail.com>
2024-04-28 10:40:15 -07:00
Tomas Babej
1d176050a7
[FEATURE] %regex_sub built-in script function (#971)
This implements %regex_sub built-in function to enhance string-processing capabilities, allowing users to perform substitutes like:

- removing non-ascii characters
- replacing subsequent whitespace characters with a single whitespace

Thanks @tbabej !
2024-04-27 22:47:29 -07:00
dependabot[bot]
ec58a80660
[DEV] Bump black from 22.3.0 to 24.4.2 (#973)
* Bump black from 22.3.0 to 24.4.2

Bumps [black](https://github.com/psf/black) from 22.3.0 to 24.4.2.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.3.0...24.4.2)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* run linter

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Bannon <jbann1994@gmail.com>
2024-04-27 15:46:47 -07:00
Jesse Bannon
caad4598fc
[DEV] Regen function doc strings on make docs (#977)
* [DEV] Regen docs with `make docs`

* default 0
2024-04-27 14:06:16 -07:00
dependabot[bot]
aa637d12dc
[DEV] Bump isort from 5.10.1 to 5.13.2 (#974)
Bumps [isort](https://github.com/pycqa/isort) from 5.10.1 to 5.13.2.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.10.1...5.13.2)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-27 13:37:20 -07:00
dependabot[bot]
10387c925d
[DEV] Update coverage[toml] requirement from ~=6.3 to >=6.3,<8.0 (#975)
Updates the requirements on [coverage[toml]](https://github.com/nedbat/coveragepy) to permit the latest version.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/6.3...7.5.0)

---
updated-dependencies:
- dependency-name: coverage[toml]
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-27 10:32:38 -07:00
dependabot[bot]
f3a979c818
[DEV] Update pytest requirement from ~=7.2 to >=7.2,<9.0 (#976)
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.0...8.1.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-27 10:03:14 -07:00
Jesse Bannon
5b34df4588
[DEV] Update fixtures (#970)
* [DEV] Update fixtures

* update yt-dlp

* fanart
2024-04-27 09:20:30 -07:00
Nathaniel Barragan
232c0acdfa
[BACKEND] Update yt-dlp, move to pyproject.toml (#965)
- Updates yt-dlp to 2024.4.9
- Moves setup.cfg to pyproject.toml
- Updates other various backend packages

Thanks @Noodlez1232 !
2024-04-26 00:09:02 -07:00
Noah Kiss
156c788689
[DOCS] Add Plex Agent Sources Information (#969)
Co-authored-by: Noah Kiss <noah@nkmk.co>
2024-04-25 23:12:08 -07:00
Jesse Bannon
ad72e1a6de
[BACKEND] Debug log exception on retry (#957)
Help diagnose retry errors better
2024-04-02 17:27:21 -07:00
Jesse Bannon
c622cf68b5
[DEV] Escapable curly braces (#955) 2024-04-01 22:40:56 -07:00
Jesse Bannon
598c574b8a
[DOCS] Show ytdl_options per url (#954) 2024-04-01 04:41:12 -07:00
Jesse Bannon
017db953bf
[FEATURE] Automatically handle playlists ordered in reverse (#948)
Playlists have always been a pain-point with ytdl-sub. If an author adds new videos to the end of a playlist, as opposed to the front, it breaks ytdl-sub's intuition of incremental scraping by breaking on the first (oldest) video. This update now makes it possible to handle this in the prebuilt TV Show presets:
- Add each URL variable (`url`, `url2`, ...) into the `download` portion of the subscription twice
- First definition is what we all know and use, simply scrapes first-to-last, then downloads last-to-first
- Second definition does the following:
  - Check to see if a URL is a YouTube playlist URL, if so...
    - Set the field to download, but with modifications to scrape last-to-first, then download first-to-last
  - Otherwise...
    - Set the field to an empty string, which means ytdl-sub will skip it
2024-04-01 04:24:41 -07:00
Jesse Bannon
083db0d9dc
[FEATURE] Simplify TV Show Collection Preset (#953)
Simplifies TV Show Collection presets, drastically. Old version:
```
  rick_a_tv_show_collection:
    preset:
      - "jellyfin_tv_show_collection"
      - "season_by_collection__episode_by_year_month_day_reversed"
      - "collection_season_1"
      - "collection_season_2"
    overrides:
      tv_show_name: "Rick A"
      collection_season_1_url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
      collection_season_1_name: "All Videos"
      collection_season_2_url: "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
      collection_season_2_name: "Official Music Videos"
```

New version:
```
  Jellyfin TV Show Collection:
    "~Rick A":
      s01_name: "All Videos"
      s01_url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
      s02_name: "Official Music Videos"
      s02_url: "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
```
2024-03-30 22:04:45 -07:00
Jesse Bannon
d5e647554e
[DEV] Fix resolve_once script bug with custom functions (#952)
* [DEV] Fix resolve_once script bug with custom functions

* function name

* fix adding custom functions
2024-03-30 00:53:05 -07:00
Jesse Bannon
82c503c515
[DEV] Update fixtures (#947)
* [DEV] Update fixtures

* bandcamp
2024-03-25 00:25:28 -07:00
Jesse Bannon
f940d3ec3e
[BACKEND] yt-dlp 2024.3.10 (#943) 2024-03-18 22:29:35 -07:00
Jesse Bannon
a02e44f09d
[FEATURE] Add dedicated track_date and track_original_date variables (#939)
As title, to make it easier to override
2024-02-28 15:05:39 -08:00
Jesse Bannon
0beeeb464d
[FEATURE] Music tag dates (#938)
Adds proper support for setting `date` and `original_date` fields via `music_tag` plugin using a standardized date format.
2024-02-28 13:48:42 -08:00
Jesse Bannon
8e6a2cb98e
[DOCS] Rewrite first subscription documentation (#932) 2024-02-20 21:04:13 -08:00
Jesse Bannon
f346b0ef52
[BUGFIX] Handle case when yt-dlp returns LazyList (#929)
Fixes https://github.com/jmbannon/ytdl-sub/issues/910 , when yt-dlp sometimes returns a non-serializable LazyList
2024-02-17 09:51:21 -08:00
Jesse Bannon
248e9a15a6
[DOCS] Various doc cleanups (#928) 2024-02-17 08:53:06 -08:00
Jesse Bannon
b95ba86279
[BUGFIX] Use epoch_date if upload_date is missing (#927)
Fixes https://github.com/jmbannon/ytdl-sub/issues/912

If a required variable (like uid) is missing, ytdl-sub will error. This bugfix prevents sites that do not provide an upload date from erroring.
2024-02-17 08:19:21 -08:00
Jesse Bannon
7c217db843
[BUGFIX] Fix usage of chapter_title_sanitized (#924)
Usage of `chapter_title_sanitized` would sometimes result in an error. This should hopefully fix it
2024-02-12 16:05:50 -08:00
Jesse Bannon
cea21ca47f
[DOCS] Add more configuration docs (#916) 2024-02-02 22:52:17 -08:00
Jesse Bannon
29616144b4
[BUGFIX] Fix multiple top-level parents for linked VEVO YouTube channels (#909)
Fixes https://github.com/jmbannon/ytdl-sub/issues/908 - when channel URLs return videos from other channels
2024-01-20 11:16:17 -08:00
Jesse Bannon
b3374cb4d5
[DEV] %map_extend scripting function (#906)
* [DEV] `%map_extend` scripting function

* lint
2024-01-19 00:51:44 -08:00
Jesse Bannon
213580ee84
[DEV] Add type-check functions (#905) 2024-01-18 23:24:10 -08:00
Jesse Bannon
29398862da
[BUGFIX] Fix YouTube channels iterating 2x entries (#903)
A recent feature to grab channel artwork for playlists caused downloading channels to misreport the number of entries it was downloading. This change fixes that
2024-01-18 19:00:19 -08:00
Jesse Bannon
f0b3991e3e
[FEATURE] Toggle date_range to break or not (#904)
Adds a the new field `breaking` to the `date_range` plugin, to toggle whether an entry breaks subsequent metadata pulls. This is useful to disable if you are grabbing a playlist that may have videos out of order, but still want to apply a date range to it.
2024-01-18 16:23:02 -08:00
Qualis Svagtlys
c7823a40ad
[DEV] Prebuilt Presets Revamp (#864)
Modifies some of the prebuilt preset logic in the backend
2024-01-18 15:01:29 -08:00
Jesse Bannon
d2e733f2e7
[BACKEND] Only download audio when using audio_extract plugin (#902)
This addition optimizes the `audio_extract` plugin by preemptively telling yt-dlp to discard the video and only download the audio stream
2024-01-18 15:00:17 -08:00
Jesse Bannon
c0ca3c3945
[BACKEND] Remove old format for video_tags, music_tags, download_strategy (#899)
Now that sufficient time has passed, we can formally deprecate the following:
- https://ytdl-sub.readthedocs.io/en/latest/deprecation_notices.html#video-tags
- https://ytdl-sub.readthedocs.io/en/latest/deprecation_notices.html#music-tags
- usage of `download_strategy` (simply remove it if you're getting an error about it)
2024-01-15 00:04:32 -08:00
Jesse Bannon
e3158583ca
[FEATURE] Toggleable plugin field enable for all dict-based plugins (#897)
For key/value-based plugins, add an `enable` field to make it easier for child presets to disable their functionality
2024-01-11 00:40:07 -08:00
Jesse Bannon
2076de3074
[BUGFIX] Smarter subscription validation (#895)
With the right functions, it was possible for subscription validation to raise a false-positive error. This should hopefully resolve that issue and give validation a small performance increase
2024-01-10 18:16:12 -08:00
Jesse Bannon
00e4cd8541
[BUGFIX] Fix rare case when yt-dlp returns None and is downloaded (#894) 2024-01-10 09:33:03 -08:00
Jesse Bannon
b9d05c5b8f
[BUGFIX] keep_max_files=0 downloading 2 (#893)
Fixes https://github.com/jmbannon/ytdl-sub/issues/892

Does not set max_downloads if keep_max_files = 0
2024-01-10 09:31:37 -08:00
Jesse Bannon
305d084a6d
[BUGFIX] Only apply --dl-override to subscriptions in --match (#890)
As title, to not accidently add breaking changes for subscriptions that are not going to run
2024-01-09 20:50:31 -08:00
Jesse Bannon
3b36ffd050
[BUGFIX] Fix source metadata getting fetched when yt-dlp breaks (#889)
Prior PR only got channel metadata for a playlist if it never 'broke' (i.e. MaxDownloads, BreakOnExisting). Will now fetch it regardless.
2024-01-09 19:25:09 -08:00
Jesse Bannon
be0f6f2d2d
[BUGFIX] Parent entry is not a dict (#888)
Fixes an AttributeError bug
2024-01-09 14:35:19 -08:00
Jesse Bannon
c29cca2b3d
[FEATURE] YouTube playlists to fetch playlist uploader's poster + fanart (#887)
Implements https://github.com/jmbannon/ytdl-sub/issues/756

TV Show presets whose URL is a playlist will try to get the playlist uploader's poster and fanart.
2024-01-08 22:51:42 -08:00
Jesse Bannon
915e29379a
[BACKEND] Try to fetch source metadata from uploader_url (#886)
Makes it possible to fix https://github.com/jmbannon/ytdl-sub/issues/756

In the case that uploader_url does not equal the input url, keep trying to fetch uploader_url to get all possible metadata that yt-dlp does not provide in a single API call
2024-01-08 18:53:00 -08:00
Jesse Bannon
961e04d027
[DOCS] Update Usage page (#883) 2024-01-08 16:20:08 -08:00
Jesse Bannon
78cdcce464
[FEATURE] Add --dl-override to pass dl arg overrides to all subscriptions (#882)
With both `--match` and `--dl-override`, it is now possible to more easily experiment with subscriptions without needing to create a separate file.

For example, suppose you are changing some values in your subscription "Rick A" and want to test them out. You can now run:
```
ytdl-sub sub --dry-run --match Rick --dl-override '--ytdl-options.max_downloads 3'
```
This will
1. Dry-run
2. Only run for the subscription "Rick A"
3. Apply setting max_downloads to 3
2024-01-08 15:19:01 -08:00
Jesse Bannon
9382be5c42
[FEATURE] --match flag to only run subset of subscriptions (#881)
Implements https://github.com/jmbannon/ytdl-sub/issues/880
and maybe fixes https://github.com/jmbannon/ytdl-sub/issues/827

Can now do `ytdl-sub sub --match SubA SubB` or `ytdl-sub sub --match SubA --match SubB`, which will only run subscriptions that contain `SubA` or `SubB` in their names
2024-01-08 13:37:07 -08:00
Jesse Bannon
d80a63631a
[BACKEND] String split scripting function (#877) 2024-01-07 00:13:32 -08:00
Jesse Bannon
ba71abc466
[BACKEND] Optional default in %array_at scripting function (#879)
As title
2024-01-06 23:47:09 -08:00
Jesse Bannon
dc74c6a52a
[BACKEND] elif scripting function (#876)
As titled
2024-01-05 17:48:59 -08:00
Jesse Bannon
8de9a5b6da
[BACKEND] Update yt-dlp to 2023.12.30 (#875)
As titled
2024-01-05 15:59:53 -08:00
Jesse Bannon
d96bf11b12
[BACKEND] More url variables in helper preset (#874)
Have `url` variables go up to 100
2024-01-05 14:45:03 -08:00
Jesse Bannon
5e384ba96e
[BACKEND] Info log for filter_include and filter_exclude plugins (#873)
Adds more verbose logging when entries get filtered using the `filter_include` and `filter_exclude` plugins
2024-01-05 14:44:16 -08:00
Jesse Bannon
4d0123c8b0
[BACKEND] Optimize memory usage (#872)
Reduce memory usage significantly when pulling large channels all at once by
- Not loading entry script until it is being processed
- Deleting the entry memory once fully processed
- Disable yt-dlp from keeping entry info in memory since ytdl-sub reads it from file
2024-01-05 13:04:01 -08:00
Qualis Svagtlys
891b951625
[DOCS] Unraid and FAQ Doc Updates (#869) 2024-01-05 11:12:09 -08:00
Jesse Bannon
db5f095a51
[BACKEND] Set max_downloads to keep_max_files if present (#871)
Implements: https://github.com/jmbannon/ytdl-sub/issues/829

Optimization to not scrape more than what you will keep
2024-01-04 16:38:20 -08:00
Jesse Bannon
2dbe426ced
[BUGFIX] Properly handle UIDs that have misbehaving chars that get sanitized (#870)
Should hopefuly fix both:
- https://github.com/jmbannon/ytdl-sub/issues/843
- https://github.com/jmbannon/ytdl-sub/issues/867

Various sites have unique IDs with characters that get sanitized (i.e. `/`, `:`). `ytdl-sub` was not using sanitized UIDs when looking for files, and would throw an error saying download was missing. This should resolve that issue.
2024-01-04 15:01:17 -08:00
Jesse Bannon
b3d298b664
[DOCS] Advanced scripting usage, rename Overrides to Static (#866) 2024-01-01 00:01:46 -08:00
Jesse Bannon
b99463d204
[DOCS] Explain String + Reduce better (#865) 2023-12-30 23:59:10 -08:00
Jesse Bannon
d1acae1a60
[DOCS] Update scripting types (#863) 2023-12-30 10:57:59 -08:00
Jesse Bannon
12430ecf1e
[DOCS] Scripting reference start (#862) 2023-12-29 22:34:36 -08:00
Jesse Bannon
735da8b647
[BUGFIX] Actually fix subscription_name with periods in it (#861)
Prior PR did not actually work - this actually fixes the bug (https://github.com/jmbannon/ytdl-sub/issues/844) where subscription names would drop periods from them, i.e. Mr. Beast would have the name  Beast.

POTENTIAL BREAKING CHANGE
If you have a subscription with a period in its name prior to this fix, this change will make it so your download archive is not found since it uses the subscription name in the download archive file path. To fix, simply change the download archive JSON's name to have the actual subscription name, and not the dropped-period one.
2023-12-29 15:41:33 -08:00
Jesse Bannon
f2d55fdb23
[REFACTOR] Organize variable definitions (#860) 2023-12-29 14:39:19 -08:00
Jesse Bannon
189e897888
[FEATURE] subscription_map value (#859)
To be used later for advanced presets. For subscriptions in the form of (+ as prefix)

```
   + Subscription Name:
     Music Videos:
       - "https://url1.com/..."
     Concerts:
       - "https://url2.com/..."
```

Stores all the contents under the subscription name into the override variable
``subscription_map`` as a Map value. The above example is stored as:

```
   {
     "Music Videos": [
       "https://url1.com/..."
     ],
     "Concerts: [
       "https://url2.com/..."
     ]
   }
```

Advanced scripting is needed to dissect this Map variable into a usable preset
2023-12-29 08:55:12 -08:00
Jesse Bannon
731d4e444c
[DOCS] Tooling to auto-generate custom sphinx docs (#849)
Tooling + updating docs
2023-12-28 22:39:59 -08:00
Jesse Bannon
1fcaba9274
[DOCS] Update links to reflect doc-upgrade (#858) 2023-12-27 11:07:11 -08:00
Jesse Bannon
65bb5c0a85
[BUGFIX] Fix periods not working in subscription names (#857)
Fixes the bug (#844) where subscription names would drop periods from them, i.e. Mr. Beast would have the name  Beast.

POTENTIAL BREAKING CHANGE
If you have a subscription with a period in its name prior to this fix, this change will make it so your download archive is not found since it uses the subscription name in the download archive file path. To fix, simply change the download archive JSON's name to have the actual subscription name, and not the dropped-period one.
2023-12-27 11:06:39 -08:00
Jesse Bannon
1375a0fbe3
[BACKEND] Remove subscription __value__ (#856)
Removes the deprecated `__value__` field in the subscriptions file (different from `__preset__` which is NOT deprecated). This was very short-lived, and most likely not used.
2023-12-27 10:49:39 -08:00
Qualis Svagtlys
008bcf1b2d
[DOCS] Begin complete overhaul of readthedocs (#847)
Readthedocs for ytdl-sub is getting a massive overhaul to both look and read like a modernized app. It is still very-much work-in-progress, stay tuned for more!

Huge thanks to @Svagtlys (aka Momo) for driving this
2023-12-27 10:27:40 -08:00
Jesse Bannon
02d51d6aec
[BACKEND] Make sibling metadata optional to compute (#855)
Partial fix to https://github.com/jmbannon/ytdl-sub/issues/853
Makes computing sibling metadata optional, defaulted to False. This prevents excessive memory usage when scraping large channels
2023-12-26 11:57:03 -08:00
Jesse Bannon
873ecc0147
[BUGFIX] Fix recursion limit when applying large reduce functions (#851)
Python would think a recursive error occurred, but in reality, the stack got too large due to poor optimization when executing array reduce functions. Thanks Melissa from Discord for the bug report!
2023-12-20 23:46:17 -08:00
Jesse Bannon
b8fb119f21
[BACKEND] Add collection_season_name variable to collection presets (#850)
Can now be used to change directory names for collection presets
2023-12-20 11:18:36 -08:00
Jesse Bannon
308c76a8f2
[FEATURE] Filter plugins, regex helper script functions (#848)
Adds generic `filter_include` and `filter_exclude` plugins in anticipation for function script usage. Will detail it more with official docs at a later time!
2023-12-20 09:52:12 -08:00
Jesse Bannon
c0e2ded1ae
[BACKEND] Improved error messages when script resolution does not work (#846) 2023-12-19 00:22:14 -08:00
Jesse Bannon
5d1fd0d7c6
[BACKEND] Validate override variable names (#845)
Validate as early as possible whether override names are invalid, to prevent confusing downstream errors
2023-12-18 17:11:25 -08:00
Jesse Bannon
e92b1cd12a
[BACKEND][HUGE] Function Support in variable syntax (#838)
A complete gutting of the internals of ytdl-sub to support functions in our variable syntax, in addition to being able to access a yt-dlp entry's .info.json fields using functions. Functionally, ytdl-sub should still look and behave the same from a user-perspective.

With so many lines of code changed (+8927, -2708), no doubt there will be new issues. Please make a GH issue or reach out on Discord if your config/subscriptions break in any way/shape/form.

Details on how to use function support will come soon in the form of proper documentation in our readthedocs.
2023-12-18 16:08:15 -08:00
Jesse Bannon
4d9a30779c
[BUGFIX] Use proper extractor key for download archive (#840)
For less popular sites, ytdl-sub's custom download archive was not working. This was because we weren't using the right extraction key (though it still worked for YouTube, Bandcamp, Soundcloud, and others). This bugfix should now exactly match yt-dlp's and never be a problem again.

Huge thanks to @Svagtlys for root-causing this 🤝
2023-12-14 17:04:03 -08:00
OmarAhmadyar
8998861b49
[BUGFIX] file_convert output file deleted (#822)
file_convert.py:modify_entry() deletes the converted output file if the output and input file names are identical.  Reordered the file deletion to avoid.

Thanks @OmarAhmadyar !
2023-11-25 00:23:46 -08:00
Jesse Bannon
544af2207b
[FEATURE] Add output_options.keep_max_files to limit number of downloads per subscription (#820)
Adds a new mechanism to limit subscriptions' number of videos to an explicit number (only supported date ranges prior to this).

Example usage:
```
output_options:
  keep_max_files: 10
```
will only keep 10 videos at max.

In addition:
- `keep_max_files` is bundled into the `Only Recent` preset and can be used via setting the `only_recent_max_files` override variable (see examples or README for usage).
- Changed the `date_range` variable name to `only_recent_date_range` for more clarity.  Usage of `date_range` will continue to work.
2023-11-17 09:01:21 -08:00
Jesse Bannon
52d1a5887e
[DEV] String validators to allow ints, floats, bools (#810) 2023-11-16 23:50:51 -08:00
Jesse Bannon
6123e35c64
[DEV] Test-case for subscription with existing download archive (#819) 2023-11-16 23:37:24 -08:00
Jesse Bannon
c7fce5dbdd
[BACKEND] Upgrade yt-dlp to 2023.11.16 (#817) 2023-11-15 16:58:29 -08:00
Jesse Bannon
9b8aa827ba
[BACKEND] Upgrade yt-dlp to 2023.11.14 (#815) 2023-11-14 19:23:35 -08:00
Jesse Bannon
f23554f3d7
[BUGFIX] Include music prebuilt presets (and all future ones) in executables (#814)
ytdl-sub executables did not include the prebuilt music presets. This change fixes that and prevents the issue from happening again for any future prebuilt preset
2023-11-11 10:55:05 -08:00
Jesse Bannon
395691f2b9
[FEATURE] Create output_directory_nfo_tags on first entry instead of last (#809) 2023-11-05 23:10:29 -08:00
Jesse Bannon
e257210e4a
[BACKEND] Shared multi-url helper preset, can now disable grabbing poster/fanart (#807) 2023-11-05 16:51:17 -08:00
Jesse Bannon
8a27cad94e
[BACKEND] multi-url helper presets, have music videos support multi-url (#804) 2023-11-05 13:03:32 -08:00
Jesse Bannon
f1a2187a17
[FEATURE] Add throttle_protection plugin (#799)
Provides options to make ytdl-sub look more 'human-like' to protect from throttling. For range-based values, a random number will be chosen within the range to avoid sleeps looking scripted.

Usage:
```
    throttle_protection:
      sleep_per_download_s:
        min: 2.2
        max: 10.8
      sleep_per_subscription_s:
        min: 9.0
        max: 14.1
      max_downloads_per_subscription:
        min: 10
        max: 36
      subscription_download_probability: 1.0
```
2023-11-05 08:23:12 -08:00
Jesse Bannon
19eee28f4a
[DOCKER] New default config/subscription in Docker (#798)
The default subscription file in the docker container now matches the one in the repo's README
2023-11-03 17:14:38 -07:00
Jesse Bannon
d64d863175
[DOCKER][BUGFIX] Upgrade base code-server image to fix ytdl-sub-gui image (#797)
As of ~2 days ago, the ytdl-sub-gui image broke from using an older version of the `code-server` image. After this push, it should be fixed + working as intended
2023-11-03 14:04:12 -07:00
Jesse Bannon
33a8da9afc
[BACKEND] Log full subscription errors (#793) 2023-11-03 10:12:15 -07:00
Jesse Bannon
e9b2214a0a
[DOCS] New README with only subscriptions (#791) 2023-11-02 23:28:55 -07:00
Jesse Bannon
f8868add5f
[DEV] Test prebuilt presets packaged (#792) 2023-11-02 12:00:22 -07:00
Jesse Bannon
2d107c7e43 [HOTFIX] Add __init__ to prebuilt presets helper directory 2023-11-02 11:30:11 -07:00
Jesse Bannon
5fb5a18abc
[FEATURE] TV Show prebuilt presets that require no config (#788)
Introduces 3 new presets + 1 helper preset to download TV show-based subscriptions without the need of a config

```
Plex TV Show by Date
Jellyfin TV Show by Date
Kodi TV Show by Date
```
and
```
Only Recent
```

Example usage can be found in https://github.com/jmbannon/ytdl-sub/blob/master/examples/tv_show_subscriptions.yaml
2023-11-01 22:39:32 -07:00
Jesse Bannon
9f408d2196
[FEATURE] Extend subscription syntax (#790)
Extends the subscription.yaml syntax at the cost of introducing a breaking change.

# New Syntax
## Mix-n-match presets and indent variables
```
# Can mix/match presets and indent override variables.
# Uses presets TV Show, Only Recent and assigns Kids, TV-Y to subscription_indent_1 and _2
TV Show | = Kids | = TV-Y | Only Recent:
  "Jake Trains": "https://..."
```

## Subscriptions with list-value support
```
TV Show | = Kids | = TV-Y | Only Recent:
  "Jake Trains":
    - "https://url.1..."  # Assigns to subscription_value and subscription_value_1
    - "https://url.2..."  # Assigns to subscription_value_2
```

## Subscriptions with override-keys support
```
TV Show | = Kids | = TV-Y | Only Recent:
  "~Jake Trains":  # the ~ means "all keys underneath get assigned as override variables"
    url: "https://url.1..."  # Assigns to url
    url2: "https://url.2..."  # Assigns to url2
```

# Breaking Changes
In the TV show subscriptions example (https://github.com/jmbannon/ytdl-sub/blob/master/examples/tv_show_subscriptions.yaml), it had
```
TV Show Full Archive:
  # Sets "Kids" for genre, "TV-Y" for content rating
  = Kids | TV-Y:
    "Jake Trains": "https://www.youtube.com/@JakeTrains"
    "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
```
This must be changed to
```
TV Show Full Archive:
  # Sets "Kids" for genre, "TV-Y" for content rating
  = Kids | = TV-Y:  # Each indent variable assignment must have an = before it
    "Jake Trains": "https://www.youtube.com/@JakeTrains"
    "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
```
2023-11-01 20:24:06 -07:00
Jesse Bannon
056b111be6
[FEATURE] Prebuilt Music Videos (#787)
Creates standardized prebuilt presets for music videos. Preset names are:

```
"Kodi Music Videos"
"Jellyfin Music Videos"
"Plex Music Videos"
```

Usage can be found in the `examples/` directory
2023-10-28 00:12:22 -07:00
Jesse Bannon
4789035821
[BACKEND] Default overrides for indent variables (#785)
For music presets, adds `track_genre_default` as an override variable (defaults to Unset)

For TV show presets, adds `tv_show_genre_default` and `tv_show_content_rating_default`, which default to 'ytdl-sub' and 'TV-14'
2023-10-25 12:37:49 -07:00
Jesse Bannon
11be119261
[BUGFIX] Do not set music_directory in prebuilt presets (#783)
New music prebuilt presets had set `music_directory`, which is bad practice since it's not visible to the user which directory it would use. This forces `music_directory` to be set either in the user's config or subscription file
2023-10-25 11:36:51 -07:00
Jesse Bannon
852347dc7e
[BUGFIX] Have __preset__ override nested preset keys (#784)
With the new preset nesting support, `__preset__` would have lower precedence which isn't ideal since it's meant to serve as a 'apply to all presets in this file'. Now the order of priority is:

`inherited presets (top-to-bottom) -> preset itself -> nested subscription presets -> __preset__ -> subscription values`
2023-10-25 11:35:52 -07:00
Jesse Bannon
d810291e7c
[BUGFIX] Write music multi-tags correctly (#782)
Music multi-tags (artists, albumartists, etc) were not writing correctly. Fix it by explicitly checking to see if the tag is multi or not in the codebase.
2023-10-25 10:37:24 -07:00
Jesse Bannon
ff857c84af
[FEATURE] Prebuilt music presets (#780)
Adds the following prebuilt presets:
```
"Single"
"SoundCloud Discography"
"YouTube Releases"
"YouTube Full Albums"
"Bandcamp"
```

which require no config.yaml to use. Usage examples can be found in the `/examples` directory
2023-10-24 09:23:05 -07:00
Jesse Bannon
c149957b04
[DOCKER] Remove default gui settings (#776) 2023-10-22 14:35:52 -07:00
Jesse Bannon
ee09e85a33
[DOCKER] Move cont-init.d files (#775) 2023-10-22 13:02:06 -07:00
Jesse Bannon
c932583165
[DOCKER] GUI Docker image using Code-Server (#774)
Adds a new docker image: `ytdl-sub-gui:latest` which is ytdl-sub packaged with VS-Code via LinuxServer.io's code-server image: https://hub.docker.com/r/linuxserver/code-server

Proper documentation will be added later
2023-10-22 01:17:09 -07:00
Jesse Bannon
8a02a40ca3
[BACKEND] Update yt-dlp version to 2023.10.13 (#772) 2023-10-21 13:13:20 -07:00
Jesse Bannon
83ecd19c77
[FEATURE] Process all subscriptions even if one or more error (#771)
Closes GH Issue #766, partially closes #520

Prior to this release, no other subscriptions would download if a subscription before it had an error. Now, subscriptions will continue to download even if one has an error, and will be reported in the output summary.
2023-10-21 12:25:38 -07:00
Jesse Bannon
82c9beb00a
[BUGFIX] Prevent default config from eagerly validating (#770) 2023-10-19 10:59:05 -07:00
Jesse Bannon
8eca524292 [DEV] Do not release with doc updates 2023-10-18 23:35:02 -07:00
Jesse Bannon
1cb70e4520
[DEV] Refactor entrypoint file (#765) 2023-10-18 23:33:25 -07:00
Jesse Bannon
a7651b202b
[DOCS] Update config docs (#764) 2023-10-18 23:20:59 -07:00
Jesse Bannon
4105c2ed40
[FEATURE] No longer require a config.yaml (#763)
The only required field in a config was `working_directory`. This is now optional (defaults to `.ytdl-sub-working-directory`) making it so a config is no longer required. This sets the stage to onboard noob users who will solely use prebuilt presets in a single subscription file
2023-10-18 09:58:37 -07:00
Jesse Bannon
aa5967bdd6
[BACKEND] season_directory_name variable for TV show presets (#762) 2023-10-17 23:30:40 -07:00
Jesse Bannon
545f186c46
[FEATURE][BUGFIX] Beautiful subscriptions syntax update + fix (#758)
A follow-up to the prior commit which fixes some bugs in the 'beautiful subscriptions' implementation. A proper migration guide will be written in the wiki once it's ready
2023-10-17 16:30:50 -07:00
Jesse Bannon
309f259464
[FEATURE] Ability to migrate download archive files (#751)
With the recent push to 'beautify subscriptions' (see https://github.com/jmbannon/ytdl-sub/releases/tag/2023.10.02), we need the ability to change subscription names from their legacy form:
```
rick_a:
  preset:
    - "tv_show"
  overrides:
    tv_show_name: "Rick A"
    url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
```
into:
```
tv_show:
  "Rick A": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
```

This however has implications from ytdl-sub's legacy download archive naming. By default, we write archives to `.ytdl-sub-{subscription_name}-download-archive.json`. If we change the subscription name, the archive will not be found, causing a complete redownload. This new feature gives us the ability to migrate download archives to a new naming schema.

# Migrating to Beautified Subscriptions
## Step 0
BACK UP ALL CONFIG + SUBSCRIPTION FILES!!!!! If something goes wrong, restore your backup and try again and/or ask for help.

## Step 1
Since we know we'll be changing our `subscripion_name` to the value of `tv_show_name`, we can use that in our newly migrated download archive name by setting this within the `tv_show` preset (or whatever your 'base' preset is).
```
presets:
  tv_show:
    output_options:
      migrated_download_archive_name: ".{tv_show_name_sanitized}-download-archive.json" 
```

## Step 2
Perform a download as usual, via `ytdl-sub sub ...`. This will load the old archive, and save it into the new archive. You should see `MIGRATION DETECTED` within the logs. Ensure it completes successfully.

Perform another download invocation and ensure you see the `MIGRATION SUCCESSFUL` within the logs.

## Step 3
Now we can set:
```
presets:
  tv_show:
    output_options:
      # rename migrated_download_archive_name to just download_archive_name
      download_archive_name: ".{tv_show_name_sanitized}-download-archive.json" 

  overrides:
    tv_show_name: "{subscription_name}"
    url: "{subscription_value}"
```
Our download archives now default to our new format, and we set `tv_show_name` + `url` to use subscription values by default.

## Step 4
We can now beautify our subscription.yaml file to:
```
tv_show:
  "Rick A": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
```
2023-10-03 16:08:14 -07:00
Jesse Bannon
ae3739cdd3
[DEV] Do not hash .info.json files in expected download summaries (#752) 2023-10-03 13:22:19 -07:00
Jesse Bannon
c119e6f644
[BUGFIX] Fix match-filter edge case and date_range.before (#750)
Fixes yet another bug that is match-filter related. The `date_range.before` value would get ignored if you specified any other match-filters. That is fixed by making them `&` behind-the-scenes.
2023-10-03 11:46:42 -07:00
Jesse Bannon
c255f40348
[FEATURE] Subscription nesting (#747)
Closes Feature Request: https://github.com/jmbannon/ytdl-sub/issues/743

Subscriptions support using presets as keys, and using keys to set override variables as values.
For example:

```
tv_show:
  only_recent:
    [News]:
      "Breaking News": "https://www.youtube.com/@SomeBreakingNews"

  [Tech]:
    "Two Minute Papers": "https://www.youtube.com/@TwoMinutePapers"
```
Will create two subscriptions named "Breaking News" and "Two Minute Papers", equivalent to:

```
"Breaking News":
  preset:
    - "tv_show"
    - "only_recent"

  overrides:
    subscription_indent_1: "News"
    subscription_name: "Breaking News"
    subscription_value: "https://www.youtube.com/@SomeBreakingNews"

"Two Minute Papers":
  preset:
    - "tv_show"

  overrides:
    subscription_indent_1: "Tech"
    subscription_name: "Two Minute Papers"     subscription_value: "https://www.youtube.com/@TwoMinutePapers"
```

You can provide as many parent presets in the form of keys, and subscription indents as ``[keys]``.
This can drastically simplify subscription definitions by setting things you want configurable like so in your
parent preset:

```
presets:
  tv_show_name:
    overrides:
      tv_show_name: "{subscription_name}"
      url: "{subscription_value}"
      genre: "{subscription_indent_1}"
```

NOTE!!!
Changing your subscription name from 'legacy' subscriptions will need their download archive file migrated to a new name. A future update will assist in this migration process. Either wait until then or, if you consider yourself a ytdl-sub expert, keep a backup of your subscription file before migrating.
2023-10-02 16:07:48 -07:00
Jesse Bannon
3e41e0f59e
[FEATURE] Ability to add subscription value via config (#744)
`Subscription Value` is the mechanism used to achieve one-liner subscriptions. It works by having the subscription `"Subscription Name": "value"`, and assigning `"value"` to an override variable via setting the subscription value to that override variable's name.

This feature allows you to set subscription value within the config. In the near future, ytdl-sub will strive to make subscription files simpler by hiding most of the "under-the-hood" logic within the config file.
2023-09-27 21:37:36 -07:00
Jesse Bannon
6615e1d1ac
[BACKEND] Always filter out live, post_live, and upcoming videos on download (#742)
Closes Issue: https://github.com/jmbannon/ytdl-sub/issues/729

Add a new field to the match_filter plugin: `download_match_filters`. This set of filters will be applied at the download stage (as opposed to `filters` which get applied at the metadata stage). By default, always set `"!is_live & !is_upcoming & !post_live"` as a download filter. This prevents ytdl-sub from hanging when trying to grab live and upcoming videos.

Docs on match-filters: https://ytdl-sub.readthedocs.io/en/latest/config.html#match-filters
2023-09-24 06:24:21 -07:00
Jesse Bannon
533e083183
[BACKEND] Update yt-dlp to 2023.9.24 (#741) 2023-09-24 05:17:52 -07:00
Jesse Bannon
723c40b59b
[REVERT] Remove default match-filter that filters live videos (#739)
Adding a default match-filter broke other people's match-filters. This is because each match-filter acts as an OR. I.e.
```
match_filters:
  filters:
    - "!is_live & !is_upcoming & !post_live"  # the one I added
    - "original_url!*=/shorts/ & !is_live"
```
Would not filter shorts, because under the hood, yt-dlp evaluates it as `(!is_live & !is_upcoming & !post_live) | (original_url!*=/shorts/ & !is_live)`

Need to rethink how to filter live videos by default 🤔
2023-09-22 08:33:49 -07:00
Jesse Bannon
f05fdbc498
[BACKEND] Filter out upcoming videos and post-live streams by default in TV show presets (#738)
Follow-up to filtering out live videos. YouTube channels with an upcoming video that hasn't premiered yet would also hang ytdl-sub. Perform a match-filter to skip those as well.

More info on match-filters here: https://ytdl-sub.readthedocs.io/en/latest/config.html#match-filters
2023-09-21 23:50:56 -07:00
Jesse Bannon
04b19a5ddd [DOCKER] Tag ubuntu docker image version correctly 2023-09-21 23:31:42 -07:00
Jesse Bannon
b0c6430407
[DOCKER] Add version tag to all images (#737) 2023-09-21 23:20:31 -07:00
Jesse Bannon
d1adf08119
[BACKEND] Add !is_live match-filter by default to TV show presets (#735)
Any YouTube channel with an active livestream would make ytdl-sub hang until it completed. This adds a match-filter to skip live videos by default for all prebuild TV show presets.

More info on match-filters here: https://ytdl-sub.readthedocs.io/en/latest/config.html#match-filters
2023-09-21 23:08:36 -07:00
Jesse Bannon
3d9c35519d
[BACKEND] More explicit error message for PermissionError (#736)
Do not print the full stack trace when a PermissionError occurs
2023-09-21 23:06:57 -07:00
Jesse Bannon
3c87dc9f8e
[BUGFIX] Fix date_range plugin as a match-filter (#726)
Bugfix to the prior release. Convert yt-dlp date formats (i.e. `today-2months`) into a date string with format `YYYYMMDD`. This will make it compatible with match-filter syntax
2023-09-20 08:30:29 -07:00
Jesse Bannon
1a118e225a
[BACKEND] Perform match-filters at metadata stage, use match-filters for date_range plugin (#725)
Solved Issue: https://github.com/jmbannon/ytdl-sub/issues/706

From our discussion with yt-dlp folks (https://github.com/yt-dlp/yt-dlp/issues/8108), we learned that best practices for date--range filtering is done using match-filters. We now set the `date_range` plugin values to be a 'breaking' match filter, and any custom ones specified from users in the `match_filters` plugin as non-breaking. This means match-filters can be performed at the metadata stage, which should yield a significant speedup

Relevant docs:
- date_range: https://ytdl-sub.readthedocs.io/en/latest/config.html#date-range
- match_filters: https://ytdl-sub.readthedocs.io/en/latest/config.html#match-filters
2023-09-19 11:19:15 -07:00
Jesse Bannon
0b62cc13d0
[DOCS] Mention YAML single vs double quotes in regex docs (#719)
Improve regex plugin docs by mentioning difference between using single-quotes and double-quotes in YAML: https://ytdl-sub.readthedocs.io/en/latest/config.html#regex
2023-09-15 13:54:08 -07:00
Jesse Bannon
0008bf3aca
[FEATURE] More verbose error logs when using ytdl-sub commands (#717)
Add a more informative error message when the config file is missing, and when no command is supplied (i.e. sub, dl, view)
2023-09-15 12:35:34 -07:00
Jesse Bannon
6c3e70a9fd
[DEV] pytest retry for flakey tests (#718) 2023-09-15 12:17:18 -07:00
Jesse Bannon
a48efdc84c
[FEATURE] format plugin to set yt-dlp format easier (#714)
Makes setting yt-dlp's `format` field easier by giving it its own documented plugin.

Old:
```
my_format_preset:
  ytdl_options:
    format: "best"
```

New:
```
my_format_preset:
  format: "best"
```

The old method will still work, so do not worry about updating configs ASAP. However, the option is available to save a few lines 😉
2023-09-13 00:02:19 -07:00
852 changed files with 44761 additions and 31932 deletions

View file

@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run unit tests with coverage
run: |
@ -29,9 +29,9 @@ jobs:
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
python -m pip install -e .[test]
python -m pytest tests/unit
python -m pytest --reruns 3 --reruns-delay 5 tests/unit
test-soundcloud:
test-integration:
runs-on: windows-latest
permissions:
contents: read
@ -42,9 +42,9 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run e2e soundcloud tests with coverage
- name: Run integration tests with coverage
run: |
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
tar -xf ffmpeg.zip
@ -52,9 +52,10 @@ jobs:
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
python -m pip install -e .[test]
python -m pytest tests/e2e/soundcloud
python -m pytest --reruns 3 --reruns-delay 5 tests/integration --ignore tests/integration/prebuilt_presets
test-bandcamp:
test-integration-prebuilt-presets:
runs-on: windows-latest
permissions:
contents: read
@ -65,33 +66,9 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run e2e soundcloud tests with coverage
run: |
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
tar -xf ffmpeg.zip
move "ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe" "ffmpeg.exe"
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
python -m pip install -e .[test]
python -m pytest tests/e2e/bandcamp
test-youtube:
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Run e2e youtube tests with coverage
- name: Run prebuilt preset integration tests with coverage
run: |
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
tar -xf ffmpeg.zip
@ -99,9 +76,9 @@ jobs:
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
python -m pip install -e .[test]
python -m pytest tests/e2e/youtube
python -m pytest --reruns 3 --reruns-delay 5 tests/integration/prebuilt_presets
test-plugins:
test-e2e:
runs-on: windows-latest
permissions:
contents: read
@ -112,9 +89,9 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run e2e plugin tests with coverage
- name: Run e2e tests with coverage
run: |
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
tar -xf ffmpeg.zip
@ -122,4 +99,4 @@ jobs:
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
python -m pip install -e .[test]
python -m pytest tests/e2e/plugins
python -m pytest tests/e2e

View file

@ -9,7 +9,7 @@ on:
- master
jobs:
test-lint:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
contents: read
@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run linters
run: |
@ -27,7 +27,7 @@ jobs:
make check_lint
test-unit:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
contents: read
@ -37,14 +37,14 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run unit tests with coverage
run: |
pip install -e .[test]
sudo apt-get update
sudo apt-get install -y ffmpeg
coverage run -m pytest tests/unit && coverage xml -o /opt/coverage/unit/coverage.xml
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/unit && coverage xml -o /opt/coverage/unit/coverage.xml
- name: Save coverage
uses: actions/cache@v3
@ -52,8 +52,8 @@ jobs:
path: /opt/coverage/unit
key: ${{github.sha}}-coverage-unit
test-soundcloud:
runs-on: ubuntu-22.04
test-integration:
runs-on: ubuntu-latest
permissions:
contents: read
@ -63,43 +63,23 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run e2e soundcloud tests with coverage
- name: Run integration tests with coverage
run: |
pip install -e .[test]
sudo apt-get update
sudo apt-get install -y ffmpeg
coverage run -m pytest tests/e2e/soundcloud && coverage xml -o /opt/coverage/soundcloud/coverage.xml
test-bandcamp:
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Run e2e soundcloud tests with coverage
run: |
pip install -e .[test]
sudo apt-get update
sudo apt-get install -y ffmpeg
coverage run -m pytest tests/e2e/bandcamp && coverage xml -o /opt/coverage/bandcamp/coverage.xml
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/integration --ignore tests/integration/prebuilt_presets && coverage xml -o /opt/coverage/integration/coverage.xml
- name: Save coverage
uses: actions/cache@v3
with:
path: /opt/coverage/bandcamp
key: ${{github.sha}}-coverage-bandcamp
path: /opt/coverage/integration
key: ${{github.sha}}-coverage-integration
test-youtube:
runs-on: ubuntu-22.04
test-integration-prebuilt-presets:
runs-on: ubuntu-latest
permissions:
contents: read
@ -109,23 +89,23 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run e2e youtube tests with coverage
- name: Run prebuilt preset integration tests with coverage
run: |
pip install -e .[test]
sudo apt-get update
sudo apt-get install -y ffmpeg
coverage run -m pytest tests/e2e/youtube && coverage xml -o /opt/coverage/youtube/coverage.xml
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/integration/prebuilt_presets && coverage xml -o /opt/coverage/integration-prebuilt-presets/coverage.xml
- name: Save coverage
uses: actions/cache@v3
with:
path: /opt/coverage/youtube
key: ${{github.sha}}-coverage-youtube
path: /opt/coverage/integration-prebuilt-presets
key: ${{github.sha}}-coverage-integration-prebuilt-presets
test-plugins:
runs-on: ubuntu-22.04
test-e2e:
runs-on: ubuntu-latest
permissions:
contents: read
@ -135,29 +115,22 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Run e2e plugin tests with coverage
- name: Run e2e tests with coverage
run: |
pip install -e .[test]
sudo apt-get update
sudo apt-get install -y ffmpeg
coverage run -m pytest tests/e2e/plugins && coverage xml -o /opt/coverage/plugins/coverage.xml
- name: Save coverage
uses: actions/cache@v3
with:
path: /opt/coverage/plugins
key: ${{github.sha}}-coverage-plugins
coverage run -m pytest tests/e2e && coverage xml -o /opt/coverage/e2e/coverage.xml
codecov-upload:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
test-unit,
test-soundcloud,
test-bandcamp,
test-youtube,
test-plugins
test-integration,
test-integration-prebuilt-presets,
test-e2e
]
permissions:
contents: read
@ -169,32 +142,26 @@ jobs:
path: /opt/coverage/unit
key: ${{github.sha}}-coverage-unit
- name: Restore soundcloud test coverage
- name: Restore integration test coverage
uses: actions/cache@v3
with:
path: /opt/coverage/soundcloud
key: ${{github.sha}}-coverage-soundcloud
path: /opt/coverage/integration
key: ${{github.sha}}-coverage-integration
- name: Restore bandcamp test coverage
- name: Restore integration prebuilt presets test coverage
uses: actions/cache@v3
with:
path: /opt/coverage/bandcamp
key: ${{github.sha}}-coverage-bandcamp
path: /opt/coverage/integration-prebuilt-presets
key: ${{github.sha}}-coverage-integration-prebuilt-presets
- name: Restore youtube test coverage
- name: Restore e2e test coverage
uses: actions/cache@v3
with:
path: /opt/coverage/youtube
key: ${{github.sha}}-coverage-youtube
- name: Restore plugins test coverage
uses: actions/cache@v3
with:
path: /opt/coverage/plugins
key: ${{github.sha}}-coverage-plugins
path: /opt/coverage/e2e
key: ${{github.sha}}-coverage-e2e
- name: Upload code coverage to codecov.io
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: /opt/coverage/unit/coverage.xml,/opt/coverage/soundcloud/coverage.xml,/opt/coverage/bandcamp/coverage.xml,/opt/coverage/youtube/coverage.xml,/opt/coverage/plugins/coverage.xml
files: /opt/coverage/unit/coverage.xml,/opt/coverage/integration/coverage.xml,/opt/coverage/integration-prebuilt-presets/coverage.xml,/opt/coverage/e2e/coverage.xml

292
.github/workflows/package-gui.yaml vendored Normal file
View file

@ -0,0 +1,292 @@
name: ytld-sub Docker GUI Build
on:
push:
# Publish `master` as Docker `latest` image.
branches:
- master
# Publish `v1.2.3` tags as releases.
tags:
- v*
# Run tests for any PRs.
pull_request:
env:
IMAGE_NAME: ytdl-sub-gui
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
version:
name: version
runs-on: ubuntu-latest
outputs:
pypi_version: ${{ steps.set_outputs.outputs.pypi_version }}
local_version: ${{ steps.set_outputs.outputs.local_version }}
init_contents: ${{ steps.set_outputs.outputs.init_contents }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: master
- name: Set date and commit hash variables
run: |
echo "DATE=$(date +'%Y.%m.%d')" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Count number of commits on master on day, minus 1 to account for post-push
run: |
echo "DATE_COMMIT_COUNT=$(($(git rev-list --count master --since='${{ env.DATE }} 00:00:00')-1))" >> $GITHUB_ENV
- name: Set pypi and local version values
run: |
echo "LOCAL_VERSION=${{ env.DATE }}+${{ env.COMMIT_HASH }}" >> $GITHUB_ENV
if [ ${{ env.DATE_COMMIT_COUNT }} -le "0" ]
then
echo "PYPI_VERSION=${{ env.DATE }}" >> $GITHUB_ENV
else
echo "PYPI_VERSION=${{ env.DATE }}.post${{ env.DATE_COMMIT_COUNT }}" >> $GITHUB_ENV
fi
- name: Test versions
run: |
echo "${{ env.PYPI_VERSION }}"
echo "${{ env.LOCAL_VERSION }}"
- id: set_outputs
run: |
echo "pypi_version=${{ env.PYPI_VERSION }}" >> "$GITHUB_OUTPUT"
echo "local_version=${{ env.LOCAL_VERSION }}" >> "$GITHUB_OUTPUT"
echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT"
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.12" ]
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Build Wheel
run: |
make docker_stage
- name: Save Python build cache
uses: actions/cache@v3
with:
path: docker/root
key: ${{github.sha}}
# Build ARM64 container, only on master branch to save time testing
package-arm64:
runs-on: ubuntu-latest
needs: [
build
]
permissions:
contents: read
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
steps:
- uses: actions/checkout@v3
- name: Restore Python build cache
uses: actions/cache@v3
with:
path: docker/root
key: ${{github.sha}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/arm64
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Build Docker Image
run: |
docker buildx build \
--platform=linux/arm64 \
--cache-to=type=local,dest=/tmp/build-cache/arm64 \
--tag $IMAGE_NAME \
--label "runnumber=${GITHUB_RUN_ID}" \
--file docker/Dockerfile.gui \
docker/
- name: Save ARM64 build cache
uses: actions/cache@v3
with:
path: /tmp/build-cache/arm64
key: ${{github.sha}}-arm64
# Build AMD64 container
package-amd64:
runs-on: ubuntu-latest
needs: [
build
]
permissions:
contents: read
steps:
- uses: actions/checkout@v3
- name: Restore Python build cache
uses: actions/cache@v3
with:
path: docker/root
key: ${{github.sha}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/amd64
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Build Docker Image
run: |
docker buildx build \
--platform=linux/amd64 \
--cache-to=type=local,dest=/tmp/build-cache/amd64 \
--tag $IMAGE_NAME \
--label "runnumber=${GITHUB_RUN_ID}" \
--file docker/Dockerfile.gui \
docker/
- name: Save AMD64 build cache
uses: actions/cache@v3
with:
path: /tmp/build-cache/amd64
key: ${{github.sha}}-amd64
# On master branch, build the docker manifest file from the cached
# docker builds and push to the registry
deploy:
runs-on: ubuntu-latest
needs: [
version,
build,
package-arm64,
package-amd64
]
permissions:
packages: write
contents: read
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
steps:
- uses: actions/checkout@v3
- name: Restore Python build cache
uses: actions/cache@v3
with:
path: docker/root
key: ${{github.sha}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: linux/amd64,linux/arm64
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v2.0.0
- name: login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Restore ARM64 build cache
uses: actions/cache@v3
with:
path: /tmp/build-cache/arm64
key: ${{github.sha}}-arm64
- name: Restore AMD64 build cache
uses: actions/cache@v3
with:
path: /tmp/build-cache/amd64
key: ${{github.sha}}-amd64
- name: Format image_id
id: formatted-image_id
run: |
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME}
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
echo IMAGE_ID=${IMAGE_ID}
echo ::set-output name=IMAGE_ID::${IMAGE_ID}
- name: Get the version
id: formatted_version
run: |
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
# Strip "v" prefix from tag name
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
# Use Docker `latest` tag convention
[ "$VERSION" == "master" ] && VERSION=latest
echo VERSION=${VERSION}
echo ::set-output name=VERSION::${VERSION}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
labels: |
runnumber=${{ github.run_number }}
maintainer=${{ github.repository_owner }}
org.opencontainers.image.authors=${{ github.repository_owner }}
org.opencontainers.image.vendor=${{ github.repository_owner }}
org.opencontainers.image.documentation=https://ytdl-sub.readthedocs.io/
tags: |
type=raw,value=${{ steps.formatted_version.outputs.VERSION }}
type=raw,value=${{ needs.version.outputs.pypi_version }}
- name: Build Docker Image and push to registry
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
context: "docker/"
file: "docker/Dockerfile.gui"
cache-from: |
type=local,src=/tmp/build-cache/amd64
type=local,src=/tmp/build-cache/arm64

View file

@ -19,11 +19,54 @@ env:
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
version:
name: version
runs-on: ubuntu-latest
outputs:
pypi_version: ${{ steps.set_outputs.outputs.pypi_version }}
local_version: ${{ steps.set_outputs.outputs.local_version }}
init_contents: ${{ steps.set_outputs.outputs.init_contents }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: master
- name: Set date and commit hash variables
run: |
echo "DATE=$(date +'%Y.%m.%d')" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Count number of commits on master on day, minus 1 to account for post-push
run: |
echo "DATE_COMMIT_COUNT=$(($(git rev-list --count master --since='${{ env.DATE }} 00:00:00')-1))" >> $GITHUB_ENV
- name: Set pypi and local version values
run: |
echo "LOCAL_VERSION=${{ env.DATE }}+${{ env.COMMIT_HASH }}" >> $GITHUB_ENV
if [ ${{ env.DATE_COMMIT_COUNT }} -le "0" ]
then
echo "PYPI_VERSION=${{ env.DATE }}" >> $GITHUB_ENV
else
echo "PYPI_VERSION=${{ env.DATE }}.post${{ env.DATE_COMMIT_COUNT }}" >> $GITHUB_ENV
fi
- name: Test versions
run: |
echo "${{ env.PYPI_VERSION }}"
echo "${{ env.LOCAL_VERSION }}"
- id: set_outputs
run: |
echo "pypi_version=${{ env.PYPI_VERSION }}" >> "$GITHUB_OUTPUT"
echo "local_version=${{ env.LOCAL_VERSION }}" >> "$GITHUB_OUTPUT"
echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT"
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10" ]
python-version: [ "3.12" ]
permissions:
contents: read
@ -49,7 +92,7 @@ jobs:
# Build ARM64 container, only on master branch to save time testing
package-arm64:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
build
]
@ -57,7 +100,11 @@ jobs:
permissions:
contents: read
if: ${{ github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[DEV]') }}
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
steps:
- uses: actions/checkout@v3
@ -94,7 +141,7 @@ jobs:
# Build AMD64 container
package-amd64:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
build
]
@ -139,8 +186,9 @@ jobs:
# On master branch, build the docker manifest file from the cached
# docker builds and push to the registry
deploy:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
version,
build,
package-arm64,
package-amd64
@ -150,7 +198,11 @@ jobs:
packages: write
contents: read
if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') }}
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
steps:
- uses: actions/checkout@v3
@ -195,7 +247,7 @@ jobs:
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
echo IMAGE_ID=${IMAGE_ID}
echo ::set-output name=IMAGE_ID::${IMAGE_ID}
- name: Get the version
@ -208,16 +260,33 @@ jobs:
# Use Docker `latest` tag convention
[ "$VERSION" == "master" ] && VERSION=latest
echo VERSION=${VERSION}
echo ::set-output name=VERSION::${VERSION}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
labels: |
runnumber=${{ github.run_number }}
maintainer=${{ github.repository_owner }}
org.opencontainers.image.authors=${{ github.repository_owner }}
org.opencontainers.image.vendor=${{ github.repository_owner }}
org.opencontainers.image.documentation=https://ytdl-sub.readthedocs.io/
tags: |
type=raw,value=ubuntu-${{ steps.formatted_version.outputs.VERSION }}
type=raw,value=ubuntu-${{ needs.version.outputs.pypi_version }}
- name: Build Docker Image and push to registry
run: |
docker buildx build --push \
--platform=linux/amd64,linux/arm64 \
--cache-from=type=local,src=/tmp/build-cache/amd64 \
--cache-from=type=local,src=/tmp/build-cache/arm64 \
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:ubuntu-${{ steps.formatted_version.outputs.VERSION }} \
--label "runnumber=ubuntu-${GITHUB_RUN_ID}" \
--file docker/Dockerfile.ubuntu \
docker/
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
context: "docker/"
file: "docker/Dockerfile.ubuntu"
cache-from: |
type=local,src=/tmp/build-cache/amd64
type=local,src=/tmp/build-cache/arm64

View file

@ -19,11 +19,54 @@ env:
jobs:
# Push image to GitHub Packages.
# See also https://docs.docker.com/docker-hub/builds/
version:
name: version
runs-on: ubuntu-latest
outputs:
pypi_version: ${{ steps.set_outputs.outputs.pypi_version }}
local_version: ${{ steps.set_outputs.outputs.local_version }}
init_contents: ${{ steps.set_outputs.outputs.init_contents }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: master
- name: Set date and commit hash variables
run: |
echo "DATE=$(date +'%Y.%m.%d')" >> $GITHUB_ENV
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Count number of commits on master on day, minus 1 to account for post-push
run: |
echo "DATE_COMMIT_COUNT=$(($(git rev-list --count master --since='${{ env.DATE }} 00:00:00')-1))" >> $GITHUB_ENV
- name: Set pypi and local version values
run: |
echo "LOCAL_VERSION=${{ env.DATE }}+${{ env.COMMIT_HASH }}" >> $GITHUB_ENV
if [ ${{ env.DATE_COMMIT_COUNT }} -le "0" ]
then
echo "PYPI_VERSION=${{ env.DATE }}" >> $GITHUB_ENV
else
echo "PYPI_VERSION=${{ env.DATE }}.post${{ env.DATE_COMMIT_COUNT }}" >> $GITHUB_ENV
fi
- name: Test versions
run: |
echo "${{ env.PYPI_VERSION }}"
echo "${{ env.LOCAL_VERSION }}"
- id: set_outputs
run: |
echo "pypi_version=${{ env.PYPI_VERSION }}" >> "$GITHUB_OUTPUT"
echo "local_version=${{ env.LOCAL_VERSION }}" >> "$GITHUB_OUTPUT"
echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT"
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10" ]
python-version: [ "3.12" ]
permissions:
contents: read
@ -49,7 +92,7 @@ jobs:
# Build ARM64 container, only on master branch to save time testing
package-arm64:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
build
]
@ -93,7 +136,7 @@ jobs:
# Build AMD64 container
package-amd64:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
build
]
@ -137,8 +180,9 @@ jobs:
# On master branch, build the docker manifest file from the cached
# docker builds and push to the registry
deploy:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
needs: [
version,
build,
package-arm64,
package-amd64
@ -148,7 +192,11 @@ jobs:
packages: write
contents: read
if: ${{ github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[DEV]') }}
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
steps:
- uses: actions/checkout@v3
@ -193,7 +241,7 @@ jobs:
# Change all uppercase to lowercase
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
echo IMAGE_ID=${IMAGE_ID}
echo ::set-output name=IMAGE_ID::${IMAGE_ID}
- name: Get the version
@ -206,15 +254,33 @@ jobs:
# Use Docker `latest` tag convention
[ "$VERSION" == "master" ] && VERSION=latest
echo VERSION=${VERSION}
echo ::set-output name=VERSION::${VERSION}
- name: Build Docker Image and push to registry
run: |
docker buildx build --push \
--platform=linux/amd64,linux/arm64 \
--cache-from=type=local,src=/tmp/build-cache/amd64 \
--cache-from=type=local,src=/tmp/build-cache/arm64 \
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:${{ steps.formatted_version.outputs.VERSION }} \
--label "runnumber=${GITHUB_RUN_ID}" \
docker/
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
labels: |
runnumber=${{ github.run_number }}
maintainer=${{ github.repository_owner }}
org.opencontainers.image.authors=${{ github.repository_owner }}
org.opencontainers.image.vendor=${{ github.repository_owner }}
org.opencontainers.image.documentation=https://ytdl-sub.readthedocs.io/
tags: |
type=raw,value=${{ steps.formatted_version.outputs.VERSION }}
type=raw,value=${{ needs.version.outputs.pypi_version }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
context: "docker/"
cache-from: |
type=local,src=/tmp/build-cache/amd64
type=local,src=/tmp/build-cache/arm64

View file

@ -61,9 +61,16 @@ jobs:
strategy:
matrix:
arch: [ "aarch64", "x86_64" ]
runs-on: ubuntu-latest
include:
- arch: "aarch64"
runner: "ubuntu-24.04-arm"
container: "quay.io/pypa/manylinux_2_28_aarch64"
- arch: "x86_64"
runner: "ubuntu-latest"
container: "quay.io/pypa/manylinux_2_28_x86_64"
runs-on: ${{ matrix.runner }}
container:
image: quay.io/pypa/manylinux_2_28_x86_64
image: ${{ matrix.container }}
steps:
- uses: actions/checkout@v3
- name: Write version to init file
@ -76,17 +83,17 @@ jobs:
dnf install -y epel-release tar wget make gcc openssl-devel bzip2-devel libffi-devel zlib-devel
- name: Install Python
run: |
wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tar.xz
tar -xf Python-3.10.10.tar.xz
cd Python-3.10.10 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
wget https://www.python.org/ftp/python/3.12.9/Python-3.12.9.tar.xz
tar -xf Python-3.12.9.tar.xz
cd Python-3.12.9 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
make -j 8
make altinstall
python3.10 --version
python3.10 -m ensurepip --upgrade
python3.12 --version
python3.12 -m ensurepip --upgrade
- name: Build Package
run: |
python3.10 -m pip install -e .
python3.10 -m pip install pyinstaller
python3.12 -m pip install -e .
python3.12 -m pip install pyinstaller
# Build executable
pyinstaller ytdl-sub.spec
mkdir -p /opt/builds
@ -95,7 +102,7 @@ jobs:
mv dist/ytdl-sub /opt/builds/ytdl-sub_${{ matrix.arch }}
- name: Upload build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ytdl-sub_${{ matrix.arch }}
path: /opt/builds/ytdl-sub_${{ matrix.arch }}
@ -106,13 +113,13 @@ jobs:
name: build-windows
needs:
- version
runs-on: windows-2019
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.12"
- name: Write version to init file
run: |
echo '${{ needs.version.outputs.init_contents }}'> src/ytdl_sub/__init__.py
@ -124,7 +131,7 @@ jobs:
.\dist\ytdl-sub.exe -h
- name: Upload build
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ytdl-sub_exe
path: .\dist\ytdl-sub.exe
@ -145,19 +152,19 @@ jobs:
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
- name: Restore exe build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ytdl-sub_exe
path: /opt/builds
- name: Restore aarch64 build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ytdl-sub_aarch64
path: /opt/builds
- name: Restore x86_64 build
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ytdl-sub_x86_64
path: /opt/builds
@ -171,7 +178,11 @@ jobs:
ls -lh /opt/builds
- name: Create Release
if: ${{ github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[DEV]') }}
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
id: create_release
uses: softprops/action-gh-release@v1
with:
@ -189,13 +200,13 @@ jobs:
name: pypi-publish
needs:
- version
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: '3.12'
- name: Write version to init file
run: |
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
@ -207,7 +218,11 @@ jobs:
python3 -m build
- name: Publish distribution 📦 to PyPI
if: ${{ github.ref == 'refs/heads/master' && !contains(github.event.head_commit.message, '[DEV]') }}
if: ${{
github.ref == 'refs/heads/master'
&& !contains(github.event.head_commit.message, '[DEV]')
&& !contains(github.event.head_commit.message, '[DOCS]')
}}
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}

10
.gitignore vendored
View file

@ -141,8 +141,16 @@ dmypy.json
docker/*.whl
docker/root/*.whl
docker/root/defaults/examples
docker/testing/volumes
.local/
.ytdl-sub-working-directory
.ytdl-sub-lock
ffmpeg.exe
ffprobe.exe
ffprobe.exe
tools/docgen/out
prof/

View file

@ -1,15 +1,17 @@
version: 2
build:
os: ubuntu-20.04
os: "ubuntu-22.04"
tools:
python: "3.10"
sphinx:
configuration: docs/conf.py
configuration: docs/source/conf.py
fail_on_warning: true
python:
install:
- requirements: docs/source/requirements.txt
- method: pip
path: .
extra_requirements:

View file

@ -1,7 +1,20 @@
# Defensive settings for make:
# https://tech.davis-hansson.com/p/make/
SHELL:=bash
.ONESHELL:
.SHELLFLAGS:=-eu -o pipefail -c
.SILENT:
.DELETE_ON_ERROR:
MAKEFLAGS+=--warn-undefined-variables
MAKEFLAGS+=--no-builtin-rules
export PS1?=$$
# Prefix echoed recipe commands with the recipe line number for debugging:
export PS4?=:$$LINENO+
# Get version related variables
export DATE=$(shell date +'%Y.%m.%d')
export DATE_COMMIT_COUNT=$(shell git rev-list --count HEAD --since="$(DATE) 00:00:00")
export COMMIT_HASH=$(shell git rev-parse --short HEAD)
export DATE:=$(shell date +'%Y.%m.%d')
export DATE_COMMIT_COUNT:=$(shell git rev-list --count HEAD --since="$(DATE) 00:00:00")
export COMMIT_HASH:=$(shell git rev-parse --short HEAD)
# Set Local version to YYYY.MM.DD-<hash>
export LOCAL_VERSION="$(DATE)+$(COMMIT_HASH)"
@ -13,18 +26,26 @@ else
export PYPI_VERSION="$(DATE).post$(DATE_COMMIT_COUNT)"
endif
# Finished with `$(shell)`, echo recipe commands going forward
.SHELLFLAGS+= -x
### Top-level targets:
.PHONY: all
all: check_lint docs docker docker_ubuntu docker_gui
lint:
@-isort .
@-black .
@-pylint src/
@-pydocstyle src/*
python3 -m ruff format .
python3 -m ruff check --fix .
python3 -m pylint src
check_lint:
isort . --check-only --diff \
&& black . --check \
&& pylint src/ \
&& pydocstyle src/*
ruff format --check . \
&& ruff check . \
&& pylint src/
wheel: clean
$(shell echo "__pypi_version__ = \"$(PYPI_VERSION)\"\n__local_version__ = \"$(LOCAL_VERSION)\"" > src/ytdl_sub/__init__.py)
$(shell echo "__pypi_version__ = \"$(PYPI_VERSION)\"" > src/ytdl_sub/__init__.py)
$(shell echo "__local_version__ = \"$(LOCAL_VERSION)\"" >> src/ytdl_sub/__init__.py)
cat src/ytdl_sub/__init__.py
pip3 install build
python3 -m build
@ -34,19 +55,23 @@ docker_stage: wheel
docker: docker_stage
sudo docker build --progress=plain --no-cache -t ytdl-sub:local docker/
docker_ubuntu: docker_stage
sudo docker build --progress=plain --no-cache -t ytdl-sub:local_ubuntu -f docker/Dockerfile.ubuntu docker/
sudo docker build --progress=plain --no-cache -t ytdl-sub-ubuntu:local -f docker/Dockerfile.ubuntu docker/
docker_gui: docker_stage
sudo docker build --progress=plain --no-cache -t ytdl-sub-gui:local -f docker/Dockerfile.gui docker/
executable: clean
pyinstaller ytdl-sub.spec
mv dist/ytdl-sub dist/ytdl-sub${EXEC_SUFFIX}
docs:
sphinx-build -a -b html docs docs/_html
REGENERATE_DOCS=1 pytest tests/unit/docgen/test_docgen.py
sphinx-build --write-all --fail-on-warning --nitpicky -b html \
"./docs/source/" "./docs/build/"
clean:
rm -rf \
.pytest_cache/ \
build/ \
dist/ \
src/ytdl_sub.egg-info/ \
docs/_html/ \
docs/build/ \
.coverage \
docker/root/*.whl \
docker/root/defaults/examples \

334
README.md
View file

@ -33,221 +33,187 @@ maximum flexibility while maintaining simplicity.
#### Jellyfin
![jelly_mv](https://user-images.githubusercontent.com/10107080/182677256-43aeb029-0c3f-4648-9fd2-352b9666b262.PNG)
### SoundCloud Albums and Singles
#### MusicBee (any file or tag-based music players)
### SoundCloud Discography
#### Writes proper music-tags via beets API
![sc_mb](https://user-images.githubusercontent.com/10107080/182685415-06adf477-3dd3-475d-bbcd-53b0152b9f0a.PNG)
### Bandcamp Discography
#### Navidrome (any file or tag-based music servers)
![bc_nav](https://user-images.githubusercontent.com/10107080/212503861-1d8748e6-6f6d-4043-b543-84226cd1f662.png)
## How it Works
`ytdl-sub` uses YAML configs to define a layout for how you want media to look
after it is downloaded. See our
[walk-through guide](https://github.com/jmbannon/ytdl-sub/wiki)
on how to get started. Ready-to-use
[example configurations](https://github.com/jmbannon/ytdl-sub/tree/master/examples)
can be found here alongside our
[readthedocs](https://ytdl-sub.readthedocs.io/en/latest/config.html#)
for detailed information on config fields.
### Config
The `config.yaml` defines how our downloads will look. For this example, let us
download YouTube channels and generate metadata to look like TV shows using
ytdl-sub's prebuilt presets. No additional plugins or programs are needed for
Kodi, Jellyfin, Plex, or Emby to recognize your downloads. This can also be
used to download any yt-dlp supported URL, including YouTube playlists, Bitchute channels, etc.
`ytdl-sub` uses YAML files to define subscriptions. Each subscription imports _presets_ that
define how to handle and output media files. `ytdl-sub` comes packaged with many _prebuilt presets_
that do the work of config-building, so you can start downloading immediately.
```yaml
# Set the working directory which will be used to stage downloads
# before placing them in your desired output directory.
configuration:
working_directory: '.ytdl-sub-downloads'
# subscriptions.yaml:
# Everything in here can be downloaded using the command:
# ytdl-sub sub subscriptions.yaml
# Presets are where you create 'sub-configs' that can can be
# merged together to dictate what is downloaded, how to format it,
# and what metadata to generate.
presets:
# __preset__ is a place to define global overrides for all subscriptions
__preset__:
overrides:
# Root folder of all ytdl-sub TV Shows
tv_show_directory: "/tv_shows"
# Root folder of all ytdl-sub Music
music_directory: "/music"
# Root folder of all ytdl-sub Music Videos
music_video_directory: "/music_videos"
# For 'Only Recent' preset, only keep vids within this range and limit
only_recent_date_range: "2months"
only_recent_max_files: 30
# Pass any arg directly to yt-dlp's Python API
ytdl_options:
cookiefile: "/config/ytdl-sub-configs/cookie.txt"
# Let us create a preset called `only_recent_videos` that will
# only download recent videos in the last 2 months.
only_recent_videos:
###################################################################
# TV Show Presets. Can replace Plex with Plex/Jellyfin/Emby/Kodi
# Use the `date_range` plugin to specify ytdl-sub to only
# download videos after today MINUS {download_range}, which
# is an override variable that we can alter per channel.
date_range:
after: "today-{download_range}"
Plex TV Show by Date:
# Any yt-dlp argument can be passed via ytdl-sub. Let us set
# yt-dlp's `break_on_reject` to True to stop downloading after
# any video is rejected. Videos will be rejected if they are
# uploaded after our {download_range}.
ytdl_options:
break_on_reject: True
# Sets genre tag to "Documentaries"
= Documentaries:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
# Deletes any videos uploaded after {download_range}.
output_options:
keep_files_after: "today-{download_range}"
# Sets genre tag to "Kids", "TV-Y" for content rating
= Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
# Set the override variable {download_range} to 2months.
# This will serve as our default value. We can override
# this per channel or in a child preset.
overrides:
download_range: "2months"
= Music:
# TV show subscriptions can support multiple urls and store in the same TV Show
"Rick Beato":
- "https://www.youtube.com/@RickBeato"
- "https://www.youtube.com/@rickbeato240"
####################################################################
# Set genre tag to "News", use `Only Recent` preset to only store videos uploaded recently
= News | Only Recent:
"BBC News": "https://www.youtube.com/@BBCNews"
# Now let us create a preset that downloads videos and formats
# as TV shows.
tv_show:
Plex TV Show Collection:
= Music:
# Prefix with ~ to set specific override variables
"~Beyond the Guitar":
s01_name: "Videos"
s01_url: "https://www.youtube.com/c/BeyondTheGuitar"
s02_name: "Covers"
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
# Presets can inherit all attributes from other presets. Our
# `tv_show` preset will inherit these presets built into ytdl-sub.
preset:
# Let us specify all the TV show by date presets to support all
# players. You only need to specify one, but this ensures
# compatibility with all players.
- "kodi_tv_show_by_date"
- "jellyfin_tv_show_by_date"
- "plex_tv_show_by_date"
# Now we choose a preset that defines how our seasons and
# episode numbers look.
- "season_by_year__episode_by_month_day"
###################################################################
# Music Presets.
# Set override variables that will be applicable to all downloads
# in main presets.
overrides:
tv_show_directory: "/tv_shows" # Replace with desired directory
YouTube Releases:
= Jazz: # Sets genre tag to "Jazz"
"Thelonious Monk": "https://www.youtube.com/@theloniousmonk3870/releases"
YouTube Full Albums:
= Lofi:
"Game Chops": "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i"
SoundCloud Discography:
= Chill Hop:
"UKNOWY": "https://soundcloud.com/uknowymunich"
= Synthwave:
"Lazerdiscs Records": "https://soundcloud.com/lazerdiscsrecords"
"Earmake": "https://soundcloud.com/earmake"
Bandcamp:
= Lofi:
"Emily Hopkins": "https://emilyharpist.bandcamp.com/"
###################################################################
# Music Video Presets. Can replace Plex with Plex/Jellyfin/Kodi
"Plex Music Videos":
= Pop: # Sets genre tag to "Pop"
"Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
"Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
```
### Subscriptions
The `subscriptions.yaml` file is where we define content to download using
presets in the `config.yaml`. Each subscription can overwrite any field used
in a preset.
```yaml
# The name of our subscription. Let us create one to download
# ALL of Rick A's videos
rick_a:
# Inherit our `tv_show` preset we made above
preset:
- "tv_show"
# Set override variables to set the channel URL and the
# name we want to give the TV show.
overrides:
tv_show_name: "Rick A"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
# Let us make another subscription that will only download Rick A's
# video's in the last 2 weeks.
rick_a_recent:
# Inherit our `tv_show` AND `only_recent_videos` preset
# Bottom-most presets take precedence.
preset:
- "tv_show"
- "only_recent_videos"
# Set override variables for this subscription. Modify the
# `download_range` to only download and keep 2 weeks' worth
# of videos.
overrides:
tv_show_name: "Rick A"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
download_range: "2weeks"
```
The download can now be performed using:
```shell
All of this can be downloaded and ready to import to your favorite player
using the command
```commandline
ytdl-sub sub subscriptions.yaml
```
To preview what your output files before doing any downloads, you can dry run using:
```shell
ytdl-sub --dry-run sub subscriptions.yaml
```
### One-time Download
There are things we will only want to download once and never again. Anything
you can define in a subscription can be defined using CLI arguments. This
example is equivalent to the subscription example above:
```shell
ytdl-sub dl \
--preset "tv_show" \
--overrides.tv_show_name "Rick A" \
--overrides.url "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
```
#### Download Aliases
In the `config.yaml`, we can define aliases to make `dl` commands shorter.
```yaml
configuration:
dl_aliases:
tv: "--preset tv_show"
name: "--overrides.tv_show_name"
url: "--overrides.url"
```
The above command can now be shortened to
```shell
ytdl-sub dl \
--tv \
--name "Rick A" \
--url "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
```
See our
[example subscriptions](https://github.com/jmbannon/ytdl-sub/tree/master/examples)
for more detailed examples and use-cases.
### Output
After `ytdl-sub` runs, the end result will download and format the channel
files into something ready to be consumed by your favorite media player or
server.
After `ytdl-sub` runs, the end result will download and format the files into something ready
to be consumed by your favorite media player/server.
```
/path/to/tv_shows/Rick Aß
/Season 2021
s2021.e0317 - Pattys Day Video-thumb.jpg
s2021.e0317 - Pattys Day Video.mp4
s2021.e0317 - Pattys Day Video.nfo
/Season 2022
s2022.e1225 - Merry Christmas-thumb.jpg
s2022.e1225 - Merry Christmas.mp4
s2022.e1225 - Merry Christmas.nfo
poster.jpg
fanart.jpg
tvshow.nfo
tv_shows/
Jake Trains/
Season 2021/
s2021.e031701 - Pattys Day Video-thumb.jpg
s2021.e031701 - Pattys Day Video.mp4
s2021.e031701 - Pattys Day Video.nfo
s2021.e031702 - Second Pattys Day Video-thumb.jpg
s2021.e031702 - Second Pattys Day Video.mp4
s2021.e031702 - Second Pattys Day Video.nfo
Season 2022/
s2022.e122501 - Merry Christmas-thumb.jpg
s2022.e122501 - Merry Christmas.mp4
s2022.e122501 - Merry Christmas.nfo
poster.jpg
fanart.jpg
tvshow.nfo
music/
Artist/
[2022] Some Single/
01 - Some Single.mp3
folder.jpg
[2023] Latest Album/
01 - Track Title.mp3
02 - Another Track.mp3
folder.jpg
music_videos/
Elton John/
Elton John - Rocketman.jpg
Elton John - Rocketman.mp4
```
### Beyond TV Shows
The above example made heavy-use of `ytdl-sub` prebuilt presets and hides many
features that are offered. `ytdl-sub` strives to support _any_ use case that first requires
a download via yt-dlp. Use `ytdl-sub` to download, format, and convert media for your media
player to recognize downloads as:
- Movies
- TV shows
- From a single channel or playlist
- From multiple channels or playlists
- From individual videos
- Extracted audio as podcasts
- Music videos
- Music, including:
- Individual songs
- Albums
- Discographies
## Custom Configs
Any part of this process is modifiable by using custom configs. See our
[walk-through guide](https://ytdl-sub.readthedocs.io/en/latest/guides/index.html)
on how to build your first config from scratch. Ready-to-use
[example configurations](https://github.com/jmbannon/ytdl-sub/tree/master/examples)
can be found here alongside our
[readthedocs](https://ytdl-sub.readthedocs.io/en/latest/index.html)
for detailed information on all config fields.
## Installation
`ytdl-sub` can be installed on the following platforms.
- [Docker Compose](https://ytdl-sub.readthedocs.io/en/latest/install.html#docker-compose_)
- [with CPU passthrough](https://ytdl-sub.readthedocs.io/en/latest/install.html#cpu-passthrough)
- [with GPU passthrough](https://ytdl-sub.readthedocs.io/en/latest/install.html#nvidia-gpu-passthrough)
- [Docker CLI](https://ytdl-sub.readthedocs.io/en/latest/install.html#docker)
- [Windows](https://ytdl-sub.readthedocs.io/en/latest/install.html#windows)
- [Unraid](https://ytdl-sub.readthedocs.io/en/latest/install.html#unraid)
- [Linux](https://ytdl-sub.readthedocs.io/en/latest/install.html#linux)
- [Linux ARM](https://ytdl-sub.readthedocs.io/en/latest/install.html#linux-arm)
- [PIP](https://ytdl-sub.readthedocs.io/en/latest/install.html#pip)
- [Local Install](https://ytdl-sub.readthedocs.io/en/latest/install.html#local-install)
- [Local Docker Build](https://ytdl-sub.readthedocs.io/en/latest/install.html#local-docker-build)
- [Docker Compose](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#install-with-docker-compose)
- [Web-GUI](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#install-with-docker-compose)
- [Headless](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#install-with-docker-compose)
- [CPU / GPU Passthrough](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#device-passthrough)
- [Docker CLI](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#docker-cli)
- [Windows](https://ytdl-sub.readthedocs.io/en/latest/guides/install/windows.html)
- [Unraid](https://ytdl-sub.readthedocs.io/en/latest/guides/install/unraid.html)
- [Linux](https://ytdl-sub.readthedocs.io/en/latest/guides/install/linux.html)
- [Linux ARM](https://ytdl-sub.readthedocs.io/en/latest/guides/install/linux.html)
- [PIP](https://ytdl-sub.readthedocs.io/en/latest/guides/install/agnostic.html#pip-install)
- [Local Install](https://ytdl-sub.readthedocs.io/en/latest/guides/install/agnostic.html#local-install)
- [Local Docker Build](https://ytdl-sub.readthedocs.io/en/latest/guides/install/agnostic.html#local-docker-build)
### Docker Installation
Docker installs can be either headless or use the Web-GUI image, which comprises
[LSIO's](https://www.linuxserver.io/)
[code-server](https://hub.docker.com/r/linuxserver/code-server)
Docker image with `ytdl-sub` preinstalled. This is the recommended way to use ``ytdl-sub``.
![image](https://github.com/jmbannon/ytdl-sub/assets/10107080/c2aac8a1-5443-4345-b438-be4b17187c80)
## Contributing
There are many ways to contribute, even without coding. Please take a look in
@ -258,5 +224,5 @@ pick up a bug.
We are pretty active in our
[Discord channel](https://discord.gg/v8j9RAHb4k)
if you have any questions. Also see our
[FAQ](https://github.com/jmbannon/ytdl-sub/wiki/FAQ)
[FAQ](https://ytdl-sub.readthedocs.io/en/latest/faq/index.html)
for commonly asked questions.

View file

@ -3,18 +3,25 @@ FROM ghcr.io/linuxserver/baseimage-alpine:edge
###############################################################################
# YTDL-SUB INSTALL
# Needed for phantomjs
# For phantomjs
ENV OPENSSL_CONF="/etc/ssl"
# For downloading thumbnails
ENV SSL_CERT_DIR="/etc/ssl/certs/"
# Working directory used at both build and run times:
ENV DEFAULT_WORKSPACE="/config"
COPY root/ /
RUN mkdir -p /config && \
RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
apk update --no-cache && \
apk upgrade --no-cache && \
apk add --no-cache --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \
vim \
g++ \
nano \
unzip \
make \
deno \
libffi-dev \
"python3>=3.10" \
py3-pip \
fontconfig \
@ -25,34 +32,44 @@ RUN mkdir -p /config && \
"aria2>=1.36.0" && \
ffmpeg -version && \
aria2c --version && \
deno --version && \
# Install phantomjs if using x86_64, ensure it is properly installed
if [[ $(uname -m) == "x86_64" ]]; then \
echo "installing phantomjs" && \
apk add --no-cache gcompat && \
cd /usr/share && \
curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xj && \
mv /usr/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs phantomjs && \
rm -rf /usr/share/phantomjs-2.1.1-linux-x86_64 && \
tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/share/phantomjs && \
rm -rf phantomjs-2.1.1-linux-x86_64 && \
rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
ln -s /usr/share/phantomjs /usr/bin/phantomjs && \
echo "Phantom JS version:" && \
phantomjs --version && \
cd -; \
fi && \
echo "hi" && \
# Install ytdl-sub, ensure it is installed properly
python3 -m pip install --break-system-packages --no-cache-dir ytdl_sub-*.whl && \
# Configure pip globally
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
# Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly
python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \
ytdl-sub -h && \
# Delete unneeded packages after install
rm ytdl_sub-*.whl && \
apk del \
g++ \
make \
py3-pip \
py3-setuptools
libffi-dev && \
python3 -m pip --help
###############################################################################
# CONTAINER CONFIGS
ENV EDITOR="nano" \
HOME="/config"
HOME="${DEFAULT_WORKSPACE}" \
DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \
CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \
CRON_WRAPPER_SCRIPT="${DEFAULT_WORKSPACE}/.cron_wrapper" \
LOGS_TO_STDOUT="${DEFAULT_WORKSPACE}/.cron.log" \
LSIO_FIRST_PARTY=false
VOLUME /config
VOLUME "${DEFAULT_WORKSPACE}"
WORKDIR "${DEFAULT_WORKSPACE}"

97
docker/Dockerfile.gui Normal file
View file

@ -0,0 +1,97 @@
FROM lscr.io/linuxserver/code-server:4.98.2
# For phantomjs
ENV OPENSSL_CONF="/etc/ssl"
# For downloading thumbnails
ENV SSL_CERT_DIR="/etc/ssl/certs/"
# Working directory used at both build and run times:
ENV DEFAULT_WORKSPACE="/config/ytdl-sub-configs"
###############################################################################
# YTDL-SUB INSTALL
SHELL ["/bin/bash", "-c"]
COPY root/ /
RUN mkdir -p /config && \
apt-get -y update && \
apt-get -y upgrade && \
apt-get install --no-install-recommends -y \
software-properties-common && \
apt-get -y update && \
apt-get -y upgrade && \
apt-get install --no-install-recommends -y \
vim \
g++ \
nano \
unzip \
make \
python3-pip \
fontconfig \
xz-utils \
bzip2 \
aria2 \
python3-venv && \
if [[ $(uname -m) == "x86_64" ]]; then \
curl -L -o ffmpeg.tar.gz https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz && \
tar -xf ffmpeg.tar.gz && \
chmod +x ffmpeg-master-latest-linux64-gpl/bin/ffmpeg && \
chmod +x ffmpeg-master-latest-linux64-gpl/bin/ffprobe && \
mv ffmpeg-master-latest-linux64-gpl/bin/ffmpeg /usr/bin/ffmpeg && \
mv ffmpeg-master-latest-linux64-gpl/bin/ffprobe /usr/bin/ffprobe && \
rm ffmpeg.tar.gz && \
rm -rf ffmpeg-master-latest-linux64-gpl/ ; \
else \
curl -L -o ffmpeg.tar.gz https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz && \
tar -xf ffmpeg.tar.gz && \
chmod +x ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg && \
chmod +x ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe && \
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg /usr/bin/ffmpeg && \
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe /usr/bin/ffprobe && \
rm ffmpeg.tar.gz && \
rm -rf ffmpeg-master-latest-linuxarm64-gpl/ ; \
fi && \
# Ensure ffmpeg is installed
ffmpeg -version && \
# Install phantomjs if using x86_64, ensure it is properly installed
if [[ $(uname -m) == "x86_64" ]]; then \
echo "installing phantomjs" && \
tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/phantomjs && \
rm -rf phantomjs-2.1.1-linux-x86_64 && \
rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
echo "Phantom JS version:" && \
phantomjs --version ; \
fi && \
# Install Deno, required for YouTube downloads
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y --no-modify-path && \
deno --help && \
# Configure pip globally
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
# Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly
python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \
ytdl-sub -h && \
# Delete unneeded packages after install
rm ytdl_sub-*.whl && \
apt-get remove -y \
g++ \
make \
xz-utils \
bzip2 && \
apt-get autoremove -y && \
apt-get purge -y --auto-remove && \
rm -rf /var/lib/apt/lists/* && \
python3 -m pip --help
###############################################################################
# CONTAINER CONFIGS
ENV EDITOR="nano" \
HOME="/config" \
DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \
CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \
CRON_WRAPPER_SCRIPT="/config/.cron_wrapper" \
LOGS_TO_STDOUT=/config/.cron.log \
LSIO_FIRST_PARTY=false
VOLUME /config
WORKDIR "${DEFAULT_WORKSPACE}"

1
docker/Dockerfile.headless Symbolic link
View file

@ -0,0 +1 @@
Dockerfile

View file

@ -1,17 +1,21 @@
FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy
FROM ghcr.io/linuxserver/baseimage-ubuntu:noble
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND=noninteractive
# Needed for phantomjs
ENV OPENSSL_CONF=/etc/ssl
# For phantomjs
ENV OPENSSL_CONF="/etc/ssl"
# For downloading thumbnails
ENV SSL_CERT_DIR="/etc/ssl/certs/"
# Working directory used at both build and run times:
ENV DEFAULT_WORKSPACE="/config"
###############################################################################
# YTDL-SUB INSTALL
SHELL ["/bin/bash", "-c"]
COPY root/ /
RUN mkdir -p /config && \
RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
apt-get -y update && \
apt-get -y upgrade && \
apt-get install --no-install-recommends -y \
@ -22,8 +26,8 @@ RUN mkdir -p /config && \
vim \
g++ \
nano \
unzip \
make \
python3.10-dev \
python3-pip \
fontconfig \
xz-utils \
@ -53,16 +57,21 @@ RUN mkdir -p /config && \
ffmpeg -version && \
# Install phantomjs if using x86_64, ensure it is properly installed
if [[ $(uname -m) == "x86_64" ]]; then \
curl -L -o phantomjs.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
tar -xvf phantomjs.tar.bz2 && \
echo "installing phantomjs" && \
tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/phantomjs && \
rm -rf phantomjs-2.1.1-linux-x86_64/ && \
rm phantomjs.tar.bz2 && \
rm -rf phantomjs-2.1.1-linux-x86_64 && \
rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
echo "Phantom JS version:" && \
phantomjs --version ; \
fi && \
# Install ytdl-sub, ensure it is installed properly
pip install --no-cache-dir ytdl_sub-*.whl && \
# Install Deno, required for YouTube downloads
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y --no-modify-path && \
deno --help && \
# Configure pip globally
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
# Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly
python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \
ytdl-sub -h && \
# Delete unneeded packages after install
rm ytdl_sub-*.whl && \
@ -70,17 +79,22 @@ RUN mkdir -p /config && \
g++ \
make \
xz-utils \
bzip2 \
python3.10-dev \
python3-venv && \
bzip2 && \
apt-get autoremove -y && \
apt-get purge -y --auto-remove && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/* && \
python3 -m pip --help
###############################################################################
# CONTAINER CONFIGS
ENV EDITOR="nano" \
HOME="/config"
HOME="${DEFAULT_WORKSPACE}" \
DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \
CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \
CRON_WRAPPER_SCRIPT="${DEFAULT_WORKSPACE}/.cron_wrapper" \
LOGS_TO_STDOUT="${DEFAULT_WORKSPACE}/.cron.log" \
LSIO_FIRST_PARTY=false
VOLUME /config
VOLUME "${DEFAULT_WORKSPACE}"
WORKDIR "${DEFAULT_WORKSPACE}"

View file

@ -0,0 +1,86 @@
#!/usr/bin/with-contenv bash
echo "Starting ytdl-sub..."
# copy config
[[ ! -e "$DEFAULT_WORKSPACE/config.yaml" ]] && \
mkdir -p "$DEFAULT_WORKSPACE" && \
cp /defaults/config.yaml "$DEFAULT_WORKSPACE/config.yaml"
[[ ! -e "$DEFAULT_WORKSPACE/subscriptions.yaml" ]] && \
mkdir -p "$DEFAULT_WORKSPACE" && \
cp /defaults/subscriptions.yaml "$DEFAULT_WORKSPACE/subscriptions.yaml"
[[ ! -d "$DEFAULT_WORKSPACE/examples" ]] && \
mkdir -p "$DEFAULT_WORKSPACE/examples" && \
cp -r /defaults/examples/* "$DEFAULT_WORKSPACE/examples"
[[ ! -e "/config/.bashrc" ]] && \
echo "alias ls='ls --color=auto'" > /config/.bashrc && \
echo "cd ." >> /config/.bashrc
# always create empty cron log file on start
echo "" > "$LOGS_TO_STDOUT"
# permissions
chown -R ${PUID:-abc}:${PGID:-abc} \
/config
# update command reference:
# https://github.com/yt-dlp/yt-dlp/wiki/Installation#with-pip
if [ "$UPDATE_YT_DLP_ON_START" == "stable" ] ; then
echo "UPDATE_YT_DLP_ON_START is set to stable, attempting to update to a new stable version of yt-dlp if it exists."
python3 -m pip install -U "yt-dlp[default]"
elif [ "$UPDATE_YT_DLP_ON_START" == "nightly" ] ; then
echo "UPDATE_YT_DLP_ON_START is set to nightly, attempting to update to the latest nightly version of yt-dlp."
python3 -m pip install -U --pre "yt-dlp[default]"
elif [ "$UPDATE_YT_DLP_ON_START" == "master" ] ; then
echo "UPDATE_YT_DLP_ON_START is set to master, pulling yt-dlp's latest commit for install."
python3 -m pip install -U pip hatchling wheel
python3 -m pip install --force-reinstall "yt-dlp[default] @ https://github.com/yt-dlp/yt-dlp/archive/master.tar.gz"
else
echo "UPDATE_YT_DLP_ON_START is not set, using packaged version."
fi
# set up cron
if [ "$CRON_SCHEDULE" != "" ] ; then
[[ ! -e "$CRON_SCRIPT" ]] && \
cp /defaults/cron "$CRON_SCRIPT"
# create cron script wrapper
echo '#!/bin/bash' > "$CRON_WRAPPER_SCRIPT"
# Echo commands for easier user debugging:
echo "set -x" >> "$CRON_WRAPPER_SCRIPT"
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> "$CRON_WRAPPER_SCRIPT"
echo "cd \"$DEFAULT_WORKSPACE\"" >> "$CRON_WRAPPER_SCRIPT"
echo ". \"$CRON_SCRIPT\" >> \"$LOGS_TO_STDOUT\" 2>&1" >> "$CRON_WRAPPER_SCRIPT"
chmod +x "$CRON_WRAPPER_SCRIPT"
chown abc:abc "$CRON_WRAPPER_SCRIPT"
# Set the crontab file to the schedule, cleanly
CRON_SCHEDULE_CLEAN="${CRON_SCHEDULE//\"/}"
CRON_SCHEDULE_CLEAN="${CRON_SCHEDULE_CLEAN//\'/}"
echo "# min hour day month weekday command" > /config/crontabs/abc
echo "$CRON_SCHEDULE_CLEAN $CRON_WRAPPER_SCRIPT" >> /config/crontabs/abc
chmod +x "$CRON_SCRIPT"
chown abc:abc "$CRON_SCRIPT"
crontab -u abc /config/crontabs/abc
CRON_SUCCESS=$?
if [ $CRON_SUCCESS -eq 0 ] ; then
echo "Cron enabled with schedule $CRON_SCHEDULE_CLEAN"
if [ "$CRON_RUN_ON_START" = true ] ; then
echo "Running cron script on start in the background"
# ensure it runs as abc to respect puid/guid with delay for tail to start
su -s "/bin/bash" -c "sleep 5 && . '$CRON_WRAPPER_SCRIPT'" abc > /dev/null 2>&1 &
fi
else
echo "Error in CRON_SCHEDULE definition, disabling cron."
exit 1
fi
else
echo "CRON_SCHEDULE not specified, leaving crontabs as-is. Current configuration in /config/crontabs/abc"
cat /config/crontabs/abc
fi

View file

@ -1,23 +1,22 @@
# Bare-bones config. Here are some useful links to get started:
# Walk-through Guide: https://github.com/jmbannon/ytdl-sub/wiki/1.-Introduction
# Walk-through Guide: https://ytdl-sub.readthedocs.io/en/latest/guides/index.html
# Config Examples: https://github.com/jmbannon/ytdl-sub/tree/master/examples
# Prebuilt Presets: https://ytdl-sub.readthedocs.io/en/latest/presets.html
# Config Docs: https://ytdl-sub.readthedocs.io/en/latest/config.html
# Prebuilt Presets: https://ytdl-sub.readthedocs.io/en/latest/prebuilt_presets/index.html
# Config Reference: https://ytdl-sub.readthedocs.io/en/latest/config_reference/index.html
#
# Examples are included in the /config/examples/ directory.
# Any config and subscription can be ran using:
# ytdl-sub --config /path/to/config.yaml sub /path/to/subscriptions.yaml
# The subscriptions in `subscriptions.yaml` uses prebuilt presets which do not require
# any additions to this config. They can be downloaded using the command:
#
# ytdl-sub --config config.yaml sub subscriptions.yaml
#
# Or dry-ran with:
# ytdl-sub --dry-run --config /path/to/config.yaml sub /path/to/subscriptions.yaml
#
# ytdl-sub --dry-run --config config.yaml sub subscriptions.yaml
#
# See the documentation above on how to build your own custom presets.
#
configuration:
working_directory: '/tmp/ytdl-sub-downloads'
presets:
video:
output_options:
output_directory: "/tmp/ytdl-sub-output"
file_name: "{uid}.{ext}"
thumbnail_name: "{uid}.{thumbnail_ext}"
# Avoid unnecessarily long large file renames, set this to a path on the same
# filesystem as the destination for downloaded files in the `overrides: /
# *_directory:` paths:
working_directory: ".ytdl-sub-working-directory"

15
docker/root/defaults/cron Normal file
View file

@ -0,0 +1,15 @@
# Place your ytdl-sub command(s) here.
#
# This script is executed in the same directory as this file which also contains the
# default `./config.yaml` and `./subscriptions.yaml`, so you don't need to use the
# `--config` CLI option or pass a `SUBPATH` to the `$ ytdl-sub sub` sub-command.
#
# Test your configuration and subscriptions carefully before automating downloads to
# prevent triggering throttles or bans:
#
# https://ytdl-sub.readthedocs.io/en/latest/guides/getting_started/downloading.html
#
# Once you've tested your configuration and you're ready to download entries unattended,
# remove the next line and un-comment the following line:
echo "WARNING: Read /config/ytdl-sub-configs/cron and modify to automate downloads."
# ytdl-sub sub

View file

@ -1,17 +1,88 @@
# Bare-bones config. Here are some useful links to get started:
# Walk-through Guide: https://github.com/jmbannon/ytdl-sub/wiki/1.-Introduction
# Config Examples: https://github.com/jmbannon/ytdl-sub/tree/master/examples
# Prebuilt Presets: https://ytdl-sub.readthedocs.io/en/latest/presets.html
# Config Docs: https://ytdl-sub.readthedocs.io/en/latest/config.html
#
# Examples are included in the /config/examples/ directory.
# Any config and subscription can be ran using:
# ytdl-sub --config /path/to/config.yaml sub /path/to/subscriptions.yaml
#
# Or dry-ran with:
# ytdl-sub --dry-run --config /path/to/config.yaml sub /path/to/subscriptions.yaml
#
rammstein_music_videos:
preset: "video"
download: "https://youtube.com/playlist?list=PLVTLbc6i-h_iuhdwUfuPDLFLXG2QQnz-x"
# subscriptions.yaml:
# Everything in here can be downloaded using the command:
# ytdl-sub sub subscriptions.yaml
# __preset__ is a place to define global overrides for all subscriptions
__preset__:
overrides:
# Root folder of all ytdl-sub TV Shows
tv_show_directory: "/tv_shows"
# Root folder of all ytdl-sub Music
music_directory: "/music"
# Root folder of all ytdl-sub Music Videos
music_video_directory: "/music_videos"
# For 'Only Recent' preset, only keep vids within this range and limit
# only_recent_date_range: "2months"
# only_recent_max_files: 30
# Pass any arg directly to yt-dlp's Python API
# ytdl_options:
# cookiefile: "/config/ytdl-sub-configs/cookie.txt"
###################################################################
# Subscriptions nested under this will use the
# `Plex TV Show by Date` preset.
#
# Can choose between:
# - Plex TV Show by Date:
# - Jellyfin TV Show by Date:
# - Kodi TV Show by Date:
Plex TV Show by Date:
# Sets genre tag to "Documentaries"
= Documentaries:
"NOVA PBS": "https://www.youtube.com/@novapbs"
# "National Geographic": "https://www.youtube.com/@NatGeo"
# "Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
# Sets genre tag to "Kids", "TV-Y" for content rating
# = Kids | = TV-Y:
# "Jake Trains": "https://www.youtube.com/@JakeTrains"
# "Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
# = Music:
# # TV show subscriptions can support multiple urls and store in the same TV Show
# "Rick Beato":
# - "https://www.youtube.com/@RickBeato"
# - "https://www.youtube.com/@rickbeato240"
# Set genre tag to "News", use `Only Recent` preset to only store videos uploaded recently
# = News | Only Recent:
# "BBC News": "https://www.youtube.com/@BBCNews"
###################################################################
# Subscriptions nested under these will use the various prebuilt
# music presets
# YouTube Releases:
# = Jazz: # Sets genre tag to "Jazz"
# "Thelonious Monk": "https://www.youtube.com/@theloniousmonk3870/releases"
# YouTube Full Albums:
# = Lofi:
# "Game Chops": "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i"
# SoundCloud Discography:
# = Chill Hop:
# "UKNOWY": "https://soundcloud.com/uknowymunich"
# = Synthwave:
# "Lazerdiscs Records": "https://soundcloud.com/lazerdiscsrecords"
# "Earmake": "https://soundcloud.com/earmake"
# Bandcamp:
# = Lofi:
# "Emily Hopkins": "https://emilyharpist.bandcamp.com/"
###################################################################
# Can choose between:
# - Plex Music Videos:
# - Jellyfin Music Videos:
# - Kodi Music Videos:
# "Plex Music Videos":
# = Pop: # Sets genre tag to "Pop"
# "Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
# "Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"

View file

@ -1,13 +0,0 @@
#!/usr/bin/with-contenv bash
# copy config
[[ ! -e /config/config.yaml ]] && \
cp /defaults/config.yaml /config/config.yaml
[[ ! -e /config/subscriptions.yaml ]] && \
cp /defaults/subscriptions.yaml /config/subscriptions.yaml
[[ ! -d /config/examples ]] && \
cp -R /defaults/examples /config/
# permissions
chown -R ${PUID:-abc}:${PGID:-abc} \
/config

View file

@ -0,0 +1,15 @@
――――――――――――――――――――――――――――――――――――
██╗ ██╗████████╗██████╗ ██╗
╚██╗ ██╔╝╚══██╔══╝██╔══██╗██║
╚████╔╝ ██║ ██║ ██║██║
╚██╔╝ ██║ ██║ ██║██║
██║ ██║ ██████╔╝███████╗
╚═╝ ╚═╝ ╚═════╝ ╚══════╝
███████╗██╗ ██╗██████╗
██╔════╝██║ ██║██╔══██╗
███████╗██║ ██║██████╔╝
╚════██║██║ ██║██╔══██╗
███████║╚██████╔╝██████╔╝
╚══════╝ ╚═════╝ ╚═════╝
――――――――――――――――――――――――――――――――――――

46
docker/testing/Makefile Normal file
View file

@ -0,0 +1,46 @@
# Local building and testing of the Docker image variants.
# Defensive settings for make:
# https://tech.davis-hansson.com/p/make/
SHELL:=bash
.ONESHELL:
.SHELLFLAGS:=-eu -o pipefail -c
.SILENT:
.DELETE_ON_ERROR:
MAKEFLAGS+=--warn-undefined-variables
MAKEFLAGS+=--no-builtin-rules
export PS1?=$$
# Prefix echoed recipe commands with the recipe line number for debugging:
export PS4?=:$$LINENO+
VARIANTS=headless gui ubuntu
ROOT_PREREQS:=$(shell find ../root -type f)
# Finished with `$(shell)`, echo recipe commands going forward
.SHELLFLAGS+= -x
### Top-level targets:
.PHONY: all
all: build
.PHONY: build
build: $(VARIANTS:%=./build/ytdl-sub-%.log)
.PHONY: run
run: build $(VARIANTS:%=./volumes/ytdl-sub-%/)
docker compose up
### Real targets:
# Re-build the local images when changes require it.
./build/ytdl-sub-%.log: ../Dockerfile.% $(ROOT_PREREQS)
mkdir -pv "$(dir $(@))"
docker compose build "$(@:build/ytdl-sub-%.log=ytdl-sub-%)" |&
tee -a "$(@)"
# Ensure volumes are owned by the developer's normal user:
./volumes/ytdl-sub-%/:
mkdir -pv "$(@)"

View file

@ -0,0 +1,50 @@
services:
ytdl-sub-gui:
build:
context: "../"
dockerfile: "./Dockerfile.gui"
image: "ytdl-sub-gui:local"
container_name: "ytdl-sub-gui"
environment:
PUID: "1000"
PGID: "1000"
TZ: "America/Los_Angeles"
CRON_SCHEDULE: '*/1 * * * *'
CRON_RUN_ON_START: "true"
UPDATE_YT_DLP_ON_START: "stable"
volumes:
- "./volumes/ytdl-sub-gui/:/config/"
ports:
- "8443:8443"
restart: "unless-stopped"
ytdl-sub-headless:
build:
context: "../"
image: "ytdl-sub:local"
container_name: "ytdl-sub-headless"
environment:
PUID: "1000"
PGID: "1000"
TZ: "America/Los_Angeles"
CRON_SCHEDULE: '*/1 * * * *'
CRON_RUN_ON_START: "true"
UPDATE_YT_DLP_ON_START: "stable"
volumes:
- "./volumes/ytdl-sub-headless/:/config/"
restart: "unless-stopped"
ytdl-sub-ubuntu:
build:
context: "../"
dockerfile: "./Dockerfile.ubuntu"
image: "ytdl-sub-ubuntu:local"
container_name: "ytdl-sub-ubuntu"
environment:
PUID: "1000"
PGID: "1000"
TZ: "America/Los_Angeles"
CRON_SCHEDULE: '*/1 * * * *'
CRON_RUN_ON_START: "true"
UPDATE_YT_DLP_ON_START: "stable"
volumes:
- "./volumes/ytdl-sub-ubuntu/:/config/"
restart: "unless-stopped"

View file

@ -5,8 +5,8 @@
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:

View file

@ -1,67 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath("../src"))
# -- Project information -----------------------------------------------------
project = "ytdl-sub"
copyright = "2022, Jesse Bannon"
author = "Jesse Bannon"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_options = {"navigation_depth": 10}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ["_static"]
# Do not show full module path in api docs
add_module_names = False
python_use_unqualified_type_names = False
napoleon_numpy_docstrings = True
napoleon_use_rtype = False
# -- Options for autodocs -------------------------------------------------
autodoc_default_options = {"autodoc_typehints_format": "short"}

View file

@ -1,374 +0,0 @@
Config
======
ytdl-sub is configured using a ``config.yaml`` file.
.. _config_yaml:
config.yaml
-----------
The ``config.yaml`` is made up of two sections:
.. code-block:: yaml
configuration:
presets:
You can jump to any section and subsection of the config using the navigation
section to the left.
Note for Windows users, paths can be represented with ``C:/forward/slashes/like/linux``.
If you wish to represent paths like Windows, you will need to ``C:\\double\\bashslash\\paths``
in order to escape the backslash character.
configuration
^^^^^^^^^^^^^
The ``configuration`` section contains app-wide configs applied to all presets
and subscriptions.
.. autoclass:: ytdl_sub.config.config_validator.ConfigOptions()
:members:
:member-order: bysource
:exclude-members: persist_logs, experimental
persist_logs
""""""""""""
Within ``configuration``, define whether logs from subscription downloads
should be persisted.
.. code-block:: yaml
configuration:
persist_logs:
logs_directory: "/path/to/log/directory"
Log files are stored as
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
.. autoclass:: ytdl_sub.config.config_validator.PersistLogsValidator()
:members:
:member-order: bysource
presets
^^^^^^^
``presets`` define a `formula` for how to format downloaded media and metadata.
download_strategy
"""""""""""""""""
Download strategies dictate what is getting downloaded from a source. Each
download strategy has its own set of parameters.
.. _url:
url
'''
.. autoclass:: ytdl_sub.downloaders.url.url.UrlDownloadOptions()
:members: url, playlist_thumbnails, source_thumbnails, download_reverse
:member-order: bysource
multi_url
'''''''''
.. autoclass:: ytdl_sub.downloaders.url.multi_url.MultiUrlDownloadOptions()
:members: urls, variables
-------------------------------------------------------------------------------
output_options
""""""""""""""
.. autoclass:: ytdl_sub.config.preset_options.OutputOptions()
:members:
:member-order: bysource
:exclude-members: get_upload_date_range_to_keep, partial_validate
-------------------------------------------------------------------------------
.. _ytdl_options:
ytdl_options
""""""""""""
.. autoclass:: ytdl_sub.config.preset_options.YTDLOptions()
-------------------------------------------------------------------------------
.. _overrides:
overrides
"""""""""
.. autoclass:: ytdl_sub.config.preset_options.Overrides()
.. _parent preset:
preset
""""""
Presets support inheritance by defining a parent preset:
.. code-block:: yaml
presets:
custom_preset:
...
parent_preset:
...
child_preset:
preset: "parent_preset"
In the example above, ``child_preset`` inherits all fields defined in ``parent_preset``.
It is advantageous to use parent presets where possible to reduce duplicate yaml
definitions.
Presets also support inheritance from multiple presets:
.. code-block:: yaml
child_preset:
preset:
- "custom_preset"
- "parent_preset"
In this example, ``child_preset`` will inherit all fields from ``custom_preset``
and ``parent_preset`` in that order. The bottom-most preset has the highest
priority.
If you are only inheriting from one preset, the syntax ``preset: "parent_preset"`` is
valid YAML. Inheriting from multiple presets require use of a list.
-------------------------------------------------------------------------------
Plugins
"""""""
Plugins are used to perform any type of post-processing to the already downloaded files.
audio_extract
'''''''''''''
.. autoclass:: ytdl_sub.plugins.audio_extract.AudioExtractOptions()
:members:
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
chapters
''''''''
.. autoclass:: ytdl_sub.plugins.chapters.ChaptersOptions()
:members:
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
date_range
''''''''''
.. autoclass:: ytdl_sub.plugins.date_range.DateRangeOptions()
:members:
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
embed_thumbnail
''''''''''''''''
.. autoclass:: ytdl_sub.plugins.embed_thumbnail.EmbedThumbnailOptions()
-------------------------------------------------------------------------------
file_convert
''''''''''''
.. autoclass:: ytdl_sub.plugins.file_convert.FileConvertOptions()
:members:
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
match_filters
'''''''''''''
.. autoclass:: ytdl_sub.plugins.match_filters.MatchFiltersOptions()
:members:
:exclude-members: partial_validate
-------------------------------------------------------------------------------
music_tags
''''''''''
.. autoclass:: ytdl_sub.plugins.music_tags.MusicTagsOptions()
-------------------------------------------------------------------------------
nfo_tags
''''''''
.. autoclass:: ytdl_sub.plugins.nfo_tags.NfoTagsOptions()
:members: nfo_name, nfo_root, tags, kodi_safe
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
output_directory_nfo_tags
'''''''''''''''''''''''''
.. autoclass:: ytdl_sub.plugins.output_directory_nfo_tags.OutputDirectoryNfoTagsOptions()
:members: nfo_name, nfo_root, tags, kodi_safe
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
regex
'''''
.. autoclass:: ytdl_sub.plugins.regex.RegexOptions()
:members: skip_if_match_fails
.. autoclass:: ytdl_sub.plugins.regex.VariableRegex()
:members: match, capture_group_names, capture_group_defaults, exclude
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
split_by_chapters
'''''''''''''''''
.. autoclass:: ytdl_sub.plugins.split_by_chapters.SplitByChaptersOptions()
:members: when_no_chapters
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
subtitles
'''''''''
.. autoclass:: ytdl_sub.plugins.subtitles.SubtitleOptions()
:members: subtitles_name, subtitles_type, embed_subtitles, languages, allow_auto_generated_subtitles
:member-order: bysource
:exclude-members: partial_validate
-------------------------------------------------------------------------------
video_tags
''''''''''
.. autoclass:: ytdl_sub.plugins.video_tags.VideoTagsOptions()
-------------------------------------------------------------------------------
.. _subscription_yaml:
subscription.yaml
-----------------
The ``subscription.yaml`` file is where we use our `presets`_ in the `config.yaml`_
to define a `subscription`: something we want to recurrently download such as a specific
channel or playlist.
The only difference between a ``subscription`` and ``preset`` is that the subscription
must have all required fields and ``{variables}`` defined so it can perform a download.
Below is an example that downloads a YouTube playlist:
.. code-block:: yaml
:caption: config.yaml
presets:
playlist_preset_ex:
download:
download_strategy: "url"
url: "{url}"
output_options:
output_directory: "{output_directory}/{playlist_name}"
file_name: "{playlist_name}.{title}.{ext}"
overrides:
output_directory: "/path/to/ytdl-sub-videos"
.. code-block:: yaml
:caption: subscription.yaml
my_subscription_name:
preset: "playlist_preset_ex"
overrides:
playlist_name: "diy-playlist"
url: "https://youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
Our preset ``playlist_preset_ex`` defines three
custom variables: ``{output_directory}``, ``{playlist_name}``, and ``{url}``. The subscription sets
the `parent preset`_ to ``playlist_preset_ex``, and must define the variables ``{playlist_name}``
and ``{url}`` since the preset did not.
File Preset
^^^^^^^^^^^
You can apply a preset to all subscriptions in the ``subscription.yaml`` file
by using the file-wide ``__preset__``:
.. code-block:: yaml
:caption: subscription.yaml
__preset__:
preset: "playlist_preset_ex"
my_subscription_name:
overrides:
url: "https://youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
playlist_name: "diy-playlist"
This ``subscription.yaml`` is equivalent to the one above it because all
subscriptions automatically set ``__preset__`` as a `parent preset`_.
File Subscription Value
^^^^^^^^^^^^^^^^^^^^^^^
With a clever config and use of ``__preset__``, your subscriptions can typically boil
down to a name and url. You can set ``__value__`` to the name of an override variable,
and use the override variable ``subscription_name`` to achieve one-liner subscriptions.
Using the example above, we can do:
.. code-block:: yaml
:caption: subscription.yaml
__preset__:
preset: "playlist_preset_ex"
overrides:
playlist_name: "{subscription_name}"
__value__: "url"
# single-line subscription
"diy-playlist": "https://youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
``"diy-playlist"`` gets assigned to the ``playlist_name`` override variable by setting
it with ``subscription_name`` , and the url gets assigned to ``url`` by setting ``__value__``
to write values to it.
Traditional subscriptions that can override presets will still work when using ``__value__``.
-------------------------------------------------------------------------------
.. _source-variables:
Source Variables
----------------
.. autoclass:: ytdl_sub.entries.variables.entry_variables.EntryVariables
:members:
:inherited-members:
:undoc-members:
Override Variables
------------------
.. autoclass:: ytdl_sub.config.preset_options.OverridesVariables()
:members:
-------------------------------------------------------------------------------
Config Types
------------
The `config.yaml`_ uses various types for its configurable fields. Below is a definition for each type.
.. autoclass:: ytdl_sub.validators.string_formatter_validators.StringFormatterValidator()
.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesStringFormatterValidator()
.. autoclass:: ytdl_sub.validators.file_path_validators.StringFormatterFileNameValidator()
.. autoclass:: ytdl_sub.validators.string_datetime.StringDatetimeValidator()
.. autoclass:: ytdl_sub.validators.string_formatter_validators.DictFormatterValidator()
.. autoclass:: ytdl_sub.validators.string_formatter_validators.OverridesDictFormatterValidator()

View file

@ -1,51 +0,0 @@
Deprecation Notices
===================
July 2023
---------
music_tags
^^^^^^^^^^
Music tags are getting simplified. ``tags`` will now reside directly under music_tags, and
``embed_thumbnail`` is getting moved to its own plugin (supports video files as well). Convert from:
.. code-block:: yaml
my_example_preset:
music_tags:
embed_thumbnail: True
tags:
artist: "Elvis Presley"
To the following:
.. code-block:: yaml
my_example_preset:
embed_thumbnail: True
music_tags:
artist: "Elvis Presley"
The old format will be removed in October 2023.
video_tags
^^^^^^^^^^
Video tags are getting simplified as well. ``tags`` will now reside directly under video_tags.
Convert from:
.. code-block:: yaml
my_example_preset:
video_tags:
tags:
title: "Elvis Presley Documentary"
To the following:
.. code-block:: yaml
my_example_preset:
video_tags:
title: "Elvis Presley Documentary"

View file

@ -1,29 +0,0 @@
Getting Started
===============
Walk-through Guide
-------------------
If you haven't read it yet, it's highly recommended to go through our
`walk-through guide <https://github.com/jmbannon/ytdl-sub/wiki/1.-Introduction>`_
to get familiar with how ``ytdl-sub`` works.
Example Configs
---------------
If you are ready to start downloading, see our
`examples directory <https://github.com/jmbannon/ytdl-sub/tree/master/examples>`_
for ready-to-use configs and subscriptions. Read through them carefully before use.
Using Example Configs
^^^^^^^^^^^^^^^^^^^^^^
Copy and paste the examples into local yaml files, modify the
``working_directory`` and ``output_directory`` with your desired paths,
and perform a dry-run using
.. code-block:: bash
ytdl-sub \
--dry-run \
--config path/to/config.yaml \
sub path/to/subscriptions.yaml
This will simulate what a download will look like.

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,29 +0,0 @@
ytdl-sub readthedocs
====================
Our readthedocs page is dedicated towards ytdl-sub config documentation.
If you are new to ytdl-sub, head over to the
`GitHub Wiki <https://github.com/jmbannon/ytdl-sub/wiki>`_
to see our
`walkthrough <https://github.com/jmbannon/ytdl-sub/wiki/1.-Introduction>`_ and
`FAQ <https://github.com/jmbannon/ytdl-sub/wiki/FAQ>`_. For full examples of
ytdl-sub configs, check out the
`examples directory <https://github.com/jmbannon/ytdl-sub/tree/master/examples>`_.
For navigating config docs, use the left-side bar on the
:ref:`config_yaml` page to find every available ytdl-sub field.
Contents
========
.. toctree::
:maxdepth: 2
install
usage
getting_started
presets
config
deprecation_notices

View file

@ -1,209 +0,0 @@
Install
=======
``ytdl-sub`` can be installed on the following platforms.
.. contents::
:depth: 2
All installations require a 64-bit CPU. 32-bit is not supported.
Docker Compose
--------------
The ytdl-sub docker image uses
`Linux Server's <https://www.linuxserver.io/>`_
`base alpine image <https://github.com/linuxserver/docker-baseimage-alpine/>`_
It looks, feels, and operates like other LinuxServer images. This is the
recommended way to use ytdl-sub.
The docker image is intended to be used as a console. For automating
``subscriptions.yaml`` downloads to pull new media, see
`this guide <https://github.com/jmbannon/ytdl-sub/wiki/7.-Automate-Downloading-New-Content-Using-Your-Configs/>`_
on how set up a cron job in the docker container.
.. code-block:: yaml
services:
ytdl-sub:
image: ghcr.io/jmbannon/ytdl-sub:latest
container_name: ytdl-sub
environment:
- PUID=1000
- PGID=1000
- TZ=America/Los_Angeles
- DOCKER_MODS=linuxserver/mods:universal-cron
volumes:
- <path/to/ytdl-sub/config>:/config
- <path/to/tv_shows>:/tv_shows # optional
- <path/to/movies>:/movies # optional
- <path/to/music_videos>:/music_videos # optional
- <path/to/music>:/music # optional
restart: unless-stopped
CPU Passthrough
^^^^^^^^^^^^^^^^^^^^^^
For CPU passthrough, you must use the ``ytdl-sub`` Ubuntu version with the following additions:
.. code-block:: yaml
services:
ytdl-sub:
image: ghcr.io/jmbannon/ytdl-sub:ubuntu-latest
container_name: ytdl-sub
environment:
- PUID=1000
- PGID=1000
- TZ=America/Los_Angeles
- DOCKER_MODS=linuxserver/mods:universal-cron
volumes:
- <path/to/ytdl-sub/config>:/config
- <path/to/tv_shows>:/tv_shows # optional
- <path/to/movies>:/movies # optional
- <path/to/music_videos>:/music_videos # optional
- <path/to/music>:/music # optional
devices:
- /dev/dri:/dev/dri # CPU passthrough
restart: unless-stopped
Nvidia GPU Passthrough
^^^^^^^^^^^^^^^^^^^^^^
For GPU passthrough, you must use the ``ytdl-sub`` Ubuntu version with the following additions:
.. code-block:: yaml
services:
ytdl-sub:
image: ghcr.io/jmbannon/ytdl-sub:ubuntu-latest
container_name: ytdl-sub
environment:
- PUID=1000
- PGID=1000
- TZ=America/Los_Angeles
- DOCKER_MODS=linuxserver/mods:universal-cron
- NVIDIA_DRIVER_CAPABILITIES=all # Nvidia ENV args
- NVIDIA_VISIBLE_DEVICES=all
volumes:
- <path/to/ytdl-sub/config>:/config
- <path/to/tv_shows>:/tv_shows # optional
- <path/to/movies>:/movies # optional
- <path/to/music_videos>:/music_videos # optional
- <path/to/music>:/music # optional
deploy:
resources:
reservations:
devices:
- capabilities: [gpu] # GPU passthrough
restart: unless-stopped
Docker
--------------
.. code-block:: bash
docker run -d \
--name=ytdl-sub \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=America/Los_Angeles \
-e DOCKER_MODS=linuxserver/mods:universal-cron \
-v <path/to/ytdl-sub/config>:/config \
-v <OPTIONAL/path/to/tv_shows>:/tv_shows \
-v <OPTIONAL/path/to/movies>:/movies \
-v <OPTIONAL/path/to/music_videos>:/music_videos \
-v <OPTIONAL/path/to/music>:/music \
--restart unless-stopped \
ghcr.io/jmbannon/ytdl-sub:latest
Windows
--------------
From powershell, run:
.. code-block:: powershell
# Download ffmpeg/ffprobe dependencies from yt-dlp
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
tar -xf ffmpeg.zip
move "ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe" "ffmpeg.exe"
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
# Download ytdl-sub
curl.exe -L -o ytdl-sub.exe https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub.exe
ytdl-sub.exe -h
Unraid
--------------
See the
`community app <https://unraid.net/community/apps?q=ytdl-sub#r>`_
``ytdl-sub``. Uses Docker under the hood.
Linux
--------------
Requires ffmpeg as a dependency. Can typically be installed with any Linux package manager.
.. code-block:: bash
curl -L -o ytdl-sub https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub
chmod +x ytdl-sub
ytdl-sub -h
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to date:
.. code-block:: bash
curl -L -o ffmpeg.tar.gz https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz
tar -xf ffmpeg.tar.gz
chmod +x ffmpeg-master-latest-linux64-gpl/bin/ffmpeg
chmod +x ffmpeg-master-latest-linux64-gpl/bin/ffprobe
# May need sudo / root permissions to perform
mv ffmpeg-master-latest-linux64-gpl/bin/ffmpeg /usr/bin/ffmpeg
mv ffmpeg-master-latest-linux64-gpl/bin/ffprobe /usr/bin/ffprobe
Linux ARM
--------------
Requires ffmpeg as a dependency. Can typically be installed with any Linux package manager.
.. code-block:: bash
curl -L -o ytdl-sub https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub_aarch64
chmod +x ytdl-sub
ytdl-sub -h
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to date:
.. code-block:: bash
curl -L -o ffmpeg.tar.gz https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz
tar -xf ffmpeg.tar.gz
chmod +x ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg
chmod +x ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe
# May need sudo / root permissions to perform
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg /usr/bin/ffmpeg
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe /usr/bin/ffprobe
PIP
--------------
You can install our
`PyPI package <https://pypi.org/project/ytdl-sub/>`_.
Both ffmpeg and Python 3.10 or greater are required.
.. code-block:: bash
python3 -m pip install -U ytdl-sub
Local Install
--------------
With a Python 3.10 virtual environment, you can clone and install the repo using
.. code-block:: bash
git clone https://github.com/jmbannon/ytdl-sub.git
cd ytdl-sub
pip install -e .
Local Docker Build
-------------------
Run ``make docker`` in the root directory of this repo to build the image. This
will build the python wheel and install it in the Dockerfile.

View file

@ -7,8 +7,8 @@ REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (

View file

@ -1,183 +0,0 @@
Presets
=======
``ytdl-sub`` offers a number of built-in presets using best practices for formatting
media in various players. For advanced users, you can find the prebuilt preset
definitions
`here <https://github.com/jmbannon/ytdl-sub/tree/master/src/ytdl_sub/prebuilt_presets>`_.
TV Shows
--------
There are two main methods for downloading and formatting videos as a TV show.
TV Show by Date
^^^^^^^^^^^^^^^
TV Show by Date will organize something like a YouTube channel or playlist
into a tv show, where seasons and episodes are organized using upload date.
Player Presets
""""""""""""""
* ``kodi_tv_show_by_date``
* ``jellyfin_tv_show_by_date``
* ``plex_tv_show_by_date``
Episode Formatting Presets
""""""""""""""""""""""""""
* ``season_by_year__episode_by_month_day``
* ``season_by_year_month__episode_by_day``
* ``season_by_year__episode_by_month_day_reversed``
* Episode numbers are reversed, meaning more recent episodes appear at the
top of a season by having a lower value.
* ``season_by_year__episode_by_download_index``
* Episodes are numbered by the download order. NOTE that this fetched using
the length of the download archive. Do not use if you intend to remove
old videos.
Usage
"""""
A preset/subscription requires specifying a player and episode formatting preset
and overriding the following variables:
.. code-block:: yaml
rick_a_tv_show_by_date:
preset:
- "jellyfin_tv_show_by_date"
- "season_by_year__episode_by_month_day"
overrides:
# required
tv_show_name: "Rick A"
tv_show_directory: "/path/to/youtube_shows"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
# can be modified from their default value
# tv_show_genre: "ytdl-sub"
# tv_show_content_rating: "TV-14"
# episode_title: "{upload_date_standardized} - {title}"
# episode_description: "{webpage_url}"
In addition, you can add additional URLs to create a single TV by using the override variables
``url2``, ``url3``, ..., ``url20``:
.. code-block:: yaml
overrides:
tv_show_name: "Rick A"
tv_show_directory: "/path/to/youtube_shows"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
url2: "https://www.youtube.com/@just.rick_6"
TV Show Collection
^^^^^^^^^^^^^^^^^^
TV Show Collections are made up from multiple URLs, where each URL is a season.
If a video belongs to multiple URLs (i.e. a channel and a channel's playlist),
it will resolve to the bottom-most season.
Two main use cases of a collection are:
1. Organize a YouTube channel TV show where Season 1 contains any video
not in a 'season playlist', Season 2 for 'Playlist A', Season 3 for
'Playlist B', etc.
2. Organize one or more YouTube channels/playlists, where each season
represents a separate channel/playlist.
Player Presets
""""""""""""""
* ``kodi_tv_show_collection``
* ``jellyfin_tv_show_collection``
* ``plex_tv_show_collection``
Episode Formatting Presets
""""""""""""""""""""""""""
* ``season_by_collection__episode_by_year_month_day``
* ``season_by_collection__episode_by_year_month_day_reversed``
* ``season_by_collection__episode_by_playlist_index``
* Only use playlist_index episode formatting for playlists that
will be fully downloaded once and never again. Otherwise,
indices can change.
* ``season_by_collection__episode_by_playlist_index_reversed``
Season Presets
""""""""""""""
* ``collection_season_1``
* ``collection_season_2``
* ``collection_season_3``
* ``collection_season_4``
* ``...``
* ``collection_season_40``
Example
"""""""
A preset/subscription requires specifying a player, episode formatting, and
one or more season presets, with the following override variables:
.. code-block:: yaml
rick_a_tv_show_collection:
preset:
- "jellyfin_tv_show_collection"
- "season_by_collection__episode_by_year_month_day_reversed"
- "collection_season_1"
- "collection_season_2"
overrides:
# required
tv_show_name: "Rick A"
tv_show_directory: "/path/to/youtube_shows"
collection_season_1_url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
collection_season_1_name: "All Videos"
collection_season_2_url: "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
collection_season_2_name: "Official Music Videos"
# can be modified from their default value
# tv_show_genre: "ytdl-sub"
# episode_title: "{upload_date_standardized} - {title}"
# episode_description: "{webpage_url}"
Common
------
Common presets are applicable to any config.
Best Video Quality
^^^^^^^^^^^^^^^^^^
Add the following preset to download the best available video and audio quality, and remux
it into an MP4 container:
* ``best_video_quality``
Max 1080p
^^^^^^^^^^^^^^^^^^
Add the following preset to download the best available audio and video quality, with the video not greater than 1080p, and remux it into an MP4 container:
* ``max_1080p``
Chunk Initial Download
^^^^^^^^^^^^^^^^^^^^^^
If you are archiving a large channel, ``ytdl-sub`` will try pulling each video's metadata from
newest to oldest before starting any downloads. It is a long process and not ideal. A better method
is to chunk the process by using the following preset:
* ``chunk_initial_download``
It will download videos starting from the oldest one, and only download 20 at a time. You can
change this number by setting:
.. code-block:: yaml
ytdl_options:
max_downloads: 30 # Desired number to download per invocation
Once the entire channel is downloaded, remove this preset. Then it will pull metadata from newest to
oldest again, and stop pulling additional metadata once it reaches a video that has already been
downloaded.

70
docs/source/conf.py Normal file
View file

@ -0,0 +1,70 @@
# Configuration file for the Sphinx documentation builder.
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "ytdl-sub"
copyright = "2026, Jesse Bannon"
author = "Jesse Bannon"
release = ""
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autosectionlabel",
"sphinx.ext.extlinks",
"sphinx_copybutton",
"sphinx_design",
]
templates_path = ["_templates"]
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_book_theme"
html_theme_options = {
"icon_links": [
{
"name": "GitHub",
"url": "https://github.com/jmbannon/ytdl-sub",
"icon": "fa-brands fa-square-github",
"type": "fontawesome",
},
{
"name": "Discord",
"url": "https://discord.gg/v8j9RAHb4k",
"icon": "https://img.shields.io/discord/994270357957648404?logo=Discord",
"type": "url",
},
],
"announcement": "",
"navigation_depth": 10,
"show_toc_level": 10,
}
html_static_path = ["_static"]
html_css_files = ["custom.css"]
# Make sure the all autosectionlabel targets are unique
autosectionlabel_prefix_document = True
suppress_warnings = [
"autosectionlabel.*",
]
extlinks = {
"yt-dlp": ("https://github.com/yt-dlp/yt-dlp/%s", "yt-dlp%s"),
"unraid": ("https://unraid.net/%s", "unraid%s"),
"lsio": ("https://www.linuxserver.io/%s", "lsio%s"),
"lsio-gh": ("https://github.com/linuxserver/%s", "%s image"),
"ytdl-sub-gh": ("https://github.com/jmbannon/ytdl-sub/%s", "src %s"),
}

View file

@ -0,0 +1,134 @@
..
WARNING: This RST file is generated from docstrings in:
The respective function docstrings within ytdl_sub/config/config_validator.py
In order to make a change to this file, edit the respective docstring
and run `make docs`. This will automatically sync the Python RST-based
docstrings into this file. If the docstrings and RST file are out of sync,
it will fail TestDocGen tests in GitHub CI.
Configuration File
==================
ytdl-sub is configured using a ``config.yaml`` file.
The ``config.yaml`` is made up of two sections:
.. code-block:: yaml
configuration:
presets:
Note for Windows users, paths can be represented with ``C:/forward/slashes/like/linux``.
If you prefer to use a Windows backslash, note that it must have
``C:\\double\\bashslash\\paths`` in order to escape the backslash character. This is due
to it being a YAML escape character.
.. code-block:: yaml
configuration:
dl_aliases:
mv: "--preset music_video"
u: "--download.url"
experimental:
enable_update_with_info_json: True
ffmpeg_path: "/usr/bin/ffmpeg"
ffprobe_path: "/usr/bin/ffprobe"
file_name_max_bytes: 255
lock_directory: "/tmp"
persist_logs:
keep_successful_logs: True
logs_directory: "/var/log/ytdl-sub-logs"
umask: "022"
working_directory: ".ytdl-sub-working-directory"
dl_aliases
----------
.. _dl_aliases:
Alias definitions to shorten :ref:`dl arguments <usage:Download Options>`. For example,
.. code-block:: yaml
configuration:
dl_aliases:
mv: "--preset music_video"
u: "--download.url"
Simplifies
.. code-block:: bash
ytdl-sub dl --preset "Jellyfin Music Videos" --download.url "youtube.com/watch?v=a1b2c3"
to
.. code-block:: bash
ytdl-sub dl --mv --u "youtube.com/watch?v=a1b2c3"
experimental
------------
Experimental flags reside under the ``experimental`` key.
``enable_update_with_info_json``
Enables modifying subscription files using info.json files using the argument
``--update-with-info-json``. This feature is still being tested and has the ability to
destroy files. Ensure you have a full backup before usage. You have been warned!
ffmpeg_path
-----------
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux,
``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows.
ffprobe_path
------------
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux,
``./ffprobe.exe`` in the same directory as ytdl-sub for Windows.
file_name_max_bytes
-------------------
Max file name size in bytes. Most OS's typically default to 255 bytes.
lock_directory
--------------
The directory to temporarily store file locks, which prevents multiple instances
of ``ytdl-sub`` from running. Note that file locks do not work on
network-mounted directories. Ensure that this directory resides on the host
machine. Defaults to ``/tmp``.
persist_logs
------------
By default, no logs are persisted. Specifying this key will enable persisted logs. The following
options are available.
``keep_successful_logs``
Defaults to ``True``. When this key is ``False``, only write log files for failed
subscriptions.
``logs_directory``
Required field. Write log files to this directory with names like
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
umask
-----
Umask in octal format to apply to every created file. Defaults to ``022``.
working_directory
-----------------
The directory to temporarily store downloaded files before moving them into their final
directory. Defaults to ``.ytdl-sub-working-directory``, created in the same directory
that ytdl-sub is invoked from.
Presets
=======
Custom presets are defined in this section. Refer to the
:ref:`Getting Started Guide<guides/getting_started/first_config:Basic Configuration>`
on how to configure.

View file

@ -0,0 +1,51 @@
=========
Reference
=========
This section contains direct references to the code of ``ytdl-sub`` and information on
how it functions.
Terminology
-----------
Must-know terminology:
- ``subscription``: URL(s) that you want to download with specific metadata
requirements.
- ``preset``: A media profile comprised of YAML configuration that can specify anything
from metadata layout, media quality, or any feature of ytdl-sub, to apply to
subscriptions. A preset can inherit other presets.
- ``prebuilt preset``: Presets that are included in ytdl-sub. These do most of the work
defining plugins, overrides, etc in order to make downloads ready for player
consumption.
- ``override``: Verb describing the act of overriding something in a preset. For
example, the TV Show presets practically expect you to *override* the URL variable to
tell ytdl-sub where to download from.
- ``override variables``: User-defined variables that are intended to *override*
something.
- ``subscription file``: The file to specify all of your subscriptions and some override
variables.
Intermediate terminology:
- ``plugin``: Modular logic to apply to a subscription. To use a plugin, it must be
defined in a preset.
- ``config file``: An optional file where you can define custom presets and other
advanced configuration.
- ``yt-dlp``: The underlying application that handles downloading for ytdl-sub.
Advanced terminology:
- ``entry variables``: Variables that derive from a downloaded yt-dlp entry (media).
- ``static variables``: Variables that do not have a dependency to entry variables.
- ``scripting``: Syntax that allows the use of entry variables, static variables, and
functions in override variables.
.. toctree::
config_yaml
subscription_yaml
plugins
scripting/index
prebuilt_presets/index

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
=======================
Common
=======================
.. highlight:: yaml
Filter Keywords
---------------
.. literalinclude::
/../../src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml
Filter Duration
---------------
.. literalinclude::
/../../src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml
Media Quality
-------------
.. literalinclude::
/../../src/ytdl_sub/prebuilt_presets/helpers/media_quality.yaml
Only Recent Videos
------------------
.. literalinclude::
/../../src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml

View file

@ -0,0 +1,11 @@
=========================
Prebuilt Preset Reference
=========================
This section contains the code for the prebuilt presets. If you just want to understand
how to use the presets, check :doc:`this section instead</prebuilt_presets/index>`.
.. toctree::
common
tv_show
music

View file

@ -0,0 +1,10 @@
=====
Music
=====
All audio music based presets inherit from ``_music_base``.
.. highlight:: yaml
.. literalinclude::
/../../src/ytdl_sub/prebuilt_presets/music/singles.yaml

View file

@ -0,0 +1,10 @@
========================
TV Show
========================
All TV show based presets inherit from ``_episode_base``.
.. highlight:: yaml
.. literalinclude::
/../../src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml

View file

@ -0,0 +1,688 @@
..
WARNING: This RST file is generated from docstrings in:
src/ytdl_sub/entries/script/variable_definitions.py
In order to make a change to this file, edit the respective docstring
and run `make docs`. This will automatically sync the Python RST-based
docstrings into this file. If the docstrings and RST file are out of sync,
it will fail TestDocGen tests in GitHub CI.
Entry Variables
===============
Entry Variables
---------------
channel
~~~~~~~
:type: ``String``
:description:
The channel name if it exists, otherwise returns the uploader.
channel_id
~~~~~~~~~~
:type: ``String``
:description:
The channel id if it exists, otherwise returns the entry uploader ID.
chapters
~~~~~~~~
:type: ``Array``
:description:
Chapters if they exist
comments
~~~~~~~~
:type: ``Array``
:description:
Comments if they are requested
creator
~~~~~~~
:type: ``String``
:description:
The creator name if it exists, otherwise returns the channel.
description
~~~~~~~~~~~
:type: ``String``
:description:
The description if it exists. Otherwise, returns an emtpy string.
duration
~~~~~~~~
:type: ``Integer``
:description:
The duration of the entry in seconds if it exists. Defaults to zero otherwise.
epoch
~~~~~
:type: ``Integer``
:description:
The unix epoch of when the metadata was scraped by yt-dlp.
epoch_date
~~~~~~~~~~
:type: ``String``
:description:
The epoch's date, in YYYYMMDD format.
epoch_hour
~~~~~~~~~~
:type: ``String``
:description:
The epoch's hour
ext
~~~
:type: ``String``
:description:
The downloaded entry's file extension
extractor
~~~~~~~~~
:type: ``String``
:description:
The yt-dlp extractor name
extractor_key
~~~~~~~~~~~~~
:type: ``String``
:description:
The yt-dlp extractor key
height
~~~~~~
:type: ``Integer``
:description:
Height in pixels of the video. If this value is unavailable (i.e. audio download), it
will default to 0.
ie_key
~~~~~~
:type: ``String``
:description:
The ie_key, used in legacy yt-dlp things as the 'info-extractor key'.
If it does not exist, return ``extractor_key``
info_json_ext
~~~~~~~~~~~~~
:type: ``String``
:description:
The "info.json" extension
requested_subtitles
~~~~~~~~~~~~~~~~~~~
:type: ``Map``
:description:
Subtitles if they are requested and exist
sponsorblock_chapters
~~~~~~~~~~~~~~~~~~~~~
:type: ``Array``
:description:
Sponsorblock Chapters if they are requested and exist
thumbnail_ext
~~~~~~~~~~~~~
:type: ``String``
:description:
The download entry's thumbnail extension. Will always return 'jpg'. Until there is a
need to support other image types, we always convert to jpg.
title
~~~~~
:type: ``String``
:description:
The title of the entry. If a title does not exist, returns its unique ID.
title_sanitized_plex
~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The sanitized title with additional sanitizing for Plex. It replaces numbers with
fixed-width numbers so Plex does not recognize them as season or episode numbers.
uid
~~~
:type: ``String``
:description:
The entry's unique ID
uid_sanitized_plex
~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The sanitized uid with additional sanitizing for Plex. Replaces numbers with
fixed-width numbers so Plex does not recognize them as season or episode numbers.
uploader
~~~~~~~~
:type: ``String``
:description:
The uploader if it exists, otherwise return the uploader ID.
uploader_id
~~~~~~~~~~~
:type: ``String``
:description:
The uploader id if it exists, otherwise return the unique ID.
uploader_url
~~~~~~~~~~~~
:type: ``String``
:description:
The uploader url if it exists, otherwise returns the webpage_url.
webpage_url
~~~~~~~~~~~
:type: ``String``
:description:
The url to the webpage.
width
~~~~~
:type: ``Integer``
:description:
Width in pixels of the video. If this value is unavailable (i.e. audio download), it
will default to 0.
----------------------------------------------------------------------------------------------------
Metadata Variables
------------------
entry_metadata
~~~~~~~~~~~~~~
:type: ``Map``
:description:
The entry's info.json
playlist_metadata
~~~~~~~~~~~~~~~~~
:type: ``Map``
:description:
Metadata from the playlist (i.e. the parent metadata, like playlist -> entry)
sibling_metadata
~~~~~~~~~~~~~~~~
:type: ``Array``
:description:
Metadata from any sibling entries that reside in the same playlist as this entry.
source_metadata
~~~~~~~~~~~~~~~
:type: ``Map``
:description:
Metadata from the source
(i.e. the grandparent metadata, like channel -> playlist -> entry)
----------------------------------------------------------------------------------------------------
Playlist Variables
------------------
playlist_count
~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Playlist count if it exists, otherwise returns ``1``.
Note that for channels/playlists, any change (i.e. adding or removing a video) will make
this value change. Use with caution.
playlist_description
~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist description if it exists, otherwise returns the entry's description.
playlist_index
~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Playlist index if it exists, otherwise returns ``1``.
Note that for channels/playlists, any change (i.e. adding or removing a video) will make
this value change. Use with caution.
playlist_index_padded
~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index padded two digits
playlist_index_padded6
~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index padded six digits.
playlist_index_reversed
~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Playlist index reversed via ``playlist_count - playlist_index + 1``
playlist_index_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index_reversed padded two digits
playlist_index_reversed_padded6
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
playlist_index_reversed padded six digits.
playlist_max_upload_date
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
Max upload_date for all entries in this entry's playlist if it exists, otherwise returns
``upload_date``
playlist_max_upload_year
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
Max upload_year for all entries in this entry's playlist if it exists, otherwise returns
``upload_year``
playlist_max_upload_year_truncated
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The max playlist truncated upload year for all entries in this entry's playlist if it
exists, otherwise returns ``upload_year_truncated``.
playlist_title
~~~~~~~~~~~~~~
:type: ``String``
:description:
Name of its parent playlist/channel if it exists, otherwise returns its title.
playlist_uid
~~~~~~~~~~~~
:type: ``String``
:description:
The playlist unique ID if it exists, otherwise return the entry unique ID.
playlist_uploader
~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist uploader if it exists, otherwise return the entry uploader.
playlist_uploader_id
~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist uploader id if it exists, otherwise returns the entry uploader ID.
playlist_uploader_url
~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist uploader url if it exists, otherwise returns the playlist webpage_url.
playlist_webpage_url
~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The playlist webpage url if it exists. Otherwise, returns the entry webpage url.
----------------------------------------------------------------------------------------------------
Release Date Variables
----------------------
release_date
~~~~~~~~~~~~
:type: ``String``
:description:
The entrys release date, in YYYYMMDD format. If not present, return the upload date.
release_date_standardized
~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The uploaded date formatted as YYYY-MM-DD
release_day
~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day as an integer (no padding).
release_day_of_year
~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The day of the year, i.e. February 1st returns ``32``
release_day_of_year_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload day of year, but padded i.e. February 1st returns "032"
release_day_of_year_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_year} + 1 - {release_day}``,
i.e. February 2nd would have release_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
release_day_of_year_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload day of year, but padded i.e. December 31st returns "001"
release_day_padded
~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The entry's upload day padded to two digits, i.e. the fifth returns "05"
release_day_reversed
~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_month} + 1 - {release_day}``,
i.e. August 8th would have release_day_reversed of ``31 + 1 - 8`` = ``24``
release_day_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload day, but padded. i.e. August 30th returns "02".
release_month
~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload month as an integer (no padding).
release_month_padded
~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The entry's upload month padded to two digits, i.e. March returns "03"
release_month_reversed
~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload month, but reversed using ``13 - {release_month}``, i.e. March returns ``10``
release_month_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload month, but padded. i.e. November returns "02"
release_year
~~~~~~~~~~~~
:type: ``Integer``
:description:
The entry's upload year
release_year_truncated
~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The last two digits of the upload year, i.e. 22 in 2022
release_year_truncated_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload year truncated, but reversed using ``100 - {release_year_truncated}``, i.e.
2022 returns ``100 - 22`` = ``78``
----------------------------------------------------------------------------------------------------
Source Variables
----------------
source_count
~~~~~~~~~~~~
:type: ``Integer``
:description:
The source count if it exists, otherwise returns ``1``.
source_description
~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source description if it exists, otherwise returns the playlist description.
source_index
~~~~~~~~~~~~
:type: ``Integer``
:description:
Source index if it exists, otherwise returns ``1``.
It is recommended to not use this unless you know the source will never add new content
(it is easy for this value to change).
source_index_padded
~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source index, padded two digits.
source_title
~~~~~~~~~~~~
:type: ``String``
:description:
Name of the source (i.e. channel with multiple playlists) if it exists, otherwise
returns its playlist_title.
source_uid
~~~~~~~~~~
:type: ``String``
:description:
The source unique id if it exists, otherwise returns the playlist unique ID.
source_uploader
~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source uploader if it exists, otherwise return the playlist_uploader
source_uploader_id
~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source uploader id if it exists, otherwise returns the playlist_uploader_id
source_uploader_url
~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source uploader url if it exists, otherwise returns the source webpage_url.
source_webpage_url
~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The source webpage url if it exists, otherwise returns the playlist webpage url.
----------------------------------------------------------------------------------------------------
Upload Date Variables
---------------------
upload_date
~~~~~~~~~~~
:type: ``String``
:description:
The entrys uploaded date, in YYYYMMDD format. If not present, return todays date.
upload_date_standardized
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The uploaded date formatted as YYYY-MM-DD
upload_day
~~~~~~~~~~
:type: ``Integer``
:description:
The upload day as an integer (no padding).
upload_day_of_year
~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The day of the year, i.e. February 1st returns ``32``
upload_day_of_year_padded
~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload day of year, but padded i.e. February 1st returns "032"
upload_day_of_year_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_year} + 1 - {upload_day}``,
i.e. February 2nd would have upload_day_of_year_reversed of ``365 + 1 - 32`` = ``334``
upload_day_of_year_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload day of year, but padded i.e. December 31st returns "001"
upload_day_padded
~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The entry's upload day padded to two digits, i.e. the fifth returns "05"
upload_day_reversed
~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload day, but reversed using ``{total_days_in_month} + 1 - {upload_day}``,
i.e. August 8th would have upload_day_reversed of ``31 + 1 - 8`` = ``24``
upload_day_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload day, but padded. i.e. August 30th returns "02".
upload_month
~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload month as an integer (no padding).
upload_month_padded
~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The entry's upload month padded to two digits, i.e. March returns "03"
upload_month_reversed
~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload month, but reversed using ``13 - {upload_month}``, i.e. March returns ``10``
upload_month_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The reversed upload month, but padded. i.e. November returns "02"
upload_year
~~~~~~~~~~~
:type: ``Integer``
:description:
The entry's upload year
upload_year_truncated
~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The last two digits of the upload year, i.e. 22 in 2022
upload_year_truncated_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The upload year truncated, but reversed using ``100 - {upload_year_truncated}``, i.e.
2022 returns ``100 - 22`` = ``78``
----------------------------------------------------------------------------------------------------
Ytdl-Sub Variables
------------------
download_index
~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The i'th entry downloaded. NOTE that this is fetched dynamically from the download
archive.
download_index_padded6
~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The download_index padded six digits
upload_date_index
~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The i'th entry downloaded with this upload date.
upload_date_index_padded
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload_date_index padded two digits
upload_date_index_reversed
~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
100 - upload_date_index
upload_date_index_reversed_padded
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The upload_date_index padded two digits
ytdl_sub_input_url
~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The input URL used in ytdl-sub to create this entry.
ytdl_sub_input_url_count
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The total number of input URLs as defined in the subscription.
ytdl_sub_input_url_index
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The index of the input URL as defined in the subscription, top-most being the 0th index.
ytdl_sub_keep_files_date_eval
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``String``
:description:
The standardized date variable supplied in ``output_options.keep_files_date_eval``.

View file

@ -0,0 +1,210 @@
=========
Scripting
=========
``ytdl-sub`` fields (file-names, tags, etc) are defined using variables and scripts. The
links below contain reference documentation for each built-in variable and scripting
function.
.. toctree::
:maxdepth: 1
entry_variables
static_variables
scripting_functions
scripting_types
How it Works
------------
Fields in the config that support ``formatters`` mean they support scripting, and will
*format* the field using its defined script.
In its most basic form, a script is a string comprised of variables and/or functions.
Static String
~~~~~~~~~~~~~
The following example sets ``ytdl-sub``'s output directory. It is
considered *static* because it does not depend on anything from an entry.
.. code-block:: yaml
output_options:
output_directory: "/path/to/tv_shows/Custom YTDL-SUB TV Show"
Static Variables
~~~~~~~~~~~~~~~~
``ytdl-sub`` offers a few built-in static variables, including ``subscription_name``.
We can use this instead of hard-coding it above:
.. code-block:: yaml
output_options:
output_directory: "/path/to/tv_shows/{subscription_name}"
The syntax for variable usage is curly-braces with the variable name within it. Assuming
our subscription is actually named "Custom YTDL-SUB TV Show", then ``ytdl-sub`` will
actually write to that directory.
Entry Variables
~~~~~~~~~~~~~~~
For context, an *entry* is a video or audio file downloaded from ``yt-dlp``. *Entry
variables* are variables that are derived from an entry's ``info.json`` file. This file
comes from ``yt-dlp`` and contains every piece of metadata that it scraped.
These variables are not considered static since they change per entry download. There
are a few fields in ``ytdl-sub`` (i.e. ``output_directory``) that must be static. For
others, we are free to use values that derive from an entry.
Suppose we want to customize the name of an entry's output file and thumbnail to include
its title in its name. We can do that using entry variables:
.. code-block:: yaml
output_options:
output_directory: "/path/to/tv_shows/{subscription_name}"
file_name: "{title}.{ext}"
thumbnail_name: "{title}.{thumbnail_ext}"
Creating Custom Variables
~~~~~~~~~~~~~~~~~~~~~~~~~
Suppose we want to include the date in our file names. This means we'd need to update
both the ``file_name`` and ``thumbnail_name`` fields to include it.
Instead, we can create a custom *override variable*. This is ``ytdl-sub``'s method for
creating and overriding custom variables.
These are created in the ``overrides`` section. Let's take our above example and create
a ``custom_file_name`` variable to use for the entry file and thumbnail fields:
.. code-block:: yaml
output_options:
output_directory: "/path/to/tv_shows/{subscription_name}"
file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
overrides:
custom_file_name: "{upload_date_standardized} {title}"
Sanitizing Variables
~~~~~~~~~~~~~~~~~~~~
For experienced ``yt-dlp`` scrapers, you may be thinking:
- What if the title has characters that do not play nice with my operating system?
``ytdl-sub`` is able to *sanitize* any variable, meaning it replaces any problematic
characters with safe alternatives that can be used in file names. We can ensure our file
names and directories are safe by using:
.. code-block:: yaml
output_options:
output_directory: "/path/to/tv_shows/{subscription_name_sanitized}"
file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
overrides:
custom_file_name: "{upload_date_standardized} {title_sanitized}"
Simply add a ``_sanitized`` suffix to any variable name to make it sanitized.
.. note::
Make sure you do not sanitize custom variables that intentionally create directories,
(i.e. sanitizing ``/path/to/tv_shows/``) otherwise they will... be sanitized and not
resolve to directories!
Using Scripting Functions
~~~~~~~~~~~~~~~~~~~~~~~~~
Let's suppose you are an avid command-line user, and like all of your file names to be
``snake_cased_with_no_spaces``. We can use the `replace
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#replace>`_
*scripting function* to create and use a snake-cased title.
.. code-block:: yaml
output_options:
output_directory: "/path/to/tv_shows/{subscription_name_sanitized}"
file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
overrides:
snake_cased_title: >-
{
%replace( title, ' ', '_' )
}
custom_file_name: "{upload_date_standardized}_{snake_cased_title_sanitized}"
Scripting functions are similar to variables - they must be used within curly-braces.
It is good practice to use ``>-`` when defining variables that use functions. It is
YAML's way of saying:
- Allow a string to be multi-lined, and do not include newlines before or after it.
See for yourself `here
<https://yaml-online-parser.appspot.com/?yaml=output_options%3A%0A%20%20output_directory%3A%20%22%7Bsubscription_name_sanitized%7D%22%0A%20%20file_name%3A%20%22%7Bcustom_file_name%7D.%7Bext%7D%22%0A%20%20thumbnail_name%3A%20%22%7Bcustom_file_name%7D.%7Bthumbnail_ext%7D%22%0A%0Aoverrides%3A%0A%20%20snake_cased_title%3A%20%3E-%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%25replace%28%20title%2C%20%27%20%27%2C%20%27_%27%20%29%0A%20%20%20%20%7D%0A%20%20custom_file_name%3A%20%22%7Bupload_date_standardized%7D%20%7Bsnake_cased_title_sanitized%7D%22&type=json>`_.
Any whitespace within curly-braces is okay since it will be parsed out. This is needed
to make scripting function usage readable.
.. important::
It is important to use ``>-`` over other YAML new-line directives like ``>`` because
they add newlines before or after curly-braces, and will be included in your
variable's output string.
Advanced Scripting
------------------
Accessing ``info.json`` Fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The entirety of an entry's ``info.json`` file resides in the `Map
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_types.html#map>`_
variable `entry_metadata
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/entry_variables.html#entry-metadata>`_.
Any field can be accessed by using the `map_get
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
function like so:
.. code-block:: yaml
:caption:
Fetches the 'artist' value from the .info.json, returns null if it does not exist.
artist: >-
{ %map_get( entry_metadata, "artist", null ) }
Creating Custom Functions
~~~~~~~~~~~~~~~~~~~~~~~~~
Custom functions can be created in the overrides section using the following syntax:
.. code-block:: yaml
overrides:
"%get_entry_metadata_field": >-
{ %map_get( entry_metadata, $0, null ) }
Custom function definitions must have ``%`` as a prefix to the function name, be
surrounded by quotes to make YAML parsing happy, and can support arguments using ``$0``,
``$1``, ... to indicate their first argument, second argument, etc.
Using our new custom function, we can simply the ``artist`` variable definition above to:
.. code-block:: yaml
overrides:
"%get_entry_metadata_field": >-
{ %map_get( entry_metadata, $0, null ) }
artist: >-
{ get_entry_metadata_field("artist") }

View file

@ -0,0 +1,889 @@
..
WARNING: This RST file is generated from docstrings in:
The respective function files under src/ytdl_sub/script/functions/
In order to make a change to this file, edit the respective docstring
and run `make docs`. This will automatically sync the Python RST-based
docstrings into this file. If the docstrings and RST file are out of sync,
it will fail TestDocGen tests in GitHub CI.
Scripting Functions
===================
Array Functions
---------------
array
~~~~~
:spec: ``array(maybe_array: AnyArgument) -> Array``
:description:
Tries to cast an unknown variable type to an Array.
array_apply
~~~~~~~~~~~
:spec: ``array_apply(array: Array, lambda_function: Lambda) -> Array``
:description:
Apply a lambda function on every element in the Array.
:usage:
.. code-block:: python
{
%array_apply( [1, 2, 3] , %string )
}
# ["1", "2", "3"]
array_apply_fixed
~~~~~~~~~~~~~~~~~
:spec: ``array_apply_fixed(array: Array, fixed_argument: AnyArgument, lambda2_function: LambdaTwo, reverse_args: Optional[Boolean]) -> Array``
:description:
Apply a lambda function on every element in the Array, with ``fixed_argument``
passed as a second argument to every invocation.
array_at
~~~~~~~~
:spec: ``array_at(array: Array, idx: Integer, default: Optional[AnyArgument]) -> AnyArgument``
:description:
Return the element in the Array at index ``idx``. If ``idx`` exceeds the array length,
either return ``default`` if provided or throw an error.
array_contains
~~~~~~~~~~~~~~
:spec: ``array_contains(array: Array, value: AnyArgument) -> Boolean``
:description:
Return True if the value exists in the Array. False otherwise.
array_enumerate
~~~~~~~~~~~~~~~
:spec: ``array_enumerate(array: Array, lambda_function: LambdaTwo) -> Array``
:description:
Apply a lambda function on every element in the Array, where each arg
passed to the lambda function is ``idx, element`` as two separate args.
array_extend
~~~~~~~~~~~~
:spec: ``array_extend(arrays: Array, ...) -> Array``
:description:
Combine multiple Arrays into a single Array.
array_first
~~~~~~~~~~~
:spec: ``array_first(array: Array, fallback: AnyArgument) -> AnyArgument``
:description:
Returns the first element whose boolean conversion is True. Returns fallback
if all elements evaluate to False.
array_flatten
~~~~~~~~~~~~~
:spec: ``array_flatten(array: Array) -> Array``
:description:
Flatten any nested Arrays into a single-dimensional Array.
array_index
~~~~~~~~~~~
:spec: ``array_index(array: Array, value: AnyArgument) -> Integer``
:description:
Return the index of the value within the Array if it exists. If it does not, it will
throw an error.
array_overlay
~~~~~~~~~~~~~
:spec: ``array_overlay(array: Array, overlap: Array, only_missing: Optional[Boolean]) -> Array``
:description:
Overlaps ``overlap`` onto ``array``. Can optionally only overlay missing indices.
array_product
~~~~~~~~~~~~~
:spec: ``array_product(arrays: Array, ...) -> Array``
:description:
Returns the Cartesian product of elements from different arrays
array_reduce
~~~~~~~~~~~~
:spec: ``array_reduce(array: Array, lambda_reduce_function: LambdaReduce) -> AnyArgument``
:description:
Apply a reduce function on pairs of elements in the Array, until one element remains.
Executes using the left-most and reduces in the right direction.
array_reverse
~~~~~~~~~~~~~
:spec: ``array_reverse(array: Array) -> Array``
:description:
Reverse an Array.
array_size
~~~~~~~~~~
:spec: ``array_size(array: Array) -> Integer``
:description:
Returns the size of an Array.
array_slice
~~~~~~~~~~~
:spec: ``array_slice(array: Array, start: Integer, end: Optional[Integer]) -> Array``
:description:
Returns the slice of the Array.
----------------------------------------------------------------------------------------------------
Boolean Functions
-----------------
and
~~~
:spec: ``and(values: AnyArgument, ...) -> Boolean``
:description:
``and`` operator. Returns True if all values evaluate to True. False otherwise.
bool
~~~~
:spec: ``bool(value: AnyArgument) -> Boolean``
:description:
Cast any type to a Boolean.
eq
~~
:spec: ``eq(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``==`` operator. Returns True if left == right. False otherwise.
gt
~~
:spec: ``gt(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``>`` operator. Returns True if left > right. False otherwise.
gte
~~~
:spec: ``gte(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``>=`` operator. Returns True if left >= right. False otherwise.
is_array
~~~~~~~~
:spec: ``is_array(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is a Map. False otherwise.
is_bool
~~~~~~~
:spec: ``is_bool(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is a Float. False otherwise.
is_float
~~~~~~~~
:spec: ``is_float(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is a Float. False otherwise.
is_int
~~~~~~
:spec: ``is_int(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is an Integer. False otherwise.
is_map
~~~~~~
:spec: ``is_map(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is a Map. False otherwise.
is_null
~~~~~~~
:spec: ``is_null(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is null (i.e. an empty string). False otherwise.
is_numeric
~~~~~~~~~~
:spec: ``is_numeric(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is either an Integer or Float. False otherwise.
is_string
~~~~~~~~~
:spec: ``is_string(value: AnyArgument) -> Boolean``
:description:
Returns True if a value is a String. False otherwise.
lt
~~
:spec: ``lt(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``<`` operator. Returns True if left < right. False otherwise.
lte
~~~
:spec: ``lte(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``<=`` operator. Returns True if left <= right. False otherwise.
ne
~~
:spec: ``ne(left: AnyArgument, right: AnyArgument) -> Boolean``
:description:
``!=`` operator. Returns True if left != right. False otherwise.
not
~~~
:spec: ``not(value: Boolean) -> Boolean``
:description:
``not`` operator. Returns the opposite of value.
or
~~
:spec: ``or(values: AnyArgument, ...) -> Boolean``
:description:
``or`` operator. Returns True if any value evaluates to True. False otherwise.
xor
~~~
:spec: ``xor(values: AnyArgument, ...) -> Boolean``
:description:
``^`` operator. Returns True if exactly one value is set to True. False otherwise.
----------------------------------------------------------------------------------------------------
Conditional Functions
---------------------
elif
~~~~
:spec: ``elif(if_elif_else: AnyArgument, ...) -> AnyArgument``
:description:
Conditional ``if`` statement that is capable of doing else-ifs (``elif``) via
adjacent arguments. It is expected for there to be an odd number of arguments >= 3 to
supply at least one conditional and an else.
:usage:
.. code-block:: python
%elif(
condition1,
return1,
condition2,
return2,
...
else_return
)
if
~~
:spec: ``if(condition: Boolean, true: ReturnableArgumentA, false: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]``
:description:
Conditional ``if`` statement that returns the ``true`` or ``false`` parameter
depending on the ``condition`` value.
if_passthrough
~~~~~~~~~~~~~~
:spec: ``if_passthrough(maybe_true_arg: ReturnableArgumentA, else_arg: ReturnableArgumentB) -> Union[ReturnableArgumentA, ReturnableArgumentB]``
:description:
Conditional ``if`` statement that returns the ``maybe_true_arg`` if it evaluates to True,
otherwise returns ``else_arg``.
----------------------------------------------------------------------------------------------------
Date Functions
--------------
datetime_strftime
~~~~~~~~~~~~~~~~~
:spec: ``datetime_strftime(posix_timestamp: Integer, date_format: String) -> String``
:description:
Converts a posix timestamp to a date using strftime formatting.
----------------------------------------------------------------------------------------------------
Error Functions
---------------
assert
~~~~~~
:spec: ``assert(value: ReturnableArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` evaluates to
False. If it evaluates to True, it will return ``value``.
assert_eq
~~~~~~~~~
:spec: ``assert_eq(value: ReturnableArgument, equals: AnyArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` does not equal
``equals``. If they do equal, then return ``value``.
assert_ne
~~~~~~~~~
:spec: ``assert_ne(value: ReturnableArgument, equals: AnyArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` equals
``equals``. If they do equal, then return ``value``.
assert_then
~~~~~~~~~~~
:spec: ``assert_then(value: AnyArgument, ret: ReturnableArgument, assert_message: String) -> ReturnableArgument``
:description:
Explicitly throw an error with the provided assert message if ``value`` evaluates to
False. If it evaluates to True, it will return ``ret``.
throw
~~~~~
:spec: ``throw(error_message: String) -> AnyArgument``
:description:
Explicitly throw an error with the provided error message.
----------------------------------------------------------------------------------------------------
Json Functions
--------------
from_json
~~~~~~~~~
:spec: ``from_json(argument: String) -> AnyArgument``
:description:
Converts a JSON string into an actual type.
----------------------------------------------------------------------------------------------------
Map Functions
-------------
map
~~~
:spec: ``map(maybe_mapping: AnyArgument) -> Map``
:description:
Tries to cast an unknown variable type to a Map.
map_apply
~~~~~~~~~
:spec: ``map_apply(mapping: Map, lambda_function: LambdaTwo) -> Array``
:description:
Apply a lambda function on the Map, where each arg
passed to the lambda function is ``key, value`` as two separate args.
map_contains
~~~~~~~~~~~~
:spec: ``map_contains(mapping: Map, key: AnyArgument) -> Boolean``
:description:
Returns True if the key is in the Map. False otherwise.
map_enumerate
~~~~~~~~~~~~~
:spec: ``map_enumerate(mapping: Map, lambda_function: LambdaThree) -> Array``
:description:
Apply a lambda function on the Map, where each arg
passed to the lambda function is ``idx, key, value`` as three separate args.
map_extend
~~~~~~~~~~
:spec: ``map_extend(maps: Map, ...) -> Map``
:description:
Return maps combined in the order from left-to-right. Duplicate keys will use the
right-most map's value.
map_get
~~~~~~~
:spec: ``map_get(mapping: Map, key: AnyArgument, default: Optional[AnyArgument]) -> AnyArgument``
:description:
Return ``key``'s value within the Map. If ``key`` does not exist, and ``default`` is
provided, it will return ``default``. Otherwise, will error.
map_get_non_empty
~~~~~~~~~~~~~~~~~
:spec: ``map_get_non_empty(mapping: Map, key: AnyArgument, default: AnyArgument) -> AnyArgument``
:description:
Return ``key``'s value within the Map. If ``key`` does not exist or is an empty string,
return ``default``. Otherwise, will error.
map_size
~~~~~~~~
:spec: ``map_size(mapping: Map) -> Integer``
:description:
Returns the size of a Map.
----------------------------------------------------------------------------------------------------
Numeric Functions
-----------------
add
~~~
:spec: ``add(values: Numeric, ...) -> Numeric``
:description:
``+`` operator. Returns the sum of all values.
div
~~~
:spec: ``div(left: Numeric, right: Numeric) -> Numeric``
:description:
``/`` operator. Returns ``left / right``.
float
~~~~~
:spec: ``float(value: AnyArgument) -> Float``
:description:
Cast to Float.
int
~~~
:spec: ``int(value: AnyArgument) -> Integer``
:description:
Cast to Integer.
max
~~~
:spec: ``max(values: Numeric, ...) -> Numeric``
:description:
Returns max of all values.
min
~~~
:spec: ``min(values: Numeric, ...) -> Numeric``
:description:
Returns min of all values.
mod
~~~
:spec: ``mod(left: Numeric, right: Numeric) -> Numeric``
:description:
``%`` operator. Returns ``left % right``.
mul
~~~
:spec: ``mul(values: Numeric, ...) -> Numeric``
:description:
``*`` operator. Returns the product of all values.
pow
~~~
:spec: ``pow(base: Numeric, exponent: Numeric) -> Numeric``
:description:
``**`` operator. Returns the exponential of the base and exponent value.
range
~~~~~
:spec: ``range(end: Integer, start: Optional[Integer], step: Optional[Integer]) -> Array``
:description:
Returns the desired range of Integers in the form of an Array.
sub
~~~
:spec: ``sub(values: Numeric, ...) -> Numeric``
:description:
``-`` operator. Subtracts all values from left to right.
----------------------------------------------------------------------------------------------------
Print Functions
---------------
print
~~~~~
:spec: ``print(message: AnyArgument, passthrough: ReturnableArgument, level: Optional[Integer]) -> ReturnableArgument``
:description:
Log the ``message`` and return ``passthrough``. Optionally can pass level,
where < 0 is debug, 0 is info, 1 is warning, > 1 is error. (default ``0``)
print_if_false
~~~~~~~~~~~~~~
:spec: ``print_if_false(message: AnyArgument, passthrough: ReturnableArgument, level: Optional[Integer]) -> ReturnableArgument``
:description:
Log the ``message`` if ``passthrough`` evaluates to ``false``. Return
``passthrough``. Optionally can pass level, where < 0 is debug, 0 is info, 1
is warning, > 1 is error. (default ``0``)
print_if_true
~~~~~~~~~~~~~
:spec: ``print_if_true(message: AnyArgument, passthrough: ReturnableArgument, level: Optional[Integer]) -> ReturnableArgument``
:description:
Log the ``message`` if ``passthrough`` evaluates to ``true``. Return
``passthrough``. Optionally can pass level, where < 0 is debug, 0 is info, 1
is warning, > 1 is error. (default ``0``)
----------------------------------------------------------------------------------------------------
Regex Functions
---------------
regex_capture_groups
~~~~~~~~~~~~~~~~~~~~
:spec: ``regex_capture_groups(regex: String) -> Integer``
:description:
Returns number of capture groups in regex
regex_capture_many
~~~~~~~~~~~~~~~~~~
:spec: ``regex_capture_many(string: String, regex_array: Array, default: Optional[Array]) -> Array``
:description:
Returns the input string and first regex's capture groups that match to the string
in an array. If a default is not provided, then all number of regex capture groups
must be equal across all regex strings. In addition, an error will be thrown if
no matches are found.
If the default is provided, then the number of capture groups must be less than
or equal to the length of the default value array. Any element not captured
will return the respective default value.
:usage:
.. code-block:: python
{
%regex_capture_many(
"2020-02-27",
[
"No (.*) matches here",
"([0-9]+)-([0-9]+)-27"
],
[ "01", "01" ]
)
}
# ["2020-02-27", "2020", "02"]
regex_capture_many_required
~~~~~~~~~~~~~~~~~~~~~~~~~~~
:spec: ``regex_capture_many_required(string: String, regex_array: Array) -> Array``
:description:
Deprecated. Use %regex_capture_many instead.
regex_capture_many_with_defaults
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:spec: ``regex_capture_many_with_defaults(string: String, regex_array: Array, default: Optional[Array]) -> Array``
:description:
Deprecated. Use %regex_capture_many instead.
regex_fullmatch
~~~~~~~~~~~~~~~
:spec: ``regex_fullmatch(regex: String, string: String) -> Array``
:description:
Checks for entire string to be a match. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array.
regex_match
~~~~~~~~~~~
:spec: ``regex_match(regex: String, string: String) -> Array``
:description:
Checks for a match only at the beginning of the string. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array.
regex_search
~~~~~~~~~~~~
:spec: ``regex_search(regex: String, string: String) -> Array``
:description:
Checks for a match anywhere in the string. If a match exists, returns
the string as the first element of the Array. If there are capture groups, returns each
group as a subsequent element in the Array.
regex_search_any
~~~~~~~~~~~~~~~~
:spec: ``regex_search_any(string: String, regex_array: Array) -> Boolean``
:description:
Returns True if any regex pattern in the regex array matches the string. False otherwise.
regex_sub
~~~~~~~~~
:spec: ``regex_sub(regex: String, replacement: String, string: String) -> String``
:description:
Returns the string obtained by replacing the leftmost non-overlapping occurrences of the
pattern in string by the replacement string. The replacement string can reference the
match groups via backslash escapes. Callables as replacement argument are not supported.
----------------------------------------------------------------------------------------------------
String Functions
----------------
capitalize
~~~~~~~~~~
:spec: ``capitalize(string: String) -> String``
:description:
Capitalize the first character in the string.
concat
~~~~~~
:spec: ``concat(values: AnyArgument, ...) -> String``
:description:
Concatenate multiple Strings into a single String.
contains
~~~~~~~~
:spec: ``contains(string: String, contains: String) -> Boolean``
:description:
Returns True if ``contains`` is in ``string``. False otherwise.
contains_all
~~~~~~~~~~~~
:spec: ``contains_all(string: String, contains_array: Array) -> Boolean``
:description:
Returns true if all elements in ``contains_array`` are in ``string``. False otherwise.
contains_any
~~~~~~~~~~~~
:spec: ``contains_any(string: String, contains_array: Array) -> Boolean``
:description:
Returns true if any element in ``contains_array`` is in ``string``. False otherwise.
join
~~~~
:spec: ``join(separator: String, array: Array) -> String``
:description:
Join all elements in the array together as a string, and insert the
separator between them.
:usage:
.. code-block:: python
{
%join( ", ", ["item1", "item2"] )
}
# "item1, item2"
lower
~~~~~
:spec: ``lower(string: String) -> String``
:description:
Lower-case the entire String.
pad
~~~
:spec: ``pad(string: String, length: Integer, char: String) -> String``
:description:
Pads the string to the given length
pad_zero
~~~~~~~~
:spec: ``pad_zero(numeric: Numeric, length: Integer) -> String``
:description:
Pads a numeric with zeros to the given length
replace
~~~~~~~
:spec: ``replace(string: String, old: String, new: String, count: Optional[Integer]) -> String``
:description:
Replace the ``old`` part of the String with the ``new``. Optionally only replace it
``count`` number of times.
slice
~~~~~
:spec: ``slice(string: String, start: Integer, end: Optional[Integer]) -> String``
:description:
Returns the slice of the Array.
split
~~~~~
:spec: ``split(string: String, sep: String, max_split: Optional[Integer]) -> Array``
:description:
Splits the input string into multiple strings.
string
~~~~~~
:spec: ``string(value: AnyArgument) -> String``
:description:
Cast to String.
strip
~~~~~
:spec: ``strip(string: String) -> String``
:description:
Strip a string of all its whitespace at the beginning and end.
:usage:
.. code-block:: python
{
%trim(" delete the outer! ")
}
# "delete the outer!"
titlecase
~~~~~~~~~
:spec: ``titlecase(string: String) -> String``
:description:
Capitalize each word in the string.
unescape
~~~~~~~~
:spec: ``unescape(string: String) -> String``
:description:
Unescape symbols like newlines or tabs to their true form.
:usage:
.. code-block:: python
{
%unescape( "Hello\nWorld" )
}
# Hello
# World
upper
~~~~~
:spec: ``upper(string: String) -> String``
:description:
Upper-case the entire String.
----------------------------------------------------------------------------------------------------
Ytdl-Sub Functions
------------------
legacy_bracket_safety
~~~~~~~~~~~~~~~~~~~~~
:spec: ``legacy_bracket_safety(value: ReturnableArgument) -> ReturnableArgument``
ytdl-sub used to replace brackets ('{', '}') with unicode brackets ('', '') to not
interfere with its legacy variable scripting system. This function replicates that
behavior.
sanitize
~~~~~~~~
:spec: ``sanitize(value: AnyArgument, ...) -> String``
Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use
for file/directory names on any OS.
sanitize_plex_episode
~~~~~~~~~~~~~~~~~~~~~
:spec: ``sanitize_plex_episode(string: String) -> String``
Sanitize a string using ``sanitize`` and replace numerics with their respective fixed-width
numbers. This is used to have Plex avoid scraping numbers like ``4x4`` as the
season and/or episode.
to_date_metadata
~~~~~~~~~~~~~~~~
:spec: ``to_date_metadata(yyyymmdd: String) -> Map``
Takes a date in the form of YYYYMMDD and returns a Map containing:
- date (String, YYYYMMDD)
- date_standardized (String, YYYY-MM-DD)
- year (Integer)
- month (Integer)
- day (Integer)
- year_truncated (Integer, YY from YY[YY])
- month_padded (String)
- day_padded (String)
- year_truncated_reversed (Integer, 100 - year_truncated)
- month_reversed (Integer, 13 - month)
- month_reversed_padded (String)
- day_reversed (Integer, total_days_in_month + 1 - day)
- day_reversed_padded (String)
- day_of_year (Integer)
- day_of_year_padded (String, padded 3)
- day_of_year_reversed (Integer, total_days_in_year + 1 - day_of_year)
- day_of_year_reversed_padded (String, padded 3)
to_native_filepath
~~~~~~~~~~~~~~~~~~
:spec: ``to_native_filepath(filepath: String) -> String``
Convert any unix-based path separators ('/') with the OS's native
separator. In addition, expand ~ to absolute directories.
truncate_filepath_if_too_long
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:spec: ``truncate_filepath_if_too_long(filepath: String) -> String``
If a file-path is too long for the OS, this function will truncate it while preserving
the extension.

View file

@ -0,0 +1,408 @@
===============
Scripting Types
===============
Types
-----
String
~~~~~~
Strings are a series of characters surrounded by quotes.
.. code-block:: yaml
string_variable: "This is a String variable"
.. note::
For non-String types, they must be defined as parameters to scripting functions. This
is because anything in a variable definition that is not within curly-braces gets
evaluated as a String.
We can define Strings within curly-braces by setting them as parameters to a function:
.. tab-set::
.. tab-item:: Multi-Line Single Quote
.. code-block:: yaml
string_variable: >-
{
%string('This is a String variable')
}
.. tab-item:: Multi-Line Double Quote
.. code-block:: yaml
string_variable: >-
{
%string("This is a String variable")
}
There are a few ways to make variables that use curly braces more compact, including:
.. tab-set::
.. tab-item:: New-Line Single Quote
.. code-block:: yaml
string_variable: >-
{ %string('This is a String variable') }
.. tab-item:: New-Line Double Quote
.. code-block:: yaml
string_variable: >-
{ %string("This is a String variable") }
.. tab-item:: Same-Line
.. code-block:: yaml
string_variable: "{ %string('This is a String variable') }"
In the case that you want to define a string variable that contains both single and
double quotes, triple-quotes can be used to avoid *closing* the String.
.. tab-set::
.. tab-item:: Triple-Single Quote
.. code-block:: yaml
string_variable: >-
{
%string('''This has both " and ' in it.''')
}
.. tab-item:: Triple-Double Quote
.. code-block:: yaml
string_variable: >-
{
%string("""This has both " and ' in it.""")
}
If you want a plain string that contains literal curly braces, you can escape them like
so:
.. code-block:: yaml
string_variable: "This contains \\{ literal curly braces \\}"
Integer
~~~~~~~
Integers are whole numbers with no decimal.
.. tab-set::
.. tab-item:: Multi-Line
.. code-block:: yaml
int_variable: >-
{
%int(2022)
}
.. tab-item:: New-Line
.. code-block:: yaml
int_variable: >-
{ %int(2022) }
.. tab-item:: Same-Line
.. code-block:: yaml
int_variable: "{ %int(2022) }"
Float
~~~~~
Floats are floating-point decimals numbers.
.. tab-set::
.. tab-item:: Multi-Line
.. code-block:: yaml
float_variable: >-
{
%float(3.14)
}
.. tab-item:: New-Line
.. code-block:: yaml
float_variable: >-
{ %float(3.14) }
.. tab-item:: Same-Line
.. code-block:: yaml
float_variable: "{ %float(3.14) }"
Boolean
~~~~~~~
A type is considered boolean if it spells out ``True`` or ``False``, case-insensitive.
.. tab-set::
.. tab-item:: Multi-Line
.. code-block:: yaml
bool_variable: >-
{
%bool(True)
}
.. tab-item:: New-Line
.. code-block:: yaml
bool_variable: >-
{ %bool(True) }
.. tab-item:: Same-Line
.. code-block:: yaml
bool_variable: "{ %bool(FALSE) }"
Array
~~~~~
An Array contains multiple types of any kind, including nested Arrays and Maps. Arrays
are defined using brackets (``[ ]``), and are accessed using zero-based indexing.
.. tab-set::
.. tab-item:: Multi-Line
.. code-block:: yaml
array_variable: >-
{
[
"element with index 0",
1,
2.0,
[ "Nested Array 3" ]
]
}
element_0: >-
{
%array_at(array_variable, 0)
}
.. tab-item:: New-Line
.. code-block:: yaml
array_variable: >-
{ ["element with index 0", 1, 2.0, ["Nested Array 3"]] }
element_0: >-
{ %array_at(array_variable, 0) }
.. tab-item:: Same-Line
.. code-block:: yaml
array_variable: "{ ['element with index 0', 1, 2.0, ['Nested Array 3' ]] }"
element_0: "{ %array_at(array_variable, 0) }"
Map
~~~
A Map is a key-value store, containing mappings between keys and values. Maps are
defined using curly-braces (``{ }``), and are accessed using their keys.
.. tab-set::
.. tab-item:: Multi-Line
.. code-block:: yaml
map_variable: >-
{
{
"string_key": "string_value",
1: "int_key",
"list_value": [ "elem0", 1, 2.0 ]
}
}
string_value: >-
{
%map_get(map_variable, "string_key")
}
.. tab-item:: New-Line
.. code-block:: yaml
map_variable: >-
{ {"string_key": "string_value", 1: "int_key", "list_value": ["elem0", 1, 2.0]} }
string_value: >-
{ %map_get(map_variable, "string_key") }
.. tab-item:: Same-Line
.. code-block:: yaml
map_variable: "{ {'string_key': 'string_value', 1: 'int_key', 'list_value': [ 'elem0', 1, 2.0 ]} }"
string_value: "{ %map_get(map_variable, 'string_key') }"
Null
~~~~
Null is represented by an empty String, and can be conveyed by spelling out ``null``,
case-insensitive.
.. tab-set::
.. tab-item:: Literal
.. code-block:: yaml
null_variable: ""
.. tab-item:: New-Line
.. code-block:: yaml
null_variable: >-
{ %string(null) }
.. tab-item:: Same-Line
.. code-block:: yaml
null_variable: "{ %string(null) }"
Function Type-Hints
-------------------
AnyArgument
~~~~~~~~~~~
AnyArgument means any of the above Types are valid as input or output to a scripting
function.
.. note::
Strict typing is enforced. For functions that return ``AnyArgument`` need to be casted before
passing into functions that expect a particular type.
Numeric
~~~~~~~
Numeric refers to either an Integer or Float.
Optional
~~~~~~~~
Optional means a particular scripting function argument can be either provided or not
included. For example, the function `map_get
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
has an optional default value. Both of these usages are valid:
.. tab-set::
.. tab-item:: Map Get
.. code-block:: yaml
will_throw_key_does_not_exist_error: "{ %map_get( {}, 'key' ) }"
.. tab-item:: Map Get with Optional Default Value
.. code-block:: yaml
will_return_default: "{ %map_get( {}, 'key', 'default value' ) }"
Lambda
~~~~~~
Lambda parameters are a reference to a function, and will call that lambda function on
the input. In this example,
.. code-block:: yaml
lambda_array_numeric_to_string: >-
{
%array_apply( [ 1, 2, 3, 4], %string )
}
We apply ``%string`` as a lambda function to `array_apply
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-apply>`_,
which is called on every element in the input array. The output becomes ``["1", "2",
"3", "4"]``.
This example has one input-argument being passed into the lambda. For other lambda-based
functions like `array_enumerate
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-enumerate>`_,
it expects the lambda function to have two input arguments. These are denoted using
``LambdaTwo``, ``LambdaThree``, etc within the function spec.
LambdaReduce
~~~~~~~~~~~~
LambdaReduce parameters are a reference to a function that will perform a *reduce* - an
operation that reduces an Array to a single value by calling the LambdaReduce function
repeatedly on two elements in the Array until it is reduced to a single value.
In this example,
.. code-block:: yaml
lambda_reduce_sum: >-
{
%array_reduce( [ 1, 2, 3, 4], %add )
}
We call `array_reduce
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-reduce>`_
on the input array, using `add
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#add>`_
as the LambdaReduce function. This will reduce the Array to a single value by internally
calling
- *reduce-call 1*: ``%add(1, 2) = 3`` (first two elements)
- *reduce-call 2*: ``%add(3, 3) = 6`` (output from first two and third element)
- *reduce-call 3*: ``%add(6, 4) = 10`` (output from first three elements and fourth element)
And evaluate to ``10``.
ReturnableArguments
~~~~~~~~~~~~~~~~~~~
Returnable arguments are used in conditional functions like `if
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#if>`_,
which implies the argument passed into the function is the function's output. For
example,
.. code-block:: yaml
conditional_function: >-
{
%if( True, "Return this if True", "Return this if False" )
}
is going to return ``"Return this if True"`` since the condition parameter is ``True``.

View file

@ -0,0 +1,103 @@
..
WARNING: This RST file is generated from docstrings in:
src/ytdl_sub/entries/variables/override_variables.py
In order to make a change to this file, edit the respective docstring
and run `make docs`. This will automatically sync the Python RST-based
docstrings into this file. If the docstrings and RST file are out of sync,
it will fail TestDocGen tests in GitHub CI.
Static Variables
================
Subscription Variables
----------------------
subscription_array
~~~~~~~~~~~~~~~~~~
For subscriptions in the form of
.. code-block:: yaml
"Subscription Name":
- "https://url1.com/..."
- "https://url2.com/..."
Store all values into an array named ``subscription_array``.
subscription_has_download_archive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns True if the subscription has any entries recorded in a download archive. False
otherwise.
subscription_indent_i
~~~~~~~~~~~~~~~~~~~~~
For subscriptions where the ancestor keys contain the ``= ...`` prefix, the
variables ``subscription_indent_1``, ``subscription_indent_2``, and so on get
set to each subsequent value. For example, given the following subscriptions
file snippet:
.. code-block:: yaml
Preset 1 | = Indent Value 1 | Preset 2:
Preset 3 | = Indent Value 2 | Preset 4:
"Subscription Name": "https://..."
The ``{subscription_indent_1}`` variable will be ``Indent Value 1`` and
``{subscription_indent_2}`` will be ``Indent Value 2``. The most common use of
these variables is to :doc:`set the genre and rating for subscriptions from the
YAML keys <../prebuilt_presets/tv_show>`.
subscription_map
~~~~~~~~~~~~~~~~
For subscriptions in the form of
.. code-block:: yaml
+ Subscription Name:
Music Videos:
- "https://url1.com/..."
Concerts:
- "https://url2.com/..."
Stores all the contents under the subscription name into the override variable
``subscription_map`` as a Map value. The above example is stored as:
.. code-block:: python
{
"Music Videos": [
"https://url1.com/..."
],
"Concerts: [
"https://url2.com/..."
]
}
subscription_name
~~~~~~~~~~~~~~~~~
Name of the subscription. For subscriptions types that use a prefix (``~``, ``+``),
the prefix and all whitespace afterwards is stripped from the subscription name.
subscription_value
~~~~~~~~~~~~~~~~~~
For subscriptions in the form of
.. code-block:: yaml
"Subscription Name": "https://..."
``subscription_value`` gets set to ``https://...``.
subscription_value_i
~~~~~~~~~~~~~~~~~~~~
For subscriptions in the form of
.. code-block:: yaml
"Subscription Name":
- "https://url1.com/..."
- "https://url2.com/..."
``subscription_value_1`` and ``subscription_value_2`` get set to ``https://url1.com/...``
and ``https://url2.com/...``. Note that ``subscription_value_1`` also gets set to
``subscription_value``.

View file

@ -0,0 +1,128 @@
==================
Subscription File
==================
A subscription file is designed to both define and organize many things to download in
condensed YAML.
.. hint::
Read the :ref:`getting started guide <guides/getting_started/index:Getting Started>`
first before reviewing this section.
File Preset
-----------
Many examples show ``__preset__`` at the top. This is known as the *subscription file
preset*. It is where a single :ref:`preset <guides/getting_started/first_config:Custom
Preset Definition>` can be defined that gets applied to each subscription within the
file.
This is a good place to apply file-wide variables such as ``tv_show_directory`` or
supply a cookies file path.
.. code-block:: yaml
__preset__:
# Variables that override defaults from `overrides:` for presets in YAML keys:
overrides:
tv_show_directory: "/tv_shows"
# Directly set plugin options:
ytdl_options:
cookiefile: "/config/ytdl-sub-configs/cookie.txt"
Layout
------
A subscription file is comprised of YAML keys and values. Keys can be either
- a preset
- an override value
- a subscription name
Take the following example:
.. code-block:: yaml
Jellyfin TV Show by Date:
= News:
"Breaking News": "https://www.youtube.com/@SomeBreakingNews"
"BBC News": "https://www.youtube.com/@BBCNews"
All three types of keys are used for the following:
- ``Jellyfin TV Show by Date`` - a prebuilt preset
- ``= News`` - an override value for genre
- ``Breaking News``, ``BBC News`` - The subscription names
The lowest level, most indented keys should always be the subscription name. It is good
practice to put subscription names in quotes to differentiate between preset names and
subscription names.
Values should always be the subscription itself. The simplest form is just the
URL. Further sections will show more exotic examples that go beyond a single URL.
Inheritance
-----------
A subscription inherits every key above it. In the above example, both ``Breaking News``
and ``BBC News`` inherits the ``Jellyfin TV Show by Date`` preset and the ``= News``
override value.
.. note::
There are no limits or boundaries on how one structures their presets. This
flexibility is intended for subscription authors to organize their downloads as they
see fit.
Multi Keys
----------
Subscription keys support pipe syntax, or ``|``, which allows multiple keys to be
defined on a single line. The following is equivalent to the above example:
.. code-block:: yaml
Jellyfin TV Show by Date | = News:
"Breaking News": "https://www.youtube.com/@SomeBreakingNews"
"BBC News": "https://www.youtube.com/@BBCNews"
Override Mode
-------------
Often times, it is convenient to set multiple override values for a single
subscription. We can put a preset in *override mode* by using tilda syntax, or ``~``.
Suppose we want to apply the :ref:`Only Recent <prebuilt_presets/helpers:Only Recent>`
preset to the above examples. But for ``BBC News`` specifically, we want to set the date
range to be different than the default ``2months`` value to ``2weeks``.
We can change it as follows:
.. code-block:: yaml
Jellyfin TV Show by Date
= News | Only Recent:
"Breaking News": "https://www.youtube.com/@SomeBreakingNews"
"~BBC News":
url: "https://www.youtube.com/@BBCNews"
only_recent_date_range: "2weeks"
.. important::
When using override mode, we need to set the ``url`` variable since we are no longer
using the simplified *subscription_value*. For more info on how this works, read about
:ref:`subscription variables <config_reference/scripting/static_variables:Subscription
Variables>`.
Map Mode
--------
Map mode is for highly advanced presets that benefit from a more complex subscription
definition. TODO: Show music video example here.

78
docs/source/debugging.rst Normal file
View file

@ -0,0 +1,78 @@
Debugging
=========
Run with ``--log-level debug`` to show all log messages, often too much information for
normal operation but useful when investigating a specific problem.
:ref:`ytdl-sub builds on yt-dlp <introduction:motivation>`, which is in itself a complex
tool. It performs an intricate and fragile task, web scraping, which in turn :ref:`is
subject to the whims of external services <guides/getting_started/index:minimize the
work to only what's necessary>` outside its control. Finally, because :ref:`ytdl-sub is
a lower-level tool <guides/getting_started/index:prerequisite knowledge>`, many users,
if not most, will have problems getting their configuration working and it can be
difficult to determine when the root cause is their configuration, just a limit imposed
by the services, or, least likely, a bug in one of the tools involved.
To expedite resolution and conserve the limited resources of both yourself and
volunteers, do as much investigation yourself as you can:
#. Start by assuming the issue is your configuration:
Review :doc:`the guides <./guides/index>` to confirm your understanding. Increase
output using the ``--log-level`` CLI option and read the output carefully for hints
and clues. Use those clues to `search the docs`_. Read :doc:`the reference docs
<./config_reference/index>` of the involved ``ytdl-sub`` components.
#. Try to determine if the issue is happening in ``yt-dlp`` or ``ytdl-sub``:
The user's configuration tells ``ytdl-sub`` how to run ``yt-dlp``. ``yt-dlp`` handles
all the web scraping and downloading. ``ytdl-sub`` then assembles the files and metadata
produced by ``yt-dlp`` and places them in your library.
If the issue is happening while scraping or downloading from the external service,
then it's happening in the running of ``yt-dlp``. Look for output showing failed
downloads, ``403`` errors, or signs of throttles. That doesn't mean it's a bug in
``yt-dlp``, it could be in how your configuration tells ``ytdl-sub`` to run
``yt-dlp`` or limits imposed by the service that are constantly changing, but you may
be able to find answers from other ``yt-dlp`` users running into similar issues.
See `the yt-dlp known issues`_ and `search their issues`_ for clues and hints. Read
the comments for more understanding, workarounds, and maybe even fixes. If you still
don't understand the cause after reading everything you can find there, try to find
help in `the yt-dlp Discord`_.
#. If the issue is happening in ``ytdl-sub``, reach out for help:
Once you've done everything you can to get your configuration working and you've
determined that the issue isn't happening in ``yt-dlp``, look for answers in
``ytdl-sub``:
#. Cut your configuration and subscriptions down to the minimum that reproduces the
issue.
#. Run with the ``--log-level debug`` CLI option and copy the full output.
#. `Search the ytdl-sub issues`_ using clues and hints from the output.
#. `Open a support post in Discord`_ with those details and all other relevant
details.
#. If someone from the Discord discussion directs you to, then `open a new issue`_
with those same details.
.. _`the yt-dlp known issues`:
https://github.com/yt-dlp/yt-dlp/wiki/FAQ#known-issues
.. _`search their issues`:
https://github.com/yt-dlp/yt-dlp/issues
.. _`the yt-dlp Discord`:
https://discord.gg/H5MNcFW63r
.. _`search the docs`:
https://ytdl-sub.readthedocs.io/en/latest/search.html
.. _`search the ytdl-sub issues`:
https://github.com/jmbannon/ytdl-sub/issues
.. _`open a support post in Discord`:
https://discord.com/channels/994270357957648404/1084886228266127460
.. _`open a new issue`:
https://github.com/jmbannon/ytdl-sub/issues/new

View file

@ -0,0 +1,113 @@
Deprecation Notices
===================
Dec 2025
--------
Override variables names can no longer be plugin names, to avoid the common pitfall of
defining a plugin underneath ``overrides``.
In the past, there was usage of a ``date_range`` override variable in a few example configs
that complimented the ``Only Recent`` preset. This overrride variable usage needs to be
replaced with ``only_recent_date_range``.
Sep 2024
--------
regex plugin
~~~~~~~~~~~~
Regex plugin has been removed in favor of scripting. The function
:ref:`config_reference/scripting/scripting_functions:regex_capture_many` has been
created to replicate the plugin's behavior. See the following converted example:
.. code-block:: yaml
:caption: regex plugin
regex:
from:
title:
match:
- ".*? - (.*)" # Captures 'Some - Song' from 'Emily Hopkins - Some - Song'
capture_group_names:
- "captured_track_title"
capture_group_defaults:
- "{title}"
overrides:
track_title: "{captured_track_title}"
.. code-block:: yaml
:caption: scripting
overrides:
# Captures 'Some - Song' from 'Emily Hopkins - Some - Song'
captured_track_title: >-
{
%regex_capture_many(
title,
[ ".*? - (.*)" ],
[ title ]
)
}
track_title: "{%array_at(captured_track_title, 1)}"
Oct 2023
--------
subscription preset and value
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The use of ``__value__`` will go away in Dec 2023 in favor of the method found in
:ref:`config_reference/subscription_yaml:Subscription File`. ``__preset__`` will still
be supported for the time being.
July 2023
---------
music_tags
~~~~~~~~~~
Music tags are getting simplified. ``tags`` will now reside directly under music_tags,
and ``embed_thumbnail`` is getting moved to its own plugin (supports video files as
well). Convert from:
.. code-block:: yaml
my_example_preset:
music_tags:
embed_thumbnail: True
tags:
artist: "Elvis Presley"
To the following:
.. code-block:: yaml
my_example_preset:
embed_thumbnail: True
music_tags:
artist: "Elvis Presley"
The old format will be removed in October 2023.
video_tags
~~~~~~~~~~
Video tags are getting simplified as well. ``tags`` will now reside directly under
video_tags. Convert from:
.. code-block:: yaml
my_example_preset:
video_tags:
tags:
title: "Elvis Presley Documentary"
To the following:
.. code-block:: yaml
my_example_preset:
video_tags:
title: "Elvis Presley Documentary"

294
docs/source/faq/index.rst Normal file
View file

@ -0,0 +1,294 @@
===
FAQ
===
Since ytdl-sub is relatively new to the public, there has not been many question asked
yet. We will update this as more questions get asked.
.. contents:: Frequently Asked Questions
:depth: 3
How do I...
-----------
...remove the date in the video title?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The :ref:`config_reference/prebuilt_presets/tv_show:TV Show` presets by default include
the upload date in the ``episode_title`` override variable. This variable is used to set
the title in things like the video metadata, NFO file, etc, which is subsequently read
by media players. This can be overwritten as you see fit by redefining it:
.. code-block:: yaml
overrides:
episode_title: "{title}" # Only sets the video title
...download age-restricted YouTube videos?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See `yt-dl's recommended way
<https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl>`_ to
download your YouTube cookie, then add it to your :ref:`ytdl options
<config_reference/plugins:ytdl_options>` section of your config:
.. code-block:: yaml
ytdl_options:
cookiefile: "/path/to/cookies/file.txt"
...automate my downloads?
~~~~~~~~~~~~~~~~~~~~~~~~~
:doc:`This page </guides/getting_started/automating_downloads>` shows how to set up
``ytdl-sub`` to run automatically on various platforms.
...download large channels?
~~~~~~~~~~~~~~~~~~~~~~~~~~~
See the prebuilt preset :doc:`chunk_initial_download </prebuilt_presets/helpers>`.
...filter to include or exclude based on certain keywords?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See the prebuilt preset :doc:`Filter Keywords </prebuilt_presets/helpers>`.
...prevent creation of NFO file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Creation of NFO files is done by the NFO tags plugin. It, as any other plugin, can be
disabled:
.. code-block:: yaml
nfo_tags:
enabled: False
...prevent download of images
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The :ref:`config_reference/prebuilt_presets/tv_show:TV Show` presets by default
downloads images corresponding to show and each episode. This can be prevented by
overriding following variables:
.. code-block:: yaml
overrides:
tv_show_fanart_file_name: "" # to stop creation of fanart.jpg in subscription
tv_show_poster_file_name: "" # to stop creation of poster.jpg in subscription
thumbnail_name: "" # to stop creation of episode thumbnails
...use only part of the media's title
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ytdl-sub offers a range of functions that can be used to parse a subset of a title for
use in your media player. Consider the example:
* I want to remove "NOVA PBS - " from the title ``NOVA PBS - Hidden Cities All Around
Us``.
There are several solutions using ytdl-sub's scripting capabilities to override
``episode_title`` by manipulating the original media's ``title``.
.. code-block:: yaml
:caption: Replace exclusion with empty string
"~Nova PBS":
url: "https://www.youtube.com/@novapbs"
episode_title: >-
{
%replace( title, "NOVA PBS - ", "" )
}
.. code-block:: yaml
:caption: Split once using delimiter, grab last value in the split array.
"~Nova PBS":
url: "https://www.youtube.com/@novapbs"
episode_title: >-
{
%array_at( %split(title, " - ", 1), -1 )
}
.. code-block:: yaml
:caption:
Regex capture. Supports multiple capture strings and default values if captures
are unsuccessful.
"~Nova PBS":
url: "https://www.youtube.com/@novapbs"
captured_episode_title: >-
{
%regex_capture_many(
title,
[ "NOVA PBS - (.*)" ],
[ title ]
)
}
episode_title: >-
{ %array_at( captured_episode_title, 1 ) }
There is no single solution to this problem - it will vary case-by-case. See our full
suite of :ref:`scripting functions
<config_reference/scripting/scripting_functions:Scripting Functions>` to create your own
clever scraping mechanisms.
...force ytdl-sub to re-download a file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes users may wish to replace a file already in the archive, for example, if the
current file is a lower resolution than desired, missing subtitles, corrupt, etc..
``ytdl-sub`` decides what files have already been downloaded by entries in :ref:`the
download archive file <config_reference/plugins:output_options>`,
``./.ytdl-sub-...-download-archive.json``, at the top of the subscription/series/show
:ref:`output directory <config_reference/plugins:output_options>` in the appropriate
``overrides: / ..._directory:`` library path, *and* the presence of the corresponding
downloaded files under the same path. To force ``ytdl-sub`` to re-download an entry both
need to be removed:
- Move aside the downloaded files:
Rename or move the downloaded files, including the associated files with the same
base/stem name, such as ``./*.nfo``, ``./*.info-json``, etc..
- Ensure ``ytdl-sub`` is not running and won't run, such as by cron:
``ytdl-sub`` loads the ``./.ytdl-sub-...-download-archive.json`` file early, keeps it
in memory, and writes it back out late. If it's running or starts running while you're
modifying that file, then your changes will be overwritten when it exits.
- Remove the ``./.ytdl-sub-...-download-archive.json`` JSON array item:
Search for the stem name, the basename without any extension or suffix, common to all
the downloaded files in this file and delete that whole entry, from the YouTube ID
string to the closing curly braces. Be ware of JSON traling commas.
- Run ``$ ytdl-sub sub`` again with the appropriate CLI plugin options:
In normal operation, :ref:`yt-dlp minimizes requests and the files considered for
download <guides/getting_started/index:minimize the work to only what's
necessary>`. To re-download, those options must be disabled or modified. Disable
:ref:`the 'break_on_existing' option <config_reference/plugins:ytdl_options>`, set
:ref:`the 'date_range:' plugin <config_reference/plugins:date_range>`, and :ref:`limit
the subscriptions <guides/getting_started/downloading:preview>` to
download only the files that you've renamed in the steps above.
Set the appropriate dates, :ref:`including a sufficient margin
<config_reference/plugins:date_range>`, and subscription name to include only the
files you've renamed, and re-run. For example, if you've renamed all the files from
2024 in the ``NOVA PBS`` subscription:
.. code-block:: shell
ytdl-sub --match="NOVA PBS" sub -o "\
--ytdl_options.break_on_existing False \
--date_range.after 20240101 \
--date_range.before 20250101 \
"
...download a file missing from the archive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The root causes are unknown, but sometimes even after successful, complete runs, some
files will be missing from the archive. To attempt to download those missing files,
use `the same CLI options as re-downloading a file`_
.. _`the same CLI options as re-downloading a file`:
`...force ytdl-sub to re-download a file`_
...get support?
~~~~~~~~~~~~~~~
See :doc:`the debugging documentation <../debugging>`.
...reach out to contribute?
~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you would like to contribute, we're happy to accept any help, including from
non-coders! To find out how you can help this project, you can:
- `Join our Discord <https://discord.gg/v8j9RAHb4k>`_ and leave a comment in
#development with where you think you can assist or what skills you would like to
contribute.
- If you just want to fix one thing, you're welcome to :ytdl-sub-gh:`submit a pull
request <compare>` with information on what issue you're resolving and it will be
reviewed as soon as possible.
There is a bug where...
-----------------------
...ytdl-sub is not downloading
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...ytdl-sub is downloading at 360p or other lower quality
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...ytdl-sub downloads 2-4 videos and then fails
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These are often just limits imposed by the external services that are not bugs. There
may be little that can be done about them, but see :ref:`the '_throttle_protection'
preset <prebuilt_presets/helpers:_throttle_protection>` for more information.
...date_range is not downloading older videos after I changed the range
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Your preset most likely has ``break_on_existing`` set to True, which will stop
downloading additional metadata/videos if the video exists in your download archive. Set
the following in your config to skip downloading videos that exist instead of stopping
altogether.
.. code-block:: yaml
ytdl_options:
break_on_existing: False
After you download your new date_range duration, re-enable ``break_on_existing`` to
speed up successive downloads.
...it is downloading non-English title and description metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most likely the video has a non-English language set to its 'native' language. You can
tell yt-dlp to explicitly download English metadata using.
.. code-block:: yaml
ytdl_options:
extractor_args:
youtube:
lang:
- "en"
...Plex is not showing my TV shows correctly
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. Set the following for your ytdl-sub library that has been added to Plex.
.. figure:: ../../images/plex_scanner_agent.png
:alt:
The Plex library editor, under the advanced settings, showing the required options
for Plex to show the TV shows correctly.
- **Scanner:** Plex Series Scanner
- **Agent:** Personal Media shows
- **Visibility:** Exclude from home screen and global search
- **Episode sorting:** Library default
- **YES** Enable video preview thumbnails
2. Under **Settings** > **Agents**, confirm Plex Personal Media Shows/Movies scanner has
**Local Media Assets** enabled.
.. figure:: ../../images/plex_agent_sources.png
:alt:
The Plex Agents settings page has Local Media Assets enabled for Personal Media
Shows and Movies tabs.
...ytdl-sub errors when downloading a 360p video with resolution assert
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:ref:`See how to either ignore this specific video or disable resolution assertion entirely here. <resolution assert handling>`

View file

@ -0,0 +1,98 @@
Development and Contributing
============================
Requirements
------------
- python >= 3.10
- ffmpeg/ffprobe 4.4.5 (test checksums rely on this version)
- make
Local Install
-------------
.. tab-set-code::
.. code-block:: shell
pip install -e .[test,lint,docs]
.. code-block:: zsh
pip install -e .\[test,lint,docs\]
Linter
------
All source code contributed must be formatted to our linter specification. Run the
following to auto-format and check for any issues with your code:
.. code-block:: shell
make lint
Adding Documentation
--------------------
Docs can be found in ``ytdl-sub/docs/source/``, and are built using the command:
.. code-block:: shell
:caption:
Viewable at http://localhost:63342/ytdl-sub/docs/build/html/index.html once built
make docs
Some of the documentation is built using doc-strings from the python source code. The
above command will rebuild those as well.
Testing
-------
Tests are written using pytest. Many of them evaluate checksums of output files to
ensure no unintended changes are introduced to the way ``ytdl-sub`` produces files. This
checksum can be inaccurate for end-to-end tests, but are reliable for integration tests.
If integration tests are failing, ensure...
- you're using the correct ffmpeg version
- you are developing on Linux or Mac (have not tested windows yet)
- your local ``ytdl-sub`` dependencies are up-to-date
Docker
------
Test changes to the Docker image variants locally:
.. code-block:: shell
cd ./docker/testing/
make -j run
See ``./docker/testing/docker-compose.yml`` for the Compose services for each image
variant.
IDE Setup
---------
PyCharm is our preferred IDE. The codebase is simple enough to where it's not required,
but is highly recommended.
TODO: screenshots of configuration
Reproducing a Failing Subscription
----------------------------------
Subscriptions will dump their entire *compiled* yaml at the beginning of exeuction
:doc:`when using '--log-level debug' <../../debugging>`. This can be copy-pasted into
the file ``resources/file_fixtures/repro.yaml``.
Running the test ``e2e.test_debug_repro.TestReproduce.test_debug_log_repro`` will fully
reproduce that subscription in order to debug it.

View file

@ -0,0 +1,132 @@
Automating
==========
Automate downloading your subscriptions by running the :ref:`'sub' sub-command
<usage:subscriptions options>` periodically. There are various tools that can run
commands on a schedule you may use any of them that work with your installation
method. Most users use `cron`_ in `Docker containers <docker and unraid_>`_.
Docker and Unraid
-----------------
:doc:`The 'ytdl-sub' Docker container images <../install/docker>` provide optional cron
support. Enable cron support by setting `a cron schedule`_ in the ``CRON_SCHEDULE``
environment variable:
.. code-block:: yaml
:caption: ./compose.yaml
:emphasize-lines: 4
services:
ytdl-sub:
environment:
CRON_SCHEDULE: "0 */6 * * *"
# WARNING: See "Getting Started" -> "Automating" docs regarding throttles/bans:
# CRON_RUN_ON_START: false
Then recreate the container to apply the change and start it to generate the default
``/config/ytdl-sub-configs/cron`` script. Read the comments in that script and edit as
appropriate.
The container cron wrapper script will write output from the cron job to
``/config/ytdl-sub-configs/.cron.log``. The default image ``ENTRYPOINT`` will ``$ tail
...`` that file so you can monitor the cron job in the container's output and thus also
in the Docker logs.
You may also set the ``CRON_RUN_ON_START`` environment variable to ``true`` to have the
image run your cron script whenever the container starts in addition to the cron
schedule.
.. warning::
Using ``CRON_RUN_ON_START`` may cause your cron script to run too often and may
trigger throttles and bans. When enabled, your cron script will run *whenever* the
container starts including when the host reboots, when ``# dockerd`` restarts such as
when upgrading Docker itself, when a new image is pulled, when something applies
Compose changes, etc.. This may result in running ``ytdl-sub`` right before or after
the next cron scheduled run.
.. _linux-setup:
Linux, Mac OS X, BSD, or other UNIX's
-------------------------------------
For installations on systems already running ``# crond``, you can also use cron to run
``ytdl-sub`` periodically. Write a script to run ``ytdl-sub`` in the cron job. Be sure
the script changes to the same directory as your configuration and uses the full path to
``ytdl-sub``:
.. code-block:: shell
:caption: ~/.local/bin/ytdl-sub-cron
:emphasize-lines: 2,3
#!/bin/bash
cd "~/.config/ytdl-sub/"
~/.local/bin/ytdl-sub --dry-run sub -o '--ytdl_options.max_downloads 3' |&
tee -a "~/.local/state/ytdl-sub/.cron.log"
Then tell ``# crond`` when to run the script:
.. code-block:: console
echo "0 */6 * * * ${HOME}/.local/bin/ytdl-sub-cron" | crontab "-"
Remove the ``--dry-run`` and ``-o ...`` CLI options from your cron script when you've
tested your configuration and you're ready to download entries unattended.
.. _windows-setup:
Windows
-------
For most Windows users, the best way to run commands periodically is `the Task
Scheduler`_:
.. attention::
These instructions are untested. Use at your own risk. If you use them, whether they
work or not, please let us know how it went in `a support post in Discord`_ or `a new
GitHub issue`_.
#. Open the Task Scheduler app.
#. Click ``Create Basic Task`` at the top of the right sidebar.
#. Set all the fields as appropriate until you get to the ``Action``...
#. For the ``Action``, select ``Start a program``...
#. Click ``Browse...`` to the installed ``ytdl-sub.exe`` executable...
#. Add CLI arguments to ``Add arguments (optional):``, for example ``--dry-run sub -o
'--ytdl_options.max_downloads 3'``...
#. Set ``Start in (optional):`` to the directory containing your configuration.
#. Finish the rest of the ``Create Basic Task`` wizard.
Next Steps
----------
At this point, ``ytdl-sub`` should run periodically and keep your subscriptions current
in your media library without your intervention. As your :doc:`subscriptions file
<./subscriptions>` grows or you discover new use cases, it becomes worth while to
simplify things by :doc:`defining your own custom presets <./first_config>`.
.. _`cron`:
https://en.wikipedia.org/wiki/Cron
.. _`a cron schedule`:
https://crontab.cronhub.io/
.. _`the Task Scheduler`:
https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page
.. _`a support post in Discord`:
https://discord.com/channels/994270357957648404/1084886228266127460
.. _`a new GitHub issue`:
https://github.com/jmbannon/ytdl-sub/issues/new

View file

@ -0,0 +1,69 @@
Downloading
===========
Once you've :doc:`defined your subscriptions <./subscriptions>`, it's time to test your
configuration and try your first download. As a web scraping tool, :ref:`it's important
to minimize the requests sent to external services
<guides/getting_started/index:minimize the work to only what's necessary>` to avoid
triggering throttling or bans. Further, a full download of even one subscription can
take significant time. Test each change to your subscriptions carefully and quickly as
follows.
Preview
-------
Preview what ``ytdl-sub`` would do for this subscription. Run the :ref:`'sub'
sub-command <usage:subscriptions options>` with CLI options to restrict requests as much
as possible:
- Pull metadata and *simulate* a download without actually downloading any media files
using the ``--dry-run`` CLI option.
- Limit requests by narrowing the run to one subscription by giving a
subscription name to the ``--match`` CLI option.
- Stop after just a few downloads to further minimize requests and make testing faster
using the ``max_downloads`` setting from ``yt-dlp``.
Change to the directory containing your ``./subscriptions.yaml`` file and run with those
options:
.. code-block:: console
cd "/config/ytdl-sub-configs/"
ytdl-sub --dry-run --match="NOVA PBS" sub -o '--ytdl_options.max_downloads 3'
Examine the output carefully, investigate anything that doesn't look right and repeat
this step until everything looks right.
Review
------
Review the results of real downloads. Run it again without the ``--dry-run`` option to
actually download media and place the files in your library:
.. code-block:: console
ytdl-sub --match="NOVA PBS" sub -o '--ytdl_options.max_downloads 3'
Examine the output carefully again. Then examine how the resulting downloads work in
your library. Repeat with a larger value for ``max_downloads`` and examine the output
and downloads again.
Next Steps
----------
Once you're `previewed <preview_>`_ and `reviewed <review_>`_ successful downloads of
each of your subscriptions, you're ready to run a full download of all your
subscriptions. Run the sub-command without the CLI options you used to limit what
``ytdl-sub`` does while testing:
.. code-block:: console
ytdl-sub sub
If you're ready to let ``ytdl-sub`` run unattended, it's time to :doc:`automate
downloads <./automating_downloads>`.

View file

@ -0,0 +1,263 @@
Basic Configuration
===================
A configuration file serves two purposes:
1. Set application-level functionality that is not specifiable in a subscription file.
.. note::
ytdl-sub does not require a configuration file. However,
certain application settings may be desirable for tweak, such as setting
``working_directory`` to make ytdl-sub perform the initial download
to an SSD drive.
2. Create custom presets.
.. note::
In the prior Initial Subscription examples, we leveraged the prebuilt preset
``Jellyfin TV Show by Date``. This preset is entirely built using the same
YAML configuration system offered to users by using a configuration file.
The following section attempts to demystify and explain how to...
- Set an application setting
- Know whether or not custom presets are actually needed
- How to create a custom preset
- How to use a custom preset on subscriptions
-------------
how this works, and show-case how
.. code-block:: yaml
:linenos:
configuration:
working_directory: ".ytdl-sub-working-directory"
presets:
TV Show:
preset:
- "Jellyfin TV Show by Date"
- "Max 1080p"
embed_thumbnail: True
throttle_protection:
sleep_per_download_s:
min: 2.2
max: 10.8
sleep_per_subscription_s:
min: 9.0
max: 14.1
max_downloads_per_subscription:
min: 10
max: 36
overrides:
tv_show_directory: "/tv_shows"
TV Show Only Recent:
preset:
- "TV Show"
- "Only Recent"
Configuration Section
---------------------
The :ref:`configuration <config_reference/config_yaml:Configuration File>` section sets
options for ytdl-sub execution. Most users should set the path where ``ytdl-sub``
temporarily stores downloaded data before assembling it and moving it into your
library. To avoid unnecessarily long large file renames, use a path on the same
filesystem as your library in the ``overrides: / *_directory:`` paths:
.. code-block:: yaml
:lineno-start: 1
configuration:
working_directory: ".ytdl-sub-working-directory"
Preset Section
--------------
Underneath ``presets``, we define two custom presets with the names ``TV Show`` and ``TV
Show Only Recent``.
.. code-block:: yaml
presets:
TV Show:
...
TV Show Only Recent:
...
The indentation example above shows how to define multiple presets.
Custom Preset Definition
------------------------
Before we break down the above ``TV Show`` preset, lets first outline a preset layout:
.. code-block:: yaml
Preset Name:
preset:
...
plugin(s):
...
overrides:
...
Presets can contain three important things:
1. ``preset`` section, which can inherit :ref:`prebuilt presets
<config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` or other presets
defined in your config.
2. :ref:`Plugin definitions <config_reference/plugins:Plugins>`
3. :ref:`overrides <config_reference/plugins:overrides>`, which can override inherited
preset variables
Presets do not have to define all of these, as we'll see in the ``TV Show Only Recent``
preset.
Inheriting Presets
~~~~~~~~~~~~~~~~~~
.. code-block:: yaml
:lineno-start: 5
TV Show:
preset:
- "Jellyfin TV Show by Date"
- "Max 1080p"
The following snippet shows that the ``TV Show`` preset will inherit all properties of
the prebuilt presets ``Jellyfin TV Show by Date`` and ``Max 1080p`` in that order.
Order matters for preset inheritance. Bottom-most presets will override ones above them.
It is highly advisable to use :ref:`prebuilt presets
<config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` as a starting point
for custom preset building, as they do the work of preset building to ensure things show
as expected in their respective media players. Read on to see how to override prebuilt
preset specifics such as title.
Defining Plugins
~~~~~~~~~~~~~~~~
.. code-block:: yaml
:lineno-start: 10
embed_thumbnail: True
throttle_protection:
sleep_per_download_s:
min: 2.2
max: 10.8
sleep_per_subscription_s:
min: 9.0
max: 14.1
max_downloads_per_subscription:
min: 10
max: 36
Our ``TV Show`` sets two plugins, :ref:`throttle_protection
<config_reference/plugins:throttle_protection>` and :ref:`embed_thumbnail
<config_reference/plugins:embed_thumbnail>`. Each plugin's documentation shows the
respective fields that they support.
If an inherited preset defines the same plugin, the custom preset will use
'merge-and-append' strategy to combine their definitions. What this means is:
1. If the field is a map (i.e. has sub-params like ``sleep_per_download_s`` above) or
array, it will try to merge them
2. If both the inherited preset and custom preset set the same exact field and value
(i.e. ``embed_thumbnail``) the custom preset will overwrite it
Setting Override Variables
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: yaml
:lineno-start: 23
overrides:
tv_show_directory: "/ytdl_sub_tv_shows"
All override variables reside underneath the :ref:`overrides
<config_reference/plugins:overrides>` section.
It is important to remember that individual subscriptions can override specific override
variables. When defining variables in a preset, it is best practice to define them with
the intention that
1. All subscriptions will use its value them
2. Use them as placeholders to perform other logic, then have subscriptions or child
presets define their specific value
For simplicity, we'll focus on (1) for now. The above snippet sets the
``tv_show_directory`` variable to a file path. This variable name is specific to the
prebuilt TV show presets.
See the :ref:`prebuilt preset reference
<config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` to see all
available variables that are overridable.
Using Custom Presets in Subscriptions
--------------------------------------
Subscription files can use custom presets just like any other prebuilt preset. Below
shows a complete subscription file using the above two custom presets.
.. code-block:: yaml
TV Show:
= Documentaries:
"NOVA PBS": "https://www.youtube.com/@novapbs"
= Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
TV Show Only Recent:
= News:
"BBC News": "https://www.youtube.com/@BBCNews"
Notice how we do not need to define ``tv_show_directory`` in the ``__preset__`` section
like in prior examples. This is because our custom presets do the work of defining it.
Reference Custom Config in the CLI
----------------------------------
Be sure to tell ytdl-sub to use your config by using the argument ``--config
/path/to/config.yaml``.
If you run ytdl-sub in the same directory, and the config file is named ``config.yaml``,
it will use it by default.
Visualizing a subscription in Preset form
-----------------------------------------
Subscription file syntax is designed to minimize boiler-plate when authoring new subscriptions.
You can unpack any subscription using the ``inspect`` sub-command to see its boiler-plate *preset format*.
.. code-block:: bash
ytdl-sub inspect --match "BBC News" /path/to/subscriptions.yaml
This can be utilized for numerous purposes including:
* Ensuring your custom preset is getting applied correctly.
* Figuring out which variables set things like file names, metadata, etc.
* Understanding how subscription syntax translates to preset representation.
The default ``--level`` of inspect will fill in defined variables. Using ``--level original`` will
present the subscription's raw layout with no fill.

View file

@ -0,0 +1,157 @@
Getting Started
===============
Prerequisite Knowledge
----------------------
Using ``ytdl-sub`` requires some technical knowledge. You must be able to:
- do `basic CLI shell navigation`_
- read and write `YAML text files`_
If you plan on using a :ref:`Docker headless image variant
<guides/install/docker:headless image>` of ``ytdl-sub``, you can:
- use ``$ nano /config/...`` to edit configuration files inside the container
- or bind mount ``/config/`` as a Docker volume and use the editor of your choice from
the host
Soon, it's time to start configuring ``ytdl-sub``. We provide a :doc:`./quick_start`
with rigid, rote instructions on how to get a minimal configuration up and running, but
if that serves all your needs, then you're probably better off with :ref:`one of the
more user-friendly yt-dlp wrappers available <introduction:motivation>`. As a lower
level tool with no GUI, most ``ytdl-sub`` users will need to understand at least some of
how ``ytdl-sub`` works, how it "thinks". So before you start configuring ``ytdl-sub``,
`read on <architecture>`_ to learn how ``ytdl-sub`` works.
.. _`basic CLI shell navigation`:
https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Environment_setup/Command_line
.. _`YAML text files`: http://thomasloven.com/blog/2018/08/YAML-For-Nonprogrammers/
Architecture
------------
For most users, ``ytdl-sub`` works as follows:
Subscriptions use presets
~~~~~~~~~~~~~~~~~~~~~~~~~
Run ``$ ytdl-sub sub`` to read :doc:`a subscription file <./subscriptions>` that defines
what subscriptions to download and place into your media library. Each subscription
selects which :doc:`presets <../../prebuilt_presets/index>` to apply. Those presets
configure how each subscription is downloaded and placed in the media library.
Presets configure plugins
~~~~~~~~~~~~~~~~~~~~~~~~~
:doc:`A preset <../../prebuilt_presets/index>` is effectively a set of plugin
configurations. Specifically, a preset consists of:
- base presets that it inherits from and extends
- plugin configurations
When a preset has multiple base presets and more than one of those base presets
configures the same keys for a plugin, the later/lower base preset overrides the plugin
key configurations of earlier/higher base presets. Similarly, when the preset configures
the same keys for a plugin that one of its base plugins configures, the preset
configuration overrides the base presets.
Plugins do the work
~~~~~~~~~~~~~~~~~~~
``ytdl-sub`` applies the plugins that the presets configure when it downloads a
subscription. :doc:`The plugins <../../config_reference/plugins>` control how to run
``yt-dlp``, which media in the subscription to download, how to collect and format
metadata for those media, how to place the resulting files into your media library, and
more.
Presets and subscriptions accept overrides
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Presets accept override keys and values and the preset uses those overrides to modify
their plugin configurations. Similarly, individual subscriptions can supply overrides of
their presets for just that subscription.
Subscriptions are grouped by indentation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most subscriptions have more in common with each other than not. Thus, defining the
presets and overrides for each subscription would result in mostly repetition and would
multiply the burden of management for the user. The more subscriptions the more work.
To avoid this redundant work, and so that the subscription configurations describe the
intent of the user, subscriptions are nested/indented under parent/ancestor keys that
define their shared configuration. To support this, ``ytdl-sub`` uses special handling
of the ancestor YAML keys above each subscription. A subscription is the most
nested/indented/descendant key that specifies the URLs for that subscription. The
ancestor keys above that subscription describe the shared presets of that subscription
and all the other descendant subscriptions under them.
Genres are also more often shared between subscriptions than not. To accommodate that
reality, the ancestor keys of subscriptions may also use :ref:`the special '= ...'
prefix to pass specific overrides
<config_reference/scripting/static_variables:subscription_indent_i>` supported by the
preset. By convention in the pre-built media type presets, the first ``= ...`` value
specifies the genre for all descendant subscriptions.
Finally, ancestor keys may use :ref:`the '... | ...' special character
<config_reference/subscription_yaml:multi keys>` to combine multiple presets and/or
genres for the descendant subscriptions beneath.
The configuration file extends pre-defined presets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Users define additional presets in :doc:`their configuration file <./first_config>` that
they then use in most of their subscriptions. Most user-defined presets extend the
:doc:`../../prebuilt_presets/index` provided by ``ytdl-sub``.
Minimize the work to only what's necessary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Throttling and bans are a core problem for any web scraping tool, perhaps even more so
for ``yt-dlp``, and no good actor *wants* to be an onerous burden on a
service. Similarly, many web scraping use cases involve very large sets of data that are
too big to process as a whole for performance. It's important to narrow the amount of
data considered and minimize requests.
To these ends, most presets tell ``yt-dlp`` not to consider files before the most
recently downloaded file using :ref:`the 'break_on_existing' option
<config_reference/plugins:ytdl_options>`. Similarly, and particularly for huge channels
or playlists, most users should use either :ref:`an 'Only Recent' preset
<prebuilt_presets/helpers:only recent>` and/or :ref:`the 'Chunk Downloads' preset
<prebuilt_presets/helpers:chunk downloads>` to restrict the number of downloads
considered.
Caveats
~~~~~~~
Some of these descriptions are not technically complete. For example, a subscription may
use no preset at all and will just run ``yt-dlp`` without any customization or post
processing. The subscriptions file has special support for :ref:`overriding the presets
of all subscriptions in the file <config_reference/subscription_yaml:file preset>`. The
configuration file supports :ref:`a few special options
<config_reference/config_yaml:Configuration File>` that are not about defining presets. See
:doc:`the reference documentation <../../config_reference/index>` for technically
complete details, but for almost all of the use cases served by ``ytdl-sub``, the above
is accurate and representative.
Next Steps
----------
With ``ytdl-sub`` installed and the above understood, the next step is to :doc:`start
adding subscriptions <./subscriptions>`.
.. toctree::
:hidden:
subscriptions
downloading
automating_downloads
first_config
quick_start

View file

@ -0,0 +1,54 @@
Quick Start
===========
:ref:`Again <guides/getting_started/index:prerequisite knowledge>`, if the following
serves all your needs, then you're probably better off with :ref:`one of the more
user-friendly yt-dlp wrappers available <introduction:motivation>`. If you still want to
get ``ytdl-sub`` up and running quickly and without understanding, then follow these
instructions to the letter.
#. Install using :ref:`the official Docker GUI image variant <guides/install/docker:gui
image>`.
#. Update the paths for your media library:
Edit :ref:`the subscriptions file <guides/install/docker:configuration>`. Near the
top, under ``__preset__:`` and then ``overrides:``, update the values under the
``*_directory:`` keys with the correct paths for your media library *as they appear
inside the container*.
#. Select your media library software:
Change the ``Plex TV Show by Date:`` *key itself* to the preset for your media
library software. See the comment above for the available options.
#. Select the genre:
Under the library software preset key from the previous step, change the ``=
Documentaries`` *key itself* to the genre for this subscription prefixed with ``=
...``. When adding other subscriptions that have the same genre, place them under the
same key.
#. Update the subscription name and URL:
Under the genre key from the previous step, update the ``"NOVA PBS":`` key to the
directory name the downloaded files should be placed beneath. This directory will be
created under the ``tv_show_directory:`` from step #2. Then update the
``"https://www.youtube.com/@novapbs"`` value to the URL of the channel or playlist
for this subscription.
#. :ref:`Preview <guides/getting_started/downloading:preview>` and :ref:`Review
<guides/getting_started/downloading:review>` the subscription.
#. Add the rest of your subscriptions:
Repeat steps #3-6 for each of your subscriptions. Be sure to repeat the preview and
review steps for each subscription. In general, move slowly and carefully review
everything. It's best to catch issues early :ref:`to avoid repeating downloads and to
minimize requests <guides/getting_started/index:minimize the work to only what's
necessary>`.
#. Automate downloads:
:ref:`Set up ytdl-sub to run periodically
<guides/getting_started/automating_downloads:docker and unraid>`.

View file

@ -0,0 +1,167 @@
Subscriptions
=============
Once you understand :ref:`how ytdl-sub works
<guides/getting_started/index:architecture>`, it's time to start writing your
:doc:`../../config_reference/subscription_yaml`.
Media library paths
-------------------
Everyone's media library may use different paths so ``ytdl-sub`` can't provide
defaults. Tell ``ytdl-sub`` where to put your media using :ref:`overrides
<guides/getting_started/index:presets and subscriptions accept overrides>`:
.. code-block:: yaml
:caption: subscriptions.yaml
:emphasize-lines: 3-
__preset__:
overrides:
tv_show_directory: "/tv_shows"
music_directory: "/music"
music_video_directory: "/music_videos"
See the reference documentation for details about :ref:`the '__preset__:' special key
<config_reference/subscription_yaml:file preset>`.
Media library software and media types
--------------------------------------
Different media library software, such as `Jellyfin`_, `Kodi`_, Plex, or Emby, have
different requirements for where media files are placed, how those files are named, how
metadata is formatted, and more. Those software also have different requirements for
different types of media, such as shows/series, music, music videos, etc.. Use
:doc:`prebuilt presets <../../prebuilt_presets/index>` in :ref:`YAML keys
<guides/getting_started/index:subscriptions are grouped by indentation>` to tell
``ytdl-sub`` which media library software and media type to process downloaded files
for.
The actual subscription is defined in the lowest indentation level YAML keys. The
example below defines a subscription named ``NOVA PBS`` to archive downloads from the
entries in the ``https://www.youtube.com/@novapbs`` URL.
.. code-block:: yaml
:caption: subscriptions.yaml
:emphasize-lines: 1,4
Jellyfin TV Show by Date:
"NOVA PBS": "https://www.youtube.com/@novapbs"
Bandcamp:
"Emily Hopkins": "https://emilyharpist.bandcamp.com/"
.. _`Jellyfin`:
https://jellyfin.org/
.. _`Kodi`:
https://kodi.tv/
Which entries
-------------
The :doc:`helper presets <../../prebuilt_presets/helpers>` also provide support for
controlling which entries are downloaded and archived. These presets are intended to be
combined with the library software and media type presets.
Combine presets using :ref:`the '.. | ...' special character
<guides/getting_started/index:subscriptions are grouped by indentation>` in the YAML
keys:
.. code-block:: yaml
:caption: subscriptions.yaml
:emphasize-lines: 2,6
# Only download entries whose upload date is within the past 2 months:
Kodi TV Show by Date | Only Recent:
"NOVA PBS": "https://www.youtube.com/@novapbs"
# Only download 20 entries per run:
Soundcloud Discography | Chunk Downloads:
"UKNOWY": "https://soundcloud.com/uknowymunich"
What format, quality, or resolution
-----------------------------------
The :doc:`media quality presets <../../prebuilt_presets/media_quality>` provide support
for controlling which ``yt-dlp`` media "format" to download, such as ``1080p`` video
resolution or ``320k`` audio bitrate.
Users may also group and combine presets :ref:`using the YAML hierarchy
<guides/getting_started/index:subscriptions are grouped by indentation>`. Subscriptions
merge all the presets from their ancestor YAML keys. The hierarchy indentation depth may
be as deep as needed to group your subscriptions for easy maintenance:
.. code-block:: yaml
:caption: subscriptions.yaml
:emphasize-lines: 3,7,12
Jellyfin TV Show by Date | Only Recent:
# Download the highest resolution available:
Max Video Quality:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
# Download the highest resolution available that is 720p or less:
Max 720p:
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
Soundcloud Discography | Chunk Downloads:
# Only download audio using the Opus codec, not MP3 or other codecs:
Max Opus Quality:
"UKNOWY": "https://soundcloud.com/uknowymunich"
Genre and rating metadata
-------------------------
Presets may also support using arbitrary values from :ref:`YAML keys prefixed with '=
...' <guides/getting_started/index:subscriptions are grouped by indentation>`. The ``=
...`` prefix may be used at any indentation depth and may also be combined with presets
and other ``= ...`` values using the ``... | ...`` special character to best group your
subscriptions.
:ref:`By convention <config_reference/scripting/static_variables:subscription_indent_i>`
in the built-in library software and media type presets, the first ``= ...`` value
specifies the genre for all descendant subscriptions. For the ``TV Show ...`` presets,
the second ``= ...`` value specifies the rating for all descendant subscriptions:
.. code-block:: yaml
:caption: subscriptions.yaml
:emphasize-lines: 1,3
= Kids:
Jellyfin TV Show by Date | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
Soundcloud Discography:
"Foo Kids Band": "https://soundcloud.com/foo-kids-band"
Override variables for one subscription
---------------------------------------
Most variable overrides aren't actually specific to just one subscription and should be
set in :doc:`your own custom presets <./first_config>`. But use :ref:`the override mode
'~...' prefix <config_reference/subscription_yaml:override mode>` when an override is
specific to only one subscription and will never be shared with another:
.. code-block:: yaml
:caption: subscriptions.yaml
:emphasize-lines: 2-
Jellyfin TV Show by Date:
"~NOVA PBS":
url: "https://www.youtube.com/@novapbs"
tv_show_directory: "/media/Unique/Series/Path"
Next Steps
----------
Once you've defined your subscriptions, it's time to :doc:`test your configuration and
try your first download <./downloading>`.

View file

@ -0,0 +1,7 @@
Guides
======
.. toctree::
install/index
getting_started/index
development/index

View file

@ -0,0 +1,49 @@
====================
Environment Agnostic
====================
The PIP install method is not recommended; use of this method may cause unintended
requirement conflicts if you have other locally installed apps that depend on ffmpeg.
PIP Install
--------------
You can install our `PyPI package <https://pypi.org/project/ytdl-sub/>`_. Both ffmpeg
and Python 3.10 or greater are required.
.. code-block:: bash
python3 -m pip install -U ytdl-sub
Install for Development
=======================
These environment-agnostic methods of installing ``ytdl-sub`` are meant for local
development of ``ytdl-sub``. If you want to contribute your changes, please read
:doc:`/guides/development/index`.
Local Install
--------------
With a Python 3.10 virtual environment, you can clone and install the repo.
.. code-block:: bash
git clone https://github.com/jmbannon/ytdl-sub.git
cd ytdl-sub
pip install -e .
Local Docker Build
-------------------
Run ``make docker`` in the root directory of this repo to build the image. This will
build the python wheel and install it in the Dockerfile.
.. code-block:: bash
git clone https://github.com/jmbannon/ytdl-sub.git
cd ytdl-sub
make docker

View file

@ -0,0 +1,146 @@
======
Docker
======
The ``ytdl-sub`` Docker images use :lsio:`LSIO-based images <\ >` and install ytdl-sub
on top. There are two flavors or variants to choose from. For a more user-friendly
experience editing the `configuration`_, we recommend the `GUI image`_
variant. :ref:`Docker Compose <guides/install/docker:install with docker compose>` is
the recommended way of managing a ``ytdl-sub`` docker container. See :ref:`Automating
Downloads <guides/getting_started/automating_downloads:docker and unraid>` for how to
automate running ``ytdl-sub`` in a container running either variant.
GUI Image
---------
The GUI image is based on LSIO's :lsio-gh:`docker-code-server` to provide you full
management of ``ytdl-sub``, such as file editing and terminal access, all within your
browser using the VS Code web UI. See its documentation regarding environment variables
and other details. Once running, open `the web UI`_ to edit the `configuration`_ and run
``ytdl-sub``.
.. _`the web UI`: http://localhost:8443
Headless Image
--------------
The headless image is based on LSIO's :lsio-gh:`docker-baseimage-alpine`. Once running,
the default command just starts services including cron for :ref:`Automating Downloads
<guides/getting_started/automating_downloads:docker and unraid>` but otherwise doesn't
run ``ytdl-sub``. You may run arbitrary ``ytdl-sub`` commands using the
``--rm --user="${PUID}:${PGID}" --entrypoint="ytdl-sub"`` options to either ``$ docker
run`` or ``$ docker compose run``. Overriding the image's ``ENTRYPOINT`` is important so
that cron doesn't run ``ytdl-sub`` while you're running it manually.
For example::
$ docker compose run --rm --user="${PUID}:${PGID}" --entrypoint="ytdl-sub" ytdl-sub sub
.. note::
In `the recommended GUI image <gui image_>`_, the ``DEFAULT_WORKSPACE`` directory is
``/config/ytdl-sub-configs/`` which is used throughout the documentation and
examples. In the headless images, that directory is just ``/config/``, so substitute
that path if using a headless image.
Install with Docker Compose
---------------------------
Docker Compose provides a declarative way to configure and orchestrate containers which
makes them easier to manage and re-use. Create a ``compose.yaml`` file in your project
directory such as:
.. code-block:: yaml
:caption: compose.yaml
services:
ytdl-sub:
# The GUI image variant:
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
# Or use the headless image variant:
# image: ghcr.io/jmbannon/ytdl-sub:latest
# For CPU/GPU passthrough, use the GUI image above or the headless Ubuntu image:
# image: ghcr.io/jmbannon/ytdl-sub:ubuntu-latest
container_name: ytdl-sub
restart: unless-stopped
environment:
- TZ=America/Los_Angeles
# Set these as appropriate so your users can access the downloaded files in
# your library:
- PUID=1000
- PGID=1000
# Optionally passthrough your NVidia GPU:
# - NVIDIA_DRIVER_CAPABILITIES=all
# - NVIDIA_VISIBLE_DEVICES=all
volumes:
- <path/to/ytdl-sub/config>:/config
- <path/to/tv_shows>:/tv_shows # optional
- <path/to/movies>:/movies # optional
- <path/to/music_videos>:/music_videos # optional
- <path/to/music>:/music # optional
# Not necessary for the headless image variant:
ports:
- 8443:8443
# Optionally passthrough the CPU for hardware acceleration:
# devices:
# - /dev/dri:/dev/dri
# Optionally passthrough the GPU:
# deploy:
# resources:
# reservations:
# devices:
# - capabilities: ["gpu"]
Docker CLI
----------
You can run the container on an ad-hoc basis without Docker Compose using the Docker CLI
instead. It will not restart if stopped for any reason, including rebooting the
host. The following command is for the gui image:
.. code-block:: bash
docker run -d \
--name=ytdl-sub \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=America/Los_Angeles \
-p 8443:8443 \
-v <path/to/ytdl-sub/config>:/config \
-v <OPTIONAL/path/to/tv_shows>:/tv_shows \
-v <OPTIONAL/path/to/movies>:/movies \
-v <OPTIONAL/path/to/music_videos>:/music_videos \
-v <OPTIONAL/path/to/music>:/music \
ghcr.io/jmbannon/ytdl-sub-gui:latest
See `the Docker reference <https://docs.docker.com/engine/reference/run/>`_ for further
details.
Environment Variables
---------------------
``ytdl-sub`` docker images support the following environment variables.
.. csv-table:: Docker Environment Variables
:header: "Name", "Supported Values", "Description"
:widths: 15, 10, 60
"``PUID``", "integer", "User ID"
"``PGID``", "integer", "Group ID"
"``TZ``", "timezone", "Optional. Timezone to use in the logs. For supported values, see this `list <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List>`_. "
"``CRON_SCHEDULE``", "cron schedule `format <https://crontab.guru/#0_*/6_*_*_*>`_", "Optional. Schedule to run the ``cron`` file in ytdl-sub's container. More info :ref:`here <guides/getting_started/automating_downloads:docker and unraid>`."
"``CRON_RUN_ON_START``", "true/false", "Optional. Whether to run the cron script on container start."
"``UPDATE_YT_DLP_ON_START``", "stable/nightly/master", "Optional. Whether to update yt-dlp to the latest configured version on container start."
For the GUI image, you can set LSIO's underlying code-server `env variables <https://docs.linuxserver.io/images/docker-code-server/#environment-variables-e>`_ as well."
Configuration
-------------
In these examples, the configuration files will be at
``<path/to/ytdl-sub/config>/config.yaml`` and
``<path/to/ytdl-sub/config>/subscriptions.yaml``. Start the container the first time to
populate those files with default examples.

View file

@ -0,0 +1,35 @@
Install by Platform
===================
``ytdl-sub`` can be installed on the following platforms.
All installations require a 64-bit CPU. 32-bit is not supported.
.. margin::
.. tip::
The recommended install method of ``ytdl-sub`` is one of our :doc:`docker containers
</guides/install/docker>`.
:doc:`/guides/install/docker`
:doc:`/guides/install/unraid`
:doc:`/guides/install/linux`
:doc:`/guides/install/windows`
:doc:`/guides/install/agnostic`
Once you've completed your installation, please refer to the
:doc:`../getting_started/index` guide for next steps
.. toctree::
:hidden:
docker
linux
unraid
windows
agnostic

View file

@ -0,0 +1,52 @@
=====
Linux
=====
``ytdl-sub`` should be installable using any Linux package manager, and requires ffmpeg
to be installed.
.. tab-set::
.. tab-item:: Linux
.. code-block:: bash
curl -L -o ytdl-sub https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub
chmod +x ytdl-sub
./ytdl-sub -h
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to
date:
.. code-block:: bash
curl -L -o ffmpeg.tar.gz https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz
tar -xf ffmpeg.tar.gz
chmod +x ffmpeg-master-latest-linux64-gpl/bin/ffmpeg
chmod +x ffmpeg-master-latest-linux64-gpl/bin/ffprobe
# May need sudo / root permissions to perform
mv ffmpeg-master-latest-linux64-gpl/bin/ffmpeg /usr/bin/ffmpeg
mv ffmpeg-master-latest-linux64-gpl/bin/ffprobe /usr/bin/ffprobe
.. tab-item:: Linux ARM
.. code-block:: bash
curl -L -o ytdl-sub https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub_aarch64
chmod +x ytdl-sub
./ytdl-sub -h
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to
date:
.. code-block:: bash
curl -L -o ffmpeg.tar.gz https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz
tar -xf ffmpeg.tar.gz
chmod +x ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg
chmod +x ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe
# May need sudo / root permissions to perform
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffmpeg /usr/bin/ffmpeg
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe /usr/bin/ffprobe

View file

@ -0,0 +1,29 @@
======
Unraid
======
You can install our :unraid:`unraid community apps <community/apps?q=ytdl-sub#r>`
through the `Unraid Community Apps plugin <https://unraid.net/community/apps>`_.
If you installed the ``ytdl-sub-gui`` app, the code-server will be running at
http://localhost:8443 (replace ``localhost`` with the IP of the computer running Unraid
if you aren't trying to access ``ytdl-sub`` on that computer). Open this page in a
browser to access and interact with ``ytdl-sub``.
If you installed the ``ytdl-sub`` app (headless), open the normal app-specific console
to access and interact with ``ytdl-sub``. Once open, you must first run ``su abc -s
/bin/bash`` to change to the non-root user. You can confirm that this command worked by
running ``whoami`` and verifying that the result is ``abc``.
.. warning::
If you use the below option to access the ``ytdl-sub`` console, be sure to run ``su
abc -s /bin/bash`` first thing. You can confirm that this command worked by running
``whoami`` and verifying that the result is ``abc``. Do **NOT** run ``ytdl-sub`` as
the root user! Running as root will set the owner of all modified files to root,
which prevents most media managers and players from accessing the files.
.. figure:: ../../../images/unraid_badconsole.png
:alt:
The Unraid community app plugin GUI, with an arrow pointing at the "Console"
option in the dropdown after selecting ytdl-sub-gui

View file

@ -0,0 +1,17 @@
=======
Windows
=======
From powershell, run:
.. code-block:: powershell
# Download ffmpeg/ffprobe dependencies from yt-dlp
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
tar -xf ffmpeg.zip
move "ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe" "ffmpeg.exe"
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
# Download ytdl-sub
curl.exe -L -o ytdl-sub.exe https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub.exe
ytdl-sub.exe -h

15
docs/source/index.rst Normal file
View file

@ -0,0 +1,15 @@
ytdl-sub User Guide
===================
.. toctree::
:maxdepth: 2
:titlesonly:
introduction
guides/index
prebuilt_presets/index
usage
config_reference/index
debugging
faq/index
deprecation_notices

View file

@ -0,0 +1,98 @@
=================
What is ytdl-sub?
=================
.. _yt-dlp: https://github.com/yt-dlp/yt-dlp
.. _kodi: https://github.com/xbmc/xbmc
.. _jellyfin: https://github.com/jellyfin/jellyfin
.. _plex: https://github.com/plexinc/pms-docker
.. _emby: https://github.com/plexinc/pms-docker
``ytdl-sub`` is a command-line tool that builds on and orchestrates `yt-dlp`_ to
download media from YouTube and/or other online services. It provides a declarative,
expressive YAML configuration system that allows you to describe which media to download
and how it should appear in your media library servers and applications such as
`Jellyfin`_, `Plex`_, `Emby`_, `Kodi`_, modern music players, etc..
To these ends, ``ytdl-sub``:
- wraps and runs `yt-dlp`_, per your configuration to:
- download the media, remux and/or optionally transcode it
- prepares additional metadata both embedded and in external files
- renames the resulting files
- places them in your library
.. figure:: https://user-images.githubusercontent.com/10107080/182677243-b4184e51-9780-4094-bd40-ea4ff58555d0.PNG
:alt: The Jellyfin web interface, showing the thumbnails of various YouTube shows.
Youtube channels as TV shows in Jellyfin
.. figure:: https://user-images.githubusercontent.com/10107080/182677256-43aeb029-0c3f-4648-9fd2-352b9666b262.PNG
:alt: The Jellyfin web interace, showing the thumbnails of various music videos starring the Red Hot Chili Peppers
Music videos and concerts in Jellyfin
.. figure:: https://user-images.githubusercontent.com/10107080/182677268-d1bf2ff0-9b9c-4a04-98ec-443a67ada734.png
:alt: The Kodi app interface, showing a list of artists available to watch under the "Music videos" heading
Music videos and concerts in Kodi
.. figure:: https://user-images.githubusercontent.com/10107080/182685415-06adf477-3dd3-475d-bbcd-53b0152b9f0a.PNG
:alt: The MusicBee app interface, showing a list of album artists and the thumbnails of all downloaded songs produced by the currently selected artist
SoundCloud albums and singles in MusicBee
Motivation
----------
`yt-dlp`_ has grown into a well maintained, central repository of the intricate,
inscrutable, and extensive technical knowledge required to automate downloading media
from online services. When those services change their APIs or otherwise change
behavior, `yt-dlp`_ is the central, low-level tool to update. It does a best-in-class
job at that task, and it does that job more effectively by narrowing focus to just that.
As much knowledge as it encapsulates and as well as it does that, it still requires a
great deal of additional knowledge to make its output accessible to end-users. Mostly
this gap is about extracting and formatting metadata and correctly placing the resulting
output files in a media library.
A number of tools, applications, and other projects have grown up around that central
`yt-dlp`_ pillar to fill in those gaps, and this project was one of the early
entrants. Many are `full-featured services that provide web UIs`_ including some that
`provide media player web UIs`_. Most of those other projects necessarily narrow their
scope to provide a more polished and integrated user experience.
Similarly, ``ytdl-sub`` can run automatically to accomplish the same goals, but aims to
serve users that need lower-level control and/or have use cases not covered by the more
narrow scope of those other projects. To some degree, this makes this project
intrinsically less user friendly and requires more technical experience or learning.
Want something that "Just Works", try one of the other projects; we recommend
`Pinchflat`_ as the next step towards that end. Want to download from more than just
YouTube? Don't like the other restrictions inherent in the goals of those other
projects? Have unique use cases? Then dig in, learn, and we hope ``ytdl-sub`` gives you
enough rope and `a foot-gun`_ to get you there.
.. _`full-featured services that provide web UIs`:
https://github.com/kieraneglin/pinchflat
.. _`provide media player web UIs`:
https://www.tubearchivist.com/
.. _`Pinchflat`: `full-featured services that provide web UIs`_
.. _`a foot-gun`: https://en.wiktionary.org/wiki/footgun
Why download instead of stream?
-------------------------------
Most of the tools in this `yt-dlp`_ ecosystem serve a similar set of larger, more
general use cases, and so does ``ytdl-sub``:
- Don't rely on profit-driven corporate persons to keep more obscure content available.
- Even if they do, don't depend on them to make it possible to use it in different ways.
- Even when you pay, don't count on them not inserting ads later.
- Regardless, don't depend on them to curate content for yourself and/or your family.
- Free yourself and/or your family from what the algorithm would feed them next.

View file

@ -0,0 +1,237 @@
==============
Helper Presets
==============
.. hint::
See how to apply helper presets :doc:`here </prebuilt_presets/index>`
Only Recent
-----------
To only download a recent number of videos, apply the ``Only Recent`` preset. Once a
video's upload date is outside of the range, or you hit max files, older videos will be
deleted automatically.
.. code-block:: yaml
__preset__:
overrides:
# Set to a non-zero value to only keep this many files at once per sub
only_recent_max_files: 0
only_recent_date_range: "7days"
Plex TV Show by Date | Only Recent:
= Documentaries:
"NOVA PBS": "https://www.youtube.com/@novapbs"
To prevent deletion of files, use the preset ``Only Recent Archive`` instead.
Filter Keywords
---------------
``Filter Keywords`` can include or exclude media with any of the listed keywords. Both
keywords and title/description are lower-cased before filtering.
Default behavior for Keyword evaluation is ANY, meaning the filter will succeed if any
of the keywords are present. This can be set to ANY or ALL using the respective
``_eval`` variable.
Supports the following override variables:
* ``title_include_keywords``, ``title_include_eval``
* ``title_exclude_keywords``, ``title_exclude_eval``
* ``description_include_keywords``, ``title_exclude_eval``
* ``description_exclude_keywords``, ``title_exclude_eval``
.. tip::
Use the `~` tilda subscription mode to set a subscription's list override variables.
Tilda mode allows override variables to be set directly underneath it.
.. code-block:: yaml
Plex TV Show by Date | Filter Keywords:
= Documentaries:
"~NOVA PBS":
url: "https://www.youtube.com/@novapbs"
title_exclude_keywords:
- "preview"
- "trailer"
"~To Catch a Smuggler":
url: "https://www.youtube.com/@NatGeo"
title_include_keywords:
- "To Catch a Smuggler"
= Sports:
"~Maple Leafs Highlights":
url: "https://www.youtube.com/@NHL"
title_include_eval: "ALL"
title_include_keywords:
- "maple leafs"
- "highlights"
Filter Duration
---------------
``Filter Duration`` can include or exclude media based on its duration.
Supports the following override variables:
* ``filter_duration_min_s``
* ``filter_duration_max_s``
.. tip::
Use the `~` tilda subscription mode to set a subscription's list override variables.
Tilda mode allows override variables to be set directly underneath it.
.. code-block:: yaml
Plex TV Show by Date | Filter Duration:
= Documentaries:
"~NOVA PBS":
url: "https://www.youtube.com/@novapbs"
filter_duration_min_s: 120 # Only download videos at least 2m long
= Sports:
"~Maple Leafs Highlights":
url: "https://www.youtube.com/@NHL"
filter_duration_max_s: 180 # Only get highlight videos less than 3m long
Chunk Downloads
---------------
If you are archiving a large channel, ``ytdl-sub`` will try pulling each video's
metadata from newest to oldest before starting any downloads. It is a long process and
not ideal. A better method is to chunk the process by using the following preset:
``Chunk Downloads``
It will download videos starting from the oldest one, and only download 20 at a time by
default. You can change this number by setting the override variable
``chunk_max_downloads``.
.. code-block:: yaml
__preset__:
overrides:
chunk_max_downloads: 20
Plex TV Show by Date:
# Chunk these ones
= Documentaries | Chunk Downloads:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
# But not these ones
= Documentaries:
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
Once the entire channel is downloaded, remove the usage of this preset. It will then
pull metadata from newest to oldest again, and stop once it reaches a video that has
already been downloaded.
_throttle_protection
--------------------
.. note::
This preset is already a base preset of those higher-level presets that require it,
so users seldom need to use it directly, for example, unless they're writing presets
from scratch.
This preset is primarily a sensible default configuration of :ref:`the
'throttle_protection' plugin <config_reference/plugins:throttle_protection>` along with
an override to disable the plugin:
.. code-block:: yaml
overrides:
# Disable throttle protection:
enable_throttle_protection: false
In addition to throttling by denying download requests, some services also throttle
downloads by only allowing downloads of the lowest resolution quality. At the time of
writing, only YouTube does this by allowing only 360p downloads when throttled. To work
around this kind of throttling, this preset includes :ref:`an assertion
<config_reference/scripting/scripting_functions:error functions>` that will stop
downloading when ``ytdl-sub`` downloads a video at 360p or lower. It supports the
following overrides:
.. code-block:: yaml
overrides:
# Disable resolution quality throttle protection:
enable_resolution_assert: false
# Change the resolution below which to assume downloading is throttled:
resolution_assert_height_gte: 720
.. _resolution assert handling:
Handling Low Quality Videos
~~~~~~~~~~~~~~~~~~~~~~~~~~~
A side effect from throttle protection's resolution assert is, if the only resolution available is 360p or lower, it will
error. You can either disable resolution assert entirely (see above), or ignore specific titles in the subscription
using the ``resolution_assert_ignore_titles`` variable. Add a subset of the title (case-sensitive) as a list entry
to your subscription, like so:
.. code-block:: yaml
# use tilda mode to set override variables to the subscription
"~My Subscription":
url: "https://youtube.com/@channel"
resolution_assert_ignore_titles:
- "This 360p Video Title"
_url
----
All prebuilt presets share the same internal ``_multi_url`` preset which comes equipped with
a few available customizations.
Sibling Metadata
~~~~~~~~~~~~~~~~
*Sibling* refers to any entry within the same *playlist*. For channel downloads, this would
imply **every** video that gets downloaded since yt-dlp treats the channel as the *playlist*.
Setting the variable ``include_sibling_metadata`` will include all sibling metadata within
each individual entry's metadata. This is used specifically for music presets. When downloading
a playlist as an album for example, it will take the max year amongst all the other sibling's metadata
to have a consistent album year that can be used in file or directory naming.
Webpage URL
~~~~~~~~~~~
``ytdl-sub`` performs downloads in two stages.
1. Metadata scrape from the original URL
2. Individual entry downloads
For step 2, ``ytdl-sub`` will use the ``webpage_url`` variable by default for the input URL to yt-dlp.
This can be modified in case it's not working as expected by using the variable ``modified_webpage_url``.
Example:
.. code-block:: yaml
:caption:
Removes yt-dlp smuggle data from the URL
overrides:
modified_webpage_url: >-
{ %regex_sub("#__youtubedl_smuggle=.*", "", webpage_url) }

View file

@ -0,0 +1,37 @@
================
Prebuilt Presets
================
``ytdl-sub`` offers a number of built-in presets using best practices for formatting
media in various players.
.. hint::
Apply multiple presets to your subscriptions using pipes. Pipes can define multiple
presets and values on the same line to apply to all subscriptions nested below them.
.. code-block:: yaml
:caption:
Applies Max Video Quality preset to all TV shows, and Chunk Downloads preset to
some
Plex TV Show by Date | Max Video Quality:
= Documentaries | Chunk Downloads:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
= Documentaries:
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
For advanced users, you can review the prebuilt preset definitions :doc:`here
</config_reference/prebuilt_presets/index>`.
.. toctree::
:titlesonly:
tv_shows
music
music_videos
media_quality
helpers

View file

@ -0,0 +1,33 @@
======================
Media Quality Presets
======================
.. hint::
See how to apply media quality presets :doc:`here </prebuilt_presets/index>`
Video
-----
The following presets set video quality specifications to yt-dlp.
- ``Max Video Quality``
- ``Max 2160p``
- ``Max 1440p``
- ``Max 1080p``
- ``Max 720p``
- ``Max 480p``
Audio
-----
The following presets set audio quality specifications to yt-dlp. These assume you are
only extracting audio (no video).
- ``Max Audio Quality``, format is determined by the source
- ``Max MP3 Quality``
- ``Max Opus Quality``
- ``MP3 320k``
- ``MP3 128k``

View file

@ -0,0 +1,90 @@
=============
Music Presets
=============
Music downloadable by yt-dlp comes in many flavors. ``ytdl-sub`` offers a suite of
various presets for handling some of the most popular forms of uploaded music content.
YouTube Releases
----------------
Many artists, especially those auto-uploaded as ``Topics`` in YouTube have a section on
their channel named "Releases", or "Albums and Singles". The ``YouTube Releases`` preset
aims to scrape this *playlist of playlists*.
Playlists are recognized as the album, and videos within it are tracks.
.. code-block:: yaml
YouTube Releases:
= Jazz: # Sets genre tag to "Jazz"
"Thelonious Monk": "https://www.youtube.com/@officialtheloniousmonk/releases"
If you are only interested in a subset of albums, you can provide their playlists as
separate values in the form of an array, like so:
.. code-block:: yaml
YouTube Releases:
= Jazz:
"Thelonious Monk":
- "https://www.youtube.com/playlist?list=OLAK5uy_lcqINwfzkw73TPnAt6MlpB6V0gM9VzQu8" # Monk on Monk
- "https://www.youtube.com/playlist?list=OLAK5uy_nhuvjuZOO3yLIWCbQzbiWfyzkGapSIuYw" # Late Night Thelonious Monk
YouTube Full Albums
-------------------
In many cases, albums are uploaded to YouTube as a single video, where each track as
separated by either chapters or timestamps in a description. The ``YouTube Full Albums``
preset will take each video and split it by the chapters to form an album.
Videos are recognized as the album, and chapters within it are tracks.
.. code-block:: yaml
YouTube Full Albums:
= Lofi:
"Game Chops": "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i"
If you are only interested in a subset of albums, you can provide their video as
separate values in the form of an array, like so:
.. code-block:: yaml
YouTube Full Albums:
= Lofi:
"Game Chops":
- "https://www.youtube.com/watch?v=m7vBrD7LMLI" # Zelda & Sleep Ensemble Collection
- "https://www.youtube.com/watch?v=w0XebCwSpKI" # Study Buddy ~ video game lofi mix
Soundcloud Discography
----------------------
SoundCloud tracks can be uploaded as either a single, part of an album, or a
collaboration with another artist. At this time, ``SoundCloud Discography`` only scrapes
singles and albums. It will attempt to group tracks into albums before falling back to
single format.
.. code-block:: yaml
SoundCloud Discography:
= Chill Hop:
"UKNOWY": "https://soundcloud.com/uknowymunich"
= Synthwave:
"Lazerdiscs Records": "https://soundcloud.com/lazerdiscsrecords"
"Earmake": "https://soundcloud.com/earmake"
Bandcamp
--------
Bandcamp albums and singles can be scraped using the ``Bandcamp`` preset.
.. code-block:: yaml
Bandcamp:
= Lofi:
"Emily Hopkins": "https://emilyharpist.bandcamp.com/"

View file

@ -0,0 +1,5 @@
===================
Music Video Presets
===================
WIP

View file

@ -0,0 +1,255 @@
===============
TV Show Presets
===============
Player-Specific Presets
-----------------------
``ytdl-sub`` provides player-specific versions of certain presets, which apply settings
to optimize the downloads for that player.
The following actions are taken based on the indicated player:
Kodi
~~~~
* Everything that the Jellyfin version does
* Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with
````
Jellyfin
~~~~~~~~
* Places any season-specific poster art in the main show folder
* Generates NFO tags
Emby
~~~~
* Places any season-specific poster art in the main show folder
* Generates NFO tags
* For named seasons, creates a ``season.nfo`` file per season
Plex
~~~~~~~~
* :ref:`Special sanitization
<config_reference/scripting/entry_variables:title_sanitized_plex>` of numbers so Plex
doesn't recognize numbers that are part of the title as the episode number
* Converts all downloaded videos to the mp4 format
* Places any season-specific poster art into the season folder
----------------------------------------------
TV Show by Date
---------------
TV Show by Date will organize something like a YouTube channel or playlist into a tv
show, where seasons and episodes are organized using upload date.
Example
~~~~~~~
Must define ``tv_show_directory``. Available presets:
* ``Kodi TV Show by Date``
* ``Jellyfin TV Show by Date``
* ``Emby TV Show by Date``
* ``Plex TV Show by Date``
.. code-block:: yaml
__preset__:
overrides:
tv_show_directory: "/tv_shows"
Plex TV Show by Date:
# Sets genre tag to "Documentaries"
= Documentaries:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
# Sets genre tag to "Kids", "TV-Y" for content rating
= Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
= Music:
# TV show subscriptions can support multiple urls and store in the same TV Show
"Rick Beato":
- "https://www.youtube.com/@RickBeato"
- "https://www.youtube.com/@rickbeato240"
Advanced Usage
~~~~~~~~~~~~~~
If you prefer a different season/episode organization method, you can set the following
override variables.
.. code-block:: yaml
__preset__:
overrides:
tv_show_directory: "/tv_shows"
tv_show_by_date_season_ordering: "upload-year-month"
tv_show_by_date_episode_ordering: "upload-day"
Or for a specific preset
.. code-block:: yaml
"~Kids Toys Play":
url: "https://www.youtube.com/@KidsToysPlayChannel"
tv_show_by_date_season_ordering: "upload-year-month"
tv_show_by_date_episode_ordering: "upload-day"
The following are supported. Be sure the combined season + episode ordering include the
year, month, day, i.e. upload-year + upload-month-day.
Season Ordering
"""""""""""""""
``tv_show_by_date_season_ordering`` supports one of the following:
* ``upload-year`` (default)
* ``upload-year-month``
* ``release-year``
* ``release-year-month``
Episode Ordering
""""""""""""""""
``tv_show_by_date_episode_ordering`` supports one of the following:
* ``upload-month-day`` (default)
* ``upload-month-day-reversed``
* Reversed means more recent episodes appear at the top of a season by having a lower
value.
* ``upload-day``
* ``release-day``
* ``release-month-day``
* ``release-month-day-reversed``
* ``download-index``
* Episodes are numbered by the download order. **NOTE**: this is fetched using the
length of the download archive. Do not use if you intend to remove old videos.
TV Show by Date presets use the following for defaults:
.. code-block:: yaml
tv_show_by_date_season_ordering: "upload-year"
tv_show_by_date_episode_ordering: "upload-month-day"
TV Show Collection
------------------
TV Show Collections set each URL as its own season. If a video belongs to multiple URLs
(i.e. a channel and a channel's playlist), the video will only download once and reside
in the higher-numbered season.
Two main use cases of a collection are:
1. Organize a YouTube channel TV show where Season 1 contains any video not in a
'season playlist', Season 2 for 'Playlist A', Season 3 for 'Playlist B', etc.
2. Organize one or more YouTube channels/playlists, where each season represents a
separate channel/playlist.
Today, ytdl-supports up to 40 seasons with 11 URLs per season.
Example
~~~~~~~
Must define ``tv_show_directory``. Available presets:
* ``Kodi TV Show Collection``
* ``Jellyfin TV Show Collection``
* ``Emby TV Show Collection``
* ``Plex TV Show Collection``
.. code-block:: yaml
__preset__:
overrides:
tv_show_directory: "/tv_shows"
Plex TV Show Collection:
= Music:
# Prefix with ~ to set specific override variables
"~Beyond the Guitar":
s01_name: "Videos"
s01_url: "https://www.youtube.com/c/BeyondTheGuitar"
s02_name: "Covers"
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
Other notable features include:
* TV show poster info is pulled from the first URL in s01.
* Duplicate videos in different URLs (channel /videos vs playlist) will not download twice.
* The video will attributed to the season with the highest number.
* Individual seasons support both single and multi URL.
* s00 is supported for specials.
.. code-block:: yaml
"~Beyond the Guitar":
s00_name: "Specials"
s00_url:
- "https://www.youtube.com/watch?v=vXzguOdulAI"
- "https://www.youtube.com/watch?v=IGwYDvaGAz0"
s01_name: "Videos"
s01_url:
- "https://www.youtube.com/c/BeyondTheGuitar"
- "https://www.youtube.com/@BeyondTheGuitarAcademy"
s02_name: "Covers"
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
Advanced Usage
~~~~~~~~~~~~~~
If you prefer a different episode organization method, you can set the following
override variables.
.. code-block:: yaml
__preset__:
overrides:
tv_show_directory: "/tv_shows"
tv_show_collection_episode_ordering: "release-year-month-day"
Or for a specific preset
.. code-block:: yaml
"~Beyond the Guitar":
tv_show_collection_episode_ordering: "release-year-month-day"
s01_name: "Videos"
s01_url: "https://www.youtube.com/c/BeyondTheGuitar"
s02_name: "Covers"
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
The following are supported.
Episode Ordering
""""""""""""""""
``tv_show_collection_episode_ordering`` supports one of the following:
* ``upload-year-month-day`` (default)
* ``upload-year-month-day-reversed``
* ``release-year-month-day``
* ``release-year-month-day-reversed``
* ``playlist-index``
* Only use ``playlist-index`` episode formatting for playlists that will be fully
downloaded once and never again. Otherwise, indices can change.
* ``playlist-index-reversed``
TV Show Collection presets use upload-year-month-day as the default.

View file

@ -0,0 +1,3 @@
sphinx-book-theme==1.1.0
sphinx-copybutton==0.5.2
sphinx-design==0.5.0

146
docs/source/usage.rst Normal file
View file

@ -0,0 +1,146 @@
Usage
=====
.. code-block::
ytdl-sub [GENERAL OPTIONS] {sub,dl,view} [COMMAND OPTIONS]
For Windows users, it would be ``ytdl-sub.exe``
General Options
---------------
CLI options common to all sub-commands. Must be specified before the sub-command, for
example ``$ ytdl-sub --dry-run sub ...``:
.. code-block:: text
-h, --help show this help message and exit
-v, --version show program's version number and exit
-c CONFIGPATH, --config CONFIGPATH
path to the config yaml, uses config.yaml if not provided
-d, --dry-run preview what a download would output, does not perform any video downloads or writes to output directories
-l quiet|info|verbose|debug, --log-level quiet|info|verbose|debug
level of logs to print to console, defaults to verbose
-t TRANSACTIONPATH, --transaction-log TRANSACTIONPATH
path to store the transaction log output of all files added, modified, deleted
-st, --suppress-transaction-log
do not output transaction logs to console or file
-nc, --suppress-colors
do not use colors in ytdl-sub output
-m MATCH [MATCH ...], --match MATCH [MATCH ...]
match subscription names to one or more substrings, and only run those subscriptions
Subscriptions Options
---------------------
Download all subscriptions specified in each :doc:`subscriptions file
<./guides/getting_started/subscriptions>`.
.. code-block::
ytdl-sub [GENERAL OPTIONS] sub [SUBPATH ...]
``SUBPATH`` is one or more paths to subscription files and defaults to
``./subscriptions.yaml`` if none are given. It will use the config specified by
``--config``, or ``./config.yaml``, if not provided.
.. code-block:: text
:caption: Additional Options
-u, --update-with-info-json
update all subscriptions with the current config using info.json files
-o DL_OVERRIDE, --dl-override DL_OVERRIDE
override all subscription config values using `dl` syntax, i.e. --dl-override='--ytdl_options.max_downloads 3'
Download Options
----------------
Download a single subscription in the form of CLI arguments instead of from :doc:`a
subscriptions file <./guides/getting_started/subscriptions>`:
.. code-block::
ytdl-sub [GENERAL OPTIONS] dl [SUBSCRIPTION ARGUMENTS]
``SUBSCRIPTION ARGUMENTS`` are the same as YAML arguments, but use periods (``.``)
instead of indents. For example, you can represent this subscription:
.. code-block:: yaml
rick_a:
preset:
- "tv_show"
overrides:
tv_show_name: "Rick A"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
Using the command:
.. code-block:: bash
ytdl-sub dl \
--preset "tv_show" \
--overrides.tv_show_name "Rick A" \
--overrides.url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
See how to shorten commands using :ref:`download aliases <config_reference/config_yaml:dl_aliases>`.
View Options
------------
Preview the source variables for a given URL. Helpful to create new subscriptions:
.. code-block::
ytdl-sub view [-sc] [URL]
.. code-block:: text
:caption: Additional Options
-sc, --split-chapters
View source variables after splitting by chapters
CLI to SUB Options
------------------
Convert yt-dlp cli arguments to ytdl-sub `ytdl_options` arguments.
.. code-block::
ytdl-sub cli-to-sub [YT-DLP ARGS]
Inspect
-------
Inspect a single subscription's underlying preset representation.
This can be utilized for numerous purposes including:
* Ensuring your custom preset is getting applied correctly.
* Figuring out which variables set things like file names, metadata, etc.
* Understanding how subscription syntax translates to preset representation.
Usage:
.. code-block:: bash
ytdl-sub inspect --match "Game Chops" --mock 'title=Lets Play' examples/music_subscriptions.yaml
.. code-block:: text
:caption: Additional Options
-l 0,1,2,3, --level 0,1,2,3
level of inspection to perform:
0 - original present the subscription as-is
1 - fill fill in defined values
2 - resolve resolve all possible variables (default)
3 - internal resolve all variables to their internal representation
-m MATCH [MATCH ...], --match MATCH [MATCH ...]
match subscription names to one or more substrings, and only run those subscriptions
-o DL_OVERRIDE, --dl-override DL_OVERRIDE
override all subscription config values using `dl` syntax, i.e. --dl-override='--ytdl_options.max_downloads 3'
-k VAR=VALUE, --mock VAR=VALUE
ability to mock one or more variable values, i.e. --mock 'title=Lets Play'

View file

@ -1,84 +0,0 @@
Usage
=======
.. code-block::
ytdl-sub [GENERAL OPTIONS] {sub,dl,view} [COMMAND OPTIONS]
For Windows users, it would be ``ytdl-sub.exe``
General Options
---------------
General options must be specified before the command (i.e. ``sub``).
.. code-block:: text
-h, --help show this help message and exit
-v, --version show program's version number and exit
-c CONFIGPATH, --config CONFIGPATH
path to the config yaml, uses config.yaml if not provided
-d, --dry-run preview what a download would output, does not perform any video downloads or writes to output directories
-l quiet|info|verbose|debug, --log-level quiet|info|verbose|debug
level of logs to print to console, defaults to info
-t TRANSACTIONPATH, --transaction-log TRANSACTIONPATH
path to store the transaction log output of all files added, modified, deleted
-st, --suppress-transaction-log
do not output transaction logs to console or file
Sub Options
-----------
Download all subscriptions specified in each ``SUBPATH``.
.. code-block::
ytdl-sub [GENERAL OPTIONS] sub [SUBPATH ...]
``SUBPATH`` is one or more paths to subscription files, uses ``subscriptions.yaml`` if not provided.
It will use the config specified by ``--config``, or ``config.yaml`` if not provided.
Download Options
-----------------
Download a single subscription in the form of CLI arguments.
.. code-block::
ytdl-sub [GENERAL OPTIONS] dl [SUBSCRIPTION ARGUMENTS]
``SUBSCRIPTION ARGUMENTS`` are exactly the same as YAML arguments, but use periods (``.``) instead
of indents for specifying YAML from the CLI. For example, you can represent this subscription:
.. code-block:: yaml
rick_a:
preset:
- "tv_show"
overrides:
tv_show_name: "Rick A"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
Using the command:
.. code-block:: bash
ytdl-sub dl \
--preset "tv_show" \
--overrides.tv_show_name "Rick A" \
--overrides.url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
See how to shorten commands using
`download aliases <https://ytdl-sub.readthedocs.io/en/latest/config.html#ytdl_sub.config.config_validator.ConfigOptions.dl_aliases>`_.
View Options
-----------------
.. code-block::
ytdl-sub view [-sc] [URL]
.. code-block:: text
-sc, --split-chapters
View source variables after splitting by chapters
Preview the source variables for a given URL. Helps when creating new configs.

View file

@ -1,7 +1,13 @@
# Example Configurations
This directory shows how you can use ytdl-sub for various use cases. These
are the configs I personally use and have incorporated as part of the e2e tests.
This directory shows how use ytdl-sub's built-in presets to start downloading immediately with no
configuration required. Simply run:
Each example has a `config.yaml` and `subscription.yaml`. The config defines
_how_ you format your media, whereas the subscription defines _what_ you
download plus some additional configuring if needed.
### Unix
```commandline
ytdl-sub sub tv_show_subscriptions.yaml
```
### Windows
```commandline
./ytdl-sub.exe sub tv_show_subscriptions.yaml
```

View file

@ -0,0 +1,101 @@
###############################################################################
# Top-level configurations to apply umask and write log files
configuration:
umask: "002"
persist_logs:
logs_directory: '/config/logs'
keep_successful_logs: False
presets:
###############################################################################
# Set tv_show_directory here instead of in the subscriptions file
tv_show_paths:
overrides:
tv_show_directory: "/tv_shows"
###############################################################################
# Filter out any YouTube shorts
no_shorts:
match_filters:
filters:
- "original_url!*=/shorts/"
###############################################################################
# Remove all the following sponsorblock sections
sponsorblock:
chapters:
sponsorblock_categories:
- "outro"
- "selfpromo"
- "preview"
- "interaction"
- "sponsor"
- "music_offtopic"
- "intro"
remove_sponsorblock_categories: "all"
force_key_frames: False
###############################################################################
# Wait 2 days before downloading in hopes to get more accurate sponsorblock
sponsorblock_wait:
# Import the sponsorblock preset defined above
preset:
- "sponsorblock"
date_range:
before: "today-2days"
###############################################################################
# base preset to use on all TV Show-based subscriptions
base:
preset:
- "Kodi TV Show by Date" # Set intended player
- "best_video_quality" # prebuilt preset to get best quality
- "tv_show_paths"
# Embed chapters into video files
chapters:
embed_chapters: True
# Embed English subtitles into video files (supports more)
subtitles:
embed_subtitles: True
languages:
- "en"
allow_auto_generated_subtitles: True
# ytdl_options lets you pass any arg into yt-dlp's Python API
ytdl_options:
# Set the cookie file
# cookiefile: "/config/ytdl-sub-configs/youtube_cookies.txt"
# For YouTube, get English metadata if multiple languages are present
extractor_args:
youtube:
lang:
- "en"
###############################################################################
# Custom preset to archive an entire channel
TV Show Full Archive:
preset:
- "base"
- "sponsorblock_wait" # wait for sponsorblock when full-archiving
###############################################################################
# Custom preset to only fetch and keep recent videos.
# Format the videos in reverse order, so the first video is the most recent
# Also include the prebuilt "Only Recent" preset
TV Show Only Recent:
preset:
- "base"
- "sponsorblock"
- "no_shorts"
- "season_by_year__episode_by_month_day_reversed"
- "Only Recent"
overrides:
only_recent_date_range: "2months"
only_recent_max_files: 30

View file

@ -0,0 +1,29 @@
# Subscriptions using custom presets made in `tv_show_config.yaml`
TV Show Full Archive:
= Documentaries | = TV-PG:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
= Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
= Gardening | = TV-Y:
# TV Show presets support multiple URLs as a list
"Gardening with Ciscoe":
- "https://www.youtube.com/@gardeningwithciscoe4430"
- "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_"
- "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn"
- "https://www.youtube.com/watch?v=2vq-vPubS5I"
TV Show Only Recent:
= News | = TV-14:
# Subscriptions can prefix a tilda to specify override variables
# to set only for that subscriptions
"~BBC News":
url: "https://www.youtube.com/@BBCNews" # use url2, url3, ... for multi-url in this form
only_recent_date_range: "2weeks"
"Frontline PBS": "https://www.youtube.com/@frontline"
"Whitehouse": "https://www.bitchute.com/channel/zWsYVmCOu4JA/" # Supports non-YT sites

View file

@ -1,194 +0,0 @@
# This config builds presets to download and format the following use-cases:
# - YouTube
# - Songs
# - From a single video
# - Albums
# - From a video, where each song is represented by chapters
# - From a playlist where each video is a song on the album
# - Discographies
# - From a channel or a channel's set of playlists
# - Bandcamp
# - Track, Album, Artist URL as a Discography
# - Soundcloud
# - Track, Album, Artist URL as a Discography
#
# All files will look like:
#
# music_directory/
# Artist/
# [2022] Some Single/
# 01 - Some Single.mp3
# folder.jpg
# [2023] Latest Album/
# 01 - Track Title.mp3
# 02 - Another Track.mp3
# folder.jpg
#
# The idea is to format files under music_directory as `Artist/Album/Song`. Singles that do not
# belong to an album will be represented like albums with a single track.
#
# Each file will be properly tagged regardless of file extension (suppers flac, ogg, mp3, etc)
# and should show up nicely in music players that take advantage of tags.
configuration:
working_directory: '.ytdl-sub-downloads'
umask: "002"
presets:
# The `base` preset that represents how all files will be structured after downloading.
# Every other preset afterwards will inherit this preset.
base:
# Store all music under our music_directory (to be set as an override variable).
# Store each resulting file with its full track path, and treat thumbnails as the album covers.
#
# Maintain a download archive. This will produce a hidden file in your music directory
# containing an archive of all audio previously downloaded. This makes it so we do not
# re-download files we already have.
output_options:
output_directory: "{music_directory}"
file_name: "{track_full_path}"
thumbnail_name: "{album_cover_path}"
maintain_download_archive: True
# Set break_on_existing to True. This will stop fetching any more metadata once we
# hit a video/audio that we already have downloaded.
ytdl_options:
break_on_existing: True
# Extract any audio from files using this codec and quality.
audio_extract:
codec: "mp3"
quality: 320
# Set these music tags on every resulting audio file.
# It is recommended to keep most of this as-is, and use override
# variables to set them to be what you want.
music_tags:
artist: "{track_artist}"
artists: "{track_artist}"
albumartist: "{track_artist}"
albumartists: "{track_artist}"
title: "{track_title}"
album: "{track_album}"
track: "{track_number}"
tracktotal: "{track_total}"
year: "{track_year}"
genre: "{track_genre}"
# Optionally embed the thumbnail into the track
embed_thumbnail: False
# For every configurable field, make it an override variable,
# so we can carefully tune different use-cases by only modifying override variables.
overrides:
# Track Overrides. By default, set overrides to make each song its own album
track_title: "{title}"
track_album: "{title}"
track_artist: "{channel}"
track_number: "1"
track_number_padded: "01"
track_total: "1"
track_year: "{upload_year}"
track_genre: "Unset"
# Filename Overrides
track_file_name: "{track_number_padded} - {track_title_sanitized}.{ext}"
album_file_name: "folder.{thumbnail_ext}"
# Directory Name Overrides
music_directory: "OVERRIDE THIS WITH YOUR MUSIC DIRECTORY"
artist_dir: "{track_artist_sanitized}"
album_dir: "[{track_year}] {track_album_sanitized}"
# Full Filepath Overrides
track_full_path: "{artist_dir}/{album_dir}/{track_file_name}"
album_cover_path: "{artist_dir}/{album_dir}/{album_file_name}"
####################################################################################################
# Make the 'single` preset accept a single URL using the override variable 'url'
# Each audio file will reside in its own album.
single:
# Inherit from `base`
preset: "base"
download:
url: "{url}"
####################################################################################################
# Make the 'albums_from_playlists' preset format audio files to reside under albums, where
# each album is a playlist. If a file downloaded using this preset is not part of a playlist,
# it will default to how it'd look as a `single`.
albums_from_playlists:
# Inherit from single
preset: "single"
# Override various track properties using playlist variables.
overrides:
track_album: "{playlist_title}"
track_number: "{playlist_index}"
track_number_padded: "{playlist_index_padded}"
track_total: "{playlist_count}"
track_year: "{playlist_max_upload_year}"
####################################################################################################
# Make the 'albums_from_chapters' preset format audio files to reside under an album, where the
# video itself is an album containing all the songs in it represented by chapters.
albums_from_chapters:
# Inherit from single
preset: "single"
# Embed chapters if present. If no chapters are present, allow parsing comments that contain
# timestamps to each song, and use those as chapters.
chapters:
embed_chapters: True
allow_chapters_from_comments: True
# Split each file by its chapters. If a file does not have chapters, simply 'pass' on it and
# process the next audio/video file.
split_by_chapters:
when_no_chapters: "pass"
# Override various track properties using chapter variables.
overrides:
track_title: "{chapter_title}" # Chapter title is the track title
track_album: "{title}" # Video's title is the album title
track_number: "{chapter_index}"
track_number_padded: "{chapter_index_padded}"
track_total: "{chapter_count}"
####################################################################################################
# Make the 'soundcloud_discography' preset specially made for ripping SoundCloud artists.
# We will use the 'multi_url' approach to download both albums and non-album tracks
soundcloud_discography:
preset: "base"
# Download using the multi_url strategy
download:
# The first URL will be all the artist's tracks.
# Treat these as singles - an album with a single track
- url: "{sc_artist_url}/tracks"
variables:
sc_track_album: "{title}"
sc_track_number: "1"
sc_track_number_padded: "01"
sc_track_total: "1"
sc_track_year: "{upload_year}"
# Set the second URL to the artist's albums. If a track belongs to both
# to an album and tracks (in the URL above), it will resolve to this
# URL and include the album metadata we set below.
- url: "{sc_artist_url}/albums"
variables:
sc_track_album: "{playlist_title}"
sc_track_number: "{playlist_index}"
sc_track_number_padded: "{playlist_index_padded}"
sc_track_total: "{playlist_count}"
sc_track_year: "{playlist_max_upload_year}"
# Override various track properties using playlist variables.
overrides:
track_album: "{sc_track_album}"
track_number: "{sc_track_number}"
track_number_padded: "{sc_track_number_padded}"
track_total: "{sc_track_total}"
track_year: "{sc_track_year}"

View file

@ -1,137 +0,0 @@
# Define any number of subscriptions in a single file, or use multiple
# files to organize (i.e by use-case, website, genre, etc)
#
# Each example show-cases every use case:
# - YouTube
# - Songs
# - From a single video
# - Albums
# - From a video, where each song is represented by chapters
# - From a playlist where each video is a song on the album
# - Discographies
# - From a channel or a channel's set of playlists
# - Bandcamp
# - Track, Album, Artist URL as a Discography
# - Soundcloud
# - Track, Album, Artist URL as a Discography
#
####################################################################################################
# YOUTUBE - PLAYLIST OF ALBUMS
# See https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i
#
# Downloads the GameChops Albums' playlist. Each video in the playlist is an album with
# chapters representing songs.
game_chops:
preset: "albums_from_chapters"
# Perform regex to extract title from the chapter's title to not include the track number.
# Configuring this hard is a bit overkill, but I'm a perfectionist :-)
regex:
from:
chapter_title:
match:
- "^\\d+\\.\\.(.*)" # Captures 'title' from '1..title'
capture_group_names:
- "captured_track_title"
capture_group_defaults:
- "{chapter_title}"
# Explicitly set the URL, track artist, and genre.
# Override various track properties using the regex variables we extracted.
overrides:
url: "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i"
track_artist: "GameChops"
track_genre: "Lofi"
track_title: "{captured_track_title}"
####################################################################################################
# YOUTUBE - MANY ALBUMS FROM MANY ARTISTS UPLOADED BY A CHANNEL
# See https://www.youtube.com/@heavymetalofeasternbloc
#
# Downloads the entire 'Heavy Metal from the Eastern Bloc' channel. Each video they upload
# is an album from Eastern Europe with chapters representing songs.
eastern_bloc:
# Inherit albums from chapters
preset: "albums_from_chapters"
# Perform regex on many fields (title, description, chapter title) to extract as much
# metadata as possible
regex:
skip_if_match_fails: False # Error if regex match fails
from:
title:
match:
- "^(.*) - (.*) \\|\\|.*" # Captures artist, album from 'artist - album ||...'
- "^(.*) - (.*) \\[.*" # Captures artist, ablum from 'artist - album [...'
- "^(.*) - (.*)" # Captures artist, ablum from 'artist - album'
capture_group_names:
- "captured_track_artist"
- "captured_track_album"
description:
match:
- "Genre:\\s*(.*)\\s*\n(?:Rec.+|Year):\\s*.*(\\d{4})\\s*\\n" # Captures genre and recorded year
capture_group_names:
- "captured_track_genre"
- "captured_track_year"
chapter_title:
match:
- "^(?:\\d+\\.\\s*|)(.*)" # Captures title from '1. title'
capture_group_names:
- "captured_track_title"
capture_group_defaults:
- "{chapter_title}"
# Explicitly set the URL.
# Override various track properties using the regex variables we extracted.
overrides:
url: "https://www.youtube.com/@heavymetalofeasternbloc"
track_artist: "{captured_track_artist}"
track_album: "{captured_track_album}"
track_title: "{captured_track_title}"
track_year: "{captured_track_year}"
track_genre: "Eastern Bloc {captured_track_genre}"
####################################################################################################
# BANDCAMP - DISCOGRAPHY
# See https://emilyharpist.bandcamp.com/
#
# Downloads Emily's entire bandcamp discography. yt-dlp represents albums as playlist.
emily_hopkins:
preset: "albums_from_playlists"
regex:
from:
title:
match:
- "^Emily Hopkins - (.*)" # Captures 'title' from 'Emily Hopkins - title'
capture_group_names:
- "captured_track_title"
capture_group_defaults:
- "{title}"
# Explicitly set the URL, track artist, and genre.
# Override various track properties using the regex variables we extracted.
overrides:
url: "https://emilyharpist.bandcamp.com/"
track_artist: "Emily Hopkins"
track_genre: "Lofi"
track_title: "{captured_track_title}"
####################################################################################################
# SOUNDCLOUD - DISCOGRAPHY
# See https://soundcloud.com/jessebannon
#
# Downloads my acoustic 'album' and various tracks from SoundCloud using the soundcloud
# discography preset. I'm not very good so keep your expectations low :-)
jmbannon:
# Inherit soundcloud_discography preset
preset: "soundcloud_discography"
# Explicitly set the SoundCloud artist url, track artist, and genre.
overrides:
sc_artist_url: "sc_artist_url"
track_artist: "jmbannon"
track_genre: "Acoustic"

View file

@ -0,0 +1,52 @@
# Files will be stored in the form of:
#
# music_directory/
# Artist/
# [2022] Some Single/
# 01 - Some Single.mp3
# folder.jpg
# [2023] Latest Album/
# 01 - Track Title.mp3
# 02 - Another Track.mp3
# folder.jpg
# Override variables globally for all subscriptions
__preset__:
overrides:
music_directory: "/music"
# Supports downloading YouTube /releases tab. Also works for any playlist (or playlist of playlists)
# where each video is a single track.
YouTube Releases:
= Jazz: # Sets genre tag to "Jazz"
"Lester Young": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists"
"Thelonious Monk": "https://www.youtube.com/@theloniousmonk3870/releases"
"Stan Getz": "https://www.youtube.com/@stangetzofficial/releases"
"Art Blakey": "https://www.youtube.com/channel/UCMki-b0zfAQiMQ0nbsrIuBQ/playlists"
# Supports downloading playlists or individual videos where a single video is a full album.
# ytdl-sub will split the album based on video chapters, and make each chapter a track.
YouTube Full Albums:
= Lofi:
"Game Chops": "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i"
# Supports downloading a SoundCloud's artists albums + singles. Be sure to not include
# any extension after the SoundCloud artist's name in the URL.
SoundCloud Discography:
= Chill Hop:
"UKNOWY": "https://soundcloud.com/uknowymunich"
= Electronic:
"Italo Brutalo": "https://soundcloud.com/italobrutalo"
"SURVIVE": "https://soundcloud.com/s-u-r-v-i-v-e"
"French79": "https://soundcloud.com/french79music"
"VHS Dreams": "https://soundcloud.com/vhsdreamsofficial"
= Synthwave:
"Lazerdiscs Records": "https://soundcloud.com/lazerdiscsrecords"
"Earmake": "https://soundcloud.com/earmake"
"Poly Poly": "https://soundcloud.com/poly_poly"
# Supports downloading a Bandcamp artist
Bandcamp:
= Lofi:
"Emily Hopkins": "https://emilyharpist.bandcamp.com/"

View file

@ -0,0 +1,45 @@
# Files will be stored in the form of:
#
# music_videos/
# Elton John/
# Elton John - Rocketman.jpg
# Elton John - Rocketman.mp4
# System of a Down/
# System of a Down - Chop Suey.jpg
# System of a Down - Chop Suey.mp4
# ...
# Override variables globally for all subscriptions
__preset__:
overrides:
music_video_directory: "/music_videos"
# Choose between Jellyfin/Kodi/Plex Music Videos preset:
# - Plex Music Videos:
# - Jellyfin Music Videos:
# - Kodi Music Videos:
"Plex Music Videos":
= Pop: # Sets genre tag to "Pop"
"Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
"Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
= Blues:
"Eric Clapton": "https://www.youtube.com/playlist?list=PLABGggHhsbEeaRtdzqnxYoEINsJE_4GF4"
= Rock:
# Prefixing with '+' puts the subscription into 'map-mode'.
# Music video presets in map-mode support grouping videos into different
# categories, which get set on the album field.
#
# URLs can either be strings, or maps that can overload title, year, date
"+ Guns N' Roses":
Music Videos:
- "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ"
Concerts:
- title: "Live at The Ritz - New York City"
year: "1988"
url: "https://www.youtube.com/watch?v=OldpIhHPsbs"
- title: "Live at The Hollywood Bowl"
date: "2023-01-11"
url: "https://www.youtube.com/watch?v=Z7hutGlvq9I"

View file

@ -1,65 +0,0 @@
# This example shows how to download and format a music video OR playlist
# of music videos to display in Kodi as a music video. The format will
# look like:
#
# /path/to/Music Videos
# Elton John/
# Elton John - Rocketman-thumb.jpg
# Elton John - Rocketman.mp4
# Elton John - Rocketman.nfo
# System of a Down/
# System of a Down - Chop Suey-thumb.jpg
# System of a Down - Chop Suey.mp4
# System of a Down - Chop Suey.nfo
# ...
#
configuration:
working_directory: '.ytdl-sub-downloads'
presets:
music_video:
# We will only use a single URL to download music video(s).
# Make {url} an override variable to set later.
download:
- "{url}"
# For advanced YTDL users only; any YTDL parameter can be set here.
# To download age-restricted videos, you will need to set your cookie
# file here as a ytdl parameter. For more info, see
# https://ytdl-sub.readthedocs.io/en/latest/faq.html#download-age-restricted-youtube-videos
ytdl_options:
# cookiefile: "path/to/cookie_file.txt
break_on_existing: True # Stop downloads if it already exists
# For each video downloaded, set the file and thumbnail name here.
# We set both with {music_video_name}, which is a variable we define in
# the overrides section further below to represent consistent naming format.
output_options:
output_directory: "{music_video_directory}"
file_name: "{music_video_name}.{ext}"
thumbnail_name: "{music_video_name}-thumb.jpg"
info_json_name: "{music_video_name}.{info_json_ext}"
maintain_download_archive: True
# For each video downloaded, add a music video NFO file for it. Populate it
# with tags that Kodi will read and use to display it in the music or music
# videos section.
nfo_tags:
nfo_name: "{music_video_name}.nfo"
nfo_root: "musicvideo"
tags:
artist: "{artist}"
title: "{title}"
album: "Music Videos"
year: "{upload_year}"
# Overrides is a section where we can define our own variables, and use them in
# any other section. We define our music video directory and episode file name
# here, which gets reused above for the video, thumbnail, and NFO file.
# Recommended to override the artist variable since {channel} is not always
# the artist's exact name.
overrides:
music_video_directory: "path/to/Music Videos"
music_video_name: "{artist_sanitized}/{artist_sanitized} - {title_sanitized}"
artist: "{channel}"

View file

@ -1,24 +0,0 @@
###############################################################################
# DOWNLOAD MUSIC VIDEO PLAYLIST
john_smith:
preset: "music_video"
overrides:
url: "https://youtube.com/playlist?list=UCsvn_Po0SmunchJYtttWpOxMg"
artist: "John Smith and the Instrument Players"
###############################################################################
# DOWNLOAD SINGLE MUSIC VIDEO VIA CLI
# It is not always ideal to download all of an artist's music videos.
# Maybe you only like one song of theirs. We can reuse our preset
# to download a single video instead.
#
# Of course, defining yaml configuration to download a single video once
# and never again seems weird. Instead, we can perform this download via
# command-line:
#
# ytdl-sub dl \
# --preset "music_video" \
# --overrides.url "https://youtube.com/watch?v=QhY6r6oAErg" \
# --overrides.artist "John Smith and the Instrument Players"
#

View file

@ -1,127 +0,0 @@
# This config uses prebuilt presets included with ytdl-sub to download and format
# channels from YouTube or other sites supported by yt-dlp into a TV show for
# your favorite player. The directory format will look something like
#
# /tv_shows
# /Season 2021
# s2021.e0317 - Pattys Day Video-thumb.jpg
# s2021.e0317 - Pattys Day Video.mp4
# s2021.e0317 - Pattys Day Video.nfo
# /Season 2022
# s2022.e1225 - Merry Christmas-thumb.jpg
# s2022.e1225 - Merry Christmas.mp4
# s2022.e1225 - Merry Christmas.nfo
# poster.jpg
# fanart.jpg
# tvshow.nfo
#
# The idea is to use dates as numerics to represent season and episode numbers.
configuration:
working_directory: '.ytdl-sub-downloads'
presets:
# Your main TV show preset - all your tv show subscriptions will use this.
tv_show:
preset:
# Choose one of the following player types:
# - "kodi_tv_show_by_date"
# - "jellyfin_tv_show_by_date"
# - "plex_tv_show_by_date"
- "kodi_tv_show_by_date" # replace with desired player type
# Choose one of the following season/episode formats:
# - "season_by_year__episode_by_month_day"
# - "season_by_year_month__episode_by_day"
# - "season_by_year__episode_by_month_day_reversed"
# - "season_by_year__episode_by_download_index"
- "season_by_year__episode_by_month_day" # replace with desired season/episode format
# Include any of the presets listed below in your 'main preset' if you want
# it applied to every TV show. Or, use them on the individual subscriptions.
# - "only_recent_videos"
# - "add_subtitles"
# - "sponsorblock"
# - "include_info_json"
# To download age-restricted videos, you will need to uncomment and set your cookie
# file here as a ytdl parameter. For more info, see
# https://ytdl-sub.readthedocs.io/en/latest/faq.html#download-age-restricted-youtube-videos
#
# ytdl_options:
# cookiefile: "/config/cookie_file.txt" # replace with actual cookie file path
overrides:
tv_show_directory: "/tv_shows" # replace with path to tv show directory
# Fields in the prebuilt preset that can be changed:
#
# episode_title: "{upload_date_standardized} - {title}"
# episode_plot: "{webpage_url}" # source variable for the video description is {description}
####################################################################################################
# Preset to only download and keep recent videos
only_recent_videos:
# Only download videos within the download_range
date_range:
after: "today-{download_range}"
# Stops fetching metadata if the video is out of range (saves time + bandwidth)
ytdl_options:
break_on_reject: True
# Deletes any videos older than download_range. WARNING: do not use
# "season_by_year__episode_by_download_index" if you plan to delete older videos
output_options:
keep_files_after: "today-{download_range}"
# Set the duration of download_range, defaults to 2 months
overrides:
download_range: "2months"
####################################################################################################
# Preset to download subtitles (either by file or embedded)
add_subtitles:
subtitles:
# Embed subtitles into the video
embed_subtitles: True
# And/or download them as a file. Uncomment to download as file:
# subtitles_name: "{episode_file_path}.{lang}.{subtitles_ext}"
# subtitles_type: "srt"
languages: "en" # supports list of multiple languages
allow_auto_generated_subtitles: True # allow auto subtitles
####################################################################################################
# Preset to cut sponsor segments from videos
sponsorblock:
# If you download using cron, it is wise to add a delay before downloading ad-filled content to
# give folks time to submit sponsor segments. Uncomment to wait 2 days before download a video.
# date_range:
# before: "today-2days"
chapters:
# Remove all of these sponsorblock categories
sponsorblock_categories:
- "intro"
- "outro"
- "selfpromo"
- "preview"
- "interaction"
- "sponsor"
- "music_offtopic"
remove_sponsorblock_categories: "all"
force_key_frames: False
####################################################################################################
# Preset for the hoarders who want to also save the info.json file
include_info_json:
output_options:
info_json_name: "{episode_file_path}.{info_json_ext}"

View file

@ -1,16 +1,77 @@
# Define any number subscriptions in a single file, or use multiple
# files to organize (i.e by genre)
rick_a:
# Mix and match any number of presets to use. You can either lump all settings into
# a single preset to save some lines, or modularize them and use many.
preset:
- "tv_show"
- "only_recent_videos"
# This example downloads the entirety of a channel (not limited to YouTube).
# Files will be stored in the form of:
#
# /tv_shows
# /Season 2021
# s2021.e031701 - Pattys Day Video-thumb.jpg
# s2021.e031701 - Pattys Day Video.mp4
# s2021.e031701 - Pattys Day Video.nfo
# s2021.e031702 - Second Pattys Day Video-thumb.jpg
# s2021.e031702 - Second Pattys Day Video.mp4
# s2021.e031702 - Second Pattys Day Video.nfo
# /Season 2022
# s2022.e122501 - Merry Christmas-thumb.jpg
# s2022.e122501 - Merry Christmas.mp4
# s2022.e122501 - Merry Christmas.nfo
# poster.jpg
# fanart.jpg
# tvshow.nfo
#
# The idea is to use dates as numerics to represent season and episode numbers.
# Overrides to the prebuilt presets
__preset__:
overrides:
# Required to specify for every subscription
tv_show_name: "Rick A"
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
# Optional
tv_show_genre: "Music"
tv_show_directory: "/tv_shows" # Root folder of all ytdl-sub TV Shows
# For 'Only Recent' preset, only keep vids within this range and limit
only_recent_date_range: "2months"
only_recent_max_files: 30
# Choose the player you intend to use by setting the top-level key to be either:
# - Plex TV Show by Date:
# - Jellyfin TV Show by Date:
# - Emby TV Show by Date:
# - Kodi TV Show by Date:
Plex TV Show by Date:
# Sets genre tag to "Documentaries"
= Documentaries:
"NOVA PBS": "https://www.youtube.com/@novapbs"
"National Geographic": "https://www.youtube.com/@NatGeo"
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
# Sets genre tag to "Kids", "TV-Y" for content rating
= Kids | = TV-Y:
"Jake Trains": "https://www.youtube.com/@JakeTrains"
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
# Sets genre tag to "Music"
= Music:
# Subscriptions can support multiple urls and store in the same
# TV Show
"Rick Beato":
- "https://www.youtube.com/@RickBeato"
- "https://www.youtube.com/@rickbeato240"
# Set "News" for genre, use `Only Recent` preset to only store videos uploaded recently
= News | Only Recent:
"BBC News": "https://www.youtube.com/@BBCNews"
# Sets URLs to be explicit seasons. If a video resides in multiple URLs, it will
# only appear once in the higher-numbered season. This is how you can separate a channel's
# videos and playlists you are interested in.
#
# Choose the player you intend to use by setting the top-level key to be either:
# - Plex TV Show Collection:
# - Jellyfin TV Show Collection:
# - Emby TV Show Collection:
# - Kodi TV Show Collection:
Plex TV Show Collection:
= Music:
"~Beyond the Guitar":
s01_name: "Videos"
s01_url: "https://www.youtube.com/c/BeyondTheGuitar"
s02_name: "Music Videos"
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"

View file

@ -1,10 +1,69 @@
[tool.isort]
profile = "black"
line_length = 100
force_single_line = true
[project]
name ="ytdl-sub"
dynamic = [ "version" ]
authors = [ { name = "Jesse Bannon" } ]
description = "Automate downloading metadata generation with YoutubeDL"
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
classifiers = [
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Multimedia :: Video",
"License :: Public Domain",
"Environment :: Console",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"yt-dlp[default]==2026.6.9",
"colorama~=0.4",
"mergedeep~=1.3",
"mediafile~=0.12",
"PyYAML~=6.0",
]
urls = { Homepage = "https://github.com/jmbannon/ytdl-sub" }
[tool.black]
line_length = 100
[build-system]
requires = [ "setuptools >= 67.0" ]
build-backend = "setuptools.build_meta"
[tool.setuptools]
platforms = [ "Unix" ]
[tool.setuptools.dynamic]
version = { attr = "ytdl_sub.__pypi_version__" }
[tool.setuptools.package-dir]
"" = "src"
[tool.setuptools.package-data]
"*" = ["*.yaml"]
[tool.setuptools.packages.find]
where = ["src"]
[project.optional-dependencies]
test = [
"coverage[toml]>=6.3,<8.0",
"pytest>=7.2,<10.0",
"pytest-rerunfailures>=14,<17",
]
lint = [
"pylint==4.0.5",
"ruff==0.15.16",
]
docs = [
"sphinx>=7,<10",
"sphinx-rtd-theme>=2,<4",
"sphinx-book-theme~=1.0",
"sphinx-copybutton~=0.5",
"sphinx_design~=0.6",
]
build = [
"build~=1.2",
"twine>=5,<7",
"pyinstaller~=6.5",
]
[project.scripts]
ytdl-sub = "ytdl_sub.main:main"
[tool.pylint.MASTER]
disable = [
@ -15,30 +74,43 @@ disable = [
"R0913", # Too many arguments
"R0901", # too-many-ancestors
"R0902", # too-many-instance-attributes
"R1711", # useless-return
"R0917", # too many positional arguments
"W0511", # TODO
]
load-plugins = "pylint.extensions.docparams"
[tool.pydocstyle]
inherit = false
match = "[^test_].*\\.py"
ignore = [
"D100", # docstring in public module
"D101", # Missing docstring in public class (covered by pylint)
"D104", # docstring in public package
"D107", # docstring in init
"D200", # One-line should fit on one line
"D203", # 1 blank line before class docstring
"D205", # 1 blank line between summary and description
"D212", # Multi-line should start at first line
"D400", # Should end with a period
"D401", # Return vs Returns
"D413", # Missing blank line after last section
"D415", # Should end with a period
]
[tool.coverage.run]
include = [
"src/*"
]
[tool.coverage.report]
exclude_also = [
"raise UNREACHABLE.*",
]
# ruff
[tool.ruff]
line-length = 100
indent-width = 4
# Assume Python 3.10
target-version = "py310"
[tool.ruff.lint]
extend-select = ["I"]
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"

View file

@ -1,59 +0,0 @@
[metadata]
name = ytdl-sub
version = attr:ytdl_sub.__pypi_version__
author = Jesse Bannon
description = Automate downloading and metadata generation with YoutubeDL
long_description = file: README.md
long_description_content_type= text/markdown
author_email = use_github_issues@nope.com
url = https://github.com/jmbannon/ytdl-sub
license = GNUv3
platforms = Unix
classifiers =
Topic :: Multimedia :: Sound/Audio
Topic :: Multimedia :: Video
License :: Public Domain
Environment :: Console
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[options.entry_points]
console_scripts =
ytdl-sub = ytdl_sub.main:main
[options]
package_dir =
= src
packages=find:
install_requires =
yt-dlp==2023.7.6
argparse==1.4.0
colorama==0.4.6
mergedeep==1.3.4
mediafile==0.12.0
PyYAML==5.3.1
[options.package_data]
* = *.yaml
[options.packages.find]
where=src
[options.extras_require]
test =
coverage[toml]==6.3.2
pytest==7.1.1
lint =
black==22.3.0
isort==5.10.1
pylint==2.13.5
pydocstyle[toml]==6.1.1
docs =
sphinx==4.5.0
sphinx-rtd-theme==1.0.0
build =
build
twine
pyinstaller

View file

@ -1,2 +1,2 @@
__pypi_version__ = "2023.03.24.post7"
__local_version__ = "2023.03.24+14e4a4b"
__pypi_version__ = "2023.10.22.post3"
__local_version__ = "2023.10.22+bfba4f0"

View file

@ -0,0 +1,346 @@
import gc
import os
import random
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
from yt_dlp.utils import sanitize_filename
from ytdl_sub.cli.output_summary import output_summary
from ytdl_sub.cli.output_transaction_log import (
_maybe_validate_transaction_log_file,
output_transaction_log,
)
from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub
from ytdl_sub.cli.parsers.dl import DownloadArgsParser
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, parser
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.validators.variable_validation import ResolutionLevel
from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled, ValidationException
from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.file_lock import working_directory_lock
from ytdl_sub.utils.logger import Logger
# pylint: disable=too-many-branches
logger = Logger.get()
# View is a command to run a simple dry-run on a URL using the `_view` preset.
# Use ytdl-sub dl arguments to use the preset
_VIEW_EXTRA_ARGS_FORMATTER = "--preset _view --overrides.url {}"
def _log_time() -> str:
return datetime.now().strftime("%Y-%m-%d-%H%M%S")
def _maybe_write_subscription_log_file(
config: ConfigFile,
subscription: Subscription,
dry_run: bool,
exception: Optional[Exception] = None,
) -> None:
success: bool = exception is None
# If dry-run, do nothing
if dry_run:
return
# If persisting logs is disabled, do nothing
if not config.config_options.persist_logs:
return
# If persisting successful logs is disabled, do nothing
if success and not config.config_options.persist_logs.keep_successful_logs:
return
log_subscription_name = sanitize_filename(subscription.name).lower().replace(" ", "_")
log_success = "success" if success else "error"
log_filename = f"{_log_time()}.{log_subscription_name}.{log_success}.log"
persist_log_path = Path(config.config_options.persist_logs.logs_directory) / log_filename
if not success:
Logger.log_exception(exception=exception, log_filepath=persist_log_path)
os.makedirs(os.path.dirname(persist_log_path), exist_ok=True)
FileHandler.copy(Logger.debug_log_filename(), persist_log_path)
def _download_subscriptions_from_yaml_files(
config: ConfigFile,
subscription_paths: List[str],
subscription_matches: List[str],
subscription_override_dict: Dict,
update_with_info_json: bool,
dry_run: bool,
shuffle: bool,
) -> List[Subscription]:
"""
Downloads all subscriptions from one or many subscription yaml files.
Parameters
----------
config
Configuration file
subscription_paths
Path to subscription files to download
subscription_matches
Optional list of substrings to match subscription names to (only run if matched)
update_with_info_json
Whether to actually download or update using existing info json
dry_run
Whether to dry run or not
shuffle
Whether to shuffle the subscription download order
Returns
-------
List of subscriptions processed
Raises
------
Exception
Any exception during download
"""
subscriptions: List[Subscription] = []
# Load all the subscriptions first to perform all validation before downloading
for path in subscription_paths:
subscriptions += Subscription.from_file_path(
config=config,
subscription_path=path,
subscription_matches=subscription_matches,
subscription_override_dict=subscription_override_dict,
)
if shuffle:
logger.info("Shuffling subscriptions")
random.shuffle(subscriptions)
for subscription in subscriptions:
with subscription.exception_handling():
logger.info(
"Beginning subscription %s for %s",
("dry run" if dry_run else "download"),
subscription.name,
)
logger.debug("Subscription full yaml:\n%s", subscription.as_yaml())
if update_with_info_json:
subscription.update_with_info_json(dry_run=dry_run)
else:
subscription.download(dry_run=dry_run)
_maybe_write_subscription_log_file(
config=config,
subscription=subscription,
dry_run=dry_run,
exception=subscription.exception,
)
Logger.cleanup(has_error=False)
gc.collect() # Garbage collect after each subscription download
return subscriptions
def _download_subscription_from_cli(
config: ConfigFile, dry_run: bool, extra_args: List[str]
) -> Subscription:
"""
Downloads a one-off subscription using the CLI
Parameters
----------
config
Configuration file
dry_run
Whether this is a dry-run
extra_args
Extra arguments from argparse that contain dynamic subscription options
Returns
-------
Subscription and its download transaction log
"""
dl_args_parser = DownloadArgsParser(
extra_arguments=extra_args, config_options=config.config_options
)
subscription_args_dict = dl_args_parser.to_subscription_dict()
subscription_name = dl_args_parser.get_dl_subscription_name()
subscription = Subscription.from_dict(
config=config, preset_name=subscription_name, preset_dict=subscription_args_dict
)
logger.info("Beginning CLI %s", ("dry run" if dry_run else "download"))
subscription.download(dry_run=dry_run)
return subscription
def _view_url_from_cli(config: ConfigFile, url: str, split_chapters: bool) -> Subscription:
"""
`ytdl-sub view` dry-runs a URL to print its source variables. Use the pre-built `_view` preset,
inject the URL argument, and dry-run.
"""
preset = "_view_split_chapters" if split_chapters else "_view"
subscription = Subscription.from_dict(
config=config,
preset_name="ytdl-sub-view",
preset_dict={"preset": preset, "overrides": {"url": url}},
)
logger.info(
"Viewing source variables for URL '%s'%s",
url,
" with split chapters" if split_chapters else "",
)
subscription.download(dry_run=True)
return subscription
def _parse_inspect_mocks(mocks: Optional[List[str]]) -> Dict[str, str]:
out: Dict[str, str] = {}
for mock in mocks or []:
spl = mock.split("=", 1)
if len(spl) == 1:
raise ValidationException("inspect mock must be in the form of VAR=VALUE")
out[spl[0].strip()] = spl[1]
return out
def _inspect(
config: ConfigFile,
subscription_paths: List[str],
subscription_matches: List[str],
subscription_override_dict: Dict,
inspection_level: int,
mocks: Dict[str, str],
) -> None:
subscriptions: List[Subscription] = []
for path in subscription_paths:
subscriptions += Subscription.from_file_path(
config=config,
subscription_path=path,
subscription_matches=subscription_matches,
subscription_override_dict=subscription_override_dict,
)
if len(subscriptions) > 1:
print(
"inspect can only inspect a single subscription. Use --match to filter for a single one"
)
return
print(subscriptions[0].resolved_yaml(resolution_level=inspection_level, mocks=mocks))
def main() -> List[Subscription]:
"""
Entrypoint for ytdl-sub, without the error handling
"""
# If no args are provided, print help and exit
if len(sys.argv) < 2:
parser.print_help()
return []
args, extra_args = parser.parse_known_args()
if args.subparser == "cli-to-sub":
print_cli_to_sub(args=extra_args)
return []
# Load the config
if args.config:
config = ConfigFile.from_file_path(args.config)
elif os.path.isfile(DEFAULT_CONFIG_FILE_NAME):
config = ConfigFile.from_file_path(DEFAULT_CONFIG_FILE_NAME)
else:
logger.info("No config specified, using defaults.")
config = ConfigFile.default()
subscriptions: List[Subscription] = []
if args.subparser == "inspect":
subscription_override_dict = {}
if args.dl_override:
subscription_override_dict = DownloadArgsParser.from_dl_override(
override=args.dl_override, config=config
).to_subscription_dict()
_inspect(
config=config,
subscription_paths=args.subscription_paths,
subscription_matches=args.match,
subscription_override_dict=subscription_override_dict,
inspection_level=ResolutionLevel.level_number(args.inspection_level),
mocks=_parse_inspect_mocks(args.mock),
)
return []
# If transaction log file is specified, make sure we can open it
_maybe_validate_transaction_log_file(transaction_log_file_path=args.transaction_log)
with working_directory_lock(config=config):
if args.subparser == "sub":
if (
args.update_with_info_json
and not config.config_options.experimental.enable_update_with_info_json
):
raise ExperimentalFeatureNotEnabled(
"--update-with-info-json requires setting"
" configuration.experimental.enable_update_with_info_json to True. This"
" feature is ",
"still being tested and has the ability to destroy files. Ensure you have a ",
"full backup before usage. You have been warned!",
)
subscription_override_dict = {}
if args.dl_override:
subscription_override_dict = DownloadArgsParser.from_dl_override(
override=args.dl_override, config=config
).to_subscription_dict()
logger.info("Validating subscriptions...")
subscriptions = _download_subscriptions_from_yaml_files(
config=config,
subscription_paths=args.subscription_paths,
subscription_matches=args.match,
subscription_override_dict=subscription_override_dict,
update_with_info_json=args.update_with_info_json,
dry_run=args.dry_run,
shuffle=args.shuffle,
)
# One-off download
elif args.subparser == "dl":
logger.info("Validating presets...")
subscriptions.append(
_download_subscription_from_cli(
config=config, dry_run=args.dry_run, extra_args=extra_args
)
)
elif args.subparser == "view":
subscriptions.append(
_view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
)
else:
raise ValidationException("Must provide one of the commands: sub, dl, view, cli-to-sub")
if not args.suppress_transaction_log:
output_transaction_log(
subscriptions=subscriptions,
transaction_log_file_path=args.transaction_log,
)
output_summary(subscriptions, suppress_colors=args.suppress_colors)
return subscriptions

View file

@ -1,354 +0,0 @@
import gc
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import List
from typing import Optional
from typing import Tuple
from colorama import Fore
from yt_dlp.utils import sanitize_filename
from ytdl_sub.cli.download_args_parser import DownloadArgsParser
from ytdl_sub.cli.main_args_parser import parser
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled
from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
from ytdl_sub.utils.file_lock import working_directory_lock
from ytdl_sub.utils.logger import Logger
logger = Logger.get()
# View is a command to run a simple dry-run on a URL using the `_view` preset.
# Use ytdl-sub dl arguments to use the preset
_VIEW_EXTRA_ARGS_FORMATTER = "--preset _view --overrides.url {}"
def _maybe_write_subscription_log_file(
config: ConfigFile,
subscription: Subscription,
dry_run: bool,
exception: Optional[Exception] = None,
) -> None:
success: bool = exception is None
# If dry-run, do nothing
if dry_run:
return
# If persisting logs is disabled, do nothing
if not config.config_options.persist_logs:
return
# If persisting successful logs is disabled, do nothing
if success and not config.config_options.persist_logs.keep_successful_logs:
return
log_time = datetime.now().strftime("%Y-%m-%d-%H%M%S")
log_subscription_name = sanitize_filename(subscription.name).lower().replace(" ", "_")
log_success = "success" if success else "error"
log_filename = f"{log_time}.{log_subscription_name}.{log_success}.log"
persist_log_path = Path(config.config_options.persist_logs.logs_directory) / log_filename
if not success:
Logger.log_exit_exception(exception=exception, log_filepath=persist_log_path)
os.makedirs(os.path.dirname(persist_log_path), exist_ok=True)
FileHandler.copy(Logger.debug_log_filename(), persist_log_path)
def _download_subscriptions_from_yaml_files(
config: ConfigFile, subscription_paths: List[str], update_with_info_json: bool, dry_run: bool
) -> List[Tuple[Subscription, FileHandlerTransactionLog]]:
"""
Downloads all subscriptions from one or many subscription yaml files.
Parameters
----------
config
Configuration file
subscription_paths
Path to subscription files to download
update_with_info_json
Whether to actually download or update using existing info json
dry_run
Whether to dry run or not
Returns
-------
List of (subscription, transaction_log)
Raises
------
Exception
Any exception during download
"""
subscriptions: List[Subscription] = []
output: List[Tuple[Subscription, FileHandlerTransactionLog]] = []
# Load all the subscriptions first to perform all validation before downloading
for path in subscription_paths:
subscriptions += Subscription.from_file_path(config=config, subscription_path=path)
for subscription in subscriptions:
logger.info(
"Beginning subscription %s for %s",
("dry run" if dry_run else "download"),
subscription.name,
)
logger.debug("Subscription full yaml:\n%s", subscription.as_yaml())
try:
if update_with_info_json:
transaction_log = subscription.update_with_info_json(dry_run=dry_run)
else:
transaction_log = subscription.download(dry_run=dry_run)
except Exception as exc: # pylint: disable=broad-except
_maybe_write_subscription_log_file(
config=config, subscription=subscription, dry_run=dry_run, exception=exc
)
raise
else:
output.append((subscription, transaction_log))
_maybe_write_subscription_log_file(
config=config, subscription=subscription, dry_run=dry_run
)
Logger.cleanup() # Cleanup logger after each successful subscription download
finally:
gc.collect() # Garbage collect after each subscription download
return output
def _download_subscription_from_cli(
config: ConfigFile, dry_run: bool, extra_args: List[str]
) -> Tuple[Subscription, FileHandlerTransactionLog]:
"""
Downloads a one-off subscription using the CLI
Parameters
----------
config
Configuration file
dry_run
Whether this is a dry-run
extra_args
Extra arguments from argparse that contain dynamic subscription options
Returns
-------
Subscription and its download transaction log
"""
dl_args_parser = DownloadArgsParser(
extra_arguments=extra_args, config_options=config.config_options
)
subscription_args_dict = dl_args_parser.to_subscription_dict()
subscription_name = f"cli-dl-{dl_args_parser.get_args_hash()}"
subscription = Subscription.from_dict(
config=config, preset_name=subscription_name, preset_dict=subscription_args_dict
)
logger.info("Beginning CLI %s", ("dry run" if dry_run else "download"))
return subscription, subscription.download(dry_run=dry_run)
def _view_url_from_cli(
config: ConfigFile, url: str, split_chapters: bool
) -> Tuple[Subscription, FileHandlerTransactionLog]:
"""
`ytdl-sub view` dry-runs a URL to print its source variables. Use the pre-built `_view` preset,
inject the URL argument, and dry-run.
"""
preset = "_view_split_chapters" if split_chapters else "_view"
subscription = Subscription.from_dict(
config=config,
preset_name="ytdl-sub-view",
preset_dict={"preset": preset, "overrides": {"url": url}},
)
logger.info(
"Viewing source variables for URL '%s'%s",
url,
" with split chapters" if split_chapters else "",
)
return subscription, subscription.download(dry_run=True)
def _maybe_validate_transaction_log_file(transaction_log_file_path: Optional[str]) -> None:
if transaction_log_file_path:
try:
with open(transaction_log_file_path, "w", encoding="utf-8"):
pass
except Exception as exc:
raise ValidationException(
f"Transaction log file '{transaction_log_file_path}' cannot be written to. "
f"Reason: {str(exc)}"
) from exc
def _output_transaction_log(
transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]],
transaction_log_file_path: str,
) -> None:
transaction_log_file_contents = ""
for subscription, transaction_log in transaction_logs:
if transaction_log.is_empty:
transaction_log_contents = f"\nNo files changed for {subscription.name}"
else:
transaction_log_contents = (
f"Transaction log for {subscription.name}:\n"
f"{transaction_log.to_output_message(subscription.output_directory)}"
)
if transaction_log_file_path:
transaction_log_file_contents += transaction_log_contents
else:
logger.info(transaction_log_contents)
if transaction_log_file_contents:
with open(transaction_log_file_path, "w", encoding="utf-8") as transaction_log_file:
transaction_log_file.write(transaction_log_file_contents)
def _green(value: str) -> str:
return Fore.GREEN + value + Fore.RESET
def _red(value: str) -> str:
return Fore.RED + value + Fore.RESET
def _no_color(value: str) -> str:
return Fore.RESET + value + Fore.RESET
def _str_int(value: int) -> str:
if value > 0:
return f"+{value}"
return str(value)
def _color_int(value: int) -> str:
str_int = _str_int(value)
if value > 0:
return _green(str_int)
if value < 0:
return _red(str_int)
return _no_color(str_int)
def _output_summary(transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]]):
summary: List[str] = []
# Initialize widths to 0
width_sub_name: int = 0
width_num_entries_added: int = 0
width_num_entries_modified: int = 0
width_num_entries_removed: int = 0
width_num_entries: int = 0
# Calculate min width needed
for subscription, _ in transaction_logs:
width_sub_name = max(width_sub_name, len(subscription.name))
width_num_entries_added = max(
width_num_entries_added, len(_color_int(subscription.num_entries_added))
)
width_num_entries_modified = max(
width_num_entries_modified, len(_color_int(subscription.num_entries_modified))
)
width_num_entries_removed = max(
width_num_entries_removed, len(_color_int(subscription.num_entries_removed * -1))
)
width_num_entries = max(width_num_entries, len(str(subscription.num_entries)))
# Add spacing for aesthetics
width_sub_name += 4
width_num_entries += 4
# Build the summary
for subscription, _ in transaction_logs:
num_entries_added = _color_int(subscription.num_entries_added)
num_entries_modified = _color_int(subscription.num_entries_modified)
num_entries_removed = _color_int(subscription.num_entries_removed * -1)
num_entries = str(subscription.num_entries)
status = _green("success")
summary.append(
f"{subscription.name:<{width_sub_name}} "
f"{num_entries_added:>{width_num_entries_added}} "
f"{num_entries_modified:>{width_num_entries_modified}} "
f"{num_entries_removed:>{width_num_entries_removed}} "
f"{num_entries:>{width_num_entries}} "
f"{status}"
)
return "\n".join(summary)
def main() -> List[Tuple[Subscription, FileHandlerTransactionLog]]:
"""
Entrypoint for ytdl-sub, without the error handling
"""
# If no args are provided, print help and exit
if len(sys.argv) < 2:
parser.print_help()
return []
args, extra_args = parser.parse_known_args()
# Load the config
config: ConfigFile = ConfigFile.from_file_path(args.config)
transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]] = []
# If transaction log file is specified, make sure we can open it
_maybe_validate_transaction_log_file(transaction_log_file_path=args.transaction_log)
with working_directory_lock(config=config):
if args.subparser == "sub":
if (
args.update_with_info_json
and not config.config_options.experimental.enable_update_with_info_json
):
raise ExperimentalFeatureNotEnabled(
"--update-with-info-json requires setting"
" configuration.experimental.enable_update_with_info_json to True. This"
" feature is ",
"still being tested and has the ability to destroy files. Ensure you have a ",
"full backup before usage. You have been warned!",
)
transaction_logs = _download_subscriptions_from_yaml_files(
config=config,
subscription_paths=args.subscription_paths,
update_with_info_json=args.update_with_info_json,
dry_run=args.dry_run,
)
# One-off download
elif args.subparser == "dl":
transaction_logs.append(
_download_subscription_from_cli(
config=config, dry_run=args.dry_run, extra_args=extra_args
)
)
elif args.subparser == "view":
transaction_logs.append(
_view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
)
if not args.suppress_transaction_log:
_output_transaction_log(
transaction_logs=transaction_logs,
transaction_log_file_path=args.transaction_log,
)
# Hack to always show download summary, even if logs are set to quiet
logger.warning("Download Summary:\n%s", _output_summary(transaction_logs))
return transaction_logs

View file

@ -0,0 +1,117 @@
from typing import List
from colorama import Fore
from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.logger import Logger
logger = Logger.get()
def _green(value: str, suppress_colors: bool = False) -> str:
return value if suppress_colors else Fore.GREEN + value + Fore.RESET
def _red(value: str, suppress_colors: bool = False) -> str:
return value if suppress_colors else Fore.RED + value + Fore.RESET
def _no_color(value: str, suppress_colors: bool = False) -> str:
return value if suppress_colors else Fore.RESET + value + Fore.RESET
def _str_int(value: int) -> str:
if value > 0:
return f"+{value}"
return str(value)
def _color_int(value: int, suppress_colors: bool = False) -> str:
str_int = _str_int(value)
if value > 0:
return _green(str_int, suppress_colors)
if value < 0:
return _red(str_int, suppress_colors)
return _no_color(str_int, suppress_colors)
def output_summary(subscriptions: List[Subscription], suppress_colors: bool) -> None:
"""
Parameters
----------
subscriptions
Processed subscriptions
suppress_colors
Whether to have color or not
Returns
-------
Output summary to print
"""
# many locals for proper output printing
# pylint: disable=too-many-locals
if len(subscriptions) == 0:
logger.info("No subscriptions ran")
return
summary: List[str] = []
# Initialize totals to 0
total_subs: int = len(subscriptions)
total_subs_str = f"Total: {total_subs}"
total_added: int = sum(sub.num_entries_added for sub in subscriptions)
total_modified: int = sum(sub.num_entries_modified for sub in subscriptions)
total_removed: int = sum(sub.num_entries_removed for sub in subscriptions)
total_entries: int = sum(sub.num_entries for sub in subscriptions)
total_errors: int = sum(sub.exception is not None for sub in subscriptions)
# Initialize widths to 0
width_sub_name: int = max(len(sub.name) for sub in subscriptions) + 4 # aesthetics
width_num_entries_added: int = len(_color_int(total_added, suppress_colors))
width_num_entries_modified: int = len(_color_int(total_modified, suppress_colors))
width_num_entries_removed: int = len(_color_int(total_removed, suppress_colors))
width_num_entries: int = len(str(total_entries)) + 4 # aesthetics
# Build the summary
for subscription in subscriptions:
num_entries_added = _color_int(subscription.num_entries_added, suppress_colors)
num_entries_modified = _color_int(subscription.num_entries_modified, suppress_colors)
num_entries_removed = _color_int(subscription.num_entries_removed * -1, suppress_colors)
num_entries = str(subscription.num_entries)
status = (
_red(subscription.exception.__class__.__name__, suppress_colors)
if subscription.exception
else _green("", suppress_colors)
)
summary.append(
f"{subscription.name:<{width_sub_name}} "
f"{num_entries_added:>{width_num_entries_added}} "
f"{num_entries_modified:>{width_num_entries_modified}} "
f"{num_entries_removed:>{width_num_entries_removed}} "
f"{num_entries:>{width_num_entries}} "
f"{status}"
)
total_errors_str = (
_green("Success", suppress_colors)
if total_errors == 0
else _red(f"Error{'s' if total_errors > 1 else ''}", suppress_colors)
)
summary.append(
f"{total_subs_str:<{width_sub_name}} "
f"{_color_int(total_added, suppress_colors):>{width_num_entries_added}} "
f"{_color_int(total_modified, suppress_colors):>{width_num_entries_modified}} "
f"{_color_int(total_removed * -1, suppress_colors):>{width_num_entries_removed}} "
f"{total_entries:>{width_num_entries}} "
f"{total_errors_str}"
)
if total_errors > 0:
summary.append("")
summary.append(f"See `{Logger.error_log_filename()}` for details on errors.")
summary.append("Consider making a GitHub issue including the uploaded log file.")
# Hack to always show download summary, even if logs are set to quiet
logger.warning("Download Summary:\n%s", "\n".join(summary))

View file

@ -0,0 +1,53 @@
from typing import List, Optional
from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.logger import Logger
logger = Logger.get()
def _maybe_validate_transaction_log_file(transaction_log_file_path: Optional[str]) -> None:
if transaction_log_file_path:
try:
with open(transaction_log_file_path, "w", encoding="utf-8"):
pass
except Exception as exc:
raise ValidationException(
f"Transaction log file '{transaction_log_file_path}' cannot be written to. "
f"Reason: {str(exc)}"
) from exc
def output_transaction_log(
subscriptions: List[Subscription],
transaction_log_file_path: Optional[str],
) -> None:
"""
Maybe print and/or write transaction logs to a file
Parameters
----------
subscriptions
Processed subscriptions
transaction_log_file_path
Optional file path to write to
"""
transaction_log_file_contents = ""
for subscription in subscriptions:
if subscription.transaction_log.is_empty:
transaction_log_contents = f"\nNo files changed for {subscription.name}"
else:
transaction_log_contents = (
f"Transaction log for {subscription.name}:\n"
f"{subscription.transaction_log.to_output_message(subscription.output_directory)}"
)
if transaction_log_file_path:
transaction_log_file_contents += transaction_log_contents
else:
logger.info(transaction_log_contents)
if transaction_log_file_contents:
with open(transaction_log_file_path, "w", encoding="utf-8") as transaction_log_file:
transaction_log_file.write(transaction_log_file_contents)

View file

@ -0,0 +1,63 @@
from typing import List
import yt_dlp
import yt_dlp.options
from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.yaml import dump_yaml
logger = Logger.get()
# pylint: disable=missing-function-docstring
##############################################################
# --- BEGIN ----
# Copy of https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py
create_parser = yt_dlp.options.create_parser
def parse_patched_options(opts):
patched_parser = create_parser()
patched_parser.defaults.update(
{
"ignoreerrors": False,
"retries": 0,
"fragment_retries": 0,
"extract_flat": False,
"concat_playlist": "never",
"update_self": False,
}
)
yt_dlp.options.create_parser = lambda: patched_parser
try:
return yt_dlp.parse_options(opts)
finally:
yt_dlp.options.create_parser = create_parser
default_opts = parse_patched_options([]).ydl_opts
def cli_to_api(opts, cli_defaults=False):
opts = (yt_dlp.parse_options if cli_defaults else parse_patched_options)(opts).ydl_opts
diff = {k: v for k, v in opts.items() if default_opts[k] != v}
if "postprocessors" in diff:
diff["postprocessors"] = [
pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]
]
return diff
# --- END ----
##############################################################
def print_cli_to_sub(args: List[str]) -> None:
api_args = cli_to_api(args)
if not api_args:
logger.info("Does not resolve to any yt-dlp args")
return
print(dump_yaml({"ytdl_options": api_args}))

View file

@ -1,13 +1,12 @@
import hashlib
import re
import shlex
from typing import Dict
from typing import List
from typing import Tuple
from typing import Any, Dict, List, Tuple
from mergedeep import mergedeep
from ytdl_sub.cli.main_args_parser import MainArguments
from ytdl_sub.cli.parsers.main import MainArguments
from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.config_validator import ConfigOptions
from ytdl_sub.utils.exceptions import InvalidDlArguments
@ -116,7 +115,7 @@ class DownloadArgsParser:
return largest_consecutive + 1
@classmethod
def _argument_name_and_value_to_dict(cls, arg_name: str, arg_value: str) -> Dict:
def _argument_name_and_value_to_dict(cls, arg_name: str, arg_value: Any) -> Dict:
"""
:param arg_name: Argument name in the form of 'key1.key2.key3'
:param arg_value: Argument value
@ -134,11 +133,15 @@ class DownloadArgsParser:
next_dict[arg_name_split[-1]] = arg_value
# TODO: handle ints/floats
if arg_value == "True":
next_dict[arg_name_split[-1]] = True
elif arg_value == "False":
next_dict[arg_name_split[-1]] = False
if isinstance(arg_value, str):
if arg_value == "True":
next_dict[arg_name_split[-1]] = True
elif arg_value == "False":
next_dict[arg_name_split[-1]] = False
elif arg_value.isdigit():
next_dict[arg_name_split[-1]] = int(arg_value)
elif arg_value.replace(".", "", 1).isdigit():
next_dict[arg_name_split[-1]] = float(arg_value)
return argument_dict
@ -236,9 +239,19 @@ class DownloadArgsParser:
return subscription_dict
def get_args_hash(self) -> str:
def get_dl_subscription_name(self) -> str:
"""
:return: Hash of the arguments provided
Returns a deterministic name based on input args
"""
hash_string = str(sorted(self._unknown_arguments))
return hashlib.sha256(hash_string.encode()).hexdigest()[-8:]
to_hash = str(sorted(self._unknown_arguments))
hash_str = hashlib.sha256(to_hash.encode()).hexdigest()[-8:]
return f"cli-dl-{hash_str}"
@classmethod
def from_dl_override(cls, override: str, config: ConfigFile) -> "DownloadArgsParser":
"""
Create a DownloadArgsParser from a sub --override argument value
"""
return DownloadArgsParser(
extra_arguments=override.split(), config_options=config.config_options
)

View file

@ -1,10 +1,12 @@
import argparse
import dataclasses
from typing import List
from typing import Dict, List
from ytdl_sub import __local_version__
from ytdl_sub.utils.logger import LoggerLevels
DEFAULT_CONFIG_FILE_NAME: str = "config.yaml"
@dataclasses.dataclass
class CLIArgument:
@ -38,6 +40,11 @@ class MainArguments:
long="--suppress-transaction-log",
is_positional=True,
)
MATCH = CLIArgument(
short="-m",
long="--match",
)
SUPPRESS_COLORS = CLIArgument(short="-nc", long="--suppress-colors")
@classmethod
def all(cls) -> List[CLIArgument]:
@ -52,6 +59,8 @@ class MainArguments:
cls.LOG_LEVEL,
cls.TRANSACTION_LOG,
cls.SUPPRESS_TRANSACTION_LOG,
cls.MATCH,
cls.SUPPRESS_COLORS,
]
@classmethod
@ -86,8 +95,8 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
MainArguments.CONFIG.long,
metavar="CONFIGPATH",
type=str,
help="path to the config yaml, uses config.yaml if not provided",
default=argparse.SUPPRESS if suppress_defaults else "config.yaml",
help=f"path to the config yaml, uses {DEFAULT_CONFIG_FILE_NAME} if not provided",
default=argparse.SUPPRESS if suppress_defaults else None, # Default is set downstream
)
arg_parser.add_argument(
MainArguments.DRY_RUN.short,
@ -102,8 +111,8 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
MainArguments.LOG_LEVEL.long,
metavar="|".join(LoggerLevels.names()),
type=str,
help="level of logs to print to console, defaults to info",
default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.INFO.name,
help="level of logs to print to console, defaults to verbose",
default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.VERBOSE.name,
choices=LoggerLevels.names(),
dest="ytdl_sub_log_level",
)
@ -122,6 +131,23 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
help="do not output transaction logs to console or file",
default=argparse.SUPPRESS if suppress_defaults else False,
)
arg_parser.add_argument(
MainArguments.SUPPRESS_COLORS.short,
MainArguments.SUPPRESS_COLORS.long,
action="store_true",
help="do not use colors in ytdl-sub output",
default=argparse.SUPPRESS if suppress_defaults else False,
)
arg_parser.add_argument(
MainArguments.MATCH.short,
MainArguments.MATCH.long,
dest="match",
nargs="+",
action="extend",
type=str,
help="match subscription names to one or more substrings, and only run those subscriptions",
default=argparse.SUPPRESS if suppress_defaults else [],
)
###################################################################################################
@ -133,6 +159,8 @@ parser.add_argument("-v", "--version", action="version", version="%(prog)s " + _
_add_shared_arguments(parser, suppress_defaults=False)
subparsers = parser.add_subparsers(dest="subparser")
###################################################################################################
# SUBSCRIPTION PARSER
class SubArguments:
@ -140,6 +168,14 @@ class SubArguments:
short="-u",
long="--update-with-info-json",
)
OVERRIDE = CLIArgument(
short="-o",
long="--dl-override",
)
SHUFFLE = CLIArgument(
short="-sh",
long="--shuffle",
)
subscription_parser = subparsers.add_parser("sub")
@ -158,6 +194,20 @@ subscription_parser.add_argument(
help="update all subscriptions with the current config using info.json files",
default=False,
)
subscription_parser.add_argument(
SubArguments.OVERRIDE.short,
SubArguments.OVERRIDE.long,
type=str,
help="override all subscription config values using `dl` syntax, "
"i.e. --dl-override='--ytdl_options.max_downloads 3'",
)
subscription_parser.add_argument(
SubArguments.SHUFFLE.short,
SubArguments.SHUFFLE.long,
action="store_true",
help="shuffle subscription order when downloading",
default=False,
)
###################################################################################################
# DOWNLOAD PARSER
@ -182,3 +232,81 @@ view_parser.add_argument(
help="View source variables after splitting by chapters",
)
view_parser.add_argument("url", help="URL to view source variables for")
###################################################################################################
# CLI-TO-SUB PARSER
cli_to_sub_parser = subparsers.add_parser("cli-to-sub")
###################################################################################################
# INSPECT PARSER
class InspectArguments:
LEVEL = CLIArgument(
short="-l",
long="--level",
)
LevelChoices: Dict[str, str] = {
"0": "original",
"1": "fill",
"2": "resolve",
"3": "internal",
}
MOCK = CLIArgument(
short="-k",
long="--mock",
)
inspect_parser = subparsers.add_parser("inspect", formatter_class=argparse.RawTextHelpFormatter)
inspect_parser.add_argument(
InspectArguments.LEVEL.short,
InspectArguments.LEVEL.long,
metavar=",".join(str(i) for i in range(4)),
type=str,
help="""level of inspection to perform:
0 - original present the subscription as-is
1 - fill fill in defined values
2 - resolve resolve all possible variables (default)
3 - internal resolve all variables to their internal representation
""",
default="resolve",
choices=list(InspectArguments.LevelChoices.keys())
+ list(InspectArguments.LevelChoices.values()),
dest="inspection_level",
)
inspect_parser.add_argument(
MainArguments.MATCH.short,
MainArguments.MATCH.long,
dest="match",
nargs="+",
action="extend",
type=str,
help="match subscription names to one or more substrings, and only run those subscriptions",
default=[],
)
inspect_parser.add_argument(
"subscription_paths",
metavar="SUBPATH",
nargs="*",
help="path to subscription files, uses subscriptions.yaml if not provided",
default=["subscriptions.yaml"],
)
inspect_parser.add_argument(
SubArguments.OVERRIDE.short,
SubArguments.OVERRIDE.long,
type=str,
help="override all subscription config values using `dl` syntax, "
"i.e. --dl-override='--ytdl_options.max_downloads 3'",
)
inspect_parser.add_argument(
InspectArguments.MOCK.short,
InspectArguments.MOCK.long,
metavar="VAR=VALUE",
action="append",
help="ability to mock one or more variable values, i.e. --mock 'title=Lets Play'",
)

Some files were not shown because too many files have changed in this diff Show more