Compare commits
3 commits
master
...
j/simplify
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c69c18f623 | ||
|
|
c2383614f3 | ||
|
|
94db90f113 |
647 changed files with 28192 additions and 20024 deletions
55
.github/workflows/ci-windows.yaml
vendored
55
.github/workflows/ci-windows.yaml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run unit tests with coverage
|
||||
run: |
|
||||
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
python -m pip install -e .[test]
|
||||
python -m pytest --reruns 3 --reruns-delay 5 tests/unit
|
||||
|
||||
test-integration:
|
||||
test-soundcloud:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -42,9 +42,9 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run integration tests with coverage
|
||||
- name: Run e2e soundcloud tests with coverage
|
||||
run: |
|
||||
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
|
||||
tar -xf ffmpeg.zip
|
||||
|
|
@ -52,10 +52,9 @@ jobs:
|
|||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||
|
||||
python -m pip install -e .[test]
|
||||
python -m pytest --reruns 3 --reruns-delay 5 tests/integration --ignore tests/integration/prebuilt_presets
|
||||
python -m pytest --reruns 3 --reruns-delay 5 tests/e2e/soundcloud
|
||||
|
||||
|
||||
test-integration-prebuilt-presets:
|
||||
test-bandcamp:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -66,19 +65,20 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run prebuilt preset integration tests with coverage
|
||||
- name: Run e2e soundcloud tests with coverage
|
||||
run: |
|
||||
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
|
||||
tar -xf ffmpeg.zip
|
||||
move "ffmpeg-master-latest-win64-gpl\bin\ffmpeg.exe" "ffmpeg.exe"
|
||||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||
|
||||
python -m pip install -e .[test]
|
||||
python -m pytest --reruns 3 --reruns-delay 5 tests/integration/prebuilt_presets
|
||||
|
||||
test-e2e:
|
||||
python -m pip install -e .[test]
|
||||
python -m pytest --reruns 3 --reruns-delay 5 tests/e2e/bandcamp
|
||||
|
||||
|
||||
test-youtube:
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -89,9 +89,9 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run e2e tests with coverage
|
||||
- name: Run e2e youtube tests with coverage
|
||||
run: |
|
||||
curl.exe -L -o ffmpeg.zip https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip
|
||||
tar -xf ffmpeg.zip
|
||||
|
|
@ -99,4 +99,27 @@ jobs:
|
|||
move "ffmpeg-master-latest-win64-gpl\bin\ffprobe.exe" "ffprobe.exe"
|
||||
|
||||
python -m pip install -e .[test]
|
||||
python -m pytest tests/e2e
|
||||
python -m pytest --reruns 3 --reruns-delay 5 tests/e2e/youtube
|
||||
|
||||
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 --reruns 3 --reruns-delay 5 tests/e2e/plugins
|
||||
|
|
|
|||
107
.github/workflows/ci.yaml
vendored
107
.github/workflows/ci.yaml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
- master
|
||||
jobs:
|
||||
test-lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run linters
|
||||
run: |
|
||||
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
make check_lint
|
||||
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run unit tests with coverage
|
||||
run: |
|
||||
|
|
@ -52,8 +52,8 @@ jobs:
|
|||
path: /opt/coverage/unit
|
||||
key: ${{github.sha}}-coverage-unit
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
test-soundcloud:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
@ -63,23 +63,43 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run integration tests with coverage
|
||||
- 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 --reruns 3 --reruns-delay 5 tests/integration --ignore tests/integration/prebuilt_presets && coverage xml -o /opt/coverage/integration/coverage.xml
|
||||
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/e2e/soundcloud && coverage xml -o /opt/coverage/soundcloud/coverage.xml
|
||||
|
||||
test-bandcamp:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run e2e soundcloud tests with coverage
|
||||
run: |
|
||||
pip install -e .[test]
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg
|
||||
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/e2e/bandcamp && coverage xml -o /opt/coverage/bandcamp/coverage.xml
|
||||
|
||||
- name: Save coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/integration
|
||||
key: ${{github.sha}}-coverage-integration
|
||||
path: /opt/coverage/bandcamp
|
||||
key: ${{github.sha}}-coverage-bandcamp
|
||||
|
||||
test-integration-prebuilt-presets:
|
||||
runs-on: ubuntu-latest
|
||||
test-youtube:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
@ -89,23 +109,23 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run prebuilt preset integration tests with coverage
|
||||
- name: Run e2e youtube tests with coverage
|
||||
run: |
|
||||
pip install -e .[test]
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg
|
||||
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/integration/prebuilt_presets && coverage xml -o /opt/coverage/integration-prebuilt-presets/coverage.xml
|
||||
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/e2e/youtube && coverage xml -o /opt/coverage/youtube/coverage.xml
|
||||
|
||||
- name: Save coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/integration-prebuilt-presets
|
||||
key: ${{github.sha}}-coverage-integration-prebuilt-presets
|
||||
path: /opt/coverage/youtube
|
||||
key: ${{github.sha}}-coverage-youtube
|
||||
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
test-plugins:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
@ -115,22 +135,29 @@ jobs:
|
|||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Run e2e tests with coverage
|
||||
- name: Run e2e plugin tests with coverage
|
||||
run: |
|
||||
pip install -e .[test]
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg
|
||||
coverage run -m pytest tests/e2e && coverage xml -o /opt/coverage/e2e/coverage.xml
|
||||
coverage run -m pytest --reruns 3 --reruns-delay 5 tests/e2e/plugins && coverage xml -o /opt/coverage/plugins/coverage.xml
|
||||
|
||||
- name: Save coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/plugins
|
||||
key: ${{github.sha}}-coverage-plugins
|
||||
|
||||
codecov-upload:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
test-unit,
|
||||
test-integration,
|
||||
test-integration-prebuilt-presets,
|
||||
test-e2e
|
||||
test-soundcloud,
|
||||
test-bandcamp,
|
||||
test-youtube,
|
||||
test-plugins
|
||||
]
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -142,26 +169,32 @@ jobs:
|
|||
path: /opt/coverage/unit
|
||||
key: ${{github.sha}}-coverage-unit
|
||||
|
||||
- name: Restore integration test coverage
|
||||
- name: Restore soundcloud test coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/integration
|
||||
key: ${{github.sha}}-coverage-integration
|
||||
path: /opt/coverage/soundcloud
|
||||
key: ${{github.sha}}-coverage-soundcloud
|
||||
|
||||
- name: Restore integration prebuilt presets test coverage
|
||||
- name: Restore bandcamp test coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/integration-prebuilt-presets
|
||||
key: ${{github.sha}}-coverage-integration-prebuilt-presets
|
||||
path: /opt/coverage/bandcamp
|
||||
key: ${{github.sha}}-coverage-bandcamp
|
||||
|
||||
- name: Restore e2e test coverage
|
||||
- name: Restore youtube test coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/e2e
|
||||
key: ${{github.sha}}-coverage-e2e
|
||||
path: /opt/coverage/youtube
|
||||
key: ${{github.sha}}-coverage-youtube
|
||||
|
||||
- name: Restore plugins test coverage
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /opt/coverage/plugins
|
||||
key: ${{github.sha}}-coverage-plugins
|
||||
|
||||
- name: Upload code coverage to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: /opt/coverage/unit/coverage.xml,/opt/coverage/integration/coverage.xml,/opt/coverage/integration-prebuilt-presets/coverage.xml,/opt/coverage/e2e/coverage.xml
|
||||
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
|
||||
50
.github/workflows/package-gui.yaml
vendored
50
.github/workflows/package-gui.yaml
vendored
|
|
@ -63,10 +63,10 @@ jobs:
|
|||
echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.12" ]
|
||||
python-version: [ "3.10" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
|
||||
# Build ARM64 container, only on master branch to save time testing
|
||||
package-arm64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
build
|
||||
]
|
||||
|
|
@ -141,7 +141,7 @@ jobs:
|
|||
|
||||
# Build AMD64 container
|
||||
package-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
build
|
||||
]
|
||||
|
|
@ -186,7 +186,7 @@ jobs:
|
|||
# On master branch, build the docker manifest file from the cached
|
||||
# docker builds and push to the registry
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
version,
|
||||
build,
|
||||
|
|
@ -247,7 +247,7 @@ jobs:
|
|||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
echo IMAGE_ID=${IMAGE_ID}
|
||||
|
||||
|
||||
echo ::set-output name=IMAGE_ID::${IMAGE_ID}
|
||||
|
||||
- name: Get the version
|
||||
|
|
@ -260,33 +260,17 @@ jobs:
|
|||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "master" ] && VERSION=latest
|
||||
echo VERSION=${VERSION}
|
||||
|
||||
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
|
||||
labels: |
|
||||
runnumber=${{ github.run_number }}
|
||||
maintainer=${{ github.repository_owner }}
|
||||
org.opencontainers.image.authors=${{ github.repository_owner }}
|
||||
org.opencontainers.image.vendor=${{ github.repository_owner }}
|
||||
org.opencontainers.image.documentation=https://ytdl-sub.readthedocs.io/
|
||||
tags: |
|
||||
type=raw,value=${{ 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
|
||||
run: |
|
||||
docker buildx build --push \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--cache-from=type=local,src=/tmp/build-cache/amd64 \
|
||||
--cache-from=type=local,src=/tmp/build-cache/arm64 \
|
||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:${{ steps.formatted_version.outputs.VERSION }} \
|
||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:${{ needs.version.outputs.pypi_version }} \
|
||||
--label "runnumber=${GITHUB_RUN_ID}" \
|
||||
--file docker/Dockerfile.gui \
|
||||
docker/
|
||||
50
.github/workflows/package-ubuntu.yaml
vendored
50
.github/workflows/package-ubuntu.yaml
vendored
|
|
@ -63,10 +63,10 @@ jobs:
|
|||
echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.12" ]
|
||||
python-version: [ "3.10" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
|
||||
# Build ARM64 container, only on master branch to save time testing
|
||||
package-arm64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
build
|
||||
]
|
||||
|
|
@ -141,7 +141,7 @@ jobs:
|
|||
|
||||
# Build AMD64 container
|
||||
package-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
build
|
||||
]
|
||||
|
|
@ -186,7 +186,7 @@ jobs:
|
|||
# On master branch, build the docker manifest file from the cached
|
||||
# docker builds and push to the registry
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
version,
|
||||
build,
|
||||
|
|
@ -247,7 +247,7 @@ jobs:
|
|||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
echo IMAGE_ID=${IMAGE_ID}
|
||||
|
||||
|
||||
echo ::set-output name=IMAGE_ID::${IMAGE_ID}
|
||||
|
||||
- name: Get the version
|
||||
|
|
@ -260,33 +260,17 @@ jobs:
|
|||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "master" ] && VERSION=latest
|
||||
echo VERSION=${VERSION}
|
||||
|
||||
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
|
||||
labels: |
|
||||
runnumber=${{ github.run_number }}
|
||||
maintainer=${{ github.repository_owner }}
|
||||
org.opencontainers.image.authors=${{ github.repository_owner }}
|
||||
org.opencontainers.image.vendor=${{ github.repository_owner }}
|
||||
org.opencontainers.image.documentation=https://ytdl-sub.readthedocs.io/
|
||||
tags: |
|
||||
type=raw,value=ubuntu-${{ steps.formatted_version.outputs.VERSION }}
|
||||
type=raw,value=ubuntu-${{ needs.version.outputs.pypi_version }}
|
||||
|
||||
- name: Build Docker Image and push to registry
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
context: "docker/"
|
||||
file: "docker/Dockerfile.ubuntu"
|
||||
cache-from: |
|
||||
type=local,src=/tmp/build-cache/amd64
|
||||
type=local,src=/tmp/build-cache/arm64
|
||||
run: |
|
||||
docker buildx build --push \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--cache-from=type=local,src=/tmp/build-cache/amd64 \
|
||||
--cache-from=type=local,src=/tmp/build-cache/arm64 \
|
||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:ubuntu-${{ steps.formatted_version.outputs.VERSION }} \
|
||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:ubuntu-${{ needs.version.outputs.pypi_version }} \
|
||||
--label "runnumber=ubuntu-${GITHUB_RUN_ID}" \
|
||||
--file docker/Dockerfile.ubuntu \
|
||||
docker/
|
||||
51
.github/workflows/package.yaml
vendored
51
.github/workflows/package.yaml
vendored
|
|
@ -63,10 +63,10 @@ jobs:
|
|||
echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.12" ]
|
||||
python-version: [ "3.10" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
|
||||
# Build ARM64 container, only on master branch to save time testing
|
||||
package-arm64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
build
|
||||
]
|
||||
|
|
@ -136,7 +136,7 @@ jobs:
|
|||
|
||||
# Build AMD64 container
|
||||
package-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
build
|
||||
]
|
||||
|
|
@ -180,7 +180,7 @@ jobs:
|
|||
# On master branch, build the docker manifest file from the cached
|
||||
# docker builds and push to the registry
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [
|
||||
version,
|
||||
build,
|
||||
|
|
@ -241,7 +241,7 @@ jobs:
|
|||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
echo IMAGE_ID=${IMAGE_ID}
|
||||
|
||||
|
||||
echo ::set-output name=IMAGE_ID::${IMAGE_ID}
|
||||
|
||||
- name: Get the version
|
||||
|
|
@ -254,33 +254,16 @@ jobs:
|
|||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "master" ] && VERSION=latest
|
||||
echo VERSION=${VERSION}
|
||||
|
||||
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.formatted-image_id.outputs.IMAGE_ID }}
|
||||
labels: |
|
||||
runnumber=${{ github.run_number }}
|
||||
maintainer=${{ github.repository_owner }}
|
||||
org.opencontainers.image.authors=${{ github.repository_owner }}
|
||||
org.opencontainers.image.vendor=${{ github.repository_owner }}
|
||||
org.opencontainers.image.documentation=https://ytdl-sub.readthedocs.io/
|
||||
tags: |
|
||||
type=raw,value=${{ 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
|
||||
- name: Build Docker Image and push to registry
|
||||
run: |
|
||||
docker buildx build --push \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--cache-from=type=local,src=/tmp/build-cache/amd64 \
|
||||
--cache-from=type=local,src=/tmp/build-cache/arm64 \
|
||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:${{ steps.formatted_version.outputs.VERSION }} \
|
||||
--tag ${{ steps.formatted-image_id.outputs.IMAGE_ID }}:${{ needs.version.outputs.pypi_version }} \
|
||||
--label "runnumber=${GITHUB_RUN_ID}" \
|
||||
docker/
|
||||
43
.github/workflows/release.yaml
vendored
43
.github/workflows/release.yaml
vendored
|
|
@ -61,16 +61,9 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
arch: [ "aarch64", "x86_64" ]
|
||||
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 }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ matrix.container }}
|
||||
image: quay.io/pypa/manylinux_2_28_x86_64
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Write version to init file
|
||||
|
|
@ -83,17 +76,17 @@ jobs:
|
|||
dnf install -y epel-release tar wget make gcc openssl-devel bzip2-devel libffi-devel zlib-devel
|
||||
- name: Install Python
|
||||
run: |
|
||||
wget https://www.python.org/ftp/python/3.12.9/Python-3.12.9.tar.xz
|
||||
tar -xf Python-3.12.9.tar.xz
|
||||
cd Python-3.12.9 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
|
||||
wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tar.xz
|
||||
tar -xf Python-3.10.10.tar.xz
|
||||
cd Python-3.10.10 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
|
||||
make -j 8
|
||||
make altinstall
|
||||
python3.12 --version
|
||||
python3.12 -m ensurepip --upgrade
|
||||
python3.10 --version
|
||||
python3.10 -m ensurepip --upgrade
|
||||
- name: Build Package
|
||||
run: |
|
||||
python3.12 -m pip install -e .
|
||||
python3.12 -m pip install pyinstaller
|
||||
python3.10 -m pip install -e .
|
||||
python3.10 -m pip install pyinstaller
|
||||
# Build executable
|
||||
pyinstaller ytdl-sub.spec
|
||||
mkdir -p /opt/builds
|
||||
|
|
@ -102,7 +95,7 @@ jobs:
|
|||
mv dist/ytdl-sub /opt/builds/ytdl-sub_${{ matrix.arch }}
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ytdl-sub_${{ matrix.arch }}
|
||||
path: /opt/builds/ytdl-sub_${{ matrix.arch }}
|
||||
|
|
@ -113,13 +106,13 @@ jobs:
|
|||
name: build-windows
|
||||
needs:
|
||||
- version
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.10"
|
||||
- name: Write version to init file
|
||||
run: |
|
||||
echo '${{ needs.version.outputs.init_contents }}'> src/ytdl_sub/__init__.py
|
||||
|
|
@ -131,7 +124,7 @@ jobs:
|
|||
.\dist\ytdl-sub.exe -h
|
||||
|
||||
- name: Upload build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ytdl-sub_exe
|
||||
path: .\dist\ytdl-sub.exe
|
||||
|
|
@ -152,19 +145,19 @@ jobs:
|
|||
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
|
||||
|
||||
- name: Restore exe build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ytdl-sub_exe
|
||||
path: /opt/builds
|
||||
|
||||
- name: Restore aarch64 build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ytdl-sub_aarch64
|
||||
path: /opt/builds
|
||||
|
||||
- name: Restore x86_64 build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: ytdl-sub_x86_64
|
||||
path: /opt/builds
|
||||
|
|
@ -200,13 +193,13 @@ jobs:
|
|||
name: pypi-publish
|
||||
needs:
|
||||
- version
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '3.10'
|
||||
- name: Write version to init file
|
||||
run: |
|
||||
echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -146,11 +146,8 @@ docker/testing/volumes
|
|||
.local/
|
||||
|
||||
.ytdl-sub-working-directory
|
||||
.ytdl-sub-lock
|
||||
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
|
||||
tools/docgen/out
|
||||
|
||||
prof/
|
||||
tools/docgen/out
|
||||
|
|
@ -7,7 +7,6 @@ build:
|
|||
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
python:
|
||||
install:
|
||||
|
|
@ -15,4 +14,4 @@ python:
|
|||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- docs
|
||||
41
Makefile
41
Makefile
|
|
@ -1,20 +1,7 @@
|
|||
# Defensive settings for make:
|
||||
# https://tech.davis-hansson.com/p/make/
|
||||
SHELL:=bash
|
||||
.ONESHELL:
|
||||
.SHELLFLAGS:=-eu -o pipefail -c
|
||||
.SILENT:
|
||||
.DELETE_ON_ERROR:
|
||||
MAKEFLAGS+=--warn-undefined-variables
|
||||
MAKEFLAGS+=--no-builtin-rules
|
||||
export PS1?=$$
|
||||
# Prefix echoed recipe commands with the recipe line number for debugging:
|
||||
export PS4?=:$$LINENO+
|
||||
|
||||
# Get version related variables
|
||||
export DATE:=$(shell date +'%Y.%m.%d')
|
||||
export DATE_COMMIT_COUNT:=$(shell git rev-list --count HEAD --since="$(DATE) 00:00:00")
|
||||
export COMMIT_HASH:=$(shell git rev-parse --short HEAD)
|
||||
export DATE=$(shell date +'%Y.%m.%d')
|
||||
export DATE_COMMIT_COUNT=$(shell git rev-list --count HEAD --since="$(DATE) 00:00:00")
|
||||
export COMMIT_HASH=$(shell git rev-parse --short HEAD)
|
||||
|
||||
# Set Local version to YYYY.MM.DD-<hash>
|
||||
export LOCAL_VERSION="$(DATE)+$(COMMIT_HASH)"
|
||||
|
|
@ -26,22 +13,13 @@ else
|
|||
export PYPI_VERSION="$(DATE).post$(DATE_COMMIT_COUNT)"
|
||||
endif
|
||||
|
||||
# Finished with `$(shell)`, echo recipe commands going forward
|
||||
.SHELLFLAGS+= -x
|
||||
|
||||
|
||||
### Top-level targets:
|
||||
|
||||
.PHONY: all
|
||||
all: check_lint docs docker docker_ubuntu docker_gui
|
||||
|
||||
lint:
|
||||
python3 -m ruff format .
|
||||
python3 -m ruff check --fix .
|
||||
python3 -m pylint src
|
||||
@-isort .
|
||||
@-black .
|
||||
@-pylint src/
|
||||
check_lint:
|
||||
ruff format --check . \
|
||||
&& ruff check . \
|
||||
isort . --check-only --diff \
|
||||
&& black . --check \
|
||||
&& pylint src/
|
||||
wheel: clean
|
||||
$(shell echo "__pypi_version__ = \"$(PYPI_VERSION)\"" > src/ytdl_sub/__init__.py)
|
||||
|
|
@ -63,8 +41,7 @@ executable: clean
|
|||
mv dist/ytdl-sub dist/ytdl-sub${EXEC_SUFFIX}
|
||||
docs:
|
||||
REGENERATE_DOCS=1 pytest tests/unit/docgen/test_docgen.py
|
||||
sphinx-build --write-all --fail-on-warning --nitpicky -b html \
|
||||
"./docs/source/" "./docs/build/"
|
||||
sphinx-build -M html docs/source/ docs/build/
|
||||
clean:
|
||||
rm -rf \
|
||||
.pytest_cache/ \
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ __preset__:
|
|||
|
||||
# Pass any arg directly to yt-dlp's Python API
|
||||
ytdl_options:
|
||||
cookiefile: "/config/ytdl-sub-configs/cookie.txt"
|
||||
cookiefile: "/config/cookie.txt"
|
||||
|
||||
###################################################################
|
||||
# TV Show Presets. Can replace Plex with Plex/Jellyfin/Emby/Kodi
|
||||
# TV Show Presets. Can replace Plex with Plex/Jellyfin/Kodi
|
||||
|
||||
Plex TV Show by Date:
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ Plex TV Show Collection:
|
|||
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
|
||||
|
||||
###################################################################
|
||||
# Music Presets.
|
||||
# Music Presets. Can replace Plex with Plex/Jellyfin/Kodi
|
||||
|
||||
YouTube Releases:
|
||||
= Jazz: # Sets genre tag to "Jazz"
|
||||
|
|
@ -128,7 +128,7 @@ Bandcamp:
|
|||
"Emily Hopkins": "https://emilyharpist.bandcamp.com/"
|
||||
|
||||
###################################################################
|
||||
# Music Video Presets. Can replace Plex with Plex/Jellyfin/Kodi
|
||||
# Music Video Presets
|
||||
"Plex Music Videos":
|
||||
= Pop: # Sets genre tag to "Pop"
|
||||
"Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
|
||||
|
|
|
|||
|
|
@ -3,24 +3,18 @@ FROM ghcr.io/linuxserver/baseimage-alpine:edge
|
|||
###############################################################################
|
||||
# YTDL-SUB INSTALL
|
||||
|
||||
# For phantomjs
|
||||
# Needed for phantomjs
|
||||
ENV OPENSSL_CONF="/etc/ssl"
|
||||
# For downloading thumbnails
|
||||
ENV SSL_CERT_DIR="/etc/ssl/certs/"
|
||||
# Working directory used at both build and run times:
|
||||
ENV DEFAULT_WORKSPACE="/config"
|
||||
|
||||
COPY root/ /
|
||||
RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
||||
RUN mkdir -p /config && \
|
||||
apk update --no-cache && \
|
||||
apk upgrade --no-cache && \
|
||||
apk add --no-cache --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \
|
||||
vim \
|
||||
g++ \
|
||||
nano \
|
||||
unzip \
|
||||
make \
|
||||
deno \
|
||||
libffi-dev \
|
||||
"python3>=3.10" \
|
||||
py3-pip \
|
||||
|
|
@ -32,44 +26,37 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
"aria2>=1.36.0" && \
|
||||
ffmpeg -version && \
|
||||
aria2c --version && \
|
||||
deno --version && \
|
||||
# Install phantomjs if using x86_64, ensure it is properly installed
|
||||
if [[ $(uname -m) == "x86_64" ]]; then \
|
||||
echo "installing phantomjs" && \
|
||||
apk add --no-cache gcompat && \
|
||||
tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
|
||||
mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/share/phantomjs && \
|
||||
rm -rf phantomjs-2.1.1-linux-x86_64 && \
|
||||
rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
|
||||
cd /usr/share && \
|
||||
curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xj && \
|
||||
mv /usr/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs phantomjs && \
|
||||
rm -rf /usr/share/phantomjs-2.1.1-linux-x86_64 && \
|
||||
ln -s /usr/share/phantomjs /usr/bin/phantomjs && \
|
||||
echo "Phantom JS version:" && \
|
||||
phantomjs --version && \
|
||||
cd -; \
|
||||
fi && \
|
||||
# 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 && \
|
||||
echo "hi" && \
|
||||
# Install ytdl-sub, ensure it is installed properly
|
||||
python3 -m pip install --break-system-packages --no-cache-dir ytdl_sub-*.whl && \
|
||||
ytdl-sub -h && \
|
||||
# Delete unneeded packages after install
|
||||
rm ytdl_sub-*.whl && \
|
||||
apk del \
|
||||
g++ \
|
||||
make \
|
||||
libffi-dev && \
|
||||
python3 -m pip --help
|
||||
|
||||
libffi-dev \
|
||||
py3-pip \
|
||||
py3-setuptools
|
||||
|
||||
###############################################################################
|
||||
# CONTAINER CONFIGS
|
||||
|
||||
ENV EDITOR="nano" \
|
||||
HOME="${DEFAULT_WORKSPACE}" \
|
||||
HOME="/config" \
|
||||
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
|
||||
LOGS_TO_STDOUT=/config/cron_logs.txt
|
||||
|
||||
VOLUME "${DEFAULT_WORKSPACE}"
|
||||
WORKDIR "${DEFAULT_WORKSPACE}"
|
||||
VOLUME /config
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
FROM lscr.io/linuxserver/code-server:4.98.2
|
||||
FROM lscr.io/linuxserver/code-server:4.18.0-ls181
|
||||
|
||||
# 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"
|
||||
# Needed for phantomjs
|
||||
ENV OPENSSL_CONF=/etc/ssl
|
||||
|
||||
###############################################################################
|
||||
# YTDL-SUB INSTALL
|
||||
|
|
@ -23,8 +19,8 @@ RUN mkdir -p /config && \
|
|||
vim \
|
||||
g++ \
|
||||
nano \
|
||||
unzip \
|
||||
make \
|
||||
python3.10-dev \
|
||||
python3-pip \
|
||||
fontconfig \
|
||||
xz-utils \
|
||||
|
|
@ -54,21 +50,16 @@ RUN mkdir -p /config && \
|
|||
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 && \
|
||||
curl -L -o phantomjs.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
|
||||
tar -xvf phantomjs.tar.bz2 && \
|
||||
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 && \
|
||||
rm -rf phantomjs-2.1.1-linux-x86_64/ && \
|
||||
rm phantomjs.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 && \
|
||||
# Install ytdl-sub, ensure it is installed properly
|
||||
pip install --no-cache-dir ytdl_sub-*.whl && \
|
||||
ytdl-sub -h && \
|
||||
# Delete unneeded packages after install
|
||||
rm ytdl_sub-*.whl && \
|
||||
|
|
@ -76,22 +67,22 @@ RUN mkdir -p /config && \
|
|||
g++ \
|
||||
make \
|
||||
xz-utils \
|
||||
bzip2 && \
|
||||
bzip2 \
|
||||
python3.10-dev \
|
||||
python3-venv && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get purge -y --auto-remove && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
python3 -m pip --help
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
###############################################################################
|
||||
# CONTAINER CONFIGS
|
||||
|
||||
ENV EDITOR="nano" \
|
||||
|
||||
ENV YTDL_SUB_TYPE="gui" \
|
||||
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
|
||||
LOGS_TO_STDOUT=/config/ytdl-sub-configs/cron_logs.txt \
|
||||
DEFAULT_WORKSPACE=/config/ytdl-sub-configs
|
||||
|
||||
VOLUME /config
|
||||
WORKDIR "${DEFAULT_WORKSPACE}"
|
||||
VOLUME /config
|
||||
|
|
@ -1 +0,0 @@
|
|||
Dockerfile
|
||||
|
|
@ -1,21 +1,17 @@
|
|||
FROM ghcr.io/linuxserver/baseimage-ubuntu:noble
|
||||
FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy
|
||||
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# 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"
|
||||
# Needed for phantomjs
|
||||
ENV OPENSSL_CONF=/etc/ssl
|
||||
|
||||
###############################################################################
|
||||
# YTDL-SUB INSTALL
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
COPY root/ /
|
||||
RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
||||
RUN mkdir -p /config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
|
|
@ -26,8 +22,8 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
vim \
|
||||
g++ \
|
||||
nano \
|
||||
unzip \
|
||||
make \
|
||||
python3.10-dev \
|
||||
python3-pip \
|
||||
fontconfig \
|
||||
xz-utils \
|
||||
|
|
@ -57,21 +53,16 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
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 && \
|
||||
curl -L -o phantomjs.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \
|
||||
tar -xvf phantomjs.tar.bz2 && \
|
||||
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 && \
|
||||
rm -rf phantomjs-2.1.1-linux-x86_64/ && \
|
||||
rm phantomjs.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 && \
|
||||
# Install ytdl-sub, ensure it is installed properly
|
||||
pip install --no-cache-dir ytdl_sub-*.whl && \
|
||||
ytdl-sub -h && \
|
||||
# Delete unneeded packages after install
|
||||
rm ytdl_sub-*.whl && \
|
||||
|
|
@ -79,22 +70,19 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
g++ \
|
||||
make \
|
||||
xz-utils \
|
||||
bzip2 && \
|
||||
bzip2 \
|
||||
python3.10-dev \
|
||||
python3-venv && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get purge -y --auto-remove && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
python3 -m pip --help
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
###############################################################################
|
||||
# CONTAINER CONFIGS
|
||||
|
||||
ENV EDITOR="nano" \
|
||||
HOME="${DEFAULT_WORKSPACE}" \
|
||||
HOME="/config" \
|
||||
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
|
||||
LOGS_TO_STDOUT=/config/cron_logs.txt
|
||||
|
||||
VOLUME "${DEFAULT_WORKSPACE}"
|
||||
WORKDIR "${DEFAULT_WORKSPACE}"
|
||||
VOLUME /config
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
#!/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
|
||||
60
docker/root/custom-cont-init.d/defaults-gui
Normal file
60
docker/root/custom-cont-init.d/defaults-gui
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# Exit if not gui
|
||||
if [ "$YTDL_SUB_TYPE" != "gui" ] ; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# always create the config dir with empty cron logs
|
||||
mkdir -p /config/ytdl-sub-configs
|
||||
echo "" > /config/ytdl-sub-configs/cron_logs.txt
|
||||
|
||||
echo "[ytdl-sub-init] Checking defaults..."
|
||||
|
||||
# copy config
|
||||
[[ ! -e /config/ytdl-sub-configs/config.yaml ]] && \
|
||||
cp /defaults/config.yaml /config/ytdl-sub-configs/config.yaml
|
||||
[[ ! -e /config/ytdl-sub-configs/subscriptions.yaml ]] && \
|
||||
cp /defaults/subscriptions.yaml /config/ytdl-sub-configs/subscriptions.yaml
|
||||
[[ ! -d /config/ytdl-sub-configs/examples ]] && \
|
||||
mkdir -p /config/ytdl-sub-configs/examples && \
|
||||
cp -r /defaults/examples/* /config/ytdl-sub-configs/examples
|
||||
[[ ! -d /config/ytdl-sub-configs/cron_script ]] && \
|
||||
cp /defaults/cron_script /config/ytdl-sub-configs/cron_script
|
||||
chmod +x /config/ytdl-sub-configs/cron_script
|
||||
|
||||
# permissions
|
||||
chown -R ${PUID:-abc}:${PGID:-abc} \
|
||||
/config
|
||||
|
||||
if [ -n "$CRON_SCHEDULE" ] ; then
|
||||
# copy cron wrapper over
|
||||
cp /defaults/cron_wrapper.gui /opt/cron_wrapper
|
||||
chmod +x /opt/cron_wrapper
|
||||
chown abc:abc /opt/cron_wrapper
|
||||
|
||||
# execute cron wrapper in the crontab file
|
||||
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" > /config/crontabs/abc
|
||||
echo "" >> /config/crontabs/abc
|
||||
echo "# min hour day month weekday command" >> /config/crontabs/abc
|
||||
echo " ${CRON_SCHEDULE} /opt/cron_wrapper" >> /config/crontabs/abc
|
||||
|
||||
# restart cron
|
||||
/etc/init.d/cron reload
|
||||
|
||||
# check cron
|
||||
crontab -u abc /config/crontabs/abc
|
||||
ret=$?
|
||||
|
||||
if [ $ret -eq 0 ]; then
|
||||
echo "[ytdl-sub-init] Cron schedule is successfully set to ${CRON_SCHEDULE}"
|
||||
if [ -n "$CRON_RUN_ON_START" ] ; then
|
||||
# Run cron script on start, logs get emitted to docker so do not write to cron logs
|
||||
echo "[ytdl-sub-init] Cron on start enabled, starting"
|
||||
sudo -b -u abc bash -c "cd /config/ytdl-sub-configs && ./cron_script"
|
||||
fi
|
||||
else
|
||||
echo "[ytdl-sub-init] Cron schedule is invalid, disabling"
|
||||
sleep 10
|
||||
fi
|
||||
fi
|
||||
24
docker/root/custom-cont-init.d/defaults-headless
Normal file
24
docker/root/custom-cont-init.d/defaults-headless
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/with-contenv bash
|
||||
|
||||
# Exit if gui
|
||||
if [ "$YTDL_SUB_TYPE" == "gui" ] ; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# always create the config dir with empty cron logs
|
||||
mkdir -p /config
|
||||
echo "" > /config/cron_logs.txt
|
||||
|
||||
echo "[ytdl-sub-init] Checking defaults..."
|
||||
|
||||
# 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
|
||||
|
|
@ -1,22 +1,2 @@
|
|||
# Bare-bones config. Here are some useful links to get started:
|
||||
# Walk-through Guide: https://ytdl-sub.readthedocs.io/en/latest/guides/index.html
|
||||
# Config Examples: https://github.com/jmbannon/ytdl-sub/tree/master/examples
|
||||
# Prebuilt Presets: https://ytdl-sub.readthedocs.io/en/latest/prebuilt_presets/index.html
|
||||
# Config Reference: https://ytdl-sub.readthedocs.io/en/latest/config_reference/index.html
|
||||
#
|
||||
# The subscriptions in `subscriptions.yaml` uses prebuilt presets which do not require
|
||||
# any additions to this config. They can be downloaded using the command:
|
||||
#
|
||||
# ytdl-sub --config config.yaml sub subscriptions.yaml
|
||||
#
|
||||
# Or dry-ran with:
|
||||
#
|
||||
# ytdl-sub --dry-run --config config.yaml sub subscriptions.yaml
|
||||
#
|
||||
# See the documentation above on how to build your own custom presets.
|
||||
#
|
||||
configuration:
|
||||
# Avoid unnecessarily long large file renames, set this to a path on the same
|
||||
# filesystem as the destination for downloaded files in the `overrides: /
|
||||
# *_directory:` paths:
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# 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
|
||||
2
docker/root/defaults/cron_script
Normal file
2
docker/root/defaults/cron_script
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
echo "Running cron_script"
|
||||
ytdl-sub --log-level info --config config.yaml sub subscriptions.yaml
|
||||
3
docker/root/defaults/cron_wrapper.gui
Normal file
3
docker/root/defaults/cron_wrapper.gui
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
cd /config/ytdl-sub-configs
|
||||
bash ./cron_script | tee -a ./cron_logs.txt
|
||||
3
docker/root/defaults/cron_wrapper.headless
Normal file
3
docker/root/defaults/cron_wrapper.headless
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
cd /config
|
||||
bash ./cron_script | tee -a ./cron_logs.txt
|
||||
Binary file not shown.
|
|
@ -15,12 +15,12 @@ __preset__:
|
|||
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
|
||||
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"
|
||||
ytdl_options:
|
||||
cookiefile: "/config/cookie.txt"
|
||||
|
||||
###################################################################
|
||||
# Subscriptions nested under this will use the
|
||||
|
|
@ -35,54 +35,52 @@ 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"
|
||||
"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"
|
||||
= 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"
|
||||
= 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"
|
||||
= 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 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"
|
||||
|
||||
# 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"
|
||||
|
||||
# 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/"
|
||||
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"
|
||||
"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,15 +0,0 @@
|
|||
――――――――――――――――――――――――――――――――――――
|
||||
██╗ ██╗████████╗██████╗ ██╗
|
||||
╚██╗ ██╔╝╚══██╔══╝██╔══██╗██║
|
||||
╚████╔╝ ██║ ██║ ██║██║
|
||||
╚██╔╝ ██║ ██║ ██║██║
|
||||
██║ ██║ ██████╔╝███████╗
|
||||
╚═╝ ╚═╝ ╚═════╝ ╚══════╝
|
||||
|
||||
███████╗██╗ ██╗██████╗
|
||||
██╔════╝██║ ██║██╔══██╗
|
||||
███████╗██║ ██║██████╔╝
|
||||
╚════██║██║ ██║██╔══██╗
|
||||
███████║╚██████╔╝██████╔╝
|
||||
╚══════╝ ╚═════╝ ╚═════╝
|
||||
――――――――――――――――――――――――――――――――――――
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# 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 "$(@)"
|
||||
|
|
@ -1,50 +1,15 @@
|
|||
services:
|
||||
ytdl-sub-gui:
|
||||
build:
|
||||
context: "../"
|
||||
dockerfile: "./Dockerfile.gui"
|
||||
image: "ytdl-sub-gui:local"
|
||||
container_name: "ytdl-sub-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"
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Los_Angeles
|
||||
- CRON_SCHEDULE=* * * * * */1
|
||||
- CRON_RUN_ON_START=True
|
||||
volumes:
|
||||
- "./volumes/ytdl-sub-gui/:/config/"
|
||||
- ./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"
|
||||
- 8443:8443
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "ytdl-sub"
|
||||
copyright = "2026, Jesse Bannon"
|
||||
copyright = "2024, Jesse Bannon"
|
||||
author = "Jesse Bannon"
|
||||
release = ""
|
||||
|
||||
|
|
@ -15,8 +15,10 @@ release = ""
|
|||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx_copybutton",
|
||||
"sphinx_design",
|
||||
]
|
||||
|
|
@ -68,3 +70,19 @@ extlinks = {
|
|||
"lsio-gh": ("https://github.com/linuxserver/%s", "%s image"),
|
||||
"ytdl-sub-gh": ("https://github.com/jmbannon/ytdl-sub/%s", "src %s"),
|
||||
}
|
||||
|
||||
# -- Options for autodoc ----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration
|
||||
|
||||
# Automatically extract typehints when specified and place them in
|
||||
# descriptions of the relevant function/method.
|
||||
autodoc_default_options = {
|
||||
"autodoc_typehints_format": "short",
|
||||
"autodoc_class_signature": "separated",
|
||||
"add_module_names": False,
|
||||
# "add_class_names": False,
|
||||
}
|
||||
|
||||
python_use_unqualified_type_names = True
|
||||
napoleon_numpy_docstring = True
|
||||
napoleon_use_rtype = False
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
..
|
||||
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
|
||||
==================
|
||||
-----------
|
||||
config.yaml
|
||||
-----------
|
||||
|
||||
ytdl-sub is configured using a ``config.yaml`` file.
|
||||
|
||||
The ``config.yaml`` is made up of two sections:
|
||||
|
|
@ -17,118 +14,77 @@ The ``config.yaml`` is made up of two sections:
|
|||
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 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.
|
||||
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: subscription_value, persist_logs, experimental
|
||||
|
||||
persist_logs
|
||||
""""""""""""
|
||||
Within ``configuration``, define whether logs from subscription downloads
|
||||
should be persisted.
|
||||
|
||||
.. 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"
|
||||
logs_directory: "/path/to/log/directory"
|
||||
|
||||
umask: "022"
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
Log files are stored as
|
||||
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
|
||||
|
||||
dl_aliases
|
||||
----------
|
||||
.. _dl_aliases:
|
||||
.. autoclass:: ytdl_sub.config.config_validator.PersistLogsValidator()
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
Alias definitions to shorten :ref:`dl arguments <usage:Download Options>`. For example,
|
||||
presets
|
||||
~~~~~~~
|
||||
``presets`` define a `formula` for how to format downloaded media and metadata.
|
||||
|
||||
This section is work-in-progress!
|
||||
|
||||
preset
|
||||
""""""
|
||||
Presets support inheritance by defining a parent preset:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
configuration:
|
||||
dl_aliases:
|
||||
mv: "--preset music_video"
|
||||
u: "--download.url"
|
||||
presets:
|
||||
custom_preset:
|
||||
...
|
||||
parent_preset:
|
||||
...
|
||||
child_preset:
|
||||
preset: "parent_preset"
|
||||
|
||||
Simplifies
|
||||
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.
|
||||
|
||||
.. code-block:: bash
|
||||
Presets also support inheritance from multiple presets:
|
||||
|
||||
ytdl-sub dl --preset "Jellyfin Music Videos" --download.url "youtube.com/watch?v=a1b2c3"
|
||||
.. code-block:: yaml
|
||||
|
||||
to
|
||||
child_preset:
|
||||
preset:
|
||||
- "custom_preset"
|
||||
- "parent_preset"
|
||||
|
||||
.. code-block:: bash
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
|
@ -2,50 +2,11 @@
|
|||
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.
|
||||
|
||||
This section contains direct references to the code of ``ytdl-sub`` and information on how it functions.
|
||||
|
||||
.. toctree::
|
||||
config_yaml
|
||||
subscription_yaml
|
||||
subscriptions_yaml
|
||||
plugins
|
||||
scripting/index
|
||||
prebuilt_presets/index
|
||||
prebuilt_presets/index
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
..
|
||||
WARNING: This RST file is generated from docstrings in:
|
||||
The respective plugin files under src/ytdl_sub/plugins/
|
||||
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.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
|
|
@ -28,6 +21,7 @@ Extracts audio from a video file.
|
|||
The codec to output after extracting the audio. Supported codecs are aac, flac, mp3, m4a,
|
||||
opus, vorbis, wav, and best to grab the best possible format at runtime.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -36,6 +30,7 @@ Extracts audio from a video file.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``quality``
|
||||
|
||||
:expected type: Float
|
||||
|
|
@ -43,6 +38,7 @@ Extracts audio from a video file.
|
|||
Optional. Specify ffmpeg audio quality. Insert a value between ``0`` (better) and ``9``
|
||||
(worse) for variable bitrate, or a specific bitrate like ``128`` for 128k.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
chapters
|
||||
|
|
@ -81,12 +77,14 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
Defaults to False. If chapters do not exist in the video/description itself, attempt to
|
||||
scrape comments to find the chapters.
|
||||
|
||||
|
||||
``embed_chapters``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Defaults to True. Embed chapters into the file.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -95,6 +93,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``force_key_frames``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
|
|
@ -102,6 +101,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
Defaults to False. Force keyframes at cuts when removing sections. This is slow due to
|
||||
needing a re-encode, but the resulting video may have fewer artifacts around the cuts.
|
||||
|
||||
|
||||
``remove_chapters_regex``
|
||||
|
||||
:expected type: Optional[List[RegexString]
|
||||
|
|
@ -109,6 +109,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
List of regex patterns to match chapter titles against and remove them from the
|
||||
entry.
|
||||
|
||||
|
||||
``remove_sponsorblock_categories``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
|
|
@ -117,6 +118,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
categories that are specified in ``sponsorblock_categories`` or "all", which removes
|
||||
everything specified in ``sponsorblock_categories``.
|
||||
|
||||
|
||||
``sponsorblock_categories``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
|
|
@ -125,6 +127,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
"intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic",
|
||||
"poi_highlight", or "all" to include all categories.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
date_range
|
||||
|
|
@ -137,11 +140,9 @@ Dates must adhere to a yt-dlp datetime. From their docs:
|
|||
A string in the format YYYYMMDD or
|
||||
(now|today|yesterday|date)[+-][0-9](microsecond|second|minute|hour|day|week|month|year)(s)
|
||||
|
||||
Valid examples are ``now-2weeks`` or ``20200101``. Can use override variables in
|
||||
this. Note that yt-dlp will round times to the closest day, meaning that `day` is
|
||||
the lowest granularity possible. Also note that, considering time zones, it's best
|
||||
to include a margin of an extra day on either side to be sure it includes the
|
||||
intended download files.
|
||||
Valid examples are ``now-2weeks`` or ``20200101``. Can use override variables in this.
|
||||
Note that yt-dlp will round times to the closest day, meaning that `day` is the lowest
|
||||
granularity possible.
|
||||
|
||||
:Usage:
|
||||
|
||||
|
|
@ -150,20 +151,20 @@ intended download files.
|
|||
date_range:
|
||||
before: "now"
|
||||
after: "today-2weeks"
|
||||
breaks: True
|
||||
type: "upload_date"
|
||||
|
||||
``after``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Only download videos after or on this datetime, inclusive.
|
||||
Only download videos after this datetime.
|
||||
|
||||
|
||||
``before``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Only download videos only before this datetime, not inclusive.
|
||||
Only download videos before this datetime.
|
||||
|
||||
|
||||
``breaks``
|
||||
|
||||
|
|
@ -172,6 +173,7 @@ intended download files.
|
|||
Toggle to enable breaking subsequent metadata downloads if an entry's upload date
|
||||
is out of range. Defaults to True.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -180,12 +182,6 @@ intended download files.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
``type``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Which type of date to use. Must be either ``upload_date`` or ``release_date``.
|
||||
Defaults to ``upload_date``.
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -290,6 +286,7 @@ Also supports custom ffmpeg conversions:
|
|||
- Video: avi, flv, mkv, mov, mp4, webm
|
||||
- Audio: aac, flac, mp3, m4a, opus, vorbis, wav
|
||||
|
||||
|
||||
``convert_with``
|
||||
|
||||
:expected type: Optional[String]
|
||||
|
|
@ -298,6 +295,7 @@ Also supports custom ffmpeg conversions:
|
|||
yt-dlp whereas ``ffmpeg`` specifies it will be converted using a custom command specified
|
||||
with ``ffmpeg_post_process_args``. Defaults to ``yt-dlp``.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -306,6 +304,7 @@ Also supports custom ffmpeg conversions:
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``ffmpeg_post_process_args``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -318,6 +317,7 @@ Also supports custom ffmpeg conversions:
|
|||
The output file will use the extension specified in ``convert_to``. Post-processing args
|
||||
can still be set with ``convert_with`` set to ``yt-dlp``.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
filter_exclude
|
||||
|
|
@ -453,20 +453,23 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``kodi_safe``
|
||||
|
||||
:expected type: OverridesBooleanFormatterValidator
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Defaults to False. Kodi does not support > 3-byte unicode characters, which include
|
||||
emojis and some foreign language characters. Setting this to True will replace those
|
||||
characters with '□'.
|
||||
|
||||
|
||||
``nfo_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The NFO file name.
|
||||
|
||||
|
||||
``nfo_root``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -479,6 +482,7 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
<episodedetails>
|
||||
</episodedetails>
|
||||
|
||||
|
||||
``tags``
|
||||
|
||||
:expected type: NfoTags
|
||||
|
|
@ -515,6 +519,7 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
<genre>Comedy</genre>
|
||||
<genre>Drama</genre>
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
output_directory_nfo_tags
|
||||
|
|
@ -546,20 +551,23 @@ Usage:
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``kodi_safe``
|
||||
|
||||
:expected type: OverridesBooleanFormatterValidator
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Defaults to False. Kodi does not support > 3-byte unicode characters, which include
|
||||
emojis and some foreign language characters. Setting this to True will replace those
|
||||
characters with '□'.
|
||||
|
||||
|
||||
``nfo_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The NFO file name.
|
||||
|
||||
|
||||
``nfo_root``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -572,6 +580,7 @@ Usage:
|
|||
<tvshow>
|
||||
</tvshow>
|
||||
|
||||
|
||||
``tags``
|
||||
|
||||
:expected type: NfoTags
|
||||
|
|
@ -606,6 +615,7 @@ Usage:
|
|||
<genre>Comedy</genre>
|
||||
<genre>Drama</genre>
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
output_options
|
||||
|
|
@ -630,8 +640,6 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
maintain_download_archive: True
|
||||
keep_files_before: now
|
||||
keep_files_after: 19000101
|
||||
keep_max_files: 1000
|
||||
keep_files_date_eval: "{upload_date_standardized}"
|
||||
|
||||
``download_archive_name``
|
||||
|
||||
|
|
@ -640,6 +648,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
The file name to store a subscriptions download archive placed relative to
|
||||
the output directory. Defaults to ``.ytdl-sub-{subscription_name}-download-archive.json``
|
||||
|
||||
|
||||
``file_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -647,6 +656,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
The file name for the media file. This can include directories such as
|
||||
``"Season {upload_year}/{title}.{ext}"``, and will be placed in the output directory.
|
||||
|
||||
|
||||
``info_json_name``
|
||||
|
||||
:expected type: Optional[EntryFormatter]
|
||||
|
|
@ -655,6 +665,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
as ``"Season {upload_year}/{title}.{info_json_ext}"``, and will be placed in the output
|
||||
directory. Can be set to empty string or `null` to disable info json writes.
|
||||
|
||||
|
||||
``keep_files_after``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -666,6 +677,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
files after ``19000101``, which implies all files. Can be used in conjunction with
|
||||
``keep_max_files``.
|
||||
|
||||
|
||||
``keep_files_before``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -677,14 +689,6 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
files before ``now``, which implies all files. Can be used in conjunction with
|
||||
``keep_max_files``.
|
||||
|
||||
``keep_files_date_eval``
|
||||
|
||||
:expected type: str
|
||||
:description:
|
||||
Uses this standardized date in the form of YYYY-MM-DD to record in the
|
||||
download archive for a given entry. Subsequently, uses this value to
|
||||
perform evaluation for keep_files_before/after and keep_max_files. Defaults
|
||||
to the entry's upload_date_standardized variable.
|
||||
|
||||
``keep_max_files``
|
||||
|
||||
|
|
@ -695,6 +699,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
Only keeps N most recently uploaded videos. If set to <= 0, ``keep_max_files`` will not be
|
||||
applied. Can be used in conjunction with ``keep_files_before`` and ``keep_files_after``.
|
||||
|
||||
|
||||
``maintain_download_archive``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
|
|
@ -709,6 +714,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
|
||||
Defaults to False.
|
||||
|
||||
|
||||
``migrated_download_archive_name``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -718,19 +724,13 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
name first, and fallback to ``download_archive_name``. It will always save to this file
|
||||
and remove the original ``download_archive_name``.
|
||||
|
||||
|
||||
``output_directory``
|
||||
|
||||
:expected type: OverridesFormatter
|
||||
:description:
|
||||
The output directory to store all media files downloaded.
|
||||
|
||||
``preserve_mtime``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Preserve the video's original upload time as the file modification time.
|
||||
When True, sets the file's mtime to match the video's upload_date from
|
||||
yt-dlp metadata. Defaults to False.
|
||||
|
||||
``thumbnail_name``
|
||||
|
||||
|
|
@ -740,6 +740,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
as ``"Season {upload_year}/{title}.{thumbnail_ext}"``, and will be placed in the output
|
||||
directory. Can be set to empty string or `null` to disable thumbnail writes.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
overrides
|
||||
|
|
@ -769,6 +770,121 @@ In addition, any override variable defined will automatically create a ``sanitiz
|
|||
for use. In the example above, ``output_directory_sanitized`` will exist and perform
|
||||
sanitization on the value when used.
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
regex
|
||||
-----
|
||||
.. attention::
|
||||
|
||||
This plugin will eventually be deprecated and replaced by scripting functions.
|
||||
You can replicate the example below using the following.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# Only includes videos with 'Official Video'
|
||||
filter_include:
|
||||
- >-
|
||||
{ %contains( %lower(title), "official video" ) }
|
||||
|
||||
# Excludes videos with '#short' in its description
|
||||
filter_exclude:
|
||||
- >-
|
||||
{ %contains( %lower(description), '#short' ) }
|
||||
|
||||
# Creates a capture array with defaults, and assigns
|
||||
# each capture group to its own variable
|
||||
overrides:
|
||||
description_date_capture: >-
|
||||
{
|
||||
%regex_capture_many_with_defaults(
|
||||
description,
|
||||
[ "([0-9]{4})-([0-9]{2})-([0-9]{2})" ],
|
||||
[ upload_year, upload_month, upload_day ]
|
||||
)
|
||||
}
|
||||
captured_upload_year: >-
|
||||
{ %array_at(description_date_capture, 1) }
|
||||
captured_upload_month: >-
|
||||
{ %array_at(description_date_capture, 2) }
|
||||
captured_upload_day: >-
|
||||
{ %array_at(description_date_capture, 3) }
|
||||
|
||||
Performs regex matching on an entry's source or override variables. Regex can be used to filter
|
||||
entries from proceeding with download or capture groups to create new source variables.
|
||||
|
||||
NOTE that YAML differentiates between single-quote (``'``) and double-quote (``"``), which can
|
||||
affect regex. Double-quote implies string literals, i.e. ``"\n"`` is the literal chars ``\n``,
|
||||
whereas single-quote, ``'\n'`` gets evaluated to a new line. To escape ``\`` when using
|
||||
single-quote, use ``\\``. This is necessary if you want your regex to be something like
|
||||
``\d\n`` to match a number and adjacent new-line. It must be written as ``\\d\n``.
|
||||
|
||||
If you want to regex-search multiple source variables to create a logical OR effect, you can
|
||||
create an override variable that contains the concatenation of them, and search that with regex.
|
||||
For example, creating the override variable ``"title_and_description": "{title} {description}"``
|
||||
and using ``title_and_description`` can regex match/exclude from either ``title`` or
|
||||
``description``.
|
||||
|
||||
:Usage:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
regex:
|
||||
# By default, if any match fails and has no defaults, the entry will
|
||||
# be skipped. If False, ytdl-sub will error and stop all downloads
|
||||
# from proceeding.
|
||||
skip_if_match_fails: True
|
||||
|
||||
from:
|
||||
# For each entry's `title` value...
|
||||
title:
|
||||
# Perform this regex match on it to act as a filter.
|
||||
# This will only download videos with "[Official Video]" in it. Note that we
|
||||
# double backslash to make YAML happy
|
||||
match:
|
||||
- '\\[Official Video\\]'
|
||||
|
||||
# For each entry's `description` value...
|
||||
description:
|
||||
# Match with capture groups and defaults.
|
||||
# This tries to scrape a date from the description and produce new
|
||||
# source variables
|
||||
match:
|
||||
- '([0-9]{4})-([0-9]{2})-([0-9]{2})'
|
||||
# Exclude any entry where the description contains #short
|
||||
exclude:
|
||||
- '#short'
|
||||
|
||||
# Each capture group creates these new source variables, respectively,
|
||||
# as well a sanitized version, i.e. `captured_upload_year_sanitized`
|
||||
capture_group_names:
|
||||
- "captured_upload_year"
|
||||
- "captured_upload_month"
|
||||
- "captured_upload_day"
|
||||
|
||||
# And if the string does not match, use these as respective default
|
||||
# values for the new source variables.
|
||||
capture_group_defaults:
|
||||
- "{upload_year}"
|
||||
- "{upload_month}"
|
||||
- "{upload_day}"
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Can typically be left undefined to always default to enable. For preset convenience,
|
||||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``skip_if_match_fails``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Defaults to True. If True, when any match fails and has no defaults, the entry will be
|
||||
skipped. If False, ytdl-sub will error and all downloads will not proceed.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
split_by_chapters
|
||||
|
|
@ -804,90 +920,6 @@ used with no modifications.
|
|||
If a file has no chapters and is set to "pass", then ``chapter_title`` is
|
||||
set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1.
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
square_thumbnail
|
||||
----------------
|
||||
Whether to make thumbnails square. Supports both file and embedded-based thumbnails. Ideal
|
||||
for representing audio albums.
|
||||
|
||||
:Usage:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
square_thumbnail: True
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
static_nfo_tags
|
||||
---------------
|
||||
Adds an NFO file for every entry, but does not link it to an entry in the download
|
||||
archive. This is intended to produce ``season.nfo`` files in each season
|
||||
directory. Each entry within a season will overwrite this file with its season
|
||||
name. If the entry gets deleted from ytdl-sub, this file will remain since it's not
|
||||
linked.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
presets:
|
||||
my_example_preset:
|
||||
static_nfo_tags:
|
||||
# required
|
||||
nfo_name: "season.nfo"
|
||||
nfo_root: "season"
|
||||
tags:
|
||||
title: "My custom season name!"
|
||||
# optional
|
||||
kodi_safe: False
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Can typically be left undefined to always default to enable. For preset convenience,
|
||||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
``kodi_safe``
|
||||
|
||||
:expected type: OverridesBooleanFormatterValidator
|
||||
:description:
|
||||
Defaults to False. Kodi does not support > 3-byte unicode characters, which include
|
||||
emojis and some foreign language characters. Setting this to True will replace those
|
||||
characters with '□'.
|
||||
|
||||
``nfo_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The NFO file name.
|
||||
|
||||
``nfo_root``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The root tag of the NFO's XML. In the usage above, it would look like
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<season>
|
||||
</season>
|
||||
|
||||
``tags``
|
||||
|
||||
:expected type: NfoTags
|
||||
:description:
|
||||
Tags within the nfo_root tag. In the usage above, it would look like
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<season>
|
||||
<title>My custom season name!</title>
|
||||
</season>
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -916,6 +948,7 @@ It will set the respective language to the correct subtitle file.
|
|||
:description:
|
||||
Defaults to False. Whether to allow auto generated subtitles.
|
||||
|
||||
|
||||
``embed_subtitles``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
|
|
@ -923,6 +956,7 @@ It will set the respective language to the correct subtitle file.
|
|||
Defaults to False. Whether to embed the subtitles into the video file. Note that
|
||||
webm files can only embed "vtt" subtitle types.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -931,6 +965,7 @@ It will set the respective language to the correct subtitle file.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``languages``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
|
|
@ -938,12 +973,6 @@ It will set the respective language to the correct subtitle file.
|
|||
Language code(s) to download for subtitles. Supports a single or list of multiple
|
||||
language codes. Defaults to only "en".
|
||||
|
||||
``languages_required``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
:description:
|
||||
Language code(s) that are required to be present for downloads to continue. If missing,
|
||||
ytdl-sub will throw an error. NOTE: currently this only checks file-based subtitles.
|
||||
|
||||
``subtitles_name``
|
||||
|
||||
|
|
@ -954,12 +983,14 @@ It will set the respective language to the correct subtitle file.
|
|||
and will be placed in the output directory. ``lang`` is dynamic since you can download
|
||||
multiple subtitles. It will set the respective language to the correct subtitle file.
|
||||
|
||||
|
||||
``subtitles_type``
|
||||
|
||||
:expected type: Optional[String]
|
||||
:description:
|
||||
Defaults to "srt". One of the subtitle file types "srt", "vtt", "ass", "lrc".
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
throttle_protection
|
||||
|
|
@ -968,9 +999,6 @@ Provides options to make ytdl-sub look more 'human-like' to protect from throttl
|
|||
range-based values, a random number will be chosen within the range to avoid sleeps looking
|
||||
scripted.
|
||||
|
||||
Range min and max values support static override variables within their definitions.
|
||||
``sleep_per_download_s`` supports both static and override variables.
|
||||
|
||||
:Usage:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
|
@ -978,9 +1006,6 @@ Range min and max values support static override variables within their definiti
|
|||
presets:
|
||||
my_example_preset:
|
||||
throttle_protection:
|
||||
sleep_per_request_s:
|
||||
min: 5.5
|
||||
max: 10.4
|
||||
sleep_per_download_s:
|
||||
min: 2.2
|
||||
max: 10.8
|
||||
|
|
@ -1000,12 +1025,14 @@ Range min and max values support static override variables within their definiti
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``max_downloads_per_subscription``
|
||||
|
||||
:expected type: Optional[Range]
|
||||
:description:
|
||||
Number of downloads to perform per subscription.
|
||||
|
||||
|
||||
``sleep_per_download_s``
|
||||
|
||||
:expected type: Optional[Range]
|
||||
|
|
@ -1013,14 +1040,6 @@ Range min and max values support static override variables within their definiti
|
|||
Number in seconds to sleep between each download. Does not include time it takes for
|
||||
ytdl-sub to perform post-processing.
|
||||
|
||||
``sleep_per_request_s``
|
||||
|
||||
:expected type: Optional[Range]
|
||||
:description:
|
||||
Number in seconds to sleep between each request during metadata download. Note that
|
||||
metadata download refers to the initial info.json download, not the actual audio/video
|
||||
download for the entry. Also, yt-dlp only supports a single value at this time for this,
|
||||
so will always use the max value.
|
||||
|
||||
``sleep_per_subscription_s``
|
||||
|
||||
|
|
@ -1028,6 +1047,7 @@ Range min and max values support static override variables within their definiti
|
|||
:description:
|
||||
Number in seconds to sleep between each subscription.
|
||||
|
||||
|
||||
``subscription_download_probability``
|
||||
|
||||
:expected type: Optional[Float]
|
||||
|
|
@ -1036,6 +1056,7 @@ Range min and max values support static override variables within their definiti
|
|||
recommended to set if you run ytdl-sub in a cron-job, that way you are statistically
|
||||
guaranteed over time to eventually download the subscription.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
video_tags
|
||||
|
|
|
|||
|
|
@ -4,30 +4,12 @@ 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
|
||||
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/media_quality.yaml
|
||||
|
||||
Only Recent Videos
|
||||
------------------
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml
|
||||
|
|
@ -2,10 +2,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>`.
|
||||
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
|
||||
music
|
||||
|
|
@ -6,5 +6,4 @@ All audio music based presets inherit from ``_music_base``.
|
|||
|
||||
.. highlight:: yaml
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/music/singles.yaml
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/music/singles.yaml
|
||||
|
|
@ -6,5 +6,4 @@ All TV show based presets inherit from ``_episode_base``.
|
|||
|
||||
.. highlight:: yaml
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
..
|
||||
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
|
||||
===============
|
||||
|
|
@ -90,13 +83,6 @@ extractor_key
|
|||
: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``
|
||||
|
|
@ -179,13 +165,6 @@ webpage_url
|
|||
: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
|
||||
|
|
@ -680,9 +659,3 @@ 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``.
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
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.
|
||||
``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
|
||||
|
|
@ -14,7 +13,6 @@ function.
|
|||
scripting_functions
|
||||
scripting_types
|
||||
|
||||
|
||||
How it Works
|
||||
------------
|
||||
|
||||
|
|
@ -46,22 +44,22 @@ We can use this instead of hard-coding it above:
|
|||
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.
|
||||
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
|
||||
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.
|
||||
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:
|
||||
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
|
||||
|
||||
|
|
@ -76,8 +74,8 @@ 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.
|
||||
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:
|
||||
|
|
@ -99,9 +97,9 @@ 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:
|
||||
``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
|
||||
|
||||
|
|
@ -118,16 +116,16 @@ 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!
|
||||
(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>`_
|
||||
``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
|
||||
|
|
@ -145,48 +143,42 @@ Let's suppose you are an avid command-line user, and like all of your file names
|
|||
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:
|
||||
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.
|
||||
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.
|
||||
|
||||
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>`_.
|
||||
|
||||
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>`_
|
||||
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.
|
||||
: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
|
||||
|
|
@ -195,9 +187,9 @@ Custom functions can be created in the overrides section using the following syn
|
|||
"%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.
|
||||
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:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
..
|
||||
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
|
||||
===================
|
||||
|
|
@ -521,13 +514,6 @@ pow
|
|||
: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``
|
||||
|
|
@ -537,37 +523,6 @@ sub
|
|||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
---------------
|
||||
|
||||
|
|
@ -578,50 +533,6 @@ regex_capture_groups
|
|||
: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``
|
||||
|
|
@ -649,13 +560,6 @@ regex_search
|
|||
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``
|
||||
|
|
@ -679,7 +583,7 @@ capitalize
|
|||
|
||||
concat
|
||||
~~~~~~
|
||||
:spec: ``concat(values: AnyArgument, ...) -> String``
|
||||
:spec: ``concat(values: String, ...) -> String``
|
||||
|
||||
:description:
|
||||
Concatenate multiple Strings into a single String.
|
||||
|
|
@ -691,38 +595,6 @@ contains
|
|||
: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``
|
||||
|
|
@ -773,23 +645,6 @@ 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``
|
||||
|
|
@ -797,24 +652,6 @@ titlecase
|
|||
: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``
|
||||
|
|
@ -837,7 +674,7 @@ behavior.
|
|||
|
||||
sanitize
|
||||
~~~~~~~~
|
||||
:spec: ``sanitize(value: AnyArgument, ...) -> String``
|
||||
: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.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
===============
|
||||
|
||||
Scripting Types
|
||||
===============
|
||||
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
|
|
@ -17,9 +16,8 @@ Strings are a series of characters surrounded by quotes.
|
|||
|
||||
.. 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.
|
||||
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:
|
||||
|
||||
|
|
@ -67,8 +65,8 @@ There are a few ways to make variables that use curly braces more compact, inclu
|
|||
|
||||
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.
|
||||
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::
|
||||
|
||||
|
|
@ -90,8 +88,7 @@ double quotes, triple-quotes can be used to avoid *closing* the String.
|
|||
%string("""This has both " and ' in it.""")
|
||||
}
|
||||
|
||||
If you want a plain string that contains literal curly braces, you can escape them like
|
||||
so:
|
||||
If you want a plain string that contains literal curly braces, you can escape them like so:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -188,8 +185,8 @@ A type is considered boolean if it spells out ``True`` or ``False``, case-insens
|
|||
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.
|
||||
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::
|
||||
|
||||
|
|
@ -230,8 +227,8 @@ are defined using brackets (``[ ]``), and are accessed using zero-based indexing
|
|||
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.
|
||||
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::
|
||||
|
||||
|
|
@ -270,7 +267,6 @@ defined using curly-braces (``{ }``), and are accessed using their keys.
|
|||
|
||||
Null
|
||||
~~~~
|
||||
|
||||
Null is represented by an empty String, and can be conveyed by spelling out ``null``,
|
||||
case-insensitive.
|
||||
|
||||
|
|
@ -301,9 +297,7 @@ Function Type-Hints
|
|||
|
||||
AnyArgument
|
||||
~~~~~~~~~~~
|
||||
|
||||
AnyArgument means any of the above Types are valid as input or output to a scripting
|
||||
function.
|
||||
AnyArgument means any of the above Types are valid as input or output to a scripting function.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
@ -312,15 +306,13 @@ function.
|
|||
|
||||
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>`_
|
||||
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::
|
||||
|
|
@ -339,9 +331,8 @@ has an optional default value. Both of these usages are valid:
|
|||
|
||||
Lambda
|
||||
~~~~~~
|
||||
|
||||
Lambda parameters are a reference to a function, and will call that lambda function on
|
||||
the input. In this example,
|
||||
Lambda parameters are a reference to a function, and will call that lambda function
|
||||
on the input. In this example,
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -350,23 +341,20 @@ the input. In this example,
|
|||
%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"]``.
|
||||
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>`_,
|
||||
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.
|
||||
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,
|
||||
|
||||
|
|
@ -377,12 +365,11 @@ In this example,
|
|||
%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
|
||||
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)
|
||||
|
|
@ -393,10 +380,9 @@ 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,
|
||||
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
|
||||
|
||||
|
|
@ -405,4 +391,4 @@ example,
|
|||
%if( True, "Return this if True", "Return this if False" )
|
||||
}
|
||||
|
||||
is going to return ``"Return this if True"`` since the condition parameter is ``True``.
|
||||
is going to return ``"Return this if True"`` since the condition parameter is ``True``.
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
..
|
||||
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
|
||||
================
|
||||
|
|
@ -31,21 +24,16 @@ 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:
|
||||
For subscriptions in the form of
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Preset 1 | = Indent Value 1 | Preset 2:
|
||||
Preset 3 | = Indent Value 2 | Preset 4:
|
||||
Preset | = Indent Value 1:
|
||||
= Indent Value 2:
|
||||
"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_indent_1`` and ``subscription_indent_2`` get set to
|
||||
``Indent Value 1`` and ``Indent Value 2``.
|
||||
|
||||
subscription_map
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
==================
|
||||
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.
|
||||
147
docs/source/config_reference/subscriptions_yaml.rst
Normal file
147
docs/source/config_reference/subscriptions_yaml.rst
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
==================
|
||||
Subscriptions File
|
||||
==================
|
||||
------------------
|
||||
subscriptions.yaml
|
||||
------------------
|
||||
|
||||
The ``subscriptions.yaml`` file is where we use our :ref:`config_reference/config_yaml:presets` in the :ref:`config_reference/config_yaml: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 ``{override_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: "{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.
|
||||
|
||||
Beautifying Subscriptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Subscriptions support using presets as keys, and using keys to set override variables as values.
|
||||
For example:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscription.yaml
|
||||
|
||||
TV Show Full Archive:
|
||||
= News:
|
||||
"Breaking News": "https://www.youtube.com/@SomeBreakingNews"
|
||||
|
||||
TV Show Only Recent:
|
||||
= Tech | TV-Y:
|
||||
"Two Minute Papers": "https://www.youtube.com/@TwoMinutePapers"
|
||||
|
||||
Will create two subscriptions named "Breaking News" and "Two Minute Papers", equivalent to:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
"Breaking News":
|
||||
preset:
|
||||
- "TV Show Full Archive"
|
||||
|
||||
overrides:
|
||||
subscription_indent_1: "News"
|
||||
subscription_name: "Breaking News"
|
||||
subscription_value: "https://www.youtube.com/@SomeBreakingNews"
|
||||
|
||||
"Two Minute Papers":
|
||||
preset:
|
||||
- "TV Show Only Recent"
|
||||
|
||||
overrides:
|
||||
subscription_indent_1: "Tech"
|
||||
subscription_indent_2: "TV-Y"
|
||||
subscription_name: "Two Minute Papers"
|
||||
subscription_value: "https://www.youtube.com/@TwoMinutePapers"
|
||||
|
||||
You can provide as many parent presets in the form of ``keys``, and subscription indents as ``= keys``.
|
||||
This can drastically simplify subscription definitions by setting things like so in your
|
||||
parent preset:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
presets:
|
||||
"TV Show Preset":
|
||||
overrides:
|
||||
subscription_indent_1: "default-genre"
|
||||
subscription_indent_2: "default-content-rating"
|
||||
|
||||
tv_show_name: "{subscription_name}"
|
||||
url: "{subscription_value}"
|
||||
genre: "{subscription_indent_1}"
|
||||
content_rating: "{subscription_indent_2}"
|
||||
|
||||
.. _subscription value:
|
||||
|
||||
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``.
|
||||
|
||||
|
||||
Subscription Value
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
NOTE: This is deprecated in favor of using the method in :ref:`config_reference/subscriptions_yaml:beautifying subscriptions`.
|
||||
|
||||
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:
|
||||
- "tv_show"
|
||||
overrides:
|
||||
tv_show_name: "{subscription_name}"
|
||||
|
||||
__value__: "url"
|
||||
|
||||
# single-line subscription, sets "Brandon Acker" and the subscription value
|
||||
# to the override variables tv_show_name and url
|
||||
"Brandon Acker": "https://www.youtube.com/@brandonacker"
|
||||
|
||||
Traditional subscriptions that can override presets will still work when using ``__value__``.
|
||||
``__value__`` can also be set within a :ref:`config_reference/config_yaml:config.yaml`.
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,66 +1,13 @@
|
|||
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.
|
||||
:ref:`config_reference/subscriptions_yaml:beautifying subscriptions`. ``__preset__`` will still be supported for the time being.
|
||||
|
||||
July 2023
|
||||
---------
|
||||
|
|
@ -68,9 +15,8 @@ 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:
|
||||
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
|
||||
|
||||
|
|
@ -94,8 +40,8 @@ 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:
|
||||
Video tags are getting simplified as well. ``tags`` will now reside directly under video_tags.
|
||||
Convert from:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,45 @@
|
|||
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.
|
||||
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:
|
||||
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
|
||||
|
||||
...get support or reach out to contribute?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you need support, you can:
|
||||
|
||||
* :ytdl-sub-gh:`Open an issue on GitHub <issues/new>`
|
||||
|
||||
* `Join our Discord <https://discord.gg/v8j9RAHb4k>`_
|
||||
|
||||
If you would like to contribute, we're happy to accept any help, even 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.
|
||||
|
||||
...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:
|
||||
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
|
||||
|
||||
|
|
@ -41,220 +50,27 @@ download your YouTube cookie, then add it to your :ref:`ytdl options
|
|||
...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.
|
||||
|
||||
:doc:`This page </guides/getting_started/automating_downloads>` shows how to set up ``ytdl-sub`` to run automatically on various platforms.
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
@ -270,9 +86,7 @@ tell yt-dlp to explicitly download English metadata using.
|
|||
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.
|
||||
: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
|
||||
|
|
@ -280,15 +94,7 @@ tell yt-dlp to explicitly download English metadata using.
|
|||
- **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.
|
||||
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>`
|
||||
:alt: The Plex Agents settings page has Local Media Assets enabled for Personal Media Shows and Movies tabs.
|
||||
|
|
|
|||
|
|
@ -1,98 +1,5 @@
|
|||
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.
|
||||
.. toctree::
|
||||
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
Advanced Configuration
|
||||
======================
|
||||
|
||||
If the :doc:`prebuilt presets </prebuilt_presets/index>` aren't suitable for your needs, you may want to set up an advanced configuration.
|
||||
|
||||
Layout of a Config file
|
||||
-----------------------
|
||||
|
||||
The layout of the ``config.yaml`` file is relatively straightforward:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
presets:
|
||||
|
||||
preset_name:
|
||||
plugin1:
|
||||
plugin1_option1: value1
|
||||
|
||||
This creates a preset named ``preset_name``, which contains the made-up
|
||||
:doc:`plugin </config_reference/plugins>` ``plugin1``. Under each plugin are its settings.
|
||||
A preset can contain multiple plugins.
|
||||
|
||||
Preset Inheritance
|
||||
------------------
|
||||
|
||||
You can modularize your presets via preset inheritance. For example,
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
presets:
|
||||
|
||||
TV Show:
|
||||
presets:
|
||||
- "Jellyfin TV Show by Date"
|
||||
|
||||
overrides:
|
||||
tv_show_directory: "/ytdl_sub_tv_shows"
|
||||
|
||||
TV Show Only Recent:
|
||||
presets:
|
||||
- "TV Show"
|
||||
- "Only Recent"
|
||||
|
||||
overrides:
|
||||
only_recent_date_range: "3weeks"
|
||||
|
||||
This creates two presets:
|
||||
|
||||
* ``TV Show``
|
||||
* Inherits the :doc:`prebuilt </prebuilt_presets/index>` ``Jellyfin TV Show by Date`` preset
|
||||
* Sets the output tv show directory
|
||||
* ``TV Show Only Recent``
|
||||
* Inherits the ``TV Show`` preset made above it and the ``Only Recent`` prebuilt preset
|
||||
* Sets only_recent preset to only keep the last 3 weeks worth of videos
|
||||
|
||||
Inheritance makes it easy to extend existing presets to include logic for your specific needs.
|
||||
|
|
@ -1,132 +1,140 @@
|
|||
Automating
|
||||
==========
|
||||
Automating Downloads
|
||||
====================
|
||||
|
||||
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_>`_.
|
||||
One of the key capabilities of ``ytdl-sub`` is how well it runs without user input, but to take advantage of this you must set up scheduling to execute the commands at some interval. How you set up this scheduling depends on which version of ``ytdl-sub`` you downloaded.
|
||||
|
||||
|
||||
:ref:`Guide for Docker and Unraid Containers <guides/getting_started/automating_downloads:docker and unraid>`
|
||||
|
||||
:ref:`Guide for Linux <guides/getting_started/automating_downloads:linux>`
|
||||
|
||||
:ref:`Guide for Windows <guides/getting_started/automating_downloads:windows>`
|
||||
|
||||
.. _cron tab manpage: https://man7.org/linux/man-pages/man5/crontab.5.html#EXAMPLE_CRON_FILE
|
||||
|
||||
.. _docker-unraid-setup:
|
||||
|
||||
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:
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: GUI Image
|
||||
|
||||
The script that will execute automatically is located at ``/config/ytdl-sub-configs/run-cron``.
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: ./compose.yaml
|
||||
:emphasize-lines: 4
|
||||
Access your container at http://localhost:8443/, then in the GUI terminal run these commands:
|
||||
|
||||
services:
|
||||
ytdl-sub:
|
||||
environment:
|
||||
CRON_SCHEDULE: "0 */6 * * *"
|
||||
# WARNING: See "Getting Started" -> "Automating" docs regarding throttles/bans:
|
||||
# CRON_RUN_ON_START: false
|
||||
.. code-block:: shell
|
||||
|
||||
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.
|
||||
echo '#!/bin/bash' > /config/ytdl-sub-configs/run_cron
|
||||
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> /config/ytdl-sub-configs/run_cron
|
||||
echo "echo 'Cron started, running ytdl-sub...'" >> /config/ytdl-sub-configs/run_cron
|
||||
echo "cd /config/ytdl-sub-configs" >> /config/ytdl-sub-configs/run_cron
|
||||
echo "ytdl-sub --config=config.yaml sub subscriptions.yaml" >> /config/ytdl-sub-configs/run_cron
|
||||
chmod +x /config/ytdl-sub-configs/run_cron
|
||||
chown abc:abc /config/ytdl-sub-configs/run_cron
|
||||
|
||||
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 can test the newly created script by running:
|
||||
|
||||
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.
|
||||
.. code-block:: shell
|
||||
|
||||
.. warning::
|
||||
/config/ytdl-sub-configs/run_cron
|
||||
|
||||
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.
|
||||
To create the cron definition, run the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
echo "# min hour day month weekday command" > /config/crontabs/abc
|
||||
echo " 0 */6 * * * /config/ytdl-sub-configs/run_cron" >> /config/crontabs/abc
|
||||
|
||||
This will run the script every 6 hours. To run every hour, change ``*/6`` to ``*/1``, or to run once a day, change the same value to the hour (in 24hr format) that you want it to run at. See the `cron tab manpage`_ for more options.
|
||||
|
||||
.. tab-item:: Headless Image
|
||||
|
||||
.. _LinuxServer's Universal Cron mod: https://github.com/linuxserver/docker-mods/tree/universal-cron
|
||||
|
||||
The first step is to ensure you have `LinuxServer's Universal Cron mod`_ enabled via the environment variable. For the GUI image, this is already included (no need to add it).
|
||||
|
||||
.. 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 # <-- Make sure you have this!
|
||||
volumes:
|
||||
# ensure directories have user permissions
|
||||
- </path/to/ytdl-sub/config>:/config
|
||||
- </path/to/ytdl-sub/tv_shows>:/tv_shows
|
||||
restart: unless-stopped
|
||||
|
||||
This line will tell your container to install and enable cron on start.
|
||||
|
||||
If you had to add this line, you will need to restart your container.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
docker compose restart
|
||||
|
||||
The script that will execute automatically is located at ``/config/run-cron``.
|
||||
|
||||
Access your container from the terminal by running:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
docker exec -itu abc ytdl-sub /bin/bash
|
||||
|
||||
then in the terminal run these commands:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
echo '#!/bin/bash' > /config/ytdl-sub-configs/run_cron
|
||||
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> /config/ytdl-sub-configs/run_cron
|
||||
echo "echo 'Cron started, running ytdl-sub...'" >> /config/ytdl-sub-configs/run_cron
|
||||
echo "cd /config/ytdl-sub-configs" >> /config/ytdl-sub-configs/run_cron
|
||||
echo "ytdl-sub --config=config.yaml sub subscriptions.yaml" >> /config/ytdl-sub-configs/run_cron
|
||||
chmod +x /config/ytdl-sub-configs/run_cron
|
||||
chown abc:abc /config/ytdl-sub-configs/run_cron
|
||||
|
||||
You can test the newly created script by running:
|
||||
|
||||
.. code-block::
|
||||
|
||||
/config/run_cron
|
||||
|
||||
To create the cron definition, run the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
echo "# min hour day month weekday command" > /config/crontabs/abc
|
||||
echo " 0 */6 * * * /config/run_cron" >> /config/crontabs/abc
|
||||
|
||||
This will run the script every 6 hours. To run every hour, change ``*/6`` to ``*/1``, or to run once a day, change the same value to the hour (in 24hr format) that you want it to run at. See the `cron tab manpage`_ for more options.
|
||||
|
||||
.. _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``:
|
||||
Linux
|
||||
-----
|
||||
|
||||
.. 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"
|
||||
crontab -e
|
||||
0 */6 * * * /config/run_cron
|
||||
|
||||
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
|
||||
-------
|
||||
To be tested (please contact code owner or join the discord server if you can test this out for us)
|
||||
|
||||
For most Windows users, the best way to run commands periodically is `the Task
|
||||
Scheduler`_:
|
||||
.. code-block:: powershell
|
||||
|
||||
.. 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
|
||||
ytdl-sub.exe --config \path\to\config\config.yaml sub \path\to\config\subscriptions.yaml
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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>`.
|
||||
15
docs/source/guides/getting_started/examples.rst
Normal file
15
docs/source/guides/getting_started/examples.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
=====================
|
||||
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.
|
||||
|
|
@ -1,263 +1,33 @@
|
|||
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
|
||||
Your first configuration will look pretty simple:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
configuration:
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
working_directory: '.ytdl-sub-downloads'
|
||||
|
||||
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.
|
||||
The first two lines in this ``config.yaml`` file are the ``configuration``, and define the ``working_directory``, which is described near the bottom of :ref:`this section <guides/getting_started/index:quick overview of \`\`ytdl-sub\`\`>`
|
||||
|
||||
|
||||
Using Custom Presets in Subscriptions
|
||||
--------------------------------------
|
||||
Line 4 begins the definition of your custom ``presets``, with line 5 being the name of your first custom ``preset``.
|
||||
|
||||
Subscription files can use custom presets just like any other prebuilt preset. Below
|
||||
shows a complete subscription file using the above two custom presets.
|
||||
Lines 7 and 8 tell ``ytdl-sub`` which :doc:`/prebuilt_presets/index` to expand on; these ``presets`` already indicate that the downloaded files should be:
|
||||
|
||||
.. code-block:: yaml
|
||||
- in a format usable by, and with metadata accessible to, Jellyfin
|
||||
- sorted by upload date, and
|
||||
- only uploaded in the last 2 months (and will also delete any files in the media library which were uploaded over 2 months ago)
|
||||
|
||||
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.
|
||||
Line 11 is an override variable, ``tv_show_directory``, that tells ``ytdl-sub`` where to save your downloaded files once they've been processed, also known as the ``output_directory``. In this case, the downloaded files will be saved to the ``youtube`` folder in the root ``tv_shows`` directory.
|
||||
32
docs/source/guides/getting_started/first_download.rst
Normal file
32
docs/source/guides/getting_started/first_download.rst
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
Initial Download
|
||||
================
|
||||
|
||||
Once you have a ``subscriptions.yaml`` file created and filled out, you can perform your first
|
||||
download. Access ``ytdl-sub``, navigate to the directory containing your ``subscriptions.yaml``
|
||||
file, then run the below command:
|
||||
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: Dry run
|
||||
|
||||
A dry run lets you check that your configuration doesn't throw any errors and what the expected output files of actually doing the download are, without actually downloading the full media.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
ytdl-sub --dry-run sub
|
||||
|
||||
.. tab-item:: Normal run
|
||||
|
||||
A normal run will download all files as determined by your ``presets`` and, once processing is finished, move the downloaded and processed files to your ``output_directory``.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
ytdl-sub sub
|
||||
|
||||
.. tab-item:: One-time download
|
||||
|
||||
Sometimes you may only want to download media once, in which case adding them to your ``subscriptions.yaml`` file is unneccessary. As an example, the below code will download the same videos as our subscription file:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
ytdl-sub dl --preset "Jellyfin TV Show by Date" --overrides.subscription_name "NOVA PBS" --overrides.subscription_value "https://www.youtube.com/@novapbs" --overrides.tv_show_genre "Documentaries"
|
||||
132
docs/source/guides/getting_started/first_sub.rst
Normal file
132
docs/source/guides/getting_started/first_sub.rst
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
Initial Subscription
|
||||
====================
|
||||
|
||||
Your first subscription should look something like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
__preset__:
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
music_directory: "/music"
|
||||
|
||||
# Can choose between:
|
||||
# - Plex TV Show by Date:
|
||||
# - Jellyfin TV Show by Date:
|
||||
# - Kodi TV Show by Date:
|
||||
#
|
||||
Jellyfin TV Show by Date:
|
||||
= Documentaries:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
|
||||
= Kids | = TV-Y:
|
||||
"Jake Trains": "https://www.youtube.com/@JakeTrains"
|
||||
|
||||
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"
|
||||
|
||||
Lets break this down:
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 1
|
||||
|
||||
__preset__:
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
music_directory: "/music"
|
||||
|
||||
|
||||
The first :ref:`__preset__ <config_reference/subscriptions_yaml:File Preset>` section is where we
|
||||
can set modifications that apply to every subscription in this file.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 6
|
||||
|
||||
# Can choose between:
|
||||
# - Plex TV Show by Date:
|
||||
# - Jellyfin TV Show by Date:
|
||||
# - Kodi TV Show by Date:
|
||||
#
|
||||
|
||||
Lines 6-10 are comments that get ignored when parsing YAML since they are prefixed with ``#``.
|
||||
It is good practice to leave informative comments in your config or subscription files to remind
|
||||
yourself of various things.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 11
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
|
||||
On line 11, we set the key to ``Jellyfin TV Show by Date``. This is a
|
||||
:ref:`prebuilt preset <prebuilt_presets/index:prebuilt presets>` that configures
|
||||
subscriptions to look like TV shows in the Jellyfin media player (can be changed to
|
||||
one of the presets outlined in the comment above). Setting it as a YAML key implies that all
|
||||
subscriptions underneath it will *inherit* this preset.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 11
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
= Documentaries:
|
||||
|
||||
Line 12 sets the key to ``= Documentaries``. When keys are prefixed with ``=``, it means we are
|
||||
setting the
|
||||
:ref:`subscription indent variable <config_reference/subscriptions_yaml:Beautifying Subscriptions>`.
|
||||
For TV Show presets, the first subscription indent variable maps to the TV show's genre.
|
||||
Setting subscription indent variables as a key implies all subscriptions underneath it will
|
||||
have this variable set.
|
||||
|
||||
To better understand what variables are used in prebuilt presets, refer to the
|
||||
:ref:`prebuilt preset reference <config_reference/prebuilt_presets/index:Prebuilt Preset Reference>`.
|
||||
Here you will see the underlying variables used in prebuilt presets that can be overwritten.
|
||||
We already overwrote a few of the variables in the ``__preset__`` section above to define our
|
||||
output directory.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 11
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
= Documentaries:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
|
||||
Line 13 is where we define our first subscription. We set the subscription name to ``NOVA PBS``,
|
||||
and the subscription value to ``https://www.youtube.com/@novapbs``. Referring to the
|
||||
:ref:`TV show preset reference <config_reference/prebuilt_presets/tv_show:TV Show>`,
|
||||
we can see that ``{subscription_name}`` is used to set the ``tv_show_name`` variable.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 11
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
= Documentaries:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
|
||||
= Kids | = TV-Y:
|
||||
"Jake Trains": "https://www.youtube.com/@JakeTrains"
|
||||
|
||||
Line 15 underneath ``Jellyfin TV Show by Date``, but at the same level as ``= Documentaries``.
|
||||
This means we'll inherit the TV show preset, but not the documentaries indent variable. We instead
|
||||
set the indent variables to ``= Kids | = TV-Y``. This sets two indent variables. We can set
|
||||
multiple presets and/or indent variables on the same key by using ``|`` as a separator.
|
||||
|
||||
Referring to the
|
||||
:ref:`TV show preset reference <config_reference/prebuilt_presets/tv_show:TV Show>`, the first
|
||||
two indent variables map to the TV show genre and TV show content rating.
|
||||
|
||||
The above info should be enough to understand the rest of the subscription file.
|
||||
|
|
@ -1,157 +1,104 @@
|
|||
Getting Started
|
||||
===============
|
||||
|
||||
Now that you've completed your install of ``ytdl-sub``, it's time to get started. This is a 3-step process:
|
||||
|
||||
- Create your configuration file (if the :doc:`/prebuilt_presets/index` don't fit your needs)
|
||||
- Create your subscription file
|
||||
- Automate starting YTDL-Sub
|
||||
|
||||
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/
|
||||
.. _navigate directories: https://en.wikipedia.org/wiki/Cd_(command)
|
||||
.. _YAML syntax: https://yaml.org/spec/1.2.2/#chapter-2-language-overview
|
||||
|
||||
|
||||
In order to use ``ytdl-sub`` in any of the forms listed in these docs, you will need some basic knowledge.
|
||||
|
||||
Architecture
|
||||
------------
|
||||
Be sure that you:
|
||||
☑ Can `navigate directories`_ in a command line interface (or CLI)
|
||||
|
||||
For most users, ``ytdl-sub`` works as follows:
|
||||
☑ Have a basic understanding of `YAML syntax`_
|
||||
|
||||
Subscriptions use presets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
If you plan on using the headless image of ``ytdl-sub``, you:
|
||||
☑ Can use ``nano`` or ``vim`` to edit OR
|
||||
|
||||
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.
|
||||
☑ Can mount the config directory somewhere you can open it using gui text editors
|
||||
|
||||
Presets configure plugins
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Additional useful (but not required) knowledge:
|
||||
☑ Understanding how :yt-dlp:`\ ` works
|
||||
|
||||
:doc:`A preset <../../prebuilt_presets/index>` is effectively a set of plugin
|
||||
configurations. Specifically, a preset consists of:
|
||||
Overview
|
||||
--------
|
||||
``ytdl-sub`` uses two types of YAML files:
|
||||
|
||||
- base presets that it inherits from and extends
|
||||
- plugin configurations
|
||||
subscriptions.yaml
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
Defines ``subscriptions``, which specify the media we want to recurrently download, like YouTube
|
||||
channels and playlists, SoundCloud artists, or any
|
||||
:yt-dlp:`yt-dlp supported site <blob/master/supportedsites.md>`. ``subscriptions`` use ``presets``
|
||||
to define how ``ytdl-sub`` should handle downloading, processing, and saving them.
|
||||
|
||||
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.
|
||||
``ytdl-sub`` comes packaged with many
|
||||
:ref:`prebuilt presets <prebuilt_presets/index:Prebuilt Presets>`
|
||||
that will play nicely with well-known media players.
|
||||
|
||||
Plugins do the work
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
config.yaml
|
||||
~~~~~~~~~~~
|
||||
To customize ``ytdl-sub`` to beyond the prebuilt presets, you will need a ``config.yaml`` file. This
|
||||
file is where custom ``presets`` can be defined to orchestrate ``ytdl-sub`` to your very specific needs.
|
||||
|
||||
``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.
|
||||
Running ytdl-sub
|
||||
~~~~~~~~~~~~~~~~
|
||||
To invoke ``ytdl-sub`` to download subscriptions, use the following command:
|
||||
|
||||
Presets and subscriptions accept overrides
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. tab-set-code::
|
||||
|
||||
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.
|
||||
.. code-block:: shell
|
||||
|
||||
ytdl-sub sub subscriptions.yaml
|
||||
|
||||
Subscriptions are grouped by indentation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. code-block:: powershell
|
||||
|
||||
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.
|
||||
ytdl-sub.exe sub subscriptions.yaml
|
||||
|
||||
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.
|
||||
``ytdl-sub`` initially downloads all files to a defined ``working_directory``. This is a temporary
|
||||
storage spot for metadata and media files so that errors during processing- if they occur- don't
|
||||
affect your existing media library. Once all file processing is complete, your media files are
|
||||
moved to the ``output_directory``.
|
||||
|
||||
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.
|
||||
Ready to Start?
|
||||
---------------
|
||||
|
||||
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.
|
||||
Now that you have installed ``ytdl-sub``, checked your skills, and gotten a bit of background on how ``ytdl-sub`` functions, read through the articles below to get started:
|
||||
|
||||
The configuration file extends pre-defined presets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
:doc:`Step 1: Initial Subscriptions <first_sub>`
|
||||
|
||||
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``.
|
||||
:doc:`Step 2: Your First Download <first_download>`
|
||||
|
||||
Minimize the work to only what's necessary
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
:doc:`Step 3: Automating Downloads <automating_downloads>`
|
||||
|
||||
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.
|
||||
Want to go a step further?
|
||||
|
||||
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.
|
||||
If you want to use atypical paths or specific configuration options, check out :doc:`Basic Configuration <first_config>`
|
||||
|
||||
For tips on creating your own presets when the prebuilt presets aren't cutting it, check out :doc:`Advanced Configuration <advanced_configuration>`
|
||||
|
||||
Caveats
|
||||
~~~~~~~
|
||||
Other docs that may be of use:
|
||||
|
||||
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>`.
|
||||
:doc:`/prebuilt_presets/index`
|
||||
|
||||
:doc:`examples`
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
:caption: Getting Started Guide
|
||||
:maxdepth: 1
|
||||
|
||||
subscriptions
|
||||
downloading
|
||||
first_sub
|
||||
first_download
|
||||
automating_downloads
|
||||
first_config
|
||||
quick_start
|
||||
advanced_configuration
|
||||
examples
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
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>`.
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
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>`.
|
||||
|
|
@ -2,15 +2,13 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
|
|
@ -19,13 +17,10 @@ and Python 3.10 or greater are required.
|
|||
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`.
|
||||
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
|
||||
|
|
@ -37,13 +32,12 @@ With a Python 3.10 virtual environment, you can clone and install the repo.
|
|||
|
||||
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.
|
||||
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
|
||||
make docker
|
||||
|
|
@ -2,105 +2,142 @@
|
|||
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.
|
||||
For automating ``subscriptions.yaml`` downloads to pull new media, see :ref:`this page <guides/getting_started/automating_downloads:docker and unraid>` on how to set up a cron job in any of the docker containers.
|
||||
|
||||
The ``ytdl-sub`` Docker images use :lsio:`LSIO-based images <\ >` and install ytdl-sub on top. There are two flavors to choose from.
|
||||
|
||||
.. margin::
|
||||
|
||||
.. tip::
|
||||
|
||||
The recommended docker image is the GUI image.
|
||||
|
||||
:ref:`Docker Compose <guides/install/docker:install with docker compose>` is the recommended way of setting up a ``ytdl-sub`` docker container.
|
||||
|
||||
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
|
||||
The GUI image uses LSIO's :lsio-gh:`docker-code-server image <\ >` for its base image. More info on other code-server environment variables can be found within its documentation.
|
||||
|
||||
After starting, the code-server will be running at http://localhost:8443. Open this page in a browser to access and interact with ``ytdl-sub``.
|
||||
|
||||
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.
|
||||
The headless image uses LSIO's :lsio-gh:`docker-baseimage-alpine image <\ >` for its base image. Execute the following command to access and interact with ``ytdl-sub``:
|
||||
|
||||
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.
|
||||
.. code-block:: bash
|
||||
|
||||
docker exec -u abc -it ytdl-sub /bin/bash
|
||||
|
||||
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:
|
||||
Docker Compose is an easy "set it and forget it" install method. Follow the instructions below to create a ``compose.yaml`` file for your chosen ``ytdl-sub`` image.
|
||||
|
||||
.. margin::
|
||||
|
||||
.. important::
|
||||
|
||||
Set the PUID and PGID to the UID and GID associated with the user you want to own the downloaded files. Setting these values to root UID and GID may create issues with your media managers.
|
||||
|
||||
.. tab-set::
|
||||
|
||||
.. tab-item:: GUI Image
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: compose.yaml
|
||||
|
||||
services:
|
||||
ytdl-sub:
|
||||
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
||||
container_name: ytdl-sub
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Los_Angeles
|
||||
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
|
||||
ports:
|
||||
- 8443:8443
|
||||
restart: unless-stopped
|
||||
|
||||
.. tab-item:: Headless Image
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: compose.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
|
||||
|
||||
Device Passthrough
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
For CPU or GPU passthrough, you must use either the GUI image or the headless Ubuntu image
|
||||
``ghcr.io/jmbannon/ytdl-sub:ubuntu-latest``.
|
||||
|
||||
The docker-compose examples use the GUI image.
|
||||
|
||||
CPU Passthrough
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: compose.yaml
|
||||
:emphasize-lines: 5-6
|
||||
: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"]
|
||||
services:
|
||||
ytdl-sub:
|
||||
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
||||
container_name: ytdl-sub
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # CPU passthrough
|
||||
restart: unless-stopped
|
||||
|
||||
GPU Passthrough
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. Awe
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: compose.yaml
|
||||
:emphasize-lines: 5-13
|
||||
|
||||
services:
|
||||
ytdl-sub:
|
||||
image: ghcr.io/jmbannon/ytdl-sub-gui:latest
|
||||
container_name: ytdl-sub
|
||||
environment:
|
||||
- ..
|
||||
- NVIDIA_DRIVER_CAPABILITIES=all # Nvidia ENV args
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- capabilities: ["gpu"] # GPU passthrough
|
||||
restart: unless-stopped
|
||||
|
||||
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:
|
||||
If you prefer to only run the container once, you can use the CLI command instead. The following command is for the gui image, and will not restart if it comes down for any reason. See `the Docker reference <https://docs.docker.com/engine/reference/run/>`_ for further information on the parameters and other options you can use.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
@ -115,32 +152,4 @@ host. The following command is for the gui image:
|
|||
-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.
|
||||
ghcr.io/jmbannon/ytdl-sub-gui:latest
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
Install by Platform
|
||||
===================
|
||||
|
||||
``ytdl-sub`` can be installed on the following platforms.
|
||||
|
||||
All installations require a 64-bit CPU. 32-bit is not supported.
|
||||
|
|
@ -9,8 +8,7 @@ All installations require a 64-bit CPU. 32-bit is not supported.
|
|||
|
||||
.. tip::
|
||||
|
||||
The recommended install method of ``ytdl-sub`` is one of our :doc:`docker containers
|
||||
</guides/install/docker>`.
|
||||
The recommended install method of ``ytdl-sub`` is one of our :doc:`docker containers </guides/install/docker>`. For install on Unraid, check out our :unraid:`unraid community apps <community/apps?q=ytdl-sub#r>`.
|
||||
|
||||
:doc:`/guides/install/docker`
|
||||
|
||||
|
|
@ -22,8 +20,9 @@ All installations require a 64-bit CPU. 32-bit is not supported.
|
|||
|
||||
:doc:`/guides/install/agnostic`
|
||||
|
||||
Once you've completed your installation, please refer to the
|
||||
:doc:`../getting_started/index` guide for next steps
|
||||
|
||||
|
||||
Once you've completed your installation, please refer to the :doc:`../getting_started/index` guide for next steps
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
Linux
|
||||
=====
|
||||
|
||||
``ytdl-sub`` should be installable using any Linux package manager, and requires ffmpeg
|
||||
to be installed.
|
||||
``ytdl-sub`` should be installable using any Linux package manager, and requires ffmpeg to be installed.
|
||||
|
||||
|
||||
.. tab-set::
|
||||
|
||||
|
|
@ -13,10 +13,9 @@ to be installed.
|
|||
|
||||
curl -L -o ytdl-sub https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub
|
||||
chmod +x ytdl-sub
|
||||
./ytdl-sub -h
|
||||
ytdl-sub -h
|
||||
|
||||
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to
|
||||
date:
|
||||
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to date:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
@ -35,10 +34,9 @@ to be installed.
|
|||
|
||||
curl -L -o ytdl-sub https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub_aarch64
|
||||
chmod +x ytdl-sub
|
||||
./ytdl-sub -h
|
||||
ytdl-sub -h
|
||||
|
||||
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to
|
||||
date:
|
||||
You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to date:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
@ -49,4 +47,4 @@ to be installed.
|
|||
|
||||
# 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
|
||||
mv ffmpeg-master-latest-linuxarm64-gpl/bin/ffprobe /usr/bin/ffprobe
|
||||
|
|
@ -1,29 +1,16 @@
|
|||
======
|
||||
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>`_.
|
||||
|
||||
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-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``.
|
||||
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.
|
||||
.. warning::
|
||||
|
||||
.. 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
|
||||
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
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
=======
|
||||
Windows
|
||||
=======
|
||||
|
||||
--------------
|
||||
From powershell, run:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
|
@ -14,4 +12,4 @@ From powershell, run:
|
|||
|
||||
# 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
|
||||
ytdl-sub.exe -h
|
||||
|
|
@ -10,6 +10,7 @@ ytdl-sub User Guide
|
|||
prebuilt_presets/index
|
||||
usage
|
||||
config_reference/index
|
||||
debugging
|
||||
faq/index
|
||||
deprecation_notices
|
||||
|
||||
.. note:: The docs are heavily work-in-progress. Please bear with us while we're under construction!
|
||||
|
|
@ -8,23 +8,10 @@ What is ytdl-sub?
|
|||
.. _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..
|
||||
``ytdl-sub`` is a command-line tool that downloads media via `yt-dlp`_ and prepares it for your favorite media player (`Kodi`_, `Jellyfin`_, `Plex`_, `Emby`_, modern music players).
|
||||
|
||||
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
|
||||
Visual examples
|
||||
===============
|
||||
|
||||
.. 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.
|
||||
|
|
@ -47,52 +34,10 @@ To these ends, ``ytdl-sub``:
|
|||
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 ytdl-sub?
|
||||
-------------
|
||||
There is a lack of open-source tools to download media and generate metadata to play it in these players. Most solutions involve using multiple tools or bash scripts to achieve this. ``ytdl-sub`` aims to consolidate all of this logic into a single easy-to-use application that can run automatically once configured.
|
||||
|
||||
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.
|
||||
We believe it is important to download what you like because there is no guarantee it will stay online forever. We also believe it is important to download it in such a way that it is easy to consume. Most solutions today force you to watch/listen to your downloaded content via file system or web browser. ``ytdl-sub`` aims to format downloaded content for any media player.
|
||||
|
|
@ -1,237 +1,37 @@
|
|||
|
||||
|
||||
|
||||
==============
|
||||
Helper Presets
|
||||
==============
|
||||
|
||||
.. hint::
|
||||
Common presets are not usable by themselves- setting one of these as the sole preset of your subscription and attempting to download will not work. But you can add these presets to quickly modify an existing preset to better suit your needs.
|
||||
|
||||
See how to apply helper presets :doc:`here </prebuilt_presets/index>`
|
||||
Best A/V Quality
|
||||
----------------
|
||||
|
||||
Add the following preset to download the best available video and audio quality, and remux it into an MP4 container:
|
||||
|
||||
``best_video_quality``
|
||||
|
||||
|
||||
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
|
||||
Max 1080p Video
|
||||
---------------
|
||||
|
||||
``Filter Keywords`` can include or exclude media with any of the listed keywords. Both
|
||||
keywords and title/description are lower-cased before filtering.
|
||||
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:
|
||||
|
||||
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.
|
||||
``max_1080p``
|
||||
|
||||
Supports the following override variables:
|
||||
Chunk Initial Download
|
||||
----------------------
|
||||
|
||||
* ``title_include_keywords``, ``title_include_eval``
|
||||
* ``title_exclude_keywords``, ``title_exclude_eval``
|
||||
* ``description_include_keywords``, ``title_exclude_eval``
|
||||
* ``description_exclude_keywords``, ``title_exclude_eval``
|
||||
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:
|
||||
|
||||
.. tip::
|
||||
``chunk_initial_download``
|
||||
|
||||
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``.
|
||||
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
|
||||
|
||||
__preset__:
|
||||
overrides:
|
||||
chunk_max_downloads: 20
|
||||
ytdl_options:
|
||||
max_downloads: 30 # Desired number to download per invocation
|
||||
|
||||
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) }
|
||||
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.
|
||||
|
|
@ -3,35 +3,12 @@ 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>`.
|
||||
media in various players. 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
|
||||
tv_shows
|
||||
music
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
======================
|
||||
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``
|
||||
|
|
@ -1,90 +1,3 @@
|
|||
=============
|
||||
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/"
|
||||
=============
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
===================
|
||||
Music Video Presets
|
||||
===================
|
||||
|
||||
WIP
|
||||
|
|
@ -2,63 +2,49 @@
|
|||
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.
|
||||
``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
|
||||
Kodi
|
||||
--------
|
||||
* Everything that the Jellyfin version does
|
||||
* Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with ``□``
|
||||
|
||||
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
|
||||
--------
|
||||
* :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
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
Generic Presets
|
||||
===============
|
||||
|
||||
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.
|
||||
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``
|
||||
* ``"Kodi TV Show by Date"``
|
||||
* ``"Jellyfin TV Show by Date"``
|
||||
* ``"Plex TV Show by Date"``
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -88,89 +74,47 @@ Must define ``tv_show_directory``. Available presets:
|
|||
Advanced Usage
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
If you prefer a different season/episode organization method, you can set the following
|
||||
override variables.
|
||||
If you prefer a different organization method, you can instead apply multiple presets to your subscriptions.
|
||||
|
||||
.. code-block:: yaml
|
||||
You will need a base of one of the below:
|
||||
|
||||
__preset__:
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
tv_show_by_date_season_ordering: "upload-year-month"
|
||||
tv_show_by_date_episode_ordering: "upload-day"
|
||||
* ``kodi_tv_show_by_date``
|
||||
* ``jellyfin_tv_show_by_date``
|
||||
* ``plex_tv_show_by_date``
|
||||
|
||||
Or for a specific preset
|
||||
And then add one of these:
|
||||
|
||||
.. code-block:: yaml
|
||||
* ``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 is fetched using the length of the download archive. Do not use if you intend to remove old videos.
|
||||
|
||||
"~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.
|
||||
(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.
|
||||
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.
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
Must define ``tv_show_directory``. Available presets:
|
||||
|
||||
* ``Kodi TV Show Collection``
|
||||
* ``Jellyfin TV Show Collection``
|
||||
* ``Emby TV Show Collection``
|
||||
* ``Plex TV Show Collection``
|
||||
* ``"Kodi TV Show Collection"``
|
||||
* ``"Jellyfin TV Show Collection"``
|
||||
* ``"Plex TV Show Collection"``
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -187,69 +131,22 @@ Must define ``tv_show_directory``. Available presets:
|
|||
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.
|
||||
If you prefer a different organization method, you can instead apply multiple presets to your subscriptions.
|
||||
|
||||
.. code-block:: yaml
|
||||
You will need a base of one of the below:
|
||||
|
||||
__preset__:
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
tv_show_collection_episode_ordering: "release-year-month-day"
|
||||
* ``kodi_tv_show_collection``
|
||||
* ``jellyfin_tv_show_collection``
|
||||
* ``plex_tv_show_collection``
|
||||
|
||||
Or for a specific preset
|
||||
And then add one of these:
|
||||
|
||||
.. 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.
|
||||
* ``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``
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Usage
|
||||
=====
|
||||
=======
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
|
@ -7,12 +7,10 @@ Usage
|
|||
|
||||
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 ...``:
|
||||
General options must be specified before the command (i.e. ``sub``).
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
|
|
@ -22,30 +20,24 @@ example ``$ ytdl-sub --dry-run sub ...``:
|
|||
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
|
||||
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
|
||||
-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>`.
|
||||
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 and defaults to
|
||||
``./subscriptions.yaml`` if none are given. It will use the config specified by
|
||||
``--config``, or ``./config.yaml``, if not provided.
|
||||
``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.
|
||||
|
||||
.. code-block:: text
|
||||
:caption: Additional Options
|
||||
|
|
@ -55,19 +47,16 @@ Download all subscriptions specified in each :doc:`subscriptions file
|
|||
-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>`:
|
||||
-----------------
|
||||
Download a single subscription in the form of CLI arguments.
|
||||
|
||||
.. 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:
|
||||
``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
|
||||
|
||||
|
|
@ -87,14 +76,11 @@ Using the command:
|
|||
--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>`.
|
||||
|
||||
See how to shorten commands using
|
||||
`download aliases <https://ytdl-sub.readthedocs.io/en/latest/config_reference/config_yaml.html#ytdl_sub.config.config_validator.ConfigOptions.dl_aliases>`_.
|
||||
|
||||
View Options
|
||||
------------
|
||||
|
||||
Preview the source variables for a given URL. Helpful to create new subscriptions:
|
||||
|
||||
-----------------
|
||||
.. code-block::
|
||||
|
||||
ytdl-sub view [-sc] [URL]
|
||||
|
|
@ -105,42 +91,5 @@ Preview the source variables for a given URL. Helpful to create new subscription
|
|||
-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'
|
||||
Preview the source variables for a given URL. Helps when creating new configs.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
###############################################################################
|
||||
# Top-level configurations to apply umask and write log files
|
||||
# Top-level configurations to apply umask and persist error logs
|
||||
configuration:
|
||||
umask: "002"
|
||||
persist_logs:
|
||||
logs_directory: '/config/logs'
|
||||
logs_directory: './logs'
|
||||
keep_successful_logs: False
|
||||
|
||||
presets:
|
||||
|
|
@ -69,7 +69,7 @@ presets:
|
|||
# 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"
|
||||
# cookiefile: "/config/youtube_cookies.txt"
|
||||
|
||||
# For YouTube, get English metadata if multiple languages are present
|
||||
extractor_args:
|
||||
|
|
@ -98,4 +98,4 @@ presets:
|
|||
|
||||
overrides:
|
||||
only_recent_date_range: "2months"
|
||||
only_recent_max_files: 30
|
||||
only_recent_max_files: 30
|
||||
|
|
@ -24,6 +24,6 @@ TV Show Only Recent:
|
|||
# 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"
|
||||
date_range: "2weeks"
|
||||
"Frontline PBS": "https://www.youtube.com/@frontline"
|
||||
"Whitehouse": "https://www.bitchute.com/channel/zWsYVmCOu4JA/" # Supports non-YT sites
|
||||
|
|
@ -31,7 +31,6 @@ __preset__:
|
|||
# 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:
|
||||
|
||||
|
|
@ -65,7 +64,6 @@ Plex TV Show by Date:
|
|||
# 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:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ classifiers = [
|
|||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
dependencies = [
|
||||
"yt-dlp[default]==2026.6.9",
|
||||
"yt-dlp==2024.5.27",
|
||||
"colorama~=0.4",
|
||||
"mergedeep~=1.3",
|
||||
"mediafile~=0.12",
|
||||
|
|
@ -43,28 +43,36 @@ where = ["src"]
|
|||
[project.optional-dependencies]
|
||||
test = [
|
||||
"coverage[toml]>=6.3,<8.0",
|
||||
"pytest>=7.2,<10.0",
|
||||
"pytest-rerunfailures>=14,<17",
|
||||
"pytest>=7.2,<9.0",
|
||||
"pytest-rerunfailures~=14.0",
|
||||
]
|
||||
lint = [
|
||||
"pylint==4.0.5",
|
||||
"ruff==0.15.16",
|
||||
"black==24.4.2",
|
||||
"isort==5.13.2",
|
||||
"pylint==3.2.3",
|
||||
]
|
||||
docs = [
|
||||
"sphinx>=7,<10",
|
||||
"sphinx-rtd-theme>=2,<4",
|
||||
"sphinx~=7.0",
|
||||
"sphinx-rtd-theme~=2.0",
|
||||
"sphinx-book-theme~=1.0",
|
||||
"sphinx-copybutton~=0.5",
|
||||
"sphinx_design~=0.6",
|
||||
]
|
||||
build = [
|
||||
"build~=1.2",
|
||||
"twine>=5,<7",
|
||||
"twine~=5.0",
|
||||
"pyinstaller~=6.5",
|
||||
]
|
||||
[project.scripts]
|
||||
ytdl-sub = "ytdl_sub.main:main"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
force_single_line = true
|
||||
|
||||
[tool.black]
|
||||
line_length = 100
|
||||
target-version = ["py310"]
|
||||
|
||||
[tool.pylint.MASTER]
|
||||
disable = [
|
||||
"C0115", # Missing class docstring
|
||||
|
|
@ -75,7 +83,6 @@ disable = [
|
|||
"R0901", # too-many-ancestors
|
||||
"R0902", # too-many-instance-attributes
|
||||
"R1711", # useless-return
|
||||
"R0917", # too many positional arguments
|
||||
"W0511", # TODO
|
||||
]
|
||||
|
||||
|
|
@ -90,27 +97,3 @@ include = [
|
|||
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"
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
__pypi_version__ = "2023.10.22.post3"
|
||||
__local_version__ = "2023.10.22+bfba4f0"
|
||||
__pypi_version__ = "2024.06.19.post2"
|
||||
__local_version__ = "2024.06.19+c2383614"
|
||||
|
|
|
|||
|
|
@ -1,31 +1,28 @@
|
|||
import gc
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import 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.output_transaction_log import _maybe_validate_transaction_log_file
|
||||
from ytdl_sub.cli.output_transaction_log import output_transaction_log
|
||||
from ytdl_sub.cli.parsers.dl import DownloadArgsParser
|
||||
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, parser
|
||||
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME
|
||||
from ytdl_sub.cli.parsers.main import 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.exceptions import ExperimentalFeatureNotEnabled
|
||||
from ytdl_sub.utils.exceptions import 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.
|
||||
|
|
@ -77,7 +74,6 @@ def _download_subscriptions_from_yaml_files(
|
|||
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.
|
||||
|
|
@ -94,8 +90,6 @@ def _download_subscriptions_from_yaml_files(
|
|||
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
|
||||
-------
|
||||
|
|
@ -117,10 +111,6 @@ def _download_subscriptions_from_yaml_files(
|
|||
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(
|
||||
|
|
@ -171,7 +161,7 @@ def _download_subscription_from_cli(
|
|||
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_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
|
||||
|
|
@ -205,44 +195,6 @@ def _view_url_from_cli(config: ConfigFile, url: str, split_chapters: bool) -> Su
|
|||
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
|
||||
|
|
@ -254,10 +206,6 @@ def main() -> List[Subscription]:
|
|||
|
||||
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)
|
||||
|
|
@ -269,23 +217,6 @@ def main() -> List[Subscription]:
|
|||
|
||||
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)
|
||||
|
||||
|
|
@ -317,7 +248,6 @@ def main() -> List[Subscription]:
|
|||
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
|
||||
|
|
@ -333,7 +263,7 @@ def main() -> List[Subscription]:
|
|||
_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")
|
||||
raise ValidationException("Must provide one of the commands: sub, dl, view")
|
||||
|
||||
if not args.suppress_transaction_log:
|
||||
output_transaction_log(
|
||||
|
|
@ -341,6 +271,6 @@ def main() -> List[Subscription]:
|
|||
transaction_log_file_path=args.transaction_log,
|
||||
)
|
||||
|
||||
output_summary(subscriptions, suppress_colors=args.suppress_colors)
|
||||
output_summary(subscriptions)
|
||||
|
||||
return subscriptions
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ 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 _green(value: str) -> str:
|
||||
return 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 _red(value: str) -> str:
|
||||
return 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 _no_color(value: str) -> str:
|
||||
return Fore.RESET + value + Fore.RESET
|
||||
|
||||
|
||||
def _str_int(value: int) -> str:
|
||||
|
|
@ -26,23 +26,21 @@ def _str_int(value: int) -> str:
|
|||
return str(value)
|
||||
|
||||
|
||||
def _color_int(value: int, suppress_colors: bool = False) -> str:
|
||||
def _color_int(value: int) -> str:
|
||||
str_int = _str_int(value)
|
||||
if value > 0:
|
||||
return _green(str_int, suppress_colors)
|
||||
return _green(str_int)
|
||||
if value < 0:
|
||||
return _red(str_int, suppress_colors)
|
||||
return _no_color(str_int, suppress_colors)
|
||||
return _red(str_int)
|
||||
return _no_color(str_int)
|
||||
|
||||
|
||||
def output_summary(subscriptions: List[Subscription], suppress_colors: bool) -> None:
|
||||
def output_summary(subscriptions: List[Subscription]) -> None:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
subscriptions
|
||||
Processed subscriptions
|
||||
suppress_colors
|
||||
Whether to have color or not
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -67,21 +65,21 @@ def output_summary(subscriptions: List[Subscription], suppress_colors: bool) ->
|
|||
|
||||
# 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_added: int = len(_color_int(total_added))
|
||||
width_num_entries_modified: int = len(_color_int(total_modified))
|
||||
width_num_entries_removed: int = len(_color_int(total_removed))
|
||||
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_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 = (
|
||||
_red(subscription.exception.__class__.__name__, suppress_colors)
|
||||
_red(subscription.exception.__class__.__name__)
|
||||
if subscription.exception
|
||||
else _green("✔", suppress_colors)
|
||||
else _green("✔")
|
||||
)
|
||||
|
||||
summary.append(
|
||||
|
|
@ -94,16 +92,14 @@ def output_summary(subscriptions: List[Subscription], suppress_colors: bool) ->
|
|||
)
|
||||
|
||||
total_errors_str = (
|
||||
_green("Success", suppress_colors)
|
||||
if total_errors == 0
|
||||
else _red(f"Error{'s' if total_errors > 1 else ''}", suppress_colors)
|
||||
_green("Success") if total_errors == 0 else _red(f"Error{'s' if total_errors > 1 else ''}")
|
||||
)
|
||||
|
||||
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"{_color_int(total_added):>{width_num_entries_added}} "
|
||||
f"{_color_int(total_modified):>{width_num_entries_modified}} "
|
||||
f"{_color_int(total_removed):>{width_num_entries_removed}} "
|
||||
f"{total_entries:>{width_num_entries}} "
|
||||
f"{total_errors_str}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Optional
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.subscriptions.subscription import Subscription
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
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,7 +1,10 @@
|
|||
import hashlib
|
||||
import re
|
||||
import shlex
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
|
||||
from mergedeep import mergedeep
|
||||
|
||||
|
|
@ -239,13 +242,12 @@ class DownloadArgsParser:
|
|||
|
||||
return subscription_dict
|
||||
|
||||
def get_dl_subscription_name(self) -> str:
|
||||
def get_args_hash(self) -> str:
|
||||
"""
|
||||
Returns a deterministic name based on input args
|
||||
:return: Hash of the arguments provided
|
||||
"""
|
||||
to_hash = str(sorted(self._unknown_arguments))
|
||||
hash_str = hashlib.sha256(to_hash.encode()).hexdigest()[-8:]
|
||||
return f"cli-dl-{hash_str}"
|
||||
hash_string = str(sorted(self._unknown_arguments))
|
||||
return hashlib.sha256(hash_string.encode()).hexdigest()[-8:]
|
||||
|
||||
@classmethod
|
||||
def from_dl_override(cls, override: str, config: ConfigFile) -> "DownloadArgsParser":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import argparse
|
||||
import dataclasses
|
||||
from typing import Dict, List
|
||||
from typing import List
|
||||
|
||||
from ytdl_sub import __local_version__
|
||||
from ytdl_sub.utils.logger import LoggerLevels
|
||||
|
|
@ -44,7 +44,6 @@ class MainArguments:
|
|||
short="-m",
|
||||
long="--match",
|
||||
)
|
||||
SUPPRESS_COLORS = CLIArgument(short="-nc", long="--suppress-colors")
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> List[CLIArgument]:
|
||||
|
|
@ -60,7 +59,6 @@ class MainArguments:
|
|||
cls.TRANSACTION_LOG,
|
||||
cls.SUPPRESS_TRANSACTION_LOG,
|
||||
cls.MATCH,
|
||||
cls.SUPPRESS_COLORS,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
|
@ -111,8 +109,8 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
|
|||
MainArguments.LOG_LEVEL.long,
|
||||
metavar="|".join(LoggerLevels.names()),
|
||||
type=str,
|
||||
help="level of logs to print to console, defaults to verbose",
|
||||
default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.VERBOSE.name,
|
||||
help="level of logs to print to console, defaults to info",
|
||||
default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.INFO.name,
|
||||
choices=LoggerLevels.names(),
|
||||
dest="ytdl_sub_log_level",
|
||||
)
|
||||
|
|
@ -131,13 +129,6 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults
|
|||
help="do not output transaction logs to console or file",
|
||||
default=argparse.SUPPRESS if suppress_defaults else False,
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
MainArguments.SUPPRESS_COLORS.short,
|
||||
MainArguments.SUPPRESS_COLORS.long,
|
||||
action="store_true",
|
||||
help="do not use colors in ytdl-sub output",
|
||||
default=argparse.SUPPRESS if suppress_defaults else False,
|
||||
)
|
||||
arg_parser.add_argument(
|
||||
MainArguments.MATCH.short,
|
||||
MainArguments.MATCH.long,
|
||||
|
|
@ -172,10 +163,6 @@ class SubArguments:
|
|||
short="-o",
|
||||
long="--dl-override",
|
||||
)
|
||||
SHUFFLE = CLIArgument(
|
||||
short="-sh",
|
||||
long="--shuffle",
|
||||
)
|
||||
|
||||
|
||||
subscription_parser = subparsers.add_parser("sub")
|
||||
|
|
@ -201,13 +188,6 @@ subscription_parser.add_argument(
|
|||
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
|
||||
|
|
@ -232,81 +212,3 @@ view_parser.add_argument(
|
|||
help="View source variables after splitting by chapters",
|
||||
)
|
||||
view_parser.add_argument("url", help="URL to view source variables for")
|
||||
|
||||
###################################################################################################
|
||||
# CLI-TO-SUB PARSER
|
||||
cli_to_sub_parser = subparsers.add_parser("cli-to-sub")
|
||||
|
||||
###################################################################################################
|
||||
# INSPECT PARSER
|
||||
|
||||
|
||||
class InspectArguments:
|
||||
LEVEL = CLIArgument(
|
||||
short="-l",
|
||||
long="--level",
|
||||
)
|
||||
LevelChoices: Dict[str, str] = {
|
||||
"0": "original",
|
||||
"1": "fill",
|
||||
"2": "resolve",
|
||||
"3": "internal",
|
||||
}
|
||||
|
||||
MOCK = CLIArgument(
|
||||
short="-k",
|
||||
long="--mock",
|
||||
)
|
||||
|
||||
|
||||
inspect_parser = subparsers.add_parser("inspect", formatter_class=argparse.RawTextHelpFormatter)
|
||||
inspect_parser.add_argument(
|
||||
InspectArguments.LEVEL.short,
|
||||
InspectArguments.LEVEL.long,
|
||||
metavar=",".join(str(i) for i in range(4)),
|
||||
type=str,
|
||||
help="""level of inspection to perform:
|
||||
0 - original present the subscription as-is
|
||||
1 - fill fill in defined values
|
||||
2 - resolve resolve all possible variables (default)
|
||||
3 - internal resolve all variables to their internal representation
|
||||
""",
|
||||
default="resolve",
|
||||
choices=list(InspectArguments.LevelChoices.keys())
|
||||
+ list(InspectArguments.LevelChoices.values()),
|
||||
dest="inspection_level",
|
||||
)
|
||||
inspect_parser.add_argument(
|
||||
MainArguments.MATCH.short,
|
||||
MainArguments.MATCH.long,
|
||||
dest="match",
|
||||
nargs="+",
|
||||
action="extend",
|
||||
type=str,
|
||||
help="match subscription names to one or more substrings, and only run those subscriptions",
|
||||
default=[],
|
||||
)
|
||||
|
||||
inspect_parser.add_argument(
|
||||
"subscription_paths",
|
||||
metavar="SUBPATH",
|
||||
nargs="*",
|
||||
help="path to subscription files, uses subscriptions.yaml if not provided",
|
||||
default=["subscriptions.yaml"],
|
||||
)
|
||||
|
||||
inspect_parser.add_argument(
|
||||
SubArguments.OVERRIDE.short,
|
||||
SubArguments.OVERRIDE.long,
|
||||
type=str,
|
||||
help="override all subscription config values using `dl` syntax, "
|
||||
"i.e. --dl-override='--ytdl_options.max_downloads 3'",
|
||||
)
|
||||
|
||||
inspect_parser.add_argument(
|
||||
InspectArguments.MOCK.short,
|
||||
InspectArguments.MOCK.long,
|
||||
metavar="VAR=VALUE",
|
||||
action="append",
|
||||
help="ability to mock one or more variable values, i.e. --mock 'title=Lets Play'",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from ytdl_sub.config.config_validator import ConfigValidator
|
||||
from ytdl_sub.config.preset import Preset
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
import os
|
||||
import posixpath
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from mergedeep import mergedeep
|
||||
from yt_dlp.utils import datetime_from_str
|
||||
|
||||
from ytdl_sub.config.defaults import (
|
||||
DEFAULT_FFMPEG_PATH,
|
||||
DEFAULT_FFPROBE_PATH,
|
||||
DEFAULT_LOCK_DIRECTORY,
|
||||
MAX_FILE_NAME_BYTES,
|
||||
)
|
||||
from ytdl_sub.config.defaults import DEFAULT_FFMPEG_PATH
|
||||
from ytdl_sub.config.defaults import DEFAULT_FFPROBE_PATH
|
||||
from ytdl_sub.config.defaults import DEFAULT_LOCK_DIRECTORY
|
||||
from ytdl_sub.config.defaults import MAX_FILE_NAME_BYTES
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS
|
||||
from ytdl_sub.utils.exceptions import SubscriptionPermissionError
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.validators.file_path_validators import FFmpegFileValidator, FFprobeFileValidator
|
||||
from ytdl_sub.validators.file_path_validators import FFmpegFileValidator
|
||||
from ytdl_sub.validators.file_path_validators import FFprobeFileValidator
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.validators import (
|
||||
BoolValidator,
|
||||
IntValidator,
|
||||
LiteralDictValidator,
|
||||
StringValidator,
|
||||
)
|
||||
from ytdl_sub.validators.validators import BoolValidator
|
||||
from ytdl_sub.validators.validators import IntValidator
|
||||
from ytdl_sub.validators.validators import LiteralDictValidator
|
||||
from ytdl_sub.validators.validators import StringValidator
|
||||
|
||||
|
||||
class ExperimentalValidator(StrictDictValidator):
|
||||
"""
|
||||
Experimental flags reside under the ``experimental`` key.
|
||||
"""
|
||||
|
||||
_optional_keys = {"enable_update_with_info_json"}
|
||||
_allow_extra_keys = True
|
||||
|
||||
|
|
@ -50,11 +43,6 @@ class ExperimentalValidator(StrictDictValidator):
|
|||
|
||||
|
||||
class PersistLogsValidator(StrictDictValidator):
|
||||
"""
|
||||
By default, no logs are persisted. Specifying this key will enable persisted logs. The following
|
||||
options are available.
|
||||
"""
|
||||
|
||||
_required_keys = {"logs_directory"}
|
||||
_optional_keys = {"keep_logs_after", "keep_successful_logs"}
|
||||
|
||||
|
|
@ -79,8 +67,7 @@ class PersistLogsValidator(StrictDictValidator):
|
|||
@property
|
||||
def logs_directory(self) -> str:
|
||||
"""
|
||||
Required field. Write log files to this directory with names like
|
||||
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
|
||||
Required. The directory to store the logs in.
|
||||
"""
|
||||
return self._logs_directory.value
|
||||
|
||||
|
|
@ -105,54 +92,12 @@ class PersistLogsValidator(StrictDictValidator):
|
|||
@property
|
||||
def keep_successful_logs(self) -> bool:
|
||||
"""
|
||||
Defaults to ``True``. When this key is ``False``, only write log files for failed
|
||||
subscriptions.
|
||||
Optional. Whether to store logs when downloading is successful. Defaults to True.
|
||||
"""
|
||||
return self._keep_successful_logs.value
|
||||
|
||||
|
||||
class ConfigOptions(StrictDictValidator):
|
||||
"""
|
||||
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"
|
||||
|
||||
"""
|
||||
|
||||
_optional_keys = {
|
||||
"working_directory",
|
||||
"umask",
|
||||
|
|
@ -198,18 +143,11 @@ class ConfigOptions(StrictDictValidator):
|
|||
key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
|
||||
)
|
||||
|
||||
if not FileHandler.is_path_writable(self.working_directory):
|
||||
raise SubscriptionPermissionError(
|
||||
"ytdl-sub does not have permissions to the working directory: "
|
||||
f"{self.working_directory}"
|
||||
)
|
||||
|
||||
@property
|
||||
def working_directory(self) -> str:
|
||||
"""
|
||||
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.
|
||||
directory. Defaults to .ytdl-sub-working-directory
|
||||
"""
|
||||
# Expands tildas to actual paths, use native os sep
|
||||
return os.path.expanduser(self._working_directory.value.replace(posixpath.sep, os.sep))
|
||||
|
|
@ -217,7 +155,7 @@ class ConfigOptions(StrictDictValidator):
|
|||
@property
|
||||
def umask(self) -> Optional[str]:
|
||||
"""
|
||||
Umask in octal format to apply to every created file. Defaults to ``022``.
|
||||
Umask (octal format) to apply to every created file. Defaults to "022".
|
||||
"""
|
||||
return self._umask.value
|
||||
|
||||
|
|
@ -226,7 +164,7 @@ class ConfigOptions(StrictDictValidator):
|
|||
"""
|
||||
.. _dl_aliases:
|
||||
|
||||
Alias definitions to shorten :ref:`dl arguments <usage:Download Options>`. For example,
|
||||
Alias definitions to shorten ``ytdl-sub dl`` arguments. For example,
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -276,25 +214,24 @@ class ConfigOptions(StrictDictValidator):
|
|||
def lock_directory(self) -> str:
|
||||
"""
|
||||
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``.
|
||||
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``.
|
||||
"""
|
||||
return self._lock_directory.value
|
||||
|
||||
@property
|
||||
def ffmpeg_path(self) -> str:
|
||||
"""
|
||||
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux,
|
||||
``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows.
|
||||
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux, and
|
||||
``ffmpeg.exe`` for Windows (in the same directory as ytdl-sub).
|
||||
"""
|
||||
return self._ffmpeg_path.value
|
||||
|
||||
@property
|
||||
def ffprobe_path(self) -> str:
|
||||
"""
|
||||
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux,
|
||||
``./ffprobe.exe`` in the same directory as ytdl-sub for Windows.
|
||||
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux, and
|
||||
``ffprobe.exe`` for Windows (in the same directory as ytdl-sub).
|
||||
"""
|
||||
return self._ffprobe_path.value
|
||||
|
||||
|
|
|
|||
|
|
@ -2,21 +2,6 @@ import os
|
|||
|
||||
from ytdl_sub.utils.system import IS_WINDOWS
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _existing_path(*paths: str) -> str:
|
||||
"""
|
||||
Tries to search multiple paths for a file, returns a
|
||||
path if it exists. If none are valid files, returns
|
||||
the first one
|
||||
"""
|
||||
for path in paths:
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return paths[0]
|
||||
|
||||
|
||||
if IS_WINDOWS:
|
||||
DEFAULT_LOCK_DIRECTORY = "" # Not supported in Windows
|
||||
DEFAULT_FFMPEG_PATH = ".\\ffmpeg.exe"
|
||||
|
|
@ -24,13 +9,9 @@ if IS_WINDOWS:
|
|||
|
||||
MAX_FILE_NAME_BYTES = 255
|
||||
else:
|
||||
DEFAULT_LOCK_DIRECTORY = ".ytdl-sub-lock"
|
||||
DEFAULT_FFMPEG_PATH = os.getenv(
|
||||
"YTDL_SUB_FFMPEG_PATH", _existing_path("/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg")
|
||||
)
|
||||
DEFAULT_FFPROBE_PATH = os.getenv(
|
||||
"YTDL_SUB_FFPROBE_PATH", _existing_path("/usr/bin/ffprobe", "/usr/local/bin/ffmpeg")
|
||||
)
|
||||
DEFAULT_LOCK_DIRECTORY = "/tmp"
|
||||
DEFAULT_FFMPEG_PATH = "/usr/bin/ffmpeg"
|
||||
DEFAULT_FFPROBE_PATH = "/usr/bin/ffprobe"
|
||||
|
||||
MAX_FILE_NAME_BYTES = os.pathconf("/", "PC_NAME_MAX")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,24 @@
|
|||
from typing import Any, Dict, Iterable, Optional, Set, Type, TypeVar
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
import mergedeep
|
||||
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.variables.override_variables import (
|
||||
REQUIRED_OVERRIDE_VARIABLE_NAMES,
|
||||
OverrideHelpers,
|
||||
)
|
||||
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||
from ytdl_sub.entries.variables.override_variables import OverrideHelpers
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.types.function import BuiltInFunction
|
||||
from ytdl_sub.script.types.resolvable import Resolvable, String
|
||||
from ytdl_sub.script.types.syntax_tree import SyntaxTree
|
||||
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
||||
from ytdl_sub.utils.exceptions import (
|
||||
InvalidVariableNameException,
|
||||
StringFormattingException,
|
||||
ValidationException,
|
||||
)
|
||||
from ytdl_sub.utils.exceptions import InvalidVariableNameException
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.utils.scriptable import Scriptable
|
||||
from ytdl_sub.validators.string_formatter_validators import (
|
||||
StringFormatterValidator,
|
||||
UnstructuredDictFormatterValidator,
|
||||
)
|
||||
|
||||
ExpectedT = TypeVar("ExpectedT")
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator
|
||||
|
||||
|
||||
class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
||||
|
|
@ -92,24 +86,6 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
|
||||
return True
|
||||
|
||||
def ensure_variable_names_not_a_plugin(self, plugin_names: Iterable[str]) -> None:
|
||||
"""
|
||||
Throws an error if an override variable or function has the same name as a
|
||||
preset key. This is to avoid confusion when accidentally defining things in
|
||||
overrides that are meant to be in the preset.
|
||||
"""
|
||||
for name in self.keys:
|
||||
if name.startswith("%"):
|
||||
name = name[1:]
|
||||
|
||||
if name in plugin_names:
|
||||
raise self._validation_exception(
|
||||
f"Override variable with name {name} cannot be used since it is"
|
||||
" the name of a plugin. Perhaps you meant to define it as a plugin? If so,"
|
||||
" indent it left to make it at the same level as overrides.",
|
||||
exception_class=InvalidVariableNameException,
|
||||
)
|
||||
|
||||
def ensure_variable_name_valid(self, name: str) -> None:
|
||||
"""
|
||||
Ensures the variable name does not collide with any entry variables or built-in functions.
|
||||
|
|
@ -137,35 +113,29 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
)
|
||||
|
||||
def initial_variables(
|
||||
self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None
|
||||
) -> Dict[str, SyntaxTree]:
|
||||
self, unresolved_variables: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
Variables and format strings for all Override variables + additional variables (Optional)
|
||||
"""
|
||||
initial_variables: Dict[str, SyntaxTree] = self.dict_with_parsed_format_strings
|
||||
if unresolved_variables:
|
||||
initial_variables |= unresolved_variables
|
||||
return ScriptUtils.add_sanitized_parsed_variables(initial_variables)
|
||||
initial_variables: Dict[str, str] = {}
|
||||
mergedeep.merge(
|
||||
initial_variables,
|
||||
self.dict_with_format_strings,
|
||||
unresolved_variables if unresolved_variables else {},
|
||||
)
|
||||
return ScriptUtils.add_sanitized_variables(initial_variables)
|
||||
|
||||
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
|
||||
"""
|
||||
Initialize the override script with any unresolved variables
|
||||
"""
|
||||
self.script.add_parsed(
|
||||
self.script.add(
|
||||
self.initial_variables(
|
||||
unresolved_variables={
|
||||
var_name: SyntaxTree(
|
||||
ast=[
|
||||
BuiltInFunction(
|
||||
name="throw",
|
||||
args=[
|
||||
String(f"Plugin variable {var_name} has not been created yet")
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
var_name: f"{{%throw('Plugin variable {var_name} has not been created yet')}}"
|
||||
for var_name in unresolved_variables
|
||||
}
|
||||
)
|
||||
|
|
@ -174,43 +144,12 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
self.update_script()
|
||||
return self
|
||||
|
||||
def _apply_to_resolvable(
|
||||
self,
|
||||
formatter: StringFormatterValidator,
|
||||
entry: Optional[Entry],
|
||||
function_overrides: Optional[Dict[str, str]],
|
||||
) -> Resolvable:
|
||||
script: Script = self.script
|
||||
unresolvable: Set[str] = self.unresolvable
|
||||
if entry:
|
||||
script = entry.script
|
||||
unresolvable = entry.unresolvable
|
||||
|
||||
# Update the script internally so long as we are not supplying overrides
|
||||
# that could alter the script with one-off state
|
||||
update = function_overrides is None
|
||||
|
||||
try:
|
||||
return script.resolve_once(
|
||||
dict({"tmp_var": formatter.format_string}, **(function_overrides or {})),
|
||||
unresolvable=unresolvable,
|
||||
update=update,
|
||||
)["tmp_var"]
|
||||
except ScriptVariableNotResolved as exc:
|
||||
raise StringFormattingException(
|
||||
"Tried to resolve the following script, but could not due to unresolved "
|
||||
f"variables:\n {formatter.format_string}\n"
|
||||
"This is most likely due to circular dependencies in variables. "
|
||||
"If you think otherwise, please file a bug on GitHub and post your config. Thanks!"
|
||||
) from exc
|
||||
|
||||
def apply_formatter(
|
||||
self,
|
||||
formatter: StringFormatterValidator,
|
||||
entry: Optional[Entry] = None,
|
||||
function_overrides: Optional[Dict[str, str]] = None,
|
||||
expected_type: Type[ExpectedT] = str,
|
||||
) -> ExpectedT:
|
||||
function_overrides: Dict[str, str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -220,8 +159,6 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
Optional. Entry to add source variables to the formatter
|
||||
function_overrides
|
||||
Optional. Explicit values to override the overrides themselves and source variables
|
||||
expected_type
|
||||
The expected type that should return. Defaults to string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -232,15 +169,25 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
StringFormattingException
|
||||
If the formatter that is trying to be resolved cannot
|
||||
"""
|
||||
out = formatter.post_process(
|
||||
self._apply_to_resolvable(
|
||||
formatter=formatter, entry=entry, function_overrides=function_overrides
|
||||
).native
|
||||
)
|
||||
script: Script = self.script
|
||||
unresolvable: Set[str] = self.unresolvable
|
||||
if entry:
|
||||
script = entry.script
|
||||
unresolvable = entry.unresolvable
|
||||
|
||||
if not isinstance(out, expected_type):
|
||||
raise StringFormattingException(
|
||||
f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'"
|
||||
try:
|
||||
return formatter.post_process(
|
||||
str(
|
||||
script.resolve_once(
|
||||
dict({"tmp_var": formatter.format_string}, **(function_overrides or {})),
|
||||
unresolvable=unresolvable,
|
||||
)["tmp_var"]
|
||||
)
|
||||
)
|
||||
|
||||
return out
|
||||
except ScriptVariableNotResolved as exc:
|
||||
raise StringFormattingException(
|
||||
"Tried to resolve the following script, but could not due to unresolved "
|
||||
f"variables:\n {formatter.format_string}\n"
|
||||
"This is most likely due to circular dependencies in variables. "
|
||||
"If you think otherwise, please file a bug on GitHub and post your config. Thanks!"
|
||||
) from exc
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from functools import cached_property
|
||||
from typing import Dict, Generic, List, Optional, Tuple, Type
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT, ToggleableOptionsDictValidator
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import (
|
||||
DownloadArchiver,
|
||||
EnhancedDownloadArchive,
|
||||
)
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
|
@ -43,7 +49,9 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
|
|||
Returns True if enabled, False if disabled.
|
||||
"""
|
||||
if isinstance(self.plugin_options, ToggleableOptionsDictValidator):
|
||||
return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool)
|
||||
return ScriptUtils.bool_formatter_output(
|
||||
self.overrides.apply_formatter(self.plugin_options.enable)
|
||||
)
|
||||
return True
|
||||
|
||||
def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:
|
||||
|
|
@ -61,13 +69,6 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
|
|||
ytdl options to enable/disable when downloading entries for this specific plugin
|
||||
"""
|
||||
|
||||
def initialize_subscription(self) -> bool:
|
||||
"""
|
||||
Before any downloading begins, perform initialization before the subscription runs.
|
||||
Returns true if this subscription should run, false otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
def modify_entry_metadata(self, entry: Entry) -> Optional[Entry]:
|
||||
"""
|
||||
After entry metadata has been gathered, perform preprocessing on the metadata
|
||||
|
|
@ -114,17 +115,6 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
|
|||
"""
|
||||
return None
|
||||
|
||||
def post_completion_entry(self, file_metadata: FileMetadata) -> None:
|
||||
"""
|
||||
After the entry file is moved to its final location, run this hook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file_metadata
|
||||
Metadata about the completed entry's file download
|
||||
"""
|
||||
return None
|
||||
|
||||
def post_process_subscription(self):
|
||||
"""
|
||||
After all downloaded files have been post-processed, apply a subscription-wide post process
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from typing import Dict, List, Optional, Tuple, Type
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin, SplitPlugin
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.plugin.plugin import SplitPlugin
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.downloaders.url.downloader import (
|
||||
UrlDownloaderCollectionVariablePlugin,
|
||||
UrlDownloaderThumbnailPlugin,
|
||||
)
|
||||
from ytdl_sub.downloaders.url.downloader import UrlDownloaderCollectionVariablePlugin
|
||||
from ytdl_sub.downloaders.url.downloader import UrlDownloaderThumbnailPlugin
|
||||
from ytdl_sub.plugins.audio_extract import AudioExtractPlugin
|
||||
from ytdl_sub.plugins.chapters import ChaptersPlugin
|
||||
from ytdl_sub.plugins.date_range import DateRangePlugin
|
||||
|
|
@ -20,9 +23,8 @@ from ytdl_sub.plugins.match_filters import MatchFiltersPlugin
|
|||
from ytdl_sub.plugins.music_tags import MusicTagsPlugin
|
||||
from ytdl_sub.plugins.nfo_tags import NfoTagsPlugin
|
||||
from ytdl_sub.plugins.output_directory_nfo_tags import OutputDirectoryNfoTagsPlugin
|
||||
from ytdl_sub.plugins.regex import RegexPlugin
|
||||
from ytdl_sub.plugins.split_by_chapters import SplitByChaptersPlugin
|
||||
from ytdl_sub.plugins.square_thumbnail import SquareThumbnailPlugin
|
||||
from ytdl_sub.plugins.static_nfo_tags import StaticNfoTagsPlugin
|
||||
from ytdl_sub.plugins.subtitles import SubtitlesPlugin
|
||||
from ytdl_sub.plugins.throttle_protection import ThrottleProtectionPlugin
|
||||
from ytdl_sub.plugins.video_tags import VideoTagsPlugin
|
||||
|
|
@ -38,7 +40,6 @@ class PluginMapping:
|
|||
"audio_extract": AudioExtractPlugin,
|
||||
"date_range": DateRangePlugin,
|
||||
"embed_thumbnail": EmbedThumbnailPlugin,
|
||||
"square_thumbnail": SquareThumbnailPlugin,
|
||||
"file_convert": FileConvertPlugin,
|
||||
"format": FormatPlugin,
|
||||
"match_filters": MatchFiltersPlugin,
|
||||
|
|
@ -46,7 +47,7 @@ class PluginMapping:
|
|||
"video_tags": VideoTagsPlugin,
|
||||
"nfo_tags": NfoTagsPlugin,
|
||||
"output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin,
|
||||
"static_nfo_tags": StaticNfoTagsPlugin,
|
||||
"regex": RegexPlugin,
|
||||
"subtitles": SubtitlesPlugin,
|
||||
"chapters": ChaptersPlugin,
|
||||
"split_by_chapters": SplitByChaptersPlugin,
|
||||
|
|
@ -73,6 +74,7 @@ class PluginMapping:
|
|||
SplitByChaptersPlugin,
|
||||
FilterExcludePlugin,
|
||||
FilterIncludePlugin,
|
||||
RegexPlugin,
|
||||
# add all others
|
||||
]
|
||||
|
||||
|
|
@ -84,17 +86,9 @@ class PluginMapping:
|
|||
MusicTagsPlugin,
|
||||
VideoTagsPlugin,
|
||||
NfoTagsPlugin,
|
||||
StaticNfoTagsPlugin,
|
||||
SquareThumbnailPlugin,
|
||||
EmbedThumbnailPlugin,
|
||||
]
|
||||
|
||||
_ORDER_POST_COMPLETION: List[Type[Plugin]] = [
|
||||
# Throttle protection should always be last
|
||||
# to not sleep over other logic
|
||||
ThrottleProtectionPlugin
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _order_by(
|
||||
cls, plugin_types: List[Type[Plugin]], operation: PluginOperation
|
||||
|
|
@ -105,8 +99,6 @@ class PluginMapping:
|
|||
ordering = cls._ORDER_MODIFY_ENTRY
|
||||
elif operation == PluginOperation.POST_PROCESS:
|
||||
ordering = cls._ORDER_POST_PROCESS
|
||||
elif operation == PluginOperation.POST_COMPLETION:
|
||||
ordering = cls._ORDER_POST_COMPLETION
|
||||
else:
|
||||
raise ValueError("PluginOperation does not support ordering")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ from enum import Enum
|
|||
|
||||
|
||||
class PluginOperation(Enum):
|
||||
ANY = -2
|
||||
DOWNLOADER = -1
|
||||
MODIFY_ENTRY_METADATA = 0
|
||||
MODIFY_ENTRY = 1
|
||||
POST_PROCESS = 2
|
||||
POST_COMPLETION = 3
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
from typing import Iterable, List, Optional, Set, Tuple, Type
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import OptionsValidator, OptionsValidatorT
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT
|
||||
|
||||
|
||||
class PresetPlugins:
|
||||
|
|
@ -40,34 +44,3 @@ class PresetPlugins:
|
|||
if plugin_type in plugin_option_types:
|
||||
return self.plugin_options[plugin_option_types.index(plugin_type)]
|
||||
return None
|
||||
|
||||
def get_added_and_modified_variables(
|
||||
self, additional_options: List[OptionsValidator]
|
||||
) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
|
||||
"""
|
||||
Iterates and returns the plugin options, added variables, modified variables
|
||||
"""
|
||||
for plugin_options in self.plugin_options + additional_options:
|
||||
added_variables: Set[str] = set()
|
||||
modified_variables: Set[str] = set()
|
||||
|
||||
for plugin_added_variables in plugin_options.added_variables(
|
||||
unresolved_variables=set(),
|
||||
).values():
|
||||
added_variables |= set(plugin_added_variables)
|
||||
|
||||
for plugin_modified_variables in plugin_options.modified_variables().values():
|
||||
modified_variables = plugin_modified_variables
|
||||
|
||||
yield plugin_options, added_variables, modified_variables
|
||||
|
||||
def get_all_variables(self, additional_options: List[OptionsValidator]) -> Set[str]:
|
||||
"""
|
||||
Returns set of all added and modified variables' names.
|
||||
"""
|
||||
all_variables: Set[str] = set()
|
||||
for _, added, modified in self.get_added_and_modified_variables(additional_options):
|
||||
all_variables.update(added)
|
||||
all_variables.update(modified)
|
||||
|
||||
return all_variables
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import copy
|
||||
from typing import Any, Dict, List, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
from mergedeep import mergedeep
|
||||
|
||||
|
|
@ -7,14 +9,18 @@ from ytdl_sub.config.config_validator import ConfigValidator
|
|||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
||||
from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
||||
from ytdl_sub.config.preset_options import OutputOptions, YTDLOptions
|
||||
from ytdl_sub.config.preset_options import OutputOptions
|
||||
from ytdl_sub.config.preset_options import YTDLOptions
|
||||
from ytdl_sub.config.validators.variable_validation import VariableValidation
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES, PUBLISHED_PRESET_NAMES
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES
|
||||
from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
from ytdl_sub.utils.yaml import dump_yaml
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.validators import StringListValidator, validation_exception
|
||||
from ytdl_sub.validators.validators import StringListValidator
|
||||
from ytdl_sub.validators.validators import validation_exception
|
||||
|
||||
PRESET_KEYS = {
|
||||
"preset",
|
||||
|
|
@ -49,12 +55,6 @@ class _PresetShell(StrictDictValidator):
|
|||
|
||||
|
||||
class Preset(_PresetShell):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def preset_partial_validate(cls, config: ConfigValidator, name: str, value: Any) -> None:
|
||||
"""
|
||||
|
|
@ -172,37 +172,6 @@ class Preset(_PresetShell):
|
|||
mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE)
|
||||
)
|
||||
|
||||
def _initialize_overrides_script(self, overrides: Overrides) -> Overrides:
|
||||
"""
|
||||
Do some gymnastics to initialize the Overrides script.
|
||||
"""
|
||||
unresolved_variables: Set[str] = set()
|
||||
|
||||
for (
|
||||
plugin_options,
|
||||
added_variables,
|
||||
modified_variables,
|
||||
) in self.plugins.get_added_and_modified_variables(
|
||||
additional_options=[self.downloader_options, self.output_options]
|
||||
):
|
||||
for added_variable in added_variables:
|
||||
if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
|
||||
# pylint: disable=protected-access
|
||||
raise plugin_options._validation_exception(
|
||||
f"Cannot use the variable name {added_variable} because it exists as a"
|
||||
" built-in ytdl-sub variable name."
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
|
||||
# Set unresolved as variables that are added but do not exist as
|
||||
# entry/override variables since they are created at run-time
|
||||
unresolved_variables |= added_variables | modified_variables
|
||||
|
||||
# Initialize overrides with unresolved variables + modified variables to throw an error.
|
||||
# For modified variables, this is to prevent a resolve(update=True) to setting any
|
||||
# dependencies until it has been explicitly added
|
||||
return overrides.initialize_script(unresolved_variables=unresolved_variables)
|
||||
|
||||
def __init__(self, config: ConfigValidator, name: str, value: Any):
|
||||
super().__init__(name=name, value=value)
|
||||
|
||||
|
|
@ -223,10 +192,13 @@ class Preset(_PresetShell):
|
|||
)
|
||||
|
||||
self.plugins: PresetPlugins = self._validate_and_get_plugins()
|
||||
self.overrides = self._initialize_overrides_script(
|
||||
overrides=self._validate_key(key="overrides", validator=Overrides, default={})
|
||||
)
|
||||
self.overrides.ensure_variable_names_not_a_plugin(plugin_names=PRESET_KEYS)
|
||||
self.overrides = self._validate_key(key="overrides", validator=Overrides, default={})
|
||||
|
||||
VariableValidation(
|
||||
downloader_options=self.downloader_options,
|
||||
output_options=self.output_options,
|
||||
plugins=self.plugins,
|
||||
).initialize_preset_overrides(overrides=self.overrides).ensure_proper_usage()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
@ -255,18 +227,11 @@ class Preset(_PresetShell):
|
|||
"""
|
||||
return cls(config=config, name=preset_name, value=preset_dict)
|
||||
|
||||
def yaml(self, subscription_only: bool) -> str:
|
||||
@property
|
||||
def yaml(self) -> str:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
subscription_only:
|
||||
Only include the subscription contents, not the surrounding boiler-plate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Preset in YAML format
|
||||
"""
|
||||
if subscription_only:
|
||||
return dump_yaml(self._value)
|
||||
|
||||
return dump_yaml({"presets": {self._name: self._value}})
|
||||
|
|
|
|||
|
|
@ -1,28 +1,19 @@
|
|||
from typing import Any, Dict, Optional, Set
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.validators.options import OptionsDictValidator
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES as v
|
||||
from ytdl_sub.utils.exceptions import SubscriptionPermissionError, ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.validators.file_path_validators import (
|
||||
OverridesStringFormatterFilePathValidator,
|
||||
StringFormatterFileNameValidator,
|
||||
)
|
||||
from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
|
||||
from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.string_datetime import StringDatetimeValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import (
|
||||
OverridesIntegerFormatterValidator,
|
||||
OverridesStringFormatterValidator,
|
||||
StandardizedDateValidator,
|
||||
StringFormatterValidator,
|
||||
UnstructuredOverridesDictFormatterValidator,
|
||||
)
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesIntegerFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.validators import BoolValidator
|
||||
from ytdl_sub.validators.validators import LiteralDictValidator
|
||||
|
||||
|
||||
class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
|
||||
class YTDLOptions(LiteralDictValidator):
|
||||
"""
|
||||
Allows you to add any ytdl argument to ytdl-sub's downloader.
|
||||
The argument names can differ slightly from the command-line argument names. See
|
||||
|
|
@ -55,34 +46,12 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
|
|||
where each key is a ytdl argument. Include in the example are some popular ytdl_options.
|
||||
"""
|
||||
|
||||
def to_native_dict(self, overrides: Overrides) -> Dict:
|
||||
"""
|
||||
Materializes the entire ytdl-options dict from OverrideStringFormatters into
|
||||
native python.
|
||||
"""
|
||||
out = {
|
||||
key: overrides.apply_formatter(val, expected_type=object)
|
||||
for key, val in self.dict.items()
|
||||
}
|
||||
if "cookiefile" in out:
|
||||
if not FileHandler.is_file_existent(out["cookiefile"]):
|
||||
raise ValidationException(
|
||||
f"Specified cookiefile {out['cookiefile']} but it does not exist as a file."
|
||||
)
|
||||
|
||||
if not FileHandler.is_file_readable(out["cookiefile"]):
|
||||
raise SubscriptionPermissionError(
|
||||
f"Cannot read cookiefile {out['cookiefile']} due to permissions issue."
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
# Disable for proper docstring formatting
|
||||
# pylint: disable=line-too-long
|
||||
|
||||
|
||||
class OutputOptions(OptionsDictValidator):
|
||||
class OutputOptions(StrictDictValidator):
|
||||
"""
|
||||
Defines where to output files and thumbnails after all post-processing has completed.
|
||||
|
||||
|
|
@ -104,8 +73,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
maintain_download_archive: True
|
||||
keep_files_before: now
|
||||
keep_files_after: 19000101
|
||||
keep_max_files: 1000
|
||||
keep_files_date_eval: "{upload_date_standardized}"
|
||||
"""
|
||||
|
||||
_required_keys = {"output_directory", "file_name"}
|
||||
|
|
@ -118,9 +85,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
"keep_files_before",
|
||||
"keep_files_after",
|
||||
"keep_max_files",
|
||||
"download_archive_standardized_date",
|
||||
"keep_files_date_eval",
|
||||
"preserve_mtime",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -178,15 +142,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
self._keep_max_files = self._validate_key_if_present(
|
||||
"keep_max_files", OverridesIntegerFormatterValidator
|
||||
)
|
||||
self._keep_files_date_eval = self._validate_key(
|
||||
"keep_files_date_eval",
|
||||
StandardizedDateValidator,
|
||||
default=f"{{{v.upload_date_standardized.variable_name}}}",
|
||||
)
|
||||
|
||||
self._preserve_mtime = self._validate_key_if_present(
|
||||
key="preserve_mtime", validator=BoolValidator, default=False
|
||||
)
|
||||
|
||||
if (
|
||||
self._keep_files_before or self._keep_files_after or self._keep_max_files
|
||||
|
|
@ -303,18 +258,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
"""
|
||||
return self._keep_files_after
|
||||
|
||||
@property
|
||||
def keep_files_date_eval(self) -> StandardizedDateValidator:
|
||||
"""
|
||||
:expected type: str
|
||||
:description:
|
||||
Uses this standardized date in the form of YYYY-MM-DD to record in the
|
||||
download archive for a given entry. Subsequently, uses this value to
|
||||
perform evaluation for keep_files_before/after and keep_max_files. Defaults
|
||||
to the entry's upload_date_standardized variable.
|
||||
"""
|
||||
return self._keep_files_date_eval
|
||||
|
||||
@property
|
||||
def keep_max_files(self) -> Optional[OverridesIntegerFormatterValidator]:
|
||||
"""
|
||||
|
|
@ -326,21 +269,3 @@ class OutputOptions(OptionsDictValidator):
|
|||
applied. Can be used in conjunction with ``keep_files_before`` and ``keep_files_after``.
|
||||
"""
|
||||
return self._keep_max_files
|
||||
|
||||
@property
|
||||
def preserve_mtime(self) -> bool:
|
||||
"""
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Preserve the video's original upload time as the file modification time.
|
||||
When True, sets the file's mtime to match the video's upload_date from
|
||||
yt-dlp metadata. Defaults to False.
|
||||
"""
|
||||
return self._preserve_mtime.value
|
||||
|
||||
def added_variables(self, unresolved_variables: Set[str]) -> Dict[PluginOperation, Set[str]]:
|
||||
return {
|
||||
# PluginOperation.MODIFY_ENTRY_METADATA: {
|
||||
# VARIABLES.ytdl_sub_entry_date_eval.variable_name
|
||||
# }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from abc import ABC
|
||||
from typing import Dict, Set, TypeVar
|
||||
from typing import Dict
|
||||
from typing import Set
|
||||
from typing import TypeVar
|
||||
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
|
|
@ -40,7 +42,9 @@ class OptionsValidator(Validator, ABC):
|
|||
|
||||
def added_variables(
|
||||
self,
|
||||
resolved_variables: Set[str],
|
||||
unresolved_variables: Set[str],
|
||||
plugin_op: PluginOperation,
|
||||
) -> Dict[PluginOperation, Set[str]]:
|
||||
"""
|
||||
If the plugin adds source variables, list them here.
|
||||
|
|
@ -59,9 +63,9 @@ class ToggleableOptionsDictValidator(OptionsDictValidator):
|
|||
_optional_keys = {"enable"}
|
||||
|
||||
def __init__(self, name, value):
|
||||
assert "enable" in self._optional_keys, (
|
||||
f"{self.__class__.__name__} does not have enable as an optional field"
|
||||
)
|
||||
assert (
|
||||
"enable" in self._optional_keys
|
||||
), f"{self.__class__.__name__} does not have enable as an optional field"
|
||||
super().__init__(name, value)
|
||||
|
||||
self._enable = self._validate_key(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
from typing import Dict, List, Optional, Set
|
||||
import copy
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
||||
|
|
@ -7,212 +13,202 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
|||
from ytdl_sub.config.preset_options import OutputOptions
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES, VARIABLES
|
||||
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.utils.name_validation import is_function
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.script.script import _is_function
|
||||
from ytdl_sub.utils.scriptable import BASE_SCRIPT
|
||||
from ytdl_sub.validators.string_formatter_validators import to_variable_dependency_format_string
|
||||
from ytdl_sub.validators.string_formatter_validators import validate_formatters
|
||||
|
||||
# Entry variables to mock during validation
|
||||
_DUMMY_ENTRY_VARIABLES: Dict[str, str] = {
|
||||
name: to_variable_dependency_format_string(
|
||||
# pylint: disable=protected-access
|
||||
script=BASE_SCRIPT,
|
||||
parsed_format_string=BASE_SCRIPT._variables[name],
|
||||
# pylint: enable=protected-access
|
||||
)
|
||||
for name in BASE_SCRIPT.variable_names
|
||||
}
|
||||
|
||||
class ResolutionLevel:
|
||||
ORIGINAL = 0
|
||||
FILL = 1
|
||||
RESOLVE = 2
|
||||
INTERNAL = 3
|
||||
|
||||
@classmethod
|
||||
def name_of(cls, resolution_level: int) -> str:
|
||||
"""
|
||||
Name of the resolution level.
|
||||
"""
|
||||
if resolution_level == cls.ORIGINAL:
|
||||
return "original"
|
||||
if resolution_level == cls.FILL:
|
||||
return "fill"
|
||||
if resolution_level == cls.RESOLVE:
|
||||
return "resolve"
|
||||
if resolution_level == cls.INTERNAL:
|
||||
return "internal"
|
||||
raise ValueError("Invalid resolution level")
|
||||
def _add_dummy_variables(variables: Iterable[str]) -> Dict[str, str]:
|
||||
dummy_variables: Dict[str, str] = {}
|
||||
for var in variables:
|
||||
dummy_variables[var] = ""
|
||||
dummy_variables[f"{var}_sanitized"] = ""
|
||||
|
||||
@classmethod
|
||||
def level_number(cls, resolution_arg: str) -> int:
|
||||
"""
|
||||
Numeric resolution level
|
||||
"""
|
||||
if resolution_arg in ("0", "original"):
|
||||
return 0
|
||||
if resolution_arg in ("1", "fill"):
|
||||
return 1
|
||||
if resolution_arg in ("2", "resolve"):
|
||||
return 2
|
||||
if resolution_arg in ("3", "internal"):
|
||||
return 3
|
||||
raise ValueError("Invalid resolution level")
|
||||
return dummy_variables
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> List[int]:
|
||||
"""
|
||||
All possible resolution levels.
|
||||
"""
|
||||
return [cls.ORIGINAL, cls.FILL, cls.RESOLVE, cls.INTERNAL]
|
||||
|
||||
def _add_dummy_overrides(overrides: Overrides) -> Dict[str, str]:
|
||||
# Have the dummy override variable contain all variable deps that it uses in the string
|
||||
dummy_overrides: Dict[str, str] = {}
|
||||
for override_name in _override_variables(overrides):
|
||||
if _is_function(override_name):
|
||||
continue
|
||||
|
||||
# pylint: disable=protected-access
|
||||
dummy_overrides[override_name] = to_variable_dependency_format_string(
|
||||
script=overrides.script, parsed_format_string=overrides.script._variables[override_name]
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
return dummy_overrides
|
||||
|
||||
|
||||
def _get_added_and_modified_variables(
|
||||
plugins: PresetPlugins, downloader_options: MultiUrlValidator
|
||||
) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
|
||||
"""
|
||||
Iterates and returns the plugin options, added variables, modified variables
|
||||
"""
|
||||
options: List[OptionsValidator] = plugins.plugin_options
|
||||
options.append(downloader_options)
|
||||
|
||||
for plugin_options in options:
|
||||
added_variables: Set[str] = set()
|
||||
modified_variables: Set[str] = set()
|
||||
|
||||
for plugin_added_variables in plugin_options.added_variables(
|
||||
resolved_variables=set(),
|
||||
unresolved_variables=set(),
|
||||
plugin_op=PluginOperation.ANY,
|
||||
).values():
|
||||
added_variables |= set(plugin_added_variables)
|
||||
|
||||
for plugin_modified_variables in plugin_options.modified_variables().values():
|
||||
modified_variables = plugin_modified_variables
|
||||
|
||||
yield plugin_options, added_variables, modified_variables
|
||||
|
||||
|
||||
def _override_variables(overrides: Overrides) -> Set[str]:
|
||||
return set(list(overrides.initial_variables().keys()))
|
||||
|
||||
|
||||
class VariableValidation:
|
||||
def _get_resolve_partial_filter(self) -> Set[str]:
|
||||
# Exclude sanitized variables from partial validation. This lessens the work
|
||||
# and prevents double-evaluation, which can lead to bad behavior like double-prints.
|
||||
return {
|
||||
name
|
||||
for name in self.script.variable_names
|
||||
if name not in self.unresolved_variables and not name.endswith("_sanitized")
|
||||
}
|
||||
|
||||
def _apply_resolution_level(self, mocks: Optional[Dict[str, str]]) -> None:
|
||||
if self._resolution_level == ResolutionLevel.FILL:
|
||||
self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True)
|
||||
# Only partial resolve definitions that are already resolved
|
||||
self.unresolved_variables |= {
|
||||
name
|
||||
for name in self.overrides.keys
|
||||
if not is_function(name) and not self.script.definition_of(name).maybe_resolvable
|
||||
}
|
||||
elif self._resolution_level == ResolutionLevel.RESOLVE:
|
||||
# Partial resolve everything, but not including internal variables
|
||||
self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True)
|
||||
elif self._resolution_level == ResolutionLevel.INTERNAL:
|
||||
# Partial resolve everything including internal variables
|
||||
pass
|
||||
else:
|
||||
raise ValueError("Invalid resolution level for validation")
|
||||
|
||||
if mocks is not None:
|
||||
for mock_name in mocks.keys():
|
||||
if mock_name in self.unresolved_variables:
|
||||
self.unresolved_variables.remove(mock_name)
|
||||
|
||||
self.script.add(
|
||||
variables=mocks,
|
||||
unresolvable=self.unresolved_variables,
|
||||
)
|
||||
|
||||
self.script = self.script.resolve_partial(
|
||||
unresolvable=self.unresolved_variables,
|
||||
output_filter=self._get_resolve_partial_filter(),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
overrides: Overrides,
|
||||
downloader_options: MultiUrlValidator,
|
||||
output_options: OutputOptions,
|
||||
plugins: PresetPlugins,
|
||||
resolution_level: int = ResolutionLevel.RESOLVE,
|
||||
mocks: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
self.overrides = overrides
|
||||
self.downloader_options = downloader_options
|
||||
self.output_options = output_options
|
||||
self.plugins = plugins
|
||||
|
||||
self.script: Script = self.overrides.script
|
||||
self.unresolved_variables = (
|
||||
self.plugins.get_all_variables(
|
||||
additional_options=[self.output_options, self.downloader_options]
|
||||
)
|
||||
| UNRESOLVED_VARIABLES
|
||||
)
|
||||
self.unresolved_runtime_variables = self.plugins.get_all_variables(
|
||||
additional_options=[self.output_options, self.downloader_options]
|
||||
)
|
||||
self._resolution_level = resolution_level
|
||||
self._apply_resolution_level(mocks=mocks)
|
||||
self.script: Optional[Script] = None
|
||||
self.resolved_variables: Set[str] = set()
|
||||
self.unresolved_variables: Set[str] = set()
|
||||
|
||||
def _add_runtime_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
|
||||
def initialize_preset_overrides(self, overrides: Overrides) -> "VariableValidation":
|
||||
"""
|
||||
Do some gymnastics to initialize the Overrides script.
|
||||
"""
|
||||
override_variables = set(list(overrides.initial_variables().keys()))
|
||||
|
||||
# Set resolved variables as all entry + override variables
|
||||
# at this point to generate every possible added/modified variable
|
||||
self.resolved_variables = set(_DUMMY_ENTRY_VARIABLES.keys()) | override_variables
|
||||
plugin_variables: Set[str] = set()
|
||||
|
||||
for (
|
||||
plugin_options,
|
||||
added_variables,
|
||||
modified_variables,
|
||||
) in _get_added_and_modified_variables(
|
||||
plugins=self.plugins,
|
||||
downloader_options=self.downloader_options,
|
||||
):
|
||||
|
||||
for added_variable in added_variables:
|
||||
if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
|
||||
# pylint: disable=protected-access
|
||||
raise plugin_options._validation_exception(
|
||||
f"Cannot use the variable name {added_variable} because it exists as a"
|
||||
" built-in ytdl-sub variable name."
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
|
||||
# Set unresolved as variables that are added but do not exist as
|
||||
# entry/override variables since they are created at run-time
|
||||
self.unresolved_variables |= added_variables | modified_variables
|
||||
plugin_variables |= added_variables | modified_variables
|
||||
|
||||
# Then update resolved variables to reflect that
|
||||
self.resolved_variables -= self.unresolved_variables
|
||||
|
||||
# Initialize overrides with unresolved variables + modified variables to throw an error.
|
||||
# For modified variables, this is to prevent a resolve(update=True) to setting any
|
||||
# dependencies until it has been explicitly added
|
||||
overrides = overrides.initialize_script(unresolved_variables=self.unresolved_variables)
|
||||
|
||||
# copy the script and mock entry variables
|
||||
self.script = copy.deepcopy(overrides.script)
|
||||
self.script.add(
|
||||
variables=_add_dummy_overrides(overrides=overrides)
|
||||
| _add_dummy_variables(variables=plugin_variables)
|
||||
| _DUMMY_ENTRY_VARIABLES
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def _update_script(self) -> None:
|
||||
_ = self.script.resolve(unresolvable=self.unresolved_variables, update=True)
|
||||
|
||||
def _add_subscription_override_variables(self) -> None:
|
||||
"""
|
||||
Add dummy subscription variables for script validation
|
||||
"""
|
||||
self.resolved_variables |= REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||
|
||||
def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
|
||||
"""
|
||||
Add dummy variables for script validation
|
||||
"""
|
||||
added_variables = options.added_variables(
|
||||
unresolved_variables=self.unresolved_runtime_variables,
|
||||
resolved_variables=self.resolved_variables,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
plugin_op=plugin_op,
|
||||
).get(plugin_op, set())
|
||||
modified_variables = options.modified_variables().get(plugin_op, set())
|
||||
|
||||
self.unresolved_runtime_variables -= added_variables | modified_variables
|
||||
resolved_variables = added_variables | modified_variables
|
||||
|
||||
def _output_override_variables(self) -> Dict:
|
||||
output = {}
|
||||
for name in self.overrides.keys:
|
||||
value = self.script.definition_of(name)
|
||||
if name in self.script.function_names:
|
||||
# Keep custom functions as-is
|
||||
output[name] = self.overrides.dict_with_format_strings[name]
|
||||
elif resolved := value.maybe_resolvable:
|
||||
output[name] = resolved.native
|
||||
else:
|
||||
output[name] = ScriptUtils.to_native_script(value)
|
||||
self.resolved_variables |= resolved_variables
|
||||
self.unresolved_variables -= resolved_variables
|
||||
|
||||
return output
|
||||
|
||||
def ensure_proper_usage(self, partial_resolve_formatters: bool = False) -> Dict:
|
||||
def ensure_proper_usage(self) -> None:
|
||||
"""
|
||||
Validate variables resolve as plugins are executed, and return
|
||||
a mock script which contains actualized added variables from the plugins
|
||||
"""
|
||||
resolved_subscription: Dict = {}
|
||||
|
||||
self._add_runtime_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
|
||||
|
||||
# Always add output options first
|
||||
self._add_runtime_variables(
|
||||
PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options
|
||||
)
|
||||
self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
|
||||
self._add_subscription_override_variables()
|
||||
|
||||
# Metadata variables to be added
|
||||
for plugin_options in PluginMapping.order_options_by(
|
||||
self.plugins.zipped(), PluginOperation.MODIFY_ENTRY_METADATA
|
||||
):
|
||||
self._add_runtime_variables(
|
||||
PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options
|
||||
)
|
||||
self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options)
|
||||
|
||||
for plugin_options in PluginMapping.order_options_by(
|
||||
self.plugins.zipped(), PluginOperation.MODIFY_ENTRY
|
||||
):
|
||||
self._add_runtime_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
|
||||
self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
|
||||
|
||||
# Validate that any formatter in the plugin options can resolve
|
||||
resolved_subscription |= validate_formatters(
|
||||
validate_formatters(
|
||||
script=self.script,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
unresolved_runtime_variables=self.unresolved_runtime_variables,
|
||||
validator=plugin_options,
|
||||
partial_resolve_formatters=partial_resolve_formatters,
|
||||
)
|
||||
|
||||
resolved_subscription |= validate_formatters(
|
||||
validate_formatters(
|
||||
script=self.script,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
unresolved_runtime_variables=self.unresolved_runtime_variables,
|
||||
validator=self.output_options,
|
||||
partial_resolve_formatters=partial_resolve_formatters,
|
||||
)
|
||||
|
||||
# TODO: make this a function
|
||||
raw_download_output = validate_formatters(
|
||||
script=self.script,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
unresolved_runtime_variables=self.unresolved_runtime_variables,
|
||||
validator=self.downloader_options.urls,
|
||||
partial_resolve_formatters=partial_resolve_formatters,
|
||||
)
|
||||
resolved_subscription["download"] = []
|
||||
for url_output in raw_download_output["download"]:
|
||||
if isinstance(url_output["url"], list):
|
||||
url_output["url"] = [url for url in url_output["url"] if bool(url)]
|
||||
|
||||
if url_output["url"]:
|
||||
resolved_subscription["download"].append(url_output)
|
||||
|
||||
resolved_subscription["overrides"] = self._output_override_variables()
|
||||
|
||||
return resolved_subscription
|
||||
assert not self.unresolved_variables
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.validators.options import OptionsDictValidator
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePlugin
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import (
|
||||
VARIABLE_SCRIPTS,
|
||||
VARIABLES,
|
||||
VariableDefinitions,
|
||||
)
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileHandler, get_file_extension
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import (
|
||||
DownloadMapping,
|
||||
EnhancedDownloadArchive,
|
||||
)
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.file_handler import get_file_extension
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadMapping
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import abc
|
||||
from abc import ABC
|
||||
from typing import Dict, Generic, Iterable, List, Optional, Type, final
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import final
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin import BasePlugin, Plugin
|
||||
from ytdl_sub.config.plugin.plugin import BasePlugin
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
|
|
|
|||
|
|
@ -1,29 +1,34 @@
|
|||
import contextlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from yt_dlp.utils import RejectedVideoReached
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePlugin, SourcePluginExtension
|
||||
from ytdl_sub.downloaders.url.validators import (
|
||||
MultiUrlValidator,
|
||||
UrlThumbnailListValidator,
|
||||
UrlValidator,
|
||||
)
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePlugin
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePluginExtension
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.downloaders.url.validators import UrlThumbnailListValidator
|
||||
from ytdl_sub.downloaders.url.validators import UrlValidator
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.downloaders.ytdlp import YTDLP
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.entry_parent import EntryParent
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
from ytdl_sub.utils.thumbnail import (
|
||||
ThumbnailTypes,
|
||||
download_and_convert_url_thumbnail,
|
||||
try_convert_download_thumbnail,
|
||||
)
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.utils.thumbnail import ThumbnailTypes
|
||||
from ytdl_sub.utils.thumbnail import download_and_convert_url_thumbnail
|
||||
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
|
@ -48,12 +53,12 @@ class UrlDownloaderBasePluginExtension(SourcePluginExtension[MultiUrlValidator])
|
|||
|
||||
if 0 <= input_url_idx < len(self.plugin_options.urls.list):
|
||||
validator = self.plugin_options.urls.list[input_url_idx]
|
||||
if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list):
|
||||
if self.overrides.apply_formatter(validator.url) == entry_input_url:
|
||||
return validator
|
||||
|
||||
# Match the first validator based on the URL, if one exists
|
||||
for validator in self.plugin_options.urls.list:
|
||||
if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list):
|
||||
if self.overrides.apply_formatter(validator.url) == entry_input_url:
|
||||
return validator
|
||||
|
||||
# Return the first validator if none exist
|
||||
|
|
@ -93,6 +98,7 @@ class UrlDownloaderThumbnailPlugin(UrlDownloaderBasePluginExtension):
|
|||
|
||||
# If latest entry, always update the thumbnail on each entry
|
||||
if thumbnail_id == ThumbnailTypes.LATEST_ENTRY:
|
||||
|
||||
# always save in dry-run even if it doesn't exist...
|
||||
if self.is_dry_run or entry.is_thumbnail_downloaded():
|
||||
self.save_file(
|
||||
|
|
@ -252,16 +258,6 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
.to_dict()
|
||||
)
|
||||
|
||||
def webpage_url(self, entry: Entry) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
The webpage_url to use for the actual download
|
||||
"""
|
||||
url_idx = entry.get(v.ytdl_sub_input_url_index, int)
|
||||
webpage_url_formatter = self.plugin_options.urls.list[url_idx].webpage_url
|
||||
return self.overrides.apply_formatter(webpage_url_formatter, entry=entry)
|
||||
|
||||
def metadata_ytdl_options(self, ytdl_option_overrides: Dict) -> Dict:
|
||||
"""
|
||||
Returns
|
||||
|
|
@ -363,7 +359,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
if (self.is_dry_run or not self.is_entry_thumbnails_enabled)
|
||||
else entry.is_thumbnail_downloaded_via_ytdlp
|
||||
),
|
||||
url=self.webpage_url(entry=entry),
|
||||
url=entry.webpage_url,
|
||||
)
|
||||
return Entry(
|
||||
download_entry_dict,
|
||||
|
|
@ -371,13 +367,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
)
|
||||
|
||||
def _iterate_child_entries(
|
||||
self, entries: List[Entry], validator: UrlValidator
|
||||
self, entries: List[Entry], download_reversed: bool
|
||||
) -> Iterator[Entry]:
|
||||
# Iterate a list of entries, and delete the entries after yielding
|
||||
entries_to_iter: List[Optional[Entry]] = entries
|
||||
|
||||
indices = list(range(len(entries_to_iter)))
|
||||
if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool):
|
||||
if download_reversed:
|
||||
indices = reversed(indices)
|
||||
|
||||
for idx in indices:
|
||||
|
|
@ -399,13 +395,17 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
entries_to_iter[idx] = None
|
||||
|
||||
def _iterate_parent_entry(
|
||||
self, parent: EntryParent, validator: UrlValidator
|
||||
self, parent: EntryParent, download_reversed: bool
|
||||
) -> Iterator[Entry]:
|
||||
yield from self._iterate_child_entries(entries=parent.entry_children(), validator=validator)
|
||||
yield from self._iterate_child_entries(
|
||||
entries=parent.entry_children(), download_reversed=download_reversed
|
||||
)
|
||||
|
||||
# Recursion the parent's parent entries
|
||||
for parent_child in reversed(parent.parent_children()):
|
||||
yield from self._iterate_parent_entry(parent=parent_child, validator=validator)
|
||||
yield from self._iterate_parent_entry(
|
||||
parent=parent_child, download_reversed=download_reversed
|
||||
)
|
||||
|
||||
def _download_url_metadata(
|
||||
self, url: str, include_sibling_metadata: bool, ytdl_options_overrides: Dict
|
||||
|
|
@ -439,7 +439,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
self,
|
||||
parents: List[EntryParent],
|
||||
orphans: List[Entry],
|
||||
validator: UrlValidator,
|
||||
download_reversed: bool,
|
||||
) -> Iterator[Entry]:
|
||||
"""
|
||||
Downloads the leaf entries from EntryParent trees
|
||||
|
|
@ -447,34 +447,38 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
# Delete info json files afterwards so other collection URLs do not use them
|
||||
with self._separate_download_archives(clear_info_json_files=True):
|
||||
for parent in parents:
|
||||
yield from self._iterate_parent_entry(parent=parent, validator=validator)
|
||||
yield from self._iterate_parent_entry(
|
||||
parent=parent, download_reversed=download_reversed
|
||||
)
|
||||
|
||||
yield from self._iterate_child_entries(entries=orphans, validator=validator)
|
||||
yield from self._iterate_child_entries(
|
||||
entries=orphans, download_reversed=download_reversed
|
||||
)
|
||||
|
||||
def _download_metadata(self, url: str, validator: UrlValidator) -> Iterable[Entry]:
|
||||
metadata_ytdl_options = self.metadata_ytdl_options(
|
||||
ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides)
|
||||
ytdl_option_overrides=validator.ytdl_options.dict
|
||||
)
|
||||
|
||||
include_sibling_metadata = self.overrides.apply_formatter(
|
||||
validator.include_sibling_metadata, expected_type=bool
|
||||
download_reversed = ScriptUtils.bool_formatter_output(
|
||||
self.overrides.apply_formatter(validator.download_reverse)
|
||||
)
|
||||
|
||||
parents, orphan_entries = self._download_url_metadata(
|
||||
url=url,
|
||||
include_sibling_metadata=include_sibling_metadata,
|
||||
include_sibling_metadata=validator.include_sibling_metadata,
|
||||
ytdl_options_overrides=metadata_ytdl_options,
|
||||
)
|
||||
|
||||
# TODO: Encapsulate this logic into its own class
|
||||
self._url_state = URLDownloadState(
|
||||
entries_total=sum(parent.num_children() for parent in parents) + len(orphan_entries),
|
||||
entries_total=sum(parent.num_children() for parent in parents) + len(orphan_entries)
|
||||
)
|
||||
|
||||
download_logger.info("Beginning downloads for %s", url)
|
||||
yield from self._iterate_entries(
|
||||
parents=parents,
|
||||
orphans=orphan_entries,
|
||||
validator=validator,
|
||||
download_reversed=download_reversed,
|
||||
)
|
||||
|
||||
def download_metadata(self) -> Iterable[Entry]:
|
||||
|
|
@ -482,25 +486,19 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
# download the bottom-most urls first since they are top-priority
|
||||
for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))):
|
||||
# URLs can be empty. If they are, then skip
|
||||
if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)):
|
||||
if not (url := self.overrides.apply_formatter(url_validator.url)):
|
||||
continue
|
||||
|
||||
for url in reversed(urls):
|
||||
assert isinstance(url, str)
|
||||
for entry in self._download_metadata(url=url, validator=url_validator):
|
||||
entry.initialize_script(self.overrides).add(
|
||||
{
|
||||
v.ytdl_sub_input_url: url,
|
||||
v.ytdl_sub_input_url_index: idx,
|
||||
v.ytdl_sub_input_url_count: len(self.collection.urls.list),
|
||||
}
|
||||
)
|
||||
|
||||
if not url:
|
||||
continue
|
||||
|
||||
for entry in self._download_metadata(url=url, validator=url_validator):
|
||||
entry.initialize_script(self.overrides).add(
|
||||
{
|
||||
v.ytdl_sub_input_url: url,
|
||||
v.ytdl_sub_input_url_index: idx,
|
||||
v.ytdl_sub_input_url_count: len(self.collection.urls.list),
|
||||
}
|
||||
)
|
||||
|
||||
yield entry
|
||||
yield entry
|
||||
|
||||
def download(self, entry: Entry) -> Optional[Entry]:
|
||||
"""
|
||||
|
|
@ -533,8 +531,8 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
download_logger.info("Entry rejected by download match-filter, skipping ..")
|
||||
return None
|
||||
|
||||
upload_date_idx = self._enhanced_download_archive.mapping.get_num_entries_with_date(
|
||||
standardized_date=entry.get(v.ytdl_sub_keep_files_date_eval, str)
|
||||
upload_date_idx = self._enhanced_download_archive.mapping.get_num_entries_with_upload_date(
|
||||
upload_date_standardized=entry.get(v.upload_date_standardized, str)
|
||||
)
|
||||
download_idx = self._enhanced_download_archive.num_entries
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.preset_options import YTDLOptions
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import (
|
||||
DictFormatterValidator,
|
||||
OverridesBooleanFormatterValidator,
|
||||
OverridesStringFormatterValidator,
|
||||
StringFormatterValidator,
|
||||
)
|
||||
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.validators import BoolValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
|
||||
|
||||
|
|
@ -20,7 +22,7 @@ class UrlThumbnailValidator(StrictDictValidator):
|
|||
def __init__(self, name, value):
|
||||
super().__init__(name, value)
|
||||
|
||||
self._thumb_name = self._validate_key(key="name", validator=StringFormatterValidator)
|
||||
self._name = self._validate_key(key="name", validator=StringFormatterValidator)
|
||||
self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator)
|
||||
|
||||
@property
|
||||
|
|
@ -28,7 +30,7 @@ class UrlThumbnailValidator(StrictDictValidator):
|
|||
"""
|
||||
File name for the thumbnail
|
||||
"""
|
||||
return self._thumb_name
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def uid(self) -> OverridesStringFormatterValidator:
|
||||
|
|
@ -42,19 +44,6 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]):
|
|||
_inner_list_type = UrlThumbnailValidator
|
||||
|
||||
|
||||
class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator):
|
||||
def post_process(self, resolved: Any) -> List[str]:
|
||||
if isinstance(resolved, str):
|
||||
return [resolved]
|
||||
if isinstance(resolved, list):
|
||||
for value in resolved:
|
||||
if not isinstance(value, str):
|
||||
raise self._validation_exception("Must be a string or an array of strings.")
|
||||
return resolved
|
||||
|
||||
raise self._validation_exception("Must be a string or an array of strings.")
|
||||
|
||||
|
||||
class UrlValidator(StrictDictValidator):
|
||||
_required_keys = {"url"}
|
||||
_optional_keys = {
|
||||
|
|
@ -64,7 +53,6 @@ class UrlValidator(StrictDictValidator):
|
|||
"download_reverse",
|
||||
"ytdl_options",
|
||||
"include_sibling_metadata",
|
||||
"webpage_url",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -80,7 +68,7 @@ class UrlValidator(StrictDictValidator):
|
|||
super().__init__(name, value)
|
||||
|
||||
# TODO: url validate using yt-dlp IE
|
||||
self._url = self._validate_key(key="url", validator=OverridesOneOrManyUrlValidator)
|
||||
self._url = self._validate_key(key="url", validator=OverridesStringFormatterValidator)
|
||||
self._variables = self._validate_key_if_present(
|
||||
key="variables", validator=DictFormatterValidator, default={}
|
||||
)
|
||||
|
|
@ -98,12 +86,7 @@ class UrlValidator(StrictDictValidator):
|
|||
key="ytdl_options", validator=YTDLOptions, default={}
|
||||
)
|
||||
self._include_sibling_metadata = self._validate_key(
|
||||
key="include_sibling_metadata",
|
||||
validator=OverridesBooleanFormatterValidator,
|
||||
default="False",
|
||||
)
|
||||
self._webpage_url = self._validate_key(
|
||||
key="webpage_url", validator=StringFormatterValidator, default="{webpage_url}"
|
||||
key="include_sibling_metadata", validator=BoolValidator, default=False
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -187,27 +170,14 @@ class UrlValidator(StrictDictValidator):
|
|||
return self._ytdl_options
|
||||
|
||||
@property
|
||||
def include_sibling_metadata(self) -> OverridesBooleanFormatterValidator:
|
||||
def include_sibling_metadata(self) -> bool:
|
||||
"""
|
||||
Optional. Whether to include sibling metadata as an entry variable, which comprises basic
|
||||
metadata from all other entries (including itself) that belong to the same playlist. For
|
||||
channels or large playlists, this becomes memory-intensive since you are storing
|
||||
``n^2`` metadata. Defaults to False.
|
||||
"""
|
||||
return self._include_sibling_metadata
|
||||
|
||||
@property
|
||||
def webpage_url(self) -> StringFormatterValidator:
|
||||
"""
|
||||
Optional. After ytdl-sub performs the metadata download, it will inspect each
|
||||
entry's .info.json file and perform the actual download from yt-dlp using
|
||||
`webpage_url <config_reference/scripting/entry_variables:webpage_url>`. This
|
||||
can be overwritten by supplying parameter with a modification to ``webpage_url`` in the
|
||||
form of an override variable.
|
||||
|
||||
Defaults to ``{webpage_url}``.
|
||||
"""
|
||||
return self._webpage_url
|
||||
return self._include_sibling_metadata.value
|
||||
|
||||
|
||||
class UrlStringOrDictValidator(UrlValidator):
|
||||
|
|
@ -350,21 +320,24 @@ class MultiUrlValidator(OptionsValidator):
|
|||
|
||||
def added_variables(
|
||||
self,
|
||||
resolved_variables: Set[str],
|
||||
unresolved_variables: Set[str],
|
||||
plugin_op: PluginOperation,
|
||||
) -> Dict[PluginOperation, Set[str]]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
List of variables added. The first collection url always contains all the variables.
|
||||
"""
|
||||
for url in self._urls.list:
|
||||
for variable_name, definition in url.variables.dict_with_format_strings.items():
|
||||
used_variables = set(var.name for var in parse(definition).variables)
|
||||
if unresolved := used_variables & unresolved_variables:
|
||||
raise self._validation_exception(
|
||||
f"variable {variable_name} cannot use the variables "
|
||||
f"{', '.join(sorted(list(unresolved)))} because it depends on other"
|
||||
" variables that are computed later in execution"
|
||||
)
|
||||
if plugin_op != PluginOperation.ANY:
|
||||
for url in self._urls.list:
|
||||
for variable_name, definition in url.variables.dict_with_format_strings.items():
|
||||
used_variables = set(var.name for var in parse(definition).variables)
|
||||
if unresolved := used_variables & unresolved_variables:
|
||||
raise self._validation_exception(
|
||||
f"variable {variable_name} cannot use the variables "
|
||||
f"{', '.join(sorted(list(unresolved)))} because it depends on other"
|
||||
" variables that are computed later in execution"
|
||||
)
|
||||
|
||||
return {PluginOperation.DOWNLOADER: set(self._urls.list[0].variables.keys)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import copy
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
import mergedeep
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import os
|
|||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import yt_dlp as ytdl
|
||||
from yt_dlp.utils import ExistingVideoReached, MaxDownloadsReached, RejectedVideoReached
|
||||
from yt_dlp.utils import ExistingVideoReached
|
||||
from yt_dlp.utils import MaxDownloadsReached
|
||||
from yt_dlp.utils import RejectedVideoReached
|
||||
|
||||
from ytdl_sub.thread.log_entries_downloaded_listener import LogEntriesDownloadedListener
|
||||
from ytdl_sub.utils.exceptions import FileNotDownloadedException
|
||||
|
|
@ -219,7 +224,7 @@ class YTDLP:
|
|||
except RejectedVideoReached:
|
||||
cls.logger.debug(
|
||||
"RejectedVideoReached, stopping additional downloads "
|
||||
"(Can be disable by setting `date_range.breaks` to False)."
|
||||
"(Can be disable by setting `date_range.breaking` to False)."
|
||||
)
|
||||
except ExistingVideoReached:
|
||||
cls.logger.debug(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue