Compare commits
375 commits
2023.09.02
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
658f5ef3e1 | ||
|
|
1c4633b815 | ||
|
|
19b0e621ca | ||
|
|
4421c4eede | ||
|
|
b195f37412 | ||
|
|
108c7cfa14 | ||
|
|
772f01a734 | ||
|
|
bb96f00ca5 | ||
|
|
915d445e21 | ||
|
|
970c74ba45 | ||
|
|
e34b9b6295 | ||
|
|
49e3b5f887 | ||
|
|
5cb9bbea9c | ||
|
|
52a1ceaeea | ||
|
|
1156e0070a | ||
|
|
efbac43b9f | ||
|
|
a6aea037c3 | ||
|
|
cb3952de5c | ||
|
|
0983a594fd | ||
|
|
57fd6901e4 | ||
|
|
a758a8870d | ||
|
|
ff66ff5be0 | ||
|
|
64820daf6c | ||
|
|
3b5f9cba58 | ||
|
|
e300c3d23a | ||
|
|
d526545abb | ||
|
|
ced547b14a | ||
|
|
c163f9766a | ||
|
|
6b7805c6e1 | ||
|
|
d6eda27371 | ||
|
|
d37945db7e | ||
|
|
a097c6a476 | ||
|
|
97ecae79db | ||
|
|
c1431c8d55 | ||
|
|
60cdbad8c7 | ||
|
|
264e458c1c | ||
|
|
c4e112e8d5 | ||
|
|
97df4dac1d | ||
|
|
942c983f60 | ||
|
|
530d2f7666 | ||
|
|
41a5bf60aa | ||
|
|
3a07d058aa | ||
|
|
6b99c31e45 | ||
|
|
b2056bec5d | ||
|
|
c46de048ca | ||
|
|
869fc0b836 | ||
|
|
7ac525f875 | ||
|
|
d373935fff | ||
|
|
1abe2a44f5 | ||
|
|
344753cc63 | ||
|
|
0f96d8e24e | ||
|
|
157a0b59c4 | ||
|
|
a5ae69550e | ||
|
|
8d5d2be6a7 | ||
|
|
19f47cd914 | ||
|
|
24e71ce733 | ||
|
|
63753c5649 | ||
|
|
08180c0412 | ||
|
|
a74c79d014 | ||
|
|
2c33c3b49b | ||
|
|
e507e6ed46 | ||
|
|
ed55f3a3f7 | ||
|
|
c3ca3c6379 | ||
|
|
d3e978e517 | ||
|
|
a5dd438398 | ||
|
|
7364aac00c | ||
|
|
1b2e34bad4 | ||
|
|
863eae68a2 | ||
|
|
f62b47888f | ||
|
|
e6624ee329 | ||
|
|
2deafc9f2d | ||
|
|
609bec8aaf | ||
|
|
cc3a36e66a | ||
|
|
4f8490d88c | ||
|
|
548187abed | ||
|
|
1d75ea3f06 | ||
|
|
45c155c039 | ||
|
|
0c34f819de | ||
|
|
72419a3e98 | ||
|
|
a7cf361821 | ||
|
|
b9aefb4bea | ||
|
|
2e67a57bc9 | ||
|
|
3eb838eb40 | ||
|
|
c2afe05c11 | ||
|
|
1e0c9928a1 | ||
|
|
321556ecf7 | ||
|
|
746370ef9a | ||
|
|
ccdad4d4cd | ||
|
|
efd5a88f92 | ||
|
|
df8b4a1df8 | ||
|
|
adb08ed5f9 | ||
|
|
283ccc8608 | ||
|
|
d1e584d505 | ||
|
|
895e6ea972 | ||
|
|
abd75f8005 | ||
|
|
26922e1787 | ||
|
|
28b5a81d19 | ||
|
|
a642028d7b | ||
|
|
8eb5215a0c | ||
|
|
f729efad52 | ||
|
|
a4b910ae74 | ||
|
|
f88b27ffec | ||
|
|
60ff645750 | ||
|
|
e980f60519 | ||
|
|
288f106878 | ||
|
|
e1f81e5274 | ||
|
|
3c45e2217e | ||
|
|
191fa3c1bb | ||
|
|
7d9b4d9fa5 | ||
|
|
2465acb451 | ||
|
|
6b86e5efdf | ||
|
|
975c5072a9 | ||
|
|
47dc861727 | ||
|
|
2f55326981 | ||
|
|
00e9ceaf3d | ||
|
|
6c180c9478 | ||
|
|
a055c9b07a | ||
|
|
9871f62b91 | ||
|
|
ed5443515c | ||
|
|
d2e2947c75 | ||
|
|
fc2da4d525 | ||
|
|
f6bce88ebd | ||
|
|
faf9fd8a10 | ||
|
|
cc85e18d43 | ||
|
|
a68d5e4539 | ||
|
|
0bd97dbe96 | ||
|
|
66fd7cf41c | ||
|
|
36e23839f3 | ||
|
|
bc15fe09bd | ||
|
|
987b1cd028 | ||
|
|
c40b032eed | ||
|
|
8ea0360b1c | ||
|
|
d926617d5d | ||
|
|
a4293398b7 | ||
|
|
dfcbadb69d | ||
|
|
d5e8157b6c | ||
|
|
0c0f05e4d0 | ||
|
|
a1681bf71a | ||
|
|
19d28e9baa | ||
|
|
e27a7c5495 | ||
|
|
6cf41a06b5 | ||
|
|
2161dc5ac7 | ||
|
|
1897f884ab | ||
|
|
6c3fd8d5c7 | ||
|
|
e90e95982b | ||
|
|
d21aebfbaf | ||
|
|
665ded7cec | ||
|
|
1264f0c656 | ||
|
|
51c134a036 | ||
|
|
bb2bc49a49 | ||
|
|
0ebcfb6a57 | ||
|
|
279ab4a566 | ||
|
|
35402438a5 | ||
|
|
8d992cbcab | ||
|
|
cd58cfe13f | ||
|
|
d25cde5122 | ||
|
|
2b1e74bc8e | ||
|
|
f1db01bf2f | ||
|
|
d3ac54deae | ||
|
|
57c5d3c800 | ||
|
|
e0441b5405 | ||
|
|
74c7152cb1 | ||
|
|
07d810171d | ||
|
|
ebe9908361 | ||
|
|
d06ad07916 | ||
|
|
d84310a13c | ||
|
|
d83523ffe5 | ||
|
|
3bf31a5e07 | ||
|
|
53d84d8c1e | ||
|
|
8c3f8528ad | ||
|
|
a6341971ff | ||
|
|
d3295cef86 | ||
|
|
0f542ac4e5 | ||
|
|
52038ed999 | ||
|
|
e5451395e0 | ||
|
|
663fb76132 | ||
|
|
6a756cfecf | ||
|
|
bf64be8a59 | ||
|
|
80054aa77b | ||
|
|
1bdc65f2e1 | ||
|
|
43c10c19e0 | ||
|
|
102e2a9042 | ||
|
|
88110a816d | ||
|
|
7676fd9488 | ||
|
|
31e4da9ff7 | ||
|
|
c8ef766627 | ||
|
|
18d0d74326 | ||
|
|
0006c0ea94 | ||
|
|
8547ab569c | ||
|
|
78a3e90479 | ||
|
|
1499d7486c | ||
|
|
28c29683cb | ||
|
|
0acf4c9f33 | ||
|
|
084761fc61 | ||
|
|
c0ba30b220 | ||
|
|
ed4a2814b9 | ||
|
|
97054043c4 | ||
|
|
a3f59d5420 | ||
|
|
648201ed9c | ||
|
|
2417bec691 | ||
|
|
481ced34c5 | ||
|
|
01e1f46b86 | ||
|
|
1caade46bd | ||
|
|
f2ec26d77e | ||
|
|
1b57d79c8f | ||
|
|
88fa55c9dc | ||
|
|
fd5882051a | ||
|
|
513d1f5a97 | ||
|
|
79ba60021c | ||
|
|
f963125398 | ||
|
|
6c3150ac0c | ||
|
|
a37d9ed10e | ||
|
|
3fbdd26257 | ||
|
|
6510fd0999 | ||
|
|
10cb82a69a | ||
|
|
759948361f | ||
|
|
941fa43bb9 | ||
|
|
c44b98505e | ||
|
|
4e3e270df9 | ||
|
|
cf4c7f6d52 | ||
|
|
221c11ac3e | ||
|
|
e529674b85 | ||
|
|
a0957ffe93 | ||
|
|
5a62fc5222 | ||
|
|
bba8d86117 | ||
|
|
e08f6e4154 | ||
|
|
0ed8aec83d | ||
|
|
85ee967d39 | ||
|
|
1b3e7a9173 | ||
|
|
f010b574df | ||
|
|
fa13ba30cf | ||
|
|
f7305ebf8a | ||
|
|
5e9472986a | ||
|
|
5866c12104 | ||
|
|
5e335b195c | ||
|
|
6e31ad3600 | ||
|
|
30a2ad7a63 | ||
|
|
74625c293d | ||
|
|
ed969b84d2 | ||
|
|
87f9378d33 | ||
|
|
02db0c41a5 | ||
|
|
8461f5f229 | ||
|
|
325d229061 | ||
|
|
64d3082a8a | ||
|
|
1d176050a7 | ||
|
|
ec58a80660 | ||
|
|
caad4598fc | ||
|
|
aa637d12dc | ||
|
|
10387c925d | ||
|
|
f3a979c818 | ||
|
|
5b34df4588 | ||
|
|
232c0acdfa | ||
|
|
156c788689 | ||
|
|
ad72e1a6de | ||
|
|
c622cf68b5 | ||
|
|
598c574b8a | ||
|
|
017db953bf | ||
|
|
083db0d9dc | ||
|
|
d5e647554e | ||
|
|
82c503c515 | ||
|
|
f940d3ec3e | ||
|
|
a02e44f09d | ||
|
|
0beeeb464d | ||
|
|
8e6a2cb98e | ||
|
|
f346b0ef52 | ||
|
|
248e9a15a6 | ||
|
|
b95ba86279 | ||
|
|
7c217db843 | ||
|
|
cea21ca47f | ||
|
|
29616144b4 | ||
|
|
b3374cb4d5 | ||
|
|
213580ee84 | ||
|
|
29398862da | ||
|
|
f0b3991e3e | ||
|
|
c7823a40ad | ||
|
|
d2e733f2e7 | ||
|
|
c0ca3c3945 | ||
|
|
e3158583ca | ||
|
|
2076de3074 | ||
|
|
00e4cd8541 | ||
|
|
b9d05c5b8f | ||
|
|
305d084a6d | ||
|
|
3b36ffd050 | ||
|
|
be0f6f2d2d | ||
|
|
c29cca2b3d | ||
|
|
915e29379a | ||
|
|
961e04d027 | ||
|
|
78cdcce464 | ||
|
|
9382be5c42 | ||
|
|
d80a63631a | ||
|
|
ba71abc466 | ||
|
|
dc74c6a52a | ||
|
|
8de9a5b6da | ||
|
|
d96bf11b12 | ||
|
|
5e384ba96e | ||
|
|
4d0123c8b0 | ||
|
|
891b951625 | ||
|
|
db5f095a51 | ||
|
|
2dbe426ced | ||
|
|
b3d298b664 | ||
|
|
b99463d204 | ||
|
|
d1acae1a60 | ||
|
|
12430ecf1e | ||
|
|
735da8b647 | ||
|
|
f2d55fdb23 | ||
|
|
189e897888 | ||
|
|
731d4e444c | ||
|
|
1fcaba9274 | ||
|
|
65bb5c0a85 | ||
|
|
1375a0fbe3 | ||
|
|
008bcf1b2d | ||
|
|
02d51d6aec | ||
|
|
873ecc0147 | ||
|
|
b8fb119f21 | ||
|
|
308c76a8f2 | ||
|
|
c0e2ded1ae | ||
|
|
5d1fd0d7c6 | ||
|
|
e92b1cd12a | ||
|
|
4d9a30779c | ||
|
|
8998861b49 | ||
|
|
544af2207b | ||
|
|
52d1a5887e | ||
|
|
6123e35c64 | ||
|
|
c7fce5dbdd | ||
|
|
9b8aa827ba | ||
|
|
f23554f3d7 | ||
|
|
395691f2b9 | ||
|
|
e257210e4a | ||
|
|
8a27cad94e | ||
|
|
f1a2187a17 | ||
|
|
19eee28f4a | ||
|
|
d64d863175 | ||
|
|
33a8da9afc | ||
|
|
e9b2214a0a | ||
|
|
f8868add5f | ||
|
|
2d107c7e43 | ||
|
|
5fb5a18abc | ||
|
|
9f408d2196 | ||
|
|
056b111be6 | ||
|
|
4789035821 | ||
|
|
11be119261 | ||
|
|
852347dc7e | ||
|
|
d810291e7c | ||
|
|
ff857c84af | ||
|
|
c149957b04 | ||
|
|
ee09e85a33 | ||
|
|
c932583165 | ||
|
|
8a02a40ca3 | ||
|
|
83ecd19c77 | ||
|
|
82c9beb00a | ||
|
|
8eca524292 | ||
|
|
1cb70e4520 | ||
|
|
a7651b202b | ||
|
|
4105c2ed40 | ||
|
|
aa5967bdd6 | ||
|
|
545f186c46 | ||
|
|
309f259464 | ||
|
|
ae3739cdd3 | ||
|
|
c119e6f644 | ||
|
|
c255f40348 | ||
|
|
3e41e0f59e | ||
|
|
6615e1d1ac | ||
|
|
533e083183 | ||
|
|
723c40b59b | ||
|
|
f05fdbc498 | ||
|
|
04b19a5ddd | ||
|
|
b0c6430407 | ||
|
|
d1adf08119 | ||
|
|
3d9c35519d | ||
|
|
3c87dc9f8e | ||
|
|
1a118e225a | ||
|
|
0b62cc13d0 | ||
|
|
0008bf3aca | ||
|
|
6c3e70a9fd | ||
|
|
a48efdc84c |
852 changed files with 44761 additions and 31932 deletions
53
.github/workflows/ci-windows.yaml
vendored
53
.github/workflows/ci-windows.yaml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run unit tests with coverage
|
- name: Run unit tests with coverage
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -29,9 +29,9 @@ jobs:
|
||||||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||||
|
|
||||||
python -m pip install -e .[test]
|
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
|
runs-on: windows-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -42,9 +42,9 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run e2e soundcloud tests with coverage
|
- name: Run integration tests with coverage
|
||||||
run: |
|
run: |
|
||||||
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
|
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
|
tar -xf ffmpeg.zip
|
||||||
|
|
@ -52,9 +52,10 @@ jobs:
|
||||||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||||
|
|
||||||
python -m pip install -e .[test]
|
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
|
runs-on: windows-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -65,9 +66,9 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run e2e soundcloud tests with coverage
|
- name: Run prebuilt preset integration tests with coverage
|
||||||
run: |
|
run: |
|
||||||
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
|
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
|
tar -xf ffmpeg.zip
|
||||||
|
|
@ -75,10 +76,9 @@ jobs:
|
||||||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||||
|
|
||||||
python -m pip install -e .[test]
|
python -m pip install -e .[test]
|
||||||
python -m pytest tests/e2e/bandcamp
|
python -m pytest --reruns 3 --reruns-delay 5 tests/integration/prebuilt_presets
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
test-youtube:
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -89,9 +89,9 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run e2e youtube tests with coverage
|
- name: Run e2e tests with coverage
|
||||||
run: |
|
run: |
|
||||||
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
|
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
|
tar -xf ffmpeg.zip
|
||||||
|
|
@ -99,27 +99,4 @@ jobs:
|
||||||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||||
|
|
||||||
python -m pip install -e .[test]
|
python -m pip install -e .[test]
|
||||||
python -m pytest tests/e2e/youtube
|
python -m pytest tests/e2e
|
||||||
|
|
||||||
test-plugins:
|
|
||||||
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 plugin 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/plugins
|
|
||||||
|
|
|
||||||
109
.github/workflows/ci.yaml
vendored
109
.github/workflows/ci.yaml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
||||||
- master
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
test-lint:
|
test-lint:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run linters
|
- name: Run linters
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
make check_lint
|
make check_lint
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
@ -37,14 +37,14 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run unit tests with coverage
|
- name: Run unit tests with coverage
|
||||||
run: |
|
run: |
|
||||||
pip install -e .[test]
|
pip install -e .[test]
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y ffmpeg
|
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
|
- name: Save coverage
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
|
|
@ -52,8 +52,8 @@ jobs:
|
||||||
path: /opt/coverage/unit
|
path: /opt/coverage/unit
|
||||||
key: ${{github.sha}}-coverage-unit
|
key: ${{github.sha}}-coverage-unit
|
||||||
|
|
||||||
test-soundcloud:
|
test-integration:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
@ -63,43 +63,23 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run e2e soundcloud tests with coverage
|
- name: Run integration tests with coverage
|
||||||
run: |
|
run: |
|
||||||
pip install -e .[test]
|
pip install -e .[test]
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y ffmpeg
|
sudo apt-get install -y ffmpeg
|
||||||
coverage run -m pytest tests/e2e/soundcloud && coverage xml -o /opt/coverage/soundcloud/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
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Save coverage
|
- name: Save coverage
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /opt/coverage/bandcamp
|
path: /opt/coverage/integration
|
||||||
key: ${{github.sha}}-coverage-bandcamp
|
key: ${{github.sha}}-coverage-integration
|
||||||
|
|
||||||
test-youtube:
|
test-integration-prebuilt-presets:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
@ -109,23 +89,23 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
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: |
|
run: |
|
||||||
pip install -e .[test]
|
pip install -e .[test]
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y ffmpeg
|
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
|
- name: Save coverage
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /opt/coverage/youtube
|
path: /opt/coverage/integration-prebuilt-presets
|
||||||
key: ${{github.sha}}-coverage-youtube
|
key: ${{github.sha}}-coverage-integration-prebuilt-presets
|
||||||
|
|
||||||
test-plugins:
|
test-e2e:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|
@ -135,29 +115,22 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run e2e plugin tests with coverage
|
- name: Run e2e tests with coverage
|
||||||
run: |
|
run: |
|
||||||
pip install -e .[test]
|
pip install -e .[test]
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y ffmpeg
|
sudo apt-get install -y ffmpeg
|
||||||
coverage run -m pytest tests/e2e/plugins && coverage xml -o /opt/coverage/plugins/coverage.xml
|
coverage run -m pytest tests/e2e && coverage xml -o /opt/coverage/e2e/coverage.xml
|
||||||
|
|
||||||
- name: Save coverage
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: /opt/coverage/plugins
|
|
||||||
key: ${{github.sha}}-coverage-plugins
|
|
||||||
|
|
||||||
codecov-upload:
|
codecov-upload:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
test-unit,
|
test-unit,
|
||||||
test-soundcloud,
|
test-integration,
|
||||||
test-bandcamp,
|
test-integration-prebuilt-presets,
|
||||||
test-youtube,
|
test-e2e
|
||||||
test-plugins
|
|
||||||
]
|
]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -169,32 +142,26 @@ jobs:
|
||||||
path: /opt/coverage/unit
|
path: /opt/coverage/unit
|
||||||
key: ${{github.sha}}-coverage-unit
|
key: ${{github.sha}}-coverage-unit
|
||||||
|
|
||||||
- name: Restore soundcloud test coverage
|
- name: Restore integration test coverage
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /opt/coverage/soundcloud
|
path: /opt/coverage/integration
|
||||||
key: ${{github.sha}}-coverage-soundcloud
|
key: ${{github.sha}}-coverage-integration
|
||||||
|
|
||||||
- name: Restore bandcamp test coverage
|
- name: Restore integration prebuilt presets test coverage
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /opt/coverage/bandcamp
|
path: /opt/coverage/integration-prebuilt-presets
|
||||||
key: ${{github.sha}}-coverage-bandcamp
|
key: ${{github.sha}}-coverage-integration-prebuilt-presets
|
||||||
|
|
||||||
- name: Restore youtube test coverage
|
- name: Restore e2e test coverage
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /opt/coverage/youtube
|
path: /opt/coverage/e2e
|
||||||
key: ${{github.sha}}-coverage-youtube
|
key: ${{github.sha}}-coverage-e2e
|
||||||
|
|
||||||
- name: Restore plugins test coverage
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: /opt/coverage/plugins
|
|
||||||
key: ${{github.sha}}-coverage-plugins
|
|
||||||
|
|
||||||
- name: Upload code coverage to codecov.io
|
- name: Upload code coverage to codecov.io
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
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
292
.github/workflows/package-gui.yaml
vendored
Normal 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
|
||||||
101
.github/workflows/package-ubuntu.yaml
vendored
101
.github/workflows/package-ubuntu.yaml
vendored
|
|
@ -19,11 +19,54 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
# Push image to GitHub Packages.
|
# Push image to GitHub Packages.
|
||||||
# See also https://docs.docker.com/docker-hub/builds/
|
# 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:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ "3.10" ]
|
python-version: [ "3.12" ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -49,7 +92,7 @@ jobs:
|
||||||
|
|
||||||
# Build ARM64 container, only on master branch to save time testing
|
# Build ARM64 container, only on master branch to save time testing
|
||||||
package-arm64:
|
package-arm64:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
build
|
build
|
||||||
]
|
]
|
||||||
|
|
@ -57,7 +100,11 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|
@ -94,7 +141,7 @@ jobs:
|
||||||
|
|
||||||
# Build AMD64 container
|
# Build AMD64 container
|
||||||
package-amd64:
|
package-amd64:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
build
|
build
|
||||||
]
|
]
|
||||||
|
|
@ -139,8 +186,9 @@ jobs:
|
||||||
# On master branch, build the docker manifest file from the cached
|
# On master branch, build the docker manifest file from the cached
|
||||||
# docker builds and push to the registry
|
# docker builds and push to the registry
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
|
version,
|
||||||
build,
|
build,
|
||||||
package-arm64,
|
package-arm64,
|
||||||
package-amd64
|
package-amd64
|
||||||
|
|
@ -150,7 +198,11 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|
@ -211,13 +263,30 @@ jobs:
|
||||||
|
|
||||||
echo ::set-output name=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
|
- name: Build Docker Image and push to registry
|
||||||
run: |
|
uses: docker/build-push-action@v6
|
||||||
docker buildx build --push \
|
with:
|
||||||
--platform=linux/amd64,linux/arm64 \
|
platforms: linux/amd64,linux/arm64
|
||||||
--cache-from=type=local,src=/tmp/build-cache/amd64 \
|
push: true
|
||||||
--cache-from=type=local,src=/tmp/build-cache/arm64 \
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:ubuntu-${{ steps.formatted_version.outputs.VERSION }} \
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
--label "runnumber=ubuntu-${GITHUB_RUN_ID}" \
|
context: "docker/"
|
||||||
--file docker/Dockerfile.ubuntu \
|
file: "docker/Dockerfile.ubuntu"
|
||||||
docker/
|
cache-from: |
|
||||||
|
type=local,src=/tmp/build-cache/amd64
|
||||||
|
type=local,src=/tmp/build-cache/arm64
|
||||||
|
|
|
||||||
96
.github/workflows/package.yaml
vendored
96
.github/workflows/package.yaml
vendored
|
|
@ -19,11 +19,54 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
# Push image to GitHub Packages.
|
# Push image to GitHub Packages.
|
||||||
# See also https://docs.docker.com/docker-hub/builds/
|
# 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:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ "3.10" ]
|
python-version: [ "3.12" ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -49,7 +92,7 @@ jobs:
|
||||||
|
|
||||||
# Build ARM64 container, only on master branch to save time testing
|
# Build ARM64 container, only on master branch to save time testing
|
||||||
package-arm64:
|
package-arm64:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
build
|
build
|
||||||
]
|
]
|
||||||
|
|
@ -93,7 +136,7 @@ jobs:
|
||||||
|
|
||||||
# Build AMD64 container
|
# Build AMD64 container
|
||||||
package-amd64:
|
package-amd64:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
build
|
build
|
||||||
]
|
]
|
||||||
|
|
@ -137,8 +180,9 @@ jobs:
|
||||||
# On master branch, build the docker manifest file from the cached
|
# On master branch, build the docker manifest file from the cached
|
||||||
# docker builds and push to the registry
|
# docker builds and push to the registry
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
needs: [
|
needs: [
|
||||||
|
version,
|
||||||
build,
|
build,
|
||||||
package-arm64,
|
package-arm64,
|
||||||
package-amd64
|
package-amd64
|
||||||
|
|
@ -148,7 +192,11 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|
@ -209,12 +257,30 @@ jobs:
|
||||||
|
|
||||||
echo ::set-output name=VERSION::${VERSION}
|
echo ::set-output name=VERSION::${VERSION}
|
||||||
|
|
||||||
- name: Build Docker Image and push to registry
|
|
||||||
run: |
|
- name: Docker meta
|
||||||
docker buildx build --push \
|
id: meta
|
||||||
--platform=linux/amd64,linux/arm64 \
|
uses: docker/metadata-action@v5
|
||||||
--cache-from=type=local,src=/tmp/build-cache/amd64 \
|
with:
|
||||||
--cache-from=type=local,src=/tmp/build-cache/arm64 \
|
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
|
||||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:${{ steps.formatted_version.outputs.VERSION }} \
|
labels: |
|
||||||
--label "runnumber=${GITHUB_RUN_ID}" \
|
runnumber=${{ github.run_number }}
|
||||||
docker/
|
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
|
||||||
|
|
|
||||||
55
.github/workflows/release.yaml
vendored
55
.github/workflows/release.yaml
vendored
|
|
@ -61,9 +61,16 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ "aarch64", "x86_64" ]
|
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:
|
container:
|
||||||
image: quay.io/pypa/manylinux_2_28_x86_64
|
image: ${{ matrix.container }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Write version to init file
|
- 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
|
dnf install -y epel-release tar wget make gcc openssl-devel bzip2-devel libffi-devel zlib-devel
|
||||||
- name: Install Python
|
- name: Install Python
|
||||||
run: |
|
run: |
|
||||||
wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tar.xz
|
wget https://www.python.org/ftp/python/3.12.9/Python-3.12.9.tar.xz
|
||||||
tar -xf Python-3.10.10.tar.xz
|
tar -xf Python-3.12.9.tar.xz
|
||||||
cd Python-3.10.10 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
|
cd Python-3.12.9 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
|
||||||
make -j 8
|
make -j 8
|
||||||
make altinstall
|
make altinstall
|
||||||
python3.10 --version
|
python3.12 --version
|
||||||
python3.10 -m ensurepip --upgrade
|
python3.12 -m ensurepip --upgrade
|
||||||
- name: Build Package
|
- name: Build Package
|
||||||
run: |
|
run: |
|
||||||
python3.10 -m pip install -e .
|
python3.12 -m pip install -e .
|
||||||
python3.10 -m pip install pyinstaller
|
python3.12 -m pip install pyinstaller
|
||||||
# Build executable
|
# Build executable
|
||||||
pyinstaller ytdl-sub.spec
|
pyinstaller ytdl-sub.spec
|
||||||
mkdir -p /opt/builds
|
mkdir -p /opt/builds
|
||||||
|
|
@ -95,7 +102,7 @@ jobs:
|
||||||
mv dist/ytdl-sub /opt/builds/ytdl-sub_${{ matrix.arch }}
|
mv dist/ytdl-sub /opt/builds/ytdl-sub_${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ytdl-sub_${{ matrix.arch }}
|
name: ytdl-sub_${{ matrix.arch }}
|
||||||
path: /opt/builds/ytdl-sub_${{ matrix.arch }}
|
path: /opt/builds/ytdl-sub_${{ matrix.arch }}
|
||||||
|
|
@ -106,13 +113,13 @@ jobs:
|
||||||
name: build-windows
|
name: build-windows
|
||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
runs-on: windows-2019
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.12"
|
||||||
- name: Write version to init file
|
- name: Write version to init file
|
||||||
run: |
|
run: |
|
||||||
echo '${{ needs.version.outputs.init_contents }}'> src/ytdl_sub/__init__.py
|
echo '${{ needs.version.outputs.init_contents }}'> src/ytdl_sub/__init__.py
|
||||||
|
|
@ -124,7 +131,7 @@ jobs:
|
||||||
.\dist\ytdl-sub.exe -h
|
.\dist\ytdl-sub.exe -h
|
||||||
|
|
||||||
- name: Upload build
|
- name: Upload build
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ytdl-sub_exe
|
name: ytdl-sub_exe
|
||||||
path: .\dist\ytdl-sub.exe
|
path: .\dist\ytdl-sub.exe
|
||||||
|
|
@ -145,19 +152,19 @@ jobs:
|
||||||
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
|
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
|
||||||
|
|
||||||
- name: Restore exe build
|
- name: Restore exe build
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ytdl-sub_exe
|
name: ytdl-sub_exe
|
||||||
path: /opt/builds
|
path: /opt/builds
|
||||||
|
|
||||||
- name: Restore aarch64 build
|
- name: Restore aarch64 build
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ytdl-sub_aarch64
|
name: ytdl-sub_aarch64
|
||||||
path: /opt/builds
|
path: /opt/builds
|
||||||
|
|
||||||
- name: Restore x86_64 build
|
- name: Restore x86_64 build
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ytdl-sub_x86_64
|
name: ytdl-sub_x86_64
|
||||||
path: /opt/builds
|
path: /opt/builds
|
||||||
|
|
@ -171,7 +178,11 @@ jobs:
|
||||||
ls -lh /opt/builds
|
ls -lh /opt/builds
|
||||||
|
|
||||||
- name: Create Release
|
- 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
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
|
|
@ -189,13 +200,13 @@ jobs:
|
||||||
name: pypi-publish
|
name: pypi-publish
|
||||||
needs:
|
needs:
|
||||||
- version
|
- version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.12'
|
||||||
- name: Write version to init file
|
- name: Write version to init file
|
||||||
run: |
|
run: |
|
||||||
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
|
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
|
||||||
|
|
@ -207,7 +218,11 @@ jobs:
|
||||||
python3 -m build
|
python3 -m build
|
||||||
|
|
||||||
- name: Publish distribution 📦 to PyPI
|
- 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
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -141,8 +141,16 @@ dmypy.json
|
||||||
docker/*.whl
|
docker/*.whl
|
||||||
docker/root/*.whl
|
docker/root/*.whl
|
||||||
docker/root/defaults/examples
|
docker/root/defaults/examples
|
||||||
|
docker/testing/volumes
|
||||||
|
|
||||||
.local/
|
.local/
|
||||||
|
|
||||||
|
.ytdl-sub-working-directory
|
||||||
|
.ytdl-sub-lock
|
||||||
|
|
||||||
ffmpeg.exe
|
ffmpeg.exe
|
||||||
ffprobe.exe
|
ffprobe.exe
|
||||||
|
|
||||||
|
tools/docgen/out
|
||||||
|
|
||||||
|
prof/
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: "ubuntu-22.04"
|
||||||
tools:
|
tools:
|
||||||
python: "3.10"
|
python: "3.10"
|
||||||
|
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/source/conf.py
|
||||||
|
fail_on_warning: true
|
||||||
|
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
|
- requirements: docs/source/requirements.txt
|
||||||
- method: pip
|
- method: pip
|
||||||
path: .
|
path: .
|
||||||
extra_requirements:
|
extra_requirements:
|
||||||
|
|
|
||||||
55
Makefile
55
Makefile
|
|
@ -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
|
# Get version related variables
|
||||||
export DATE=$(shell date +'%Y.%m.%d')
|
export DATE:=$(shell date +'%Y.%m.%d')
|
||||||
export DATE_COMMIT_COUNT=$(shell git rev-list --count HEAD --since="$(DATE) 00:00:00")
|
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 COMMIT_HASH:=$(shell git rev-parse --short HEAD)
|
||||||
|
|
||||||
# Set Local version to YYYY.MM.DD-<hash>
|
# Set Local version to YYYY.MM.DD-<hash>
|
||||||
export LOCAL_VERSION="$(DATE)+$(COMMIT_HASH)"
|
export LOCAL_VERSION="$(DATE)+$(COMMIT_HASH)"
|
||||||
|
|
@ -13,18 +26,26 @@ else
|
||||||
export PYPI_VERSION="$(DATE).post$(DATE_COMMIT_COUNT)"
|
export PYPI_VERSION="$(DATE).post$(DATE_COMMIT_COUNT)"
|
||||||
endif
|
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:
|
lint:
|
||||||
@-isort .
|
python3 -m ruff format .
|
||||||
@-black .
|
python3 -m ruff check --fix .
|
||||||
@-pylint src/
|
python3 -m pylint src
|
||||||
@-pydocstyle src/*
|
|
||||||
check_lint:
|
check_lint:
|
||||||
isort . --check-only --diff \
|
ruff format --check . \
|
||||||
&& black . --check \
|
&& ruff check . \
|
||||||
&& pylint src/ \
|
&& pylint src/
|
||||||
&& pydocstyle src/*
|
|
||||||
wheel: clean
|
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
|
cat src/ytdl_sub/__init__.py
|
||||||
pip3 install build
|
pip3 install build
|
||||||
python3 -m build
|
python3 -m build
|
||||||
|
|
@ -34,19 +55,23 @@ docker_stage: wheel
|
||||||
docker: docker_stage
|
docker: docker_stage
|
||||||
sudo docker build --progress=plain --no-cache -t ytdl-sub:local docker/
|
sudo docker build --progress=plain --no-cache -t ytdl-sub:local docker/
|
||||||
docker_ubuntu: docker_stage
|
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
|
executable: clean
|
||||||
pyinstaller ytdl-sub.spec
|
pyinstaller ytdl-sub.spec
|
||||||
mv dist/ytdl-sub dist/ytdl-sub${EXEC_SUFFIX}
|
mv dist/ytdl-sub dist/ytdl-sub${EXEC_SUFFIX}
|
||||||
docs:
|
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:
|
clean:
|
||||||
rm -rf \
|
rm -rf \
|
||||||
.pytest_cache/ \
|
.pytest_cache/ \
|
||||||
build/ \
|
build/ \
|
||||||
dist/ \
|
dist/ \
|
||||||
src/ytdl_sub.egg-info/ \
|
src/ytdl_sub.egg-info/ \
|
||||||
docs/_html/ \
|
docs/build/ \
|
||||||
.coverage \
|
.coverage \
|
||||||
docker/root/*.whl \
|
docker/root/*.whl \
|
||||||
docker/root/defaults/examples \
|
docker/root/defaults/examples \
|
||||||
|
|
|
||||||
334
README.md
334
README.md
|
|
@ -33,221 +33,187 @@ maximum flexibility while maintaining simplicity.
|
||||||
#### Jellyfin
|
#### Jellyfin
|
||||||

|

|
||||||
|
|
||||||
### SoundCloud Albums and Singles
|
### SoundCloud Discography
|
||||||
#### MusicBee (any file or tag-based music players)
|
#### Writes proper music-tags via beets API
|
||||||

|

|
||||||
|
|
||||||
### Bandcamp Discography
|
### Bandcamp Discography
|
||||||
#### Navidrome (any file or tag-based music servers)
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## How it Works
|
## How it Works
|
||||||
`ytdl-sub` uses YAML configs to define a layout for how you want media to look
|
`ytdl-sub` uses YAML files to define subscriptions. Each subscription imports _presets_ that
|
||||||
after it is downloaded. See our
|
define how to handle and output media files. `ytdl-sub` comes packaged with many _prebuilt presets_
|
||||||
[walk-through guide](https://github.com/jmbannon/ytdl-sub/wiki)
|
that do the work of config-building, so you can start downloading immediately.
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# Set the working directory which will be used to stage downloads
|
# subscriptions.yaml:
|
||||||
# before placing them in your desired output directory.
|
# Everything in here can be downloaded using the command:
|
||||||
configuration:
|
# ytdl-sub sub subscriptions.yaml
|
||||||
working_directory: '.ytdl-sub-downloads'
|
|
||||||
|
|
||||||
# Presets are where you create 'sub-configs' that can can be
|
# __preset__ is a place to define global overrides for all subscriptions
|
||||||
# merged together to dictate what is downloaded, how to format it,
|
__preset__:
|
||||||
# and what metadata to generate.
|
overrides:
|
||||||
presets:
|
# Root folder of all ytdl-sub TV Shows
|
||||||
|
tv_show_directory: "/tv_shows"
|
||||||
|
|
||||||
# Let us create a preset called `only_recent_videos` that will
|
# Root folder of all ytdl-sub Music
|
||||||
# only download recent videos in the last 2 months.
|
music_directory: "/music"
|
||||||
only_recent_videos:
|
|
||||||
|
|
||||||
# Use the `date_range` plugin to specify ytdl-sub to only
|
# Root folder of all ytdl-sub Music Videos
|
||||||
# download videos after today MINUS {download_range}, which
|
music_video_directory: "/music_videos"
|
||||||
# is an override variable that we can alter per channel.
|
|
||||||
date_range:
|
|
||||||
after: "today-{download_range}"
|
|
||||||
|
|
||||||
# Any yt-dlp argument can be passed via ytdl-sub. Let us set
|
# For 'Only Recent' preset, only keep vids within this range and limit
|
||||||
# yt-dlp's `break_on_reject` to True to stop downloading after
|
only_recent_date_range: "2months"
|
||||||
# any video is rejected. Videos will be rejected if they are
|
only_recent_max_files: 30
|
||||||
# uploaded after our {download_range}.
|
|
||||||
ytdl_options:
|
|
||||||
break_on_reject: True
|
|
||||||
|
|
||||||
# Deletes any videos uploaded after {download_range}.
|
# Pass any arg directly to yt-dlp's Python API
|
||||||
output_options:
|
ytdl_options:
|
||||||
keep_files_after: "today-{download_range}"
|
cookiefile: "/config/ytdl-sub-configs/cookie.txt"
|
||||||
|
|
||||||
# Set the override variable {download_range} to 2months.
|
###################################################################
|
||||||
# This will serve as our default value. We can override
|
# TV Show Presets. Can replace Plex with Plex/Jellyfin/Emby/Kodi
|
||||||
# this per channel or in a child preset.
|
|
||||||
overrides:
|
|
||||||
download_range: "2months"
|
|
||||||
|
|
||||||
####################################################################
|
Plex TV Show by Date:
|
||||||
|
|
||||||
# Now let us create a preset that downloads videos and formats
|
# Sets genre tag to "Documentaries"
|
||||||
# as TV shows.
|
= Documentaries:
|
||||||
tv_show:
|
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||||
|
"National Geographic": "https://www.youtube.com/@NatGeo"
|
||||||
|
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
|
||||||
|
|
||||||
# Presets can inherit all attributes from other presets. Our
|
# Sets genre tag to "Kids", "TV-Y" for content rating
|
||||||
# `tv_show` preset will inherit these presets built into ytdl-sub.
|
= Kids | = TV-Y:
|
||||||
preset:
|
"Jake Trains": "https://www.youtube.com/@JakeTrains"
|
||||||
# Let us specify all the TV show by date presets to support all
|
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Set override variables that will be applicable to all downloads
|
= Music:
|
||||||
# in main presets.
|
# TV show subscriptions can support multiple urls and store in the same TV Show
|
||||||
overrides:
|
"Rick Beato":
|
||||||
tv_show_directory: "/tv_shows" # Replace with desired directory
|
- "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"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
# 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/"
|
||||||
|
|
||||||
|
###################################################################
|
||||||
|
# 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
|
All of this can be downloaded and ready to import to your favorite player
|
||||||
The `subscriptions.yaml` file is where we define content to download using
|
using the command
|
||||||
presets in the `config.yaml`. Each subscription can overwrite any field used
|
```commandline
|
||||||
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
|
|
||||||
ytdl-sub sub subscriptions.yaml
|
ytdl-sub sub subscriptions.yaml
|
||||||
```
|
```
|
||||||
To preview what your output files before doing any downloads, you can dry run using:
|
See our
|
||||||
```shell
|
[example subscriptions](https://github.com/jmbannon/ytdl-sub/tree/master/examples)
|
||||||
ytdl-sub --dry-run sub subscriptions.yaml
|
for more detailed examples and use-cases.
|
||||||
```
|
|
||||||
|
|
||||||
### 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"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Output
|
### Output
|
||||||
After `ytdl-sub` runs, the end result will download and format the channel
|
After `ytdl-sub` runs, the end result will download and format the files into something ready
|
||||||
files into something ready to be consumed by your favorite media player or
|
to be consumed by your favorite media player/server.
|
||||||
server.
|
|
||||||
```
|
```
|
||||||
/path/to/tv_shows/Rick Aß
|
tv_shows/
|
||||||
/Season 2021
|
Jake Trains/
|
||||||
s2021.e0317 - Pattys Day Video-thumb.jpg
|
Season 2021/
|
||||||
s2021.e0317 - Pattys Day Video.mp4
|
s2021.e031701 - Pattys Day Video-thumb.jpg
|
||||||
s2021.e0317 - Pattys Day Video.nfo
|
s2021.e031701 - Pattys Day Video.mp4
|
||||||
/Season 2022
|
s2021.e031701 - Pattys Day Video.nfo
|
||||||
s2022.e1225 - Merry Christmas-thumb.jpg
|
s2021.e031702 - Second Pattys Day Video-thumb.jpg
|
||||||
s2022.e1225 - Merry Christmas.mp4
|
s2021.e031702 - Second Pattys Day Video.mp4
|
||||||
s2022.e1225 - Merry Christmas.nfo
|
s2021.e031702 - Second Pattys Day Video.nfo
|
||||||
poster.jpg
|
Season 2022/
|
||||||
fanart.jpg
|
s2022.e122501 - Merry Christmas-thumb.jpg
|
||||||
tvshow.nfo
|
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
|
## Custom Configs
|
||||||
The above example made heavy-use of `ytdl-sub` prebuilt presets and hides many
|
Any part of this process is modifiable by using custom configs. See our
|
||||||
features that are offered. `ytdl-sub` strives to support _any_ use case that first requires
|
[walk-through guide](https://ytdl-sub.readthedocs.io/en/latest/guides/index.html)
|
||||||
a download via yt-dlp. Use `ytdl-sub` to download, format, and convert media for your media
|
on how to build your first config from scratch. Ready-to-use
|
||||||
player to recognize downloads as:
|
[example configurations](https://github.com/jmbannon/ytdl-sub/tree/master/examples)
|
||||||
- Movies
|
can be found here alongside our
|
||||||
- TV shows
|
[readthedocs](https://ytdl-sub.readthedocs.io/en/latest/index.html)
|
||||||
- From a single channel or playlist
|
for detailed information on all config fields.
|
||||||
- From multiple channels or playlists
|
|
||||||
- From individual videos
|
|
||||||
- Extracted audio as podcasts
|
|
||||||
- Music videos
|
|
||||||
- Music, including:
|
|
||||||
- Individual songs
|
|
||||||
- Albums
|
|
||||||
- Discographies
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
`ytdl-sub` can be installed on the following platforms.
|
`ytdl-sub` can be installed on the following platforms.
|
||||||
|
|
||||||
- [Docker Compose](https://ytdl-sub.readthedocs.io/en/latest/install.html#docker-compose_)
|
- [Docker Compose](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#install-with-docker-compose)
|
||||||
- [with CPU passthrough](https://ytdl-sub.readthedocs.io/en/latest/install.html#cpu-passthrough)
|
- [Web-GUI](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#install-with-docker-compose)
|
||||||
- [with GPU passthrough](https://ytdl-sub.readthedocs.io/en/latest/install.html#nvidia-gpu-passthrough)
|
- [Headless](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#install-with-docker-compose)
|
||||||
- [Docker CLI](https://ytdl-sub.readthedocs.io/en/latest/install.html#docker)
|
- [CPU / GPU Passthrough](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#device-passthrough)
|
||||||
- [Windows](https://ytdl-sub.readthedocs.io/en/latest/install.html#windows)
|
- [Docker CLI](https://ytdl-sub.readthedocs.io/en/latest/guides/install/docker.html#docker-cli)
|
||||||
- [Unraid](https://ytdl-sub.readthedocs.io/en/latest/install.html#unraid)
|
- [Windows](https://ytdl-sub.readthedocs.io/en/latest/guides/install/windows.html)
|
||||||
- [Linux](https://ytdl-sub.readthedocs.io/en/latest/install.html#linux)
|
- [Unraid](https://ytdl-sub.readthedocs.io/en/latest/guides/install/unraid.html)
|
||||||
- [Linux ARM](https://ytdl-sub.readthedocs.io/en/latest/install.html#linux-arm)
|
- [Linux](https://ytdl-sub.readthedocs.io/en/latest/guides/install/linux.html)
|
||||||
- [PIP](https://ytdl-sub.readthedocs.io/en/latest/install.html#pip)
|
- [Linux ARM](https://ytdl-sub.readthedocs.io/en/latest/guides/install/linux.html)
|
||||||
- [Local Install](https://ytdl-sub.readthedocs.io/en/latest/install.html#local-install)
|
- [PIP](https://ytdl-sub.readthedocs.io/en/latest/guides/install/agnostic.html#pip-install)
|
||||||
- [Local Docker Build](https://ytdl-sub.readthedocs.io/en/latest/install.html#local-docker-build)
|
- [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``.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
There are many ways to contribute, even without coding. Please take a look in
|
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
|
We are pretty active in our
|
||||||
[Discord channel](https://discord.gg/v8j9RAHb4k)
|
[Discord channel](https://discord.gg/v8j9RAHb4k)
|
||||||
if you have any questions. Also see our
|
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.
|
for commonly asked questions.
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,25 @@ FROM ghcr.io/linuxserver/baseimage-alpine:edge
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# YTDL-SUB INSTALL
|
# YTDL-SUB INSTALL
|
||||||
|
|
||||||
# Needed for phantomjs
|
# For phantomjs
|
||||||
ENV OPENSSL_CONF="/etc/ssl"
|
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/ /
|
COPY root/ /
|
||||||
RUN mkdir -p /config && \
|
RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
||||||
apk update --no-cache && \
|
apk update --no-cache && \
|
||||||
apk upgrade --no-cache && \
|
apk upgrade --no-cache && \
|
||||||
apk add --no-cache --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \
|
apk add --no-cache --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \
|
||||||
vim \
|
vim \
|
||||||
g++ \
|
g++ \
|
||||||
nano \
|
nano \
|
||||||
|
unzip \
|
||||||
make \
|
make \
|
||||||
|
deno \
|
||||||
|
libffi-dev \
|
||||||
"python3>=3.10" \
|
"python3>=3.10" \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
|
|
@ -25,34 +32,44 @@ RUN mkdir -p /config && \
|
||||||
"aria2>=1.36.0" && \
|
"aria2>=1.36.0" && \
|
||||||
ffmpeg -version && \
|
ffmpeg -version && \
|
||||||
aria2c --version && \
|
aria2c --version && \
|
||||||
|
deno --version && \
|
||||||
# Install phantomjs if using x86_64, ensure it is properly installed
|
# Install phantomjs if using x86_64, ensure it is properly installed
|
||||||
if [[ $(uname -m) == "x86_64" ]]; then \
|
if [[ $(uname -m) == "x86_64" ]]; then \
|
||||||
|
echo "installing phantomjs" && \
|
||||||
apk add --no-cache gcompat && \
|
apk add --no-cache gcompat && \
|
||||||
cd /usr/share && \
|
tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
|
||||||
curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xj && \
|
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/share/phantomjs && \
|
||||||
mv /usr/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs phantomjs && \
|
rm -rf phantomjs-2.1.1-linux-x86_64 && \
|
||||||
rm -rf /usr/share/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 && \
|
ln -s /usr/share/phantomjs /usr/bin/phantomjs && \
|
||||||
echo "Phantom JS version:" && \
|
echo "Phantom JS version:" && \
|
||||||
phantomjs --version && \
|
phantomjs --version && \
|
||||||
cd -; \
|
cd -; \
|
||||||
fi && \
|
fi && \
|
||||||
echo "hi" && \
|
# Configure pip globally
|
||||||
# Install ytdl-sub, ensure it is installed properly
|
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
|
||||||
python3 -m pip install --break-system-packages --no-cache-dir ytdl_sub-*.whl && \
|
# 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 && \
|
ytdl-sub -h && \
|
||||||
# Delete unneeded packages after install
|
# Delete unneeded packages after install
|
||||||
rm ytdl_sub-*.whl && \
|
rm ytdl_sub-*.whl && \
|
||||||
apk del \
|
apk del \
|
||||||
g++ \
|
g++ \
|
||||||
make \
|
make \
|
||||||
py3-pip \
|
libffi-dev && \
|
||||||
py3-setuptools
|
python3 -m pip --help
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# CONTAINER CONFIGS
|
# CONTAINER CONFIGS
|
||||||
|
|
||||||
ENV EDITOR="nano" \
|
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
97
docker/Dockerfile.gui
Normal 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
1
docker/Dockerfile.headless
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
Dockerfile
|
||||||
|
|
@ -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
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Needed for phantomjs
|
# For phantomjs
|
||||||
ENV OPENSSL_CONF=/etc/ssl
|
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
|
# YTDL-SUB INSTALL
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-c"]
|
SHELL ["/bin/bash", "-c"]
|
||||||
COPY root/ /
|
COPY root/ /
|
||||||
RUN mkdir -p /config && \
|
RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
||||||
apt-get -y update && \
|
apt-get -y update && \
|
||||||
apt-get -y upgrade && \
|
apt-get -y upgrade && \
|
||||||
apt-get install --no-install-recommends -y \
|
apt-get install --no-install-recommends -y \
|
||||||
|
|
@ -22,8 +26,8 @@ RUN mkdir -p /config && \
|
||||||
vim \
|
vim \
|
||||||
g++ \
|
g++ \
|
||||||
nano \
|
nano \
|
||||||
|
unzip \
|
||||||
make \
|
make \
|
||||||
python3.10-dev \
|
|
||||||
python3-pip \
|
python3-pip \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
xz-utils \
|
xz-utils \
|
||||||
|
|
@ -53,16 +57,21 @@ RUN mkdir -p /config && \
|
||||||
ffmpeg -version && \
|
ffmpeg -version && \
|
||||||
# Install phantomjs if using x86_64, ensure it is properly installed
|
# Install phantomjs if using x86_64, ensure it is properly installed
|
||||||
if [[ $(uname -m) == "x86_64" ]]; then \
|
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 && \
|
echo "installing phantomjs" && \
|
||||||
tar -xvf phantomjs.tar.bz2 && \
|
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 && \
|
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/phantomjs && \
|
||||||
rm -rf phantomjs-2.1.1-linux-x86_64/ && \
|
rm -rf phantomjs-2.1.1-linux-x86_64 && \
|
||||||
rm phantomjs.tar.bz2 && \
|
rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
|
||||||
echo "Phantom JS version:" && \
|
echo "Phantom JS version:" && \
|
||||||
phantomjs --version ; \
|
phantomjs --version ; \
|
||||||
fi && \
|
fi && \
|
||||||
# Install ytdl-sub, ensure it is installed properly
|
# Install Deno, required for YouTube downloads
|
||||||
pip install --no-cache-dir ytdl_sub-*.whl && \
|
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 && \
|
ytdl-sub -h && \
|
||||||
# Delete unneeded packages after install
|
# Delete unneeded packages after install
|
||||||
rm ytdl_sub-*.whl && \
|
rm ytdl_sub-*.whl && \
|
||||||
|
|
@ -70,17 +79,22 @@ RUN mkdir -p /config && \
|
||||||
g++ \
|
g++ \
|
||||||
make \
|
make \
|
||||||
xz-utils \
|
xz-utils \
|
||||||
bzip2 \
|
bzip2 && \
|
||||||
python3.10-dev \
|
|
||||||
python3-venv && \
|
|
||||||
apt-get autoremove -y && \
|
apt-get autoremove -y && \
|
||||||
apt-get purge -y --auto-remove && \
|
apt-get purge -y --auto-remove && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
python3 -m pip --help
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# CONTAINER CONFIGS
|
# CONTAINER CONFIGS
|
||||||
|
|
||||||
ENV EDITOR="nano" \
|
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}"
|
||||||
|
|
|
||||||
86
docker/root/custom-cont-init.d/defaults
Executable file
86
docker/root/custom-cont-init.d/defaults
Executable 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
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
# Bare-bones config. Here are some useful links to get started:
|
# 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
|
# Config Examples: https://github.com/jmbannon/ytdl-sub/tree/master/examples
|
||||||
# Prebuilt Presets: https://ytdl-sub.readthedocs.io/en/latest/presets.html
|
# Prebuilt Presets: https://ytdl-sub.readthedocs.io/en/latest/prebuilt_presets/index.html
|
||||||
# Config Docs: https://ytdl-sub.readthedocs.io/en/latest/config.html
|
# Config Reference: https://ytdl-sub.readthedocs.io/en/latest/config_reference/index.html
|
||||||
#
|
#
|
||||||
# Examples are included in the /config/examples/ directory.
|
# The subscriptions in `subscriptions.yaml` uses prebuilt presets which do not require
|
||||||
# Any config and subscription can be ran using:
|
# any additions to this config. They can be downloaded using the command:
|
||||||
# ytdl-sub --config /path/to/config.yaml sub /path/to/subscriptions.yaml
|
#
|
||||||
|
# ytdl-sub --config config.yaml sub subscriptions.yaml
|
||||||
#
|
#
|
||||||
# Or dry-ran with:
|
# 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:
|
configuration:
|
||||||
working_directory: '/tmp/ytdl-sub-downloads'
|
# Avoid unnecessarily long large file renames, set this to a path on the same
|
||||||
|
# filesystem as the destination for downloaded files in the `overrides: /
|
||||||
presets:
|
# *_directory:` paths:
|
||||||
|
working_directory: ".ytdl-sub-working-directory"
|
||||||
video:
|
|
||||||
output_options:
|
|
||||||
output_directory: "/tmp/ytdl-sub-output"
|
|
||||||
file_name: "{uid}.{ext}"
|
|
||||||
thumbnail_name: "{uid}.{thumbnail_ext}"
|
|
||||||
|
|
|
||||||
15
docker/root/defaults/cron
Normal file
15
docker/root/defaults/cron
Normal 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
|
||||||
BIN
docker/root/defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2
Normal file
BIN
docker/root/defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2
Normal file
Binary file not shown.
|
|
@ -1,17 +1,88 @@
|
||||||
# Bare-bones config. Here are some useful links to get started:
|
# subscriptions.yaml:
|
||||||
# Walk-through Guide: https://github.com/jmbannon/ytdl-sub/wiki/1.-Introduction
|
# Everything in here can be downloaded using the command:
|
||||||
# Config Examples: https://github.com/jmbannon/ytdl-sub/tree/master/examples
|
# ytdl-sub sub subscriptions.yaml
|
||||||
# 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"
|
|
||||||
|
|
||||||
|
# __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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
15
docker/root/etc/s6-overlay/s6-rc.d/init-adduser/branding
Normal file
15
docker/root/etc/s6-overlay/s6-rc.d/init-adduser/branding
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
――――――――――――――――――――――――――――――――――――
|
||||||
|
██╗ ██╗████████╗██████╗ ██╗
|
||||||
|
╚██╗ ██╔╝╚══██╔══╝██╔══██╗██║
|
||||||
|
╚████╔╝ ██║ ██║ ██║██║
|
||||||
|
╚██╔╝ ██║ ██║ ██║██║
|
||||||
|
██║ ██║ ██████╔╝███████╗
|
||||||
|
╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||||
|
|
||||||
|
███████╗██╗ ██╗██████╗
|
||||||
|
██╔════╝██║ ██║██╔══██╗
|
||||||
|
███████╗██║ ██║██████╔╝
|
||||||
|
╚════██║██║ ██║██╔══██╗
|
||||||
|
███████║╚██████╔╝██████╔╝
|
||||||
|
╚══════╝ ╚═════╝ ╚═════╝
|
||||||
|
――――――――――――――――――――――――――――――――――――
|
||||||
46
docker/testing/Makefile
Normal file
46
docker/testing/Makefile
Normal 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 "$(@)"
|
||||||
50
docker/testing/docker-compose.yml
Normal file
50
docker/testing/docker-compose.yml
Normal 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"
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
# from the environment for the first two.
|
# from the environment for the first two.
|
||||||
SPHINXOPTS ?=
|
SPHINXOPTS ?=
|
||||||
SPHINXBUILD ?= sphinx-build
|
SPHINXBUILD ?= sphinx-build
|
||||||
SOURCEDIR = .
|
SOURCEDIR = source
|
||||||
BUILDDIR = _build
|
BUILDDIR = build
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
# Put it first so that "make" without argument is like "make help".
|
||||||
help:
|
help:
|
||||||
|
|
|
||||||
67
docs/conf.py
67
docs/conf.py
|
|
@ -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"}
|
|
||||||
374
docs/config.rst
374
docs/config.rst
|
|
@ -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()
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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.
|
|
||||||
BIN
docs/images/plex_agent_sources.png
Normal file
BIN
docs/images/plex_agent_sources.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
BIN
docs/images/plex_scanner_agent.png
Normal file
BIN
docs/images/plex_scanner_agent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
docs/images/unraid_badconsole.png
Normal file
BIN
docs/images/unraid_badconsole.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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
|
|
||||||
209
docs/install.rst
209
docs/install.rst
|
|
@ -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.
|
|
||||||
|
|
||||||
|
|
@ -7,8 +7,8 @@ REM Command file for Sphinx documentation
|
||||||
if "%SPHINXBUILD%" == "" (
|
if "%SPHINXBUILD%" == "" (
|
||||||
set SPHINXBUILD=sphinx-build
|
set SPHINXBUILD=sphinx-build
|
||||||
)
|
)
|
||||||
set SOURCEDIR=.
|
set SOURCEDIR=source
|
||||||
set BUILDDIR=_build
|
set BUILDDIR=build
|
||||||
|
|
||||||
%SPHINXBUILD% >NUL 2>NUL
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
if errorlevel 9009 (
|
if errorlevel 9009 (
|
||||||
|
|
|
||||||
183
docs/presets.rst
183
docs/presets.rst
|
|
@ -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
70
docs/source/conf.py
Normal 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"),
|
||||||
|
}
|
||||||
134
docs/source/config_reference/config_yaml.rst
Normal file
134
docs/source/config_reference/config_yaml.rst
Normal 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.
|
||||||
51
docs/source/config_reference/index.rst
Normal file
51
docs/source/config_reference/index.rst
Normal 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
|
||||||
1086
docs/source/config_reference/plugins.rst
Normal file
1086
docs/source/config_reference/plugins.rst
Normal file
File diff suppressed because it is too large
Load diff
33
docs/source/config_reference/prebuilt_presets/common.rst
Normal file
33
docs/source/config_reference/prebuilt_presets/common.rst
Normal 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
|
||||||
11
docs/source/config_reference/prebuilt_presets/index.rst
Normal file
11
docs/source/config_reference/prebuilt_presets/index.rst
Normal 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
|
||||||
10
docs/source/config_reference/prebuilt_presets/music.rst
Normal file
10
docs/source/config_reference/prebuilt_presets/music.rst
Normal 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
|
||||||
10
docs/source/config_reference/prebuilt_presets/tv_show.rst
Normal file
10
docs/source/config_reference/prebuilt_presets/tv_show.rst
Normal 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
|
||||||
688
docs/source/config_reference/scripting/entry_variables.rst
Normal file
688
docs/source/config_reference/scripting/entry_variables.rst
Normal 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 entry’s 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 entry’s uploaded date, in YYYYMMDD format. If not present, return today’s 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``.
|
||||||
210
docs/source/config_reference/scripting/index.rst
Normal file
210
docs/source/config_reference/scripting/index.rst
Normal 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") }
|
||||||
889
docs/source/config_reference/scripting/scripting_functions.rst
Normal file
889
docs/source/config_reference/scripting/scripting_functions.rst
Normal 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.
|
||||||
408
docs/source/config_reference/scripting/scripting_types.rst
Normal file
408
docs/source/config_reference/scripting/scripting_types.rst
Normal 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``.
|
||||||
103
docs/source/config_reference/scripting/static_variables.rst
Normal file
103
docs/source/config_reference/scripting/static_variables.rst
Normal 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``.
|
||||||
128
docs/source/config_reference/subscription_yaml.rst
Normal file
128
docs/source/config_reference/subscription_yaml.rst
Normal 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
78
docs/source/debugging.rst
Normal 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
|
||||||
113
docs/source/deprecation_notices.rst
Normal file
113
docs/source/deprecation_notices.rst
Normal 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
294
docs/source/faq/index.rst
Normal 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>`
|
||||||
98
docs/source/guides/development/index.rst
Normal file
98
docs/source/guides/development/index.rst
Normal 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.
|
||||||
132
docs/source/guides/getting_started/automating_downloads.rst
Normal file
132
docs/source/guides/getting_started/automating_downloads.rst
Normal 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
|
||||||
69
docs/source/guides/getting_started/downloading.rst
Normal file
69
docs/source/guides/getting_started/downloading.rst
Normal 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>`.
|
||||||
263
docs/source/guides/getting_started/first_config.rst
Normal file
263
docs/source/guides/getting_started/first_config.rst
Normal 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.
|
||||||
157
docs/source/guides/getting_started/index.rst
Normal file
157
docs/source/guides/getting_started/index.rst
Normal 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
|
||||||
54
docs/source/guides/getting_started/quick_start.rst
Normal file
54
docs/source/guides/getting_started/quick_start.rst
Normal 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>`.
|
||||||
167
docs/source/guides/getting_started/subscriptions.rst
Normal file
167
docs/source/guides/getting_started/subscriptions.rst
Normal 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>`.
|
||||||
7
docs/source/guides/index.rst
Normal file
7
docs/source/guides/index.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Guides
|
||||||
|
======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
install/index
|
||||||
|
getting_started/index
|
||||||
|
development/index
|
||||||
49
docs/source/guides/install/agnostic.rst
Normal file
49
docs/source/guides/install/agnostic.rst
Normal 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
|
||||||
146
docs/source/guides/install/docker.rst
Normal file
146
docs/source/guides/install/docker.rst
Normal 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.
|
||||||
35
docs/source/guides/install/index.rst
Normal file
35
docs/source/guides/install/index.rst
Normal 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
|
||||||
52
docs/source/guides/install/linux.rst
Normal file
52
docs/source/guides/install/linux.rst
Normal 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
|
||||||
29
docs/source/guides/install/unraid.rst
Normal file
29
docs/source/guides/install/unraid.rst
Normal 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
|
||||||
17
docs/source/guides/install/windows.rst
Normal file
17
docs/source/guides/install/windows.rst
Normal 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
15
docs/source/index.rst
Normal 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
|
||||||
98
docs/source/introduction.rst
Normal file
98
docs/source/introduction.rst
Normal 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.
|
||||||
237
docs/source/prebuilt_presets/helpers.rst
Normal file
237
docs/source/prebuilt_presets/helpers.rst
Normal 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) }
|
||||||
37
docs/source/prebuilt_presets/index.rst
Normal file
37
docs/source/prebuilt_presets/index.rst
Normal 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
|
||||||
33
docs/source/prebuilt_presets/media_quality.rst
Normal file
33
docs/source/prebuilt_presets/media_quality.rst
Normal 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``
|
||||||
90
docs/source/prebuilt_presets/music.rst
Normal file
90
docs/source/prebuilt_presets/music.rst
Normal 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/"
|
||||||
5
docs/source/prebuilt_presets/music_videos.rst
Normal file
5
docs/source/prebuilt_presets/music_videos.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
===================
|
||||||
|
Music Video Presets
|
||||||
|
===================
|
||||||
|
|
||||||
|
WIP
|
||||||
255
docs/source/prebuilt_presets/tv_shows.rst
Normal file
255
docs/source/prebuilt_presets/tv_shows.rst
Normal 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.
|
||||||
3
docs/source/requirements.txt
Normal file
3
docs/source/requirements.txt
Normal 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
146
docs/source/usage.rst
Normal 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'
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
# Example Configurations
|
# Example Configurations
|
||||||
This directory shows how you can use ytdl-sub for various use cases. These
|
This directory shows how use ytdl-sub's built-in presets to start downloading immediately with no
|
||||||
are the configs I personally use and have incorporated as part of the e2e tests.
|
configuration required. Simply run:
|
||||||
|
|
||||||
Each example has a `config.yaml` and `subscription.yaml`. The config defines
|
### Unix
|
||||||
_how_ you format your media, whereas the subscription defines _what_ you
|
```commandline
|
||||||
download plus some additional configuring if needed.
|
ytdl-sub sub tv_show_subscriptions.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
```commandline
|
||||||
|
./ytdl-sub.exe sub tv_show_subscriptions.yaml
|
||||||
|
```
|
||||||
101
examples/advanced/tv_show_config.yaml
Normal file
101
examples/advanced/tv_show_config.yaml
Normal 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
|
||||||
29
examples/advanced/tv_show_subscriptions.yaml
Normal file
29
examples/advanced/tv_show_subscriptions.yaml
Normal 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
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
||||||
52
examples/music_subscriptions.yaml
Normal file
52
examples/music_subscriptions.yaml
Normal 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/"
|
||||||
|
|
||||||
45
examples/music_video_subscriptions.yaml
Normal file
45
examples/music_video_subscriptions.yaml
Normal 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"
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
||||||
|
|
@ -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"
|
|
||||||
#
|
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
||||||
|
|
@ -1,16 +1,77 @@
|
||||||
# Define any number subscriptions in a single file, or use multiple
|
# This example downloads the entirety of a channel (not limited to YouTube).
|
||||||
# files to organize (i.e by genre)
|
# Files will be stored in the form of:
|
||||||
|
#
|
||||||
rick_a:
|
# /tv_shows
|
||||||
# Mix and match any number of presets to use. You can either lump all settings into
|
# /Season 2021
|
||||||
# a single preset to save some lines, or modularize them and use many.
|
# s2021.e031701 - Pattys Day Video-thumb.jpg
|
||||||
preset:
|
# s2021.e031701 - Pattys Day Video.mp4
|
||||||
- "tv_show"
|
# s2021.e031701 - Pattys Day Video.nfo
|
||||||
- "only_recent_videos"
|
# 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:
|
overrides:
|
||||||
# Required to specify for every subscription
|
tv_show_directory: "/tv_shows" # Root folder of all ytdl-sub TV Shows
|
||||||
tv_show_name: "Rick A"
|
|
||||||
url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
|
# For 'Only Recent' preset, only keep vids within this range and limit
|
||||||
# Optional
|
only_recent_date_range: "2months"
|
||||||
tv_show_genre: "Music"
|
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"
|
||||||
|
|
|
||||||
120
pyproject.toml
120
pyproject.toml
|
|
@ -1,10 +1,69 @@
|
||||||
[tool.isort]
|
[project]
|
||||||
profile = "black"
|
name ="ytdl-sub"
|
||||||
line_length = 100
|
dynamic = [ "version" ]
|
||||||
force_single_line = true
|
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]
|
[build-system]
|
||||||
line_length = 100
|
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]
|
[tool.pylint.MASTER]
|
||||||
disable = [
|
disable = [
|
||||||
|
|
@ -15,30 +74,43 @@ disable = [
|
||||||
"R0913", # Too many arguments
|
"R0913", # Too many arguments
|
||||||
"R0901", # too-many-ancestors
|
"R0901", # too-many-ancestors
|
||||||
"R0902", # too-many-instance-attributes
|
"R0902", # too-many-instance-attributes
|
||||||
|
"R1711", # useless-return
|
||||||
|
"R0917", # too many positional arguments
|
||||||
"W0511", # TODO
|
"W0511", # TODO
|
||||||
]
|
]
|
||||||
|
|
||||||
load-plugins = "pylint.extensions.docparams"
|
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]
|
[tool.coverage.run]
|
||||||
include = [
|
include = [
|
||||||
"src/*"
|
"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"
|
||||||
59
setup.cfg
59
setup.cfg
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__pypi_version__ = "2023.03.24.post7"
|
__pypi_version__ = "2023.10.22.post3"
|
||||||
__local_version__ = "2023.03.24+14e4a4b"
|
__local_version__ = "2023.10.22+bfba4f0"
|
||||||
|
|
|
||||||
346
src/ytdl_sub/cli/entrypoint.py
Normal file
346
src/ytdl_sub/cli/entrypoint.py
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
117
src/ytdl_sub/cli/output_summary.py
Normal file
117
src/ytdl_sub/cli/output_summary.py
Normal 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))
|
||||||
53
src/ytdl_sub/cli/output_transaction_log.py
Normal file
53
src/ytdl_sub/cli/output_transaction_log.py
Normal 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)
|
||||||
63
src/ytdl_sub/cli/parsers/cli_to_sub.py
Normal file
63
src/ytdl_sub/cli/parsers/cli_to_sub.py
Normal 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}))
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
from typing import Dict
|
from typing import Any, Dict, List, Tuple
|
||||||
from typing import List
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from mergedeep import mergedeep
|
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.config.config_validator import ConfigOptions
|
||||||
from ytdl_sub.utils.exceptions import InvalidDlArguments
|
from ytdl_sub.utils.exceptions import InvalidDlArguments
|
||||||
|
|
||||||
|
|
@ -116,7 +115,7 @@ class DownloadArgsParser:
|
||||||
return largest_consecutive + 1
|
return largest_consecutive + 1
|
||||||
|
|
||||||
@classmethod
|
@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_name: Argument name in the form of 'key1.key2.key3'
|
||||||
:param arg_value: Argument value
|
:param arg_value: Argument value
|
||||||
|
|
@ -134,11 +133,15 @@ class DownloadArgsParser:
|
||||||
|
|
||||||
next_dict[arg_name_split[-1]] = arg_value
|
next_dict[arg_name_split[-1]] = arg_value
|
||||||
|
|
||||||
# TODO: handle ints/floats
|
if isinstance(arg_value, str):
|
||||||
if arg_value == "True":
|
if arg_value == "True":
|
||||||
next_dict[arg_name_split[-1]] = True
|
next_dict[arg_name_split[-1]] = True
|
||||||
elif arg_value == "False":
|
elif arg_value == "False":
|
||||||
next_dict[arg_name_split[-1]] = 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
|
return argument_dict
|
||||||
|
|
||||||
|
|
@ -236,9 +239,19 @@ class DownloadArgsParser:
|
||||||
|
|
||||||
return subscription_dict
|
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))
|
to_hash = str(sorted(self._unknown_arguments))
|
||||||
return hashlib.sha256(hash_string.encode()).hexdigest()[-8:]
|
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
|
||||||
|
)
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import argparse
|
import argparse
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from typing import List
|
from typing import Dict, List
|
||||||
|
|
||||||
from ytdl_sub import __local_version__
|
from ytdl_sub import __local_version__
|
||||||
from ytdl_sub.utils.logger import LoggerLevels
|
from ytdl_sub.utils.logger import LoggerLevels
|
||||||
|
|
||||||
|
DEFAULT_CONFIG_FILE_NAME: str = "config.yaml"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class CLIArgument:
|
class CLIArgument:
|
||||||
|
|
@ -38,6 +40,11 @@ class MainArguments:
|
||||||
long="--suppress-transaction-log",
|
long="--suppress-transaction-log",
|
||||||
is_positional=True,
|
is_positional=True,
|
||||||
)
|
)
|
||||||
|
MATCH = CLIArgument(
|
||||||
|
short="-m",
|
||||||
|
long="--match",
|
||||||
|
)
|
||||||
|
SUPPRESS_COLORS = CLIArgument(short="-nc", long="--suppress-colors")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls) -> List[CLIArgument]:
|
def all(cls) -> List[CLIArgument]:
|
||||||
|
|
@ -52,6 +59,8 @@ class MainArguments:
|
||||||
cls.LOG_LEVEL,
|
cls.LOG_LEVEL,
|
||||||
cls.TRANSACTION_LOG,
|
cls.TRANSACTION_LOG,
|
||||||
cls.SUPPRESS_TRANSACTION_LOG,
|
cls.SUPPRESS_TRANSACTION_LOG,
|
||||||
|
cls.MATCH,
|
||||||
|
cls.SUPPRESS_COLORS,
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -86,8 +95,8 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
|
||||||
MainArguments.CONFIG.long,
|
MainArguments.CONFIG.long,
|
||||||
metavar="CONFIGPATH",
|
metavar="CONFIGPATH",
|
||||||
type=str,
|
type=str,
|
||||||
help="path to the config yaml, uses config.yaml if not provided",
|
help=f"path to the config yaml, uses {DEFAULT_CONFIG_FILE_NAME} if not provided",
|
||||||
default=argparse.SUPPRESS if suppress_defaults else "config.yaml",
|
default=argparse.SUPPRESS if suppress_defaults else None, # Default is set downstream
|
||||||
)
|
)
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
MainArguments.DRY_RUN.short,
|
MainArguments.DRY_RUN.short,
|
||||||
|
|
@ -102,8 +111,8 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
|
||||||
MainArguments.LOG_LEVEL.long,
|
MainArguments.LOG_LEVEL.long,
|
||||||
metavar="|".join(LoggerLevels.names()),
|
metavar="|".join(LoggerLevels.names()),
|
||||||
type=str,
|
type=str,
|
||||||
help="level of logs to print to console, defaults to info",
|
help="level of logs to print to console, defaults to verbose",
|
||||||
default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.INFO.name,
|
default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.VERBOSE.name,
|
||||||
choices=LoggerLevels.names(),
|
choices=LoggerLevels.names(),
|
||||||
dest="ytdl_sub_log_level",
|
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",
|
help="do not output transaction logs to console or file",
|
||||||
default=argparse.SUPPRESS if suppress_defaults else False,
|
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)
|
_add_shared_arguments(parser, suppress_defaults=False)
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="subparser")
|
subparsers = parser.add_subparsers(dest="subparser")
|
||||||
|
|
||||||
|
|
||||||
###################################################################################################
|
###################################################################################################
|
||||||
# SUBSCRIPTION PARSER
|
# SUBSCRIPTION PARSER
|
||||||
class SubArguments:
|
class SubArguments:
|
||||||
|
|
@ -140,6 +168,14 @@ class SubArguments:
|
||||||
short="-u",
|
short="-u",
|
||||||
long="--update-with-info-json",
|
long="--update-with-info-json",
|
||||||
)
|
)
|
||||||
|
OVERRIDE = CLIArgument(
|
||||||
|
short="-o",
|
||||||
|
long="--dl-override",
|
||||||
|
)
|
||||||
|
SHUFFLE = CLIArgument(
|
||||||
|
short="-sh",
|
||||||
|
long="--shuffle",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
subscription_parser = subparsers.add_parser("sub")
|
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",
|
help="update all subscriptions with the current config using info.json files",
|
||||||
default=False,
|
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
|
# DOWNLOAD PARSER
|
||||||
|
|
@ -182,3 +232,81 @@ view_parser.add_argument(
|
||||||
help="View source variables after splitting by chapters",
|
help="View source variables after splitting by chapters",
|
||||||
)
|
)
|
||||||
view_parser.add_argument("url", help="URL to view source variables for")
|
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
Loading…
Reference in a new issue