diff --git a/.github/workflows/ci-windows.yaml b/.github/workflows/ci-windows.yaml index 42d45131..a9bab42f 100644 --- a/.github/workflows/ci-windows.yaml +++ b/.github/workflows/ci-windows.yaml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run unit tests with coverage run: | @@ -42,7 +42,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run integration tests with coverage run: | @@ -66,7 +66,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run prebuilt preset integration tests with coverage run: | @@ -89,7 +89,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run e2e tests with coverage run: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 84a53f94..2b830d83 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ on: - master jobs: test-lint: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: contents: read @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run linters run: | @@ -27,7 +27,7 @@ jobs: make check_lint test-unit: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: contents: read @@ -37,7 +37,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run unit tests with coverage run: | @@ -53,7 +53,7 @@ jobs: key: ${{github.sha}}-coverage-unit test-integration: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: contents: read @@ -63,7 +63,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run integration tests with coverage run: | @@ -79,7 +79,7 @@ jobs: key: ${{github.sha}}-coverage-integration test-integration-prebuilt-presets: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: contents: read @@ -89,7 +89,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run prebuilt preset integration tests with coverage run: | @@ -105,7 +105,7 @@ jobs: key: ${{github.sha}}-coverage-integration-prebuilt-presets test-e2e: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: contents: read @@ -115,7 +115,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Run e2e tests with coverage run: | @@ -125,7 +125,7 @@ jobs: coverage run -m pytest tests/e2e && coverage xml -o /opt/coverage/e2e/coverage.xml codecov-upload: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: [ test-unit, test-integration, diff --git a/.github/workflows/package-gui.yaml b/.github/workflows/package-gui.yaml index 0b9ac86c..3054d5fb 100644 --- a/.github/workflows/package-gui.yaml +++ b/.github/workflows/package-gui.yaml @@ -63,10 +63,10 @@ jobs: echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT" build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.10" ] + python-version: [ "3.12" ] permissions: contents: read @@ -92,7 +92,7 @@ jobs: # Build ARM64 container, only on master branch to save time testing package-arm64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: [ build ] @@ -141,7 +141,7 @@ jobs: # Build AMD64 container package-amd64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest 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-22.04 + runs-on: ubuntu-latest needs: [ version, build, diff --git a/.github/workflows/package-ubuntu.yaml b/.github/workflows/package-ubuntu.yaml index 40d4e0fc..798c9926 100644 --- a/.github/workflows/package-ubuntu.yaml +++ b/.github/workflows/package-ubuntu.yaml @@ -63,10 +63,10 @@ jobs: echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT" build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.10" ] + python-version: [ "3.12" ] permissions: contents: read @@ -92,7 +92,7 @@ jobs: # Build ARM64 container, only on master branch to save time testing package-arm64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: [ build ] @@ -141,7 +141,7 @@ jobs: # Build AMD64 container package-amd64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest 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-22.04 + runs-on: ubuntu-latest needs: [ version, build, diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 53d9da7a..7b60437f 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -63,10 +63,10 @@ jobs: echo 'init_contents=__pypi_version__ = "${{ env.PYPI_VERSION }}";__local_version__ = "${{ env.LOCAL_VERSION }}"' >> "$GITHUB_OUTPUT" build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.10" ] + python-version: [ "3.12" ] permissions: contents: read @@ -92,7 +92,7 @@ jobs: # Build ARM64 container, only on master branch to save time testing package-arm64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: [ build ] @@ -136,7 +136,7 @@ jobs: # Build AMD64 container package-amd64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest 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-22.04 + runs-on: ubuntu-latest needs: [ version, build, diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 219f32c3..8f4e4fe4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -61,9 +61,16 @@ jobs: strategy: matrix: arch: [ "aarch64", "x86_64" ] - runs-on: ubuntu-latest + include: + - arch: "aarch64" + runner: "ubuntu-24.04-arm" + container: "quay.io/pypa/manylinux_2_28_aarch64" + - arch: "x86_64" + runner: "ubuntu-latest" + container: "quay.io/pypa/manylinux_2_28_x86_64" + runs-on: ${{ matrix.runner }} container: - image: quay.io/pypa/manylinux_2_28_x86_64 + image: ${{ matrix.container }} steps: - uses: actions/checkout@v3 - name: Write version to init file @@ -76,17 +83,17 @@ jobs: dnf install -y epel-release tar wget make gcc openssl-devel bzip2-devel libffi-devel zlib-devel - name: Install Python run: | - wget https://www.python.org/ftp/python/3.10.10/Python-3.10.10.tar.xz - tar -xf Python-3.10.10.tar.xz - cd Python-3.10.10 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib" + wget https://www.python.org/ftp/python/3.12.9/Python-3.12.9.tar.xz + tar -xf Python-3.12.9.tar.xz + cd Python-3.12.9 && ./configure --with-ensurepip=install --prefix=/usr/local --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib" make -j 8 make altinstall - python3.10 --version - python3.10 -m ensurepip --upgrade + python3.12 --version + python3.12 -m ensurepip --upgrade - name: Build Package run: | - python3.10 -m pip install -e . - python3.10 -m pip install pyinstaller + python3.12 -m pip install -e . + python3.12 -m pip install pyinstaller # Build executable pyinstaller ytdl-sub.spec mkdir -p /opt/builds @@ -95,7 +102,7 @@ jobs: mv dist/ytdl-sub /opt/builds/ytdl-sub_${{ matrix.arch }} - name: Upload build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ytdl-sub_${{ matrix.arch }} path: /opt/builds/ytdl-sub_${{ matrix.arch }} @@ -106,13 +113,13 @@ jobs: name: build-windows needs: - version - runs-on: windows-2019 + runs-on: windows-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.12" - name: Write version to init file run: | echo '${{ needs.version.outputs.init_contents }}'> src/ytdl_sub/__init__.py @@ -124,7 +131,7 @@ jobs: .\dist\ytdl-sub.exe -h - name: Upload build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ytdl-sub_exe path: .\dist\ytdl-sub.exe @@ -145,19 +152,19 @@ jobs: echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py - name: Restore exe build - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ytdl-sub_exe path: /opt/builds - name: Restore aarch64 build - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ytdl-sub_aarch64 path: /opt/builds - name: Restore x86_64 build - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ytdl-sub_x86_64 path: /opt/builds @@ -193,13 +200,13 @@ jobs: name: pypi-publish needs: - version - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.12' - name: Write version to init file run: | echo '${{ needs.version.outputs.init_contents }}' > src/ytdl_sub/__init__.py diff --git a/.gitignore b/.gitignore index e18f90d3..5e9f1aad 100644 --- a/.gitignore +++ b/.gitignore @@ -146,8 +146,11 @@ docker/testing/volumes .local/ .ytdl-sub-working-directory +.ytdl-sub-lock ffmpeg.exe ffprobe.exe -tools/docgen/out \ No newline at end of file +tools/docgen/out + +prof/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 07eb81c9..8bdf9237 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,7 @@ build: sphinx: configuration: docs/source/conf.py + fail_on_warning: true python: install: @@ -14,4 +15,4 @@ python: - method: pip path: . extra_requirements: - - docs \ No newline at end of file + - docs diff --git a/Makefile b/Makefile index 39d8939d..0be80e53 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,20 @@ +# Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-eu -o pipefail -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules +export PS1?=$$ +# Prefix echoed recipe commands with the recipe line number for debugging: +export PS4?=:$$LINENO+ + # Get version related variables -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- export LOCAL_VERSION="$(DATE)+$(COMMIT_HASH)" @@ -13,10 +26,19 @@ else export PYPI_VERSION="$(DATE).post$(DATE_COMMIT_COUNT)" endif +# Finished with `$(shell)`, echo recipe commands going forward +.SHELLFLAGS+= -x + + +### Top-level targets: + +.PHONY: all +all: check_lint docs docker docker_ubuntu docker_gui + lint: - @-isort . - @-black . - @-pylint src/ + python3 -m isort . + python3 -m black . + python3 -m pylint src check_lint: isort . --check-only --diff \ && black . --check \ @@ -41,7 +63,8 @@ executable: clean mv dist/ytdl-sub dist/ytdl-sub${EXEC_SUFFIX} docs: REGENERATE_DOCS=1 pytest tests/unit/docgen/test_docgen.py - sphinx-build -M html docs/source/ docs/build/ + sphinx-build --write-all --fail-on-warning --nitpicky -b html \ + "./docs/source/" "./docs/build/" clean: rm -rf \ .pytest_cache/ \ diff --git a/README.md b/README.md index 01fd005c..3ba8ab5f 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ __preset__: cookiefile: "/config/cookie.txt" ################################################################### -# TV Show Presets. Can replace Plex with Plex/Jellyfin/Kodi +# TV Show Presets. Can replace Plex with Plex/Jellyfin/Emby/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. Can replace Plex with Plex/Jellyfin/Kodi +# Music Presets. YouTube Releases: = Jazz: # Sets genre tag to "Jazz" @@ -128,7 +128,7 @@ Bandcamp: "Emily Hopkins": "https://emilyharpist.bandcamp.com/" ################################################################### -# Music Video Presets +# Music Video Presets. Can replace Plex with Plex/Jellyfin/Kodi "Plex Music Videos": = Pop: # Sets genre tag to "Pop" "Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" diff --git a/docker/Dockerfile b/docker/Dockerfile index bcc213e6..adc3e443 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,16 +7,20 @@ FROM ghcr.io/linuxserver/baseimage-alpine:edge ENV OPENSSL_CONF="/etc/ssl" # For downloading thumbnails ENV SSL_CERT_DIR="/etc/ssl/certs/" +# Working directory used at both build and run times: +ENV DEFAULT_WORKSPACE="/config" COPY root/ / -RUN mkdir -p /config && \ +RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \ apk update --no-cache && \ apk upgrade --no-cache && \ apk add --no-cache --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \ vim \ g++ \ nano \ + unzip \ make \ + deno \ libffi-dev \ "python3>=3.10" \ py3-pip \ @@ -28,35 +32,44 @@ RUN mkdir -p /config && \ "aria2>=1.36.0" && \ ffmpeg -version && \ aria2c --version && \ + deno --version && \ # Install phantomjs if using x86_64, ensure it is properly installed if [[ $(uname -m) == "x86_64" ]]; then \ + echo "installing phantomjs" && \ apk add --no-cache gcompat && \ - cd /usr/share && \ - curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xj && \ - mv /usr/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs phantomjs && \ - rm -rf /usr/share/phantomjs-2.1.1-linux-x86_64 && \ + tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ + mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/share/phantomjs && \ + rm -rf phantomjs-2.1.1-linux-x86_64 && \ + rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ ln -s /usr/share/phantomjs /usr/bin/phantomjs && \ echo "Phantom JS version:" && \ phantomjs --version && \ cd -; \ fi && \ - echo "hi" && \ - # Install ytdl-sub, ensure it is installed properly - python3 -m pip install --break-system-packages --no-cache-dir ytdl_sub-*.whl && \ + # Configure pip globally + echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \ + # Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly + python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \ ytdl-sub -h && \ # Delete unneeded packages after install rm ytdl_sub-*.whl && \ apk del \ g++ \ make \ - libffi-dev \ - py3-pip \ - py3-setuptools + libffi-dev && \ + python3 -m pip --help + ############################################################################### # CONTAINER CONFIGS ENV EDITOR="nano" \ -HOME="/config" +HOME="${DEFAULT_WORKSPACE}" \ +DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \ +CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \ +CRON_WRAPPER_SCRIPT="${DEFAULT_WORKSPACE}/.cron_wrapper" \ +LOGS_TO_STDOUT="${DEFAULT_WORKSPACE}/.cron.log" \ +LSIO_FIRST_PARTY=false -VOLUME /config \ No newline at end of file +VOLUME "${DEFAULT_WORKSPACE}" +WORKDIR "${DEFAULT_WORKSPACE}" diff --git a/docker/Dockerfile.gui b/docker/Dockerfile.gui index 0b095a27..e78266ce 100644 --- a/docker/Dockerfile.gui +++ b/docker/Dockerfile.gui @@ -1,9 +1,11 @@ -FROM lscr.io/linuxserver/code-server:4.18.0-ls181 +FROM lscr.io/linuxserver/code-server:4.98.2 # For phantomjs ENV OPENSSL_CONF="/etc/ssl" # For downloading thumbnails ENV SSL_CERT_DIR="/etc/ssl/certs/" +# Working directory used at both build and run times: +ENV DEFAULT_WORKSPACE="/config/ytdl-sub-configs" ############################################################################### # YTDL-SUB INSTALL @@ -21,8 +23,8 @@ RUN mkdir -p /config && \ vim \ g++ \ nano \ + unzip \ make \ - python3.10-dev \ python3-pip \ fontconfig \ xz-utils \ @@ -52,16 +54,21 @@ RUN mkdir -p /config && \ ffmpeg -version && \ # Install phantomjs if using x86_64, ensure it is properly installed if [[ $(uname -m) == "x86_64" ]]; then \ - curl -L -o phantomjs.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ - tar -xvf phantomjs.tar.bz2 && \ + echo "installing phantomjs" && \ + tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/phantomjs && \ - rm -rf phantomjs-2.1.1-linux-x86_64/ && \ - rm phantomjs.tar.bz2 && \ + rm -rf phantomjs-2.1.1-linux-x86_64 && \ + rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ echo "Phantom JS version:" && \ phantomjs --version ; \ fi && \ - # Install ytdl-sub, ensure it is installed properly - pip install --no-cache-dir ytdl_sub-*.whl && \ + # Install Deno, required for YouTube downloads + curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y --no-modify-path && \ + deno --help && \ + # Configure pip globally + echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \ + # Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly + python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \ ytdl-sub -h && \ # Delete unneeded packages after install rm ytdl_sub-*.whl && \ @@ -69,21 +76,22 @@ RUN mkdir -p /config && \ g++ \ make \ xz-utils \ - bzip2 \ - python3.10-dev \ - python3-venv && \ + bzip2 && \ apt-get autoremove -y && \ apt-get purge -y --auto-remove && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* && \ + python3 -m pip --help ############################################################################### # CONTAINER CONFIGS - -ENV YTDL_SUB_TYPE="gui" \ -EDITOR="nano" \ +ENV EDITOR="nano" \ HOME="/config" \ -DOCKER_MODS=linuxserver/mods:universal-cron \ -DEFAULT_WORKSPACE=/config/ytdl-sub-configs +DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \ +CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \ +CRON_WRAPPER_SCRIPT="/config/.cron_wrapper" \ +LOGS_TO_STDOUT=/config/.cron.log \ +LSIO_FIRST_PARTY=false -VOLUME /config \ No newline at end of file +VOLUME /config +WORKDIR "${DEFAULT_WORKSPACE}" diff --git a/docker/Dockerfile.headless b/docker/Dockerfile.headless new file mode 120000 index 00000000..1d1fe94d --- /dev/null +++ b/docker/Dockerfile.headless @@ -0,0 +1 @@ +Dockerfile \ No newline at end of file diff --git a/docker/Dockerfile.ubuntu b/docker/Dockerfile.ubuntu index bef0f137..5ba76866 100644 --- a/docker/Dockerfile.ubuntu +++ b/docker/Dockerfile.ubuntu @@ -1,4 +1,4 @@ -FROM ghcr.io/linuxserver/baseimage-ubuntu:jammy +FROM ghcr.io/linuxserver/baseimage-ubuntu:noble # https://askubuntu.com/questions/972516/debian-frontend-environment-variable ARG DEBIAN_FRONTEND=noninteractive @@ -7,13 +7,15 @@ ARG DEBIAN_FRONTEND=noninteractive ENV OPENSSL_CONF="/etc/ssl" # For downloading thumbnails ENV SSL_CERT_DIR="/etc/ssl/certs/" +# Working directory used at both build and run times: +ENV DEFAULT_WORKSPACE="/config" ############################################################################### # YTDL-SUB INSTALL SHELL ["/bin/bash", "-c"] COPY root/ / -RUN mkdir -p /config && \ +RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \ apt-get -y update && \ apt-get -y upgrade && \ apt-get install --no-install-recommends -y \ @@ -24,8 +26,8 @@ RUN mkdir -p /config && \ vim \ g++ \ nano \ + unzip \ make \ - python3.10-dev \ python3-pip \ fontconfig \ xz-utils \ @@ -55,16 +57,21 @@ RUN mkdir -p /config && \ ffmpeg -version && \ # Install phantomjs if using x86_64, ensure it is properly installed if [[ $(uname -m) == "x86_64" ]]; then \ - curl -L -o phantomjs.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ - tar -xvf phantomjs.tar.bz2 && \ + echo "installing phantomjs" && \ + tar -xjvf /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/bin/phantomjs && \ - rm -rf phantomjs-2.1.1-linux-x86_64/ && \ - rm phantomjs.tar.bz2 && \ + rm -rf phantomjs-2.1.1-linux-x86_64 && \ + rm /defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ echo "Phantom JS version:" && \ phantomjs --version ; \ fi && \ - # Install ytdl-sub, ensure it is installed properly - pip install --no-cache-dir ytdl_sub-*.whl && \ + # Install Deno, required for YouTube downloads + curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y --no-modify-path && \ + deno --help && \ + # Configure pip globally + echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \ + # Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly + python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \ ytdl-sub -h && \ # Delete unneeded packages after install rm ytdl_sub-*.whl && \ @@ -72,17 +79,22 @@ RUN mkdir -p /config && \ g++ \ make \ xz-utils \ - bzip2 \ - python3.10-dev \ - python3-venv && \ + bzip2 && \ apt-get autoremove -y && \ apt-get purge -y --auto-remove && \ - rm -rf /var/lib/apt/lists/* + rm -rf /var/lib/apt/lists/* && \ + python3 -m pip --help ############################################################################### # CONTAINER CONFIGS ENV EDITOR="nano" \ -HOME="/config" +HOME="${DEFAULT_WORKSPACE}" \ +DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \ +CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \ +CRON_WRAPPER_SCRIPT="${DEFAULT_WORKSPACE}/.cron_wrapper" \ +LOGS_TO_STDOUT="${DEFAULT_WORKSPACE}/.cron.log" \ +LSIO_FIRST_PARTY=false -VOLUME /config \ No newline at end of file +VOLUME "${DEFAULT_WORKSPACE}" +WORKDIR "${DEFAULT_WORKSPACE}" diff --git a/docker/root/custom-cont-init.d/defaults b/docker/root/custom-cont-init.d/defaults new file mode 100755 index 00000000..f3bfd62e --- /dev/null +++ b/docker/root/custom-cont-init.d/defaults @@ -0,0 +1,84 @@ +#!/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 "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 diff --git a/docker/root/custom-cont-init.d/defaults-gui b/docker/root/custom-cont-init.d/defaults-gui deleted file mode 100644 index 01affd47..00000000 --- a/docker/root/custom-cont-init.d/defaults-gui +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/with-contenv bash - -# Exit if not gui -if [ "$YTDL_SUB_TYPE" != "gui" ] ; then - exit 0 -fi - -echo "Checking ytdl-sub-gui defaults..." - -# copy config -[[ ! -e /config/ytdl-sub-configs/config.yaml ]] && \ - mkdir -p /config/ytdl-sub-configs && \ - cp /defaults/config.yaml /config/ytdl-sub-configs/config.yaml -[[ ! -e /config/ytdl-sub-configs/subscriptions.yaml ]] && \ - mkdir -p /config/ytdl-sub-configs && \ - cp /defaults/subscriptions.yaml /config/ytdl-sub-configs/subscriptions.yaml -[[ ! -d /config/ytdl-sub-configs/examples ]] && \ - mkdir -p /config/ytdl-sub-configs/examples && \ - cp /defaults/examples/* /config/ytdl-sub-configs/examples - -# permissions -chown -R ${PUID:-abc}:${PGID:-abc} \ - /config diff --git a/docker/root/custom-cont-init.d/defaults-headless b/docker/root/custom-cont-init.d/defaults-headless deleted file mode 100644 index 8d5d632b..00000000 --- a/docker/root/custom-cont-init.d/defaults-headless +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/with-contenv bash - -# Exit if gui -if [ "$YTDL_SUB_TYPE" == "gui" ] ; then - exit 0 -fi - -echo "Checking ytdl-sub 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 \ No newline at end of file diff --git a/docker/root/defaults/config.yaml b/docker/root/defaults/config.yaml index 3e4471e1..2b191b0c 100644 --- a/docker/root/defaults/config.yaml +++ b/docker/root/defaults/config.yaml @@ -16,4 +16,7 @@ # 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" diff --git a/docker/root/defaults/cron b/docker/root/defaults/cron new file mode 100644 index 00000000..6fdbea71 --- /dev/null +++ b/docker/root/defaults/cron @@ -0,0 +1,4 @@ +echo "Beginning cron job..." + +# Place your ytdl-sub command(s) here. +# This script is executed in the same relative path as this file. \ No newline at end of file diff --git a/docker/root/defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 b/docker/root/defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 new file mode 100644 index 00000000..0281fb85 Binary files /dev/null and b/docker/root/defaults/phantomjs-2.1.1-linux-x86_64.tar.bz2 differ diff --git a/docker/root/defaults/subscriptions.yaml b/docker/root/defaults/subscriptions.yaml index a7e129dc..98535e27 100644 --- a/docker/root/defaults/subscriptions.yaml +++ b/docker/root/defaults/subscriptions.yaml @@ -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/cookie.txt" + # ytdl_options: + # cookiefile: "/config/cookie.txt" ################################################################### # Subscriptions nested under this will use the @@ -35,52 +35,54 @@ 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 Full Albums: - = Lofi: - "Game Chops": "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i" +# YouTube Releases: +# = Jazz: # Sets genre tag to "Jazz" +# "Thelonious Monk": "https://www.youtube.com/@theloniousmonk3870/releases" -SoundCloud Discography: - = Chill Hop: - "UKNOWY": "https://soundcloud.com/uknowymunich" - = Synthwave: - "Lazerdiscs Records": "https://soundcloud.com/lazerdiscsrecords" - "Earmake": "https://soundcloud.com/earmake" +# YouTube Full Albums: +# = Lofi: +# "Game Chops": "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i" -Bandcamp: - = Lofi: - "Emily Hopkins": "https://emilyharpist.bandcamp.com/" +# SoundCloud Discography: +# = Chill Hop: +# "UKNOWY": "https://soundcloud.com/uknowymunich" +# = Synthwave: +# "Lazerdiscs Records": "https://soundcloud.com/lazerdiscsrecords" +# "Earmake": "https://soundcloud.com/earmake" + +# Bandcamp: +# = Lofi: +# "Emily Hopkins": "https://emilyharpist.bandcamp.com/" ################################################################### # Can choose between: # - Plex Music Videos: # - Jellyfin Music Videos: # - Kodi Music Videos: -"Plex Music Videos": - = Pop: # Sets genre tag to "Pop" - "Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" - "Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E" \ No newline at end of file + +# "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" diff --git a/docker/root/etc/s6-overlay/s6-rc.d/init-adduser/branding b/docker/root/etc/s6-overlay/s6-rc.d/init-adduser/branding new file mode 100644 index 00000000..0aaa4244 --- /dev/null +++ b/docker/root/etc/s6-overlay/s6-rc.d/init-adduser/branding @@ -0,0 +1,15 @@ +―――――――――――――――――――――――――――――――――――― +██╗ ██╗████████╗██████╗ ██╗ +╚██╗ ██╔╝╚══██╔══╝██╔══██╗██║ + ╚████╔╝ ██║ ██║ ██║██║ + ╚██╔╝ ██║ ██║ ██║██║ + ██║ ██║ ██████╔╝███████╗ + ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ + + ███████╗██╗ ██╗██████╗ + ██╔════╝██║ ██║██╔══██╗ + ███████╗██║ ██║██████╔╝ + ╚════██║██║ ██║██╔══██╗ + ███████║╚██████╔╝██████╔╝ + ╚══════╝ ╚═════╝ ╚═════╝ +―――――――――――――――――――――――――――――――――――― \ No newline at end of file diff --git a/docker/testing/Makefile b/docker/testing/Makefile new file mode 100644 index 00000000..89e6773c --- /dev/null +++ b/docker/testing/Makefile @@ -0,0 +1,46 @@ +# Local building and testing of the Docker image variants. + +# Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-eu -o pipefail -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules +export PS1?=$$ +# Prefix echoed recipe commands with the recipe line number for debugging: +export PS4?=:$$LINENO+ + +VARIANTS=headless gui ubuntu +ROOT_PREREQS:=$(shell find ../root -type f) + +# Finished with `$(shell)`, echo recipe commands going forward +.SHELLFLAGS+= -x + + +### Top-level targets: + +.PHONY: all +all: build + +.PHONY: build +build: $(VARIANTS:%=./build/ytdl-sub-%.log) + +.PHONY: run +run: build $(VARIANTS:%=./volumes/ytdl-sub-%/) + docker compose up + + +### Real targets: + +# Re-build the local images when changes require it. +./build/ytdl-sub-%.log: ../Dockerfile.% $(ROOT_PREREQS) + mkdir -pv "$(dir $(@))" + docker compose build "$(@:build/ytdl-sub-%.log=ytdl-sub-%)" |& + tee -a "$(@)" + +# Ensure volumes are owned by the developer's normal user: +./volumes/ytdl-sub-%/: + mkdir -pv "$(@)" diff --git a/docker/testing/docker-compose.yml b/docker/testing/docker-compose.yml index 1a8797b9..2f85ff53 100644 --- a/docker/testing/docker-compose.yml +++ b/docker/testing/docker-compose.yml @@ -1,13 +1,50 @@ services: ytdl-sub-gui: - image: ytdl-sub-gui:local - container_name: ytdl-sub-gui + build: + context: "../" + dockerfile: "./Dockerfile.gui" + image: "ytdl-sub-gui:local" + container_name: "ytdl-sub-gui" environment: - - PUID=1000 - - PGID=1000 - - TZ=America/Los_Angeles + PUID: "1000" + PGID: "1000" + TZ: "America/Los_Angeles" + CRON_SCHEDULE: '*/1 * * * *' + CRON_RUN_ON_START: "true" + UPDATE_YT_DLP_ON_START: "stable" volumes: - - ./volumes/ytdl-sub-gui:/config + - "./volumes/ytdl-sub-gui/:/config/" ports: - - 8443:8443 - restart: unless-stopped + - "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" diff --git a/docs/source/config_reference/config_yaml.rst b/docs/source/config_reference/config_yaml.rst index a8f12180..e849c263 100644 --- a/docs/source/config_reference/config_yaml.rst +++ b/docs/source/config_reference/config_yaml.rst @@ -1,3 +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 ================== @@ -38,25 +45,38 @@ 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! +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ffmpeg_path ----------- -Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux, and -``ffmpeg.exe`` for Windows (in the same directory as ytdl-sub). +Path to ffmpeg executable. (default ``/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, and -``ffprobe.exe`` for Windows (in the same directory as ytdl-sub). +Path to ffprobe executable. (default ``/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. +leaf_name +--------- +Returns +------- +"name" from the first.element.of.the.name + 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``. +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. (default ``/tmp``) persist_logs ------------ @@ -64,17 +84,27 @@ TODO(jessebannon) fill out ``keep_successful_logs`` -Optional. Whether to store logs when downloading is successful. Defaults to True. +If the ``persist_logs:`` key is in the configuration, then ``ytdl-sub`` *always* +writes log files for the subscription both for successful downloads and when it +encounters an error while downloading. When this key is ``False``, only write +log files for errors. (default ``True``) + +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name ``logs_directory`` -Required. The directory to store the logs in. +Write log files to this directory with names like +``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``. (required) umask ----- -Umask (octal format) to apply to every created file. Defaults to "022". +Umask in octal format to apply to every created file. (default ``022``) working_directory ----------------- The directory to temporarily store downloaded files before moving them into their final -directory. Defaults to .ytdl-sub-working-directory +directory. (default ``./.ytdl-sub-working-directory``) diff --git a/docs/source/config_reference/index.rst b/docs/source/config_reference/index.rst index 0fe2f88c..80085d92 100644 --- a/docs/source/config_reference/index.rst +++ b/docs/source/config_reference/index.rst @@ -2,11 +2,50 @@ Reference ========= -This section contains direct references to the code of ``ytdl-sub`` and information on how it functions. +This section contains direct references to the code of ``ytdl-sub`` and information on +how it functions. + + +Terminology +----------- + +Must-know terminology: + +- ``subscription``: URL(s) that you want to download with specific metadata + requirements. +- ``preset``: A media profile comprised of YAML configuration that can specify anything + from metadata layout, media quality, or any feature of ytdl-sub, to apply to + subscriptions. A preset can inherit other presets. +- ``prebuilt preset``: Presets that are included in ytdl-sub. These do most of the work + defining plugins, overrides, etc in order to make downloads ready for player + consumption. +- ``override``: Verb describing the act of overriding something in a preset. For + example, the TV Show presets practically expect you to *override* the URL variable to + tell ytdl-sub where to download from. +- ``override variables``: User-defined variables that are intended to *override* + something. +- ``subscription file``: The file to specify all of your subscriptions and some override + variables. + +Intermediate terminology: + +- ``plugin``: Modular logic to apply to a subscription. To use a plugin, it must be + defined in a preset. +- ``config file``: An optional file where you can define custom presets and other + advanced configuration. +- ``yt-dlp``: The underlying application that handles downloading for ytdl-sub. + +Advanced terminology: + +- ``entry variables``: Variables that derive from a downloaded yt-dlp entry (media). +- ``static variables``: Variables that do not have a dependency to entry variables. +- ``scripting``: Syntax that allows the use of entry variables, static variables, and + functions in override variables. + .. toctree:: config_yaml subscription_yaml plugins scripting/index - prebuilt_presets/index \ No newline at end of file + prebuilt_presets/index diff --git a/docs/source/config_reference/plugins.rst b/docs/source/config_reference/plugins.rst index b48fe78c..1625119e 100644 --- a/docs/source/config_reference/plugins.rst +++ b/docs/source/config_reference/plugins.rst @@ -1,3 +1,10 @@ +.. + 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 ======= @@ -29,6 +36,12 @@ 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. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``quality`` :expected type: Float @@ -95,6 +108,12 @@ 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. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``remove_chapters_regex`` :expected type: Optional[List[RegexString] @@ -130,9 +149,11 @@ 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. +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. :Usage: @@ -141,18 +162,20 @@ granularity possible. date_range: before: "now" after: "today-2weeks" + breaks: True + type: "upload_date" ``after`` :expected type: Optional[OverridesFormatter] :description: - Only download videos after this datetime. + Only download videos after or on this datetime, inclusive. ``before`` :expected type: Optional[OverridesFormatter] :description: - Only download videos before this datetime. + Only download videos only before this datetime, not inclusive. ``breaks`` @@ -169,6 +192,19 @@ granularity possible. this field can be set using an override variable to easily toggle whether this plugin is enabled or not via Boolean. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + +``type`` + +:expected type: Optional[OverridesFormatter] +:description: + Which type of date to use. Must be either ``upload_date`` or ``release_date``. + Defaults to ``upload_date``. + ---------------------------------------------------------------------------------------------------- download @@ -300,6 +336,12 @@ 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``. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ---------------------------------------------------------------------------------------------------- filter_exclude @@ -437,12 +479,18 @@ with a ``.nfo`` extension. You can add any values into the NFO. ``kodi_safe`` -:expected type: Optional[Boolean] +: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 '□'. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``nfo_name`` :expected type: EntryFormatter @@ -530,12 +578,18 @@ Usage: ``kodi_safe`` -:expected type: Optional[Boolean] +: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 '□'. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``nfo_name`` :expected type: EntryFormatter @@ -612,6 +666,8 @@ 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`` @@ -657,6 +713,15 @@ 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`` :expected type: Optional[OverridesFormatter] @@ -666,6 +731,12 @@ 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``. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``maintain_download_archive`` :expected type: Optional[Boolean] @@ -695,6 +766,14 @@ Defines where to output files and thumbnails after all post-processing has compl :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`` :expected type: Optional[EntryFormatter] @@ -732,6 +811,16 @@ 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. +``dict_with_parsed_format_strings`` + +Returns dict with the parsed format strings. + +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ---------------------------------------------------------------------------------------------------- split_by_chapters @@ -754,6 +843,12 @@ used with no modifications. split_by_chapters: when_no_chapters: "pass" +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``when_no_chapters`` :expected type: String @@ -769,6 +864,97 @@ used with no modifications. ---------------------------------------------------------------------------------------------------- +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 '□'. + +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + +``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 + + + + + +``tags`` + +:expected type: NfoTags +:description: + Tags within the nfo_root tag. In the usage above, it would look like + + .. code-block:: xml + + + + My custom season name! + + +---------------------------------------------------------------------------------------------------- + subtitles --------- Defines how to download and store subtitles. Using this plugin creates two new variables: @@ -816,6 +1002,19 @@ 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. + +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``subtitles_name`` :expected type: Optional[EntryFormatter] @@ -839,6 +1038,9 @@ 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 @@ -846,6 +1048,9 @@ scripted. 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 @@ -865,6 +1070,12 @@ scripted. this field can be set using an override variable to easily toggle whether this plugin is enabled or not via Boolean. +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name + ``max_downloads_per_subscription`` :expected type: Optional[Range] @@ -878,6 +1089,15 @@ scripted. 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`` :expected type: Optional[Range] @@ -940,3 +1160,13 @@ for more details. where each key is a ytdl argument. Include in the example are some popular ytdl_options. + +``dict_with_parsed_format_strings`` + +Returns dict with the parsed format strings. + +``leaf_name`` + +Returns +------- +"name" from the first.element.of.the.name diff --git a/docs/source/config_reference/prebuilt_presets/common.rst b/docs/source/config_reference/prebuilt_presets/common.rst index ab1fd545..a1b12a57 100644 --- a/docs/source/config_reference/prebuilt_presets/common.rst +++ b/docs/source/config_reference/prebuilt_presets/common.rst @@ -4,17 +4,30 @@ Common .. highlight:: yaml -Filtering -------------- -.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/filtering.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 \ No newline at end of file +.. literalinclude:: + /../../src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml diff --git a/docs/source/config_reference/prebuilt_presets/index.rst b/docs/source/config_reference/prebuilt_presets/index.rst index d50cc51d..b25cb57a 100644 --- a/docs/source/config_reference/prebuilt_presets/index.rst +++ b/docs/source/config_reference/prebuilt_presets/index.rst @@ -2,11 +2,10 @@ 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`. - - +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`. .. toctree:: common tv_show - music \ No newline at end of file + music diff --git a/docs/source/config_reference/prebuilt_presets/music.rst b/docs/source/config_reference/prebuilt_presets/music.rst index dde4c28e..b9641198 100644 --- a/docs/source/config_reference/prebuilt_presets/music.rst +++ b/docs/source/config_reference/prebuilt_presets/music.rst @@ -6,4 +6,5 @@ All audio music based presets inherit from ``_music_base``. .. highlight:: yaml -.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/music/singles.yaml \ No newline at end of file +.. literalinclude:: + /../../src/ytdl_sub/prebuilt_presets/music/singles.yaml diff --git a/docs/source/config_reference/prebuilt_presets/tv_show.rst b/docs/source/config_reference/prebuilt_presets/tv_show.rst index 4cb993a3..f7287049 100644 --- a/docs/source/config_reference/prebuilt_presets/tv_show.rst +++ b/docs/source/config_reference/prebuilt_presets/tv_show.rst @@ -6,4 +6,5 @@ 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 diff --git a/docs/source/config_reference/scripting/entry_variables.rst b/docs/source/config_reference/scripting/entry_variables.rst index 84247234..9ad413e1 100644 --- a/docs/source/config_reference/scripting/entry_variables.rst +++ b/docs/source/config_reference/scripting/entry_variables.rst @@ -1,3 +1,10 @@ +.. + 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 =============== @@ -83,6 +90,13 @@ 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`` @@ -165,6 +179,13 @@ 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 @@ -659,3 +680,9 @@ 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``. diff --git a/docs/source/config_reference/scripting/index.rst b/docs/source/config_reference/scripting/index.rst index 4366914b..67387f9e 100644 --- a/docs/source/config_reference/scripting/index.rst +++ b/docs/source/config_reference/scripting/index.rst @@ -2,8 +2,9 @@ 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 @@ -13,6 +14,7 @@ contain reference documentation for each built-in variable and scripting functio scripting_functions scripting_types + How it Works ------------ @@ -44,22 +46,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 @@ -74,8 +76,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: @@ -97,9 +99,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 @@ -116,16 +118,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 `_ +``snake_cased_with_no_spaces``. We can use the `replace +`_ *scripting function* to create and use a snake-cased title. .. code-block:: yaml @@ -143,42 +145,48 @@ 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 `_. -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 +`_. +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 `_ -variable -`entry_metadata `_. -Any field can be accessed by using the -`map_get `_ +The entirety of an entry's ``info.json`` file resides in the `Map +`_ +variable `entry_metadata +`_. + +Any field can be accessed by using the `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 @@ -187,9 +195,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: diff --git a/docs/source/config_reference/scripting/scripting_functions.rst b/docs/source/config_reference/scripting/scripting_functions.rst index cc42c52c..e2a200ef 100644 --- a/docs/source/config_reference/scripting/scripting_functions.rst +++ b/docs/source/config_reference/scripting/scripting_functions.rst @@ -1,3 +1,10 @@ +.. + 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 =================== @@ -514,6 +521,13 @@ 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`` @@ -523,6 +537,37 @@ 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 --------------- @@ -634,7 +679,7 @@ capitalize concat ~~~~~~ -:spec: ``concat(values: String, ...) -> String`` +:spec: ``concat(values: AnyArgument, ...) -> String`` :description: Concatenate multiple Strings into a single String. @@ -660,6 +705,24 @@ contains_any :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`` @@ -710,6 +773,23 @@ 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`` diff --git a/docs/source/config_reference/scripting/scripting_types.rst b/docs/source/config_reference/scripting/scripting_types.rst index 9275d7a1..eb88620b 100644 --- a/docs/source/config_reference/scripting/scripting_types.rst +++ b/docs/source/config_reference/scripting/scripting_types.rst @@ -1,7 +1,8 @@ - +=============== Scripting Types =============== + Types ----- @@ -16,8 +17,9 @@ 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: @@ -65,8 +67,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:: @@ -88,7 +90,8 @@ 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 @@ -185,8 +188,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:: @@ -227,8 +230,8 @@ Arrays are defined using brackets (``[ ]``), and are accessed using zero-based i 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:: @@ -267,6 +270,7 @@ Maps are 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. @@ -297,7 +301,9 @@ 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:: @@ -306,13 +312,15 @@ AnyArgument means any of the above Types are valid as input or output to a scrip 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 `_ + +Optional means a particular scripting function argument can be either provided or not +included. For example, the function `map_get +`_ has an optional default value. Both of these usages are valid: .. tab-set:: @@ -331,8 +339,9 @@ 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 @@ -341,20 +350,23 @@ on the input. In this example, %array_apply( [ 1, 2, 3, 4], %string ) } -We apply ``%string`` as a lambda function to -`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 +`_, +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 `_, +This example has one input-argument being passed into the lambda. For other lambda-based +functions like `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, @@ -365,11 +377,12 @@ In this example, %array_reduce( [ 1, 2, 3, 4], %add ) } -We call -`array_reduce `_ -on the input array, using -`add `_ -as the LambdaReduce function. This will reduce the Array to a single value by internally calling +We call `array_reduce +`_ +on the input array, using `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) @@ -380,9 +393,10 @@ And evaluate to ``10``. ReturnableArguments ~~~~~~~~~~~~~~~~~~~ -Returnable arguments are used in conditional functions like -`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 +`_, +which implies the argument passed into the function is the function's output. For +example, .. code-block:: yaml @@ -391,4 +405,4 @@ which implies the argument passed into the function is the function's output. Fo %if( True, "Return this if True", "Return this if False" ) } -is going to return ``"Return this if True"`` since the condition parameter is ``True``. \ No newline at end of file +is going to return ``"Return this if True"`` since the condition parameter is ``True``. diff --git a/docs/source/config_reference/scripting/static_variables.rst b/docs/source/config_reference/scripting/static_variables.rst index 9a90e761..ff69c0ab 100644 --- a/docs/source/config_reference/scripting/static_variables.rst +++ b/docs/source/config_reference/scripting/static_variables.rst @@ -1,3 +1,10 @@ +.. + 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 ================ @@ -24,16 +31,21 @@ otherwise. subscription_indent_i ~~~~~~~~~~~~~~~~~~~~~ -For subscriptions in the form of +For subscriptions where the ancestor keys contain the ``= ...`` prefix, the +variables ``subscription_indent_1``, ``subscription_indent_2``, and so on get +set to each subsequent value. For example, given the following subscriptions +file snippet: .. code-block:: yaml - Preset | = Indent Value 1: - = Indent Value 2: + Preset 1 | = Indent Value 1 | Preset 2: + Preset 3 | = Indent Value 2 | Preset 4: "Subscription Name": "https://..." -``subscription_indent_1`` and ``subscription_indent_2`` get set to -``Indent Value 1`` and ``Indent Value 2``. +The ``{subscription_indent_1}`` variable will be ``Indent Value 1`` and +``{subscription_indent_2}`` will be ``Indent Value 2``. The most common use of +these variables is to :doc:`set the genre and rating for subscriptions from the +YAML keys <../prebuilt_presets/tv_show>`. subscription_map ~~~~~~~~~~~~~~~~ diff --git a/docs/source/config_reference/subscription_yaml.rst b/docs/source/config_reference/subscription_yaml.rst index 0505d7bb..298456f8 100644 --- a/docs/source/config_reference/subscription_yaml.rst +++ b/docs/source/config_reference/subscription_yaml.rst @@ -2,19 +2,22 @@ Subscription File ================== -A subscription file is designed to both define and organize many things -to download in condensed YAML. +A subscription file is designed to both define and organize many things to download in +condensed YAML. .. hint:: Read the :ref:`getting started guide ` 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 ` -can be defined that gets applied to each subscription within the file. + +Many examples show ``__preset__`` at the top. This is known as the *subscription file +preset*. It is where a single :ref:`preset ` 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. @@ -22,15 +25,17 @@ 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/cookie.txt" - Layout ------ + A subscription file is comprised of YAML keys and values. Keys can be either - a preset @@ -52,32 +57,33 @@ All three types of keys are used for the following: - ``= News`` - an override value for genre - ``Breaking News``, ``BBC News`` - The subscription names -The bottom-most keys, or leaf 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. +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. +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. + +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. + 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: + +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 @@ -85,16 +91,16 @@ example: "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 ``~``. + +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 ` -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``. +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: @@ -109,13 +115,14 @@ We can change it as follows: .. 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 `. + 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 `. + Map Mode -------- -Map mode is for highly advanced presets that benefit -from a more complex subscription definition. TODO: Show music video -example here. + +Map mode is for highly advanced presets that benefit from a more complex subscription +definition. TODO: Show music video example here. diff --git a/docs/source/debugging.rst b/docs/source/debugging.rst new file mode 100644 index 00000000..d70ac51f --- /dev/null +++ b/docs/source/debugging.rst @@ -0,0 +1,78 @@ +Debugging +========= + +Run with ``--log-level debug`` to show all log messages, often too much information for +normal operation but useful when investigating a specific problem. + +:ref:`ytdl-sub builds on yt-dlp `, 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 ` outside its control. Finally, because :ref:`ytdl-sub is +a lower-level tool `, 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 diff --git a/docs/source/deprecation_notices.rst b/docs/source/deprecation_notices.rst index 4c5b6a37..a75240c2 100644 --- a/docs/source/deprecation_notices.rst +++ b/docs/source/deprecation_notices.rst @@ -1,14 +1,25 @@ 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: +: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 @@ -40,13 +51,16 @@ has been created to replicate the plugin's behavior. See the following converted } 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/subscription_yaml:Subscription File`. ``__preset__`` will still +be supported for the time being. July 2023 --------- @@ -54,8 +68,9 @@ 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 @@ -79,8 +94,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 diff --git a/docs/source/faq/index.rst b/docs/source/faq/index.rst index f8ae6653..bb5f4fe0 100644 --- a/docs/source/faq/index.rst +++ b/docs/source/faq/index.rst @@ -2,45 +2,36 @@ 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 ` - -* `Join our Discord `_ - -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 `_ 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 ` 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 `_ to download your YouTube cookie, then add it to your :ref:`ytdl options ` section of your config: +See `yt-dl's recommended way +`_ to +download your YouTube cookie, then add it to your :ref:`ytdl options +` section of your config: .. code-block:: yaml @@ -50,7 +41,8 @@ See `yt-dl's recommended way ` shows how to set up ``ytdl-sub`` to run automatically on various platforms. +:doc:`This page ` shows how to set up +``ytdl-sub`` to run automatically on various platforms. ...download large channels? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -62,25 +54,207 @@ See the prebuilt preset :doc:`chunk_initial_download See the prebuilt preset :doc:`Filter Keywords `. +...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 +` 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 `, +``./.ytdl-sub-...-download-archive.json``, at the top of the subscription/series/show +:ref:`output directory ` 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 `. To re-download, those options must be disabled or modified. Disable + :ref:`the 'break_on_existing' option `, set + :ref:`the 'date_range:' plugin `, and :ref:`limit + the subscriptions ` to + download only the files that you've renamed in the steps above. + + Set the appropriate dates, :ref:`including a sufficient margin + `, 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 `_ 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 ` with information on what issue you're resolving and it will be + reviewed as soon as possible. + + There is a bug where... ----------------------- +...ytdl-sub is not downloading +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +...ytdl-sub is downloading at 360p or other lower quality +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +...ytdl-sub downloads 2-4 videos and then fails +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These are often just limits imposed by the external services that are not bugs. There +may be little that can be done about them, but see :ref:`the '_throttle_protection' +preset ` 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 @@ -96,7 +270,9 @@ Most likely the video has a non-English language set to its 'native' language. Y 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 @@ -104,7 +280,15 @@ Most likely the video has a non-English language set to its 'native' language. Y - **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. + :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. ` diff --git a/docs/source/guides/development/index.rst b/docs/source/guides/development/index.rst index d0750e65..9183331a 100644 --- a/docs/source/guides/development/index.rst +++ b/docs/source/guides/development/index.rst @@ -1,8 +1,10 @@ Development and Contributing ============================ + Requirements ------------ + - python >= 3.10 - ffmpeg/ffprobe 4.4.5 (test checksums rely on this version) - make @@ -20,15 +22,18 @@ Local Install 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: + +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 -------------------- @@ -36,18 +41,21 @@ 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 + :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. +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. + +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... @@ -55,26 +63,36 @@ If integration tests are failing, ensure... - 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. + +PyCharm is our preferred IDE. The codebase is simple enough to where it's not required, +but is highly recommended. TODO: screenshots of configuration -Debugging ---------- - -Debug Logs -^^^^^^^^^^^^^^^ -Run with ``--log-level debug`` to show all debug logs when running ytdl-sub. - Reproducing a Failing Subscription -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Subscriptions will dump their entire *compiled* yaml at the beginning of exeuction -when using ``--log-level debug``. 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. \ No newline at end of file +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. diff --git a/docs/source/guides/getting_started/automating_downloads.rst b/docs/source/guides/getting_started/automating_downloads.rst index 9608a7b1..80e86902 100644 --- a/docs/source/guides/getting_started/automating_downloads.rst +++ b/docs/source/guides/getting_started/automating_downloads.rst @@ -1,133 +1,46 @@ Automating Downloads ==================== -One of the key capabilities of ``ytdl-sub`` automating new downloads. -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 ` :ref:`Guide for Linux ` :ref:`Guide for Windows ` -.. _cron tab manpage: https://man7.org/linux/man-pages/man5/crontab.5.html#EXAMPLE_CRON_FILE +.. _cron scheduling syntax: https://crontab.guru/#0_*/6_*_*_* + .. _docker-unraid-setup: Docker and Unraid ----------------- -.. tab-set:: - - .. tab-item:: GUI Image - - The script that will execute automatically is located at ``/config/ytdl-sub-configs/run_cron``. +Cron is preconfigured in every ytdl-sub docker container. Enable by adding the following +ENV variables to your docker setup. - Access your container at http://localhost:8443/, then in the GUI terminal run these commands: +.. code-block:: yaml - .. code-block:: shell + services: + ytdl-sub: + environment: + - CRON_SCHEDULE="0 */6 * * *" + - CRON_RUN_ON_START=false - 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: +- ``CRON_SCHEDULE`` follows the standard `cron scheduling syntax`_. The above value will + run the script once every 6 hours. +- ``CRON_RUN_ON_START`` toggles whether to run your cron script on container start in + addition to the cron schedule. - .. code-block:: shell +The cron script will reside in the main directory with the file name ``cron``. Cron +logs should show when viewing the Docker logs. - /config/ytdl-sub-configs/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/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. - - .. attention:: - The Docker container needs to be restarted for changes to take effect. Run `crontab -e` after to verify settings are correct. - - .. 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 - - :/config - - :/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/run_cron - echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> /config/run_cron - echo "echo 'Cron started, running ytdl-sub...'" >> /config/run_cron - echo "cd /config" >> /config/run_cron - echo "ytdl-sub --config=config.yaml sub subscriptions.yaml" >> /config/run_cron - chmod +x /config/run_cron - chown abc:abc /config/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. - - .. attention:: - The Docker container needs to be restarted for changes to take effect. Run `crontab -e` after to verify settings are correct. .. _linux-setup: Linux ----- +Must configure crontab manually, like so: .. code-block:: shell @@ -135,14 +48,22 @@ Linux 0 */6 * * * /config/run_cron - - .. _windows-setup: Windows ------- -To be tested (please contact code owner or join the discord server if you can test this out for us) + +To be tested (please contact code owner or join the discord server if you can test this +out for us) .. code-block:: powershell ytdl-sub.exe --config \path\to\config\config.yaml sub \path\to\config\subscriptions.yaml + + +Next Steps +---------- + +Once you have a significant quantity of subscriptions or have use cases not served using +:doc:`YAML keys and the special characters <./subscriptions>`, it's time to start +:doc:`defining your own custom presets <./first_config>`. diff --git a/docs/source/guides/getting_started/downloading.rst b/docs/source/guides/getting_started/downloading.rst new file mode 100644 index 00000000..98d91297 --- /dev/null +++ b/docs/source/guides/getting_started/downloading.rst @@ -0,0 +1,69 @@ +Downloading +=========== + +Once you've :doc:`defined your subscriptions <./subscriptions>`, it's time to test your +configuration and try your first download. As a web scraping tool, :ref:`it's important +to minimize the requests sent to external services +` 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 ` 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 `_ and `reviewed `_ 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>`. diff --git a/docs/source/guides/getting_started/first_config.rst b/docs/source/guides/getting_started/first_config.rst index 2d1a2278..707bbf0a 100644 --- a/docs/source/guides/getting_started/first_config.rst +++ b/docs/source/guides/getting_started/first_config.rst @@ -33,7 +33,7 @@ how this works, and show-case how :linenos: configuration: - working_directory: '/mnt/ssd/.ytdl-sub-downloads' + working_directory: ".ytdl-sub-working-directory" presets: TV Show: @@ -55,28 +55,36 @@ how this works, and show-case how max: 36 overrides: - tv_show_directory: "/ytdl_sub_tv_shows" + tv_show_directory: "/tv_shows" TV Show Only Recent: preset: - "TV Show" - "Only Recent" + Configuration Section --------------------- -The :ref:`configuration ` section sets options for ytdl-sub execution. +The :ref:`configuration ` 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: '/mnt/ssd/.ytdl-sub-downloads' + 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``. +Underneath ``presets``, we define two custom presets with the names ``TV Show`` and ``TV +Show Only Recent``. .. code-block:: yaml @@ -88,6 +96,7 @@ Underneath ``presets``, we define two custom presets with the names ``TV Show`` The indentation example above shows how to define multiple presets. + Custom Preset Definition ------------------------ @@ -107,12 +116,15 @@ Before we break down the above ``TV Show`` preset, lets first outline a preset l Presets can contain three important things: -1. ``preset`` section, which can inherit :ref:`prebuilt presets ` - or other presets defined in your config. +1. ``preset`` section, which can inherit :ref:`prebuilt presets + ` or other presets + defined in your config. 2. :ref:`Plugin definitions ` -3. :ref:`overrides `, which can override inherited preset variables +3. :ref:`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. +Presets do not have to define all of these, as we'll see in the ``TV Show Only Recent`` +preset. Inheriting Presets ~~~~~~~~~~~~~~~~~~ @@ -125,14 +137,16 @@ Inheriting Presets - "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. +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 ` 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. +It is highly advisable to use :ref:`prebuilt presets +` 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 ~~~~~~~~~~~~~~~~ @@ -153,17 +167,18 @@ Defining Plugins min: 10 max: 36 -Our ``TV Show`` sets two plugins, :ref:`throttle_protection ` and -:ref:`embed_thumbnail `. Each plugin's documentation shows the respective -fields that they support. +Our ``TV Show`` sets two plugins, :ref:`throttle_protection +` and :ref:`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 +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 ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -174,27 +189,31 @@ Setting Override Variables overrides: tv_show_directory: "/ytdl_sub_tv_shows" -All override variables reside underneath the :ref:`overrides ` section. +All override variables reside underneath the :ref:`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 +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 +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. +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 ` -to see all available variables that are overridable. +See the :ref:`prebuilt preset reference +` to see all +available variables that are overridable. Using Custom Presets in Subscriptions -------------------------------------- -Subscription files can use custom presets just like any other prebuilt preset. -Below shows a complete subscription file using the above two custom presets. +Subscription files can use custom presets just like any other prebuilt preset. Below +shows a complete subscription file using the above two custom presets. .. code-block:: yaml @@ -212,11 +231,12 @@ Below shows a complete subscription file using the above two custom presets. 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``. +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. \ No newline at end of file +If you run ytdl-sub in the same directory, and the config file is named ``config.yaml``, +it will use it by default. diff --git a/docs/source/guides/getting_started/first_download.rst b/docs/source/guides/getting_started/first_download.rst deleted file mode 100644 index 188c5c32..00000000 --- a/docs/source/guides/getting_started/first_download.rst +++ /dev/null @@ -1,50 +0,0 @@ -Initial Download -================ - -Once you have a ``subscriptions.yaml`` file created, you can perform your first -download. Access ``ytdl-sub``, navigate to the directory containing your ``subscriptions.yaml`` -file. - -Dry Run -------- -Performing a dry run is important when applying any change to your subscriptions to -ensure output looks as expected. Dry runs will pull metadata to *simulate* a download -without actually downloading the media file. - -.. code-block:: shell - - ytdl-sub --dry-run sub subscriptions.yaml - -Faster Iteration Cycle ----------------------- -Testing subscriptions can take quite some time to perform a full download. -This can be speed up by applying an override via command-line to set max number -of downloads. - -.. code-block:: shell - - ytdl-sub --dry-run sub subscriptions.yaml -o '--ytdl_options.max_downloads 3' - -Having many subscriptions could still make this dry run take a while. A subset of -subscriptions can be dry ran using a match. - -.. code-block:: shell - :caption: Only run subscriptions that have PBS in their names - - ytdl-sub --dry-run sub subscriptions.yaml -o '--ytdl_options.max_downloads 3' --match PBS - -Downloading ------------ -Once the subscriptions file is validated, a download can be performed by omitting the dry run argument. - -.. code-block:: shell - - ytdl-sub sub subscriptions.yaml - -Multiple subscription file names can be provided to perform a download on all of them. A single file -named ``subscriptions.yaml`` does not require a file name specification since it will -look for that file name by default, making the following command valid. - -.. code-block:: shell - - ytdl-sub sub diff --git a/docs/source/guides/getting_started/first_sub.rst b/docs/source/guides/getting_started/first_sub.rst deleted file mode 100644 index 7dd5c22f..00000000 --- a/docs/source/guides/getting_started/first_sub.rst +++ /dev/null @@ -1,141 +0,0 @@ -Initial Subscription -==================== - -Your first subscription file 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" - -Let's break this down: - -.. code-block:: yaml - :lineno-start: 1 - - __preset__: - overrides: - tv_show_directory: "/tv_shows" - music_directory: "/music" - - -The first :ref:`__preset__ ` section is where we -can set modifications that apply to every subscription in this file. - -This snippet specifically adds two :ref:`override ` variables, -which are used by the presets below. - -.. note:: - It is tempting to put any override underneath ``overrides``. Keep in mind that this section - is solely for variable defining. Other :ref:`plugins ` need to be - set at the same indentation level as ``overrides``, not within it. - - -------------------------------------- - -.. 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 ` 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. - -This preset expects the variable ``tv_show_directory`` to be set, which we do above. - -------------------------------------- - -.. 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 genre. This value will get written to the respective metadata tags for both TV show -and music presets. - -Behind the scenes, this sets the override variable ``subscription_indent_1``. Further documentation -can be found here for -:ref:`subscription syntax ` and -:ref:`subscription variables `. - -------------------------------------- - -.. 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``. - -To see how presets ingest subscription definitions, refer to the -:ref:`preset references `, -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 `, 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. \ No newline at end of file diff --git a/docs/source/guides/getting_started/index.rst b/docs/source/guides/getting_started/index.rst index c86ed757..5c04e47f 100644 --- a/docs/source/guides/getting_started/index.rst +++ b/docs/source/guides/getting_started/index.rst @@ -1,57 +1,157 @@ Getting Started =============== + Prerequisite Knowledge ---------------------- -In order to use ``ytdl-sub`` in any of the forms listed in these docs, you will need some basic knowledge. +Using ``ytdl-sub`` requires some technical knowledge. You must be able to: -Be sure that you: - ☑ Can navigate directories in a command line interface (or CLI) +- do `basic CLI shell navigation`_ +- read and write `YAML text files`_ - ☑ Have a basic understanding of YAML syntax +If you plan on using a :ref:`Docker headless image variant +` of ``ytdl-sub``, you can: -If you plan on using the headless image of ``ytdl-sub``, you: - ☑ Can use ``nano`` or ``vim`` to edit OR +- 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 - ☑ Can mount the config directory somewhere you can open it using gui text editors +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 `. 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 `_ to learn how ``ytdl-sub`` works. -Additional useful (but not required) knowledge: - ☑ Understanding how :yt-dlp:`\ ` 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/ -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. +Architecture +------------ -Intermediate terminology: +For most users, ``ytdl-sub`` works as follows: -- ``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. +Subscriptions use presets +~~~~~~~~~~~~~~~~~~~~~~~~~ -Advanced terminology: +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. -- ``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. +Presets configure plugins +~~~~~~~~~~~~~~~~~~~~~~~~~ + +:doc:`A preset <../../prebuilt_presets/index>` is effectively a set of plugin +configurations. Specifically, a preset consists of: + +- base presets that it inherits from and extends +- plugin configurations + +When a preset has multiple base presets and more than one of those base presets +configures the same keys for a plugin, the later/lower base preset overrides the plugin +key configurations of earlier/higher base presets. Similarly, when the preset configures +the same keys for a plugin that one of its base plugins configures, the preset +configuration overrides the base presets. + +Plugins do the work +~~~~~~~~~~~~~~~~~~~ + +``ytdl-sub`` applies the plugins that the presets configure when it downloads a +subscription. :doc:`The plugins <../../config_reference/plugins>` control how to run +``yt-dlp``, which media in the subscription to download, how to collect and format +metadata for those media, how to place the resulting files into your media library, and +more. + +Presets and subscriptions accept overrides +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Presets accept override keys and values and the preset uses those overrides to modify +their plugin configurations. Similarly, individual subscriptions can supply overrides of +their presets for just that subscription. + +Subscriptions are grouped by indentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most subscriptions have more in common with each other than not. Thus, defining the +presets and overrides for each subscription would result in mostly repetition and would +multiply the burden of management for the user. The more subscriptions the more work. + +To avoid this redundant work, and so that the subscription configurations describe the +intent of the user, subscriptions are nested/indented under parent/ancestor keys that +define their shared configuration. To support this, ``ytdl-sub`` uses special handling +of the ancestor YAML keys above each subscription. A subscription is the most +nested/indented/descendant key that specifies the URLs for that subscription. The +ancestor keys above that subscription describe the shared presets of that subscription +and all the other descendant subscriptions under them. + +Genres are also more often shared between subscriptions than not. To accommodate that +reality, the ancestor keys of subscriptions may also use :ref:`the special '= ...' +prefix to pass specific overrides +` supported by the +preset. By convention in the pre-built media type presets, the first ``= ...`` value +specifies the genre for all descendant subscriptions. + +Finally, ancestor keys may use :ref:`the '... | ...' special character +` to combine multiple presets and/or +genres for the descendant subscriptions beneath. + +The configuration file extends pre-defined presets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Users define additional presets in :doc:`their configuration file <./first_config>` that +they then use in most of their subscriptions. Most user-defined presets extend the +:doc:`../../prebuilt_presets/index` provided by ``ytdl-sub``. + +Minimize the work to only what's necessary +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Throttling and bans are a core problem for any web scraping tool, perhaps even more so +for ``yt-dlp``, and no good actor *wants* to be an onerous burden on a +service. Similarly, many web scraping use cases involve very large sets of data that are +too big to process as a whole for performance. It's important to narrow the amount of +data considered and minimize requests. + +To these ends, most presets tell ``yt-dlp`` not to consider files before the most +recently downloaded file using :ref:`the 'break_on_existing' option +`. Similarly, and particularly for huge channels +or playlists, most users should use either :ref:`an 'Only Recent' preset +` and/or :ref:`the 'Chunk Downloads' preset +` to restrict the number of downloads +considered. + + +Caveats +~~~~~~~ + +Some of these descriptions are not technically complete. For example, a subscription may +use no preset at all and will just run ``yt-dlp`` without any customization or post +processing. The subscriptions file has special support for :ref:`overriding the presets +of all subscriptions in the file `. The +configuration file supports :ref:`a few special options +` 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>`. -Ready to Start? ---------------- -Now that you've completed your install of ``ytdl-sub``, it's time to get started. -It is recommended to go through the below sections in order to fully grasp ytdl-sub. .. toctree:: - :maxdepth: 2 + :hidden: - first_sub - first_download + subscriptions + downloading automating_downloads first_config + quick_start diff --git a/docs/source/guides/getting_started/quick_start.rst b/docs/source/guides/getting_started/quick_start.rst new file mode 100644 index 00000000..7647f774 --- /dev/null +++ b/docs/source/guides/getting_started/quick_start.rst @@ -0,0 +1,54 @@ +Quick Start +=========== + +:ref:`Again `, 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 `. 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 `. + +#. Update the paths for your media library: + + Edit :ref:`the subscriptions file `. 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 ` and :ref:`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 `. + +#. Automate downloads: + + :ref:`Set up ytdl-sub to run periodically + `. diff --git a/docs/source/guides/getting_started/subscriptions.rst b/docs/source/guides/getting_started/subscriptions.rst new file mode 100644 index 00000000..8a3d1cd0 --- /dev/null +++ b/docs/source/guides/getting_started/subscriptions.rst @@ -0,0 +1,167 @@ +Subscriptions +============= + +Once you understand :ref:`how ytdl-sub works +`, 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 +`: + +.. 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 +`. + + +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 +` 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 +` 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 +`. 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 '= +...' `. 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 ` +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 ` 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>`. diff --git a/docs/source/guides/install/agnostic.rst b/docs/source/guides/install/agnostic.rst index 658dd5ed..49466f80 100644 --- a/docs/source/guides/install/agnostic.rst +++ b/docs/source/guides/install/agnostic.rst @@ -2,13 +2,15 @@ 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 `_. -Both ffmpeg and Python 3.10 or greater are required. + +You can install our `PyPI package `_. Both ffmpeg +and Python 3.10 or greater are required. .. code-block:: bash @@ -17,10 +19,13 @@ Both ffmpeg 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 @@ -32,12 +37,13 @@ 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 \ No newline at end of file + make docker diff --git a/docs/source/guides/install/docker.rst b/docs/source/guides/install/docker.rst index bf9b04c4..aa7bfd5f 100644 --- a/docs/source/guides/install/docker.rst +++ b/docs/source/guides/install/docker.rst @@ -2,142 +2,98 @@ Docker ====== -For automating ``subscriptions.yaml`` downloads to pull new media, see :ref:`this page ` 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 or variants to choose from. For a more user-friendly +experience editing the `configuration`_, we recommend the `GUI image`_ +variant. :ref:`Docker Compose ` is +the recommended way of managing a ``ytdl-sub`` docker container. See :ref:`Automating +Downloads ` for how to +automate running ``ytdl-sub`` in a container running either variant. -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 ` is the recommended way of setting up a ``ytdl-sub`` docker container. GUI Image --------- -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. +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 -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 uses LSIO's :lsio-gh:`docker-baseimage-alpine image <\ >` for its base image. Execute the following command to access and interact with ``ytdl-sub``: +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 +` 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. -.. code-block:: bash +For example:: + + $ docker compose run --rm --user="${PUID}:${PGID}" --entrypoint="ytdl-sub" ytdl-sub sub - docker exec -u abc -it ytdl-sub /bin/bash Install with Docker Compose --------------------------- -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: - - :/config - - :/tv_shows # optional - - :/movies # optional - - :/music_videos # optional - - :/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: - - :/config - - :/tv_shows # optional - - :/movies # optional - - :/music_videos # optional - - :/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 -^^^^^^^^^^^^^^^ +Docker Compose provides a declarative way to configure and orchestrate containers which +makes them easier to manage and re-use. Create a ``compose.yaml`` file in your project +directory such as: .. code-block:: yaml - :emphasize-lines: 5-6 - :caption: compose.yaml + :caption: compose.yaml - 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 + 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: + - :/config + - :/tv_shows # optional + - :/movies # optional + - :/music_videos # optional + - :/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"] -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 ---------- -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 `_ for further information on the parameters and other options you can use. +You can run the container on an ad-hoc basis without Docker Compose using the Docker CLI +instead. It will not restart if stopped for any reason, including rebooting the +host. The following command is for the gui image: .. code-block:: bash @@ -152,4 +108,32 @@ If you prefer to only run the container once, you can use the CLI command instea -v :/movies \ -v :/music_videos \ -v :/music \ - ghcr.io/jmbannon/ytdl-sub-gui:latest \ No newline at end of file + ghcr.io/jmbannon/ytdl-sub-gui:latest + +See `the Docker reference `_ 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 `_. " + "``CRON_SCHEDULE``", "cron schedule `format `_", "Optional. Schedule to run the ``cron`` file in ytdl-sub's container. More info :ref:`here `." + "``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 `_ as well." + +Configuration +------------- + +In these examples, the configuration files will be at +``/config.yaml`` and +``/subscriptions.yaml``. Start the container the first time to +populate those files with default examples. diff --git a/docs/source/guides/install/index.rst b/docs/source/guides/install/index.rst index ff1d293f..af97f987 100644 --- a/docs/source/guides/install/index.rst +++ b/docs/source/guides/install/index.rst @@ -1,5 +1,6 @@ Install by Platform =================== + ``ytdl-sub`` can be installed on the following platforms. All installations require a 64-bit CPU. 32-bit is not supported. @@ -8,7 +9,8 @@ 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 `. For install on Unraid, check out our :unraid:`unraid community apps `. + The recommended install method of ``ytdl-sub`` is one of our :doc:`docker containers + `. :doc:`/guides/install/docker` @@ -20,9 +22,8 @@ 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: diff --git a/docs/source/guides/install/linux.rst b/docs/source/guides/install/linux.rst index f9105d80..b654bb0e 100644 --- a/docs/source/guides/install/linux.rst +++ b/docs/source/guides/install/linux.rst @@ -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:: @@ -15,7 +15,8 @@ Linux chmod +x ytdl-sub ./ytdl-sub -h - You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to date: + You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to + date: .. code-block:: bash @@ -36,7 +37,8 @@ Linux chmod +x ytdl-sub ./ytdl-sub -h - You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to date: + You can also install using yt-dlp's ffmpeg builds. This ensures your ffmpeg is up to + date: .. code-block:: bash diff --git a/docs/source/guides/install/unraid.rst b/docs/source/guides/install/unraid.rst index d27535dd..56f7ff09 100644 --- a/docs/source/guides/install/unraid.rst +++ b/docs/source/guides/install/unraid.rst @@ -1,16 +1,29 @@ +====== Unraid --------------- -You can install our :unraid:`unraid community apps ` through the `Unraid Community Apps plugin `_. +====== +You can install our :unraid:`unraid community apps ` +through the `Unraid Community Apps plugin `_. -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:: -.. 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. - 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 \ No newline at end of file + .. 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 diff --git a/docs/source/guides/install/windows.rst b/docs/source/guides/install/windows.rst index 7c4050c2..60c7a217 100644 --- a/docs/source/guides/install/windows.rst +++ b/docs/source/guides/install/windows.rst @@ -1,5 +1,7 @@ +======= Windows --------------- +======= + From powershell, run: .. code-block:: powershell @@ -12,4 +14,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 \ No newline at end of file + ytdl-sub.exe -h diff --git a/docs/source/index.rst b/docs/source/index.rst index 2fda8ee7..f25c08cd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,7 +10,6 @@ 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! \ No newline at end of file diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 0126202a..1660a87c 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -8,10 +8,23 @@ 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 downloads media via `yt-dlp`_ and prepares it for your favorite media player (`Kodi`_, `Jellyfin`_, `Plex`_, `Emby`_, modern music players). +``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.. -Visual examples -=============== +To these ends, ``ytdl-sub``: + +- wraps and runs `yt-dlp`_, per your configuration to: + + - download the media, remux and/or optionally transcode it + +- prepares additional metadata both embedded and in external files + +- renames the resulting files + +- places them in your library .. figure:: https://user-images.githubusercontent.com/10107080/182677243-b4184e51-9780-4094-bd40-ea4ff58555d0.PNG :alt: The Jellyfin web interface, showing the thumbnails of various YouTube shows. @@ -34,10 +47,52 @@ Visual examples SoundCloud albums and singles in MusicBee -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. +Motivation +---------- + +`yt-dlp`_ has grown into a well maintained, central repository of the intricate, +inscrutable, and extensive technical knowledge required to automate downloading media +from online services. When those services change their APIs or otherwise change +behavior, `yt-dlp`_ is the central, low-level tool to update. It does a best-in-class +job at that task, and it does that job more effectively by narrowing focus to just that. +As much knowledge as it encapsulates and as well as it does that, it still requires a +great deal of additional knowledge to make its output accessible to end-users. Mostly +this gap is about extracting and formatting metadata and correctly placing the resulting +output files in a media library. + +A number of tools, applications, and other projects have grown up around that central +`yt-dlp`_ pillar to fill in those gaps, and this project was one of the early +entrants. Many are `full-featured services that provide web UIs`_ including some that +`provide media player web UIs`_. Most of those other projects necessarily narrow their +scope to provide a more polished and integrated user experience. + +Similarly, ``ytdl-sub`` can run automatically to accomplish the same goals, but aims to +serve users that need lower-level control and/or have use cases not covered by the more +narrow scope of those other projects. To some degree, this makes this project +intrinsically less user friendly and requires more technical experience or learning. + +Want something that "Just Works", try one of the other projects; we recommend +`Pinchflat`_ as the next step towards that end. Want to download from more than just +YouTube? Don't like the other restrictions inherent in the goals of those other +projects? Have unique use cases? Then dig in, learn, and we hope ``ytdl-sub`` gives you +enough rope and `a foot-gun`_ to get you there. + +.. _`full-featured services that provide web UIs`: + https://github.com/kieraneglin/pinchflat +.. _`provide media player web UIs`: + https://www.tubearchivist.com/ +.. _`Pinchflat`: `full-featured services that provide web UIs`_ +.. _`a foot-gun`: https://en.wiktionary.org/wiki/footgun + Why download instead of stream? ------------------------------- -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. \ No newline at end of file + +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. diff --git a/docs/source/prebuilt_presets/helpers.rst b/docs/source/prebuilt_presets/helpers.rst index e8e37c31..4e952009 100644 --- a/docs/source/prebuilt_presets/helpers.rst +++ b/docs/source/prebuilt_presets/helpers.rst @@ -1,3 +1,6 @@ + + + ============== Helper Presets ============== @@ -6,11 +9,13 @@ Helper Presets See how to apply helper presets :doc:`here ` + 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. +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 @@ -31,9 +36,12 @@ To prevent deletion of files, use the preset ``Only Recent Archive`` instead. Filter Keywords --------------- -``Filter Keywords`` can include or exclude media with any of the listed keywords. Both keywords and title/description are lower-cased before filtering. +``Filter Keywords`` can include or exclude media with any of the listed keywords. Both +keywords and title/description are lower-cased before filtering. -Default behavior for Keyword evaluation is ANY, meaning the filter will succeed if any of the keywords are present. This can be set to ANY or ALL using the respective ``_eval`` variable. +Default behavior for Keyword evaluation is ANY, meaning the filter will succeed if any +of the keywords are present. This can be set to ANY or ALL using the respective +``_eval`` variable. Supports the following override variables: @@ -71,17 +79,49 @@ Supports the following override variables: - "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: +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 by +default. You can change this number by setting the override variable +``chunk_max_downloads``. .. code-block:: yaml @@ -100,5 +140,98 @@ change this number by setting the override variable ``chunk_max_downloads``. = 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. \ No newline at end of file +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 ` 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 +` 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) } \ No newline at end of file diff --git a/docs/source/prebuilt_presets/index.rst b/docs/source/prebuilt_presets/index.rst index bb95ac0c..7ce6d101 100644 --- a/docs/source/prebuilt_presets/index.rst +++ b/docs/source/prebuilt_presets/index.rst @@ -7,11 +7,13 @@ 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. + 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 + :caption: + Applies Max Video Quality preset to all TV shows, and Chunk Downloads preset to + some Plex TV Show by Date | Max Video Quality: @@ -22,9 +24,8 @@ media in various players. = Documentaries: "Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U" - -For advanced users, you can review the prebuilt preset -definitions :doc:`here `. +For advanced users, you can review the prebuilt preset definitions :doc:`here +`. .. toctree:: :titlesonly: @@ -33,4 +34,4 @@ definitions :doc:`here `. music music_videos media_quality - helpers \ No newline at end of file + helpers diff --git a/docs/source/prebuilt_presets/media_quality.rst b/docs/source/prebuilt_presets/media_quality.rst index ceb92479..46311b5d 100644 --- a/docs/source/prebuilt_presets/media_quality.rst +++ b/docs/source/prebuilt_presets/media_quality.rst @@ -6,8 +6,10 @@ Media Quality Presets See how to apply media quality presets :doc:`here ` + Video ----- + The following presets set video quality specifications to yt-dlp. - ``Max Video Quality`` @@ -17,10 +19,12 @@ The following presets set video quality specifications to yt-dlp. - ``Max 720p`` - ``Max 480p`` + Audio ----- -The following presets set audio quality specifications to yt-dlp. -These assume you are only extracting audio (no video). + +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`` diff --git a/docs/source/prebuilt_presets/music.rst b/docs/source/prebuilt_presets/music.rst index cab784e6..1713a849 100644 --- a/docs/source/prebuilt_presets/music.rst +++ b/docs/source/prebuilt_presets/music.rst @@ -2,21 +2,16 @@ 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. - -.. hint:: - - The subscription *value* (denoted by =) will set the genre tag for all music scraped under its key - for all 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*. +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. @@ -26,8 +21,8 @@ Playlists are recognized as the album, and videos within it are tracks. = 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: +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 @@ -37,11 +32,13 @@ of an array, like so: - "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. + +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. @@ -51,8 +48,8 @@ Videos are recognized as the album, and chapters within it are tracks. = 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: +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 @@ -62,11 +59,14 @@ of an array, like so: - "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. + +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 @@ -77,8 +77,10 @@ It will attempt to group tracks into albums before falling back to single format "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 diff --git a/docs/source/prebuilt_presets/music_videos.rst b/docs/source/prebuilt_presets/music_videos.rst index 46e6fcf9..9668396d 100644 --- a/docs/source/prebuilt_presets/music_videos.rst +++ b/docs/source/prebuilt_presets/music_videos.rst @@ -2,4 +2,4 @@ Music Video Presets =================== -WIP \ No newline at end of file +WIP diff --git a/docs/source/prebuilt_presets/tv_shows.rst b/docs/source/prebuilt_presets/tv_shows.rst index 8510350d..6dfce04a 100644 --- a/docs/source/prebuilt_presets/tv_shows.rst +++ b/docs/source/prebuilt_presets/tv_shows.rst @@ -2,49 +2,63 @@ 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. +Player-Specific Presets +----------------------- + +``ytdl-sub`` provides player-specific versions of certain presets, which apply settings +to optimize the downloads for that player. The following actions are taken based on the indicated player: +Kodi +~~~~ + +* Everything that the Jellyfin version does +* Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with + ``□`` Jellyfin --------- +~~~~~~~~ + * Places any season-specific poster art in the main show folder * Generates NFO tags -Kodi --------- -* Everything that the Jellyfin version does -* Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with ``□`` +Emby +~~~~ + +* Places any season-specific poster art in the main show folder +* Generates NFO tags + + * For named seasons, creates a ``season.nfo`` file per season Plex --------- -* :ref:`Special sanitization ` of numbers so Plex doesn't recognize numbers that are part of the title as the episode number +~~~~~~~~ + +* :ref:`Special sanitization + ` 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"`` -* ``"Plex TV Show by Date"`` +* ``Kodi TV Show by Date`` +* ``Jellyfin TV Show by Date`` +* ``Emby TV Show by Date`` +* ``Plex TV Show by Date`` .. code-block:: yaml @@ -74,47 +88,89 @@ Must define ``tv_show_directory``. Available presets: Advanced Usage ~~~~~~~~~~~~~~ -If you prefer a different organization method, you can instead apply multiple presets to your subscriptions. +If you prefer a different season/episode organization method, you can set the following +override variables. -You will need a base of one of the below: +.. code-block:: yaml -* ``kodi_tv_show_by_date`` -* ``jellyfin_tv_show_by_date`` -* ``plex_tv_show_by_date`` + __preset__: + overrides: + tv_show_directory: "/tv_shows" + tv_show_by_date_season_ordering: "upload-year-month" + tv_show_by_date_episode_ordering: "upload-day" -And then add one of these: +Or for a specific preset -* ``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. +.. code-block:: yaml + "~Kids Toys Play": + url: "https://www.youtube.com/@KidsToysPlayChannel" + tv_show_by_date_season_ordering: "upload-year-month" + tv_show_by_date_episode_ordering: "upload-day" + +The following are supported. Be sure the combined season + episode ordering include the +year, month, day, i.e. upload-year + upload-month-day. + +Season Ordering +""""""""""""""" + +``tv_show_by_date_season_ordering`` supports one of the following: + +* ``upload-year`` (default) +* ``upload-year-month`` +* ``release-year`` +* ``release-year-month`` + +Episode Ordering +"""""""""""""""" + +``tv_show_by_date_episode_ordering`` supports one of the following: + +* ``upload-month-day`` (default) +* ``upload-month-day-reversed`` + + * Reversed means more recent episodes appear at the top of a season by having a lower + value. +* ``upload-day`` +* ``release-day`` +* ``release-month-day`` +* ``release-month-day-reversed`` +* ``download-index`` + + * Episodes are numbered by the download order. **NOTE**: this is fetched using the + length of the download archive. Do not use if you intend to remove old videos. + +TV Show by Date presets use the following for defaults: + +.. code-block:: yaml + + tv_show_by_date_season_ordering: "upload-year" + tv_show_by_date_episode_ordering: "upload-month-day" TV Show Collection ------------------ TV Show Collections set each URL as its own season. If a video belongs to multiple URLs -(i.e. a channel and a channel's playlist), the video will only download once and reside in -the higher-numbered season. +(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. + 1. Organize a YouTube channel TV show where Season 1 contains any video not in a + 'season playlist', Season 2 for 'Playlist A', Season 3 for 'Playlist B', etc. + 2. Organize one or more YouTube channels/playlists, where each season represents a + separate channel/playlist. + +Today, ytdl-supports up to 40 seasons with 11 URLs per season. Example ~~~~~~~ + Must define ``tv_show_directory``. Available presets: -* ``"Kodi TV Show Collection"`` -* ``"Jellyfin TV Show Collection"`` -* ``"Plex TV Show Collection"`` +* ``Kodi TV Show Collection`` +* ``Jellyfin TV Show Collection`` +* ``Emby TV Show Collection`` +* ``Plex TV Show Collection`` .. code-block:: yaml @@ -131,22 +187,69 @@ 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 organization method, you can instead apply multiple presets to your subscriptions. +If you prefer a different episode organization method, you can set the following +override variables. -You will need a base of one of the below: +.. code-block:: yaml -* ``kodi_tv_show_collection`` -* ``jellyfin_tv_show_collection`` -* ``plex_tv_show_collection`` + __preset__: + overrides: + tv_show_directory: "/tv_shows" + tv_show_collection_episode_ordering: "release-year-month-day" -And then add one of these: +Or for a specific preset -* ``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`` +.. 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. diff --git a/docs/source/usage.rst b/docs/source/usage.rst index b9865cce..0db65064 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -1,5 +1,5 @@ Usage -======= +===== .. code-block:: @@ -7,10 +7,12 @@ Usage For Windows users, it would be ``ytdl-sub.exe`` + General Options --------------- -General options must be specified before the command (i.e. ``sub``). +CLI options common to all sub-commands. Must be specified before the sub-command, for +example ``$ ytdl-sub --dry-run sub ...``: .. code-block:: text @@ -20,24 +22,30 @@ General options must be specified before the command (i.e. ``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 info + level of logs to print to console, defaults to verbose -t TRANSACTIONPATH, --transaction-log TRANSACTIONPATH path to store the transaction log output of all files added, modified, deleted -st, --suppress-transaction-log do not output transaction logs to console or file + -nc, --suppress-colors + do not use colors in ytdl-sub output -m MATCH [MATCH ...], --match MATCH [MATCH ...] match subscription names to one or more substrings, and only run those subscriptions -Sub Options ------------ -Download all subscriptions specified in each ``SUBPATH``. + +Subscriptions Options +--------------------- + +Download all subscriptions specified in each :doc:`subscriptions file +<./guides/getting_started/subscriptions>`. .. code-block:: ytdl-sub [GENERAL OPTIONS] sub [SUBPATH ...] -``SUBPATH`` is one or more paths to subscription files, uses ``subscriptions.yaml`` if not provided. -It will use the config specified by ``--config``, or ``config.yaml`` if not provided. +``SUBPATH`` is one or more paths to subscription files and defaults to +``./subscriptions.yaml`` if none are given. It will use the config specified by +``--config``, or ``./config.yaml``, if not provided. .. code-block:: text :caption: Additional Options @@ -47,16 +55,19 @@ It will use the config specified by ``--config``, or ``config.yaml`` if not prov -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. +---------------- + +Download a single subscription in the form of CLI arguments instead of from :doc:`a +subscriptions file <./guides/getting_started/subscriptions>`: .. code-block:: ytdl-sub [GENERAL OPTIONS] dl [SUBSCRIPTION ARGUMENTS] -``SUBSCRIPTION ARGUMENTS`` are 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: +``SUBSCRIPTION ARGUMENTS`` are the same as YAML arguments, but use periods (``.``) +instead of indents. For example, you can represent this subscription: .. code-block:: yaml @@ -76,11 +87,15 @@ Using the command: --overrides.tv_show_name "Rick A" \ --overrides.url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw" -See how to shorten commands using -`download aliases `_. +See how to shorten commands using `download aliases +`_. + View Options ------------------ +------------ + +Preview the source variables for a given URL. Helpful to create new subscriptions: + .. code-block:: ytdl-sub view [-sc] [URL] @@ -91,5 +106,10 @@ View Options -sc, --split-chapters View source variables after splitting by chapters +CLI to SUB Options +------------------ +Convert yt-dlp cli arguments to ytdl-sub `ytdl_options` arguments. -Preview the source variables for a given URL. Helps when creating new configs. +.. code-block:: + + ytdl-sub cli-to-sub [YT-DLP ARGS] diff --git a/examples/advanced/tv_show_config.yaml b/examples/advanced/tv_show_config.yaml index 9cfa4ca7..d2019b9a 100644 --- a/examples/advanced/tv_show_config.yaml +++ b/examples/advanced/tv_show_config.yaml @@ -1,9 +1,9 @@ ############################################################################### -# Top-level configurations to apply umask and persist error logs +# Top-level configurations to apply umask and write log files configuration: umask: "002" persist_logs: - logs_directory: './logs' + logs_directory: '/config/logs' keep_successful_logs: False presets: @@ -98,4 +98,4 @@ presets: overrides: only_recent_date_range: "2months" - only_recent_max_files: 30 \ No newline at end of file + only_recent_max_files: 30 diff --git a/examples/advanced/tv_show_subscriptions.yaml b/examples/advanced/tv_show_subscriptions.yaml index 52e1728c..f0b18dfa 100644 --- a/examples/advanced/tv_show_subscriptions.yaml +++ b/examples/advanced/tv_show_subscriptions.yaml @@ -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 - date_range: "2weeks" + only_recent_date_range: "2weeks" "Frontline PBS": "https://www.youtube.com/@frontline" "Whitehouse": "https://www.bitchute.com/channel/zWsYVmCOu4JA/" # Supports non-YT sites \ No newline at end of file diff --git a/examples/tv_show_subscriptions.yaml b/examples/tv_show_subscriptions.yaml index eb88e4da..9ec60ad2 100644 --- a/examples/tv_show_subscriptions.yaml +++ b/examples/tv_show_subscriptions.yaml @@ -31,6 +31,7 @@ __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: @@ -64,6 +65,7 @@ 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: diff --git a/pyproject.toml b/pyproject.toml index f7c38964..772e91cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "yt-dlp[default]==2025.1.26", + "yt-dlp[default]==2026.1.29", "colorama~=0.4", "mergedeep~=1.3", "mediafile~=0.12", @@ -44,15 +44,15 @@ where = ["src"] test = [ "coverage[toml]>=6.3,<8.0", "pytest>=7.2,<9.0", - "pytest-rerunfailures>=14,<16", + "pytest-rerunfailures>=14,<17", ] lint = [ "black==24.10.0", - "isort==5.13.2", - "pylint==3.3.4", + "isort==7.0.0", + "pylint==4.0.1", ] docs = [ - "sphinx>=7,<9", + "sphinx>=7,<10", "sphinx-rtd-theme>=2,<4", "sphinx-book-theme~=1.0", "sphinx-copybutton~=0.5", diff --git a/src/ytdl_sub/cli/entrypoint.py b/src/ytdl_sub/cli/entrypoint.py index c19baf33..b825d45e 100644 --- a/src/ytdl_sub/cli/entrypoint.py +++ b/src/ytdl_sub/cli/entrypoint.py @@ -12,6 +12,7 @@ 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 from ytdl_sub.cli.output_transaction_log import output_transaction_log +from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub from ytdl_sub.cli.parsers.dl import DownloadArgsParser from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME from ytdl_sub.cli.parsers.main import parser @@ -206,6 +207,10 @@ 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) @@ -263,7 +268,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") + raise ValidationException("Must provide one of the commands: sub, dl, view, cli-to-sub") if not args.suppress_transaction_log: output_transaction_log( @@ -271,6 +276,6 @@ def main() -> List[Subscription]: transaction_log_file_path=args.transaction_log, ) - output_summary(subscriptions) + output_summary(subscriptions, suppress_colors=args.suppress_colors) return subscriptions diff --git a/src/ytdl_sub/cli/output_summary.py b/src/ytdl_sub/cli/output_summary.py index 61cf2317..44819f59 100644 --- a/src/ytdl_sub/cli/output_summary.py +++ b/src/ytdl_sub/cli/output_summary.py @@ -8,16 +8,16 @@ from ytdl_sub.utils.logger import Logger logger = Logger.get() -def _green(value: str) -> str: - return Fore.GREEN + value + Fore.RESET +def _green(value: str, suppress_colors: bool = False) -> str: + return value if suppress_colors else Fore.GREEN + value + Fore.RESET -def _red(value: str) -> str: - return Fore.RED + value + Fore.RESET +def _red(value: str, suppress_colors: bool = False) -> str: + return value if suppress_colors else Fore.RED + value + Fore.RESET -def _no_color(value: str) -> str: - return Fore.RESET + value + Fore.RESET +def _no_color(value: str, suppress_colors: bool = False) -> str: + return value if suppress_colors else Fore.RESET + value + Fore.RESET def _str_int(value: int) -> str: @@ -26,21 +26,23 @@ def _str_int(value: int) -> str: return str(value) -def _color_int(value: int) -> str: +def _color_int(value: int, suppress_colors: bool = False) -> str: str_int = _str_int(value) if value > 0: - return _green(str_int) + return _green(str_int, suppress_colors) if value < 0: - return _red(str_int) - return _no_color(str_int) + return _red(str_int, suppress_colors) + return _no_color(str_int, suppress_colors) -def output_summary(subscriptions: List[Subscription]) -> None: +def output_summary(subscriptions: List[Subscription], suppress_colors: bool) -> None: """ Parameters ---------- subscriptions Processed subscriptions + suppress_colors + Whether to have color or not Returns ------- @@ -65,21 +67,21 @@ def output_summary(subscriptions: List[Subscription]) -> None: # 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)) - width_num_entries_modified: int = len(_color_int(total_modified)) - width_num_entries_removed: int = len(_color_int(total_removed)) + width_num_entries_added: int = len(_color_int(total_added, suppress_colors)) + width_num_entries_modified: int = len(_color_int(total_modified, suppress_colors)) + width_num_entries_removed: int = len(_color_int(total_removed, suppress_colors)) width_num_entries: int = len(str(total_entries)) + 4 # aesthetics # Build the summary for subscription in subscriptions: - num_entries_added = _color_int(subscription.num_entries_added) - num_entries_modified = _color_int(subscription.num_entries_modified) - num_entries_removed = _color_int(subscription.num_entries_removed * -1) + num_entries_added = _color_int(subscription.num_entries_added, suppress_colors) + num_entries_modified = _color_int(subscription.num_entries_modified, suppress_colors) + num_entries_removed = _color_int(subscription.num_entries_removed * -1, suppress_colors) num_entries = str(subscription.num_entries) status = ( - _red(subscription.exception.__class__.__name__) + _red(subscription.exception.__class__.__name__, suppress_colors) if subscription.exception - else _green("✔") + else _green("✔", suppress_colors) ) summary.append( @@ -92,14 +94,16 @@ def output_summary(subscriptions: List[Subscription]) -> None: ) total_errors_str = ( - _green("Success") if total_errors == 0 else _red(f"Error{'s' if total_errors > 1 else ''}") + _green("Success", suppress_colors) + if total_errors == 0 + else _red(f"Error{'s' if total_errors > 1 else ''}", suppress_colors) ) summary.append( f"{total_subs_str:<{width_sub_name}} " - f"{_color_int(total_added):>{width_num_entries_added}} " - f"{_color_int(total_modified):>{width_num_entries_modified}} " - f"{_color_int(total_removed):>{width_num_entries_removed}} " + f"{_color_int(total_added, suppress_colors):>{width_num_entries_added}} " + f"{_color_int(total_modified, suppress_colors):>{width_num_entries_modified}} " + f"{_color_int(total_removed * -1, suppress_colors):>{width_num_entries_removed}} " f"{total_entries:>{width_num_entries}} " f"{total_errors_str}" ) diff --git a/src/ytdl_sub/cli/parsers/cli_to_sub.py b/src/ytdl_sub/cli/parsers/cli_to_sub.py new file mode 100644 index 00000000..d4190042 --- /dev/null +++ b/src/ytdl_sub/cli/parsers/cli_to_sub.py @@ -0,0 +1,64 @@ +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})) diff --git a/src/ytdl_sub/cli/parsers/main.py b/src/ytdl_sub/cli/parsers/main.py index 3c31e900..aa26b591 100644 --- a/src/ytdl_sub/cli/parsers/main.py +++ b/src/ytdl_sub/cli/parsers/main.py @@ -44,6 +44,7 @@ class MainArguments: short="-m", long="--match", ) + SUPPRESS_COLORS = CLIArgument(short="-nc", long="--suppress-colors") @classmethod def all(cls) -> List[CLIArgument]: @@ -59,6 +60,7 @@ class MainArguments: cls.TRANSACTION_LOG, cls.SUPPRESS_TRANSACTION_LOG, cls.MATCH, + cls.SUPPRESS_COLORS, ] @classmethod @@ -109,8 +111,8 @@ def _add_shared_arguments(arg_parser: argparse.ArgumentParser, suppress_defaults MainArguments.LOG_LEVEL.long, metavar="|".join(LoggerLevels.names()), type=str, - help="level of logs to print to console, defaults to info", - default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.INFO.name, + help="level of logs to print to console, defaults to verbose", + default=argparse.SUPPRESS if suppress_defaults else LoggerLevels.VERBOSE.name, choices=LoggerLevels.names(), dest="ytdl_sub_log_level", ) @@ -129,6 +131,13 @@ 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, @@ -212,3 +221,7 @@ 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") diff --git a/src/ytdl_sub/config/config_validator.py b/src/ytdl_sub/config/config_validator.py index f1290c51..accc4b46 100644 --- a/src/ytdl_sub/config/config_validator.py +++ b/src/ytdl_sub/config/config_validator.py @@ -12,6 +12,8 @@ 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 from ytdl_sub.validators.file_path_validators import FFprobeFileValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator @@ -75,7 +77,8 @@ class PersistLogsValidator(StrictDictValidator): @property def logs_directory(self) -> str: """ - Required. The directory to store the logs in. + Write log files to this directory with names like + ``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``. (required) """ return self._logs_directory.value @@ -100,7 +103,10 @@ class PersistLogsValidator(StrictDictValidator): @property def keep_successful_logs(self) -> bool: """ - Optional. Whether to store logs when downloading is successful. Defaults to True. + If the ``persist_logs:`` key is in the configuration, then ``ytdl-sub`` *always* + writes log files for the subscription both for successful downloads and when it + encounters an error while downloading. When this key is ``False``, only write + log files for errors. (default ``True``) """ return self._keep_successful_logs.value @@ -155,11 +161,17 @@ 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 + directory. (default ``./.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)) @@ -167,7 +179,7 @@ class ConfigOptions(StrictDictValidator): @property def umask(self) -> Optional[str]: """ - Umask (octal format) to apply to every created file. Defaults to "022". + Umask in octal format to apply to every created file. (default ``022``) """ return self._umask.value @@ -226,24 +238,25 @@ 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. (default ``/tmp``) """ return self._lock_directory.value @property def ffmpeg_path(self) -> str: """ - Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux, and - ``ffmpeg.exe`` for Windows (in the same directory as ytdl-sub). + Path to ffmpeg executable. (default ``/usr/bin/ffmpeg`` for Linux, + ``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows) """ return self._ffmpeg_path.value @property def ffprobe_path(self) -> str: """ - Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux, and - ``ffprobe.exe`` for Windows (in the same directory as ytdl-sub). + Path to ffprobe executable. (default ``/usr/bin/ffprobe`` for Linux, + ``./ffprobe.exe`` in the same directory as ytdl-sub for Windows) """ return self._ffprobe_path.value diff --git a/src/ytdl_sub/config/defaults.py b/src/ytdl_sub/config/defaults.py index d29166bf..e0746e1e 100644 --- a/src/ytdl_sub/config/defaults.py +++ b/src/ytdl_sub/config/defaults.py @@ -2,6 +2,8 @@ import os from ytdl_sub.utils.system import IS_WINDOWS +# pylint: disable=invalid-name + def _existing_path(*paths: str) -> str: """ @@ -22,7 +24,7 @@ if IS_WINDOWS: MAX_FILE_NAME_BYTES = 255 else: - DEFAULT_LOCK_DIRECTORY = "/tmp" + DEFAULT_LOCK_DIRECTORY = ".ytdl-sub-lock" DEFAULT_FFMPEG_PATH = os.getenv( "YTDL_SUB_FFMPEG_PATH", _existing_path("/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg") ) diff --git a/src/ytdl_sub/config/overrides.py b/src/ytdl_sub/config/overrides.py index 9b7a4372..f1011fcf 100644 --- a/src/ytdl_sub/config/overrides.py +++ b/src/ytdl_sub/config/overrides.py @@ -1,9 +1,10 @@ from typing import Any from typing import Dict +from typing import Iterable from typing import Optional from typing import Set - -import mergedeep +from typing import Type +from typing import TypeVar from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.script.variable_definitions import VARIABLES @@ -11,17 +12,21 @@ from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARI 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 +from ytdl_sub.script.types.resolvable import 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 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 OverridesStringFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator +ExpectedT = TypeVar("ExpectedT") + class Overrides(UnstructuredDictFormatterValidator, Scriptable): """ @@ -88,6 +93,24 @@ 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. @@ -115,29 +138,35 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): ) def initial_variables( - self, unresolved_variables: Optional[Dict[str, str]] = None - ) -> Dict[str, str]: + self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None + ) -> Dict[str, SyntaxTree]: """ Returns ------- Variables and format strings for all Override variables + additional variables (Optional) """ - 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) + 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) def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides": """ Initialize the override script with any unresolved variables """ - self.script.add( + self.script.add_parsed( self.initial_variables( unresolved_variables={ - var_name: f"{{%throw('Plugin variable {var_name} has not been created yet')}}" + var_name: SyntaxTree( + ast=[ + BuiltInFunction( + name="throw", + args=[ + String(f"Plugin variable {var_name} has not been created yet") + ], + ) + ] + ) for var_name in unresolved_variables } ) @@ -158,10 +187,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): 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( @@ -176,7 +210,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): formatter: StringFormatterValidator, entry: Optional[Entry] = None, function_overrides: Optional[Dict[str, str]] = None, - ) -> str: + expected_type: Type[ExpectedT] = str, + ) -> ExpectedT: """ Parameters ---------- @@ -186,6 +221,8 @@ 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 ------- @@ -196,28 +233,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): StringFormattingException If the formatter that is trying to be resolved cannot """ - return formatter.post_process( - str( - self._apply_to_resolvable( - formatter=formatter, entry=entry, function_overrides=function_overrides - ) - ) + out = formatter.post_process( + self._apply_to_resolvable( + formatter=formatter, entry=entry, function_overrides=function_overrides + ).native ) - def apply_overrides_formatter_to_native( - self, - formatter: OverridesStringFormatterValidator, - ) -> Any: - """ - Parameters - ---------- - formatter - Overrides formatter to apply + if not isinstance(out, expected_type): + raise StringFormattingException( + f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'" + ) - Returns - ------- - The native python form of the resolved variable - """ - return self._apply_to_resolvable( - formatter=formatter, entry=None, function_overrides=None - ).native + return out diff --git a/src/ytdl_sub/config/plugin/plugin.py b/src/ytdl_sub/config/plugin/plugin.py index 13feb41d..23232c48 100644 --- a/src/ytdl_sub/config/plugin/plugin.py +++ b/src/ytdl_sub/config/plugin/plugin.py @@ -13,7 +13,6 @@ 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.utils.script import ScriptUtils from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -49,9 +48,7 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC): Returns True if enabled, False if disabled. """ if isinstance(self.plugin_options, ToggleableOptionsDictValidator): - return ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(self.plugin_options.enable) - ) + return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool) return True def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]: @@ -122,6 +119,17 @@ 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 diff --git a/src/ytdl_sub/config/plugin/plugin_mapping.py b/src/ytdl_sub/config/plugin/plugin_mapping.py index 316096b2..bf458d6a 100644 --- a/src/ytdl_sub/config/plugin/plugin_mapping.py +++ b/src/ytdl_sub/config/plugin/plugin_mapping.py @@ -24,6 +24,8 @@ 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.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 @@ -39,6 +41,7 @@ class PluginMapping: "audio_extract": AudioExtractPlugin, "date_range": DateRangePlugin, "embed_thumbnail": EmbedThumbnailPlugin, + "square_thumbnail": SquareThumbnailPlugin, "file_convert": FileConvertPlugin, "format": FormatPlugin, "match_filters": MatchFiltersPlugin, @@ -46,6 +49,7 @@ class PluginMapping: "video_tags": VideoTagsPlugin, "nfo_tags": NfoTagsPlugin, "output_directory_nfo_tags": OutputDirectoryNfoTagsPlugin, + "static_nfo_tags": StaticNfoTagsPlugin, "subtitles": SubtitlesPlugin, "chapters": ChaptersPlugin, "split_by_chapters": SplitByChaptersPlugin, @@ -83,9 +87,17 @@ 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 @@ -96,6 +108,8 @@ 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") diff --git a/src/ytdl_sub/config/plugin/plugin_operation.py b/src/ytdl_sub/config/plugin/plugin_operation.py index 2c187f30..97706d53 100644 --- a/src/ytdl_sub/config/plugin/plugin_operation.py +++ b/src/ytdl_sub/config/plugin/plugin_operation.py @@ -6,3 +6,4 @@ class PluginOperation(Enum): MODIFY_ENTRY_METADATA = 0 MODIFY_ENTRY = 1 POST_PROCESS = 2 + POST_COMPLETION = 3 diff --git a/src/ytdl_sub/config/plugin/preset_plugins.py b/src/ytdl_sub/config/plugin/preset_plugins.py index 7eda8a9d..61305a37 100644 --- a/src/ytdl_sub/config/plugin/preset_plugins.py +++ b/src/ytdl_sub/config/plugin/preset_plugins.py @@ -1,5 +1,7 @@ +from typing import Iterable from typing import List from typing import Optional +from typing import Set from typing import Tuple from typing import Type @@ -44,3 +46,34 @@ 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 diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index 914ebc77..b89e1d53 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -2,6 +2,7 @@ import copy from typing import Any from typing import Dict from typing import List +from typing import Set from mergedeep import mergedeep @@ -11,7 +12,6 @@ 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 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 from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES @@ -172,6 +172,37 @@ 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) @@ -192,13 +223,10 @@ class Preset(_PresetShell): ) self.plugins: PresetPlugins = self._validate_and_get_plugins() - 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() + 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) @property def name(self) -> str: @@ -227,11 +255,18 @@ class Preset(_PresetShell): """ return cls(config=config, name=preset_name, value=preset_dict) - @property - def yaml(self) -> str: + def yaml(self, subscription_only: bool) -> 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}}) diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index f1ba8534..27ad8a92 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -1,15 +1,22 @@ from typing import Any from typing import Dict from typing import Optional +from typing import Set 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 +from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.file_handler import FileHandler 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 from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator +from ytdl_sub.validators.string_formatter_validators import StandardizedDateValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_formatter_validators import ( UnstructuredOverridesDictFormatterValidator, @@ -53,19 +60,31 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator): def to_native_dict(self, overrides: Overrides) -> Dict: """ Materializes the entire ytdl-options dict from OverrideStringFormatters into - native python + native python. """ - return { - key: overrides.apply_overrides_formatter_to_native(val) + 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(StrictDictValidator): +class OutputOptions(OptionsDictValidator): """ Defines where to output files and thumbnails after all post-processing has completed. @@ -87,6 +106,8 @@ class OutputOptions(StrictDictValidator): 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"} @@ -99,6 +120,9 @@ class OutputOptions(StrictDictValidator): "keep_files_before", "keep_files_after", "keep_max_files", + "download_archive_standardized_date", + "keep_files_date_eval", + "preserve_mtime", } @classmethod @@ -156,6 +180,15 @@ class OutputOptions(StrictDictValidator): 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 @@ -272,6 +305,18 @@ class OutputOptions(StrictDictValidator): """ 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]: """ @@ -283,3 +328,21 @@ class OutputOptions(StrictDictValidator): 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 + # } + } diff --git a/src/ytdl_sub/config/validators/variable_validation.py b/src/ytdl_sub/config/validators/variable_validation.py index 60488394..941d228e 100644 --- a/src/ytdl_sub/config/validators/variable_validation.py +++ b/src/ytdl_sub/config/validators/variable_validation.py @@ -1,10 +1,6 @@ -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 @@ -13,198 +9,188 @@ 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.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES +from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES +from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.script.script import Script -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.script.utils.name_validation import is_function +from ytdl_sub.utils.script import ScriptUtils 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 -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 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") - return dummy_variables - - -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( - 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 _override_variables(overrides: Overrides) -> Set[str]: - return set(list(overrides.initial_variables().keys())) + @classmethod + def all(cls) -> List[int]: + """ + All possible resolution levels. + """ + return [cls.ORIGINAL, cls.FILL, cls.RESOLVE, cls.INTERNAL] 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) -> 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") + + 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, ): + self.overrides = overrides self.downloader_options = downloader_options self.output_options = output_options self.plugins = plugins - self.script: Optional[Script] = None - self.resolved_variables: Set[str] = set() - self.unresolved_variables: Set[str] = set() - - 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 + 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 - return self + self._apply_resolution_level() - 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: + def _add_runtime_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None: """ Add dummy variables for script validation """ added_variables = options.added_variables( - unresolved_variables=self.unresolved_variables, + unresolved_variables=self.unresolved_runtime_variables, ).get(plugin_op, set()) modified_variables = options.modified_variables().get(plugin_op, set()) - resolved_variables = added_variables | modified_variables + self.unresolved_runtime_variables -= added_variables | modified_variables - self.resolved_variables |= resolved_variables - self.unresolved_variables -= resolved_variables - - def ensure_proper_usage(self) -> None: + def ensure_proper_usage(self, partial_resolve_formatters: bool = False) -> Dict: """ 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_variables(PluginOperation.DOWNLOADER, options=self.downloader_options) - self._add_subscription_override_variables() + 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 + ) # Metadata variables to be added for plugin_options in PluginMapping.order_options_by( self.plugins.zipped(), PluginOperation.MODIFY_ENTRY_METADATA ): - self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options) + self._add_runtime_variables( + PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options + ) for plugin_options in PluginMapping.order_options_by( self.plugins.zipped(), PluginOperation.MODIFY_ENTRY ): - self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options) + self._add_runtime_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options) # Validate that any formatter in the plugin options can resolve - validate_formatters( + resolved_subscription |= 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, ) - validate_formatters( + resolved_subscription |= 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, ) - assert not self.unresolved_variables + # 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) + + # TODO: make function + resolved_subscription["overrides"] = {} + for name in self.overrides.keys: + value = self.script.definition_of(name) + if name in self.script.function_names: + # Keep custom functions as-is + resolved_subscription["overrides"][name] = self.overrides.dict_with_format_strings[ + name + ] + elif resolved := value.maybe_resolvable: + resolved_subscription["overrides"][name] = resolved.native + else: + resolved_subscription["overrides"][name] = ScriptUtils.to_native_script(value) + + assert not self.unresolved_runtime_variables + return resolved_subscription diff --git a/src/ytdl_sub/downloaders/url/downloader.py b/src/ytdl_sub/downloaders/url/downloader.py index f0595945..c94d6910 100644 --- a/src/ytdl_sub/downloaders/url/downloader.py +++ b/src/ytdl_sub/downloaders/url/downloader.py @@ -25,7 +25,6 @@ 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.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 @@ -53,12 +52,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 self.overrides.apply_formatter(validator.url) == entry_input_url: + if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list): return validator # Match the first validator based on the URL, if one exists for validator in self.plugin_options.urls.list: - if self.overrides.apply_formatter(validator.url) == entry_input_url: + if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list): return validator # Return the first validator if none exist @@ -258,6 +257,16 @@ 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 @@ -359,7 +368,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=entry.webpage_url, + url=self.webpage_url(entry=entry), ) return Entry( download_entry_dict, @@ -367,13 +376,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): ) def _iterate_child_entries( - self, entries: List[Entry], download_reversed: bool + self, entries: List[Entry], validator: UrlValidator ) -> 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 download_reversed: + if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool): indices = reversed(indices) for idx in indices: @@ -395,17 +404,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): entries_to_iter[idx] = None def _iterate_parent_entry( - self, parent: EntryParent, download_reversed: bool + self, parent: EntryParent, validator: UrlValidator ) -> Iterator[Entry]: - yield from self._iterate_child_entries( - entries=parent.entry_children(), download_reversed=download_reversed - ) + yield from self._iterate_child_entries(entries=parent.entry_children(), validator=validator) # Recursion the parent's parent entries for parent_child in reversed(parent.parent_children()): - yield from self._iterate_parent_entry( - parent=parent_child, download_reversed=download_reversed - ) + yield from self._iterate_parent_entry(parent=parent_child, validator=validator) def _download_url_metadata( self, url: str, include_sibling_metadata: bool, ytdl_options_overrides: Dict @@ -439,7 +444,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): self, parents: List[EntryParent], orphans: List[Entry], - download_reversed: bool, + validator: UrlValidator, ) -> Iterator[Entry]: """ Downloads the leaf entries from EntryParent trees @@ -447,23 +452,17 @@ 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, download_reversed=download_reversed - ) + yield from self._iterate_parent_entry(parent=parent, validator=validator) - yield from self._iterate_child_entries( - entries=orphans, download_reversed=download_reversed - ) + yield from self._iterate_child_entries(entries=orphans, validator=validator) 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) ) - download_reversed = ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(validator.download_reverse) - ) - include_sibling_metadata = ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(validator.include_sibling_metadata) + + include_sibling_metadata = self.overrides.apply_formatter( + validator.include_sibling_metadata, expected_type=bool ) parents, orphan_entries = self._download_url_metadata( @@ -472,16 +471,15 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): 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, - download_reversed=download_reversed, + validator=validator, ) def download_metadata(self) -> Iterable[Entry]: @@ -489,19 +487,25 @@ 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 (url := self.overrides.apply_formatter(url_validator.url)): + if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)): 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), - } - ) + for url in reversed(urls): + assert isinstance(url, str) - yield entry + 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 def download(self, entry: Entry) -> Optional[Entry]: """ @@ -534,8 +538,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_upload_date( - upload_date_standardized=entry.get(v.upload_date_standardized, str) + 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) ) download_idx = self._enhanced_download_archive.num_entries diff --git a/src/ytdl_sub/downloaders/url/validators.py b/src/ytdl_sub/downloaders/url/validators.py index 1bd9a79f..b2010ebc 100644 --- a/src/ytdl_sub/downloaders/url/validators.py +++ b/src/ytdl_sub/downloaders/url/validators.py @@ -1,5 +1,6 @@ from typing import Any from typing import Dict +from typing import List from typing import Optional from typing import Set @@ -21,7 +22,7 @@ class UrlThumbnailValidator(StrictDictValidator): def __init__(self, name, value): super().__init__(name, value) - self._name = self._validate_key(key="name", validator=StringFormatterValidator) + self._thumb_name = self._validate_key(key="name", validator=StringFormatterValidator) self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator) @property @@ -29,7 +30,7 @@ class UrlThumbnailValidator(StrictDictValidator): """ File name for the thumbnail """ - return self._name + return self._thumb_name @property def uid(self) -> OverridesStringFormatterValidator: @@ -43,6 +44,19 @@ 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 = { @@ -52,6 +66,7 @@ class UrlValidator(StrictDictValidator): "download_reverse", "ytdl_options", "include_sibling_metadata", + "webpage_url", } @classmethod @@ -67,7 +82,7 @@ class UrlValidator(StrictDictValidator): super().__init__(name, value) # TODO: url validate using yt-dlp IE - self._url = self._validate_key(key="url", validator=OverridesStringFormatterValidator) + self._url = self._validate_key(key="url", validator=OverridesOneOrManyUrlValidator) self._variables = self._validate_key_if_present( key="variables", validator=DictFormatterValidator, default={} ) @@ -89,6 +104,9 @@ class UrlValidator(StrictDictValidator): validator=OverridesBooleanFormatterValidator, default="False", ) + self._webpage_url = self._validate_key( + key="webpage_url", validator=StringFormatterValidator, default="{webpage_url}" + ) @property def url(self) -> OverridesStringFormatterValidator: @@ -180,6 +198,19 @@ class UrlValidator(StrictDictValidator): """ 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 `. 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 + class UrlStringOrDictValidator(UrlValidator): """ diff --git a/src/ytdl_sub/downloaders/ytdlp.py b/src/ytdl_sub/downloaders/ytdlp.py index 42f3d865..c01376e4 100644 --- a/src/ytdl_sub/downloaders/ytdlp.py +++ b/src/ytdl_sub/downloaders/ytdlp.py @@ -224,7 +224,7 @@ class YTDLP: except RejectedVideoReached: cls.logger.debug( "RejectedVideoReached, stopping additional downloads " - "(Can be disable by setting `date_range.breaking` to False)." + "(Can be disable by setting `date_range.breaks` to False)." ) except ExistingVideoReached: cls.logger.debug( diff --git a/src/ytdl_sub/entries/entry.py b/src/ytdl_sub/entries/entry.py index 401afd49..f97e98f5 100644 --- a/src/ytdl_sub/entries/entry.py +++ b/src/ytdl_sub/entries/entry.py @@ -102,6 +102,9 @@ class Entry(BaseEntry, Scriptable): v.sponsorblock_chapters.metadata_key, [] ), v.comments: download_entry._kwargs_get(v.comments.metadata_key, []), + # Updates with more accurate value, which may differ from the metadata value + v.height: download_entry._kwargs_get(v.height.metadata_key, 0), + v.width: download_entry._kwargs_get(v.width.metadata_key, 0), } ) return self diff --git a/src/ytdl_sub/entries/script/variable_definitions.py b/src/ytdl_sub/entries/script/variable_definitions.py index 5526780a..97fb7759 100644 --- a/src/ytdl_sub/entries/script/variable_definitions.py +++ b/src/ytdl_sub/entries/script/variable_definitions.py @@ -823,6 +823,17 @@ class YtdlSubVariableDefinitions(ABC): variable_name="upload_date_index_reversed_padded", pad=2 ) + @cached_property + def ytdl_sub_keep_files_date_eval(self: "VariableDefinitions") -> StringVariable: + """ + :description: + The standardized date variable supplied in ``output_options.keep_files_date_eval``. + """ + return StringVariable( + variable_name="ytdl_sub_entry_date_eval", + definition=f"{{%string({self.upload_date_standardized.variable_name})}}", + ) + class EntryVariableDefinitions(ABC): @cached_property @@ -1082,6 +1093,24 @@ class EntryVariableDefinitions(ABC): definition="{ {} }", ) + @cached_property + def height(self: "VariableDefinitions") -> IntegerMetadataVariable: + """ + :description: + Height in pixels of the video. If this value is unavailable (i.e. audio download), it + will default to 0. + """ + return IntegerMetadataVariable.from_entry(metadata_key="height", default=0) + + @cached_property + def width(self: "VariableDefinitions") -> IntegerMetadataVariable: + """ + :description: + Width in pixels of the video. If this value is unavailable (i.e. audio download), it + will default to 0. + """ + return IntegerMetadataVariable.from_entry(metadata_key="width", default=0) + class VariableDefinitions( EntryVariableDefinitions, @@ -1106,6 +1135,16 @@ class VariableDefinitions( ] } + @cache + def variable_names(self, include_sanitized: bool): + """ + Returns all variable names, and can include sanitized. + """ + var_names: Set[str] = self.scripts().keys() + if include_sanitized: + var_names |= {f"{name}_sanitized" for name in var_names} + return var_names + @cache def injected_variables(self) -> Set[MetadataVariable]: """ @@ -1121,6 +1160,9 @@ class VariableDefinitions( self.ytdl_sub_input_url, self.ytdl_sub_input_url_index, self.ytdl_sub_input_url_count, + self.ytdl_sub_keep_files_date_eval, + self.width, + self.height, } @cache @@ -1130,6 +1172,7 @@ class VariableDefinitions( """ return { self.uid, + self.extractor, self.extractor_key, self.epoch, self.webpage_url, diff --git a/src/ytdl_sub/entries/script/variable_types.py b/src/ytdl_sub/entries/script/variable_types.py index f406baf6..bbf994e9 100644 --- a/src/ytdl_sub/entries/script/variable_types.py +++ b/src/ytdl_sub/entries/script/variable_types.py @@ -22,7 +22,6 @@ VariableT = TypeVar("VariableT", bound="Variable") def _get( - cast: str, metadata_variable_name: str, metadata_key: str, variable_name: Optional[str], @@ -47,7 +46,7 @@ def _get( return as_type( variable_name=variable_name or metadata_key, metadata_key=metadata_key, - definition=f"{{ %legacy_bracket_safety(%{cast}({out})) }}", + definition=f"{{ {out} }}", ) @@ -182,7 +181,6 @@ class MapMetadataVariable(MetadataVariable, MapVariable): Creates a map variable from entry metadata """ return _get( - "map", metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, @@ -204,7 +202,6 @@ class ArrayMetadataVariable(MetadataVariable, ArrayVariable): Creates an array variable from entry metadata """ return _get( - "array", metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, @@ -226,7 +223,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable): Creates a string variable from entry metadata """ return _get( - "string", metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, @@ -245,7 +241,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable): Creates a string variable from playlist metadata """ return _get( - "string", metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, @@ -264,7 +259,6 @@ class StringMetadataVariable(MetadataVariable, StringVariable): Creates a string variable from source metadata """ return _get( - "string", metadata_variable_name=SOURCE_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, @@ -301,7 +295,6 @@ class IntegerMetadataVariable(MetadataVariable, IntegerVariable): Creates an int variable from entry metadata """ return _get( - "int", metadata_variable_name=ENTRY_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, @@ -320,7 +313,6 @@ class IntegerMetadataVariable(MetadataVariable, IntegerVariable): Creates an int variable from playlist metadata """ return _get( - "int", metadata_variable_name=PLAYLIST_METADATA_VARIABLE_NAME, metadata_key=metadata_key, variable_name=variable_name, diff --git a/src/ytdl_sub/entries/variables/override_variables.py b/src/ytdl_sub/entries/variables/override_variables.py index 42bb4498..8dd2081c 100644 --- a/src/ytdl_sub/entries/variables/override_variables.py +++ b/src/ytdl_sub/entries/variables/override_variables.py @@ -11,9 +11,6 @@ from ytdl_sub.entries.script.variable_types import Variable from ytdl_sub.script.functions import Functions from ytdl_sub.script.utils.name_validation import is_valid_name -# TODO: use this -SUBSCRIPTION_ARRAY = "subscription_array" - class SubscriptionVariables: @staticmethod @@ -55,16 +52,21 @@ class SubscriptionVariables: @staticmethod def subscription_indent_i(index: int) -> StringVariable: """ - For subscriptions in the form of + For subscriptions where the ancestor keys contain the ``= ...`` prefix, the + variables ``subscription_indent_1``, ``subscription_indent_2``, and so on get + set to each subsequent value. For example, given the following subscriptions + file snippet: .. code-block:: yaml - Preset | = Indent Value 1: - = Indent Value 2: + Preset 1 | = Indent Value 1 | Preset 2: + Preset 3 | = Indent Value 2 | Preset 4: "Subscription Name": "https://..." - ``subscription_indent_1`` and ``subscription_indent_2`` get set to - ``Indent Value 1`` and ``Indent Value 2``. + 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>`. """ return StringVariable( variable_name=f"subscription_indent_{index + 1}", definition="{ %string('') }" @@ -158,7 +160,7 @@ class OverrideHelpers: True if the override name itself is valid. False otherwise. """ if name.startswith("%"): - return is_valid_name(name=name[1:]) + name = name[1:] return is_valid_name(name=name) diff --git a/src/ytdl_sub/plugins/date_range.py b/src/ytdl_sub/plugins/date_range.py index 27b9c808..0cba6f89 100644 --- a/src/ytdl_sub/plugins/date_range.py +++ b/src/ytdl_sub/plugins/date_range.py @@ -1,13 +1,18 @@ from typing import List from typing import Optional +from typing import Set from typing import Tuple from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator from ytdl_sub.utils.datetime import to_date_str -from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.validators.string_datetime import StringDatetimeValidator from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator +from ytdl_sub.validators.string_select_validator import OverridesStringSelectValidator + + +class DateRangeType(OverridesStringSelectValidator): + _select_values: Set[str] = {"upload_date", "release_date"} class DateRangeOptions(ToggleableOptionsDictValidator): @@ -20,9 +25,11 @@ class DateRangeOptions(ToggleableOptionsDictValidator): 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. + 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. :Usage: @@ -31,9 +38,11 @@ class DateRangeOptions(ToggleableOptionsDictValidator): date_range: before: "now" after: "today-2weeks" + breaks: True + type: "upload_date" """ - _optional_keys = {"enable", "before", "after", "breaks"} + _optional_keys = {"enable", "before", "after", "breaks", "type"} def __init__(self, name, value): super().__init__(name, value) @@ -42,13 +51,14 @@ class DateRangeOptions(ToggleableOptionsDictValidator): self._breaks = self._validate_key_if_present( "breaks", OverridesBooleanFormatterValidator, default="True" ) + self._type = self._validate_key("type", DateRangeType, default="upload_date") @property def before(self) -> Optional[StringDatetimeValidator]: """ :expected type: Optional[OverridesFormatter] :description: - Only download videos before this datetime. + Only download videos only before this datetime, not inclusive. """ return self._before @@ -57,7 +67,7 @@ class DateRangeOptions(ToggleableOptionsDictValidator): """ :expected type: Optional[OverridesFormatter] :description: - Only download videos after this datetime. + Only download videos after or on this datetime, inclusive. """ return self._after @@ -71,6 +81,16 @@ class DateRangeOptions(ToggleableOptionsDictValidator): """ return self._breaks + @property + def type(self) -> DateRangeType: + """ + :expected type: Optional[OverridesFormatter] + :description: + Which type of date to use. Must be either ``upload_date`` or ``release_date``. + Defaults to ``upload_date``. + """ + return self._type + class DateRangePlugin(Plugin[DateRangeOptions]): plugin_options_type = DateRangeOptions @@ -84,20 +104,19 @@ class DateRangePlugin(Plugin[DateRangeOptions]): match_filters: List[str] = [] breaking_match_filters: List[str] = [] + date_type: str = self.overrides.apply_formatter(formatter=self.plugin_options.type) if self.plugin_options.before: before_str = to_date_str( date_validator=self.plugin_options.before, overrides=self.overrides ) - match_filters.append(f"upload_date < {before_str}") + match_filters.append(f"{date_type} < {before_str}") if self.plugin_options.after: after_str = to_date_str( date_validator=self.plugin_options.after, overrides=self.overrides ) - after_filter = f"upload_date >= {after_str}" - if ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(self.plugin_options.breaks) - ): + after_filter = f"{date_type} >= {after_str}" + if self.overrides.apply_formatter(self.plugin_options.breaks, expected_type=bool): breaking_match_filters.append(after_filter) else: match_filters.append(after_filter) diff --git a/src/ytdl_sub/plugins/embed_thumbnail.py b/src/ytdl_sub/plugins/embed_thumbnail.py index 9a7c8bab..4ce2b144 100644 --- a/src/ytdl_sub/plugins/embed_thumbnail.py +++ b/src/ytdl_sub/plugins/embed_thumbnail.py @@ -11,12 +11,12 @@ from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS -from ytdl_sub.validators.validators import BoolValidator +from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator logger = Logger.get("embed-thumbnail") -class EmbedThumbnailOptions(BoolValidator, OptionsValidator): +class EmbedThumbnailOptions(OverridesBooleanFormatterValidator, OptionsValidator): """ Whether to embed thumbnails to the audio/video file or not. @@ -33,7 +33,7 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]): @property def _embed_thumbnail(self) -> bool: - return self.plugin_options.value + return self.overrides.apply_formatter(self.plugin_options, expected_type=bool) @classmethod def _embed_video_thumbnail(cls, entry: Entry) -> None: diff --git a/src/ytdl_sub/plugins/filter_exclude.py b/src/ytdl_sub/plugins/filter_exclude.py index 08f13939..d4adcc86 100644 --- a/src/ytdl_sub/plugins/filter_exclude.py +++ b/src/ytdl_sub/plugins/filter_exclude.py @@ -7,14 +7,14 @@ from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.script import ScriptUtils -from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator +from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator +from ytdl_sub.validators.validators import ListValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-exclude") -class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): +class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): """ Applies a conditional OR on any number of filters comprised of either variables or scripts. If any filter evaluates to True, the entry will be excluded. @@ -30,6 +30,8 @@ class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): { %contains( %lower(description), '#short' ) } """ + _inner_list_type = BooleanFormatterValidator + class FilterExcludePlugin(Plugin[FilterExcludeOptions]): plugin_options_type = FilterExcludeOptions @@ -53,10 +55,11 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]): return entry for formatter in self.plugin_options.list: - out = ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(formatter=formatter, entry=entry) + should_exclude = self.overrides.apply_formatter( + formatter=formatter, entry=entry, expected_type=bool ) - if bool(out): + + if should_exclude: logger.info( "Filtering '%s' from the filter %s evaluating to True", entry.title, diff --git a/src/ytdl_sub/plugins/filter_include.py b/src/ytdl_sub/plugins/filter_include.py index 5f7fcc4a..5002dfdb 100644 --- a/src/ytdl_sub/plugins/filter_include.py +++ b/src/ytdl_sub/plugins/filter_include.py @@ -7,14 +7,14 @@ from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.script import ScriptUtils -from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator +from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator +from ytdl_sub.validators.validators import ListValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-include") -class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): +class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): """ Applies a conditional AND on any number of filters comprised of either variables or scripts. If all filters evaluate to True, the entry will be included. @@ -38,6 +38,8 @@ class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): } """ + _inner_list_type = BooleanFormatterValidator + class FilterIncludePlugin(Plugin[FilterIncludeOptions]): plugin_options_type = FilterIncludeOptions @@ -61,10 +63,10 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]): return entry for formatter in self.plugin_options.list: - out = ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(formatter=formatter, entry=entry) + should_exclude = self.overrides.apply_formatter( + formatter=formatter, entry=entry, expected_type=bool ) - if not bool(out): + if not should_exclude: logger.info( "Filtering '%s' from the filter %s evaluating to False", entry.title, diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index 86fe6568..802dab63 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -5,7 +5,6 @@ from pathlib import Path from typing import Any from typing import Dict from typing import List -from typing import Optional from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator @@ -19,8 +18,8 @@ from ytdl_sub.utils.xml import to_xml from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator from ytdl_sub.validators.nfo_validators import NfoTagsValidator 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 StringFormatterValidator -from ytdl_sub.validators.validators import BoolValidator class SharedNfoTagsOptions(ToggleableOptionsDictValidator): @@ -54,8 +53,8 @@ class SharedNfoTagsOptions(ToggleableOptionsDictValidator): ) self._tags = self._validate_key_if_present(key="tags", validator=NfoTagsValidator) self._kodi_safe = self._validate_key_if_present( - key="kodi_safe", validator=BoolValidator, default=False - ).value + key="kodi_safe", validator=OverridesBooleanFormatterValidator, default="False" + ) @property def nfo_name(self) -> StringFormatterFileNameValidator: @@ -81,9 +80,9 @@ class SharedNfoTagsOptions(ToggleableOptionsDictValidator): return self._tags @property - def kodi_safe(self) -> Optional[bool]: + def kodi_safe(self) -> OverridesBooleanFormatterValidator: """ - :expected type: Optional[Boolean] + :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 @@ -141,7 +140,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC): if not nfo_tags: return - if self.plugin_options.kodi_safe: + if self.overrides.apply_formatter(self.plugin_options.kodi_safe, expected_type=bool): nfo_root = to_max_3_byte_utf8_string(nfo_root) nfo_tags = { to_max_3_byte_utf8_string(key): [ diff --git a/src/ytdl_sub/plugins/square_thumbnail.py b/src/ytdl_sub/plugins/square_thumbnail.py new file mode 100644 index 00000000..dc5db0b7 --- /dev/null +++ b/src/ytdl_sub/plugins/square_thumbnail.py @@ -0,0 +1,77 @@ +from typing import List +from typing import Optional + +from ytdl_sub.config.plugin.plugin import Plugin +from ytdl_sub.config.validators.options import OptionsValidator +from ytdl_sub.entries.entry import Entry +from ytdl_sub.utils.ffmpeg import FFMPEG +from ytdl_sub.utils.file_handler import FileHandler +from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.logger import Logger +from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator + +logger = Logger.get("square-thumbnail") + + +class SquareThumbnailOptions(OverridesBooleanFormatterValidator, OptionsValidator): + """ + Whether to make thumbnails square. Supports both file and embedded-based thumbnails. Ideal + for representing audio albums. + + :Usage: + + .. code-block:: yaml + + square_thumbnail: True + """ + + +class SquareThumbnailPlugin(Plugin[SquareThumbnailOptions]): + plugin_options_type = SquareThumbnailOptions + + @property + def _square_thumbnail(self) -> bool: + return self.overrides.apply_formatter(self.plugin_options, expected_type=bool) + + @classmethod + def _convert_to_square_thumbnail(cls, entry: Entry) -> None: + thumbnail_path = entry.get_download_thumbnail_path() + tmp_file_path = FFMPEG.tmp_file_path(thumbnail_path) + try: + ffmpeg_args: List[str] = [ + "-i", + thumbnail_path, + "-c:v", + "mjpeg", + "-qmin", + "1", + "-qscale:v", + "1", + "-vf", + "crop=min(iw\\,ih):min(iw\\,ih)", + "-bitexact", # for reproducibility + tmp_file_path, + ] + FFMPEG.run(ffmpeg_args) + FileHandler.move(tmp_file_path, thumbnail_path) + finally: + FileHandler.delete(tmp_file_path) + + def post_process_entry(self, entry: Entry) -> Optional[FileMetadata]: + """ + Maybe make the thumbnail square + """ + if not self._square_thumbnail: + return None + + if not self.is_dry_run: + if not entry.is_thumbnail_downloaded(): + logger.warning( + "Cannot make a square thumbnail for '%s' because it is not available", + entry.title, + ) + return None + + self._convert_to_square_thumbnail(entry) + + return FileMetadata("Square thumbnail") diff --git a/src/ytdl_sub/plugins/static_nfo_tags.py b/src/ytdl_sub/plugins/static_nfo_tags.py new file mode 100644 index 00000000..3476bda5 --- /dev/null +++ b/src/ytdl_sub/plugins/static_nfo_tags.py @@ -0,0 +1,71 @@ +from ytdl_sub.entries.entry import Entry +from ytdl_sub.plugins.nfo_tags import NfoTagsValidator +from ytdl_sub.plugins.nfo_tags import SharedNfoTagsOptions +from ytdl_sub.plugins.nfo_tags import SharedNfoTagsPlugin +from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator + + +class StaticNfoTagsOptions(SharedNfoTagsOptions): + """ + 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 + """ + + @property + def nfo_root(self) -> StringFormatterValidator: + """ + :expected type: EntryFormatter + :description: + The root tag of the NFO's XML. In the usage above, it would look like + + .. code-block:: xml + + + + + """ + return self._nfo_root + + @property + def tags(self) -> NfoTagsValidator: + """ + :expected type: NfoTags + :description: + Tags within the nfo_root tag. In the usage above, it would look like + + .. code-block:: xml + + + + My custom season name! + + """ + return self._tags + + +class StaticNfoTagsPlugin(SharedNfoTagsPlugin): + plugin_options_type = StaticNfoTagsOptions + + def post_process_entry(self, entry: Entry) -> None: + """ + Creates the NFO from each entry, but does not link/save it to the entry. + """ + self._create_nfo(entry=entry, save_to_entry=False) diff --git a/src/ytdl_sub/plugins/subtitles.py b/src/ytdl_sub/plugins/subtitles.py index ecf32a1d..54318512 100644 --- a/src/ytdl_sub/plugins/subtitles.py +++ b/src/ytdl_sub/plugins/subtitles.py @@ -1,8 +1,10 @@ +import os from pathlib import Path from typing import Dict from typing import List from typing import Optional from typing import Set +from typing import Tuple from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.plugin.plugin_operation import PluginOperation @@ -11,6 +13,7 @@ from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.logger import Logger @@ -57,6 +60,7 @@ class SubtitleOptions(ToggleableOptionsDictValidator): "subtitles_type", "embed_subtitles", "languages", + "languages_required", "allow_auto_generated_subtitles", } @@ -76,6 +80,9 @@ class SubtitleOptions(ToggleableOptionsDictValidator): self._languages = self._validate_key_if_present( key="languages", validator=StringListValidator, default=["en"] ).list + self._languages_required = self._validate_key_if_present( + key="languages_required", validator=StringListValidator, default=[] + ).list self._allow_auto_generated_subtitles = self._validate_key_if_present( key="allow_auto_generated_subtitles", validator=BoolValidator, default=False ).value @@ -121,6 +128,16 @@ class SubtitleOptions(ToggleableOptionsDictValidator): """ return [lang.value for lang in self._languages] + @property + def languages_required(self) -> Optional[List[str]]: + """ + :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. + """ + return [lang.value for lang in self._languages_required] + @property def allow_auto_generated_subtitles(self) -> Optional[bool]: """ @@ -146,7 +163,12 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]): plugin_options_type = SubtitleOptions def ytdl_options(self) -> Optional[Dict]: - builder = YTDLOptionsBuilder().add({"writesubtitles": True}) + builder = YTDLOptionsBuilder().add( + { + "writesubtitles": True, + "sleep_interval_subtitles": 5.367, # for safe measure + } + ) if self.plugin_options.embed_subtitles: builder.add( @@ -206,7 +228,21 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]): if self.plugin_options.embed_subtitles: file_metadata = FileMetadata(f"Embedded subtitles with lang(s) {', '.join(langs)}") if self.plugin_options.subtitles_name: + download_subtitle_lang_file_names: List[Tuple[str, str]] = [] + for lang in langs: + download_subtitle_file_name = entry.base_filename( + ext=f"{lang}.{self.plugin_options.subtitles_type}" + ) + if os.path.isfile(Path(self.working_directory) / download_subtitle_file_name): + download_subtitle_lang_file_names.append((lang, download_subtitle_file_name)) + elif lang in self.plugin_options.languages_required: + raise UserThrownRuntimeError( + f"Required the subtitle lang {lang}, but the file could not be found for " + f"the entry {entry.title}." + ) + + for lang, file_name in download_subtitle_lang_file_names: output_subtitle_file_name = self.overrides.apply_formatter( formatter=self.plugin_options.subtitles_name, entry=entry, @@ -214,9 +250,7 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]): ) self.save_file( - file_name=entry.base_filename( - ext=f"{lang}.{self.plugin_options.subtitles_type}" - ), + file_name=file_name, output_file_name=output_subtitle_file_name, entry=entry, ) diff --git a/src/ytdl_sub/plugins/throttle_protection.py b/src/ytdl_sub/plugins/throttle_protection.py index 5bd45f10..463d1ace 100644 --- a/src/ytdl_sub/plugins/throttle_protection.py +++ b/src/ytdl_sub/plugins/throttle_protection.py @@ -1,6 +1,10 @@ import random import time +from abc import ABC +from typing import Dict from typing import Optional +from typing import Type +from typing import TypeVar from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin import Plugin @@ -9,52 +13,135 @@ from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.strict_dict_validator import StrictDictValidator -from ytdl_sub.validators.validators import FloatValidator +from ytdl_sub.validators.string_formatter_validators import FloatFormatterValidator +from ytdl_sub.validators.string_formatter_validators import OverridesFloatFormatterValidator from ytdl_sub.validators.validators import ProbabilityValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("throttle-protection") +FloatValidatorT = TypeVar("FloatValidatorT", bound=FloatFormatterValidator) -class RandomizedRangeValidator(StrictDictValidator): + +class _RandomizedRangeValidator(StrictDictValidator, ABC): """ - Validator to specify a float range between [min, max) + Base class for range validation, to support both entry and static overrides. """ + _float_validator: Type[FloatValidatorT] + _required_keys = {"max"} _optional_keys = {"min"} def __init__(self, name, value): super().__init__(name, value) - self._max = self._validate_key(key="max", validator=FloatValidator).value + self._max = self._validate_key(key="max", validator=self._float_validator) self._min = self._validate_key_if_present( - key="min", validator=FloatValidator, default=0.0 - ).value + key="min", validator=self._float_validator, default=0.0 + ) - if self._min < 0: - raise self._validation_exception("min must be greater than zero") + def _randomized_float(self, overrides: Overrides, entry: Optional[Entry] = None) -> float: + actualized_min = overrides.apply_formatter(self._min, entry=entry, expected_type=float) + actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) - if self._max < self._min: + if actualized_min < 0: raise self._validation_exception( - f"max ({self._max}) must be greater than or equal to min ({self._min})" + f"min must be greater than zero, received {actualized_min}" + ) + if actualized_max < actualized_min: + raise self._validation_exception( + f"max ({actualized_max}) must be greater than or equal to min ({actualized_min})" ) - def randomized_float(self) -> float: - """ - Returns - ------- - A random float within the range - """ - return random.uniform(self._min, self._max) + return random.uniform(actualized_min, actualized_max) - def randomized_int(self) -> int: + def _randomized_int(self, overrides: Overrides, entry: Optional[Entry] = None) -> int: """ Returns ------- A random float within the range, then cast to an integer (floored) """ - return int(self.randomized_float()) + return int(self._randomized_float(overrides, entry=entry)) + + def _max_value(self, overrides: Overrides, entry: Optional[Entry] = None) -> float: + """ + Returns + ------- + Max possible value + """ + actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) + if actualized_max < 0: + raise self._validation_exception( + f"max must be greater than zero, received {actualized_max}" + ) + return actualized_max + + +class RandomizedRangeValidator(_RandomizedRangeValidator): + """ + Validator to specify a float range between [min, max) with both + override and entry variable support. + """ + + _float_validator = FloatFormatterValidator + + def randomized_float(self, overrides: Overrides, entry: Entry) -> float: + """ + Returns + ------- + A random float within the range + """ + return self._randomized_float(overrides=overrides, entry=entry) + + def randomized_int(self, overrides: Overrides, entry: Entry) -> int: + """ + Returns + ------- + A random float within the range, then cast to an integer (floored) + """ + return self._randomized_int(overrides=overrides, entry=entry) + + def max_value(self, overrides: Overrides, entry: Entry) -> float: + """ + Returns + ------- + Max possible value + """ + return self._max_value(overrides=overrides, entry=entry) + + +class RandomizedRangeOverridesValidator(_RandomizedRangeValidator): + """ + Validator to specify a float range between [min, max) with + static variable support. + """ + + _float_validator = OverridesFloatFormatterValidator + + def randomized_float(self, overrides: Overrides) -> float: + """ + Returns + ------- + A random float within the range + """ + return self._randomized_float(overrides=overrides) + + def randomized_int(self, overrides: Overrides) -> int: + """ + Returns + ------- + A random float within the range, then cast to an integer (floored) + """ + return self._randomized_int(overrides=overrides) + + def max_value(self, overrides: Overrides) -> float: + """ + Returns + ------- + Max possible value + """ + return self._max_value(overrides=overrides) class ThrottleProtectionOptions(ToggleableOptionsDictValidator): @@ -63,6 +150,9 @@ class ThrottleProtectionOptions(ToggleableOptionsDictValidator): 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 @@ -70,6 +160,9 @@ class ThrottleProtectionOptions(ToggleableOptionsDictValidator): 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 @@ -84,6 +177,7 @@ class ThrottleProtectionOptions(ToggleableOptionsDictValidator): _optional_keys = { "enable", + "sleep_per_request_s", "sleep_per_download_s", "sleep_per_subscription_s", "max_downloads_per_subscription", @@ -93,19 +187,34 @@ class ThrottleProtectionOptions(ToggleableOptionsDictValidator): def __init__(self, name, value): super().__init__(name, value) + self._sleep_per_request_s = self._validate_key_if_present( + key="sleep_per_request_s", validator=RandomizedRangeOverridesValidator + ) self._sleep_per_download_s = self._validate_key_if_present( key="sleep_per_download_s", validator=RandomizedRangeValidator ) self._sleep_per_subscription_s = self._validate_key_if_present( - key="sleep_per_subscription_s", validator=RandomizedRangeValidator + key="sleep_per_subscription_s", validator=RandomizedRangeOverridesValidator ) self._max_downloads_per_subscription = self._validate_key_if_present( - key="max_downloads_per_subscription", validator=RandomizedRangeValidator + key="max_downloads_per_subscription", validator=RandomizedRangeOverridesValidator ) self._subscription_download_probability = self._validate_key_if_present( key="subscription_download_probability", validator=ProbabilityValidator ) + @property + def sleep_per_request_s(self) -> Optional[RandomizedRangeValidator]: + """ + :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. + """ + return self._sleep_per_request_s + @property def sleep_per_download_s(self) -> Optional[RandomizedRangeValidator]: """ @@ -149,6 +258,13 @@ class ThrottleProtectionOptions(ToggleableOptionsDictValidator): class ThrottleProtectionPlugin(Plugin[ThrottleProtectionOptions]): plugin_options_type = ThrottleProtectionOptions + @classmethod + def perform_sleep(cls, sleep_time: float) -> None: + """ + Wrapper to be able to mock + """ + time.sleep(sleep_time) + def __init__( self, options: ThrottleProtectionOptions, @@ -159,12 +275,27 @@ class ThrottleProtectionPlugin(Plugin[ThrottleProtectionOptions]): self._subscription_download_counter: int = 0 self._subscription_max_downloads: Optional[int] = None + # Compute this during post-processing using entry metadata. + # Apply the sleep post-completion. + self._entry_sleep_time: Optional[float] = None + # If subscriptions have a max download limit, set it here for the first subscription if self.plugin_options.max_downloads_per_subscription: self._subscription_max_downloads = ( - self.plugin_options.max_downloads_per_subscription.randomized_int() + self.plugin_options.max_downloads_per_subscription.randomized_int( + overrides=self.overrides + ) ) + def ytdl_options(self) -> Optional[Dict]: + if self.plugin_options.sleep_per_request_s is not None: + return { + "sleep_interval_requests": self.plugin_options.sleep_per_request_s.max_value( + overrides=self.overrides + ) + } + return {} + def initialize_subscription(self) -> bool: if self.plugin_options.subscription_download_probability: proba = self.plugin_options.subscription_download_probability.value @@ -198,7 +329,7 @@ class ThrottleProtectionPlugin(Plugin[ThrottleProtectionOptions]): self._subscription_max_downloads is not None and self._subscription_download_counter == 0 ): - logger.debug( + logger.info( "Setting subscription max downloads to %d", self._subscription_max_downloads ) @@ -206,12 +337,19 @@ class ThrottleProtectionPlugin(Plugin[ThrottleProtectionOptions]): self._subscription_download_counter += 1 if self.plugin_options.sleep_per_download_s: - sleep_time = self.plugin_options.sleep_per_download_s.randomized_float() - logger.debug("Sleeping between downloads for %0.2f seconds", sleep_time) - time.sleep(sleep_time) + self._entry_sleep_time = self.plugin_options.sleep_per_download_s.randomized_float( + overrides=self.overrides, entry=entry + ) return None + def post_completion_entry(self, file_metadata: FileMetadata) -> None: + if self._entry_sleep_time: + # pylint: disable=logging-fstring-interpolation) + # needed to test logs in unit test + logger.info(f"Sleeping between downloads for {self._entry_sleep_time:.2f} seconds") + self.perform_sleep(self._entry_sleep_time) + def post_process_subscription(self): # Reset counter to 0 for the next subscription self._subscription_download_counter = 0 @@ -219,10 +357,14 @@ class ThrottleProtectionPlugin(Plugin[ThrottleProtectionOptions]): # If present, reset max downloads for the next subscription if self.plugin_options.max_downloads_per_subscription: self._subscription_max_downloads = ( - self.plugin_options.max_downloads_per_subscription.randomized_int + self.plugin_options.max_downloads_per_subscription.randomized_int( + overrides=self.overrides + ) ) if self.plugin_options.sleep_per_subscription_s: - sleep_time = self.plugin_options.sleep_per_subscription_s.randomized_float() - logger.debug("Sleeping between subscriptions for %0.2f seconds", sleep_time) - time.sleep(sleep_time) + sleep_time = self.plugin_options.sleep_per_subscription_s.randomized_float( + overrides=self.overrides + ) + logger.info("Sleeping between subscriptions for %0.2f seconds", sleep_time) + self.perform_sleep(sleep_time) diff --git a/src/ytdl_sub/plugins/video_tags.py b/src/ytdl_sub/plugins/video_tags.py index 16b7d949..00c5d460 100644 --- a/src/ytdl_sub/plugins/video_tags.py +++ b/src/ytdl_sub/plugins/video_tags.py @@ -34,7 +34,7 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]): Tags the entry's audio file using values defined in the metadata options """ tags_to_write: Dict[str, str] = {} - for tag_name, tag_formatter in self.plugin_options.dict.items(): + for tag_name, tag_formatter in sorted(self.plugin_options.dict.items()): tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) tags_to_write[tag_name] = tag_value diff --git a/src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml b/src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml index 676862a3..0bf420c3 100644 --- a/src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml +++ b/src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml @@ -11,8 +11,7 @@ presets: # Set the default date_range to 2 months overrides: - date_range: "2months" # keep for legacy-reasons - only_recent_date_range: "{date_range}" + only_recent_date_range: "2months" ############################################################################# # Only Recent diff --git a/src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml b/src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml new file mode 100644 index 00000000..0f8b1651 --- /dev/null +++ b/src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml @@ -0,0 +1,34 @@ +presets: + + ############################################################################# + # Filter Duration + # Include or exclude media based on its play time duration + Filter Duration: + overrides: + filter_duration_min_s: 0 + filter_duration_max_s: 4294967296 + + "%filter_duration_ensure_numeric": >- + { + %assert_then( + %is_numeric($0), + $0, + "filter_duration args must be numeric" + ) + } + + filter_duration_zero_msg: "Duration metadata for {title} is missing, cannot perform filter." + + "%filter_duration_eval": >- + { + %if( + %eq(duration, 0), + %print(filter_duration_zero_msg, False) + $0 + ) + } + + + filter_exclude: + - "{ %filter_duration_eval( %lt(duration, %filter_duration_ensure_numeric(filter_duration_min_s)) ) }" + - "{ %filter_duration_eval( %gt(duration, %filter_duration_ensure_numeric(filter_duration_max_s)) ) }" diff --git a/src/ytdl_sub/prebuilt_presets/helpers/filtering.yaml b/src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml similarity index 100% rename from src/ytdl_sub/prebuilt_presets/helpers/filtering.yaml rename to src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml diff --git a/src/ytdl_sub/prebuilt_presets/helpers/throttle_protection.yaml b/src/ytdl_sub/prebuilt_presets/helpers/throttle_protection.yaml new file mode 100644 index 00000000..1194e004 --- /dev/null +++ b/src/ytdl_sub/prebuilt_presets/helpers/throttle_protection.yaml @@ -0,0 +1,84 @@ +presets: + + _throttle_protection: + throttle_protection: + enable: >- + { + %print( + %if( + enable_throttle_protection, + "Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`", + "Throttle protection is disabled. Use at your own risk!" + ), + enable_throttle_protection + ) + } + sleep_per_request_s: + # yt-dlp processes eath request synchronously, so it's intrinsic delay between + # requests already represent a maximum close to real app/client behavior in the + # field. Add a small additional margin matching the example values from yt-dlp + # to be conservative: + max: 0.75 + # Not used at time of writing, but choose a value that would mimic real + # app/client behavior in the field. The next request is usually sent right away + # if processing the previous request is done asynchronously: + min: 0.0 + sleep_per_download_s: + min: 13.8 + max: 28.4 + sleep_per_subscription_s: + min: 16.3 + max: 26.1 + overrides: + enable_throttle_protection: True + + ##### Resolution Assert + enable_resolution_assert: True + resolution_assert_height_gte: 361 + resolution_assert_print: >- + { + %print( + %if( + enable_resolution_assert, + "Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`", + "Resolution assert is disabled. Use at your own risk!" + ), + enable_resolution_assert, + -1 + ) + } + # list variable that contains partial titles to ignore + resolution_assert_ignore_titles: "{ [] }" + resolution_assert_is_ignored: >- + { + %print_if_true( + %concat(title, " has a match in resolution_assert_ignore_titles, skipping resolution assert."), + %contains_any(title, resolution_assert_ignore_titles) + ) + } + + resolution_readable: "{width}x{height}" + # height is not a guaranteed populated variable, do not assert if it's missing (which defaults to 0) + resolution_assert: >- + { + %if( + %and( + enable_resolution_assert, + %ne( height, 0 ), + %not(resolution_assert_is_ignored) + ), + %assert( + %gte( height, resolution_assert_height_gte ), + %concat( + "Entry ", + title, + " downloaded at a low resolution (", + resolution_readable, + "), you've probably been throttled. ", + "Stopping further downloads, wait a few hours and try again. ", + "Disable using the override variable `enable_resolution_assert: False`." + ) + ), + "false is no-op" + ) + } diff --git a/src/ytdl_sub/prebuilt_presets/helpers/url.yaml b/src/ytdl_sub/prebuilt_presets/helpers/url.yaml index 2f7d8f37..7bd4d3f5 100644 --- a/src/ytdl_sub/prebuilt_presets/helpers/url.yaml +++ b/src/ytdl_sub/prebuilt_presets/helpers/url.yaml @@ -14,7 +14,7 @@ presets: # _multi_url: download: - - url: "{url}" + - url: "{ %array_at(urls, 0) }" playlist_thumbnails: - name: "{avatar_uncropped_thumbnail_file_name}" uid: "avatar_uncropped" @@ -26,411 +26,135 @@ presets: - name: "{banner_uncropped_thumbnail_file_name}" uid: "banner_uncropped" include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url2}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url3}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url4}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url5}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url6}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url7}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url8}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url9}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url10}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url11}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url12}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url13}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url14}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url15}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url16}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url17}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url18}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url19}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url20}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url21}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url22}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url23}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url24}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url25}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url26}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url27}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url28}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url29}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url30}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url31}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url32}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url33}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url34}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url35}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url36}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url37}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url38}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url39}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url40}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url41}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url42}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url43}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url44}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url45}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url46}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url47}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url48}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url49}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url50}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url51}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url52}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url53}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url54}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url55}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url56}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url57}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url58}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url59}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url60}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url61}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url62}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url63}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url64}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url65}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url66}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url67}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url68}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url69}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url70}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url71}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url72}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url73}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url74}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url75}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url76}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url77}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url78}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url79}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url80}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url81}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url82}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url83}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url84}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url85}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url86}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url87}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url88}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url89}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url90}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url91}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url92}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url93}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url94}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url95}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url96}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url97}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url98}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url99}" - include_sibling_metadata: "{include_sibling_metadata}" - - url: "{url100}" + webpage_url: "{modified_webpage_url}" + + - url: "{ %array_slice(urls, 1) }" include_sibling_metadata: "{include_sibling_metadata}" + webpage_url: "{modified_webpage_url}" overrides: avatar_uncropped_thumbnail_file_name: "" banner_uncropped_thumbnail_file_name: "" include_sibling_metadata: False + modified_webpage_url: "{webpage_url}" + + subscription_array: >- + { + [ + url, url2, url3, url4, url5, url6, url7, url8, url9, url10, + url11, url12, url13, url14, url15, url16, url17, url18, url19, url20, + url21, url22, url23, url24, url25, url26, url27, url28, url29, url30, + url31, url32, url33, url34, url35, url36, url37, url38, url39, url40, + url41, url42, url43, url44, url45, url46, url47, url48, url49, url50, + url51, url52, url53, url54, url55, url56, url57, url58, url59, url60, + url61, url62, url63, url64, url65, url66, url67, url68, url69, url70, + url71, url72, url73, url74, url75, url76, url77, url78, url79, url80, + url81, url82, url83, url84, url85, url86, url87, url88, url89, url90, + url91, url92, url93, url94, url95, url96, url97, url98, url99, url100 + ] + } + urls: "{subscription_array}" - subscription_array: "{ [] }" subscription_value: "" - subscription_value_2: "" - subscription_value_3: "" - subscription_value_4: "" - subscription_value_5: "" - subscription_value_6: "" - subscription_value_7: "" - subscription_value_8: "" - subscription_value_9: "" - subscription_value_10: "" - subscription_value_11: "" - subscription_value_12: "" - subscription_value_13: "" - subscription_value_14: "" - subscription_value_15: "" - subscription_value_16: "" - subscription_value_17: "" - subscription_value_18: "" - subscription_value_19: "" - subscription_value_20: "" - subscription_value_21: "" - subscription_value_22: "" - subscription_value_23: "" - subscription_value_24: "" - subscription_value_25: "" - subscription_value_26: "" - subscription_value_27: "" - subscription_value_28: "" - subscription_value_29: "" - subscription_value_30: "" - subscription_value_31: "" - subscription_value_32: "" - subscription_value_33: "" - subscription_value_34: "" - subscription_value_35: "" - subscription_value_36: "" - subscription_value_37: "" - subscription_value_38: "" - subscription_value_39: "" - subscription_value_40: "" - subscription_value_41: "" - subscription_value_42: "" - subscription_value_43: "" - subscription_value_44: "" - subscription_value_45: "" - subscription_value_46: "" - subscription_value_47: "" - subscription_value_48: "" - subscription_value_49: "" - subscription_value_50: "" - subscription_value_51: "" - subscription_value_52: "" - subscription_value_53: "" - subscription_value_54: "" - subscription_value_55: "" - subscription_value_56: "" - subscription_value_57: "" - subscription_value_58: "" - subscription_value_59: "" - subscription_value_60: "" - subscription_value_61: "" - subscription_value_62: "" - subscription_value_63: "" - subscription_value_64: "" - subscription_value_65: "" - subscription_value_66: "" - subscription_value_67: "" - subscription_value_68: "" - subscription_value_69: "" - subscription_value_70: "" - subscription_value_71: "" - subscription_value_72: "" - subscription_value_73: "" - subscription_value_74: "" - subscription_value_75: "" - subscription_value_76: "" - subscription_value_77: "" - subscription_value_78: "" - subscription_value_79: "" - subscription_value_80: "" - subscription_value_81: "" - subscription_value_82: "" - subscription_value_83: "" - subscription_value_84: "" - subscription_value_85: "" - subscription_value_86: "" - subscription_value_87: "" - subscription_value_88: "" - subscription_value_89: "" - subscription_value_90: "" - subscription_value_91: "" - subscription_value_92: "" - subscription_value_93: "" - subscription_value_94: "" - subscription_value_95: "" - subscription_value_96: "" - subscription_value_97: "" - subscription_value_98: "" - subscription_value_99: "" - subscription_value_100: "" - url: "{subscription_value}" - url2: "{subscription_value_2}" - url3: "{subscription_value_3}" - url4: "{subscription_value_4}" - url5: "{subscription_value_5}" - url6: "{subscription_value_6}" - url7: "{subscription_value_7}" - url8: "{subscription_value_8}" - url9: "{subscription_value_9}" - url10: "{subscription_value_10}" - url11: "{subscription_value_11}" - url12: "{subscription_value_12}" - url13: "{subscription_value_13}" - url14: "{subscription_value_14}" - url15: "{subscription_value_15}" - url16: "{subscription_value_16}" - url17: "{subscription_value_17}" - url18: "{subscription_value_18}" - url19: "{subscription_value_19}" - url20: "{subscription_value_20}" - url21: "{subscription_value_21}" - url22: "{subscription_value_22}" - url23: "{subscription_value_23}" - url24: "{subscription_value_24}" - url25: "{subscription_value_25}" - url26: "{subscription_value_26}" - url27: "{subscription_value_27}" - url28: "{subscription_value_28}" - url29: "{subscription_value_29}" - url30: "{subscription_value_30}" - url31: "{subscription_value_31}" - url32: "{subscription_value_32}" - url33: "{subscription_value_33}" - url34: "{subscription_value_34}" - url35: "{subscription_value_35}" - url36: "{subscription_value_36}" - url37: "{subscription_value_37}" - url38: "{subscription_value_38}" - url39: "{subscription_value_39}" - url40: "{subscription_value_40}" - url41: "{subscription_value_41}" - url42: "{subscription_value_42}" - url43: "{subscription_value_43}" - url44: "{subscription_value_44}" - url45: "{subscription_value_45}" - url46: "{subscription_value_46}" - url47: "{subscription_value_47}" - url48: "{subscription_value_48}" - url49: "{subscription_value_49}" - url50: "{subscription_value_50}" - url51: "{subscription_value_51}" - url52: "{subscription_value_52}" - url53: "{subscription_value_53}" - url54: "{subscription_value_54}" - url55: "{subscription_value_55}" - url56: "{subscription_value_56}" - url57: "{subscription_value_57}" - url58: "{subscription_value_58}" - url59: "{subscription_value_59}" - url60: "{subscription_value_60}" - url61: "{subscription_value_61}" - url62: "{subscription_value_62}" - url63: "{subscription_value_63}" - url64: "{subscription_value_64}" - url65: "{subscription_value_65}" - url66: "{subscription_value_66}" - url67: "{subscription_value_67}" - url68: "{subscription_value_68}" - url69: "{subscription_value_69}" - url70: "{subscription_value_70}" - url71: "{subscription_value_71}" - url72: "{subscription_value_72}" - url73: "{subscription_value_73}" - url74: "{subscription_value_74}" - url75: "{subscription_value_75}" - url76: "{subscription_value_76}" - url77: "{subscription_value_77}" - url78: "{subscription_value_78}" - url79: "{subscription_value_79}" - url80: "{subscription_value_80}" - url81: "{subscription_value_81}" - url82: "{subscription_value_82}" - url83: "{subscription_value_83}" - url84: "{subscription_value_84}" - url85: "{subscription_value_85}" - url86: "{subscription_value_86}" - url87: "{subscription_value_87}" - url88: "{subscription_value_88}" - url89: "{subscription_value_89}" - url90: "{subscription_value_90}" - url91: "{subscription_value_91}" - url92: "{subscription_value_92}" - url93: "{subscription_value_93}" - url94: "{subscription_value_94}" - url95: "{subscription_value_95}" - url96: "{subscription_value_96}" - url97: "{subscription_value_97}" - url98: "{subscription_value_98}" - url99: "{subscription_value_99}" - url100: "{subscription_value_100}" + url2: "" + url3: "" + url4: "" + url5: "" + url6: "" + url7: "" + url8: "" + url9: "" + url10: "" + url11: "" + url12: "" + url13: "" + url14: "" + url15: "" + url16: "" + url17: "" + url18: "" + url19: "" + url20: "" + url21: "" + url22: "" + url23: "" + url24: "" + url25: "" + url26: "" + url27: "" + url28: "" + url29: "" + url30: "" + url31: "" + url32: "" + url33: "" + url34: "" + url35: "" + url36: "" + url37: "" + url38: "" + url39: "" + url40: "" + url41: "" + url42: "" + url43: "" + url44: "" + url45: "" + url46: "" + url47: "" + url48: "" + url49: "" + url50: "" + url51: "" + url52: "" + url53: "" + url54: "" + url55: "" + url56: "" + url57: "" + url58: "" + url59: "" + url60: "" + url61: "" + url62: "" + url63: "" + url64: "" + url65: "" + url66: "" + url67: "" + url68: "" + url69: "" + url70: "" + url71: "" + url72: "" + url73: "" + url74: "" + url75: "" + url76: "" + url77: "" + url78: "" + url79: "" + url80: "" + url81: "" + url82: "" + url83: "" + url84: "" + url85: "" + url86: "" + url87: "" + url88: "" + url89: "" + url90: "" + url91: "" + url92: "" + url93: "" + url94: "" + url95: "" + url96: "" + url97: "" + url98: "" + url99: "" + url100: "" # multi-url with bilateral scraping built into it via @@ -441,406 +165,11 @@ presets: - "_url_bilateral_overrides" download: - - url: "{%bilateral_url(url) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url2) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url3) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url4) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url5) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url6) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url7) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url8) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url9) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url10) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url11) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url12) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url13) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url14) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url15) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url16) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url17) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url18) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url19) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url20) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url21) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url22) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url23) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url24) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url25) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url26) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url27) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url28) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url29) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url30) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url31) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url32) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url33) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url34) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url35) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url36) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url37) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url38) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url39) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url40) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url41) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url42) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url43) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url44) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url45) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url46) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url47) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url48) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url49) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url50) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url51) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url52) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url53) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url54) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url55) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url56) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url57) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url58) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url59) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url60) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url61) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url62) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url63) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url64) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url65) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url66) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url67) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url68) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url69) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url70) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url71) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url72) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url73) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url74) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url75) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url76) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url77) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url78) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url79) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url80) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url81) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url82) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url83) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url84) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url85) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url86) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url87) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url88) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url89) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url90) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url91) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url92) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url93) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url94) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url95) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url96) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url97) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url98) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(url99) }" - download_reverse: False - ytdl_options: - playlist_items: "-1:0:-1" - - url: "{%bilateral_url(url100) }" + - url: "{ %array_apply(urls, %bilateral_url) }" download_reverse: False ytdl_options: playlist_items: "-1:0:-1" + webpage_url: "{modified_webpage_url}" _multi_url_bilateral: preset: diff --git a/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml b/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml index b212f640..80e041a7 100644 --- a/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml +++ b/src/ytdl_sub/prebuilt_presets/helpers/url_categorized.yaml @@ -108,103 +108,4 @@ presets: } subscription_map: "{ {} }" - url: "{ %get_url_i(1) }" - url2: "{ %get_url_i(2) }" - url3: "{ %get_url_i(3) }" - url4: "{ %get_url_i(4) }" - url5: "{ %get_url_i(5) }" - url6: "{ %get_url_i(6) }" - url7: "{ %get_url_i(7) }" - url8: "{ %get_url_i(8) }" - url9: "{ %get_url_i(9) }" - url10: "{ %get_url_i(10) }" - url11: "{ %get_url_i(11) }" - url12: "{ %get_url_i(12) }" - url13: "{ %get_url_i(13) }" - url14: "{ %get_url_i(14) }" - url15: "{ %get_url_i(15) }" - url16: "{ %get_url_i(16) }" - url17: "{ %get_url_i(17) }" - url18: "{ %get_url_i(18) }" - url19: "{ %get_url_i(19) }" - url20: "{ %get_url_i(20) }" - url21: "{ %get_url_i(21) }" - url22: "{ %get_url_i(22) }" - url23: "{ %get_url_i(23) }" - url24: "{ %get_url_i(24) }" - url25: "{ %get_url_i(25) }" - url26: "{ %get_url_i(26) }" - url27: "{ %get_url_i(27) }" - url28: "{ %get_url_i(28) }" - url29: "{ %get_url_i(29) }" - url30: "{ %get_url_i(30) }" - url31: "{ %get_url_i(31) }" - url32: "{ %get_url_i(32) }" - url33: "{ %get_url_i(33) }" - url34: "{ %get_url_i(34) }" - url35: "{ %get_url_i(35) }" - url36: "{ %get_url_i(36) }" - url37: "{ %get_url_i(37) }" - url38: "{ %get_url_i(38) }" - url39: "{ %get_url_i(39) }" - url40: "{ %get_url_i(40) }" - url41: "{ %get_url_i(41) }" - url42: "{ %get_url_i(42) }" - url43: "{ %get_url_i(43) }" - url44: "{ %get_url_i(44) }" - url45: "{ %get_url_i(45) }" - url46: "{ %get_url_i(46) }" - url47: "{ %get_url_i(47) }" - url48: "{ %get_url_i(48) }" - url49: "{ %get_url_i(49) }" - url50: "{ %get_url_i(50) }" - url51: "{ %get_url_i(51) }" - url52: "{ %get_url_i(52) }" - url53: "{ %get_url_i(53) }" - url54: "{ %get_url_i(54) }" - url55: "{ %get_url_i(55) }" - url56: "{ %get_url_i(56) }" - url57: "{ %get_url_i(57) }" - url58: "{ %get_url_i(58) }" - url59: "{ %get_url_i(59) }" - url60: "{ %get_url_i(60) }" - url61: "{ %get_url_i(61) }" - url62: "{ %get_url_i(62) }" - url63: "{ %get_url_i(63) }" - url64: "{ %get_url_i(64) }" - url65: "{ %get_url_i(65) }" - url66: "{ %get_url_i(66) }" - url67: "{ %get_url_i(67) }" - url68: "{ %get_url_i(68) }" - url69: "{ %get_url_i(69) }" - url70: "{ %get_url_i(70) }" - url71: "{ %get_url_i(71) }" - url72: "{ %get_url_i(72) }" - url73: "{ %get_url_i(73) }" - url74: "{ %get_url_i(74) }" - url75: "{ %get_url_i(75) }" - url76: "{ %get_url_i(76) }" - url77: "{ %get_url_i(77) }" - url78: "{ %get_url_i(78) }" - url79: "{ %get_url_i(79) }" - url80: "{ %get_url_i(80) }" - url81: "{ %get_url_i(81) }" - url82: "{ %get_url_i(82) }" - url83: "{ %get_url_i(83) }" - url84: "{ %get_url_i(84) }" - url85: "{ %get_url_i(85) }" - url86: "{ %get_url_i(86) }" - url87: "{ %get_url_i(87) }" - url88: "{ %get_url_i(88) }" - url89: "{ %get_url_i(89) }" - url90: "{ %get_url_i(90) }" - url91: "{ %get_url_i(91) }" - url92: "{ %get_url_i(92) }" - url93: "{ %get_url_i(93) }" - url94: "{ %get_url_i(94) }" - url95: "{ %get_url_i(95) }" - url96: "{ %get_url_i(96) }" - url97: "{ %get_url_i(97) }" - url98: "{ %get_url_i(98) }" - url99: "{ %get_url_i(99) }" - url100: "{ %get_url_i(100) }" \ No newline at end of file + urls: "{ %array_apply(%range(100, 1), %get_url_i) }" \ No newline at end of file diff --git a/src/ytdl_sub/prebuilt_presets/music/albums_from_chapters.yaml b/src/ytdl_sub/prebuilt_presets/music/albums_from_chapters.yaml index 6c9b82ee..5881bfb4 100644 --- a/src/ytdl_sub/prebuilt_presets/music/albums_from_chapters.yaml +++ b/src/ytdl_sub/prebuilt_presets/music/albums_from_chapters.yaml @@ -22,4 +22,5 @@ presets: "YouTube Full Albums": preset: - - "_albums_from_chapters" \ No newline at end of file + - "_albums_from_chapters" + - "_throttle_protection" \ No newline at end of file diff --git a/src/ytdl_sub/prebuilt_presets/music/albums_from_playlists.yaml b/src/ytdl_sub/prebuilt_presets/music/albums_from_playlists.yaml index 22839279..7d847d27 100644 --- a/src/ytdl_sub/prebuilt_presets/music/albums_from_playlists.yaml +++ b/src/ytdl_sub/prebuilt_presets/music/albums_from_playlists.yaml @@ -18,6 +18,7 @@ presets: "YouTube Releases": preset: - "_albums_from_playlists" + - "_throttle_protection" "Bandcamp": preset: diff --git a/src/ytdl_sub/prebuilt_presets/music/singles.yaml b/src/ytdl_sub/prebuilt_presets/music/singles.yaml index dc7b5a25..04a9a031 100644 --- a/src/ytdl_sub/prebuilt_presets/music/singles.yaml +++ b/src/ytdl_sub/prebuilt_presets/music/singles.yaml @@ -2,6 +2,7 @@ presets: _music_base: + output_options: output_directory: "{music_directory}" file_name: "{track_full_path}" diff --git a/src/ytdl_sub/prebuilt_presets/music/other_websites.yaml b/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml similarity index 97% rename from src/ytdl_sub/prebuilt_presets/music/other_websites.yaml rename to src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml index 32eb964a..966be5f8 100644 --- a/src/ytdl_sub/prebuilt_presets/music/other_websites.yaml +++ b/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml @@ -8,7 +8,7 @@ presets: urls: # The first URL will be all the artist's tracks. # Treat these as singles - an album with a single track - - url: "{url}/tracks" + - url: "{url}" include_sibling_metadata: False variables: sc_track_album: "{title}" diff --git a/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py b/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py index ae128998..76b18443 100644 --- a/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py @@ -8,6 +8,7 @@ class MusicVideoPresets(PrebuiltPresets): preset_names = { "Kodi Music Videos", "Jellyfin Music Videos", + "Emby Music Videos", "Plex Music Videos", } diff --git a/src/ytdl_sub/prebuilt_presets/music_videos/music_video_base.yaml b/src/ytdl_sub/prebuilt_presets/music_videos/music_video_base.yaml index 25b97697..a0c93c4b 100644 --- a/src/ytdl_sub/prebuilt_presets/music_videos/music_video_base.yaml +++ b/src/ytdl_sub/prebuilt_presets/music_videos/music_video_base.yaml @@ -2,6 +2,9 @@ presets: _music_video_base: preset: + # ytdl-sub includes throttle prootection by default or all prebuilt presets. + # Can be disabled with override variable `enable_throttle_protection: False` + - "_throttle_protection" - "_url_categorized" output_options: diff --git a/src/ytdl_sub/prebuilt_presets/music_videos/music_videos.yaml b/src/ytdl_sub/prebuilt_presets/music_videos/music_videos.yaml index dcacd330..4860a2b8 100644 --- a/src/ytdl_sub/prebuilt_presets/music_videos/music_videos.yaml +++ b/src/ytdl_sub/prebuilt_presets/music_videos/music_videos.yaml @@ -7,6 +7,9 @@ presets: - "_music_video_nfo" - "_music_video_tags" + "Emby Music Videos": + preset: "Jellyfin Music Videos" + "Kodi Music Videos": preset: - "_kodi_base" diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py b/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py index b2e0df4f..0e10c448 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py @@ -8,64 +8,15 @@ class TvShowByDatePresets(PrebuiltPresets): preset_names = { "Kodi TV Show by Date", "Jellyfin TV Show by Date", + "Emby TV Show by Date", "Plex TV Show by Date", } -class TvShowByDateOldPresets(PrebuiltPresets): - """ - TV Show by Date presets create a TV show from a single URL using upload dates as season/episode - numbers. - """ - - preset_names = { - "kodi_tv_show_by_date", - "jellyfin_tv_show_by_date", - "plex_tv_show_by_date", - } - - -class TvShowByDateEpisodeFormattingPresets(PrebuiltPresets): - preset_names = { - "season_by_year__episode_by_month_day", - "season_by_year__episode_by_month_day_reversed", - "season_by_year_month__episode_by_day", - "season_by_year__episode_by_download_index", - } - - class TvShowCollectionPresets(PrebuiltPresets): preset_names = { "Kodi TV Show Collection", "Jellyfin TV Show Collection", + "Emby TV Show Collection", "Plex TV Show Collection", } - - -class TvShowCollectionOldPresets(PrebuiltPresets): - preset_names = { - "kodi_tv_show_collection", - "jellyfin_tv_show_collection", - "plex_tv_show_collection", - } - - -class TvShowCollectionEpisodeFormattingPresets(PrebuiltPresets): - preset_names = { - "season_by_collection__episode_by_year_month_day", - "season_by_collection__episode_by_year_month_day_reversed", - "season_by_collection__episode_by_playlist_index", - "season_by_collection__episode_by_playlist_index_reversed", - } - - -class TvShowCollectionSeasonPresets(PrebuiltPresets): - """Now Deprecated""" - - preset_names = { - "collection_season_1", - "collection_season_2", - "collection_season_3", - "collection_season_4", - "collection_season_5", - } diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml index b6bacd27..de02cfbb 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml @@ -3,12 +3,18 @@ presets: #################################################################################################### _episode_base: + preset: + # ytdl-sub includes throttle prootection by default or all prebuilt presets. + # Can be disabled with override variable `enable_throttle_protection: False` + - "_throttle_protection" + output_options: output_directory: "{tv_show_directory}/{tv_show_name_sanitized}" file_name: "{episode_file_path}.{ext}" thumbnail_name: "{thumbnail_file_name}" info_json_name: "{episode_file_path}.{info_json_ext}" maintain_download_archive: True + keep_files_date_eval: "{episode_date_standardized}" ytdl_options: break_on_existing: True @@ -16,6 +22,9 @@ presets: chapters: embed_chapters: True + date_range: + type: "{tv_show_date_range_type}" + overrides: # MUST DEFINE: # tv_show_directory @@ -36,14 +45,15 @@ presets: tv_show_genre: "{subscription_indent_1}" tv_show_content_rating: "{subscription_indent_2}" season_directory_name: "Season {season_number_padded}" - episode_title: "{upload_date_standardized} - {title}" + episode_title: "{episode_date_standardized} - {title}" episode_plot: "{webpage_url}\n\n{description}" - episode_year: "{upload_year}" episode_content_rating: "{tv_show_content_rating}" episode_date_standardized: "{upload_date_standardized}" episode_file_name: "s{season_number_padded}.e{episode_number_padded} - {file_title}" episode_file_path: "{season_directory_name_sanitized}/{episode_file_name_sanitized}" thumbnail_file_name: "{episode_file_path}-thumb.jpg" + episode_year: "{%slice(episode_date_standardized, 0, 4)}" - - + # Used to determine which date type to use if `Only Recent` preset + # is applied + tv_show_date_range_type: "upload_date" diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml index e676ec82..7f5fe531 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_by_date.yaml @@ -31,26 +31,141 @@ presets: - "plex_tv_show_by_date" - "season_by_year__episode_by_month_day" + "Emby TV Show by Date": + preset: "Jellyfin TV Show by Date" + #################################################################################################### # TV show from one or more sources. Uses {url}'s avatar and banner as poster and fanart _tv_show_by_date: - preset: "_multi_url_bilateral" + preset: + - "_multi_url_bilateral" + - "_tv_show_by_date_asserts" overrides: avatar_uncropped_thumbnail_file_name: "{tv_show_poster_file_name}" banner_uncropped_thumbnail_file_name: "{tv_show_fanart_file_name}" + _tv_show_by_date_asserts: + overrides: + s01_url: "" + s01_name: "" + assert_not_collection: >- + { + %assert( + %and( + %not( %bool(s01_url) ), + %not( %bool(s01_name) ) + ), + "Provided `s01_url` or `s01_name` variable to TV Show by Date preset when it expects `url`. Perhaps you meant to use the `TV Show Collection` preset?" + ) + } + #################################################################################################### + _tv_show_by_date_season_episode_ordering: + overrides: + tv_show_by_date_season_ordering: >- + { %throw("Must specify tv_show_by_date_season_ordering") } + + "%season_ordering_": >- + { %eq( %lower(tv_show_by_date_season_ordering), $0 ) } + + season_number: >- + { + %elif( + %season_ordering_( "upload-year" ), upload_year, + %season_ordering_( "upload-year-month" ), %concat(upload_year, upload_month_padded), + %season_ordering_( "release-year" ), release_year, + %season_ordering_( "release-year-month" ), %concat(release_year, release_month_padded), + %throw( + 'tv_show_by_date_season_ordering must be one of the following: "upload-year", "upload-year-month", "release-year", "release-year-month"' + ) + ) + } + + # both year and year-month are fixed width, until the year 10000 :) + season_number_padded: "{season_number}" + + ##################################### episode overrides + + tv_show_by_date_episode_ordering: >- + { %throw("Must specify tv_show_by_date_episode_ordering") } + + "%episode_ordering_": >- + { %eq( %lower(tv_show_by_date_episode_ordering), $0 ) } + + episode_number_and_padded_: >- + { + %elif( + %episode_ordering_( "upload-day" ), [ %concat(upload_day, upload_date_index_padded), 4 ], + %episode_ordering_( "upload-month-day" ), [ %concat(upload_month, upload_day_padded, upload_date_index_padded), 6], + %episode_ordering_( "upload-month-day-reversed" ), [ %concat(upload_day_of_year_reversed, upload_date_index_reversed_padded), 5], + %episode_ordering_( "release-day" ), [ %concat(release_day, upload_date_index_padded), 4 ], + %episode_ordering_( "release-month-day" ), [ %concat(release_month, release_day_padded, upload_date_index_padded), 6], + %episode_ordering_( "release-month-day-reversed" ), [ %concat(release_day_of_year_reversed, upload_date_index_reversed_padded), 5], + %episode_ordering_( "download-index" ), [ download_index, 6 ], + %throw( + 'tv_show_by_date_episode_ordering must be one of the following: "upload-day", "upload-month-day", "upload-month-day-reversed", "release-day", "release-month-day", "release-month-day-reversed", "download-index"' + ) + ) + } + + episode_number: "{ %array_at(episode_number_and_padded_, 0) }" + episode_number_padded: "{ %pad_zero( %int(episode_number), %int(%array_at(episode_number_and_padded_, 1))) }" + episode_date_standardized: >- + { + %if( + %contains(tv_show_by_date_season_ordering, "release"), + release_date_standardized, + upload_date_standardized + ) + } + + tv_show_date_range_type: >- + { + %if( + %contains(tv_show_by_date_season_ordering, "release"), + "release_date", + "upload_date" + ) + } + + ##################################### season/episode pairing validation + + "%ordering_pair_eq": >- + { + %eq([tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering], [$0, $1]) + } + tv_show_by_date_ordering_pair_validation_: >- + { + %assert_then( + %or( + %ordering_pair_eq("upload-year", "upload-month-day"), + %ordering_pair_eq("upload-year", "upload-month-day-reversed"), + %ordering_pair_eq("upload-year", "download-index"), + %ordering_pair_eq("upload-year-month", "upload-day"), + %ordering_pair_eq("release-year", "release-month-day"), + %ordering_pair_eq("release-year", "release-month-day-reversed"), + %ordering_pair_eq("release-year", "download-index"), + %ordering_pair_eq("release-year-month", "release-day") + ), + episode_number_and_padded_, + "Detected incompatibility between tv_show_by_date_season_ordering and tv_show_by_date_episode_ordering. Ensure you are not using both upload and release date, and that the year/month/day are included in the combined season and episode." + ) + } + + +#################################################################################################### +# LEGACY PRESETS _season_by_year: + preset: "_tv_show_by_date_season_episode_ordering" overrides: - season_number: "{upload_year}" - season_number_padded: "{season_number}" + tv_show_by_date_season_ordering: "upload-year" _season_by_year_month: + preset: "_tv_show_by_date_season_episode_ordering" overrides: - season_number: "{upload_year}{upload_month_padded}" - season_number_padded: "{season_number}" + tv_show_by_date_season_ordering: "upload-year-month" #################################################################################################### @@ -58,26 +173,22 @@ presets: preset: - "_season_by_year_month" overrides: - episode_number: "{upload_day}{upload_date_index_padded}" - episode_number_padded: "{upload_day_padded}{upload_date_index_padded}" + tv_show_by_date_episode_ordering: "upload-day" season_by_year__episode_by_month_day: preset: - "_season_by_year" overrides: - episode_number: "{upload_month}{upload_day_padded}{upload_date_index_padded}" - episode_number_padded: "{upload_month_padded}{upload_day_padded}{upload_date_index_padded}" + tv_show_by_date_episode_ordering: "upload-month-day" season_by_year__episode_by_month_day_reversed: preset: - "_season_by_year" overrides: - episode_number: "{upload_day_of_year_reversed}{upload_date_index_reversed_padded}" - episode_number_padded: "{upload_day_of_year_reversed_padded}{upload_date_index_reversed_padded}" + tv_show_by_date_episode_ordering: "upload-month-day-reversed" season_by_year__episode_by_download_index: preset: - "_season_by_year" overrides: - episode_number: "{download_index}" - episode_number_padded: "{download_index_padded6}" \ No newline at end of file + tv_show_by_date_episode_ordering: "download-index" diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml index 888068d1..c8a78a7a 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml +++ b/src/ytdl_sub/prebuilt_presets/tv_show/tv_show_collection.yaml @@ -18,7 +18,20 @@ presets: - "plex_tv_show_collection" - "season_by_collection__episode_by_year_month_day" + "Emby TV Show Collection": + preset: "Jellyfin TV Show Collection" + + # Emby cannot read seasons from tv_show.nfo + # Create explicit season NFOs in each season dir + static_nfo_tags: + nfo_name: "{season_directory_name_sanitized}/season.nfo" + nfo_root: "season" + tags: + title: "{collection_season_name}" + + ################## + # Legacy names kodi_tv_show_collection: preset: @@ -37,34 +50,81 @@ presets: ############################### - season_by_collection__episode_by_year_month_day: + _tv_show_collection_episode_ordering: overrides: - episode_number: "{upload_year_truncated}{upload_month_padded}{upload_day_padded}{upload_date_index_padded}" - episode_number_padded: "{episode_number}" + tv_show_collection_episode_ordering: >- + { %throw("Must specify tv_show_collection_episode_ordering") } + + "%episode_ordering_": >- + { %eq( %lower(tv_show_collection_episode_ordering), $0 ) } + + episode_number_and_padded_: >- + { + %elif( + %episode_ordering_( "upload-year-month-day" ), [ %concat(upload_year_truncated, upload_month_padded, upload_day_padded, upload_date_index_padded), 8 ], + %episode_ordering_( "upload-year-month-day-reversed" ), [ %concat(upload_year_truncated_reversed, upload_month_reversed_padded, upload_day_reversed_padded, upload_date_index_reversed_padded), 8], + %episode_ordering_( "release-year-month-day" ), [ %concat(release_year_truncated, release_month_padded, release_day_padded, upload_date_index_padded), 8 ], + %episode_ordering_( "release-year-month-day-reversed" ), [ %concat(release_year_truncated_reversed, release_month_reversed_padded, release_day_reversed_padded, upload_date_index_reversed_padded), 8], + %episode_ordering_( "playlist-index" ), [ %concat(playlist_index), 6], + %episode_ordering_( "playlist-index-reversed" ), [ %concat(playlist_index_reversed), 6], + %throw( + 'tv_show_collection_episode_ordering must be one of the following: "upload-year-month-day", "upload-year-month-day-reversed", "release-year-month-day", "release-year-month-day-reversed", "playlist-index", "playlist-index-reversed"' + ) + ) + } + + episode_number: "{ %array_at(episode_number_and_padded_, 0) }" + episode_number_padded: "{ %pad_zero( %int(episode_number), %int(%array_at(episode_number_and_padded_, 1))) }" + episode_date_standardized: >- + { + %if( + %contains(tv_show_collection_episode_ordering, "release"), + release_date_standardized, + upload_date_standardized + ) + } + + tv_show_date_range_type: >- + { + %if( + %contains(tv_show_collection_episode_ordering, "release"), + "release_date", + "upload_date" + ) + } + + + ### LEGACY PRESETS + + season_by_collection__episode_by_year_month_day: + preset: "_tv_show_collection_episode_ordering" + overrides: + tv_show_collection_episode_ordering: "upload-year-month-day" season_by_collection__episode_by_year_month_day_reversed: + preset: "_tv_show_collection_episode_ordering" overrides: - episode_number: "{upload_year_truncated_reversed}{upload_month_reversed_padded}{upload_day_reversed_padded}{upload_date_index_reversed_padded}" - episode_number_padded: "{episode_number}" + tv_show_collection_episode_ordering: "upload-year-month-day-reversed" season_by_collection__episode_by_playlist_index: + preset: "_tv_show_collection_episode_ordering" overrides: - episode_number: "{playlist_index}" - episode_number_padded: "{playlist_index_padded6}" + tv_show_collection_episode_ordering: "playlist-index" season_by_collection__episode_by_playlist_index_reversed: + preset: "_tv_show_collection_episode_ordering" overrides: - episode_number: "{playlist_index_reversed}" - episode_number_padded: "{playlist_index_reversed_padded6}" + tv_show_collection_episode_ordering: "playlist-index-reversed" ############## _tv_show_collection: preset: - "_tv_show_collection_bilateral" + - "_tv_show_collection_asserts" download: - - url: "{collection_season_1_url}" + - url: "{ %array_at( %get_season_urls(collection_season_1_url), 0) }" variables: collection_season_number: "1" collection_season_name: "{collection_season_1_name}" @@ -82,318 +142,493 @@ presets: uid: "avatar_uncropped" - name: "{tv_show_fanart_file_name}" uid: "banner_uncropped" + - url: "{ %array_slice( %get_season_urls(collection_season_1_url), 1) }" + variables: + collection_season_number: "1" + collection_season_name: "{collection_season_1_name}" - - url: "{collection_season_2_url}" + - url: "{ %array_at( %get_season_urls(collection_season_2_url), 0) }" variables: collection_season_number: "2" collection_season_name: "{collection_season_2_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_2_url), 1) }" + variables: + collection_season_number: "2" + collection_season_name: "{collection_season_2_name}" - - url: "{collection_season_3_url}" + - url: "{ %array_at( %get_season_urls(collection_season_3_url), 0) }" variables: collection_season_number: "3" collection_season_name: "{collection_season_3_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_3_url), 1) }" + variables: + collection_season_number: "3" + collection_season_name: "{collection_season_3_name}" - - url: "{collection_season_4_url}" + - url: "{ %array_at( %get_season_urls(collection_season_4_url), 0) }" variables: collection_season_number: "4" collection_season_name: "{collection_season_4_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_4_url), 1) }" + variables: + collection_season_number: "4" + collection_season_name: "{collection_season_4_name}" - - url: "{collection_season_5_url}" + - url: "{ %array_at( %get_season_urls(collection_season_5_url), 0) }" variables: collection_season_number: "5" collection_season_name: "{collection_season_5_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_5_url), 1) }" + variables: + collection_season_number: "5" + collection_season_name: "{collection_season_5_name}" - - url: "{collection_season_6_url}" + - url: "{ %array_at( %get_season_urls(collection_season_6_url), 0) }" variables: collection_season_number: "6" collection_season_name: "{collection_season_6_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_6_url), 1) }" + variables: + collection_season_number: "6" + collection_season_name: "{collection_season_6_name}" - - url: "{collection_season_7_url}" + - url: "{ %array_at( %get_season_urls(collection_season_7_url), 0) }" variables: collection_season_number: "7" collection_season_name: "{collection_season_7_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_7_url), 1) }" + variables: + collection_season_number: "7" + collection_season_name: "{collection_season_7_name}" - - url: "{collection_season_8_url}" + - url: "{ %array_at( %get_season_urls(collection_season_8_url), 0) }" variables: collection_season_number: "8" collection_season_name: "{collection_season_8_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_8_url), 1) }" + variables: + collection_season_number: "8" + collection_season_name: "{collection_season_8_name}" - - url: "{collection_season_9_url}" + - url: "{ %array_at( %get_season_urls(collection_season_9_url), 0) }" variables: collection_season_number: "9" collection_season_name: "{collection_season_9_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_9_url), 1) }" + variables: + collection_season_number: "9" + collection_season_name: "{collection_season_9_name}" - - url: "{collection_season_10_url}" + - url: "{ %array_at( %get_season_urls(collection_season_10_url), 0) }" variables: collection_season_number: "10" collection_season_name: "{collection_season_10_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_10_url), 1) }" + variables: + collection_season_number: "10" + collection_season_name: "{collection_season_10_name}" - - url: "{collection_season_11_url}" + - url: "{ %array_at( %get_season_urls(collection_season_11_url), 0) }" variables: collection_season_number: "11" collection_season_name: "{collection_season_11_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_11_url), 1) }" + variables: + collection_season_number: "11" + collection_season_name: "{collection_season_11_name}" - - url: "{collection_season_12_url}" + - url: "{ %array_at( %get_season_urls(collection_season_12_url), 0) }" variables: collection_season_number: "12" collection_season_name: "{collection_season_12_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_12_url), 1) }" + variables: + collection_season_number: "12" + collection_season_name: "{collection_season_12_name}" - - url: "{collection_season_13_url}" + - url: "{ %array_at( %get_season_urls(collection_season_13_url), 0) }" variables: collection_season_number: "13" collection_season_name: "{collection_season_13_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_13_url), 1) }" + variables: + collection_season_number: "13" + collection_season_name: "{collection_season_13_name}" - - url: "{collection_season_14_url}" + - url: "{ %array_at( %get_season_urls(collection_season_14_url), 0) }" variables: collection_season_number: "14" collection_season_name: "{collection_season_14_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_14_url), 1) }" + variables: + collection_season_number: "14" + collection_season_name: "{collection_season_14_name}" - - url: "{collection_season_15_url}" + - url: "{ %array_at( %get_season_urls(collection_season_15_url), 0) }" variables: collection_season_number: "15" collection_season_name: "{collection_season_15_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_15_url), 1) }" + variables: + collection_season_number: "15" + collection_season_name: "{collection_season_15_name}" - - url: "{collection_season_16_url}" + - url: "{ %array_at( %get_season_urls(collection_season_16_url), 0) }" variables: collection_season_number: "16" collection_season_name: "{collection_season_16_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_16_url), 1) }" + variables: + collection_season_number: "16" + collection_season_name: "{collection_season_16_name}" - - url: "{collection_season_17_url}" + - url: "{ %array_at( %get_season_urls(collection_season_17_url), 0) }" variables: collection_season_number: "17" collection_season_name: "{collection_season_17_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_17_url), 1) }" + variables: + collection_season_number: "17" + collection_season_name: "{collection_season_17_name}" - - url: "{collection_season_18_url}" + - url: "{ %array_at( %get_season_urls(collection_season_18_url), 0) }" variables: collection_season_number: "18" collection_season_name: "{collection_season_18_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_18_url), 1) }" + variables: + collection_season_number: "18" + collection_season_name: "{collection_season_18_name}" - - url: "{collection_season_19_url}" + - url: "{ %array_at( %get_season_urls(collection_season_19_url), 0) }" variables: collection_season_number: "19" collection_season_name: "{collection_season_19_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_19_url), 1) }" + variables: + collection_season_number: "19" + collection_season_name: "{collection_season_19_name}" - - url: "{collection_season_20_url}" + - url: "{ %array_at( %get_season_urls(collection_season_20_url), 0) }" variables: collection_season_number: "20" collection_season_name: "{collection_season_20_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_20_url), 1) }" + variables: + collection_season_number: "20" + collection_season_name: "{collection_season_20_name}" - - url: "{collection_season_21_url}" + - url: "{ %array_at( %get_season_urls(collection_season_21_url), 0) }" variables: collection_season_number: "21" collection_season_name: "{collection_season_21_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_21_url), 1) }" + variables: + collection_season_number: "21" + collection_season_name: "{collection_season_21_name}" - - url: "{collection_season_22_url}" + - url: "{ %array_at( %get_season_urls(collection_season_22_url), 0) }" variables: collection_season_number: "22" collection_season_name: "{collection_season_22_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_22_url), 1) }" + variables: + collection_season_number: "22" + collection_season_name: "{collection_season_22_name}" - - url: "{collection_season_23_url}" + - url: "{ %array_at( %get_season_urls(collection_season_23_url), 0) }" variables: collection_season_number: "23" collection_season_name: "{collection_season_23_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_23_url), 1) }" + variables: + collection_season_number: "23" + collection_season_name: "{collection_season_23_name}" - - url: "{collection_season_24_url}" + - url: "{ %array_at( %get_season_urls(collection_season_24_url), 0) }" variables: collection_season_number: "24" collection_season_name: "{collection_season_24_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_24_url), 1) }" + variables: + collection_season_number: "24" + collection_season_name: "{collection_season_24_name}" - - url: "{collection_season_25_url}" + - url: "{ %array_at( %get_season_urls(collection_season_25_url), 0) }" variables: collection_season_number: "25" collection_season_name: "{collection_season_25_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_25_url), 1) }" + variables: + collection_season_number: "25" + collection_season_name: "{collection_season_25_name}" - - url: "{collection_season_26_url}" + - url: "{ %array_at( %get_season_urls(collection_season_26_url), 0) }" variables: collection_season_number: "26" collection_season_name: "{collection_season_26_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_26_url), 1) }" + variables: + collection_season_number: "26" + collection_season_name: "{collection_season_26_name}" - - url: "{collection_season_27_url}" + - url: "{ %array_at( %get_season_urls(collection_season_27_url), 0) }" variables: collection_season_number: "27" collection_season_name: "{collection_season_27_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_27_url), 1) }" + variables: + collection_season_number: "27" + collection_season_name: "{collection_season_27_name}" - - url: "{collection_season_28_url}" + - url: "{ %array_at( %get_season_urls(collection_season_28_url), 0) }" variables: collection_season_number: "28" collection_season_name: "{collection_season_28_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_28_url), 1) }" + variables: + collection_season_number: "28" + collection_season_name: "{collection_season_28_name}" - - url: "{collection_season_29_url}" + - url: "{ %array_at( %get_season_urls(collection_season_29_url), 0) }" variables: collection_season_number: "29" collection_season_name: "{collection_season_29_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_29_url), 1) }" + variables: + collection_season_number: "29" + collection_season_name: "{collection_season_29_name}" - - url: "{collection_season_30_url}" + - url: "{ %array_at( %get_season_urls(collection_season_30_url), 0) }" variables: collection_season_number: "30" collection_season_name: "{collection_season_30_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_30_url), 1) }" + variables: + collection_season_number: "30" + collection_season_name: "{collection_season_30_name}" - - url: "{collection_season_31_url}" + - url: "{ %array_at( %get_season_urls(collection_season_31_url), 0) }" variables: collection_season_number: "31" collection_season_name: "{collection_season_31_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_31_url), 1) }" + variables: + collection_season_number: "31" + collection_season_name: "{collection_season_31_name}" - - url: "{collection_season_32_url}" + - url: "{ %array_at( %get_season_urls(collection_season_32_url), 0) }" variables: collection_season_number: "32" collection_season_name: "{collection_season_32_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_32_url), 1) }" + variables: + collection_season_number: "32" + collection_season_name: "{collection_season_32_name}" - - url: "{collection_season_33_url}" + - url: "{ %array_at( %get_season_urls(collection_season_33_url), 0) }" variables: collection_season_number: "33" collection_season_name: "{collection_season_33_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_33_url), 1) }" + variables: + collection_season_number: "33" + collection_season_name: "{collection_season_33_name}" - - url: "{collection_season_34_url}" + - url: "{ %array_at( %get_season_urls(collection_season_34_url), 0) }" variables: collection_season_number: "34" collection_season_name: "{collection_season_34_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_34_url), 1) }" + variables: + collection_season_number: "34" + collection_season_name: "{collection_season_34_name}" - - url: "{collection_season_35_url}" + - url: "{ %array_at( %get_season_urls(collection_season_35_url), 0) }" variables: collection_season_number: "35" collection_season_name: "{collection_season_35_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_35_url), 1) }" + variables: + collection_season_number: "35" + collection_season_name: "{collection_season_35_name}" - - url: "{collection_season_36_url}" + - url: "{ %array_at( %get_season_urls(collection_season_36_url), 0) }" variables: collection_season_number: "36" collection_season_name: "{collection_season_36_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_36_url), 1) }" + variables: + collection_season_number: "36" + collection_season_name: "{collection_season_36_name}" - - url: "{collection_season_37_url}" + - url: "{ %array_at( %get_season_urls(collection_season_37_url), 0) }" variables: collection_season_number: "37" collection_season_name: "{collection_season_37_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_37_url), 1) }" + variables: + collection_season_number: "37" + collection_season_name: "{collection_season_37_name}" - - url: "{collection_season_38_url}" + - url: "{ %array_at( %get_season_urls(collection_season_38_url), 0) }" variables: collection_season_number: "38" collection_season_name: "{collection_season_38_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_38_url), 1) }" + variables: + collection_season_number: "38" + collection_season_name: "{collection_season_38_name}" - - url: "{collection_season_39_url}" + - url: "{ %array_at( %get_season_urls(collection_season_39_url), 0) }" variables: collection_season_number: "39" collection_season_name: "{collection_season_39_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_39_url), 1) }" + variables: + collection_season_number: "39" + collection_season_name: "{collection_season_39_name}" - - url: "{collection_season_40_url}" + - url: "{ %array_at( %get_season_urls(collection_season_40_url), 0) }" variables: collection_season_number: "40" collection_season_name: "{collection_season_40_name}" playlist_thumbnails: - name: "{season_poster_file_name}" uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_40_url), 1) }" + variables: + collection_season_number: "40" + collection_season_name: "{collection_season_40_name}" + + + # Place season 0 at end + - url: "{ %array_at( %get_season_urls(collection_season_0_url), 0) }" + variables: + collection_season_number: "0" + collection_season_name: "{collection_season_0_name}" + playlist_thumbnails: + - name: "{season_poster_file_name}" + uid: "latest_entry" + - url: "{ %array_slice( %get_season_urls(collection_season_0_url), 1) }" + variables: + collection_season_number: "0" + collection_season_name: "{collection_season_0_name}" + output_directory_nfo_tags: tags: @@ -565,6 +800,7 @@ presets: collection_season_38_name: "{s38_name}" collection_season_39_name: "{s39_name}" collection_season_40_name: "{s40_name}" + collection_season_0_name: "{s00_name}" # Legacy url variable collection_season_1_url: "{s01_url}" @@ -607,6 +843,7 @@ presets: collection_season_38_url: "{s38_url}" collection_season_39_url: "{s39_url}" collection_season_40_url: "{s40_url}" + collection_season_0_url: "{s00_url}" s01_name: "" s02_name: "" @@ -648,6 +885,7 @@ presets: s38_name: "" s39_name: "" s40_name: "" + s00_name: "" s01_url: "" s02_url: "" @@ -689,13 +927,22 @@ presets: s38_url: "" s39_url: "" s40_url: "" + s00_url: "" + + "%get_season_urls": >- + { %if( %is_array( $0 ), $0, [ $0 ] ) } + + # $0 - season url variable + # $1 - get the i'th url from the array + "%get_season_url": >- + { %array_at( %get_season_urls($0), $1, "" ) } _tv_show_collection_bilateral: preset: - "_url_bilateral_overrides" download: - - url: "{ %bilateral_url(collection_season_1_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_1_url), %bilateral_url) }" variables: collection_season_number: "1" collection_season_name: "{collection_season_1_name}" @@ -703,7 +950,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_2_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_2_url), %bilateral_url) }" variables: collection_season_number: "2" collection_season_name: "{collection_season_2_name}" @@ -711,7 +958,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_3_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_3_url), %bilateral_url) }" variables: collection_season_number: "3" collection_season_name: "{collection_season_3_name}" @@ -719,7 +966,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_4_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_4_url), %bilateral_url) }" variables: collection_season_number: "4" collection_season_name: "{collection_season_4_name}" @@ -727,7 +974,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_5_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_5_url), %bilateral_url) }" variables: collection_season_number: "5" collection_season_name: "{collection_season_5_name}" @@ -735,7 +982,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_6_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_6_url), %bilateral_url) }" variables: collection_season_number: "6" collection_season_name: "{collection_season_6_name}" @@ -743,7 +990,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_7_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_7_url), %bilateral_url) }" variables: collection_season_number: "7" collection_season_name: "{collection_season_7_name}" @@ -751,7 +998,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_8_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_8_url), %bilateral_url) }" variables: collection_season_number: "8" collection_season_name: "{collection_season_8_name}" @@ -759,7 +1006,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_9_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_9_url), %bilateral_url) }" variables: collection_season_number: "9" collection_season_name: "{collection_season_9_name}" @@ -767,7 +1014,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_10_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_10_url), %bilateral_url) }" variables: collection_season_number: "10" collection_season_name: "{collection_season_10_name}" @@ -775,7 +1022,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_11_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_11_url), %bilateral_url) }" variables: collection_season_number: "11" collection_season_name: "{collection_season_11_name}" @@ -783,7 +1030,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_12_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_12_url), %bilateral_url) }" variables: collection_season_number: "12" collection_season_name: "{collection_season_12_name}" @@ -791,7 +1038,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_13_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_13_url), %bilateral_url) }" variables: collection_season_number: "13" collection_season_name: "{collection_season_13_name}" @@ -799,7 +1046,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_14_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_14_url), %bilateral_url) }" variables: collection_season_number: "14" collection_season_name: "{collection_season_14_name}" @@ -807,7 +1054,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_15_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_15_url), %bilateral_url) }" variables: collection_season_number: "15" collection_season_name: "{collection_season_15_name}" @@ -815,7 +1062,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_16_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_16_url), %bilateral_url) }" variables: collection_season_number: "16" collection_season_name: "{collection_season_16_name}" @@ -823,7 +1070,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_17_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_17_url), %bilateral_url) }" variables: collection_season_number: "17" collection_season_name: "{collection_season_17_name}" @@ -831,7 +1078,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_18_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_18_url), %bilateral_url) }" variables: collection_season_number: "18" collection_season_name: "{collection_season_18_name}" @@ -839,7 +1086,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_19_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_19_url), %bilateral_url) }" variables: collection_season_number: "19" collection_season_name: "{collection_season_19_name}" @@ -847,7 +1094,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_20_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_20_url), %bilateral_url) }" variables: collection_season_number: "20" collection_season_name: "{collection_season_20_name}" @@ -855,7 +1102,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_21_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_21_url), %bilateral_url) }" variables: collection_season_number: "21" collection_season_name: "{collection_season_21_name}" @@ -863,7 +1110,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_22_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_22_url), %bilateral_url) }" variables: collection_season_number: "22" collection_season_name: "{collection_season_22_name}" @@ -871,7 +1118,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_23_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_23_url), %bilateral_url) }" variables: collection_season_number: "23" collection_season_name: "{collection_season_23_name}" @@ -879,7 +1126,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_24_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_24_url), %bilateral_url) }" variables: collection_season_number: "24" collection_season_name: "{collection_season_24_name}" @@ -887,7 +1134,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_25_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_25_url), %bilateral_url) }" variables: collection_season_number: "25" collection_season_name: "{collection_season_25_name}" @@ -895,7 +1142,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_26_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_26_url), %bilateral_url) }" variables: collection_season_number: "26" collection_season_name: "{collection_season_26_name}" @@ -903,7 +1150,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_27_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_27_url), %bilateral_url) }" variables: collection_season_number: "27" collection_season_name: "{collection_season_27_name}" @@ -911,7 +1158,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_28_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_28_url), %bilateral_url) }" variables: collection_season_number: "28" collection_season_name: "{collection_season_28_name}" @@ -919,7 +1166,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_29_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_29_url), %bilateral_url) }" variables: collection_season_number: "29" collection_season_name: "{collection_season_29_name}" @@ -927,7 +1174,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_30_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_30_url), %bilateral_url) }" variables: collection_season_number: "30" collection_season_name: "{collection_season_30_name}" @@ -935,7 +1182,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_31_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_31_url), %bilateral_url) }" variables: collection_season_number: "31" collection_season_name: "{collection_season_31_name}" @@ -943,7 +1190,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_32_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_32_url), %bilateral_url) }" variables: collection_season_number: "32" collection_season_name: "{collection_season_32_name}" @@ -951,7 +1198,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_33_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_33_url), %bilateral_url) }" variables: collection_season_number: "33" collection_season_name: "{collection_season_33_name}" @@ -959,7 +1206,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_34_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_34_url), %bilateral_url) }" variables: collection_season_number: "34" collection_season_name: "{collection_season_34_name}" @@ -967,7 +1214,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_35_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_35_url), %bilateral_url) }" variables: collection_season_number: "35" collection_season_name: "{collection_season_35_name}" @@ -975,7 +1222,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_36_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_36_url), %bilateral_url) }" variables: collection_season_number: "36" collection_season_name: "{collection_season_36_name}" @@ -983,7 +1230,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_37_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_37_url), %bilateral_url) }" variables: collection_season_number: "37" collection_season_name: "{collection_season_37_name}" @@ -991,7 +1238,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_38_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_38_url), %bilateral_url) }" variables: collection_season_number: "38" collection_season_name: "{collection_season_38_name}" @@ -999,7 +1246,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_39_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_39_url), %bilateral_url) }" variables: collection_season_number: "39" collection_season_name: "{collection_season_39_name}" @@ -1007,7 +1254,7 @@ presets: ytdl_options: playlist_items: "-1:0:-1" - - url: "{ %bilateral_url(collection_season_40_url) }" + - url: "{ %array_apply( %get_season_urls(collection_season_40_url), %bilateral_url) }" variables: collection_season_number: "40" collection_season_name: "{collection_season_40_name}" @@ -1015,6 +1262,25 @@ presets: ytdl_options: playlist_items: "-1:0:-1" + # Season 0 at end (to download first) + - url: "{ %array_apply( %get_season_urls(collection_season_0_url), %bilateral_url) }" + variables: + collection_season_number: "0" + collection_season_name: "{collection_season_0_name}" + download_reverse: False + ytdl_options: + playlist_items: "-1:0:-1" + + _tv_show_collection_asserts: + overrides: + url: "" + assert_not_by_date: >- + { + %assert( + %not( %bool(url) ), + "Provided `url` to TV Show Collection preset when it expects `s01_url`. Perhaps you meant to use the `TV Show by Date` preset?" + ) + } #################################################################################################### # DEPRECATED SEASON PRESETS diff --git a/src/ytdl_sub/script/functions/__init__.py b/src/ytdl_sub/script/functions/__init__.py index 5b98d24b..e43f7e28 100644 --- a/src/ytdl_sub/script/functions/__init__.py +++ b/src/ytdl_sub/script/functions/__init__.py @@ -9,6 +9,7 @@ from ytdl_sub.script.functions.error_functions import ErrorFunctions from ytdl_sub.script.functions.json_functions import JsonFunctions from ytdl_sub.script.functions.map_functions import MapFunctions from ytdl_sub.script.functions.numeric_functions import NumericFunctions +from ytdl_sub.script.functions.print_functions import PrintFunctions from ytdl_sub.script.functions.regex_functions import RegexFunctions from ytdl_sub.script.functions.string_functions import StringFunctions from ytdl_sub.script.types.resolvable import Resolvable @@ -26,6 +27,7 @@ class Functions( RegexFunctions, DateFunctions, JsonFunctions, + PrintFunctions, ): _custom_functions: Dict[str, Callable[..., Resolvable]] = {} diff --git a/src/ytdl_sub/script/functions/numeric_functions.py b/src/ytdl_sub/script/functions/numeric_functions.py index b25374f9..003c8837 100644 --- a/src/ytdl_sub/script/functions/numeric_functions.py +++ b/src/ytdl_sub/script/functions/numeric_functions.py @@ -1,5 +1,7 @@ import math +from typing import Optional +from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.resolvable import AnyArgument from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer @@ -96,3 +98,18 @@ class NumericFunctions: Returns min of all values. """ return _to_numeric(min(val.value for val in values)) + + @staticmethod + def range( + end: Integer, start: Optional[Integer] = None, step: Optional[Integer] = None + ) -> Array: + """ + :description: + Returns the desired range of Integers in the form of an Array. + """ + if start is None: + start = Integer(0) + if step is None: + step = Integer(1) + + return Array(value=[Integer(idx) for idx in range(start.value, end.value, step.value)]) diff --git a/src/ytdl_sub/script/functions/print_functions.py b/src/ytdl_sub/script/functions/print_functions.py new file mode 100644 index 00000000..47084085 --- /dev/null +++ b/src/ytdl_sub/script/functions/print_functions.py @@ -0,0 +1,69 @@ +from typing import Optional + +from ytdl_sub.script.types.resolvable import AnyArgument +from ytdl_sub.script.types.resolvable import Integer +from ytdl_sub.script.types.resolvable import ReturnableArgument +from ytdl_sub.utils.logger import Logger + +logger = Logger.get(name="preset") + + +def _log(message: AnyArgument, level: Optional[Integer]) -> None: + if not str(message): + return + + if level is None: + logger.info(str(message)) + return + + level_value: int = level.native + if level_value < 0: + logger.debug(str(message)) + elif level_value == 0: + logger.info(str(message)) + elif level_value == 1: + logger.warning(str(message)) + else: # > 1 + logger.error(str(message)) + + +class PrintFunctions: + @staticmethod + def print( + message: AnyArgument, passthrough: ReturnableArgument, level: Optional[Integer] = None + ) -> 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``) + """ + _log(message=message, level=level) + return passthrough + + @staticmethod + def print_if_true( + message: AnyArgument, passthrough: ReturnableArgument, level: Optional[Integer] = None + ) -> 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``) + """ + if passthrough.value: + _log(message=message, level=level) + return passthrough + + @staticmethod + def print_if_false( + message: AnyArgument, passthrough: ReturnableArgument, level: Optional[Integer] = None + ) -> 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``) + """ + if not passthrough.value: + _log(message=message, level=level) + return passthrough diff --git a/src/ytdl_sub/script/functions/string_functions.py b/src/ytdl_sub/script/functions/string_functions.py index e9af7a54..a1f4567e 100644 --- a/src/ytdl_sub/script/functions/string_functions.py +++ b/src/ytdl_sub/script/functions/string_functions.py @@ -127,12 +127,12 @@ class StringFunctions: return Array([String(split_val) for split_val in string.value.split(sep=sep.value)]) @staticmethod - def concat(*values: String) -> String: + def concat(*values: AnyArgument) -> String: """ :description: Concatenate multiple Strings into a single String. """ - return String("".join(val.value for val in values)) + return String("".join(str(val.value) for val in values)) @staticmethod def pad(string: String, length: Integer, char: String) -> String: @@ -176,3 +176,41 @@ class StringFunctions: # World """ return String(string.value.encode("utf-8").decode("unicode_escape")) + + @staticmethod + def 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" + """ + + return String(separator.value.join(str(val) for val in array.value)) + + @staticmethod + def 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!" + """ + return String(string.value.strip()) diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 737d6ffa..73dbb3f4 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -31,6 +31,8 @@ from ytdl_sub.script.utils.exceptions import InvalidSyntaxException from ytdl_sub.script.utils.exceptions import InvalidVariableName from ytdl_sub.script.utils.exceptions import UserException from ytdl_sub.script.utils.exceptions import VariableDoesNotExist +from ytdl_sub.script.utils.name_validation import is_function +from ytdl_sub.script.utils.name_validation import to_function_name from ytdl_sub.script.utils.name_validation import validate_variable_name # pylint: disable=invalid-name @@ -144,6 +146,9 @@ class _Parser: ): self._text = text self._name = name + if name and is_function(name): + self._name = to_function_name(name) + self._custom_function_names = custom_function_names self._variable_names = variable_names self._pos = 0 @@ -605,7 +610,7 @@ def parse( name=name, custom_function_names=custom_function_names, variable_names=variable_names, - ).ast + ).ast.maybe_resolvable_casted() # pylint: enable=invalid-name diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index ab506318..41c3f1be 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -1,4 +1,5 @@ # pylint: disable=missing-raises-doc +from collections import defaultdict from typing import Dict from typing import List from typing import Optional @@ -7,40 +8,28 @@ from typing import Set from ytdl_sub.script.functions import Functions from ytdl_sub.script.parser import parse from ytdl_sub.script.script_output import ScriptOutput +from ytdl_sub.script.types.resolvable import Argument from ytdl_sub.script.types.resolvable import BuiltInFunctionType from ytdl_sub.script.types.resolvable import Lambda from ytdl_sub.script.types.resolvable import Resolvable +from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.types.variable_dependency import VariableDependency from ytdl_sub.script.utils.exceptions import UNREACHABLE from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments from ytdl_sub.script.utils.exceptions import RuntimeException from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved +from ytdl_sub.script.utils.name_validation import is_function +from ytdl_sub.script.utils.name_validation import to_function_definition_name +from ytdl_sub.script.utils.name_validation import to_function_name from ytdl_sub.script.utils.name_validation import validate_variable_name from ytdl_sub.script.utils.type_checking import FunctionSpec -def _is_function(override_name: str): - return override_name.startswith("%") - - -def _function_name(function_key: str) -> str: - """ - Drop the % in %custom_function - """ - return function_key[1:] - - -def _to_function_definition_name(function_key: str) -> str: - """ - Add % in %custom_function - """ - return f"%{function_key}" - - class Script: """ Takes a dictionary of both @@ -49,43 +38,71 @@ class Script: ``{ %custom_function: syntax }`` """ - def _ensure_no_cycle( + def _throw_cycle_error( self, name: str, dep: str, deps: List[str], definitions: Dict[str, SyntaxTree] ): - if dep not in definitions: - return # does not exist, will throw downstream in parser + type_name, pre = ( + ("custom functions", "%") if definitions is self._functions else ("variables", "") + ) + cycle_deps = [name] + deps + [dep] + cycle_deps_str = " -> ".join([f"{pre}{name}" for name in cycle_deps]) - if name in deps + [dep]: - type_name, pre = ( - ("custom functions", "%") if definitions is self._functions else ("variables", "") - ) - cycle_deps = [name] + deps + [dep] - cycle_deps_str = " -> ".join([f"{pre}{name}" for name in cycle_deps]) - - raise CycleDetected(f"Cycle detected within these {type_name}: {cycle_deps_str}") + raise CycleDetected(f"Cycle detected within these {type_name}: {cycle_deps_str}") def _traverse_variable_dependencies( self, variable_name: str, variable_dependency: SyntaxTree, deps: List[str], + ensured: Dict[str, Set[str]], ) -> None: for dep in variable_dependency.variables: - self._ensure_no_cycle( - name=variable_name, dep=dep.name, deps=deps, definitions=self._variables - ) + if variable_name == dep.name: + self._throw_cycle_error( + name=variable_name, dep=dep.name, deps=deps, definitions=self._variables + ) + + if dep.name in ensured[variable_name]: + continue + self._traverse_variable_dependencies( variable_name=variable_name, variable_dependency=self._variables[dep.name], deps=deps + [dep.name], + ensured=ensured, ) + ensured[variable_name].add(dep.name) + + for custom_func in variable_dependency.custom_function_dependencies( + custom_function_definitions=self._functions + ): + for dep in self._functions[custom_func.name].variables: + if variable_name == dep.name: + self._throw_cycle_error( + name=variable_name, + dep=dep.name, + deps=deps + [custom_func.definition_name()], + definitions=self._variables, + ) + if dep.name in ensured[variable_name]: + continue + + self._traverse_variable_dependencies( + variable_name=variable_name, + variable_dependency=self._variables[dep.name], + deps=deps + [custom_func.definition_name(), dep.name], + ensured=ensured, + ) + ensured[variable_name].add(dep.name) def _ensure_no_variable_cycles(self, variables: Dict[str, SyntaxTree]): + ensured: Dict[str, Set[str]] = defaultdict(set) for variable_name, variable_definition in variables.items(): self._traverse_variable_dependencies( variable_name=variable_name, variable_dependency=variable_definition, deps=[], + ensured=ensured, ) def _traverse_custom_function_dependencies( @@ -95,9 +112,10 @@ class Script: deps: List[str], ) -> None: for dep in custom_function_dependency.custom_functions: - self._ensure_no_cycle( - name=custom_function_name, dep=dep.name, deps=deps, definitions=self._functions - ) + if custom_function_name == dep.name: + self._throw_cycle_error( + name=custom_function_name, dep=dep.name, deps=deps, definitions=self._functions + ) self._traverse_custom_function_dependencies( custom_function_name=custom_function_name, custom_function_dependency=self._functions[dep.name], @@ -240,23 +258,23 @@ class Script: def __init__(self, script: Dict[str, str]): function_names: Set[str] = { - _function_name(name) for name in script.keys() if _is_function(name) + to_function_name(name) for name in script.keys() if is_function(name) } variable_names: Set[str] = { - validate_variable_name(name) for name in script.keys() if not _is_function(name) + validate_variable_name(name) for name in script.keys() if not is_function(name) } self._functions: Dict[str, SyntaxTree] = { # custom_function_name must be passed to properly type custom function # arguments uniquely if they're nested (i.e. $0 to $custom_func___0) - _function_name(function_key): parse( + to_function_name(function_key): parse( text=function_value, - name=_function_name(function_key), + name=to_function_name(function_key), custom_function_names=function_names, variable_names=variable_names, ) for function_key, function_value in script.items() - if _is_function(function_key) + if is_function(function_key) } self._variables: Dict[str, SyntaxTree] = { @@ -267,13 +285,13 @@ class Script: variable_names=variable_names, ) for variable_key, variable_value in script.items() - if not _is_function(variable_key) + if not is_function(variable_key) } self._validate() def _update_internally(self, resolved_variables: Dict[str, Resolvable]) -> None: for variable_name, resolved in resolved_variables.items(): - self._variables[variable_name] = SyntaxTree(ast=[resolved]) + self._variables[variable_name] = ResolvedSyntaxTree(ast=[resolved]) def _recursive_get_unresolved_output_filter_variables( self, current_var: SyntaxTree, subset_to_resolve: Set[str], unresolvable: Set[Variable] @@ -302,14 +320,21 @@ class Script: unresolvable=unresolvable, ) + for lambda_func in current_var.lambdas: + if lambda_func.value in self._functions: + subset_to_resolve |= self._recursive_get_unresolved_output_filter_variables( + current_var=self._functions[lambda_func.value], + subset_to_resolve=subset_to_resolve, + unresolvable=unresolvable, + ) + return subset_to_resolve def _get_unresolved_output_filter( self, - unresolved: Dict[Variable, SyntaxTree], output_filter: Set[str], unresolvable: Set[Variable], - ) -> Dict[Variable, SyntaxTree]: + ) -> Set[str]: """ When an output filter is applied, only a subset of variables that the filter depends on need to be resolved. @@ -330,7 +355,7 @@ class Script: unresolvable=unresolvable, ) - return {var: syntax for var, syntax in unresolved.items() if var.name in subset_to_resolve} + return subset_to_resolve def _resolve( self, @@ -366,18 +391,21 @@ class Script: unresolvable: Set[Variable] = {Variable(name) for name in (unresolvable or {})} unresolved_filter = set(resolved.keys()).union(unresolvable) - unresolved: Dict[Variable, SyntaxTree] = { - Variable(name): ast - for name, ast in self._variables.items() - if Variable(name) not in unresolved_filter - } if output_filter: - unresolved = self._get_unresolved_output_filter( - unresolved=unresolved, - output_filter=output_filter, - unresolvable=unresolvable, - ) + unresolved = { + Variable(name): self._variables[name] + for name in self._get_unresolved_output_filter( + output_filter=output_filter, + unresolvable=unresolvable, + ) + } + else: + unresolved = { + Variable(name): ast + for name, ast in self._variables.items() + if Variable(name) not in unresolved_filter + } while unresolved: unresolved_count: int = len(unresolved) @@ -398,8 +426,8 @@ class Script: # Otherwise, if it has dependencies that are all resolved, then # resolve the definition - elif not definition.is_subset_of( - variables=resolved.keys(), custom_function_definitions=self._functions + elif definition.is_subset_of( + variables=resolved, custom_function_definitions=self._functions ): resolved[variable] = unresolved[variable].resolve( resolved_variables=resolved, @@ -482,12 +510,12 @@ class Script: added_variables_to_validate: Set[str] = set() functions_to_add = { - _function_name(name): definition + to_function_name(name): definition for name, definition in variables.items() - if _is_function(name) + if is_function(name) } variables_to_add = { - name: definition for name, definition in variables.items() if not _is_function(name) + name: definition for name, definition in variables.items() if not is_function(name) } custom_function_names = set(self._functions.keys()) | functions_to_add.keys() @@ -517,11 +545,52 @@ class Script: return self + def add_parsed(self, variables: Dict[str, SyntaxTree]) -> "Script": + """ + Adds already parsed, new variables to the script. + + Parameters + ---------- + variables + Mapping containing variable name to definition. + + Returns + ------- + Script + self + """ + added_variables_to_validate: Set[str] = set() + + functions_to_add = { + to_function_name(name): definition + for name, definition in variables.items() + if is_function(name) + } + variables_to_add = { + name: definition for name, definition in variables.items() if not is_function(name) + } + + for definitions in [functions_to_add, variables_to_add]: + for name, parsed in definitions.items(): + if parsed.maybe_resolvable is None: + added_variables_to_validate.add(name) + + if name in functions_to_add: + self._functions[name] = parsed + else: + self._variables[name] = parsed + + if added_variables_to_validate: + self._validate(added_variables=added_variables_to_validate) + + return self + def resolve_once( self, variable_definitions: Dict[str, str], resolved: Optional[Dict[str, Resolvable]] = None, unresolvable: Optional[Set[str]] = None, + update: bool = False, ) -> Dict[str, Resolvable]: """ Given a new set of variable definitions, resolve them using the Script, but do not @@ -536,6 +605,8 @@ class Script: unresolvable Optional. Unresolvable variables that will be ignored in resolution, including all variables with a dependency to them. + update + Whether to update the script's state with resolved variables. Defaults to False. Returns ------- @@ -548,11 +619,51 @@ class Script: pre_resolved=resolved, unresolvable=unresolvable, output_filter=set(list(variable_definitions.keys())), + update=update, ).output finally: for name in variable_definitions.keys(): - if name in self._variables: - del self._variables[name] + self._variables.pop(name, None) + + def resolve_once_parsed( + self, + variable_definitions: Dict[str, SyntaxTree], + resolved: Optional[Dict[str, Resolvable]] = None, + unresolvable: Optional[Set[str]] = None, + update: bool = False, + ) -> Dict[str, Resolvable]: + """ + Given a new set of variable definitions, resolve them using the Script, but do not + add them to the Script itself. + + Parameters + ---------- + variable_definitions + Variables to resolve, but not store in the Script + resolved + Optional. Pre-resolved variables that should be used instead of what is in the script. + unresolvable + Optional. Unresolvable variables that will be ignored in resolution, including all + variables with a dependency to them. + update + Whether to update the script's state with resolved variables. Defaults to False. + + Returns + ------- + Dict[str, Resolvable] + Dict containing the variable names to their resolved values. + """ + try: + self.add_parsed(variable_definitions) + return self._resolve( + pre_resolved=resolved, + unresolvable=unresolvable, + output_filter=set(list(variable_definitions.keys())), + update=update, + ).output + finally: + for name in variable_definitions.keys(): + self._variables.pop(name, None) def get(self, variable_name: str) -> Resolvable: """ @@ -581,6 +692,18 @@ class Script: raise RuntimeException(f"Tried to get unresolved variable {variable_name}") + def definition_of(self, name: str) -> SyntaxTree: + """ + Returns + ------- + The definition of the variable or function. + """ + if name.startswith("%") and name[1:] in self._functions: + return self._functions[name[1:]] + if name in self._variables: + return self._variables[name] + raise RuntimeException(f"Tried to get non-existent definition with name {name}") + @property def variable_names(self) -> Set[str]: """ @@ -599,4 +722,121 @@ class Script: Set[str] Names of all functions within the Script. """ - return set(_to_function_definition_name(name) for name in self._functions.keys()) + return set(to_function_definition_name(name) for name in self._functions.keys()) + + def _resolve_partial_loop( + self, + output_filter: Optional[Set[str]], + resolved: Dict[Variable, Resolvable], + unresolved: Dict[Variable, Argument], + unresolvable: Optional[Set[str]], + ): + to_partially_resolve: Set[Variable] = ( + {Variable(name) for name in output_filter} if output_filter else set(unresolved.keys()) + ) + + partially_resolved = True + while partially_resolved: + + partially_resolved = False + + for variable in list(to_partially_resolve): + definition = unresolved[variable] + maybe_resolved = definition + + if isinstance(definition, Variable) and definition.name not in unresolvable: + if definition in resolved: + maybe_resolved = resolved[definition] + elif definition in unresolved: + maybe_resolved = unresolved[definition] + else: + raise UNREACHABLE + elif isinstance(definition, VariableDependency): + maybe_resolved = definition.partial_resolve( + resolved_variables=resolved, + unresolved_variables=unresolved, + custom_functions=self._functions, + ) + + if isinstance(maybe_resolved, Resolvable): + resolved[variable] = maybe_resolved + del unresolved[variable] + to_partially_resolve.remove(variable) + partially_resolved = True + else: + unresolved[variable] = maybe_resolved + + # If the definition changed, then the script changed + # which means we can iterate again + partially_resolved |= definition != maybe_resolved + + def _resolve_partial( + self, + unresolvable: Optional[Set[str]] = None, + output_filter: Optional[Set[str]] = None, + ) -> Dict[str, SyntaxTree]: + """ + Returns + ------- + New (deep-copied) script that resolves inner variables as much + as possible. + """ + unresolvable: Set[str] = unresolvable or {} + resolved: Dict[Variable, Resolvable] = {} + unresolved: Dict[Variable, Argument] = { + Variable(name): definition + for name, definition in self._variables.items() + if name not in unresolvable + } + + self._resolve_partial_loop( + output_filter=output_filter, + resolved=resolved, + unresolved=unresolved, + unresolvable=unresolvable, + ) + + if output_filter: + out: Dict[str, SyntaxTree] = {} + for name in output_filter: + variable_name = Variable(name) + if variable_name in resolved: + out[name] = ResolvedSyntaxTree(ast=[resolved[variable_name]]) + else: + out[name] = SyntaxTree(ast=[unresolved[variable_name]]) + + return out + + return { + var.name: ResolvedSyntaxTree(ast=[definition]) for var, definition in resolved.items() + } | {var.name: SyntaxTree(ast=[definition]) for var, definition in unresolved.items()} + + def resolve_partial( + self, + unresolvable: Optional[Set[str]] = None, + output_filter: Optional[Set[str]] = None, + ) -> "Script": + """ + Updates the internal script to resolve as much as possible. + """ + out = self._resolve_partial(unresolvable=unresolvable, output_filter=output_filter) + for var_name, definition in out.items(): + self._variables[var_name] = definition + + return self + + def resolve_partial_once( + self, variable_definitions: Dict[str, SyntaxTree], unresolvable: Optional[Set[str]] = None + ) -> Dict[str, SyntaxTree]: + """ + Partially resolves the input variable definitions as much as possible. + """ + try: + self.add_parsed(variable_definitions) + return self._resolve_partial( + unresolvable=unresolvable, + output_filter=set(list(variable_definitions.keys())), + ) + finally: + for name in variable_definitions.keys(): + self._variables.pop(name, None) diff --git a/src/ytdl_sub/script/types/array.py b/src/ytdl_sub/script/types/array.py index 8adcfe50..204697fb 100644 --- a/src/ytdl_sub/script/types/array.py +++ b/src/ytdl_sub/script/types/array.py @@ -28,7 +28,7 @@ class UnresolvedArray(_Array, VariableDependency, FutureResolvable): value: List[Argument] @property - def _iterable_arguments(self) -> List[Argument]: + def iterable_arguments(self) -> List[Argument]: return self.value def resolve( @@ -47,6 +47,27 @@ class UnresolvedArray(_Array, VariableDependency, FutureResolvable): ] ) + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, VariableDependency], + ) -> Argument | Resolvable: + maybe_resolvable_values, is_resolvable = VariableDependency.try_partial_resolve( + args=self.value, + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + out = UnresolvedArray(value=maybe_resolvable_values) + if is_resolvable: + return out.resolve( + resolved_variables=resolved_variables, custom_functions=custom_functions + ) + + return out + def future_resolvable_type(self) -> Type[Resolvable]: return Array diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index 7ecd1999..bf64108b 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -1,10 +1,10 @@ -import copy import functools from abc import ABC from dataclasses import dataclass from typing import Callable from typing import Dict from typing import List +from typing import Optional from typing import Type from typing import Union @@ -12,9 +12,11 @@ from ytdl_sub.script.functions import Functions from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.array import UnresolvedArray from ytdl_sub.script.types.resolvable import Argument +from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import BuiltInFunctionType from ytdl_sub.script.types.resolvable import FunctionType from ytdl_sub.script.types.resolvable import FutureResolvable +from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Lambda from ytdl_sub.script.types.resolvable import NamedCustomFunction from ytdl_sub.script.types.resolvable import Resolvable @@ -36,7 +38,7 @@ from ytdl_sub.script.utils.type_checking import is_union @dataclass(frozen=True) class Function(FunctionType, VariableDependency, ABC): @property - def _iterable_arguments(self) -> List[Argument]: + def iterable_arguments(self) -> List[Argument]: return self.args @@ -58,27 +60,79 @@ class CustomFunction(Function, NamedCustomFunction): # Should be validated in the Script raise UNREACHABLE - resolved_variables_with_args = copy.deepcopy(resolved_variables) + function_args: List[FunctionArgument] = [] for i, arg in enumerate(resolved_args): function_arg = FunctionArgument.from_idx(idx=i, custom_function_name=self.name) - if function_arg in resolved_variables_with_args: + if function_arg in resolved_variables: # function args should always be unique since they are only defined once # in the custom function as %custom_function_name___idx # and returned as a set from each custom function. raise UNREACHABLE - resolved_variables_with_args[function_arg] = arg + resolved_variables[function_arg] = arg + function_args.append(function_arg) - return custom_functions[self.name].resolve( - resolved_variables=resolved_variables_with_args, + out = custom_functions[self.name].resolve( + resolved_variables=resolved_variables, custom_functions=custom_functions, ) + for function_arg in function_args: + del resolved_variables[function_arg] + + return out + # Implies the custom function does not exist. This should have # been checked in the parser with raise UNREACHABLE + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, VariableDependency], + ) -> Argument | Resolvable: + maybe_resolvable_args, _ = VariableDependency.try_partial_resolve( + args=self.args, + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + for i in range(len(self.args)): + function_arg = FunctionArgument.from_idx(idx=i, custom_function_name=self.name) + function_value = maybe_resolvable_args[i] + + if isinstance(function_value, Resolvable): + resolved_variables[function_arg] = function_value + else: + unresolved_variables[function_arg] = function_value + + assert len(custom_functions[self.name].iterable_arguments) == 1 + custom_function_definition = custom_functions[self.name].iterable_arguments[0] + + maybe_resolvable_custom_function, is_resolvable = VariableDependency.try_partial_resolve( + args=[custom_function_definition], + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + for i in range(len(self.args)): + function_arg = FunctionArgument.from_idx(idx=i, custom_function_name=self.name) + + if isinstance(maybe_resolvable_args[i], Resolvable): + del resolved_variables[function_arg] + else: + del unresolved_variables[function_arg] + + if is_resolvable: + return maybe_resolvable_custom_function[0] + + # Did not resolve custom function arguments, do not proceed + return CustomFunction(name=self.name, args=maybe_resolvable_args) + class BuiltInFunction(Function, BuiltInFunctionType): def validate_args(self) -> "BuiltInFunction": @@ -307,5 +361,126 @@ class BuiltInFunction(Function, BuiltInFunctionType): f"Runtime error occurred when executing the function %{self.name}: {str(exc)}" ) from exc + def _partial_resolve_conditional( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, "VariableDependency"], + ): + """ + If the conditional partially resolvable enough to warrant evaluation, + perform it here. + """ + + if self.name == "if": + maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve( + args=[self.args[0]], + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + if is_resolvable: + boolean_output = maybe_resolvable_arg[0] + assert isinstance(boolean_output, Boolean) + return self.args[1] if boolean_output.native else self.args[2] + + if self.name == "elif": + for idx in range(0, len(self.args), 2): + maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve( + args=[self.args[idx]], + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + if is_resolvable: + boolean_output = maybe_resolvable_arg[0] + assert isinstance(boolean_output, Boolean) + if boolean_output.native: + return self.args[idx + 1] + else: + break + + if self.name == "assert_then": + maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve( + args=[self.args[0]], + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + if is_resolvable: + boolean_output = maybe_resolvable_arg[0] + assert isinstance(boolean_output, Boolean) + if boolean_output.native: + return self.args[1] + + return self + + def _try_optimized_partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, "VariableDependency"], + ) -> Optional[Argument]: + """ + If a function has enough (but not all) resolved parameters to warrant a return, + perform it here. + """ + if self.name == "array_at": + if ( + isinstance(self.args[0], UnresolvedArray) + and isinstance(self.args[1], Integer) + and len(self.args[0].value) >= self.args[1].value + ): + maybe_resolvable_values, _ = VariableDependency.try_partial_resolve( + args=[self.args[0].value[self.args[1].value]], + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + return maybe_resolvable_values[0] + + return None + + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, VariableDependency], + ) -> Argument | Resolvable: + conditional_return_args = self.function_spec.conditional_arg_indices( + num_input_args=len(self.args) + ) + + if conditional_return_args: + return self._partial_resolve_conditional( + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + if partial_resolved := self._try_optimized_partial_resolve( + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ): + return partial_resolved + + maybe_resolvable_values, is_resolvable = VariableDependency.try_partial_resolve( + args=self.args, + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + out = BuiltInFunction(name=self.name, args=maybe_resolvable_values) + if is_resolvable: + return out.resolve( + resolved_variables=resolved_variables, + custom_functions=custom_functions, + ) + + return out + def __hash__(self): return hash((self.name, *self.args)) diff --git a/src/ytdl_sub/script/types/map.py b/src/ytdl_sub/script/types/map.py index 0fca1cbb..d5e2eb27 100644 --- a/src/ytdl_sub/script/types/map.py +++ b/src/ytdl_sub/script/types/map.py @@ -31,7 +31,7 @@ class UnresolvedMap(_Map, VariableDependency, FutureResolvable): value: Dict[Argument, Argument] @property - def _iterable_arguments(self) -> List[Argument]: + def iterable_arguments(self) -> List[Argument]: return list(itertools.chain(*self.value.items())) def resolve( @@ -55,6 +55,35 @@ class UnresolvedMap(_Map, VariableDependency, FutureResolvable): return Map(output) + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, VariableDependency], + ) -> Argument | Resolvable: + maybe_resolvable_keys, is_keys_resolvable = VariableDependency.try_partial_resolve( + args=self.value.keys(), + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + maybe_resolvable_values, is_values_resolvable = VariableDependency.try_partial_resolve( + args=self.value.values(), + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + out = UnresolvedMap(value=dict(zip(maybe_resolvable_keys, maybe_resolvable_values))) + if is_keys_resolvable and is_values_resolvable: + return out.resolve( + resolved_variables=resolved_variables, + custom_functions=custom_functions, + ) + + return out + def future_resolvable_type(self) -> Type[Resolvable]: return Map diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index c042b1f5..ad9ff300 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -193,6 +193,14 @@ class NamedCustomFunction(NamedArgument, ABC): class ParsedCustomFunction(NamedCustomFunction): num_input_args: int + def definition_name(self) -> str: + """ + Returns + ------- + The function definition name, including the % + """ + return f"%{self.name}" + @dataclass(frozen=True) class FunctionType(NamedArgument, ABC): diff --git a/src/ytdl_sub/script/types/syntax_tree.py b/src/ytdl_sub/script/types/syntax_tree.py index d058852c..e914f396 100644 --- a/src/ytdl_sub/script/types/syntax_tree.py +++ b/src/ytdl_sub/script/types/syntax_tree.py @@ -3,6 +3,7 @@ from typing import Dict from typing import List from typing import Optional +from ytdl_sub.script.types.function import BuiltInFunction from ytdl_sub.script.types.resolvable import Argument from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import String @@ -15,7 +16,7 @@ class SyntaxTree(VariableDependency): ast: List[Argument] @property - def _iterable_arguments(self) -> List[Argument]: + def iterable_arguments(self) -> List[Argument]: return self.ast def resolve( @@ -40,6 +41,30 @@ class SyntaxTree(VariableDependency): # Otherwise, to concat multiple resolved outputs, we must concat as strings return String("".join([str(res) for res in resolved])) + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, VariableDependency], + ) -> Argument | Resolvable: + # Ensure this does not get returned as a SyntaxTree since nesting them is not supported. + maybe_resolvable_values, _ = VariableDependency.try_partial_resolve( + args=self.ast, + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + # If no arguments, must be empty string + if len(maybe_resolvable_values) == 0: + return String(value="") + + # Mimic the above resolve behavior + if len(maybe_resolvable_values) > 1: + return BuiltInFunction(name="concat", args=maybe_resolvable_values) + + return maybe_resolvable_values[0] + @property def maybe_resolvable(self) -> Optional[Resolvable]: """ @@ -47,6 +72,40 @@ class SyntaxTree(VariableDependency): ------- A resolvable if the AST contains a single type that is resolvable. None otherwise. """ - if len(self.ast) == 1 and isinstance(self.ast[0], Resolvable): - return self.ast[0] return None + + def maybe_resolvable_casted(self) -> "SyntaxTree": + """ + Returns + ------- + Optimized SyntaxTree if its deemed resolvable + """ + if len(self.ast) == 1 and isinstance(self.ast[0], Resolvable): + return ResolvedSyntaxTree(self.ast) + return self + + +@dataclass(frozen=True) +class ResolvedSyntaxTree(SyntaxTree): + """ + SyntaxTree with optimized helper functions if it's known to be resolved. + """ + + def resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + custom_functions: Dict[str, VariableDependency], + ) -> Resolvable: + return self.ast[0] + + @property + def maybe_resolvable(self) -> Optional[Resolvable]: + return self.ast[0] + + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, VariableDependency], + ) -> Argument | Resolvable: + return self.ast[0] diff --git a/src/ytdl_sub/script/types/variable_dependency.py b/src/ytdl_sub/script/types/variable_dependency.py index eb5f646e..a185a0af 100644 --- a/src/ytdl_sub/script/types/variable_dependency.py +++ b/src/ytdl_sub/script/types/variable_dependency.py @@ -5,6 +5,7 @@ from typing import Dict from typing import Iterable from typing import List from typing import Set +from typing import Tuple from typing import Type from typing import TypeVar from typing import final @@ -27,7 +28,7 @@ TypeT = TypeVar("TypeT") class VariableDependency(ABC): @property @abstractmethod - def _iterable_arguments(self) -> List[Argument]: + def iterable_arguments(self) -> List[Argument]: """ Returns ------- @@ -38,7 +39,7 @@ class VariableDependency(ABC): self, ttype: Type[TypeT], subclass: bool = False, instance: bool = True ) -> List[TypeT]: output: List[TypeT] = [] - for arg in self._iterable_arguments: + for arg in self.iterable_arguments: if subclass and issubclass(type(arg), ttype): output.append(arg) elif instance and isinstance(arg, ttype): @@ -104,7 +105,7 @@ class VariableDependency(ABC): All CustomFunctions that this depends on. """ output: Set[ParsedCustomFunction] = set() - for arg in self._iterable_arguments: + for arg in self.iterable_arguments: if isinstance(arg, NamedCustomFunction): if not isinstance(arg, FunctionType): # A NamedCustomFunction should also always be a FunctionType @@ -138,6 +139,28 @@ class VariableDependency(ABC): Resolved value """ + @abstractmethod + def partial_resolve( + self, + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, "VariableDependency"], + ) -> Argument | Resolvable: + """ + Parameters + ---------- + resolved_variables + Lookup of variables that have been resolved + unresolved_variables + Lookup of variables that have not been resolved + custom_functions + Lookup of any custom functions that have been parsed + + Returns + ------- + Either a fully resolved value or partially resolved value of the same type. + """ + @classmethod def _resolve_argument_type( cls, @@ -159,10 +182,35 @@ class VariableDependency(ABC): raise UNREACHABLE + @final + def custom_function_dependencies( + self, custom_function_definitions: Dict[str, "VariableDependency"] + ) -> Set[ParsedCustomFunction]: + """ + Parameters + ---------- + custom_function_definitions + Definition of all currently existing custom functions. Needed to check whether + a lambda function's input function is custom or not. + + Returns + ------- + All custom function dependencies + """ + custom_functions = self.custom_functions + for lambda_func in self.lambdas: + if lambda_func.value in custom_function_definitions: + custom_functions.add( + ParsedCustomFunction( + name=lambda_func.value, num_input_args=lambda_func.num_input_args() + ) + ) + return custom_functions + @final def is_subset_of( self, - variables: Iterable[Variable], + variables: Dict[Variable, Resolvable], custom_function_definitions: Dict[str, "VariableDependency"], ) -> bool: """ @@ -170,13 +218,14 @@ class VariableDependency(ABC): ------- True if it contains all input variables as a dependency. False otherwise. """ - for custom_function in self.custom_functions: - if custom_function_definitions[custom_function.name].is_subset_of( + # If there are lambdas, see if they are custom functions. If so, check them + for custom_function in self.custom_function_dependencies(custom_function_definitions): + if not custom_function_definitions[custom_function.name].is_subset_of( variables=variables, custom_function_definitions=custom_function_definitions ): - return True + return False - return not self.variables.issubset(variables) + return all(var in variables for var in self.variables) @final def contains( @@ -189,9 +238,53 @@ class VariableDependency(ABC): ------- True if it contains any of the input variables. False otherwise. """ - for custom_function in self.custom_functions: + # If there are lambdas, see if they are custom functions. If so, check them + for custom_function in self.custom_function_dependencies(custom_function_definitions): if custom_function_definitions[custom_function.name].contains( variables=variables, custom_function_definitions=custom_function_definitions ): return True return len(self.variables.intersection(variables)) > 0 + + @classmethod + def try_partial_resolve( + cls, + args: Iterable[Argument], + resolved_variables: Dict[Variable, Resolvable], + unresolved_variables: Dict[Variable, Argument], + custom_functions: Dict[str, "VariableDependency"], + ) -> Tuple[List[Argument], bool]: + """ + Attempts to resolve a list of arguments. Returns a tuple of them post partially resolved, + and a boolean indicating whether all of them are fully resolved. + """ + maybe_resolvable_args: List[Argument] = [] + is_resolvable = True + for arg in args: + maybe_resolvable_args.append(arg) + + if isinstance(arg, Lambda) and arg.value in custom_functions: + if not custom_functions[arg.value].is_subset_of( + variables=resolved_variables, + custom_function_definitions=custom_functions, + ): + is_resolvable = False + elif isinstance(arg, VariableDependency): + maybe_resolvable_args[-1] = arg.partial_resolve( + resolved_variables=resolved_variables, + unresolved_variables=unresolved_variables, + custom_functions=custom_functions, + ) + + if not isinstance(maybe_resolvable_args[-1], Resolvable): + is_resolvable = False + elif isinstance(arg, Variable): + if arg in resolved_variables: + maybe_resolvable_args[-1] = resolved_variables[arg] + else: + is_resolvable = False + # Could be un unresolvable + if arg in unresolved_variables: + maybe_resolvable_args[-1] = unresolved_variables[arg] + + return maybe_resolvable_args, is_resolvable diff --git a/src/ytdl_sub/script/utils/name_validation.py b/src/ytdl_sub/script/utils/name_validation.py index de187ec8..5b2597ce 100644 --- a/src/ytdl_sub/script/utils/name_validation.py +++ b/src/ytdl_sub/script/utils/name_validation.py @@ -58,3 +58,24 @@ def validate_custom_function_name(custom_function_name: str) -> None: f"Custom function name '%{custom_function_name}' is invalid:" " The name is used by a built-in function and cannot be overwritten." ) + + +def is_function(override_name: str): + """ + Whether the definition is a function or not. + """ + return override_name.startswith("%") + + +def to_function_name(function_key: str) -> str: + """ + Drop the % in %custom_function + """ + return function_key[1:] + + +def to_function_definition_name(function_key: str) -> str: + """ + Add % in %custom_function + """ + return f"%{function_key}" diff --git a/src/ytdl_sub/script/utils/type_checking.py b/src/ytdl_sub/script/utils/type_checking.py index e8a8bc79..7598a526 100644 --- a/src/ytdl_sub/script/utils/type_checking.py +++ b/src/ytdl_sub/script/utils/type_checking.py @@ -2,6 +2,7 @@ import inspect from dataclasses import dataclass from inspect import FullArgSpec +from types import NoneType from typing import Callable from typing import List from typing import Optional @@ -50,7 +51,7 @@ def get_optional_type(optional_type: Type) -> Type[NamedType]: ------- Type within the Optional[Type] """ - return [arg for arg in optional_type.__args__ if arg != type(None)][0] + return [arg for arg in optional_type.__args__ if not isinstance(arg, NoneType)][0] def _is_union_compatible( diff --git a/src/ytdl_sub/subscriptions/base_subscription.py b/src/ytdl_sub/subscriptions/base_subscription.py index 970f142d..553eb74d 100644 --- a/src/ytdl_sub/subscriptions/base_subscription.py +++ b/src/ytdl_sub/subscriptions/base_subscription.py @@ -8,10 +8,15 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset_options import OutputOptions from ytdl_sub.config.preset_options import YTDLOptions +from ytdl_sub.config.validators.variable_validation import ResolutionLevel +from ytdl_sub.config.validators.variable_validation import VariableValidation from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.entries.variables.override_variables import SubscriptionVariables +from ytdl_sub.utils.exceptions import SubscriptionPermissionError +from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.logger import Logger +from ytdl_sub.utils.yaml import dump_yaml from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("subscription") @@ -74,6 +79,14 @@ class BaseSubscription(ABC): } ) + # Validate after adding the subscription name + _ = VariableValidation( + overrides=self.overrides, + downloader_options=self.downloader_options, + output_options=self.output_options, + plugins=self.plugins, + ).ensure_proper_usage() + self._enhanced_download_archive: Optional[EnhancedDownloadArchive] = ( _initialize_download_archive( output_options=self.output_options, @@ -86,14 +99,20 @@ class BaseSubscription(ABC): # Add post-archive variables self.overrides.add( { - SubscriptionVariables.subscription_has_download_archive(): f"""{{ - %bool({self.download_archive.num_entries > 0}) - }}""", + SubscriptionVariables.subscription_has_download_archive(): ( + f"{{%bool({self.download_archive.num_entries > 0})}}" + ), } ) self._exception: Optional[Exception] = None + if not FileHandler.is_path_writable(self.output_directory): + raise SubscriptionPermissionError( + "ytdl-sub does not have write permissions to the output directory: " + f"{self.output_directory}" + ) + @property def download_archive(self) -> EnhancedDownloadArchive: """ @@ -236,4 +255,22 @@ class BaseSubscription(ABC): ------- Subscription in yaml format """ - return self._preset_options.yaml + return self._preset_options.yaml(subscription_only=False) + + def resolved_yaml(self, resolution_level: int = ResolutionLevel.RESOLVE) -> str: + """ + Returns + ------- + Human-readable, condensed YAML definition of the subscription. + """ + if resolution_level == ResolutionLevel.ORIGINAL: + return self._preset_options.yaml(subscription_only=True) + + out = VariableValidation( + overrides=self.overrides, + downloader_options=self.downloader_options, + output_options=self.output_options, + plugins=self.plugins, + resolution_level=resolution_level, + ).ensure_proper_usage(partial_resolve_formatters=True) + return dump_yaml(out) diff --git a/src/ytdl_sub/subscriptions/subscription_download.py b/src/ytdl_sub/subscriptions/subscription_download.py index 5d1b4cb3..082260cc 100644 --- a/src/ytdl_sub/subscriptions/subscription_download.py +++ b/src/ytdl_sub/subscriptions/subscription_download.py @@ -17,6 +17,7 @@ from ytdl_sub.downloaders.source_plugin import SourcePlugin from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.entries.entry import Entry +from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.subscriptions.base_subscription import BaseSubscription from ytdl_sub.subscriptions.subscription_ytdl_options import SubscriptionYTDLOptions from ytdl_sub.utils.datetime import to_date_range @@ -72,6 +73,7 @@ class SubscriptionDownload(BaseSubscription, ABC): file_metadata=entry_metadata, output_file_name=output_file_name, entry=entry, + preserve_mtime=self.output_options.preserve_mtime, ) # Always pretend to include the thumbnail in a dry-run @@ -86,6 +88,7 @@ class SubscriptionDownload(BaseSubscription, ABC): output_file_name=output_thumbnail_name, entry=entry, copy_file=True, + preserve_mtime=self.output_options.preserve_mtime, ) elif not entry.is_thumbnail_downloaded(): logger.warning( @@ -105,6 +108,7 @@ class SubscriptionDownload(BaseSubscription, ABC): file_name=entry.get_download_info_json_name(), output_file_name=output_info_json_name, entry=entry, + preserve_mtime=self.output_options.preserve_mtime, ) def _delete_working_directory(self, is_error: bool = False) -> None: @@ -151,8 +155,8 @@ class SubscriptionDownload(BaseSubscription, ABC): keep_max_files: Optional[int] = None if self.output_options.keep_max_files: # validated it can be cast to int within the validator - keep_max_files = int( - self.overrides.apply_formatter(self.output_options.keep_max_files) + keep_max_files = self.overrides.apply_formatter( + self.output_options.keep_max_files, expected_type=int ) if date_range_to_keep or self.output_options.keep_max_files is not None: @@ -215,9 +219,23 @@ class SubscriptionDownload(BaseSubscription, ABC): FileHandler.delete(entry.get_download_thumbnail_path()) FileHandler.delete(entry.get_download_info_json_path()) - @classmethod - def _preprocess_entry(cls, plugins: List[Plugin], entry: Entry) -> Optional[Entry]: + def _preprocess_entry(self, plugins: List[Plugin], entry: Entry) -> Optional[Entry]: maybe_entry: Optional[Entry] = entry + + # Inject OutputOption variables here + entry.add( + { + VARIABLES.ytdl_sub_keep_files_date_eval: ( + self.output_options.keep_files_date_eval.format_string + ) + } + ) + + # Run it to make sure it's actually a standardized date + _ = self.overrides.apply_formatter( + formatter=self.output_options.keep_files_date_eval, entry=entry + ) + for plugin in PluginMapping.order_plugins_by( plugins, PluginOperation.MODIFY_ENTRY_METADATA ): @@ -244,6 +262,9 @@ class SubscriptionDownload(BaseSubscription, ABC): if self.maintain_download_archive: self.download_archive.save_download_mappings() + for plugin in PluginMapping.order_plugins_by(plugins, PluginOperation.POST_COMPLETION): + plugin.post_completion_entry(file_metadata=entry_metadata) + def _process_entry( self, plugins: List[Plugin], dry_run: bool, entry: Entry, entry_metadata: FileMetadata ) -> None: @@ -334,6 +355,34 @@ class SubscriptionDownload(BaseSubscription, ABC): return self.download_archive.get_file_handler_transaction_log() + def get_ytdl_options( + self, plugins: Optional[List[Plugin]], dry_run: bool + ) -> SubscriptionYTDLOptions: + """ + Parameters + ---------- + plugins + Optional. If not provided, will reinitialize them + dry_run + Whether its dry run or not + + Returns + ------- + SubscriptionYTDLOptions + Both metadata and download ytdl-options + """ + if plugins is None: + plugins = self._initialize_plugins() + + return SubscriptionYTDLOptions( + preset=self._preset_options, + plugins=plugins, + enhanced_download_archive=self.download_archive, + overrides=self.overrides, + working_directory=self.working_directory, + dry_run=dry_run, + ) + def download(self, dry_run: bool = False) -> FileHandlerTransactionLog: """ Performs the subscription download @@ -353,14 +402,7 @@ class SubscriptionDownload(BaseSubscription, ABC): logging.info("Skipping %s", self.name) return FileHandlerTransactionLog() - subscription_ytdl_options = SubscriptionYTDLOptions( - preset=self._preset_options, - plugins=plugins, - enhanced_download_archive=self.download_archive, - overrides=self.overrides, - working_directory=self.working_directory, - dry_run=dry_run, - ) + subscription_ytdl_options = self.get_ytdl_options(plugins=plugins, dry_run=dry_run) downloader = MultiUrlDownloader( options=self.downloader_options, @@ -405,15 +447,7 @@ class SubscriptionDownload(BaseSubscription, ABC): self.download_archive.reinitialize(dry_run=dry_run) plugins = self._initialize_plugins() - - subscription_ytdl_options = SubscriptionYTDLOptions( - preset=self._preset_options, - plugins=plugins, - enhanced_download_archive=self.download_archive, - overrides=self.overrides, - working_directory=self.working_directory, - dry_run=dry_run, - ) + subscription_ytdl_options = self.get_ytdl_options(plugins=plugins, dry_run=dry_run) # Re-add the original downloader class' plugins plugins.extend( diff --git a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py index 12efb951..c689c88b 100644 --- a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +++ b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py @@ -19,6 +19,7 @@ from ytdl_sub.plugins.match_filters import MatchFiltersPlugin from ytdl_sub.plugins.match_filters import combine_filters from ytdl_sub.plugins.match_filters import default_filters from ytdl_sub.plugins.subtitles import SubtitlesPlugin +from ytdl_sub.plugins.throttle_protection import ThrottleProtectionPlugin from ytdl_sub.utils.ffmpeg import FFMPEG from ytdl_sub.utils.logger import Logger from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -93,8 +94,8 @@ class SubscriptionYTDLOptions: self._enhanced_download_archive.working_ytdl_file_path ) if self._preset.output_options.keep_max_files: - keep_max_files = int( - self._overrides.apply_formatter(self._preset.output_options.keep_max_files) + keep_max_files = self._overrides.apply_formatter( + self._preset.output_options.keep_max_files, expected_type=int ) if keep_max_files > 0: # yt-dlp has a weird bug with max_downloads=1, set to 2 for safe measure @@ -175,6 +176,7 @@ class SubscriptionYTDLOptions: self._global_options, self._output_options, self._plugin_match_filters, + self._plugin_ytdl_options(ThrottleProtectionPlugin), self._plugin_ytdl_options(FormatPlugin), self._plugin_ytdl_options(AudioExtractPlugin), # will override format self._user_ytdl_options, # user ytdl options... diff --git a/src/ytdl_sub/utils/chapters.py b/src/ytdl_sub/utils/chapters.py index 9ccfa2c0..bdd52f34 100644 --- a/src/ytdl_sub/utils/chapters.py +++ b/src/ytdl_sub/utils/chapters.py @@ -220,10 +220,17 @@ class Chapters: # Timestamp captured, store it if match := Timestamp.TIMESTAMP_REGEX.search(line): timestamp_str = match.group(1) - timestamps.append(Timestamp.from_str(timestamp_str)) # Remove timestamp and surrounding whitespace from it title_str = re.sub(f"\\s*{re.escape(timestamp_str)}\\s*", " ", line).strip() + + timestamp = Timestamp.from_str(timestamp_str) + if timestamps and (timestamps[-1].timestamp_sec == timestamp.timestamp_sec): + # duplicate timestamp... combine into one chapter + titles[-1] += " // " + title_str + continue + timestamps.append(timestamp) + titles.append(title_str) # If more than 3 timestamps were parsed, return it diff --git a/src/ytdl_sub/utils/exceptions.py b/src/ytdl_sub/utils/exceptions.py index bfc222ec..76d80780 100644 --- a/src/ytdl_sub/utils/exceptions.py +++ b/src/ytdl_sub/utils/exceptions.py @@ -40,3 +40,7 @@ class FileNotDownloadedException(ValueError): class ExperimentalFeatureNotEnabled(ValidationException): """Feature is not enabled for usage""" + + +class SubscriptionPermissionError(ValidationException): + """Early-caught permission error""" diff --git a/src/ytdl_sub/utils/ffmpeg.py b/src/ytdl_sub/utils/ffmpeg.py index 6d16814d..52374464 100644 --- a/src/ytdl_sub/utils/ffmpeg.py +++ b/src/ytdl_sub/utils/ffmpeg.py @@ -201,7 +201,11 @@ def add_ffmpeg_metadata_key_values(file_path: str, key_values: Dict[str, str]) - "-dn", # ignore data streams ] for key, value in key_values.items(): - ffmpeg_args.extend(["-metadata", f"{key}={value}"]) + # Some special characters (utf-16 maybe?) have null-byte + # which results in ffmpeg error, remove them outright + value_null_byte_removed = value.replace("\0", "") + ffmpeg_args.extend(["-metadata", f"{key}={value_null_byte_removed}"]) + ffmpeg_args.extend(["-codec", "copy", "-bitexact", tmp_file_path]) FFMPEG.run(ffmpeg_args) diff --git a/src/ytdl_sub/utils/file_handler.py b/src/ytdl_sub/utils/file_handler.py index f7c6e44f..1db4bb6c 100644 --- a/src/ytdl_sub/utils/file_handler.py +++ b/src/ytdl_sub/utils/file_handler.py @@ -379,6 +379,36 @@ class FileHandler: """ return self._file_handler_transaction_log + @classmethod + def is_path_writable(cls, src_file_path: Union[str, Path]) -> bool: + """ + Check whether a path is writable. If it does not exist, try to find the base directory + and check permissions on that. + """ + path = os.path.abspath(src_file_path) + + while not os.path.exists(path): + new_path = os.path.dirname(path) + if new_path == path: # reached root + break + path = new_path + + return os.access(path, os.W_OK) + + @classmethod + def is_file_existent(cls, file_path: Union[str, Path]) -> bool: + """ + Check whether a file exists. + """ + return os.path.isfile(file_path) + + @classmethod + def is_file_readable(cls, file_path: Union[str, Path]) -> bool: + """ + Check whether a file exists and is readable. + """ + return cls.is_file_existent(file_path) and os.access(file_path, os.R_OK) + @classmethod def copy(cls, src_file_path: Union[str, Path], dst_file_path: Union[str, Path]): """ @@ -430,6 +460,25 @@ class FileHandler: if os.path.isfile(file_path): os.remove(file_path) + @classmethod + def set_mtime(cls, file_path: Union[str, Path], mtime: float): + """ + Set the modification time of a file + + Parameters + ---------- + file_path + Path to the file to modify + mtime + Modification time as a Unix timestamp + """ + try: + # Set both access time and modification time + os.utime(file_path, (mtime, mtime)) + except OSError: + # If file operation fails, silently continue + pass + def move_file_to_output_directory( self, file_name: str, diff --git a/src/ytdl_sub/utils/file_lock.py b/src/ytdl_sub/utils/file_lock.py index a766dd6c..64fcf247 100644 --- a/src/ytdl_sub/utils/file_lock.py +++ b/src/ytdl_sub/utils/file_lock.py @@ -45,6 +45,7 @@ else: ) try: + os.makedirs(os.path.dirname(lock_file_path), exist_ok=True) lock_file = open(lock_file_path, "w", encoding="utf-8") except FileNotFoundError as exc: # pylint: disable=line-too-long diff --git a/src/ytdl_sub/utils/script.py b/src/ytdl_sub/utils/script.py index e3a17b2f..7252d7c4 100644 --- a/src/ytdl_sub/utils/script.py +++ b/src/ytdl_sub/utils/script.py @@ -2,12 +2,14 @@ import json import re from typing import Any from typing import Dict +from typing import Optional from ytdl_sub.script.parser import parse -from ytdl_sub.script.script import _is_function +from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.array import UnresolvedArray from ytdl_sub.script.types.function import BuiltInFunction from ytdl_sub.script.types.function import Function +from ytdl_sub.script.types.map import Map from ytdl_sub.script.types.map import UnresolvedMap from ytdl_sub.script.types.resolvable import Argument from ytdl_sub.script.types.resolvable import Boolean @@ -15,8 +17,10 @@ from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Lambda from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import UNREACHABLE +from ytdl_sub.script.utils.name_validation import is_function # pylint: disable=too-many-return-statements @@ -30,10 +34,26 @@ class ScriptUtils: sanitized_variables = { f"{name}_sanitized": f"{{%sanitize({name})}}" for name in variables.keys() - if not _is_function(name) + if not is_function(name) } return dict(variables, **sanitized_variables) + @classmethod + def add_sanitized_parsed_variables( + cls, variables: Dict[str, SyntaxTree] + ) -> Dict[str, SyntaxTree]: + """ + Helper to add sanitized variables to a Script + """ + sanitized_variables = { + f"{name}_sanitized": SyntaxTree( + ast=[BuiltInFunction(name="sanitize", args=[Variable(name)])] + ) + for name in variables.keys() + if not is_function(name) + } + return variables | sanitized_variables + @classmethod def to_script(cls, value: Any, sort_keys: bool = True) -> str: """ @@ -67,9 +87,8 @@ class ScriptUtils: ast = parse(text=value).ast if len(ast) == 1: return ast[0] - return BuiltInFunction( - name="concat", args=[BuiltInFunction(name="string", args=[arg]) for arg in ast] - ) + + return BuiltInFunction(name="concat", args=ast) if isinstance(value, bool): return Boolean(value) if isinstance(value, int): @@ -88,6 +107,19 @@ class ScriptUtils: raise UNREACHABLE + @classmethod + def _get_quote_char(cls, arg: str) -> str: + contains_single_quote = "'" in arg + contains_double_quote = '"' in arg + + if not contains_single_quote and not contains_double_quote: + return '"' + if not contains_single_quote and contains_double_quote: + return "'" + if contains_single_quote and not contains_double_quote: + return '"' + return "'''" + @classmethod def _to_script_code(cls, arg: Argument, top_level: bool = False) -> str: if not top_level and isinstance(arg, (Integer, Boolean, Float)): @@ -96,7 +128,10 @@ class ScriptUtils: if isinstance(arg, String): if arg.native == "": return "" if top_level else "''" - return arg.native if top_level else f"'''{arg.native}'''" + + quote = cls._get_quote_char(arg.native) + + return arg.native if top_level else f"{quote}{arg.native}{quote}" if isinstance(arg, Integer): out = f"%int({arg.native})" @@ -104,9 +139,9 @@ class ScriptUtils: out = f"%bool({arg.native})" elif isinstance(arg, Float): out = f"%float({arg.native})" - elif isinstance(arg, UnresolvedArray): + elif isinstance(arg, (Array, UnresolvedArray)): out = f"[ {', '.join(cls._to_script_code(val) for val in arg.value)} ]" - elif isinstance(arg, UnresolvedMap): + elif isinstance(arg, (Map, UnresolvedMap)): kv_list = ( f"{cls._to_script_code(key)}: {cls._to_script_code(val)}" for key, val in arg.value.items() @@ -122,11 +157,43 @@ class ScriptUtils: raise UNREACHABLE return f"{{ {out} }}" if top_level else out + @classmethod + def _is_top_level_string(cls, tree: SyntaxTree) -> Optional[str]: + if not ( + len(tree.ast) == 1 + and isinstance(tree.ast[0], BuiltInFunction) + and tree.ast[0].name == "concat" + ): + return None + + output = "" + for arg in tree.ast[0].args: + if isinstance(arg, BuiltInFunction) and arg.name == "string" and len(arg.args) == 1: + output += cls._to_script_code(arg.args[0], top_level=True) + else: + output += cls._to_script_code(arg, top_level=True) + + return output + + @classmethod + def _syntax_tree_to_native_script(cls, tree: SyntaxTree) -> str: + + if (output := cls._is_top_level_string(tree)) is not None: + return output + + output = "" + for arg in tree.ast: + output += cls._to_script_code(arg, top_level=True) + return output + @classmethod def to_native_script(cls, value: Any) -> str: """ Converts any JSON-compatible value into equivalent script syntax """ + if isinstance(value, SyntaxTree): + return cls._syntax_tree_to_native_script(value) + return cls._to_script_code(cls._to_script_argument(value), top_level=True) @classmethod diff --git a/src/ytdl_sub/validators/string_datetime.py b/src/ytdl_sub/validators/string_datetime.py index d50be211..53e7bb52 100644 --- a/src/ytdl_sub/validators/string_datetime.py +++ b/src/ytdl_sub/validators/string_datetime.py @@ -12,9 +12,11 @@ class StringDatetimeValidator(OverridesStringFormatterValidator): 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. + 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. """ _expected_value_type_name = "datetime string" diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index 2c709ebb..b9fdd3ea 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -1,9 +1,10 @@ +from datetime import datetime +from typing import Any from typing import Dict from typing import Set from typing import Union from typing import final -from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.script.parser import parse from ytdl_sub.script.script import Script from ytdl_sub.script.types.syntax_tree import SyntaxTree @@ -18,6 +19,8 @@ from ytdl_sub.validators.validators import LiteralDictValidator from ytdl_sub.validators.validators import StringValidator from ytdl_sub.validators.validators import Validator +# pylint: disable=protected-access + class StringFormatterValidator(StringValidator): """ @@ -50,7 +53,10 @@ class StringFormatterValidator(StringValidator): def __init__(self, name, value: str): super().__init__(name=name, value=value) try: - _ = parse(str(value)) + self._parsed = parse( + text=str(value), + name=self.leaf_name, + ) except UserException as exc: raise self._validation_exception(exc) from exc @@ -64,15 +70,60 @@ class StringFormatterValidator(StringValidator): """ return self._value - def post_process(self, resolved: str) -> str: + @property + @final + def parsed(self) -> SyntaxTree: """ Returns ------- - Apply any post processing to the resolved value + The parsed format string. """ + return self._parsed + + def post_process(self, resolved: Any) -> Any: + """ + Returns + ------- + Apply any post processing to the resolved value. Defaults to casting it to string. + """ + return str(resolved) + + +class FloatFormatterValidator(StringFormatterValidator): + _expected_value_type_name = "float" + + def post_process(self, resolved: str) -> float: + try: + out = float(resolved) + except Exception as exc: + raise self._validation_exception( + f"Expected a float, but received '{resolved}'" + ) from exc + + return out + + +class StandardizedDateValidator(StringFormatterValidator): + _expected_value_type_name = "standardized_date" + + def post_process(self, resolved: str) -> str: + try: + datetime.strptime(resolved, "%Y-%m-%d") + except ValueError as exc: + raise self._validation_exception( + f"Expected a standardized date in the form of YYYY-MM-DD, but received '{resolved}'" + ) from exc + return resolved +class BooleanFormatterValidator(StringFormatterValidator): + _expected_value_type_name = "boolean" + + def post_process(self, resolved: Any) -> bool: + return ScriptUtils.bool_formatter_output(output=str(resolved)) + + # pylint: disable=line-too-long class OverridesStringFormatterValidator(StringFormatterValidator): """ @@ -92,20 +143,30 @@ class OverridesStringFormatterValidator(StringFormatterValidator): class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator): _expected_value_type_name = "integer" - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: str) -> int: try: - int(resolved) + out = int(resolved) except Exception as exc: raise self._validation_exception( f"Expected an integer, but received '{resolved}'" ) from exc - - return resolved + return out -class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator): +class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringFormatterValidator): + """ + Float validator but static + """ + + +class OverridesBooleanFormatterValidator( + BooleanFormatterValidator, OverridesStringFormatterValidator +): _expected_value_type_name = "boolean" + def post_process(self, resolved: Any) -> bool: + return ScriptUtils.bool_formatter_output(output=str(resolved)) + class ListFormatterValidator(ListValidator[StringFormatterValidator]): _inner_list_type = StringFormatterValidator @@ -123,18 +184,24 @@ class DictFormatterValidator(LiteralDictValidator): super().__init__(name, value) for key in self._keys: - self._value[key] = self._validate_key(key=key, validator=self._key_validator) + # Gets stored in __validator_dict + _ = self._validate_key(key=key, validator=self._key_validator) @property def dict(self) -> Dict[str, StringFormatterValidator]: """Returns dict with string formatter values""" - return self._value + return self._validator_dict @property def dict_with_format_strings(self) -> Dict[str, str]: - """Returns dict with the format strings themselves""" + """Returns dict with the format strings themselves.""" return {key: string_formatter.format_string for key, string_formatter in self.dict.items()} + @property + def dict_with_parsed_format_strings(self) -> Dict[str, SyntaxTree]: + """Returns dict with the parsed format strings.""" + return {key: string_formatter.parsed for key, string_formatter in self.dict.items()} + class OverridesDictFormatterValidator(DictFormatterValidator): """ @@ -145,7 +212,22 @@ class OverridesDictFormatterValidator(DictFormatterValidator): _key_validator = OverridesStringFormatterValidator +class AnyFormatterValidator(StringFormatterValidator): + """ + Applies no post-processing. + """ + + def post_process(self, resolved: Any) -> Any: + return resolved + + +class AnyOverridesFormatterValidator(AnyFormatterValidator, OverridesStringFormatterValidator): + pass + + class UnstructuredDictFormatterValidator(DictFormatterValidator): + _key_validator = AnyFormatterValidator + def __init__(self, name, value): # Convert the unstructured-ness into a script if isinstance(value, dict): @@ -154,40 +236,30 @@ class UnstructuredDictFormatterValidator(DictFormatterValidator): class UnstructuredOverridesDictFormatterValidator(UnstructuredDictFormatterValidator): - _key_validator = OverridesStringFormatterValidator - - -def to_variable_dependency_format_string(script: Script, parsed_format_string: SyntaxTree) -> str: - """ - Create a dummy format string that contains all variable deps as a string. - """ - dummy_format_string = "" - for var in parsed_format_string.variables: - dummy_format_string += f"{{ {var.name} }}" - # pylint: disable=protected-access - for variable_dependency in script._variables[var.name].variables: - dummy_format_string += f"{{ {variable_dependency.name} }}" - # pylint: enable=protected-access - return dummy_format_string + _key_validator = AnyOverridesFormatterValidator def _validate_formatter( mock_script: Script, unresolved_variables: Set[str], + unresolved_runtime_variables: Set[str], formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], -) -> None: - is_static_formatter = False - unresolvable = unresolved_variables - if isinstance(formatter_validator, OverridesStringFormatterValidator): - is_static_formatter = True - unresolvable = unresolved_variables.union({VARIABLES.entry_metadata.variable_name}) + partial_resolve_entry_formatters: bool, +) -> Any: + parsed = formatter_validator.parsed + if resolved := parsed.maybe_resolvable: + return formatter_validator.post_process(resolved.native) + + is_static_formatter = isinstance(formatter_validator, OverridesStringFormatterValidator) - parsed = parse( - text=formatter_validator.format_string, - ) variable_names = {var.name for var in parsed.variables} custom_function_names = {f"%{func.name}" for func in parsed.custom_functions} + # Add lambda functions to custom function names, if it's custom + for lambda_func in parsed.lambdas: + if lambda_func in mock_script.function_names: + custom_function_names.add(lambda_func.value) + if not variable_names.issubset(mock_script.variable_names): raise StringFormattingVariableNotFoundException( "contains the following variables that do not exist: " @@ -198,20 +270,32 @@ def _validate_formatter( "contains the following custom functions that do not exist: " f"{', '.join(sorted(custom_function_names - mock_script.function_names))}" ) - if unresolved := variable_names.intersection(unresolvable): + if unresolved := variable_names.intersection(unresolved_runtime_variables): raise StringFormattingVariableNotFoundException( "contains the following variables that are unresolved when executing this " f"formatter: {', '.join(sorted(unresolved))}" ) + + if partial_resolve_entry_formatters and not is_static_formatter: + parsed = mock_script.resolve_partial_once( + variable_definitions={"tmp_var": formatter_validator.parsed}, + unresolvable=unresolved_variables, + )["tmp_var"] + try: - mock_script.resolve_once( - { - "tmp_var": to_variable_dependency_format_string( - script=mock_script, parsed_format_string=parsed - ) - }, - unresolvable=unresolvable, - ) + if is_static_formatter: + return formatter_validator.post_process( + mock_script.resolve_once_parsed( + {"tmp_var": formatter_validator.parsed}, + unresolvable=unresolved_variables, + update=True, + )["tmp_var"].native + ) + + if maybe_resolved := parsed.maybe_resolvable: + return formatter_validator.post_process(maybe_resolved) + + return ScriptUtils.to_native_script(parsed) except RuntimeException as exc: if isinstance(exc, ScriptVariableNotResolved) and is_static_formatter: raise StringFormattingVariableNotFoundException( @@ -224,40 +308,59 @@ def _validate_formatter( def validate_formatters( script: Script, unresolved_variables: Set[str], + unresolved_runtime_variables: Set[str], validator: Validator, -) -> None: + partial_resolve_formatters: bool, +) -> Dict: """ Ensure all OverridesStringFormatterValidator's only contain variables from the overrides and resolve. """ + resolved_dict: Dict = {} + if isinstance(validator, DictValidator): - # pylint: disable=protected-access + resolved_dict[validator.leaf_name] = {} # Usage of protected variables in other validators is fine. The reason to keep # them protected is for readability when using them in subscriptions. for validator_value in validator._validator_dict.values(): - validate_formatters( + resolved_dict[validator.leaf_name] |= validate_formatters( script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, validator=validator_value, + partial_resolve_formatters=partial_resolve_formatters, ) - # pylint: enable=protected-access elif isinstance(validator, ListValidator): + resolved_dict[validator.leaf_name] = [] for list_value in validator.list: - validate_formatters( + list_output = validate_formatters( script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, validator=list_value, + partial_resolve_formatters=partial_resolve_formatters, ) + assert len(list_output) == 1 + resolved_dict[validator.leaf_name].append(list(list_output.values())[0]) elif isinstance(validator, (StringFormatterValidator, OverridesStringFormatterValidator)): - _validate_formatter( + resolved_dict[validator.leaf_name] = _validate_formatter( mock_script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, formatter_validator=validator, + partial_resolve_entry_formatters=partial_resolve_formatters, ) elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): + resolved_dict[validator.leaf_name] = {} for validator_value in validator.dict.values(): - _validate_formatter( + resolved_dict[validator.leaf_name] |= _validate_formatter( mock_script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, formatter_validator=validator_value, + partial_resolve_entry_formatters=partial_resolve_formatters, ) + else: + resolved_dict[validator.leaf_name] = validator._value + + return resolved_dict diff --git a/src/ytdl_sub/validators/string_select_validator.py b/src/ytdl_sub/validators/string_select_validator.py index 24fe767d..9f28fcd4 100644 --- a/src/ytdl_sub/validators/string_select_validator.py +++ b/src/ytdl_sub/validators/string_select_validator.py @@ -1,5 +1,6 @@ from typing import Set +from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator from ytdl_sub.validators.validators import StringValidator @@ -17,3 +18,17 @@ class StringSelectValidator(StringValidator): raise self._validation_exception( f"Must be one of the following values: {', '.join(self._select_values)}" ) + + +class OverridesStringSelectValidator(OverridesStringFormatterValidator): + _expected_value_type_name = "overrides select" + + _select_values: Set[str] = set() + + def post_process(self, resolved: str) -> str: + if resolved not in self._select_values: + raise self._validation_exception( + f"Must be one of the following values: {', '.join(sorted(self._select_values))}" + ) + + return resolved diff --git a/src/ytdl_sub/validators/validators.py b/src/ytdl_sub/validators/validators.py index fbb13c95..6c1356ae 100644 --- a/src/ytdl_sub/validators/validators.py +++ b/src/ytdl_sub/validators/validators.py @@ -95,21 +95,11 @@ class Validator(ABC): @final @property - def _root_name(self) -> str: + def leaf_name(self) -> str: """ Returns ------- - "first" from the first.element.of.the.name - """ - return self._name.split(".")[0] - - @final - @property - def _leaf_name(self) -> str: - """ - Returns - ------- - "first" from the first.element.of.the.name + "name" from the first.element.of.the.name """ return self._name.split(".")[-1] @@ -274,7 +264,7 @@ class DictValidator(Validator): value=self._dict.get(key, default), ) - self.__validator_dict[validator_name] = validator_instance + self.__validator_dict[key] = validator_instance return validator_instance @final diff --git a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py index ec155245..50b12f42 100644 --- a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +++ b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py @@ -1,6 +1,7 @@ import copy import json import os.path +import time from dataclasses import dataclass from datetime import datetime from pathlib import Path @@ -75,7 +76,7 @@ class DownloadMapping: DownloadMapping for the entry """ return DownloadMapping( - upload_date=entry.get(v.upload_date_standardized, str), + upload_date=entry.get(v.ytdl_sub_keep_files_date_eval, str), extractor=entry.download_archive_extractor, file_names=set(), ) @@ -248,20 +249,18 @@ class DownloadMappings: del self._entry_mappings[entry_id] return self - def get_num_entries_with_upload_date(self, upload_date_standardized: str) -> int: + def get_num_entries_with_date(self, standardized_date: str) -> int: """ Parameters ---------- - upload_date_standardized + standardized_date A standardized upload date Returns ------- Number of entries in the mapping with this upload date """ - return len( - [_ for _ in self._entry_mappings.values() if _.upload_date == upload_date_standardized] - ) + return len([_ for _ in self._entry_mappings.values() if _.upload_date == standardized_date]) def get_num_entries(self) -> int: """ @@ -644,6 +643,7 @@ class EnhancedDownloadArchive: output_file_name: Optional[str] = None, entry: Optional[Entry] = None, copy_file: bool = False, + preserve_mtime: bool = False, ): """ Saves a file from the working directory to the output directory and record it in the @@ -662,6 +662,8 @@ class EnhancedDownloadArchive: Optional. Entry that this file belongs to copy_file Optional. If True, copy the file. Move otherwise + preserve_mtime + Optional. If True and entry has upload_date, set file mtime to upload date """ if output_file_name is None: output_file_name = file_name @@ -676,6 +678,22 @@ class EnhancedDownloadArchive: copy_file=copy_file, ) + # Set mtime if preserve_mtime is enabled and we have an entry with upload_date + if preserve_mtime and entry and not self._file_handler.dry_run: + upload_date = entry.get(v.ytdl_sub_keep_files_date_eval, str) + if upload_date: + try: + # Convert YYYY-mm-dd to timestamp + upload_datetime = datetime.strptime(upload_date, "%Y-%m-%d") + upload_timestamp = time.mktime(upload_datetime.timetuple()) + + # Set mtime on the output file + output_file_path = Path(self._file_handler.output_directory) / output_file_name + FileHandler.set_mtime(output_file_path, upload_timestamp) + except (ValueError, OSError): + # If date parsing or file operation fails, silently continue + pass + # Determine if it's the entry file by seeing if the file_name to move matches the entry # download file name is_entry_file = entry and entry.get_download_file_name() == file_name diff --git a/tests/conftest.py b/tests/conftest.py index 12ba38a9..42d82178 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,8 @@ from typing import Callable from typing import Dict from typing import List from typing import Optional +from typing import Tuple +from typing import Union from unittest.mock import patch import pytest @@ -115,7 +117,8 @@ def assert_logs( yield for call_args in patched_debug.call_args_list: - occurrences += int(expected_message in call_args.args[0]) + full_print = call_args.args[0] % call_args.args[1:] + occurrences += int(expected_message in full_print) if expected_occurrences is not None: assert ( @@ -164,19 +167,18 @@ def preset_dict_to_dl_args(preset_dict: Dict) -> str: @pytest.fixture -def preset_dict_to_subscription_yaml_generator() -> Callable: +def subscription_yaml_file_generator() -> Callable: @contextlib.contextmanager - def _preset_dict_to_subscription_yaml_generator(subscription_name: str, preset_dict: Dict): - subscription_dict = {subscription_name: preset_dict} + def _subscription_yaml_file_generator(yaml_dict: Dict): with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as tmp_file: - tmp_file.write(json.dumps(subscription_dict).encode("utf-8")) + tmp_file.write(json.dumps(yaml_dict).encode("utf-8")) try: yield tmp_file.name finally: FileHandler.delete(tmp_file.name) - return _preset_dict_to_subscription_yaml_generator + return _subscription_yaml_file_generator ################################################################################################### @@ -223,8 +225,14 @@ def _load_config(config_path: Path, working_directory: str) -> ConfigFile: @pytest.fixture() -def music_video_subscription_path() -> Path: - return Path("examples/music_video_subscriptions.yaml") +def music_video_subscription_path( + output_directory: str, subscription_yaml_file_generator: Callable +) -> Path: + yaml = load_yaml("examples/music_video_subscriptions.yaml") + yaml["__preset__"]["overrides"]["music_video_directory"] = output_directory + + with subscription_yaml_file_generator(yaml) as mock_filename: + yield mock_filename @pytest.fixture() @@ -238,13 +246,25 @@ def tv_show_config(working_directory, tv_show_config_path) -> ConfigFile: @pytest.fixture() -def tv_show_subscriptions_path() -> Path: - return Path("examples/tv_show_subscriptions.yaml") +def tv_show_subscriptions_path( + output_directory: str, subscription_yaml_file_generator: Callable +) -> Path: + yaml = load_yaml("examples/tv_show_subscriptions.yaml") + yaml["__preset__"]["overrides"]["tv_show_directory"] = output_directory + + with subscription_yaml_file_generator(yaml) as mock_filename: + yield mock_filename @pytest.fixture() -def advanced_tv_show_subscriptions_path() -> Path: - return Path("examples/advanced/tv_show_subscriptions.yaml") +def advanced_tv_show_subscriptions_path( + output_directory: str, subscription_yaml_file_generator: Callable +) -> Path: + yaml = load_yaml("examples/advanced/tv_show_subscriptions.yaml") + yaml["__preset__"] = {"overrides": {"tv_show_directory": output_directory}} + + with subscription_yaml_file_generator(yaml) as mock_filename: + yield mock_filename @pytest.fixture() @@ -264,11 +284,52 @@ def default_config_path(default_config) -> str: @pytest.fixture() -def music_subscriptions_path() -> Path: - return Path("examples/music_subscriptions.yaml") +def music_subscriptions_path( + output_directory: str, subscription_yaml_file_generator: Callable +) -> Path: + yaml = load_yaml("examples/music_subscriptions.yaml") + yaml["__preset__"]["overrides"]["music_directory"] = output_directory + + with subscription_yaml_file_generator(yaml) as mock_filename: + yield mock_filename + + +@pytest.fixture() +def docker_default_subscription_path( + output_directory: str, subscription_yaml_file_generator: Callable +) -> Path: + yaml = load_yaml("docker/root/defaults/subscriptions.yaml") + yaml["__preset__"]["overrides"]["tv_show_directory"] = output_directory + + with subscription_yaml_file_generator(yaml) as mock_filename: + yield mock_filename def mock_run_from_cli(args: str) -> List[Subscription]: args_list = ["ytdl-sub"] + shlex.split(args) with patch.object(sys, "argv", args_list): return main() + + +def get_match_filters( + subscription: Subscription, dry_run: bool, download_filters: bool +) -> Tuple[List[str], List[str]]: + """ + Util function to get match filters from a subscription. + + Returns + ------- + match_filters, breaking_match_filters + """ + options = subscription.get_ytdl_options(plugins=None, dry_run=dry_run) + options_dict = ( + options.download_builder().to_dict() + if download_filters + else options.metadata_builder().to_dict() + ) + if "match_filter" not in options_dict: + return [], [] + + match_filter_str = repr(options_dict["match_filter"]) + out = eval(match_filter_str.split("(", maxsplit=1)[-1].split(")")[0]) + return out diff --git a/tests/e2e/presets/test_soundcloud.py b/tests/e2e/presets/test_soundcloud.py index 8e7f5345..15c6d7d6 100644 --- a/tests/e2e/presets/test_soundcloud.py +++ b/tests/e2e/presets/test_soundcloud.py @@ -1,3 +1,5 @@ +from typing import List + import pytest from conftest import assert_logs from expected_download import assert_expected_downloads @@ -17,6 +19,7 @@ def subscription_dict(output_directory): "subscription_indent_1": "Acoustic", "music_directory": output_directory, }, + "throttle_protection": {"sleep_per_request_s": {"min": 0.1, "max": 0.5}}, } @@ -34,6 +37,21 @@ class TestSoundcloudDiscography: output_directory, dry_run, ): + ignored_mp3s: List[str] = [ + "j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3", + "j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3", + "j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3", + "j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3", + "j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3", + "j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3", + "j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3", + "j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3", + "j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3", + "j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3", + "j_b/[2022] Acoustic Treats/09 - Finding Home.mp3", + "j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3", + "j_b/[2022] Acoustic Treats/11 - Untold History.mp3", + ] discography_subscription = Subscription.from_dict( preset_dict=subscription_dict, preset_name="j_b", @@ -49,6 +67,7 @@ class TestSoundcloudDiscography: output_directory=output_directory, dry_run=dry_run, expected_download_summary_file_name="soundcloud/test_soundcloud_discography.json", + ignore_md5_hashes_for=ignored_mp3s, ) # Ensure another invocation will hit ExistingVideoReached @@ -65,4 +84,5 @@ class TestSoundcloudDiscography: output_directory=output_directory, dry_run=dry_run, expected_download_summary_file_name="soundcloud/test_soundcloud_discography.json", + ignore_md5_hashes_for=ignored_mp3s, ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index c50773ff..b0aab494 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,10 +1,11 @@ import contextlib import os -import shutil from pathlib import Path +from typing import Any from typing import Callable from typing import Dict from typing import List +from typing import Optional from unittest.mock import patch import pytest @@ -15,6 +16,7 @@ from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader from ytdl_sub.downloaders.ytdlp import YTDLP from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.plugins.throttle_protection import ThrottleProtectionPlugin v: VariableDefinitions = VARIABLES @@ -53,6 +55,8 @@ def mock_entry_dict_factory(mock_downloaded_file_path) -> Callable: is_youtube_channel: bool = False, mock_download_to_working_dir: bool = True, is_extracted_audio: bool = False, + release_date: Optional[str] = None, + mock_entry_kwargs: Optional[Dict[str, Any]] = None, ) -> Dict: entry_dict = { v.uid.metadata_key: uid, @@ -71,6 +75,10 @@ def mock_entry_dict_factory(mock_downloaded_file_path) -> Callable: v.description.metadata_key: "The Description", } + # TODO: Make this required eventually + if release_date is not None: + entry_dict[v.release_date.metadata_key] = release_date + if is_youtube_channel: entry_dict[v.playlist_metadata.metadata_key]["thumbnails"] = [ { @@ -98,6 +106,9 @@ def mock_entry_dict_factory(mock_downloaded_file_path) -> Callable: copy_file_fixture( fixture_name="thumb.jpg", output_file_path=mock_downloaded_file_path(f"{uid}.jpg") ) + + if mock_entry_kwargs: + return dict(entry_dict, **mock_entry_kwargs) return entry_dict return _mock_entry_dict_factory @@ -131,7 +142,9 @@ def mock_download_collection_thumbnail(mock_downloaded_file_path): @pytest.fixture def mock_download_collection_entries( - mock_download_collection_thumbnail, mock_entry_dict_factory: Callable, working_directory: str + mock_download_collection_thumbnail, + mock_entry_dict_factory: Callable, + working_directory: str, ): @contextlib.contextmanager def _mock_download_collection_entries_factory( @@ -139,6 +152,7 @@ def mock_download_collection_entries( num_urls: int = 1, is_extracted_audio: bool = False, is_dry_run: bool = False, + mock_entry_kwargs: Optional[Dict[str, Any]] = None, ): is_real_run = not is_dry_run @@ -153,42 +167,50 @@ def mock_download_collection_entries( mock_entry_dict_factory( uid="21-1", upload_date="20210808", + release_date="20010808", playlist_title="Download First", playlist_index=1, playlist_count=4, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), # 1 mock_entry_dict_factory( uid="20-1", upload_date="20200808", + release_date="20000808", playlist_title="Download First", playlist_index=2, playlist_count=4, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), # 2 98 mock_entry_dict_factory( uid="20-2", upload_date="20200808", + release_date="20000808", playlist_title="Download First", playlist_index=3, playlist_count=4, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), # 1 99 mock_entry_dict_factory( uid="20-3", upload_date="20200807", + release_date="20000807", playlist_title="Download First", playlist_index=4, playlist_count=4, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), ] return [ @@ -196,52 +218,62 @@ def mock_download_collection_entries( mock_entry_dict_factory( uid="20-3", upload_date="20200807", + release_date="20000807", playlist_title="Download Second", playlist_index=1, playlist_count=5, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=False, + mock_entry_kwargs=mock_entry_kwargs, ), mock_entry_dict_factory( uid="20-4", upload_date="20200806", + release_date="20000806", playlist_title="Download Second", playlist_index=2, playlist_count=5, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), mock_entry_dict_factory( uid="20-5", upload_date="20200706", + release_date="20000706", playlist_title="Download Second", playlist_index=3, playlist_count=5, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), mock_entry_dict_factory( uid="20-6", upload_date="20200706", + release_date="20000706", playlist_title="Download Second", playlist_index=4, playlist_count=5, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), mock_entry_dict_factory( uid="20-7", upload_date="20200606", + release_date="20000606", playlist_title="Download Second", playlist_index=5, playlist_count=5, is_youtube_channel=is_youtube_channel, is_extracted_audio=is_extracted_audio, mock_download_to_working_dir=is_real_run, + mock_entry_kwargs=mock_entry_kwargs, ), ] @@ -250,6 +282,9 @@ def mock_download_collection_entries( patch.object( MultiUrlDownloader, "_extract_entry_info_with_retry", new=lambda _, entry: entry ), + # Throttle protection is included in all prebuilt presets. Mock the sleep avoid + # actual sleeps + patch.object(ThrottleProtectionPlugin, "perform_sleep", new=lambda _1, _2: None), ): # Stub out metadata. TODO: update this if we do metadata plugins yield diff --git a/tests/integration/plugins/test_file_convert.py b/tests/integration/plugins/test_file_convert.py index 740cea5b..06980b22 100644 --- a/tests/integration/plugins/test_file_convert.py +++ b/tests/integration/plugins/test_file_convert.py @@ -4,7 +4,6 @@ import pytest from expected_download import assert_expected_downloads from expected_transaction_log import assert_transaction_log_matches -from ytdl_sub.entries.entry import ytdl_sub_chapters_from_comments from ytdl_sub.subscriptions.subscription import Subscription diff --git a/tests/integration/plugins/test_output_options.py b/tests/integration/plugins/test_output_options.py index c8665c1e..b65692b5 100644 --- a/tests/integration/plugins/test_output_options.py +++ b/tests/integration/plugins/test_output_options.py @@ -1,3 +1,4 @@ +import re from pathlib import Path from typing import Dict from unittest.mock import patch @@ -11,6 +12,7 @@ from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.downloaders.ytdlp import YTDLP from ytdl_sub.entries.entry import Entry from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail @@ -223,3 +225,25 @@ class TestOutputOptions: dry_run=False, expected_download_summary_file_name="plugins/output_options/test_missing_thumb.json", ) + + def test_invalid_keep_files_date_eval( + self, + config: ConfigFile, + subscription_name: str, + output_options_subscription_dict: Dict, + output_directory: str, + mock_download_collection_entries, + ): + output_options_subscription_dict["output_options"]["keep_files_date_eval"] = "nope" + + expected_error_msg = ( + "Validation error in subscription_test.output_options.keep_files_date_eval: " + "Expected a standardized date in the form of YYYY-MM-DD, but received 'nope'" + ) + + with pytest.raises(ValidationException, match=re.escape(expected_error_msg)): + _ = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=output_options_subscription_dict, + ) diff --git a/tests/integration/plugins/test_throttle_protection.py b/tests/integration/plugins/test_throttle_protection.py index 860e49ca..bd56f652 100644 --- a/tests/integration/plugins/test_throttle_protection.py +++ b/tests/integration/plugins/test_throttle_protection.py @@ -1,10 +1,14 @@ +import re from typing import Dict import pytest from conftest import assert_logs from ytdl_sub.plugins.throttle_protection import logger as throttle_protection_logger +from ytdl_sub.script.functions.print_functions import logger as script_print_logger +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException @pytest.fixture @@ -54,8 +58,8 @@ class TestThrottleProtectionPlugin: ), assert_logs( logger=throttle_protection_logger, - expected_message="Sleeping between downloads for %0.2f seconds", - log_level="debug", + expected_message="Sleeping between downloads for 0.01 seconds", + log_level="info", expected_occurrences=4, ), ): @@ -67,8 +71,8 @@ class TestThrottleProtectionPlugin: ), assert_logs( logger=throttle_protection_logger, - expected_message="Sleeping between subscriptions for %0.2f seconds", - log_level="debug", + expected_message="Sleeping between subscriptions for 0.02 seconds", + log_level="info", expected_occurrences=1, ), ): @@ -92,7 +96,7 @@ class TestThrottleProtectionPlugin: mock_download_collection_entries, disable_value, ): - throttle_subscription_dict["throttle_protection"]["enable"] = disable_value + throttle_subscription_dict["overrides"]["enable_throttle_protection"] = disable_value subscription = Subscription.from_dict( config=config, preset_name=subscription_name, @@ -106,7 +110,7 @@ class TestThrottleProtectionPlugin: assert_logs( logger=throttle_protection_logger, expected_message="Sleeping between downloads for %0.2f seconds", - log_level="debug", + log_level="info", expected_occurrences=0, ), ): @@ -135,7 +139,7 @@ class TestThrottleProtectionPlugin: ), assert_logs( logger=throttle_protection_logger, - expected_message="Reached subscription max downloads of %d", + expected_message="Reached subscription max downloads of 0 for throttle protection", log_level="info", expected_occurrences=1, ), @@ -175,3 +179,216 @@ class TestThrottleProtectionPlugin: transaction_log = subscription.download(dry_run=True) assert transaction_log.is_empty + + +class TestResolutionAssert: + @pytest.mark.parametrize( + "disable_value", + [ + "", + False, + "{bool_false_variable}", + "{empty_string_variable}", + ], + ) + def test_disabled( + self, + config, + subscription_name, + throttle_subscription_dict, + output_directory, + mock_download_collection_entries, + disable_value, + ): + throttle_subscription_dict["overrides"]["enable_resolution_assert"] = disable_value + + with assert_logs( + logger=script_print_logger, + expected_message="Resolution assert is disabled. Use at your own risk!", + log_level="debug", + expected_occurrences=1, + ): + _ = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=throttle_subscription_dict, + ) + + @pytest.mark.parametrize( + "width, height", + [ + (0, 0), # missing + (361, 361), # min value + ], + ) + def test_runs_successfully( + self, + config, + subscription_name, + throttle_subscription_dict, + output_directory, + mock_download_collection_entries, + width, + height, + ): + with assert_logs( + logger=script_print_logger, + expected_message=( + "Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. " + "Disable using the override variable `enable_resolution_assert: False`" + ), + log_level="debug", + expected_occurrences=1, + ): + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=throttle_subscription_dict, + ) + + with ( + mock_download_collection_entries( + is_youtube_channel=False, + num_urls=1, + is_extracted_audio=False, + is_dry_run=True, + mock_entry_kwargs={"height": height, "width": width}, + ), + ): + _ = subscription.download(dry_run=True) + + def test_fails_low_resolution( + self, + config, + subscription_name, + throttle_subscription_dict, + output_directory, + mock_download_collection_entries, + ): + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=throttle_subscription_dict, + ) + + expected_message = ( + "Entry Mock Entry 20-3 downloaded at a low resolution (640x360), " + "you've probably been throttled. " + "Stopping further downloads, wait a few hours and try again. " + "Disable using the override variable `enable_resolution_assert: False`" + ) + + with ( + mock_download_collection_entries( + is_youtube_channel=False, + num_urls=1, + is_extracted_audio=False, + is_dry_run=True, + mock_entry_kwargs={"height": 360, "width": 640}, + ), + pytest.raises(UserThrownRuntimeError, match=re.escape(expected_message)), + ): + _ = subscription.download(dry_run=True) + + def test_ignore_title_low_resolution( + self, + config, + subscription_name, + throttle_subscription_dict, + output_directory, + mock_download_collection_entries, + ): + throttle_subscription_dict["throttle_protection"]["max_downloads_per_subscription"] = { + "max": 1, + "min": 1, + } + throttle_subscription_dict["overrides"]["resolution_assert_ignore_titles"] = [ + "Entry 20-3", + ] + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=throttle_subscription_dict, + ) + + # expected_message = ( + # "Entry Mock Entry 20-3 downloaded at a low resolution (640x360), " + # "you've probably been throttled. " + # "Stopping further downloads, wait a few hours and try again. " + # "Disable using the override variable `enable_resolution_assert: False`" + # ) + + with ( + mock_download_collection_entries( + is_youtube_channel=False, + num_urls=1, + is_extracted_audio=False, + is_dry_run=True, + mock_entry_kwargs={"height": 360, "width": 640}, + ), + assert_logs( + logger=script_print_logger, + expected_message=( + "Mock Entry 20-3 has a match in resolution_assert_ignore_titles, " + "skipping resolution assert." + ), + log_level="info", + expected_occurrences=1, + ), + ): + _ = subscription.download(dry_run=True) + + def test_sleep_per_download_supports_entry_variables( + self, + config, + subscription_name, + throttle_subscription_dict, + output_directory, + mock_download_collection_entries, + ): + throttle_subscription_dict["throttle_protection"]["sleep_per_download_s"] = { + "min": "{%mul(3.14, duration)}", + "max": "{%mul(3.14, duration)}", + } + + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=throttle_subscription_dict, + ) + + with ( + mock_download_collection_entries( + is_youtube_channel=False, + num_urls=1, + is_extracted_audio=False, + mock_entry_kwargs={"duration": 1}, + ), + assert_logs( + logger=throttle_protection_logger, + expected_message="Sleeping between downloads for 3.14 seconds", + log_level="info", + expected_occurrences=4, + ), + ): + _ = subscription.download(dry_run=False) + + def test_sleep_per_subscription_does_not_support_entry_variables( + self, + config, + subscription_name, + throttle_subscription_dict, + output_directory, + mock_download_collection_entries, + ): + throttle_subscription_dict["throttle_protection"]["sleep_per_subscription_s"] = { + "min": "{%mul(3.14, duration)}", + "max": "{%mul(3.14, duration)}", + } + + with pytest.raises(StringFormattingVariableNotFoundException): + _ = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=throttle_subscription_dict, + ) diff --git a/tests/integration/plugins/test_thumbnail_plugins.py b/tests/integration/plugins/test_thumbnail_plugins.py new file mode 100644 index 00000000..bd222ff2 --- /dev/null +++ b/tests/integration/plugins/test_thumbnail_plugins.py @@ -0,0 +1,68 @@ +from typing import Dict + +import pytest +from expected_download import assert_expected_downloads +from expected_transaction_log import assert_transaction_log_matches + +from ytdl_sub.subscriptions.subscription import Subscription + + +@pytest.fixture +def square_thumbnail_subscription_dict(output_directory) -> Dict: + return { + "preset": "Jellyfin Music Videos", + "output_options": {"output_directory": output_directory}, + "square_thumbnail": "{perform_square_thumbnail}", + "embed_thumbnail": "{perform_embed_thumbnail}", + "overrides": { + "music_video_artist": "JMC", + "url": "https://your.name.here", + "perform_square_thumbnail": True, + "perform_embed_thumbnail": False, + }, + } + + +class TestThumbnailPlugins: + @pytest.mark.parametrize("dry_run", [True, False]) + @pytest.mark.parametrize("embed_thumbnail", [True, False]) + def test_thumbnail( + self, + config, + subscription_name, + square_thumbnail_subscription_dict, + output_directory, + mock_download_collection_entries, + dry_run, + embed_thumbnail, + ): + square_thumbnail_subscription_dict["overrides"]["perform_embed_thumbnail"] = embed_thumbnail + + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=square_thumbnail_subscription_dict, + ) + + with mock_download_collection_entries( + is_youtube_channel=False, + num_urls=1, + is_extracted_audio=False, + is_dry_run=dry_run, + ): + transaction_log = subscription.download(dry_run=dry_run) + + expected_filename = "square_thumbnail" + if embed_thumbnail: + expected_filename = "embedded_square_thumbnail" + + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name=f"plugins/thumbnail/{expected_filename}.txt", + ) + assert_expected_downloads( + output_directory=output_directory, + dry_run=dry_run, + expected_download_summary_file_name=f"plugins/thumbnail/{expected_filename}.json", + ) diff --git a/tests/integration/prebuilt_presets/test_filter_duration.py b/tests/integration/prebuilt_presets/test_filter_duration.py new file mode 100644 index 00000000..7faf123f --- /dev/null +++ b/tests/integration/prebuilt_presets/test_filter_duration.py @@ -0,0 +1,111 @@ +import pytest +from expected_transaction_log import assert_transaction_log_matches + +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError +from ytdl_sub.subscriptions.subscription import Subscription + + +@pytest.fixture +def filter_subscription_dict(output_directory): + return { + "preset": [ + "Plex TV Show by Date", + "Filter Duration", + ], + "overrides": {"url": "https://your.name.here", "tv_show_directory": output_directory}, + } + + +class TestFilterKeywords: + + def test_no_overrides( + self, + config, + filter_subscription_dict, + output_directory, + subscription_name, + mock_download_collection_entries, + ): + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=filter_subscription_dict, + ) + + with mock_download_collection_entries( + is_youtube_channel=False, num_urls=1, is_dry_run=True + ): + transaction_log = subscription.download(dry_run=True) + + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_keywords_empty.txt", + ) + + @pytest.mark.parametrize( + "filter_eval, filter_value, filters_all", + [ + ("min", 10, False), + ("max", 100, False), + ("min", 100, True), + ("max", 10, True), + ], + ) + def test_filter_duration( + self, + config, + filter_subscription_dict, + output_directory, + subscription_name, + mock_download_collection_entries, + filter_eval: str, + filter_value: int, + filters_all: bool, + ): + filter_subscription_dict["overrides"][f"filter_duration_{filter_eval}_s"] = filter_value + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=filter_subscription_dict, + ) + + with mock_download_collection_entries( + is_youtube_channel=False, num_urls=1, is_dry_run=True + ): + transaction_log = subscription.download(dry_run=True) + + if filters_all: + assert transaction_log.is_empty + else: + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_duration.txt", + ) + + @pytest.mark.parametrize( + "filter_eval", + ["min", "max"], + ) + def test_error_not_numeric( + self, + config, + filter_subscription_dict, + output_directory, + subscription_name, + mock_download_collection_entries, + filter_eval, + ): + filter_subscription_dict["overrides"][f"filter_duration_{filter_eval}_s"] = "egg" + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=filter_subscription_dict, + ) + + with ( + mock_download_collection_entries(is_youtube_channel=False, num_urls=1, is_dry_run=True), + pytest.raises(UserThrownRuntimeError, match=f"filter_duration args must be numeric"), + ): + _ = subscription.download(dry_run=True) diff --git a/tests/integration/prebuilt_presets/test_tv_show_by_date.py b/tests/integration/prebuilt_presets/test_tv_show_by_date.py index 904642d7..e886e228 100644 --- a/tests/integration/prebuilt_presets/test_tv_show_by_date.py +++ b/tests/integration/prebuilt_presets/test_tv_show_by_date.py @@ -1,19 +1,30 @@ +import re +from typing import Set + import pytest from expected_download import assert_expected_downloads from expected_transaction_log import assert_transaction_log_matches from ytdl_sub.config.config_file import ConfigFile -from ytdl_sub.prebuilt_presets.tv_show import TvShowByDateEpisodeFormattingPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowByDatePresets +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError from ytdl_sub.subscriptions.subscription import Subscription -DEPRECATED_TV_SHOW_PRESET_EQUIVALENTS = { - "Kodi TV Show by Date": "kodi_tv_show_by_date", - "Jellyfin TV Show by Date": "jellyfin_tv_show_by_date", - "Plex TV Show by Date": "plex_tv_show_by_date", -} +DEFAULT_SEASON_ORDERING = "upload-year" +DEFAULT_EPISODE_ORDERING = "upload-month-day" -DEFAULT_EPISODE_ORDERING_PRESET = "season_by_year__episode_by_month_day" +VALID_ORDERING_COMBOS = [ + # upload + ("upload-year", "upload-month-day"), + ("upload-year", "upload-month-day-reversed"), + ("upload-year", "download-index"), + ("upload-year-month", "upload-day"), + # release + ("release-year", "release-month-day"), + ("release-year", "release-month-day-reversed"), + ("release-year", "download-index"), + ("release-year-month", "release-day"), +] class TestPrebuiltTVShowPresets: @@ -24,24 +35,28 @@ class TestPrebuiltTVShowPresets: subscription_name: str, output_directory: str, tv_show_preset: str, - episode_ordering_preset: str, + season_ordering: str, + episode_ordering: str, is_youtube_channel: bool = True, is_many_urls: bool = False, ): - expected_summary_name = "unit/{}/{}/is_yt_{}{}".format( - DEPRECATED_TV_SHOW_PRESET_EQUIVALENTS[tv_show_preset], - episode_ordering_preset, + expected_summary_name = "integration/by-date/{}/{}/{}/is_yt_{}{}".format( + tv_show_preset.split(" ")[0], + season_ordering, + episode_ordering, int(is_youtube_channel), "_many_urls" if is_many_urls else "", ) preset_dict = { - "preset": [tv_show_preset, episode_ordering_preset], + "preset": tv_show_preset, "overrides": { "url": "https://your.name.here", "tv_show_name": "Best Prebuilt TV Show by Date", "tv_show_directory": output_directory, + "tv_show_by_date_season_ordering": season_ordering, + "tv_show_by_date_episode_ordering": episode_ordering, }, } if is_many_urls: @@ -67,16 +82,19 @@ class TestPrebuiltTVShowPresets: ) ###################################### Perform reformat - reformatted_tv_show_structure_preset = "season_by_year__episode_by_download_index" - reformatted_expected_summary_name = "unit/{}/{}/is_yt_{}{}".format( - DEPRECATED_TV_SHOW_PRESET_EQUIVALENTS[tv_show_preset], - reformatted_tv_show_structure_preset, + reformatted_season_ordering = "upload-year" + reformatted_episode_ordering = "download-index" + + reformatted_expected_summary_name = "integration/by-date/{}/{}/{}/is_yt_{}{}".format( + tv_show_preset.split(" ")[0], + reformatted_season_ordering, + reformatted_episode_ordering, int(is_youtube_channel), "_many_urls" if is_many_urls else "", ) reformatted_preset_dict = { - "preset": [tv_show_preset, reformatted_tv_show_structure_preset], + "preset": tv_show_preset, "output_options": { "migrated_download_archive_name": ".ytdl-sub-{tv_show_name_sanitized}-download-archive.json" }, @@ -84,6 +102,8 @@ class TestPrebuiltTVShowPresets: "url": "https://your.name.here", "tv_show_name": "Best Prebuilt TV Show by Date", "tv_show_directory": output_directory, + "tv_show_by_date_season_ordering": reformatted_season_ordering, + "tv_show_by_date_episode_ordering": reformatted_episode_ordering, }, } if is_many_urls: @@ -98,7 +118,7 @@ class TestPrebuiltTVShowPresets: output_directory=output_directory, transaction_log=reformatted_transaction_log, transaction_log_summary_file_name=( - f"{expected_summary_name}_reformatted_to_{reformatted_tv_show_structure_preset}.txt" + f"{expected_summary_name}_reformatted_to_{reformatted_season_ordering}.txt" ), ) assert_expected_downloads( @@ -123,11 +143,13 @@ class TestPrebuiltTVShowPresets: subscription_name=subscription_name, output_directory=output_directory, tv_show_preset=tv_show_preset, - episode_ordering_preset=DEFAULT_EPISODE_ORDERING_PRESET, + season_ordering=DEFAULT_SEASON_ORDERING, + episode_ordering=DEFAULT_EPISODE_ORDERING, ) @pytest.mark.parametrize( - "episode_ordering_preset", TvShowByDateEpisodeFormattingPresets.preset_names + "season_ordering, episode_ordering", + VALID_ORDERING_COMBOS, ) def test_episode_ordering_presets( self, @@ -135,7 +157,8 @@ class TestPrebuiltTVShowPresets: subscription_name, output_directory, mock_download_collection_entries, - episode_ordering_preset: str, + season_ordering: str, + episode_ordering: str, ): with mock_download_collection_entries( @@ -146,5 +169,91 @@ class TestPrebuiltTVShowPresets: subscription_name=subscription_name, output_directory=output_directory, tv_show_preset="Kodi TV Show by Date", - episode_ordering_preset=episode_ordering_preset, + season_ordering=season_ordering, + episode_ordering=episode_ordering, ) + + def test_invalid_season_ordering( + self, config, subscription_name, output_directory, mock_download_collection_entries + ): + expected_message = ( + "tv_show_by_date_season_ordering must be one of the following: " + '"upload-year", ' + '"upload-year-month", ' + '"release-year", ' + '"release-year-month"' + ) + + with ( + mock_download_collection_entries(is_youtube_channel=True, num_urls=1), + pytest.raises(UserThrownRuntimeError, match=re.escape(expected_message)), + ): + self.run( + config=config, + subscription_name=subscription_name, + output_directory=output_directory, + tv_show_preset="Kodi TV Show by Date", + season_ordering="nope", + episode_ordering=DEFAULT_EPISODE_ORDERING, + ) + + def test_invalid_episode_ordering( + self, config, subscription_name, output_directory, mock_download_collection_entries + ): + expected_message = ( + "tv_show_by_date_episode_ordering must be one of the following: " + '"upload-day", ' + '"upload-month-day", ' + '"upload-month-day-reversed", ' + '"release-day", ' + '"release-month-day", ' + '"release-month-day-reversed", ' + '"download-index"' + ) + + with ( + mock_download_collection_entries(is_youtube_channel=True, num_urls=1), + pytest.raises(UserThrownRuntimeError, match=re.escape(expected_message)), + ): + self.run( + config=config, + subscription_name=subscription_name, + output_directory=output_directory, + tv_show_preset="Kodi TV Show by Date", + season_ordering=DEFAULT_SEASON_ORDERING, + episode_ordering="not-a-valid", + ) + + def test_invalid_season_episode_ordering_combo( + self, config, subscription_name, output_directory, mock_download_collection_entries + ): + expected_message = ( + "Detected incompatibility between tv_show_by_date_season_ordering " + "and tv_show_by_date_episode_ordering. Ensure you are not using both " + "upload and release date, and that the year/month/day are included in " + "the combined season and episode." + ) + + possible_seasons: Set[str] = set() + possible_episodes: Set[str] = set() + for season_ordering, episode_ordering in VALID_ORDERING_COMBOS: + possible_seasons.add(season_ordering) + possible_episodes.add(episode_ordering) + + for season_ordering in possible_seasons: + for episode_ordering in possible_episodes: + if (season_ordering, episode_ordering) in VALID_ORDERING_COMBOS: + continue + + with ( + mock_download_collection_entries(is_youtube_channel=True, num_urls=1), + pytest.raises(UserThrownRuntimeError, match=re.escape(expected_message)), + ): + self.run( + config=config, + subscription_name=subscription_name, + output_directory=output_directory, + tv_show_preset="Kodi TV Show by Date", + season_ordering=season_ordering, + episode_ordering=episode_ordering, + ) diff --git a/tests/integration/prebuilt_presets/test_tv_show_collection.py b/tests/integration/prebuilt_presets/test_tv_show_collection.py index b4954e6e..2061efd1 100644 --- a/tests/integration/prebuilt_presets/test_tv_show_collection.py +++ b/tests/integration/prebuilt_presets/test_tv_show_collection.py @@ -1,3 +1,4 @@ +import re from typing import Dict from typing import List @@ -6,17 +7,11 @@ from expected_download import assert_expected_downloads from expected_transaction_log import assert_transaction_log_matches from ytdl_sub.config.config_file import ConfigFile -from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionEpisodeFormattingPresets from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionPresets +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError from ytdl_sub.subscriptions.subscription import Subscription -DEPRECATED_TV_SHOW_PRESET_EQUIVALENTS = { - "Kodi TV Show Collection": "kodi_tv_show_collection", - "Jellyfin TV Show Collection": "jellyfin_tv_show_collection", - "Plex TV Show Collection": "plex_tv_show_collection", -} - -DEFAULT_EPISODE_ORDERING_PRESET = "season_by_collection__episode_by_year_month_day" +DEFAULT_EPISODE_ORDERING = "upload-year-month-day" class TestPrebuiltTvShowCollectionPresets: @@ -27,22 +22,19 @@ class TestPrebuiltTvShowCollectionPresets: subscription_name: str, output_directory: str, media_player_preset: str, - tv_show_structure_preset: str, + episode_ordering: str, season_indices: List[int], is_youtube_channel: bool = True, ): - expected_summary_name = "unit/{}/{}/s_{}/is_yt_{}".format( - DEPRECATED_TV_SHOW_PRESET_EQUIVALENTS[media_player_preset], - tv_show_structure_preset, + expected_summary_name = "integration/collection/{}/{}/s_{}/is_yt_{}".format( + media_player_preset.split(" ")[0], + episode_ordering, len(season_indices), int(is_youtube_channel), ) - parent_presets: List[str] = [media_player_preset, tv_show_structure_preset] overrides: Dict[str, str] = {} for season_index in season_indices: - parent_presets.append(f"collection_season_{season_index}") - overrides = dict( overrides, **{ @@ -55,12 +47,13 @@ class TestPrebuiltTvShowCollectionPresets: config=config, preset_name=subscription_name, preset_dict={ - "preset": parent_presets, + "preset": media_player_preset, "overrides": dict( overrides, **{ "tv_show_name": "Best Prebuilt TV Show Collection", "tv_show_directory": output_directory, + "tv_show_collection_episode_ordering": episode_ordering, }, ), }, @@ -79,12 +72,10 @@ class TestPrebuiltTvShowCollectionPresets: ) ###################################### Perform reformat - reformatted_tv_show_structure_preset = ( - "season_by_collection__episode_by_playlist_index_reversed" - ) - reformatted_expected_summary_name = "unit/{}/{}/s_{}/is_yt_{}".format( - DEPRECATED_TV_SHOW_PRESET_EQUIVALENTS[media_player_preset], - reformatted_tv_show_structure_preset, + reformatted_tv_show_collection_episode_ordering = "playlist-index-reversed" + reformatted_expected_summary_name = "integration/collection/{}/{}/s_{}/is_yt_{}".format( + media_player_preset.split(" ")[0], + reformatted_tv_show_collection_episode_ordering, len(season_indices), int(is_youtube_channel), ) @@ -93,7 +84,7 @@ class TestPrebuiltTvShowCollectionPresets: config=config, preset_name=subscription_name, preset_dict={ - "preset": parent_presets + [reformatted_tv_show_structure_preset], + "preset": media_player_preset, "output_options": { "migrated_download_archive_name": ".ytdl-sub-{tv_show_name_sanitized}-download-archive.json" }, @@ -102,6 +93,7 @@ class TestPrebuiltTvShowCollectionPresets: **{ "tv_show_name": "Best Prebuilt TV Show Collection", "tv_show_directory": output_directory, + "tv_show_collection_episode_ordering": reformatted_tv_show_collection_episode_ordering, }, ), }, @@ -112,7 +104,7 @@ class TestPrebuiltTvShowCollectionPresets: output_directory=output_directory, transaction_log=reformatted_transaction_log, transaction_log_summary_file_name=( - f"{expected_summary_name}_reformatted_to_{reformatted_tv_show_structure_preset}.txt" + f"{expected_summary_name}_reformatted_to_{reformatted_tv_show_collection_episode_ordering}.txt" ), ) assert_expected_downloads( @@ -141,12 +133,20 @@ class TestPrebuiltTvShowCollectionPresets: subscription_name=subscription_name, output_directory=output_directory, media_player_preset=media_player_preset, - tv_show_structure_preset=DEFAULT_EPISODE_ORDERING_PRESET, + episode_ordering=DEFAULT_EPISODE_ORDERING, season_indices=season_indices, ) @pytest.mark.parametrize( - "episode_ordering_preset", TvShowCollectionEpisodeFormattingPresets.preset_names + "episode_ordering", + [ + "upload-year-month-day", + "upload-year-month-day-reversed", + "release-year-month-day", + "release-year-month-day-reversed", + "playlist-index", + "playlist-index-reversed", + ], ) @pytest.mark.parametrize("season_indices", [[1], [1, 2]]) def test_episode_ordering_presets( @@ -155,7 +155,7 @@ class TestPrebuiltTvShowCollectionPresets: subscription_name, output_directory, mock_download_collection_entries, - episode_ordering_preset: str, + episode_ordering: str, season_indices: List[int], ): @@ -167,6 +167,32 @@ class TestPrebuiltTvShowCollectionPresets: subscription_name=subscription_name, output_directory=output_directory, media_player_preset="Kodi TV Show Collection", - tv_show_structure_preset=episode_ordering_preset, + episode_ordering=episode_ordering, season_indices=season_indices, ) + + def test_invalid_episode_ordering( + self, config, subscription_name, output_directory, mock_download_collection_entries + ): + expected_message = ( + "tv_show_collection_episode_ordering must be one of the following: " + '"upload-year-month-day", ' + '"upload-year-month-day-reversed", ' + '"release-year-month-day", ' + '"release-year-month-day-reversed", ' + '"playlist-index", ' + '"playlist-index-reversed"' + ) + + with ( + mock_download_collection_entries(is_youtube_channel=True, num_urls=1), + pytest.raises(UserThrownRuntimeError, match=re.escape(expected_message)), + ): + self.run( + config=config, + subscription_name=subscription_name, + output_directory=output_directory, + media_player_preset="Kodi TV Show Collection", + episode_ordering="does-not-exist", + season_indices=[1], + ) diff --git a/tests/integration/prebuilt_presets/test_url.py b/tests/integration/prebuilt_presets/test_url.py new file mode 100644 index 00000000..0da8d4b5 --- /dev/null +++ b/tests/integration/prebuilt_presets/test_url.py @@ -0,0 +1,79 @@ +import pytest +from conftest import assert_logs + +from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader +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.script.variable_definitions import VARIABLES +from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.subscriptions.subscription import Subscription + +v: VariableDefinitions = VARIABLES + + +@pytest.fixture +def subscription_dict(output_directory): + return { + "preset": [ + "Plex TV Show by Date", + "Filter Keywords", + ], + "overrides": { + "url": "https://your.name.here", + "tv_show_directory": output_directory, + "title_include_keywords": ["MOCK ENTRY 20-3"], + "modified_webpage_url": "{webpage_url}_append_test", + }, + } + + +class TestUrl: + + def test_modified_url_when_downloading( + self, + config, + subscription_dict, + subscription_name, + working_directory, + ): + subscription = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=subscription_dict, + ) + + assert set( + url.webpage_url.format_string for url in subscription.downloader_options.urls.list + ) == {"{modified_webpage_url}"} + downloader = MultiUrlDownloader( + options=subscription.downloader_options, + enhanced_download_archive=subscription.download_archive, + download_ytdl_options=YTDLOptionsBuilder(), + metadata_ytdl_options=YTDLOptionsBuilder(), + overrides=subscription.overrides, + ) + + entry = Entry( + entry_dict={ + v.uid.metadata_key: "0", + v.extractor.metadata_key: "test", + v.extractor_key.metadata_key: "test", + v.epoch.metadata_key: 123, + v.webpage_url.metadata_key: "youtube.com/test_url", + v.ext.metadata_key: "mp4", + }, + working_directory=working_directory, + ) + + entry.initialize_base_script() + + entry.add_injected_variables( + download_entry=entry, + download_idx=0, + upload_date_idx=0, + ).initialize_script(subscription.overrides) + + entry.add({v.ytdl_sub_input_url_index.variable_name: 0}) + + assert downloader.webpage_url(entry) == "youtube.com/test_url_append_test" diff --git a/tests/resources.py b/tests/resources.py index c1aa18d4..a327be02 100644 --- a/tests/resources.py +++ b/tests/resources.py @@ -1,18 +1,37 @@ +import json import os import shutil from pathlib import Path +from typing import Dict DISABLE_YOUTUBE_TESTS: bool = True REGENERATE_FIXTURES: bool = False RESOURCE_PATH: Path = Path("tests") / "resources" _FILE_FIXTURE_PATH: Path = RESOURCE_PATH / "file_fixtures" +_EXPECTED_JSON_PATH: Path = RESOURCE_PATH / "expected_json" def file_fixture_path(fixture_name: str) -> Path: return _FILE_FIXTURE_PATH / fixture_name +def expected_json(input_json: Dict, json_name: str) -> Dict: + expected_json_path = _EXPECTED_JSON_PATH / json_name + os.makedirs(os.path.dirname(expected_json_path), exist_ok=True) + + if REGENERATE_FIXTURES: + with open(expected_json_path, "w", encoding="utf-8") as json_file: + json.dump(input_json, json_file, sort_keys=True, indent=2) + + return input_json + + with open(expected_json_path, "r", encoding="utf-8") as json_file: + expected_json = json.load(json_file) + + return expected_json + + def copy_file_fixture(fixture_name: str, output_file_path: Path) -> None: os.makedirs(os.path.dirname(output_file_path), exist_ok=True) shutil.copy(file_fixture_path(fixture_name), output_file_path) diff --git a/tests/resources/expected_downloads_summaries/dl/test_cli_dl_command.json b/tests/resources/expected_downloads_summaries/dl/test_cli_dl_command.json index ab305abf..01e2ccbe 100644 --- a/tests/resources/expected_downloads_summaries/dl/test_cli_dl_command.json +++ b/tests/resources/expected_downloads_summaries/dl/test_cli_dl_command.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "76e202bd03ceef93daaffffee2cfa193", "JMC/Mock Entry 20-1.info.json": "INFO_JSON", "JMC/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-1.mp4": "dbaeb2a3bfd1de1c7e9615e8bdacb910", + "JMC/Mock Entry 20-1.mp4": "d8dc9918d1646c92b4a4bd246291d849", "JMC/Mock Entry 20-1.nfo": "5d001a105d2380844db181911d5df5c6", "JMC/Mock Entry 20-2.info.json": "INFO_JSON", "JMC/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-2.mp4": "76c2c70572489c684923a51cb1b50687", + "JMC/Mock Entry 20-2.mp4": "6f925179a5b6bf7ffcaf2f70ad585296", "JMC/Mock Entry 20-2.nfo": "dccdded1ca7ebb38d2c9a0e5130bea33", "JMC/Mock Entry 20-3.info.json": "INFO_JSON", "JMC/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-3.mp4": "7bb4f21d59a6fb91836537b27a24e776", + "JMC/Mock Entry 20-3.mp4": "b48f1803f14be665677e9ac9da787593", "JMC/Mock Entry 20-3.nfo": "7b1eab48693161e83c21203b1308b64e", "JMC/Mock Entry 21-1.info.json": "INFO_JSON", "JMC/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 21-1.mp4": "7b008531ca5660b51fb8adc83c799084", + "JMC/Mock Entry 21-1.mp4": "5fb865f27ee1cac381597d1e182dfa48", "JMC/Mock Entry 21-1.nfo": "3b4b0e6820f80370d6738ce828f14a89" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Emby/upload-year/download-index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/by-date/Emby/upload-year/download-index/is_yt_1_migrated.json new file mode 100644 index 00000000..c8541f52 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Emby/upload-year/download-index/is_yt_1_migrated.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1.json new file mode 100644 index 00000000..33848fb2 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "e30d83126c2c6981c936ed894a6f159b", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.nfo": "7c0f2a9d38bf617377e474fcb9bf2bc2", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.nfo": "1f4dec756555ee345cd9d6489b63e070", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.nfo": "f6f704ae3ae0c4055590a147eb425609", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.nfo": "c6e0d368bfe9d70134e5a2ff2d3dccc6", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Jellyfin/upload-year/download-index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/by-date/Jellyfin/upload-year/download-index/is_yt_1_migrated.json new file mode 100644 index 00000000..c8541f52 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Jellyfin/upload-year/download-index/is_yt_1_migrated.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1.json new file mode 100644 index 00000000..33848fb2 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "e30d83126c2c6981c936ed894a6f159b", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.nfo": "7c0f2a9d38bf617377e474fcb9bf2bc2", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.nfo": "1f4dec756555ee345cd9d6489b63e070", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.nfo": "f6f704ae3ae0c4055590a147eb425609", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.nfo": "c6e0d368bfe9d70134e5a2ff2d3dccc6", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1.json new file mode 100644 index 00000000..0ead8778 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "95d3790190cece6e49d2f4d5f68868b1", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0701 - Mock Entry 20-3.mp4": "6dd6860e8075e5e99c85e14729c41bdb", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0701 - Mock Entry 20-3.nfo": "92d978e6f64d97e7e71e7045c9d2fb1f", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0801 - Mock Entry 20-2.mp4": "6b3537db708f88a877d3c5273df02eb0", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0801 - Mock Entry 20-2.nfo": "b1a3768f43d0f7f4dde1207a8670f9b1", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0802 - Mock Entry 20-1.mp4": "7e40cf2da8eedd93b3df9a017c78cd2e", + "Best Prebuilt TV Show by Date/Season 200008/s200008.e0802 - Mock Entry 20-1.nfo": "ea37e0d89713f971a87ddf67f8933be1", + "Best Prebuilt TV Show by Date/Season 200108/s200108.e0801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 200108/s200108.e0801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 200108/s200108.e0801 - Mock Entry 21-1.mp4": "c14c0b1901c456e87e1070539d32917e", + "Best Prebuilt TV Show by Date/Season 200108/s200108.e0801 - Mock Entry 21-1.nfo": "4e18e9e0454bc948cf8daa0151a0b137", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1.json new file mode 100644 index 00000000..8c885adb --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "d2888d629be62badedce415f8296a995", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000001 - Mock Entry 20-3.mp4": "f94c693da1964bd8e916459253ede6d4", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000001 - Mock Entry 20-3.nfo": "57b7ef31d0b9948bd67ba6a7c83dd22b", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000002 - Mock Entry 20-2.mp4": "68a76930c4456ab58f30ad1a3eb109aa", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000002 - Mock Entry 20-2.nfo": "1cc84f0bbe25315268dc453db8941c7f", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000003 - Mock Entry 20-1.mp4": "b24b301ec95f359ba28ca503e678522f", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e000003 - Mock Entry 20-1.nfo": "e77cc16953d13241f83fbd01792c15ba", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e000004 - Mock Entry 21-1.mp4": "cf36da70822036f06e5c834dac291dd3", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e000004 - Mock Entry 21-1.nfo": "cbd1abadd656f6d047775969604e9af4", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1.json new file mode 100644 index 00000000..7bc26567 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "1e8b4072d6d6aace4cac0c94ac0e711e", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14698 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14698 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14698 - Mock Entry 20-1.mp4": "f4846a3b9ee6a16df3348916077d6476", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14698 - Mock Entry 20-1.nfo": "ad715f955b255f356b6263f06941bf05", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14699 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14699 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14699 - Mock Entry 20-2.mp4": "82a1f043b879412d6219e16993b8b188", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14699 - Mock Entry 20-2.nfo": "2fa884c057a44122a245efe3c33921cd", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14799 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14799 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14799 - Mock Entry 20-3.mp4": "6626433c9c3399c6403ba5753e3b31c2", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e14799 - Mock Entry 20-3.nfo": "63f4405964a11f99ff1595cc18266489", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e14699 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e14699 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e14699 - Mock Entry 21-1.mp4": "bac622134b8f65eaeccce6f3b822be64", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e14699 - Mock Entry 21-1.nfo": "6b9659879d7a1b9f5e2dccee70d7fe66", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1.json new file mode 100644 index 00000000..290d9fa6 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "f13a43a83e725981df8417ab1d36705a", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080701 - Mock Entry 20-3.mp4": "219480c0ee2075ed86ccba740cca3d70", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080701 - Mock Entry 20-3.nfo": "6085b15cd1a32e8b45b97cee69ca4741", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080801 - Mock Entry 20-2.mp4": "7ed6abcf342149769ea2de23a0fee0a1", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080801 - Mock Entry 20-2.nfo": "599b687960602264db2f7f44dbd2f849", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080802 - Mock Entry 20-1.mp4": "1431320eb3acc337b1c70a895362397a", + "Best Prebuilt TV Show by Date/Season 2000/s2000.e080802 - Mock Entry 20-1.nfo": "6da492b1897671a3309df38e6fa33279", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e080801 - Mock Entry 21-1.mp4": "5c7f600b9a40039ea77729deda8d103f", + "Best Prebuilt TV Show by Date/Season 2001/s2001.e080801 - Mock Entry 21-1.nfo": "294fb0959c2cf640312f112c0a7aae27", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1.json new file mode 100644 index 00000000..05570b53 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "5269b5e9e4fad7b63b6fdb3fef477b1e", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.mp4": "b095358d6b335a25f3be3ae44a6d10ac", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.nfo": "4ba0e01c1e7295952c3f5e6b9b251d7d", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.mp4": "43f73b6f99439e4873bf8d4ba1802452", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.nfo": "db9196b729c7b2cd26e3f0fc649b39f0", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.mp4": "632af351287dde4d1a096460f6bd93b1", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.nfo": "ba4a387d9462f416ba176c31c1c55316", + "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.mp4": "bf0104b783e168b150ea165faa898915", + "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.nfo": "93dd5c4c493422f9a60e61a6b965b39a", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1.json new file mode 100644 index 00000000..1795a9ee --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1_migrated.json new file mode 100644 index 00000000..c8541f52 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1_migrated.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1.json new file mode 100644 index 00000000..bbd955da --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "05539b932348da7e949a63eb71f77efc", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.mp4": "c27d1898d39763a265e4fe4451777688", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.nfo": "88fee5353ad7eecd96865b34654bbde6", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.mp4": "80c087803d5ac9c0efe02361fdaaa5bd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.nfo": "d781b80ef24916acbf3bf181ab32ddea", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.mp4": "1b5084c06b33f7e0d6daf7cff03adc73", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.nfo": "252b3639b4d1686f8c79476943f89439", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.mp4": "a83870aaca55947b7a91ac90109163af", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.nfo": "e7374b766d9ec0599ecf209a0f78c0ba", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1.json new file mode 100644 index 00000000..33848fb2 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1.json @@ -0,0 +1,22 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "e30d83126c2c6981c936ed894a6f159b", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.nfo": "7c0f2a9d38bf617377e474fcb9bf2bc2", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.nfo": "1f4dec756555ee345cd9d6489b63e070", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.nfo": "f6f704ae3ae0c4055590a147eb425609", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.nfo": "c6e0d368bfe9d70134e5a2ff2d3dccc6", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show by Date/tvshow.nfo": "2439ebc18e46a67064956cb940c992e9" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Plex/upload-year/download-index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/by-date/Plex/upload-year/download-index/is_yt_1_migrated.json new file mode 100644 index 00000000..57c4e26f --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Plex/upload-year/download-index/is_yt_1_migrated.json @@ -0,0 +1,17 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "0011c85c3339ac1301d80be2f3330e67", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1.json new file mode 100644 index 00000000..33cbc2b5 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1.json @@ -0,0 +1,17 @@ +{ + "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "6cd67e6b935bc5b2521f6ed0fa2ea2ed", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", + "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Emby/playlist-index-reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Emby/playlist-index-reversed/s_1/is_yt_1_migrated.json new file mode 100644 index 00000000..69b5d7c7 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Emby/playlist-index-reversed/s_1/is_yt_1_migrated.json @@ -0,0 +1,24 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "809885e6c29bba06215b840516628090", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", + "Best Prebuilt TV Show Collection/Season 01/season.nfo": "979f2563199124cc419039135ed81841", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Emby/playlist-index-reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Emby/playlist-index-reversed/s_2/is_yt_1_migrated.json new file mode 100644 index 00000000..4aa39535 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Emby/playlist-index-reversed/s_2/is_yt_1_migrated.json @@ -0,0 +1,42 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", + "Best Prebuilt TV Show Collection/Season 01/season.nfo": "979f2563199124cc419039135ed81841", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", + "Best Prebuilt TV Show Collection/Season 02/season.nfo": "9445cf70a039402e01cf7ac607dd7d21", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1.json new file mode 100644 index 00000000..05e46280 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1.json @@ -0,0 +1,24 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "ede1deeba072bae50f7f7039deca0543", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.nfo": "043c6de15ab09f32e64414d1f4a8a89c", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.nfo": "4fb018858502fe2c7a0d3dcb89145a8d", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.nfo": "6ac2ee3bd87b453d56bd90b42bbf52a0", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.nfo": "d54ba1888f0083f8de6f401e399ad379", + "Best Prebuilt TV Show Collection/Season 01/season.nfo": "979f2563199124cc419039135ed81841", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1.json new file mode 100644 index 00000000..3417dd91 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1.json @@ -0,0 +1,42 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "9b9e26ff3d036f450501bf5adb979aeb", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.nfo": "9ceac835c0166bf4f7354106379e6d17", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.nfo": "35f15e5437cae813095fe2ca3bd2ca18", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.nfo": "9aa69e4c86bb282ac57b2024ec2fbd34", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.nfo": "709533b6b8f501196db0d98dfe2b2b16", + "Best Prebuilt TV Show Collection/Season 01/season.nfo": "979f2563199124cc419039135ed81841", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.nfo": "05f34cd0e6be4b09ea96134faec892f6", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.nfo": "54b4f84daf25626703f7b3a36b0dea49", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.nfo": "3d2644161e57b94cfac5e95732864cc3", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.nfo": "fd91adb2089d0fcdbb2e568a10ac880e", + "Best Prebuilt TV Show Collection/Season 02/season.nfo": "9445cf70a039402e01cf7ac607dd7d21", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/playlist-index-reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/playlist-index-reversed/s_1/is_yt_1_migrated.json new file mode 100644 index 00000000..622ac651 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/playlist-index-reversed/s_1/is_yt_1_migrated.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "809885e6c29bba06215b840516628090", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/playlist-index-reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/playlist-index-reversed/s_2/is_yt_1_migrated.json new file mode 100644 index 00000000..ef34edca --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/playlist-index-reversed/s_2/is_yt_1_migrated.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1.json new file mode 100644 index 00000000..c67cdbac --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "ede1deeba072bae50f7f7039deca0543", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.nfo": "043c6de15ab09f32e64414d1f4a8a89c", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.nfo": "4fb018858502fe2c7a0d3dcb89145a8d", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.nfo": "6ac2ee3bd87b453d56bd90b42bbf52a0", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.nfo": "d54ba1888f0083f8de6f401e399ad379", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1.json new file mode 100644 index 00000000..9754c9cb --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "9b9e26ff3d036f450501bf5adb979aeb", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.nfo": "9ceac835c0166bf4f7354106379e6d17", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.nfo": "35f15e5437cae813095fe2ca3bd2ca18", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.nfo": "9aa69e4c86bb282ac57b2024ec2fbd34", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.nfo": "709533b6b8f501196db0d98dfe2b2b16", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.nfo": "05f34cd0e6be4b09ea96134faec892f6", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.nfo": "54b4f84daf25626703f7b3a36b0dea49", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.nfo": "3d2644161e57b94cfac5e95732864cc3", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.nfo": "fd91adb2089d0fcdbb2e568a10ac880e", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1.json new file mode 100644 index 00000000..edf55e33 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "809885e6c29bba06215b840516628090", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1_migrated.json new file mode 100644 index 00000000..622ac651 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1_migrated.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "809885e6c29bba06215b840516628090", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1.json new file mode 100644 index 00000000..56508d3c --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1_migrated.json new file mode 100644 index 00000000..ef34edca --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1_migrated.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1.json new file mode 100644 index 00000000..901a9588 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "4e5ef7394c029e7fe230f82dfe134e4c", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.mp4": "79cac07b1964b53ff128ef041e3ed10b", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.nfo": "2a3d9b51221a541fefc29eebffe47cd4", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.mp4": "ebd20e58d50951bfcd2316bcf660ba11", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.nfo": "ec57b2c78058e18cdc015813e8a14b9d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.mp4": "331068e0bf01c2baaa131c7760a46c02", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.nfo": "cd919caf88b0eed99fd2a595c1afa6b1", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.mp4": "c646ebe3838f553abb5e9cb1dd96b85f", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.nfo": "7c4b28456fe5d398b7dad966eb194421", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1.json new file mode 100644 index 00000000..85551fe8 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "c7c2427315924db344d4659a60c59bc9", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.mp4": "a37dba001208063f0c04b2a681973c1c", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.nfo": "7d8f03bdcd8ee66fd1578a86f20365cc", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.mp4": "0bee8e6eb39dbfa8aab81fb78c74ffa0", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.nfo": "39996f43e9761247dce614247f99c808", + "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.mp4": "db88af54bfd315035ce187c2da47f95a", + "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.nfo": "cae377877c966dad492eaa3f224b86ba", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.mp4": "79cac07b1964b53ff128ef041e3ed10b", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.nfo": "974bb83bd43b9579ab1716f66589ccc1", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.mp4": "ebd20e58d50951bfcd2316bcf660ba11", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.nfo": "b12dcb09ffad7f23f21d350e1f60f128", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.mp4": "331068e0bf01c2baaa131c7760a46c02", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.nfo": "e6fd0fc3e95b77c595d1a77c2060fd98", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.mp4": "c646ebe3838f553abb5e9cb1dd96b85f", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.nfo": "23386ea27c1e64afcecdb235acb81b79", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1.json new file mode 100644 index 00000000..e9a36254 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "df87dea55d4f4076ab4c075a3bceaa3d", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052498 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052498 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052498 - Mock Entry 20-1.mp4": "170f6eaaa831b92e80ba798b5ae50b60", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052498 - Mock Entry 20-1.nfo": "93e75d3231077883d37c5baac435e182", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052499 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052499 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052499 - Mock Entry 20-2.mp4": "0e1805471b84d94f605f645d9ea94c9e", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052499 - Mock Entry 20-2.nfo": "6d022da37f3545d9ab8608f11b8059cd", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052599 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052599 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052599 - Mock Entry 20-3.mp4": "2b59eab2673eb7f2a0ec91033737634a", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052599 - Mock Entry 20-3.nfo": "f47403746915586d1bd87436210311c5", + "Best Prebuilt TV Show Collection/Season 01/s01.e99052499 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e99052499 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e99052499 - Mock Entry 21-1.mp4": "450737dd9b8307f498eddb2ac7f4e567", + "Best Prebuilt TV Show Collection/Season 01/s01.e99052499 - Mock Entry 21-1.nfo": "5c095683a8c1ae4ff950d4302dc5f06e", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1.json new file mode 100644 index 00000000..3a8abfa0 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "e89ac9f59f9f9ef22e01c9055708b81c", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052699 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052699 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052699 - Mock Entry 20-4.mp4": "1ea0b0274a45288449dddcb1c91a0ac7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100052699 - Mock Entry 20-4.nfo": "f670533b516a47b833cd0ae04bec5877", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062698 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062698 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062698 - Mock Entry 20-5.mp4": "9b03d1c2e2be9cd75920821accae7774", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062698 - Mock Entry 20-5.nfo": "a223cdfe24fb5d285704351f38db7b46", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062699 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062699 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062699 - Mock Entry 20-6.mp4": "2240a972bc91ef589b3b0a25f1e41725", + "Best Prebuilt TV Show Collection/Season 01/s01.e100062699 - Mock Entry 20-6.nfo": "bb1cc5d794aa28d43e405179622f2095", + "Best Prebuilt TV Show Collection/Season 01/s01.e100072599 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e100072599 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e100072599 - Mock Entry 20-7.mp4": "5944f9af51010e2783576fdbd2ad5436", + "Best Prebuilt TV Show Collection/Season 01/s01.e100072599 - Mock Entry 20-7.nfo": "dbd5b09d734a10795a11344a5b536b8f", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052498 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052498 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052498 - Mock Entry 20-1.mp4": "170f6eaaa831b92e80ba798b5ae50b60", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052498 - Mock Entry 20-1.nfo": "bb5c4debab0a9f5bbd0a4e303c9bed2d", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052499 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052499 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052499 - Mock Entry 20-2.mp4": "0e1805471b84d94f605f645d9ea94c9e", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052499 - Mock Entry 20-2.nfo": "de6eab4d23acac23213ee25adb8095ac", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052599 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052599 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052599 - Mock Entry 20-3.mp4": "2b59eab2673eb7f2a0ec91033737634a", + "Best Prebuilt TV Show Collection/Season 02/s02.e100052599 - Mock Entry 20-3.nfo": "23e88cb7a059946b12c0702a1f987bc7", + "Best Prebuilt TV Show Collection/Season 02/s02.e99052499 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e99052499 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e99052499 - Mock Entry 21-1.mp4": "450737dd9b8307f498eddb2ac7f4e567", + "Best Prebuilt TV Show Collection/Season 02/s02.e99052499 - Mock Entry 21-1.nfo": "ee3a0c5681e7bf7923f72c323e7a9795", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1.json new file mode 100644 index 00000000..8eb2de94 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "8f0390db6223f1c48072c28aabdc4162", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080701 - Mock Entry 20-3.mp4": "680b08a1e5032d89188def2784e62146", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080701 - Mock Entry 20-3.nfo": "2ecfb16f4216df21e9485b90b14e355f", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080801 - Mock Entry 20-2.mp4": "3c0dcdd38605f032c98aac9068732329", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080801 - Mock Entry 20-2.nfo": "090cfb9795b2ddd210afb93873cb60ce", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080802 - Mock Entry 20-1.mp4": "29d7648807690a4af93541ae340dd7bc", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080802 - Mock Entry 20-1.nfo": "e4aa8129a961c47e3151062b95f39b73", + "Best Prebuilt TV Show Collection/Season 01/s01.e01080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e01080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e01080801 - Mock Entry 21-1.mp4": "9abd6ecaf194553e9486e1bf02f552cf", + "Best Prebuilt TV Show Collection/Season 01/s01.e01080801 - Mock Entry 21-1.nfo": "34704ea06cbdfaa1c9939e061fd1633a", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1.json new file mode 100644 index 00000000..1edba6fc --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "b6ee943d41ca8b0f9b54bb9cb04da7de", + "Best Prebuilt TV Show Collection/Season 01/s01.e00060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00060601 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00060601 - Mock Entry 20-7.mp4": "a75f9d2b28d82b78ae30da3962f57f61", + "Best Prebuilt TV Show Collection/Season 01/s01.e00060601 - Mock Entry 20-7.nfo": "29b6f491d1d7cb5e62f6a3dd290ba74a", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070601 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070601 - Mock Entry 20-6.mp4": "18226f783f8c3fac3089d61307bee3ba", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070601 - Mock Entry 20-6.nfo": "8ce2b32b9371cc45908acb73a5c5d5e6", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070602 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070602 - Mock Entry 20-5.mp4": "180e86031e6af3ea1a54714460a6bf1f", + "Best Prebuilt TV Show Collection/Season 01/s01.e00070602 - Mock Entry 20-5.nfo": "4567dfd30fc7287eea63f28e465d9c54", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080601 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080601 - Mock Entry 20-4.mp4": "09fba17ad8bf9deb8157ff83880185c3", + "Best Prebuilt TV Show Collection/Season 01/s01.e00080601 - Mock Entry 20-4.nfo": "0fc29c2520ae80c4390a5b68d02e6794", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080701 - Mock Entry 20-3.mp4": "680b08a1e5032d89188def2784e62146", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080701 - Mock Entry 20-3.nfo": "7276a8c14720f76cf4e5e624eb1dea2d", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080801 - Mock Entry 20-2.mp4": "3c0dcdd38605f032c98aac9068732329", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080801 - Mock Entry 20-2.nfo": "028a4161b6bccda2cf8d2a0a3ff13f4f", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080802 - Mock Entry 20-1.mp4": "29d7648807690a4af93541ae340dd7bc", + "Best Prebuilt TV Show Collection/Season 02/s02.e00080802 - Mock Entry 20-1.nfo": "7303574842c9409a6521486ddeb7cde9", + "Best Prebuilt TV Show Collection/Season 02/s02.e01080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e01080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e01080801 - Mock Entry 21-1.mp4": "9abd6ecaf194553e9486e1bf02f552cf", + "Best Prebuilt TV Show Collection/Season 02/s02.e01080801 - Mock Entry 21-1.nfo": "5b9d248c2fd2460030aa590d8660d957", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1.json new file mode 100644 index 00000000..077b159a --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "e26598ffd47b150e6bf09e5490546e94", + "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.mp4": "cda96931a134eeb6e05e0b5aec03ddfe", + "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.nfo": "a7e21040a3fcb264f287e4eba1853f94", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.mp4": "a7f2ca37b817da0512af297b5652e5e8", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.nfo": "78c08ad76d004a74cdba012f0b746c22", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.mp4": "35ebd30923d8f321e023281ec6ef4e6f", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.nfo": "d9031d8608d12742c40a3279138c8371", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.mp4": "204ce9c28ab7b69c659f0f76b6c75bfa", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.nfo": "74bd8dc59c5b76ae112d90638977c464", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1.json new file mode 100644 index 00000000..ba4303f1 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "2990d95213df5a6c87825a28daa970f4", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.mp4": "611d34cf77a2123181a7c4096623f6b9", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.nfo": "0242d92e5e0d26745948b42c25eecfba", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.mp4": "f09ce6574a040cea52b7177f74b9ccea", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.nfo": "52b17bcad30adb6eaef4c3b21c1af76b", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.mp4": "435692c75994d929807b5f09726728ad", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.nfo": "31fb72d089fd680fea1340f3d3c41d64", + "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.mp4": "613c386e00bf1c87e4f1d1acbbd98281", + "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.nfo": "df65227ee260219883ff618c9fbdf1d4", + "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.mp4": "cda96931a134eeb6e05e0b5aec03ddfe", + "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.nfo": "0e4ea058d6a1e8760c80d3a02d25aafc", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.mp4": "a7f2ca37b817da0512af297b5652e5e8", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.nfo": "1095322364e512dff963a44b2beffe30", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.mp4": "35ebd30923d8f321e023281ec6ef4e6f", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.nfo": "5130c71d99f1c0726fee8cb8c9d82ba7", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.mp4": "204ce9c28ab7b69c659f0f76b6c75bfa", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.nfo": "bde2a5401c49b58de0eacea4145d91ae", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1.json new file mode 100644 index 00000000..c67cdbac --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1.json @@ -0,0 +1,23 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "ede1deeba072bae50f7f7039deca0543", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.nfo": "043c6de15ab09f32e64414d1f4a8a89c", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.nfo": "4fb018858502fe2c7a0d3dcb89145a8d", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.nfo": "6ac2ee3bd87b453d56bd90b42bbf52a0", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.nfo": "d54ba1888f0083f8de6f401e399ad379", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "a3f1061135b7aa0c25e5176a271d32f0" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1.json new file mode 100644 index 00000000..9754c9cb --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1.json @@ -0,0 +1,40 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "9b9e26ff3d036f450501bf5adb979aeb", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.nfo": "9ceac835c0166bf4f7354106379e6d17", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.nfo": "35f15e5437cae813095fe2ca3bd2ca18", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.nfo": "9aa69e4c86bb282ac57b2024ec2fbd34", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.nfo": "709533b6b8f501196db0d98dfe2b2b16", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.nfo": "05f34cd0e6be4b09ea96134faec892f6", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.nfo": "54b4f84daf25626703f7b3a36b0dea49", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.nfo": "3d2644161e57b94cfac5e95732864cc3", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.nfo": "fd91adb2089d0fcdbb2e568a10ac880e", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", + "Best Prebuilt TV Show Collection/season01-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/season02-poster.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/tvshow.nfo": "61943a17325c5188475651ca7c68446d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Plex/playlist-index-reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Plex/playlist-index-reversed/s_1/is_yt_1_migrated.json new file mode 100644 index 00000000..bae82de6 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Plex/playlist-index-reversed/s_1/is_yt_1_migrated.json @@ -0,0 +1,18 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "ba324b2c6532fe9d8fbe03e0dd6d0410", + "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Plex/playlist-index-reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/integration/collection/Plex/playlist-index-reversed/s_2/is_yt_1_migrated.json new file mode 100644 index 00000000..eb4cb28e --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Plex/playlist-index-reversed/s_2/is_yt_1_migrated.json @@ -0,0 +1,31 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "6ec6913047c175cedcbd41cb3ef402fc", + "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", + "Best Prebuilt TV Show Collection/Season 02/Season02.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1.json new file mode 100644 index 00000000..3ebd92c2 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1.json @@ -0,0 +1,18 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "87f96a1e055447383bfea0a12680438d", + "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1.json new file mode 100644 index 00000000..47a0f4ac --- /dev/null +++ b/tests/resources/expected_downloads_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1.json @@ -0,0 +1,31 @@ +{ + "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "04d06f293fcb5164266bedf66b4d4264", + "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", + "Best Prebuilt TV Show Collection/Season 02/Season02.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", + "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", + "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/chapters/test_chapters_from_comments.json b/tests/resources/expected_downloads_summaries/plugins/chapters/test_chapters_from_comments.json index 1c1792d3..d2d07d67 100644 --- a/tests/resources/expected_downloads_summaries/plugins/chapters/test_chapters_from_comments.json +++ b/tests/resources/expected_downloads_summaries/plugins/chapters/test_chapters_from_comments.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "76e202bd03ceef93daaffffee2cfa193", "JMC/Mock Entry 20-1.info.json": "INFO_JSON", "JMC/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-1.mp4": "40efe3f597c40c11d0d3f2f87cc7650b", + "JMC/Mock Entry 20-1.mp4": "f9f1542629edf78fb9d80f2517e6530e", "JMC/Mock Entry 20-1.nfo": "fefcf0b3e4f4ff80ad636584d50dadec", "JMC/Mock Entry 20-2.info.json": "INFO_JSON", "JMC/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-2.mp4": "0747f56d03e720128b1fa5ef25bcf3c8", + "JMC/Mock Entry 20-2.mp4": "48a46efb360b908a5c9653ef266f6574", "JMC/Mock Entry 20-2.nfo": "025c0b631da5ff5470382b38fce78d2d", "JMC/Mock Entry 20-3.info.json": "INFO_JSON", "JMC/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-3.mp4": "eb776b8e6da77a848666d88623600fd6", + "JMC/Mock Entry 20-3.mp4": "69b89a35ca5554d047d7ea375635e557", "JMC/Mock Entry 20-3.nfo": "618b0ff948d9de2e10cf1da8c0dd6615", "JMC/Mock Entry 21-1.info.json": "INFO_JSON", "JMC/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 21-1.mp4": "5d56530dc8a4cfe55efb791d82bae0f0", + "JMC/Mock Entry 21-1.mp4": "a3a18a3acf6e28ab921b8458d1526886", "JMC/Mock Entry 21-1.nfo": "e5c715749efc1603a6e2f59244d87aba" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json b/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json index 4552cb5d..13bbe1fa 100644 --- a/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json +++ b/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "19cf39d57914ba9cbd1e57ba6f1e0683", "JMC/Mock Entry 20-1.info.json": "INFO_JSON", "JMC/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-1.mkv": "1514f89a141f7b5ee3fdde9a70e63f57", + "JMC/Mock Entry 20-1.mkv": "cbc8c05ea5cf4deefd735b93af9c0259", "JMC/Mock Entry 20-1.nfo": "fefcf0b3e4f4ff80ad636584d50dadec", "JMC/Mock Entry 20-2.info.json": "INFO_JSON", "JMC/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-2.mkv": "da089645cc944fdb411ecc46facfe9f6", + "JMC/Mock Entry 20-2.mkv": "85cc274346abc0c71ab7702b0d61abde", "JMC/Mock Entry 20-2.nfo": "025c0b631da5ff5470382b38fce78d2d", "JMC/Mock Entry 20-3.info.json": "INFO_JSON", "JMC/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-3.mkv": "5dbae183aa57c4062af9d5a2e0e0f8fe", + "JMC/Mock Entry 20-3.mkv": "c5a9ce268475c3b8fa9d4d0e3c0c25ae", "JMC/Mock Entry 20-3.nfo": "618b0ff948d9de2e10cf1da8c0dd6615", "JMC/Mock Entry 21-1.info.json": "INFO_JSON", "JMC/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 21-1.mkv": "61910bf74a0175fbdf595095109d1a1c", + "JMC/Mock Entry 21-1.mkv": "ebdd8026e72625c56dd2aa8a2e19e814", "JMC/Mock Entry 21-1.nfo": "e5c715749efc1603a6e2f59244d87aba" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/output_options/empty_info_json_thumb.json b/tests/resources/expected_downloads_summaries/plugins/output_options/empty_info_json_thumb.json index 235abb09..8857b8f1 100644 --- a/tests/resources/expected_downloads_summaries/plugins/output_options/empty_info_json_thumb.json +++ b/tests/resources/expected_downloads_summaries/plugins/output_options/empty_info_json_thumb.json @@ -1,12 +1,12 @@ { ".ytdl-sub-subscription_test-download-archive.json": "7c6fbd0631d7d02025f3da457ba203e3", - "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "7d2ee7fe8003ea63ece37dd2a441c123", + "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "06e7e5beab0b68e4ee3535c801448b4c", "Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", - "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "699017cd1b67ae216eda769bef413415", + "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "dfe89d82ae778bae164e161f10ac9adc", "Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", - "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "a16741d4fbf657d1de4c50493db14062", + "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "273785dcc28969f4de4a6e3e5ff1e61e", "Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", - "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "7433153952b069a5674e2a3ed529b49b", + "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "2170bfbc2b85057ba0ec403e7e5ea66d", "Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "tvshow.nfo": "cccca1086b41af04d8ea004b1aa250e8" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/output_options/post_migration.json b/tests/resources/expected_downloads_summaries/plugins/output_options/post_migration.json index f66f7456..08f43cf5 100644 --- a/tests/resources/expected_downloads_summaries/plugins/output_options/post_migration.json +++ b/tests/resources/expected_downloads_summaries/plugins/output_options/post_migration.json @@ -2,19 +2,19 @@ ".ytdl-sub-JMC-migrated-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", "Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "7d2ee7fe8003ea63ece37dd2a441c123", + "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "06e7e5beab0b68e4ee3535c801448b4c", "Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", "Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "699017cd1b67ae216eda769bef413415", + "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "dfe89d82ae778bae164e161f10ac9adc", "Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", "Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "a16741d4fbf657d1de4c50493db14062", + "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "273785dcc28969f4de4a6e3e5ff1e61e", "Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", "Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "7433153952b069a5674e2a3ed529b49b", + "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "2170bfbc2b85057ba0ec403e7e5ea66d", "Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "tvshow.nfo": "cccca1086b41af04d8ea004b1aa250e8" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/output_options/pre_migration.json b/tests/resources/expected_downloads_summaries/plugins/output_options/pre_migration.json index 61c1c924..7ca037a7 100644 --- a/tests/resources/expected_downloads_summaries/plugins/output_options/pre_migration.json +++ b/tests/resources/expected_downloads_summaries/plugins/output_options/pre_migration.json @@ -2,19 +2,19 @@ ".ytdl-sub-subscription_test-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", "Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "7d2ee7fe8003ea63ece37dd2a441c123", + "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "06e7e5beab0b68e4ee3535c801448b4c", "Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", "Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "699017cd1b67ae216eda769bef413415", + "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "dfe89d82ae778bae164e161f10ac9adc", "Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", "Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "a16741d4fbf657d1de4c50493db14062", + "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "273785dcc28969f4de4a6e3e5ff1e61e", "Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", "Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "7433153952b069a5674e2a3ed529b49b", + "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "2170bfbc2b85057ba0ec403e7e5ea66d", "Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "tvshow.nfo": "cccca1086b41af04d8ea004b1aa250e8" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/output_options/test_missing_thumb.json b/tests/resources/expected_downloads_summaries/plugins/output_options/test_missing_thumb.json index 21967d07..6c1628ba 100644 --- a/tests/resources/expected_downloads_summaries/plugins/output_options/test_missing_thumb.json +++ b/tests/resources/expected_downloads_summaries/plugins/output_options/test_missing_thumb.json @@ -1,16 +1,16 @@ { ".ytdl-sub-subscription_test-download-archive.json": "e049068ba7f5bdc27c474b18f4c6b402", "Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "7d2ee7fe8003ea63ece37dd2a441c123", + "Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "06e7e5beab0b68e4ee3535c801448b4c", "Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", "Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "699017cd1b67ae216eda769bef413415", + "Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "dfe89d82ae778bae164e161f10ac9adc", "Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", "Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "a16741d4fbf657d1de4c50493db14062", + "Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "273785dcc28969f4de4a6e3e5ff1e61e", "Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", "Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "7433153952b069a5674e2a3ed529b49b", + "Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "2170bfbc2b85057ba0ec403e7e5ea66d", "Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "tvshow.nfo": "cccca1086b41af04d8ea004b1aa250e8" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/thumbnail/embedded_square_thumbnail.json b/tests/resources/expected_downloads_summaries/plugins/thumbnail/embedded_square_thumbnail.json new file mode 100644 index 00000000..8643977f --- /dev/null +++ b/tests/resources/expected_downloads_summaries/plugins/thumbnail/embedded_square_thumbnail.json @@ -0,0 +1,19 @@ +{ + ".ytdl-sub-subscription_test-download-archive.json": "76e202bd03ceef93daaffffee2cfa193", + "JMC/Mock Entry 20-1.info.json": "INFO_JSON", + "JMC/Mock Entry 20-1.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 20-1.mp4": "071b3d0747705613c0ec0cba71a045fa", + "JMC/Mock Entry 20-1.nfo": "fefcf0b3e4f4ff80ad636584d50dadec", + "JMC/Mock Entry 20-2.info.json": "INFO_JSON", + "JMC/Mock Entry 20-2.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 20-2.mp4": "fce852b2c7567b917a7d02b1216215a0", + "JMC/Mock Entry 20-2.nfo": "025c0b631da5ff5470382b38fce78d2d", + "JMC/Mock Entry 20-3.info.json": "INFO_JSON", + "JMC/Mock Entry 20-3.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 20-3.mp4": "b2edb14a985b95fb4c530f7e868ed66c", + "JMC/Mock Entry 20-3.nfo": "618b0ff948d9de2e10cf1da8c0dd6615", + "JMC/Mock Entry 21-1.info.json": "INFO_JSON", + "JMC/Mock Entry 21-1.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 21-1.mp4": "3b50ad36e92501fef9d18dcd7532a921", + "JMC/Mock Entry 21-1.nfo": "e5c715749efc1603a6e2f59244d87aba" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/plugins/thumbnail/square_thumbnail.json b/tests/resources/expected_downloads_summaries/plugins/thumbnail/square_thumbnail.json new file mode 100644 index 00000000..52179d42 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/plugins/thumbnail/square_thumbnail.json @@ -0,0 +1,19 @@ +{ + ".ytdl-sub-subscription_test-download-archive.json": "76e202bd03ceef93daaffffee2cfa193", + "JMC/Mock Entry 20-1.info.json": "INFO_JSON", + "JMC/Mock Entry 20-1.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 20-1.mp4": "d8dc9918d1646c92b4a4bd246291d849", + "JMC/Mock Entry 20-1.nfo": "fefcf0b3e4f4ff80ad636584d50dadec", + "JMC/Mock Entry 20-2.info.json": "INFO_JSON", + "JMC/Mock Entry 20-2.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 20-2.mp4": "6f925179a5b6bf7ffcaf2f70ad585296", + "JMC/Mock Entry 20-2.nfo": "025c0b631da5ff5470382b38fce78d2d", + "JMC/Mock Entry 20-3.info.json": "INFO_JSON", + "JMC/Mock Entry 20-3.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 20-3.mp4": "b48f1803f14be665677e9ac9da787593", + "JMC/Mock Entry 20-3.nfo": "618b0ff948d9de2e10cf1da8c0dd6615", + "JMC/Mock Entry 21-1.info.json": "INFO_JSON", + "JMC/Mock Entry 21-1.jpg": "e691c0591065c89e21fd552672a206af", + "JMC/Mock Entry 21-1.mp4": "5fb865f27ee1cac381597d1e182dfa48", + "JMC/Mock Entry 21-1.nfo": "e5c715749efc1603a6e2f59244d87aba" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/soundcloud/test_soundcloud_discography.json b/tests/resources/expected_downloads_summaries/soundcloud/test_soundcloud_discography.json index 242947bf..219447f1 100644 --- a/tests/resources/expected_downloads_summaries/soundcloud/test_soundcloud_discography.json +++ b/tests/resources/expected_downloads_summaries/soundcloud/test_soundcloud_discography.json @@ -1,19 +1,19 @@ { ".ytdl-sub-j_b-download-archive.json": "1a99156e9ece62539fb2608416a07200", - "j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3": "b35d01351e5dd1195e90a41b2edeb5b7", + "j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3": "0aecc0e9832768002f41e09a7e70b305", "j_b/[2021] Baby Santana's Dorian Groove/folder.jpg": "967892be44b8c47e1be73f055a7c6f08", - "j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3": "5c107c122048389aa40936c4fba0436e", + "j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3": "2d4154dfa596c905d71cfd970f5dc9d3", "j_b/[2021] Purple Clouds/folder.jpg": "967892be44b8c47e1be73f055a7c6f08", - "j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3": "e737c2ba118920643cec9eac09a283ea", - "j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3": "d5def3e4329b9b95c5354c838dd3bc91", - "j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3": "1694c4a2889aa7dacafc5f2fdb4fe1d7", - "j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3": "9c8ddee6aaf88b38706ce6bbdf907878", - "j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3": "4bf9e700137cb1b58e730dd312e00bb4", - "j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3": "c1920254046eda0d84b36dd34f9bafdb", - "j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3": "26f2eb8395b23cecc4000e2fd58304bf", - "j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3": "e3ceb5ae25c115d037a298b4620bce9f", - "j_b/[2022] Acoustic Treats/09 - Finding Home.mp3": "f3dcfe7c8569853a02eb12ad21b0e248", - "j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3": "4fb9a18b3fdd788ce9d9f4c8a07b6028", - "j_b/[2022] Acoustic Treats/11 - Untold History.mp3": "ad186ee1d0029f8de02357e8b8712a4b", + "j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3": "10c925fc97629ca788c44ae8082ee889", + "j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3": "da0e8a7db660adbfef255ba1763c384c", + "j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3": "42284493f75a55bd1b1d3f1ac8d1ffe1", + "j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3": "d660abefa257a10cbeae434c770bf3e5", + "j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3": "565d4dcf92e40e1485ed77fa79b52c17", + "j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3": "7c5903693230ca9ac8b44b76ed004eb4", + "j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3": "dfec9909cb8156ec8d5c28cfce858f36", + "j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3": "2c8769a9de3d1033d0e152c642512e40", + "j_b/[2022] Acoustic Treats/09 - Finding Home.mp3": "b3920d39deb847be938e03e3d74d4ac9", + "j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3": "759394ebe3db9ccef564fa9d7c0dfd09", + "j_b/[2022] Acoustic Treats/11 - Untold History.mp3": "477af6c479263539f244db8d9de04201", "j_b/[2022] Acoustic Treats/folder.jpg": "967892be44b8c47e1be73f055a7c6f08" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json index ac184954..c8541f52 100644 --- a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "d14710bc7c6ef6d7d64d7d955b7b32d0", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "1eec4bae304dcb12ba91cbe5b3ef52c1", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "ff2a6a35b533c4e0f41d0c761c9c2f08", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "9767c6fc84316bc5d27e6527c7ca62b4", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json index 71b9f021..33848fb2 100644 --- a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "e30d83126c2c6981c936ed894a6f159b", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "35770012af9ea0d5d1da45b9667b421f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.nfo": "7c0f2a9d38bf617377e474fcb9bf2bc2", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "65010c031d76bc70055befd3aac68139", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.nfo": "1f4dec756555ee345cd9d6489b63e070", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "9b1066f98309432e9d4d7aaf8d30ddcd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.nfo": "f6f704ae3ae0c4055590a147eb425609", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "129a7ee05d048db25f4c0cf0581a9318", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.nfo": "c6e0d368bfe9d70134e5a2ff2d3dccc6", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json index 65e049cf..622ac651 100644 --- a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "809885e6c29bba06215b840516628090", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json index 5fad68c9..ef34edca 100644 --- a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "1427a80ab1d6e2e97506aebee3dd7a21", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "c66baf5bb22f386d4bf6db027bb60b1a", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "36b049db676868cbd2f2633bf3ab71fb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "c25a7830dfa7a6601b8e7fbce117cc48", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json index 087583f9..c67cdbac 100644 --- a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "ede1deeba072bae50f7f7039deca0543", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "6aae009946f29e5d3095cfe24d4ce527", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.nfo": "043c6de15ab09f32e64414d1f4a8a89c", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "66f9ee8157f918711d28448fc67b7417", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.nfo": "4fb018858502fe2c7a0d3dcb89145a8d", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "8f0a39a96e6dca16c09a7a0fd51df09a", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.nfo": "6ac2ee3bd87b453d56bd90b42bbf52a0", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "d6df6d3e2e68c1b80f464d514a7f550c", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.nfo": "d54ba1888f0083f8de6f401e399ad379", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json index eaf036eb..9754c9cb 100644 --- a/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "9b9e26ff3d036f450501bf5adb979aeb", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "dc9260aa08144f4207c797e393b05939", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.nfo": "9ceac835c0166bf4f7354106379e6d17", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "c497a3f67cd0a98e690a22f8a7b8bf23", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.nfo": "35f15e5437cae813095fe2ca3bd2ca18", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "7518678ed81fe6f918cc89c10b923aa5", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.nfo": "9aa69e4c86bb282ac57b2024ec2fbd34", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "9f755810e03f6476d1c7211eefa04fc1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.nfo": "709533b6b8f501196db0d98dfe2b2b16", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "6aae009946f29e5d3095cfe24d4ce527", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.nfo": "05f34cd0e6be4b09ea96134faec892f6", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "66f9ee8157f918711d28448fc67b7417", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.nfo": "54b4f84daf25626703f7b3a36b0dea49", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "8f0a39a96e6dca16c09a7a0fd51df09a", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.nfo": "3d2644161e57b94cfac5e95732864cc3", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "d6df6d3e2e68c1b80f464d514a7f550c", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.nfo": "fd91adb2089d0fcdbb2e568a10ac880e", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1.json index d8b62c9e..1795a9ee 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "d14710bc7c6ef6d7d64d7d955b7b32d0", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "1eec4bae304dcb12ba91cbe5b3ef52c1", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "ff2a6a35b533c4e0f41d0c761c9c2f08", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "9767c6fc84316bc5d27e6527c7ca62b4", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json index ac184954..c8541f52 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "456d8882fc5e35d74f19b386d1d9a059", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "d14710bc7c6ef6d7d64d7d955b7b32d0", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.nfo": "dbe61f2c8ae41041773f713ba5376726", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "1eec4bae304dcb12ba91cbe5b3ef52c1", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.nfo": "0f071078c9fa2569bbcb998664d40681", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "ff2a6a35b533c4e0f41d0c761c9c2f08", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.nfo": "837a61dca11bbe1874ea07cb8ef8a7c9", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "9767c6fc84316bc5d27e6527c7ca62b4", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.nfo": "8ee3845c514a411425b7e9198666b61c", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json index 71b9f021..33848fb2 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "e30d83126c2c6981c936ed894a6f159b", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "35770012af9ea0d5d1da45b9667b421f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.nfo": "7c0f2a9d38bf617377e474fcb9bf2bc2", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "65010c031d76bc70055befd3aac68139", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.nfo": "1f4dec756555ee345cd9d6489b63e070", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "9b1066f98309432e9d4d7aaf8d30ddcd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.nfo": "f6f704ae3ae0c4055590a147eb425609", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "129a7ee05d048db25f4c0cf0581a9318", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.nfo": "c6e0d368bfe9d70134e5a2ff2d3dccc6", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1.json index 8e813e40..bbd955da 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "05539b932348da7e949a63eb71f77efc", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.mp4": "9b276cb01ceb989cdf215ae9c0d9d32a", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.mp4": "c27d1898d39763a265e4fe4451777688", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14698 - Mock Entry 20-1.nfo": "88fee5353ad7eecd96865b34654bbde6", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.mp4": "2ea78b08315e7fca677a639c36c65c38", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.mp4": "80c087803d5ac9c0efe02361fdaaa5bd", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14699 - Mock Entry 20-2.nfo": "d781b80ef24916acbf3bf181ab32ddea", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.mp4": "0221338f33f1ae59bda7a3eb1c0795ae", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.mp4": "1b5084c06b33f7e0d6daf7cff03adc73", "Best Prebuilt TV Show by Date/Season 2020/s2020.e14799 - Mock Entry 20-3.nfo": "252b3639b4d1686f8c79476943f89439", "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.mp4": "389f962b863817991e537475d6c3e885", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.mp4": "a83870aaca55947b7a91ac90109163af", "Best Prebuilt TV Show by Date/Season 2021/s2021.e14699 - Mock Entry 21-1.nfo": "e7374b766d9ec0599ecf209a0f78c0ba", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1.json index c8d7aa1b..05570b53 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "5269b5e9e4fad7b63b6fdb3fef477b1e", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.mp4": "cabf6750f99eab4e55a3220238a82532", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.mp4": "b095358d6b335a25f3be3ae44a6d10ac", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0701 - Mock Entry 20-3.nfo": "4ba0e01c1e7295952c3f5e6b9b251d7d", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.mp4": "eb07113fc41ea2ef9014c3275f9e0202", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.mp4": "43f73b6f99439e4873bf8d4ba1802452", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0801 - Mock Entry 20-2.nfo": "db9196b729c7b2cd26e3f0fc649b39f0", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.mp4": "ca8615ad8503aeeccfb0c79016787d56", + "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.mp4": "632af351287dde4d1a096460f6bd93b1", "Best Prebuilt TV Show by Date/Season 202008/s202008.e0802 - Mock Entry 20-1.nfo": "ba4a387d9462f416ba176c31c1c55316", "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.mp4": "9023d52c4ac00b448249a5d55bab5326", + "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.mp4": "bf0104b783e168b150ea165faa898915", "Best Prebuilt TV Show by Date/Season 202108/s202108.e0801 - Mock Entry 21-1.nfo": "93dd5c4c493422f9a60e61a6b965b39a", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1.json index ce154a2a..901a9588 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "4e5ef7394c029e7fe230f82dfe134e4c", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.mp4": "9d2ef6838874f46d7e222672f7fc5de5", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.mp4": "79cac07b1964b53ff128ef041e3ed10b", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 21-1.nfo": "2a3d9b51221a541fefc29eebffe47cd4", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.mp4": "261e446652a61457fc0a74b3880ef466", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.mp4": "ebd20e58d50951bfcd2316bcf660ba11", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-1.nfo": "ec57b2c78058e18cdc015813e8a14b9d", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.mp4": "a47a82ae9323e5f63ed78cddaf4e2613", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.mp4": "331068e0bf01c2baaa131c7760a46c02", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-2.nfo": "cd919caf88b0eed99fd2a595c1afa6b1", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.mp4": "1f176ffa4d10e80f25e63922bbf4eff7", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.mp4": "c646ebe3838f553abb5e9cb1dd96b85f", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-3.nfo": "7c4b28456fe5d398b7dad966eb194421", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1.json index aadbb9fd..85551fe8 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "c7c2427315924db344d4659a60c59bc9", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.mp4": "6cee06c349a722937d63469335ac0940", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.mp4": "a37dba001208063f0c04b2a681973c1c", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-4.nfo": "7d8f03bdcd8ee66fd1578a86f20365cc", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "36b049db676868cbd2f2633bf3ab71fb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.mp4": "7bab968253b962fef6b6afc38d8e9ce3", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.mp4": "0bee8e6eb39dbfa8aab81fb78c74ffa0", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-6.nfo": "39996f43e9761247dce614247f99c808", "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.mp4": "c97823194c3098c9dca79436d57b5070", + "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.mp4": "db88af54bfd315035ce187c2da47f95a", "Best Prebuilt TV Show Collection/Season 01/s01.e000005 - Mock Entry 20-7.nfo": "cae377877c966dad492eaa3f224b86ba", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.mp4": "9d2ef6838874f46d7e222672f7fc5de5", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.mp4": "79cac07b1964b53ff128ef041e3ed10b", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 21-1.nfo": "974bb83bd43b9579ab1716f66589ccc1", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.mp4": "261e446652a61457fc0a74b3880ef466", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.mp4": "ebd20e58d50951bfcd2316bcf660ba11", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-1.nfo": "b12dcb09ffad7f23f21d350e1f60f128", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.mp4": "a47a82ae9323e5f63ed78cddaf4e2613", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.mp4": "331068e0bf01c2baaa131c7760a46c02", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-2.nfo": "e6fd0fc3e95b77c595d1a77c2060fd98", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.mp4": "1f176ffa4d10e80f25e63922bbf4eff7", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.mp4": "c646ebe3838f553abb5e9cb1dd96b85f", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 20-3.nfo": "23386ea27c1e64afcecdb235acb81b79", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1.json index 20379646..edf55e33 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "809885e6c29bba06215b840516628090", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json index 65e049cf..622ac651 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "809885e6c29bba06215b840516628090", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.nfo": "ddf2763f976aaa79199bddbfd3b66501", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.nfo": "cda58b9d5b7ee05d5e4d5144ebfe0f94", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.nfo": "4d5775492f2731cb5dc04a84ebcb029d", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.nfo": "2980b224863465a8b8a465c9d381d1f4", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1.json index db93c907..56508d3c 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "1427a80ab1d6e2e97506aebee3dd7a21", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "c66baf5bb22f386d4bf6db027bb60b1a", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "36b049db676868cbd2f2633bf3ab71fb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "c25a7830dfa7a6601b8e7fbce117cc48", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json index 5fad68c9..ef34edca 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json": "7331c3a7cc0b43cd547b82af4ec35a3f", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "1427a80ab1d6e2e97506aebee3dd7a21", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.nfo": "42ad110e3f608de3edb94735631d39a7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "c66baf5bb22f386d4bf6db027bb60b1a", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.nfo": "ea4dee97b5bb9e49cb72f0942f851feb", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "36b049db676868cbd2f2633bf3ab71fb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.nfo": "7f71bd60012d850ece38fa644eeddc5e", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "c25a7830dfa7a6601b8e7fbce117cc48", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.nfo": "c487f013b8d5adc399a793bc803b898f", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.nfo": "4eedc1c0e99858530e6228001d024766", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.nfo": "cc1541a65508fcbfd9f024042cb6b6ce", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.nfo": "d5fa573faf1d29c367117917417df62c", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.nfo": "6c40cd5d13c8d088e57906e809a591f8", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json index 087583f9..c67cdbac 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "ede1deeba072bae50f7f7039deca0543", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "6aae009946f29e5d3095cfe24d4ce527", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.nfo": "043c6de15ab09f32e64414d1f4a8a89c", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "66f9ee8157f918711d28448fc67b7417", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.nfo": "4fb018858502fe2c7a0d3dcb89145a8d", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "8f0a39a96e6dca16c09a7a0fd51df09a", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.nfo": "6ac2ee3bd87b453d56bd90b42bbf52a0", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "d6df6d3e2e68c1b80f464d514a7f550c", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.nfo": "d54ba1888f0083f8de6f401e399ad379", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json index eaf036eb..9754c9cb 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "9b9e26ff3d036f450501bf5adb979aeb", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "dc9260aa08144f4207c797e393b05939", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.nfo": "9ceac835c0166bf4f7354106379e6d17", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "c497a3f67cd0a98e690a22f8a7b8bf23", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.nfo": "35f15e5437cae813095fe2ca3bd2ca18", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "7518678ed81fe6f918cc89c10b923aa5", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.nfo": "9aa69e4c86bb282ac57b2024ec2fbd34", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "9f755810e03f6476d1c7211eefa04fc1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.nfo": "709533b6b8f501196db0d98dfe2b2b16", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "6aae009946f29e5d3095cfe24d4ce527", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.nfo": "05f34cd0e6be4b09ea96134faec892f6", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "66f9ee8157f918711d28448fc67b7417", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.nfo": "54b4f84daf25626703f7b3a36b0dea49", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "8f0a39a96e6dca16c09a7a0fd51df09a", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.nfo": "3d2644161e57b94cfac5e95732864cc3", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "d6df6d3e2e68c1b80f464d514a7f550c", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.nfo": "fd91adb2089d0fcdbb2e568a10ac880e", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1.json index 932e7f20..077b159a 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1.json @@ -2,19 +2,19 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "e26598ffd47b150e6bf09e5490546e94", "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.mp4": "5b87fe189379fcce34facf73e7021538", + "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.mp4": "cda96931a134eeb6e05e0b5aec03ddfe", "Best Prebuilt TV Show Collection/Season 01/s01.e79052499 - Mock Entry 21-1.nfo": "a7e21040a3fcb264f287e4eba1853f94", "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.mp4": "adc4a4e47515d628981fad20b6169b6c", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.mp4": "a7f2ca37b817da0512af297b5652e5e8", "Best Prebuilt TV Show Collection/Season 01/s01.e80052498 - Mock Entry 20-1.nfo": "78c08ad76d004a74cdba012f0b746c22", "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.mp4": "cfecc8d04705b7592f07a8d695cafc3a", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.mp4": "35ebd30923d8f321e023281ec6ef4e6f", "Best Prebuilt TV Show Collection/Season 01/s01.e80052499 - Mock Entry 20-2.nfo": "d9031d8608d12742c40a3279138c8371", "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.mp4": "fc0e44becce6b0f685020d781fab86f1", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.mp4": "204ce9c28ab7b69c659f0f76b6c75bfa", "Best Prebuilt TV Show Collection/Season 01/s01.e80052599 - Mock Entry 20-3.nfo": "74bd8dc59c5b76ae112d90638977c464", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1.json index 10d451e4..ba4303f1 100644 --- a/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1.json @@ -2,35 +2,35 @@ "Best Prebuilt TV Show Collection/.ytdl-sub-subscription_test-download-archive.json": "2990d95213df5a6c87825a28daa970f4", "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.mp4": "7c015856068b9fe798d0c24144550259", + "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.mp4": "611d34cf77a2123181a7c4096623f6b9", "Best Prebuilt TV Show Collection/Season 01/s01.e80052699 - Mock Entry 20-4.nfo": "0242d92e5e0d26745948b42c25eecfba", "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.mp4": "8894ba3b2cf87fffa2d3861f85db47d4", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.mp4": "f09ce6574a040cea52b7177f74b9ccea", "Best Prebuilt TV Show Collection/Season 01/s01.e80062698 - Mock Entry 20-5.nfo": "52b17bcad30adb6eaef4c3b21c1af76b", "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.mp4": "dfb7ffe2b77618e5607cc2a770122359", + "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.mp4": "435692c75994d929807b5f09726728ad", "Best Prebuilt TV Show Collection/Season 01/s01.e80062699 - Mock Entry 20-6.nfo": "31fb72d089fd680fea1340f3d3c41d64", "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.mp4": "f3e511d2e6b09bb72bea872b817b83d9", + "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.mp4": "613c386e00bf1c87e4f1d1acbbd98281", "Best Prebuilt TV Show Collection/Season 01/s01.e80072599 - Mock Entry 20-7.nfo": "df65227ee260219883ff618c9fbdf1d4", "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.mp4": "5b87fe189379fcce34facf73e7021538", + "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.mp4": "cda96931a134eeb6e05e0b5aec03ddfe", "Best Prebuilt TV Show Collection/Season 02/s02.e79052499 - Mock Entry 21-1.nfo": "0e4ea058d6a1e8760c80d3a02d25aafc", "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.mp4": "adc4a4e47515d628981fad20b6169b6c", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.mp4": "a7f2ca37b817da0512af297b5652e5e8", "Best Prebuilt TV Show Collection/Season 02/s02.e80052498 - Mock Entry 20-1.nfo": "1095322364e512dff963a44b2beffe30", "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.mp4": "cfecc8d04705b7592f07a8d695cafc3a", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.mp4": "35ebd30923d8f321e023281ec6ef4e6f", "Best Prebuilt TV Show Collection/Season 02/s02.e80052499 - Mock Entry 20-2.nfo": "5130c71d99f1c0726fee8cb8c9d82ba7", "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.mp4": "fc0e44becce6b0f685020d781fab86f1", + "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.mp4": "204ce9c28ab7b69c659f0f76b6c75bfa", "Best Prebuilt TV Show Collection/Season 02/s02.e80052599 - Mock Entry 20-3.nfo": "bde2a5401c49b58de0eacea4145d91ae", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a", diff --git a/tests/resources/expected_downloads_summaries/unit/music/YouTube Full Albums.json b/tests/resources/expected_downloads_summaries/unit/music/YouTube Full Albums.json index fabc51b6..325074c7 100644 --- a/tests/resources/expected_downloads_summaries/unit/music/YouTube Full Albums.json +++ b/tests/resources/expected_downloads_summaries/unit/music/YouTube Full Albums.json @@ -1,43 +1,43 @@ { ".ytdl-sub-subscription_test-download-archive.json": "a428f2d540b605d0804b99600372d5d1", - "subscription_test/[2020] Mock Entry 20-1/01 - " Intro.mp3": "c09c8590553141752697c6752667666a", - "subscription_test/[2020] Mock Entry 20-1/02 - " Part 1.mp3": "b25c7e1d9e614974ea255485414acc62", - "subscription_test/[2020] Mock Entry 20-1/03 - " Part 2.mp3": "3440ca7a156d8d8290e20854586f723a", - "subscription_test/[2020] Mock Entry 20-1/04 - " Part 3.mp3": "d093d2674a88b7e8f52edbf3629868ff", + "subscription_test/[2020] Mock Entry 20-1/01 - " Intro.mp3": "76a6712260a4bb2ab0b08ec6bd692872", + "subscription_test/[2020] Mock Entry 20-1/02 - " Part 1.mp3": "60b19560ad4eeaca372adf5cd1aecd72", + "subscription_test/[2020] Mock Entry 20-1/03 - " Part 2.mp3": "13bba7567936f04abe5bdd6348bdc4c2", + "subscription_test/[2020] Mock Entry 20-1/04 - " Part 3.mp3": "00f13beb106202ed920037bd17cffe44", "subscription_test/[2020] Mock Entry 20-1/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2020] Mock Entry 20-2/01 - " Intro.mp3": "d2b61d59894df0e99169a32d8cec0b2b", - "subscription_test/[2020] Mock Entry 20-2/02 - " Part 1.mp3": "30971eafe14adf282784462e1cedf822", - "subscription_test/[2020] Mock Entry 20-2/03 - " Part 2.mp3": "9e165ed71adc64a88192e686b97a0df3", - "subscription_test/[2020] Mock Entry 20-2/04 - " Part 3.mp3": "2e6851853e3634eaf52cc9258d8e375a", + "subscription_test/[2020] Mock Entry 20-2/01 - " Intro.mp3": "0d4c25d4d64ea8898f8252625b09177c", + "subscription_test/[2020] Mock Entry 20-2/02 - " Part 1.mp3": "afa2066ecac49e12a3995d97db94655e", + "subscription_test/[2020] Mock Entry 20-2/03 - " Part 2.mp3": "47be8ad5ed93e9051d6c78bd317a49e9", + "subscription_test/[2020] Mock Entry 20-2/04 - " Part 3.mp3": "23660f56222b169868908f0e58e0733e", "subscription_test/[2020] Mock Entry 20-2/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2020] Mock Entry 20-3/01 - " Intro.mp3": "915b052131b2c50bc3b91fee23a58da5", - "subscription_test/[2020] Mock Entry 20-3/02 - " Part 1.mp3": "030e82f87f761f75ad19adfc4dbf1063", - "subscription_test/[2020] Mock Entry 20-3/03 - " Part 2.mp3": "810c579dac3632e1155911a6cb4ef506", - "subscription_test/[2020] Mock Entry 20-3/04 - " Part 3.mp3": "42a1ce49b8217dbe614cbb5eee5f552c", + "subscription_test/[2020] Mock Entry 20-3/01 - " Intro.mp3": "9c89f6dd066601ee15ef9b2eaf5bb3ee", + "subscription_test/[2020] Mock Entry 20-3/02 - " Part 1.mp3": "83b4b06ac7f069863252c559757cfc1e", + "subscription_test/[2020] Mock Entry 20-3/03 - " Part 2.mp3": "b747271438b1cdd1dad5a45f9d0d1731", + "subscription_test/[2020] Mock Entry 20-3/04 - " Part 3.mp3": "85727de0a1799336fff0be042723a10e", "subscription_test/[2020] Mock Entry 20-3/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2020] Mock Entry 20-4/01 - " Intro.mp3": "bf0d2b91e2c7cc136dc0a56be94e0d7e", - "subscription_test/[2020] Mock Entry 20-4/02 - " Part 1.mp3": "eb8c0e34fef88b940f328956c03a2ae0", - "subscription_test/[2020] Mock Entry 20-4/03 - " Part 2.mp3": "23bcef6856e58d942cb83372434f8525", - "subscription_test/[2020] Mock Entry 20-4/04 - " Part 3.mp3": "08f50f77e9654362724a6e737f44306d", + "subscription_test/[2020] Mock Entry 20-4/01 - " Intro.mp3": "c0179198948f6cb096437ef1784a5689", + "subscription_test/[2020] Mock Entry 20-4/02 - " Part 1.mp3": "b414ce97e727526cb51c2a86b8f32aed", + "subscription_test/[2020] Mock Entry 20-4/03 - " Part 2.mp3": "c939dca7b62947db3c6a6152af97993c", + "subscription_test/[2020] Mock Entry 20-4/04 - " Part 3.mp3": "422f848cce38ddec42b3bb81f9c02f15", "subscription_test/[2020] Mock Entry 20-4/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2020] Mock Entry 20-5/01 - " Intro.mp3": "01f501bade24e3cab1fb6b58a6d2a8ac", - "subscription_test/[2020] Mock Entry 20-5/02 - " Part 1.mp3": "c5a0b461e1e1a67b028c05a9a3a774d2", - "subscription_test/[2020] Mock Entry 20-5/03 - " Part 2.mp3": "edda09bc84916663bc946696f6cc5ce0", - "subscription_test/[2020] Mock Entry 20-5/04 - " Part 3.mp3": "083a8482ee343b8e71fd4ff0710649fe", + "subscription_test/[2020] Mock Entry 20-5/01 - " Intro.mp3": "0ab2892cd0f0f8a68d8ea62be01f4300", + "subscription_test/[2020] Mock Entry 20-5/02 - " Part 1.mp3": "6944e056e81ac9aacbe292996b46ea7c", + "subscription_test/[2020] Mock Entry 20-5/03 - " Part 2.mp3": "666b6f573bb7d2b7589c3ab56556f872", + "subscription_test/[2020] Mock Entry 20-5/04 - " Part 3.mp3": "486952f7f0fbb3a9ea5a7af1689c286e", "subscription_test/[2020] Mock Entry 20-5/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2020] Mock Entry 20-6/01 - " Intro.mp3": "674f23188f8808e3b44c116e41594e76", - "subscription_test/[2020] Mock Entry 20-6/02 - " Part 1.mp3": "d8fc85cfade392b78fcf66670905a2d1", - "subscription_test/[2020] Mock Entry 20-6/03 - " Part 2.mp3": "23c7ac598d181ea16b945662beeae11e", - "subscription_test/[2020] Mock Entry 20-6/04 - " Part 3.mp3": "1dd37e65243aa545a55de73cd480580a", + "subscription_test/[2020] Mock Entry 20-6/01 - " Intro.mp3": "32705b6b80915bd2c970f2518522a609", + "subscription_test/[2020] Mock Entry 20-6/02 - " Part 1.mp3": "17dad487b84362c161a4b264393c5ee7", + "subscription_test/[2020] Mock Entry 20-6/03 - " Part 2.mp3": "616b7a82970884a2ee8ec258d162f13c", + "subscription_test/[2020] Mock Entry 20-6/04 - " Part 3.mp3": "bdfa25664e166b92e1a4996ca69ac8d6", "subscription_test/[2020] Mock Entry 20-6/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2020] Mock Entry 20-7/01 - " Intro.mp3": "c42c845f3028d910137af63af04c46a0", - "subscription_test/[2020] Mock Entry 20-7/02 - " Part 1.mp3": "514db0c82a210b9ea886e6ee2ab9327f", - "subscription_test/[2020] Mock Entry 20-7/03 - " Part 2.mp3": "112b6018f05e1aa62518001b61de0123", - "subscription_test/[2020] Mock Entry 20-7/04 - " Part 3.mp3": "5e1fb5d017a54b0156fa96ad71241f00", + "subscription_test/[2020] Mock Entry 20-7/01 - " Intro.mp3": "759e3fddad88de7ada2284b7f1f1a4c6", + "subscription_test/[2020] Mock Entry 20-7/02 - " Part 1.mp3": "c51bc61e097ad644637d26060f300041", + "subscription_test/[2020] Mock Entry 20-7/03 - " Part 2.mp3": "c8004e429f4075c5c64b941aa14361da", + "subscription_test/[2020] Mock Entry 20-7/04 - " Part 3.mp3": "2620fd0ae7bad9cc9b0d1eadbb583b1f", "subscription_test/[2020] Mock Entry 20-7/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/[2021] Mock Entry 21-1/01 - " Intro.mp3": "c1718b35ec1ab0b83c519acd30f32927", - "subscription_test/[2021] Mock Entry 21-1/02 - " Part 1.mp3": "4fed9885e3ce0f2d1ae0134c2b22ff08", - "subscription_test/[2021] Mock Entry 21-1/03 - " Part 2.mp3": "240774666c7636f9490ab98a77ef75bb", - "subscription_test/[2021] Mock Entry 21-1/04 - " Part 3.mp3": "0087935c3158fb4151dac0920dd84002", + "subscription_test/[2021] Mock Entry 21-1/01 - " Intro.mp3": "a86816be03ce0aa08fa72a784a0cc843", + "subscription_test/[2021] Mock Entry 21-1/02 - " Part 1.mp3": "84f14bdc616b45c3dbadf218da57bafe", + "subscription_test/[2021] Mock Entry 21-1/03 - " Part 2.mp3": "25f353289022c0fb8e8b9da6c7a364e4", + "subscription_test/[2021] Mock Entry 21-1/04 - " Part 3.mp3": "4b593b094cbb1cd5e4349ebbf83ae933", "subscription_test/[2021] Mock Entry 21-1/folder.jpg": "e80c508c4818454300133fe1dc1a9cd7" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_False.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_False.json new file mode 100644 index 00000000..c29e6056 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_False.json @@ -0,0 +1,19 @@ +{ + ".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed", + "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", + "subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1", + "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", + "subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405", + "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", + "subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352", + "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", + "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4", + "subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_True.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_True.json new file mode 100644 index 00000000..e970dc97 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_True.json @@ -0,0 +1,23 @@ +{ + ".ytdl-sub-subscription_test-download-archive.json": "bcb2e8e8139f5bf589e027f841aa9449", + "subscription_test/Custom Title.info.json": "INFO_JSON", + "subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Custom Title.mp4": "59b7b4ed1419fa43a2faff470b764bd2", + "subscription_test/Custom Title.nfo": "996f3993b2ba8a0614bef294492dde65", + "subscription_test/Mock Entry 20-4.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-4.mp4": "67a47bc3b9583a1d99040afc2589bfe6", + "subscription_test/Mock Entry 20-4.nfo": "1e7cb1a09c15f155623f6cd796e874ff", + "subscription_test/Mock Entry 20-5.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-5.mp4": "ae8d22fe6de7ee75a80508e53d2aaaae", + "subscription_test/Mock Entry 20-5.nfo": "f6100f194ae3c3b24211ff515d97ccdf", + "subscription_test/Mock Entry 20-6.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-6.mp4": "0c4c6f12b8e7993e34d9ba1dce605683", + "subscription_test/Mock Entry 20-6.nfo": "f51ae05104f5b98531535e75d623f941", + "subscription_test/Mock Entry 20-7.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-7.mp4": "d9fd30536ff349030467e25fa611055e", + "subscription_test/Mock Entry 20-7.nfo": "852c02ea3ab196d808854ee94a9313d2" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos.json new file mode 100644 index 00000000..c29e6056 --- /dev/null +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Emby Music Videos.json @@ -0,0 +1,19 @@ +{ + ".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed", + "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", + "subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1", + "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", + "subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405", + "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", + "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", + "subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352", + "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", + "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4", + "subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d" +} \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_False.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_False.json index 72e4fbce..c29e6056 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_False.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_False.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed", "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", "subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1", "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", "subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405", "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", "subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352", "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90", + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4", "subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_True.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_True.json index ced3db94..e970dc97 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_True.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos Categorized/multi_url_True.json @@ -2,22 +2,22 @@ ".ytdl-sub-subscription_test-download-archive.json": "bcb2e8e8139f5bf589e027f841aa9449", "subscription_test/Custom Title.info.json": "INFO_JSON", "subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Custom Title.mp4": "d7c2b8e961a8fcb612bd0d69c96bf0f3", + "subscription_test/Custom Title.mp4": "59b7b4ed1419fa43a2faff470b764bd2", "subscription_test/Custom Title.nfo": "996f3993b2ba8a0614bef294492dde65", "subscription_test/Mock Entry 20-4.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-4.mp4": "10f239edf916f8fb9eb461f24a821578", + "subscription_test/Mock Entry 20-4.mp4": "67a47bc3b9583a1d99040afc2589bfe6", "subscription_test/Mock Entry 20-4.nfo": "1e7cb1a09c15f155623f6cd796e874ff", "subscription_test/Mock Entry 20-5.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-5.mp4": "046878a073fb269ec61401cdeb834707", + "subscription_test/Mock Entry 20-5.mp4": "ae8d22fe6de7ee75a80508e53d2aaaae", "subscription_test/Mock Entry 20-5.nfo": "f6100f194ae3c3b24211ff515d97ccdf", "subscription_test/Mock Entry 20-6.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-6.mp4": "a50ead5d08456f0bcb907f1912caefcf", + "subscription_test/Mock Entry 20-6.mp4": "0c4c6f12b8e7993e34d9ba1dce605683", "subscription_test/Mock Entry 20-6.nfo": "f51ae05104f5b98531535e75d623f941", "subscription_test/Mock Entry 20-7.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-7.mp4": "5b35f04254c38c33ba056274b6c3af9f", + "subscription_test/Mock Entry 20-7.mp4": "d9fd30536ff349030467e25fa611055e", "subscription_test/Mock Entry 20-7.nfo": "852c02ea3ab196d808854ee94a9313d2" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos.json index 72e4fbce..c29e6056 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Jellyfin Music Videos.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed", "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", "subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1", "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", "subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405", "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", "subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352", "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90", + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4", "subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_False.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_False.json index 72e4fbce..c29e6056 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_False.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_False.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed", "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", "subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1", "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", "subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405", "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", "subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352", "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90", + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4", "subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_True.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_True.json index ced3db94..e970dc97 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_True.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos Categorized/multi_url_True.json @@ -2,22 +2,22 @@ ".ytdl-sub-subscription_test-download-archive.json": "bcb2e8e8139f5bf589e027f841aa9449", "subscription_test/Custom Title.info.json": "INFO_JSON", "subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Custom Title.mp4": "d7c2b8e961a8fcb612bd0d69c96bf0f3", + "subscription_test/Custom Title.mp4": "59b7b4ed1419fa43a2faff470b764bd2", "subscription_test/Custom Title.nfo": "996f3993b2ba8a0614bef294492dde65", "subscription_test/Mock Entry 20-4.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-4.mp4": "10f239edf916f8fb9eb461f24a821578", + "subscription_test/Mock Entry 20-4.mp4": "67a47bc3b9583a1d99040afc2589bfe6", "subscription_test/Mock Entry 20-4.nfo": "1e7cb1a09c15f155623f6cd796e874ff", "subscription_test/Mock Entry 20-5.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-5.mp4": "046878a073fb269ec61401cdeb834707", + "subscription_test/Mock Entry 20-5.mp4": "ae8d22fe6de7ee75a80508e53d2aaaae", "subscription_test/Mock Entry 20-5.nfo": "f6100f194ae3c3b24211ff515d97ccdf", "subscription_test/Mock Entry 20-6.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-6.mp4": "a50ead5d08456f0bcb907f1912caefcf", + "subscription_test/Mock Entry 20-6.mp4": "0c4c6f12b8e7993e34d9ba1dce605683", "subscription_test/Mock Entry 20-6.nfo": "f51ae05104f5b98531535e75d623f941", "subscription_test/Mock Entry 20-7.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-7.mp4": "5b35f04254c38c33ba056274b6c3af9f", + "subscription_test/Mock Entry 20-7.mp4": "d9fd30536ff349030467e25fa611055e", "subscription_test/Mock Entry 20-7.nfo": "852c02ea3ab196d808854ee94a9313d2" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos.json index 72e4fbce..c29e6056 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Kodi Music Videos.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed", "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", "subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1", "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", "subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405", "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", "subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352", "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90", + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4", "subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_False.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_False.json index 78727e67..c6d6fdd1 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_False.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_False.json @@ -2,14 +2,14 @@ ".ytdl-sub-subscription_test-download-archive.json": "6cb47203ec56d8318a0d428b5b4efa5d", "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90" + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_True.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_True.json index 5f236acf..9b3c2a0f 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_True.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos Categorized/multi_url_True.json @@ -2,17 +2,17 @@ ".ytdl-sub-subscription_test-download-archive.json": "69a9960ce69e3813269412fa7cb1bd33", "subscription_test/Custom Title.info.json": "INFO_JSON", "subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Custom Title.mp4": "d7c2b8e961a8fcb612bd0d69c96bf0f3", + "subscription_test/Custom Title.mp4": "59b7b4ed1419fa43a2faff470b764bd2", "subscription_test/Mock Entry 20-4.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-4.mp4": "10f239edf916f8fb9eb461f24a821578", + "subscription_test/Mock Entry 20-4.mp4": "67a47bc3b9583a1d99040afc2589bfe6", "subscription_test/Mock Entry 20-5.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-5.mp4": "046878a073fb269ec61401cdeb834707", + "subscription_test/Mock Entry 20-5.mp4": "ae8d22fe6de7ee75a80508e53d2aaaae", "subscription_test/Mock Entry 20-6.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-6.mp4": "a50ead5d08456f0bcb907f1912caefcf", + "subscription_test/Mock Entry 20-6.mp4": "0c4c6f12b8e7993e34d9ba1dce605683", "subscription_test/Mock Entry 20-7.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-7.mp4": "5b35f04254c38c33ba056274b6c3af9f" + "subscription_test/Mock Entry 20-7.mp4": "d9fd30536ff349030467e25fa611055e" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos.json b/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos.json index 78727e67..c6d6fdd1 100644 --- a/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos.json +++ b/tests/resources/expected_downloads_summaries/unit/music_videos/Plex Music Videos.json @@ -2,14 +2,14 @@ ".ytdl-sub-subscription_test-download-archive.json": "6cb47203ec56d8318a0d428b5b4efa5d", "subscription_test/Mock Entry 20-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8", + "subscription_test/Mock Entry 20-1.mp4": "a164f7c885a335e7ab0c4ccf0ed38ee0", "subscription_test/Mock Entry 20-2.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02", + "subscription_test/Mock Entry 20-2.mp4": "eaa5e5207d1a9a98d9aac10ca55adb1b", "subscription_test/Mock Entry 20-3.info.json": "INFO_JSON", "subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d", + "subscription_test/Mock Entry 20-3.mp4": "3486772701dbed59f44b5028e96ec0e0", "subscription_test/Mock Entry 21-1.info.json": "INFO_JSON", "subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90" + "subscription_test/Mock Entry 21-1.mp4": "dedfd5373815d94c5187c5b27a6312f4" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json index 35af6f8f..57c4e26f 100644 --- a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_migrated.json @@ -2,16 +2,16 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json": "0011c85c3339ac1301d80be2f3330e67", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "d14710bc7c6ef6d7d64d7d955b7b32d0", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000001 - Mock Entry 20-3.mp4": "1d0714fb3ca4d31a0f88492a06a6142d", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "1eec4bae304dcb12ba91cbe5b3ef52c1", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000002 - Mock Entry 20-2.mp4": "a8f3f8580989f3927606bf6060a64e30", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "ff2a6a35b533c4e0f41d0c761c9c2f08", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e000003 - Mock Entry 20-1.mp4": "265b1104b382fd157a123842e49fcdfd", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "9767c6fc84316bc5d27e6527c7ca62b4", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e000004 - Mock Entry 21-1.mp4": "09fa4599fd48836c6c0da0760c306006", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json index adc8a5f1..33cbc2b5 100644 --- a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.json @@ -2,16 +2,16 @@ "Best Prebuilt TV Show by Date/.ytdl-sub-subscription_test-download-archive.json": "6cd67e6b935bc5b2521f6ed0fa2ea2ed", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "35770012af9ea0d5d1da45b9667b421f", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080701 - Mock Entry 20-3.mp4": "c712d7fd5dfdb9bb6c2be99f7c796c6f", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "65010c031d76bc70055befd3aac68139", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080801 - Mock Entry 20-2.mp4": "7ec7ac1923053c4b5653bbfb8924fdc6", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "9b1066f98309432e9d4d7aaf8d30ddcd", + "Best Prebuilt TV Show by Date/Season 2020/s2020.e080802 - Mock Entry 20-1.mp4": "414c243016f406451c790757949173aa", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "129a7ee05d048db25f4c0cf0581a9318", + "Best Prebuilt TV Show by Date/Season 2021/s2021.e080801 - Mock Entry 21-1.mp4": "e2f858efd08ac82749fd01db65d8341e", "Best Prebuilt TV Show by Date/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show by Date/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json index 00d40f43..bae82de6 100644 --- a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_migrated.json @@ -3,16 +3,16 @@ "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json index 3196e323..eb4cb28e 100644 --- a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json +++ b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_migrated.json @@ -3,29 +3,29 @@ "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "1427a80ab1d6e2e97506aebee3dd7a21", + "Best Prebuilt TV Show Collection/Season 01/s01.e000001 - Mock Entry 20-7.mp4": "6390603e1f9642ea573f25179b42f6af", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "c66baf5bb22f386d4bf6db027bb60b1a", + "Best Prebuilt TV Show Collection/Season 01/s01.e000002 - Mock Entry 20-6.mp4": "179e310bd4de1638d84b6da787ff9fa8", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "36b049db676868cbd2f2633bf3ab71fb", + "Best Prebuilt TV Show Collection/Season 01/s01.e000003 - Mock Entry 20-5.mp4": "3c412f47cb87e820138fb8909c672216", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "c25a7830dfa7a6601b8e7fbce117cc48", + "Best Prebuilt TV Show Collection/Season 01/s01.e000004 - Mock Entry 20-4.mp4": "8409a144e67c50573bae29bf76431d76", "Best Prebuilt TV Show Collection/Season 02/Season02.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "bab31d1ace4b9b74e5e55ed8e75a7488", + "Best Prebuilt TV Show Collection/Season 02/s02.e000001 - Mock Entry 20-3.mp4": "e107a93fa177c5f8594ccc3e023db8e8", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "ec823c5e3df83bd3e21b3bd1987a1b29", + "Best Prebuilt TV Show Collection/Season 02/s02.e000002 - Mock Entry 20-2.mp4": "2788e6b165908e6d52d7d771859b085e", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "3208b2ade911b6c4bc604b74d7128648", + "Best Prebuilt TV Show Collection/Season 02/s02.e000003 - Mock Entry 20-1.mp4": "6f0fd7cb3b3c49a8cf7baa6657cb5148", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "70aa6608cc7016fcf722b8bcd5282f4d", + "Best Prebuilt TV Show Collection/Season 02/s02.e000004 - Mock Entry 21-1.mp4": "6a983aa1b5dff65c54bd5f0b78003e07", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json index 86c112df..3ebd92c2 100644 --- a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.json @@ -3,16 +3,16 @@ "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "6aae009946f29e5d3095cfe24d4ce527", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "66f9ee8157f918711d28448fc67b7417", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "8f0a39a96e6dca16c09a7a0fd51df09a", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "d6df6d3e2e68c1b80f464d514a7f550c", + "Best Prebuilt TV Show Collection/Season 01/s01.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" } \ No newline at end of file diff --git a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json index 49ad460e..47a0f4ac 100644 --- a/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json +++ b/tests/resources/expected_downloads_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.json @@ -3,29 +3,29 @@ "Best Prebuilt TV Show Collection/Season 01/Season01.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "dc9260aa08144f4207c797e393b05939", + "Best Prebuilt TV Show Collection/Season 01/s01.e20060601 - Mock Entry 20-7.mp4": "fab0f2d0a74558a86b3935c6b962ebe3", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "c497a3f67cd0a98e690a22f8a7b8bf23", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070601 - Mock Entry 20-6.mp4": "9c2569c62ff81f067fe3d91324942c11", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "7518678ed81fe6f918cc89c10b923aa5", + "Best Prebuilt TV Show Collection/Season 01/s01.e20070602 - Mock Entry 20-5.mp4": "921e686edbf2a2e11f8d3eba9ef32592", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "9f755810e03f6476d1c7211eefa04fc1", + "Best Prebuilt TV Show Collection/Season 01/s01.e20080601 - Mock Entry 20-4.mp4": "a9fae3eca046637910a6e79aae4a8354", "Best Prebuilt TV Show Collection/Season 02/Season02.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "6aae009946f29e5d3095cfe24d4ce527", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080701 - Mock Entry 20-3.mp4": "a874866ad7096eb3bf31c72ea3315354", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "66f9ee8157f918711d28448fc67b7417", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080801 - Mock Entry 20-2.mp4": "fe6fcf8bfc13c0913ad1509703b447f1", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "8f0a39a96e6dca16c09a7a0fd51df09a", + "Best Prebuilt TV Show Collection/Season 02/s02.e20080802 - Mock Entry 20-1.mp4": "1d0f1081b402ebea9aeedf75f891c811", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1-thumb.jpg": "e80c508c4818454300133fe1dc1a9cd7", "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.info.json": "INFO_JSON", - "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "d6df6d3e2e68c1b80f464d514a7f550c", + "Best Prebuilt TV Show Collection/Season 02/s02.e21080801 - Mock Entry 21-1.mp4": "e5a844cc2c8fe19037afdf0692731414", "Best Prebuilt TV Show Collection/fanart.jpg": "15c9a67b554f415a662fdf7a3340cd61", "Best Prebuilt TV Show Collection/poster.jpg": "0b0fdbe56bab3e3fdda18730588d742a" } \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_fill.json b/tests/resources/expected_json/music/inspect_sub_fill.json new file mode 100644 index 00000000..b66bfe9b --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_fill.json @@ -0,0 +1,242 @@ +{ + "audio_extract": { + "codec": "best", + "enable": true + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": true, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "variables": {}, + "webpage_url": "{ modified_webpage_url }", + "ytdl_options": {} + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": [ + "{ track_album }" + ], + "albumartist": [ + "Lester Young" + ], + "albumartists": [ + "Lester Young" + ], + "artist": [ + "Lester Young" + ], + "artists": [ + "Lester Young" + ], + "date": [ + "{ track_date }" + ], + "genres": [ + "Jazz" + ], + "original_date": [ + "{ track_original_date }" + ], + "title": [ + "{ track_title }" + ], + "track": [ + "{ track_number }" + ], + "tracktotal": [ + "{ track_total }" + ], + "year": [ + "{ track_year }" + ] + }, + "output_options": { + "download_archive_name": ".ytdl-sub-Lester Young-download-archive.json", + "file_name": "{ track_full_path }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpzald2h7x", + "preserve_mtime": false, + "thumbnail_name": "{ album_cover_path }" + }, + "overrides": { + "album_cover_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }", + "album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }", + "artist_dir": "Lester Young", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{ webpage_url }", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpzald2h7x", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{ playlist_title }", + "track_album_artist": "Lester Young", + "track_artist": "Lester Young", + "track_date": "{ upload_date_standardized }", + "track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }", + "track_full_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }", + "track_genre": "Jazz", + "track_genre_default": "Unset", + "track_number": "{ playlist_index }", + "track_number_padded": "{ playlist_index_padded }", + "track_original_date": "{ upload_date_standardized }", + "track_title": "{ title }", + "track_total": "{ playlist_count }", + "track_year": "{ playlist_max_upload_year }", + "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_internal.json b/tests/resources/expected_json/music/inspect_sub_internal.json new file mode 100644 index 00000000..2409b16d --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_internal.json @@ -0,0 +1,242 @@ +{ + "audio_extract": { + "codec": "best", + "enable": true + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": true, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "variables": {}, + "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "ytdl_options": {} + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": [ + "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }" + ], + "albumartist": [ + "Lester Young" + ], + "albumartists": [ + "Lester Young" + ], + "artist": [ + "Lester Young" + ], + "artists": [ + "Lester Young" + ], + "date": [ + "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }" + ], + "genres": [ + "Jazz" + ], + "original_date": [ + "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }" + ], + "title": [ + "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }" + ], + "track": [ + "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }" + ], + "tracktotal": [ + "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }" + ], + "year": [ + "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }" + ] + }, + "output_options": { + "download_archive_name": ".ytdl-sub-Lester Young-download-archive.json", + "file_name": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/{ %concat( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ), \" - \", %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ), \".\", ext ) }", + "keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpdu1vad67", + "preserve_mtime": false, + "thumbnail_name": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/folder.jpg" + }, + "overrides": { + "album_cover_path": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/folder.jpg", + "album_dir": "[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", + "artist_dir": "Lester Young", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpdu1vad67", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "track_album_artist": "Lester Young", + "track_artist": "Lester Young", + "track_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "track_file_name": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }", + "track_full_path": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/{ %concat( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ), \" - \", %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ), \".\", ext ) }", + "track_genre": "Jazz", + "track_genre_default": "Unset", + "track_number": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", + "track_number_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", + "track_original_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "track_title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", + "track_total": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", + "track_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", + "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_original.json b/tests/resources/expected_json/music/inspect_sub_original.json new file mode 100644 index 00000000..988e58a9 --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_original.json @@ -0,0 +1,227 @@ +{ + "audio_extract": { + "codec": "best" + }, + "download": [ + { + "include_sibling_metadata": "{include_sibling_metadata}", + "playlist_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "url": "{ %array_at(urls, 0) }", + "webpage_url": "{modified_webpage_url}" + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "url": "{ %array_slice(urls, 1) }", + "webpage_url": "{modified_webpage_url}" + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": "{track_album}", + "albumartist": "{track_album_artist}", + "albumartists": [ + "{track_album_artist}" + ], + "artist": "{track_artist}", + "artists": [ + "{track_artist}" + ], + "date": "{track_date}", + "genres": [ + "{track_genre}" + ], + "original_date": "{track_original_date}", + "title": "{track_title}", + "track": "{track_number}", + "tracktotal": "{track_total}", + "year": "{track_year}" + }, + "output_options": { + "file_name": "{track_full_path}", + "maintain_download_archive": true, + "output_directory": "{music_directory}", + "thumbnail_name": "{album_cover_path}" + }, + "overrides": { + "album_cover_path": "{artist_dir}/{album_dir}/folder.{thumbnail_ext}", + "album_dir": "[{track_year}] {track_album_sanitized}", + "artist_dir": "{track_artist_sanitized}", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{webpage_url}", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpk6coazyn", + "resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [] }", + "resolution_assert_is_ignored": "{\n %print_if_true(\n %concat(title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\"),\n %contains_any(title, resolution_assert_ignore_titles)\n )\n}", + "resolution_assert_print": "{\n %print(\n %if(\n enable_resolution_assert,\n \"Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`\",\n \"Resolution assert is disabled. Use at your own risk!\"\n ),\n enable_resolution_assert,\n -1\n )\n}", + "resolution_readable": "{width}x{height}", + "subscription_array": "{%from_json('''[\"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\"]''')}", + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{playlist_title}", + "track_album_artist": "{track_artist}", + "track_artist": "{subscription_name}", + "track_date": "{upload_date_standardized}", + "track_file_name": "{track_number_padded} - {track_title_sanitized}.{ext}", + "track_full_path": "{artist_dir}/{album_dir}/{track_file_name}", + "track_genre": "{subscription_indent_1}", + "track_genre_default": "Unset", + "track_number": "{playlist_index}", + "track_number_padded": "{playlist_index_padded}", + "track_original_date": "{track_date}", + "track_title": "{title}", + "track_total": "{playlist_count}", + "track_year": "{playlist_max_upload_year}", + "url": "{subscription_value}", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": "{subscription_array}" + }, + "preset": [ + "_music_base", + "_multi_url", + "_albums_from_playlists", + "_throttle_protection", + "YouTube Releases", + "__preset__" + ], + "throttle_protection": { + "enable": "{\n %print(\n %if(\n enable_throttle_protection,\n \"Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`\",\n \"Throttle protection is disabled. Use at your own risk!\"\n ),\n enable_throttle_protection\n )\n}", + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "ytdl_options": { + "break_on_existing": true + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_resolve.json b/tests/resources/expected_json/music/inspect_sub_resolve.json new file mode 100644 index 00000000..347a61ac --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_resolve.json @@ -0,0 +1,242 @@ +{ + "audio_extract": { + "codec": "best", + "enable": true + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": true, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "variables": {}, + "webpage_url": "{ webpage_url }", + "ytdl_options": {} + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": [ + "{ playlist_title }" + ], + "albumartist": [ + "Lester Young" + ], + "albumartists": [ + "Lester Young" + ], + "artist": [ + "Lester Young" + ], + "artists": [ + "Lester Young" + ], + "date": [ + "{ upload_date_standardized }" + ], + "genres": [ + "Jazz" + ], + "original_date": [ + "{ upload_date_standardized }" + ], + "title": [ + "{ title }" + ], + "track": [ + "{ playlist_index }" + ], + "tracktotal": [ + "{ playlist_count }" + ], + "year": [ + "{ playlist_max_upload_year }" + ] + }, + "output_options": { + "download_archive_name": ".ytdl-sub-Lester Young-download-archive.json", + "file_name": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpd5oeacb3", + "preserve_mtime": false, + "thumbnail_name": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }" + }, + "overrides": { + "album_cover_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }", + "album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }", + "artist_dir": "Lester Young", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{ webpage_url }", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpd5oeacb3", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{ playlist_title }", + "track_album_artist": "Lester Young", + "track_artist": "Lester Young", + "track_date": "{ upload_date_standardized }", + "track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }", + "track_full_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }", + "track_genre": "Jazz", + "track_genre_default": "Unset", + "track_number": "{ playlist_index }", + "track_number_padded": "{ playlist_index_padded }", + "track_original_date": "{ upload_date_standardized }", + "track_title": "{ title }", + "track_total": "{ playlist_count }", + "track_year": "{ playlist_max_upload_year }", + "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_fill.json b/tests/resources/expected_json/music_video/inspect_sub_fill.json new file mode 100644 index 00000000..5999a01f --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_fill.json @@ -0,0 +1,314 @@ +{ + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "variables": {}, + "webpage_url": "{ modified_webpage_url }", + "ytdl_options": {} + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-Rick Astley-download-archive.json", + "file_name": "{ music_video_file_name }.{ ext }", + "info_json_name": "{ music_video_file_name }.{ info_json_ext }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpyukbh6ta", + "preserve_mtime": false, + "thumbnail_name": "{ music_video_file_name }.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, '' ) ) ) }", + "%flat_array__category_to_map_format": "{ %array_apply_fixed( %assert_then( %is_array( $1 ), $1, \"If using album categories, each category must map to an array\" ), $0, %flat_array__url_to_map_format ) }", + "%flat_array__url_keyed_map_format": "{ { %map_get( $0, \"url\" ): $0 } }", + "%flat_array__url_to_map_format": "{ %elif( %is_map( $0 ), %map_extend( $0, { \"category\": $1 } ), %is_string( $0 ), { \"url\": $0, \"category\": $1 }, %throw( \"If using album categories, each URL must be either a string or map with metadata fields\" ) ) }", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, { } ) ), $0, $1 ) }", + "%get_url_i": "{ %map_get( %map( %array_at( category_url_array, %int( %sub( $0, 1 ) ), { } ) ), \"url\", %array_at( subscription_array, %int( %sub( $0, 1 ) ), '' ) ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ [ ] }", + "category_url_map": "{ [ ] }", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "music_video_album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "Rick Astley", + "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpyukbh6ta", + "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }", + "music_video_file_name_suffix": "", + "music_video_genre": "Pop", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field( \"title\", title ) }", + "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "subscription_indent_1": "Pop", + "subscription_map": {}, + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{ music_video_album }", + "artist": "Rick Astley", + "genre": "Pop", + "premiered": "{ music_video_date }", + "title": "{ music_video_title }", + "year": "{ music_video_year }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_internal.json b/tests/resources/expected_json/music_video/inspect_sub_internal.json new file mode 100644 index 00000000..3cd8a4cd --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_internal.json @@ -0,0 +1,314 @@ +{ + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "variables": {}, + "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "ytdl_options": {} + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-Rick Astley-download-archive.json", + "file_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), '' ) }.{ %map_get( entry_metadata, \"ext\" ) }", + "info_json_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), '' ) }.info.json", + "keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmput7fc_rs", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), '' ) }.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, '' ) ) ) }", + "%flat_array__category_to_map_format": "{ %array_apply_fixed( %assert_then( %is_array( $1 ), $1, \"If using album categories, each category must map to an array\" ), $0, %flat_array__url_to_map_format ) }", + "%flat_array__url_keyed_map_format": "{ { %map_get( $0, \"url\" ): $0 } }", + "%flat_array__url_to_map_format": "{ %elif( %is_map( $0 ), %map_extend( $0, { \"category\": $1 } ), %is_string( $0 ), { \"url\": $0, \"category\": $1 }, %throw( \"If using album categories, each URL must be either a string or map with metadata fields\" ) ) }", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, { } ) ), $0, $1 ) }", + "%get_url_i": "{ %map_get( %map( %array_at( category_url_array, %int( %sub( $0, 1 ) ), { } ) ), \"url\", %array_at( subscription_array, %int( %sub( $0, 1 ) ), '' ) ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ [ ] }", + "category_url_map": "{ [ ] }", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "music_video_album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "Rick Astley", + "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmput7fc_rs", + "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", + "music_video_file_name_suffix": "", + "music_video_genre": "Pop", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "subscription_indent_1": "Pop", + "subscription_map": {}, + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "artist": "Rick Astley", + "genre": "Pop", + "premiered": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "title": "{ %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_original.json b/tests/resources/expected_json/music_video/inspect_sub_original.json new file mode 100644 index 00000000..cdd52c9f --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_original.json @@ -0,0 +1,235 @@ +{ + "download": [ + { + "download_reverse": false, + "url": "{ %array_apply(urls, %bilateral_url) }", + "webpage_url": "{modified_webpage_url}", + "ytdl_options": { + "playlist_items": "-1:0:-1" + } + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "playlist_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "url": "{ %array_at(urls, 0) }", + "webpage_url": "{modified_webpage_url}" + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "url": "{ %array_slice(urls, 1) }", + "webpage_url": "{modified_webpage_url}" + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "file_name": "{music_video_file_name}.{ext}", + "info_json_name": "{music_video_file_name}.{info_json_ext}", + "maintain_download_archive": true, + "output_directory": "{music_video_directory}", + "thumbnail_name": "{music_video_file_name}.jpg" + }, + "overrides": { + "%bilateral_url": "{ \n %if(\n %and(\n enable_bilateral_scraping,\n subscription_has_download_archive,\n %is_bilateral_url($0)\n ),\n $0,\n \"\"\n )\n}", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, null ) ) ) }", + "%flat_array__category_to_map_format": "{ \n %array_apply_fixed(\n %assert_then(\n %is_array( $1 ),\n $1,\n \"If using album categories, each category must map to an array\"\n ),\n $0,\n %flat_array__url_to_map_format \n )\n}", + "%flat_array__url_keyed_map_format": "{ { %map_get($0, \"url\"): $0 } }", + "%flat_array__url_to_map_format": "{\n %elif(\n %is_map($0),\n %map_extend( $0, { \"category\": $1 } ),\n %is_string($0),\n { \"url\": $0, \"category\": $1 },\n %throw(\"If using album categories, each URL must be either a string or map with metadata fields\")\n )\n}", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, {} ) ), $0, $1 ) }", + "%get_url_i": "{ \n %map_get(\n %map( %array_at( category_url_array, %int( %sub($0, 1) ), {} ) ),\n \"url\",\n %array_at(subscription_array, %int( %sub($0, 1) ), null)\n )\n}", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ %array_flatten( %map_apply( subscription_map, %flat_array__category_to_map_format ) ) }", + "category_url_map": "{\n %array_reduce(\n %array_apply( category_url_array, %flat_array__url_keyed_map_format ),\n %map_extend\n )\n}", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{title_sanitized_plex}", + "file_uid": "{uid_sanitized_plex}", + "include_sibling_metadata": false, + "modified_webpage_url": "{webpage_url}", + "music_video_album": "{ %get_url_field(\"category\", music_video_album_default) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "{subscription_name}", + "music_video_date": "{ \n %elif(\n %contains_url_field(\"date\"),\n %get_url_field(\"date\", upload_date_standardized),\n\n %contains_url_field(\"year\"),\n %concat( %get_url_field(\"date\", upload_year), \"-01-01\"),\n\n upload_date_standardized\n )\n}", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp3bmucwsg", + "music_video_file_name": "{music_video_artist_sanitized}/{music_video_title_sanitized}{music_video_file_name_suffix}", + "music_video_file_name_suffix": "", + "music_video_genre": "{subscription_indent_1}", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field(\"title\", title) }", + "music_video_year": "{\n %int(%elif(\n %contains_url_field(\"date\"),\n %slice( %get_url_field(\"date\", upload_date_standardized), 0, 4 ),\n\n %contains_url_field(\"year\"),\n %get_url_field(\"date\", upload_year),\n\n upload_year\n ))\n}", + "resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [] }", + "resolution_assert_is_ignored": "{\n %print_if_true(\n %concat(title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\"),\n %contains_any(title, resolution_assert_ignore_titles)\n )\n}", + "resolution_assert_print": "{\n %print(\n %if(\n enable_resolution_assert,\n \"Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`\",\n \"Resolution assert is disabled. Use at your own risk!\"\n ),\n enable_resolution_assert,\n -1\n )\n}", + "resolution_readable": "{width}x{height}", + "subscription_array": "{%from_json('''[\"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\"]''')}", + "subscription_indent_1": "Pop", + "subscription_map": "{ {} }", + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "{subscription_value}", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": "{ %array_apply(%range(100, 1), %get_url_i) }" + }, + "preset": [ + "_base", + "_plex_base", + "_url_bilateral_overrides", + "_multi_url_bilateral_inner", + "_multi_url", + "_multi_url_bilateral", + "_throttle_protection", + "_url_categorized", + "_plex_video_base", + "_music_video_base", + "_music_video_tags", + "Plex Music Videos", + "__preset__" + ], + "throttle_protection": { + "enable": "{\n %print(\n %if(\n enable_throttle_protection,\n \"Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`\",\n \"Throttle protection is disabled. Use at your own risk!\"\n ),\n enable_throttle_protection\n )\n}", + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{music_video_album}", + "artist": "{music_video_artist}", + "genre": "{music_video_genre}", + "premiered": "{music_video_date}", + "title": "{music_video_title}", + "year": "{music_video_year}" + }, + "ytdl_options": { + "break_on_existing": true + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_resolve.json b/tests/resources/expected_json/music_video/inspect_sub_resolve.json new file mode 100644 index 00000000..f3594e9c --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_resolve.json @@ -0,0 +1,314 @@ +{ + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "variables": {}, + "webpage_url": "{ webpage_url }", + "ytdl_options": {} + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-Rick Astley-download-archive.json", + "file_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", title ) ), '' ) }.{ ext }", + "info_json_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", title ) ), '' ) }.{ info_json_ext }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpstvsa5ot", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", title ) ), '' ) }.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, '' ) ) ) }", + "%flat_array__category_to_map_format": "{ %array_apply_fixed( %assert_then( %is_array( $1 ), $1, \"If using album categories, each category must map to an array\" ), $0, %flat_array__url_to_map_format ) }", + "%flat_array__url_keyed_map_format": "{ { %map_get( $0, \"url\" ): $0 } }", + "%flat_array__url_to_map_format": "{ %elif( %is_map( $0 ), %map_extend( $0, { \"category\": $1 } ), %is_string( $0 ), { \"url\": $0, \"category\": $1 }, %throw( \"If using album categories, each URL must be either a string or map with metadata fields\" ) ) }", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, { } ) ), $0, $1 ) }", + "%get_url_i": "{ %map_get( %map( %array_at( category_url_array, %int( %sub( $0, 1 ) ), { } ) ), \"url\", %array_at( subscription_array, %int( %sub( $0, 1 ) ), '' ) ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ [ ] }", + "category_url_map": "{ [ ] }", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "music_video_album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "Rick Astley", + "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpstvsa5ot", + "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }", + "music_video_file_name_suffix": "", + "music_video_genre": "Pop", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field( \"title\", title ) }", + "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "subscription_indent_1": "Pop", + "subscription_map": {}, + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "artist": "Rick Astley", + "genre": "Pop", + "premiered": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "title": "{ %get_url_field( \"title\", title ) }", + "year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_fill.json b/tests/resources/expected_json/tv_show/inspect_sub_fill.json new file mode 100644 index 00000000..6d1a8662 --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_fill.json @@ -0,0 +1,250 @@ +{ + "chapters": { + "allow_chapters_from_comments": false, + "embed_chapters": true, + "enable": true, + "force_key_frames": false + }, + "date_range": { + "breaks": true, + "enable": true, + "type": "upload_date" + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/@novapbs" + ], + "variables": {}, + "webpage_url": "{ modified_webpage_url }", + "ytdl_options": {} + } + ], + "file_convert": { + "convert_to": "mp4", + "convert_with": "yt-dlp", + "enable": true + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", + "file_name": "{ episode_file_path }.{ ext }", + "info_json_name": "{ episode_file_path }.{ info_json_ext }", + "keep_files_date_eval": "{ episode_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpc_p6gjjw/NOVA PBS", + "preserve_mtime": false, + "thumbnail_name": "{ thumbnail_file_name }" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", + "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", + "assert_not_collection": true, + "avatar_uncropped_thumbnail_file_name": "poster.jpg", + "banner_uncropped_thumbnail_file_name": "fanart.jpg", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "TV-14", + "episode_date_standardized": "{ upload_date_standardized }", + "episode_file_name": "s{ upload_year }.e{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) } - { title_sanitized_plex }", + "episode_file_path": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/{ %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) }", + "episode_number": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", + "episode_number_and_padded_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "episode_number_padded": "{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) }", + "episode_plot": "{ webpage_url }\n\n{ description }", + "episode_title": "{ upload_date_standardized } - { title }", + "episode_year": "{ %slice( upload_date_standardized, 0, 4 ) }", + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season { upload_year }", + "season_number": "{ upload_year }", + "season_number_padded": "{ upload_year }", + "season_poster_file_name": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/Season{ upload_year }.jpg", + "subscription_array": [ + "https://www.youtube.com/@novapbs" + ], + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "TV-14", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "TV-14", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "upload_date", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpc_p6gjjw", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "Documentaries", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "NOVA PBS", + "tv_show_poster_file_name": "poster.jpg", + "url": "https://www.youtube.com/@novapbs", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/@novapbs" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "TV-14", + "date": "{ episode_date_standardized }", + "episode_id": "{ episode_number }", + "genre": "Documentaries", + "show": "NOVA PBS", + "synopsis": "{ episode_plot }", + "title": "{ episode_title }", + "year": "{ episode_year }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_internal.json b/tests/resources/expected_json/tv_show/inspect_sub_internal.json new file mode 100644 index 00000000..3f1c1eaf --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_internal.json @@ -0,0 +1,250 @@ +{ + "chapters": { + "allow_chapters_from_comments": false, + "embed_chapters": true, + "enable": true, + "force_key_frames": false + }, + "date_range": { + "breaks": true, + "enable": true, + "type": "upload_date" + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/@novapbs" + ], + "variables": {}, + "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "ytdl_options": {} + } + ], + "file_convert": { + "convert_to": "mp4", + "convert_with": "yt-dlp", + "enable": true + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", + "file_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }.{ ext }", + "info_json_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }.info.json", + "keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpps6shcpt/NOVA PBS", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }-thumb.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", + "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", + "assert_not_collection": true, + "avatar_uncropped_thumbnail_file_name": "poster.jpg", + "banner_uncropped_thumbnail_file_name": "fanart.jpg", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "TV-14", + "episode_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "episode_file_name": "s{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.e{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ) } - { %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "episode_file_path": "{ %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) }/{ %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", + "episode_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) }{ %pad_zero( upload_date_index, 2 ) }", + "episode_number_and_padded_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ), 6 ] }", + "episode_number_padded": "{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ) }", + "episode_plot": "{ %map_get( entry_metadata, \"webpage_url\" ) }\n\n{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", + "episode_title": "{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) } - { %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", + "episode_year": "{ %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) }", + "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season { %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", + "season_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", + "season_number_padded": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", + "season_poster_file_name": "{ %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) }/Season{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.jpg", + "subscription_array": [ + "https://www.youtube.com/@novapbs" + ], + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "TV-14", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ), 6 ] }", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "TV-14", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "upload_date", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpps6shcpt", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "Documentaries", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "NOVA PBS", + "tv_show_poster_file_name": "poster.jpg", + "url": "https://www.youtube.com/@novapbs", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/@novapbs" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "TV-14", + "date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "episode_id": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) }{ %pad_zero( upload_date_index, 2 ) }", + "genre": "Documentaries", + "show": "NOVA PBS", + "synopsis": "{ %map_get( entry_metadata, \"webpage_url\" ) }\n\n{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", + "title": "{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) } - { %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", + "year": "{ %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_original.json b/tests/resources/expected_json/tv_show/inspect_sub_original.json new file mode 100644 index 00000000..0a7540d3 --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_original.json @@ -0,0 +1,255 @@ +{ + "chapters": { + "embed_chapters": true + }, + "date_range": { + "type": "{tv_show_date_range_type}" + }, + "download": [ + { + "download_reverse": false, + "url": "{ %array_apply(urls, %bilateral_url) }", + "webpage_url": "{modified_webpage_url}", + "ytdl_options": { + "playlist_items": "-1:0:-1" + } + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "playlist_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "url": "{ %array_at(urls, 0) }", + "webpage_url": "{modified_webpage_url}" + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "url": "{ %array_slice(urls, 1) }", + "webpage_url": "{modified_webpage_url}" + } + ], + "file_convert": { + "convert_to": "mp4" + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "file_name": "{episode_file_path}.{ext}", + "info_json_name": "{episode_file_path}.{info_json_ext}", + "keep_files_date_eval": "{episode_date_standardized}", + "maintain_download_archive": true, + "output_directory": "{tv_show_directory}/{tv_show_name_sanitized}", + "thumbnail_name": "{thumbnail_file_name}" + }, + "overrides": { + "%bilateral_url": "{ \n %if(\n %and(\n enable_bilateral_scraping,\n subscription_has_download_archive,\n %is_bilateral_url($0)\n ),\n $0,\n \"\"\n )\n}", + "%episode_ordering_": "{ %eq( %lower(tv_show_by_date_episode_ordering), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{\n %eq([tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering], [$0, $1])\n}", + "%season_ordering_": "{ %eq( %lower(tv_show_by_date_season_ordering), $0 ) }", + "assert_not_collection": "{\n %assert(\n %and(\n %not( %bool(s01_url) ),\n %not( %bool(s01_name) )\n ),\n \"Provided `s01_url` or `s01_name` variable to TV Show by Date preset when it expects `url`. Perhaps you meant to use the `TV Show Collection` preset?\"\n )\n}", + "avatar_uncropped_thumbnail_file_name": "{tv_show_poster_file_name}", + "banner_uncropped_thumbnail_file_name": "{tv_show_fanart_file_name}", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "{tv_show_content_rating}", + "episode_date_standardized": "{\n %if(\n %contains(tv_show_by_date_season_ordering, \"release\"),\n release_date_standardized,\n upload_date_standardized\n )\n}", + "episode_file_name": "s{season_number_padded}.e{episode_number_padded} - {file_title}", + "episode_file_path": "{season_directory_name_sanitized}/{episode_file_name_sanitized}", + "episode_number": "{ %array_at(episode_number_and_padded_, 0) }", + "episode_number_and_padded_": "{\n %elif(\n %episode_ordering_( \"upload-day\" ), [ %concat(upload_day, upload_date_index_padded), 4 ],\n %episode_ordering_( \"upload-month-day\" ), [ %concat(upload_month, upload_day_padded, upload_date_index_padded), 6],\n %episode_ordering_( \"upload-month-day-reversed\" ), [ %concat(upload_day_of_year_reversed, upload_date_index_reversed_padded), 5],\n %episode_ordering_( \"release-day\" ), [ %concat(release_day, upload_date_index_padded), 4 ],\n %episode_ordering_( \"release-month-day\" ), [ %concat(release_month, release_day_padded, upload_date_index_padded), 6],\n %episode_ordering_( \"release-month-day-reversed\" ), [ %concat(release_day_of_year_reversed, upload_date_index_reversed_padded), 5],\n %episode_ordering_( \"download-index\" ), [ download_index, 6 ],\n %throw(\n 'tv_show_by_date_episode_ordering must be one of the following: \"upload-day\", \"upload-month-day\", \"upload-month-day-reversed\", \"release-day\", \"release-month-day\", \"release-month-day-reversed\", \"download-index\"'\n )\n )\n}", + "episode_number_padded": "{ %pad_zero( %int(episode_number), %int(%array_at(episode_number_and_padded_, 1))) }", + "episode_plot": "{webpage_url}\n\n{description}", + "episode_title": "{episode_date_standardized} - {title}", + "episode_year": "{%slice(episode_date_standardized, 0, 4)}", + "file_title": "{title_sanitized_plex}", + "file_uid": "{uid_sanitized_plex}", + "include_sibling_metadata": false, + "modified_webpage_url": "{webpage_url}", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [] }", + "resolution_assert_is_ignored": "{\n %print_if_true(\n %concat(title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\"),\n %contains_any(title, resolution_assert_ignore_titles)\n )\n}", + "resolution_assert_print": "{\n %print(\n %if(\n enable_resolution_assert,\n \"Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`\",\n \"Resolution assert is disabled. Use at your own risk!\"\n ),\n enable_resolution_assert,\n -1\n )\n}", + "resolution_readable": "{width}x{height}", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season {season_number_padded}", + "season_number": "{\n %elif(\n %season_ordering_( \"upload-year\" ), upload_year,\n %season_ordering_( \"upload-year-month\" ), %concat(upload_year, upload_month_padded),\n %season_ordering_( \"release-year\" ), release_year,\n %season_ordering_( \"release-year-month\" ), %concat(release_year, release_month_padded),\n %throw(\n 'tv_show_by_date_season_ordering must be one of the following: \"upload-year\", \"upload-year-month\", \"release-year\", \"release-year-month\"'\n )\n )\n}", + "season_number_padded": "{season_number}", + "season_poster_file_name": "{season_directory_name_sanitized}/Season{season_number_padded}.jpg", + "subscription_array": "{%from_json('''[\"https://www.youtube.com/@novapbs\"]''')}", + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "{tv_show_content_rating_default}", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{episode_file_path}-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{\n %assert_then(\n %or(\n %ordering_pair_eq(\"upload-year\", \"upload-month-day\"),\n %ordering_pair_eq(\"upload-year\", \"upload-month-day-reversed\"),\n %ordering_pair_eq(\"upload-year\", \"download-index\"),\n %ordering_pair_eq(\"upload-year-month\", \"upload-day\"),\n %ordering_pair_eq(\"release-year\", \"release-month-day\"),\n %ordering_pair_eq(\"release-year\", \"release-month-day-reversed\"),\n %ordering_pair_eq(\"release-year\", \"download-index\"),\n %ordering_pair_eq(\"release-year-month\", \"release-day\")\n ),\n episode_number_and_padded_,\n \"Detected incompatibility between tv_show_by_date_season_ordering and tv_show_by_date_episode_ordering. Ensure you are not using both upload and release date, and that the year/month/day are included in the combined season and episode.\"\n )\n}", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "{subscription_indent_2}", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "{\n %if(\n %contains(tv_show_by_date_season_ordering, \"release\"),\n \"release_date\",\n \"upload_date\"\n )\n}", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp5yx9y73k", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "{subscription_indent_1}", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "{subscription_name}", + "tv_show_poster_file_name": "poster.jpg", + "url": "{subscription_value}", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": "{subscription_array}" + }, + "preset": [ + "_season_by_year", + "plex_tv_show_by_date", + "season_by_year__episode_by_month_day", + "Plex TV Show by Date", + "__preset__" + ], + "throttle_protection": { + "enable": "{\n %print(\n %if(\n enable_throttle_protection,\n \"Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`\",\n \"Throttle protection is disabled. Use at your own risk!\"\n ),\n enable_throttle_protection\n )\n}", + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "{episode_content_rating}", + "date": "{episode_date_standardized}", + "episode_id": "{episode_number}", + "genre": "{tv_show_genre}", + "show": "{tv_show_name}", + "synopsis": "{episode_plot}", + "title": "{episode_title}", + "year": "{episode_year}" + }, + "ytdl_options": { + "break_on_existing": true + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_resolve.json b/tests/resources/expected_json/tv_show/inspect_sub_resolve.json new file mode 100644 index 00000000..3c2898ea --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_resolve.json @@ -0,0 +1,250 @@ +{ + "chapters": { + "allow_chapters_from_comments": false, + "embed_chapters": true, + "enable": true, + "force_key_frames": false + }, + "date_range": { + "breaks": true, + "enable": true, + "type": "upload_date" + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/@novapbs" + ], + "variables": {}, + "webpage_url": "{ webpage_url }", + "ytdl_options": {} + } + ], + "file_convert": { + "convert_to": "mp4", + "convert_with": "yt-dlp", + "enable": true + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", + "file_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }.{ ext }", + "info_json_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }.{ info_json_ext }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp3j0cm_tw/NOVA PBS", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }-thumb.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", + "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", + "assert_not_collection": true, + "avatar_uncropped_thumbnail_file_name": "poster.jpg", + "banner_uncropped_thumbnail_file_name": "fanart.jpg", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "TV-14", + "episode_date_standardized": "{ upload_date_standardized }", + "episode_file_name": "s{ upload_year }.e{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) } - { title_sanitized_plex }", + "episode_file_path": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/{ %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) }", + "episode_number": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", + "episode_number_and_padded_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "episode_number_padded": "{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) }", + "episode_plot": "{ webpage_url }\n\n{ description }", + "episode_title": "{ upload_date_standardized } - { title }", + "episode_year": "{ %slice( upload_date_standardized, 0, 4 ) }", + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season { upload_year }", + "season_number": "{ upload_year }", + "season_number_padded": "{ upload_year }", + "season_poster_file_name": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/Season{ upload_year }.jpg", + "subscription_array": [ + "https://www.youtube.com/@novapbs" + ], + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "TV-14", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "TV-14", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "upload_date", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp3j0cm_tw", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "Documentaries", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "NOVA PBS", + "tv_show_poster_file_name": "poster.jpg", + "url": "https://www.youtube.com/@novapbs", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/@novapbs" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "TV-14", + "date": "{ upload_date_standardized }", + "episode_id": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", + "genre": "Documentaries", + "show": "NOVA PBS", + "synopsis": "{ webpage_url }\n\n{ description }", + "title": "{ upload_date_standardized } - { title }", + "year": "{ %slice( upload_date_standardized, 0, 4 ) }" + } +} \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt b/tests/resources/transaction_log_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Emby/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt b/tests/resources/transaction_log_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Jellyfin/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1.txt new file mode 100644 index 00000000..2515d402 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1.txt @@ -0,0 +1,130 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + title: Best Prebuilt TV Show by Date +{output_directory}/Season 200008 + s200008.e0701 - Mock Entry 20-3-thumb.jpg + s200008.e0701 - Mock Entry 20-3.info.json + s200008.e0701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 701 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s200008.e0701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 200008 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s200008.e0801 - Mock Entry 20-2-thumb.jpg + s200008.e0801 - Mock Entry 20-2.info.json + s200008.e0801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 801 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s200008.e0801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 200008 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s200008.e0802 - Mock Entry 20-1-thumb.jpg + s200008.e0802 - Mock Entry 20-1.info.json + s200008.e0802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 802 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s200008.e0802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 200008 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 +{output_directory}/Season 200108 + s200108.e0801 - Mock Entry 21-1-thumb.jpg + s200108.e0801 - Mock Entry 21-1.info.json + s200108.e0801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 801 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s200108.e0801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 200108 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1_reformatted_to_upload-year.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1_reformatted_to_upload-year.txt new file mode 100644 index 00000000..b4622a19 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year-month/release-day/is_yt_1_reformatted_to_upload-year.txt @@ -0,0 +1,145 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json +{output_directory}/Season 2020 + s2020.e000001 - Mock Entry 20-3-thumb.jpg + s2020.e000001 - Mock Entry 20-3.info.json + s2020.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2020 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000002 - Mock Entry 20-2-thumb.jpg + s2020.e000002 - Mock Entry 20-2.info.json + s2020.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000003 - Mock Entry 20-1-thumb.jpg + s2020.e000003 - Mock Entry 20-1.info.json + s2020.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s2020.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e000004 - Mock Entry 21-1-thumb.jpg + s2021.e000004 - Mock Entry 21-1.info.json + s2021.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s2021.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2021 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 200008 + s200008.e0701 - Mock Entry 20-3-thumb.jpg + s200008.e0701 - Mock Entry 20-3.info.json + s200008.e0701 - Mock Entry 20-3.mp4 + s200008.e0701 - Mock Entry 20-3.nfo + s200008.e0801 - Mock Entry 20-2-thumb.jpg + s200008.e0801 - Mock Entry 20-2.info.json + s200008.e0801 - Mock Entry 20-2.mp4 + s200008.e0801 - Mock Entry 20-2.nfo + s200008.e0802 - Mock Entry 20-1-thumb.jpg + s200008.e0802 - Mock Entry 20-1.info.json + s200008.e0802 - Mock Entry 20-1.mp4 + s200008.e0802 - Mock Entry 20-1.nfo +{output_directory}/Season 200108 + s200108.e0801 - Mock Entry 21-1-thumb.jpg + s200108.e0801 - Mock Entry 21-1.info.json + s200108.e0801 - Mock Entry 21-1.mp4 + s200108.e0801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1.txt new file mode 100644 index 00000000..b3777c57 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1.txt @@ -0,0 +1,130 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + title: Best Prebuilt TV Show by Date +{output_directory}/Season 2000 + s2000.e000001 - Mock Entry 20-3-thumb.jpg + s2000.e000001 - Mock Entry 20-3.info.json + s2000.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s2000.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2000 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s2000.e000002 - Mock Entry 20-2-thumb.jpg + s2000.e000002 - Mock Entry 20-2.info.json + s2000.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s2000.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2000 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s2000.e000003 - Mock Entry 20-1-thumb.jpg + s2000.e000003 - Mock Entry 20-1.info.json + s2000.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s2000.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2000 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 +{output_directory}/Season 2001 + s2001.e000004 - Mock Entry 21-1-thumb.jpg + s2001.e000004 - Mock Entry 21-1.info.json + s2001.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s2001.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2001 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1_reformatted_to_upload-year.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1_reformatted_to_upload-year.txt new file mode 100644 index 00000000..1a539b9a --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/download-index/is_yt_1_reformatted_to_upload-year.txt @@ -0,0 +1,145 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json +{output_directory}/Season 2020 + s2020.e000001 - Mock Entry 20-3-thumb.jpg + s2020.e000001 - Mock Entry 20-3.info.json + s2020.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2020 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000002 - Mock Entry 20-2-thumb.jpg + s2020.e000002 - Mock Entry 20-2.info.json + s2020.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000003 - Mock Entry 20-1-thumb.jpg + s2020.e000003 - Mock Entry 20-1.info.json + s2020.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s2020.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e000004 - Mock Entry 21-1-thumb.jpg + s2021.e000004 - Mock Entry 21-1.info.json + s2021.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s2021.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2021 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 2000 + s2000.e000001 - Mock Entry 20-3-thumb.jpg + s2000.e000001 - Mock Entry 20-3.info.json + s2000.e000001 - Mock Entry 20-3.mp4 + s2000.e000001 - Mock Entry 20-3.nfo + s2000.e000002 - Mock Entry 20-2-thumb.jpg + s2000.e000002 - Mock Entry 20-2.info.json + s2000.e000002 - Mock Entry 20-2.mp4 + s2000.e000002 - Mock Entry 20-2.nfo + s2000.e000003 - Mock Entry 20-1-thumb.jpg + s2000.e000003 - Mock Entry 20-1.info.json + s2000.e000003 - Mock Entry 20-1.mp4 + s2000.e000003 - Mock Entry 20-1.nfo +{output_directory}/Season 2001 + s2001.e000004 - Mock Entry 21-1-thumb.jpg + s2001.e000004 - Mock Entry 21-1.info.json + s2001.e000004 - Mock Entry 21-1.mp4 + s2001.e000004 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1.txt new file mode 100644 index 00000000..d8106b98 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1.txt @@ -0,0 +1,130 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + title: Best Prebuilt TV Show by Date +{output_directory}/Season 2000 + s2000.e14698 - Mock Entry 20-1-thumb.jpg + s2000.e14698 - Mock Entry 20-1.info.json + s2000.e14698 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 14698 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s2000.e14698 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 14698 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2000 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s2000.e14699 - Mock Entry 20-2-thumb.jpg + s2000.e14699 - Mock Entry 20-2.info.json + s2000.e14699 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 14699 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s2000.e14699 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 14699 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2000 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s2000.e14799 - Mock Entry 20-3-thumb.jpg + s2000.e14799 - Mock Entry 20-3.info.json + s2000.e14799 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 14799 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s2000.e14799 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 14799 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2000 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 +{output_directory}/Season 2001 + s2001.e14699 - Mock Entry 21-1-thumb.jpg + s2001.e14699 - Mock Entry 21-1.info.json + s2001.e14699 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 14699 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s2001.e14699 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 14699 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2001 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1_reformatted_to_upload-year.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1_reformatted_to_upload-year.txt new file mode 100644 index 00000000..64e36c64 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day-reversed/is_yt_1_reformatted_to_upload-year.txt @@ -0,0 +1,145 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json +{output_directory}/Season 2020 + s2020.e000001 - Mock Entry 20-3-thumb.jpg + s2020.e000001 - Mock Entry 20-3.info.json + s2020.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2020 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000002 - Mock Entry 20-2-thumb.jpg + s2020.e000002 - Mock Entry 20-2.info.json + s2020.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000003 - Mock Entry 20-1-thumb.jpg + s2020.e000003 - Mock Entry 20-1.info.json + s2020.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s2020.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e000004 - Mock Entry 21-1-thumb.jpg + s2021.e000004 - Mock Entry 21-1.info.json + s2021.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s2021.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2021 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 2000 + s2000.e14698 - Mock Entry 20-1-thumb.jpg + s2000.e14698 - Mock Entry 20-1.info.json + s2000.e14698 - Mock Entry 20-1.mp4 + s2000.e14698 - Mock Entry 20-1.nfo + s2000.e14699 - Mock Entry 20-2-thumb.jpg + s2000.e14699 - Mock Entry 20-2.info.json + s2000.e14699 - Mock Entry 20-2.mp4 + s2000.e14699 - Mock Entry 20-2.nfo + s2000.e14799 - Mock Entry 20-3-thumb.jpg + s2000.e14799 - Mock Entry 20-3.info.json + s2000.e14799 - Mock Entry 20-3.mp4 + s2000.e14799 - Mock Entry 20-3.nfo +{output_directory}/Season 2001 + s2001.e14699 - Mock Entry 21-1-thumb.jpg + s2001.e14699 - Mock Entry 21-1.info.json + s2001.e14699 - Mock Entry 21-1.mp4 + s2001.e14699 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1.txt new file mode 100644 index 00000000..364055d5 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1.txt @@ -0,0 +1,130 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + title: Best Prebuilt TV Show by Date +{output_directory}/Season 2000 + s2000.e080701 - Mock Entry 20-3-thumb.jpg + s2000.e080701 - Mock Entry 20-3.info.json + s2000.e080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 80701 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s2000.e080701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 80701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2000 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s2000.e080801 - Mock Entry 20-2-thumb.jpg + s2000.e080801 - Mock Entry 20-2.info.json + s2000.e080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 80801 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s2000.e080801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 80801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2000 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s2000.e080802 - Mock Entry 20-1-thumb.jpg + s2000.e080802 - Mock Entry 20-1.info.json + s2000.e080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 80802 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s2000.e080802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 80802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2000 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 +{output_directory}/Season 2001 + s2001.e080801 - Mock Entry 21-1-thumb.jpg + s2001.e080801 - Mock Entry 21-1.info.json + s2001.e080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 80801 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s2001.e080801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 80801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2001 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1_reformatted_to_upload-year.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1_reformatted_to_upload-year.txt new file mode 100644 index 00000000..ef01d6ff --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/release-year/release-month-day/is_yt_1_reformatted_to_upload-year.txt @@ -0,0 +1,145 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json +{output_directory}/Season 2020 + s2020.e000001 - Mock Entry 20-3-thumb.jpg + s2020.e000001 - Mock Entry 20-3.info.json + s2020.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2020 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000002 - Mock Entry 20-2-thumb.jpg + s2020.e000002 - Mock Entry 20-2.info.json + s2020.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000003 - Mock Entry 20-1-thumb.jpg + s2020.e000003 - Mock Entry 20-1.info.json + s2020.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s2020.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e000004 - Mock Entry 21-1-thumb.jpg + s2021.e000004 - Mock Entry 21-1.info.json + s2021.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s2021.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2021 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 2000 + s2000.e080701 - Mock Entry 20-3-thumb.jpg + s2000.e080701 - Mock Entry 20-3.info.json + s2000.e080701 - Mock Entry 20-3.mp4 + s2000.e080701 - Mock Entry 20-3.nfo + s2000.e080801 - Mock Entry 20-2-thumb.jpg + s2000.e080801 - Mock Entry 20-2.info.json + s2000.e080801 - Mock Entry 20-2.mp4 + s2000.e080801 - Mock Entry 20-2.nfo + s2000.e080802 - Mock Entry 20-1-thumb.jpg + s2000.e080802 - Mock Entry 20-1.info.json + s2000.e080802 - Mock Entry 20-1.mp4 + s2000.e080802 - Mock Entry 20-1.nfo +{output_directory}/Season 2001 + s2001.e080801 - Mock Entry 21-1-thumb.jpg + s2001.e080801 - Mock Entry 21-1.info.json + s2001.e080801 - Mock Entry 21-1.mp4 + s2001.e080801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1_reformatted_to_upload-year.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year_month__episode_by_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year-month/upload-day/is_yt_1_reformatted_to_upload-year.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1_reformatted_to_upload-year.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_download_index/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/download-index/is_yt_1_reformatted_to_upload-year.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1_reformatted_to_upload-year.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_by_date/season_by_year__episode_by_month_day_reversed/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day-reversed/is_yt_1_reformatted_to_upload-year.txt diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1.txt new file mode 100644 index 00000000..b89836ef --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1.txt @@ -0,0 +1,130 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + title: Best Prebuilt TV Show by Date +{output_directory}/Season 2020 + s2020.e080701 - Mock Entry 20-3-thumb.jpg + s2020.e080701 - Mock Entry 20-3.info.json + s2020.e080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 80701 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e080701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 80701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2020 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e080801 - Mock Entry 20-2-thumb.jpg + s2020.e080801 - Mock Entry 20-2.info.json + s2020.e080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 80801 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e080801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 80801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e080802 - Mock Entry 20-1-thumb.jpg + s2020.e080802 - Mock Entry 20-1.info.json + s2020.e080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 80802 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s2020.e080802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 80802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e080801 - Mock Entry 21-1-thumb.jpg + s2021.e080801 - Mock Entry 21-1.info.json + s2021.e080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 80801 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s2021.e080801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 80801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2021 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt new file mode 100644 index 00000000..3ff899a3 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/by-date/Kodi/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt @@ -0,0 +1,145 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json +{output_directory}/Season 2020 + s2020.e000001 - Mock Entry 20-3-thumb.jpg + s2020.e000001 - Mock Entry 20-3.info.json + s2020.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2020 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e000002 - Mock Entry 20-2-thumb.jpg + s2020.e000002 - Mock Entry 20-2.info.json + s2020.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e000003 - Mock Entry 20-1-thumb.jpg + s2020.e000003 - Mock Entry 20-1.info.json + s2020.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s2020.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2020 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e000004 - Mock Entry 21-1-thumb.jpg + s2021.e000004 - Mock Entry 21-1.info.json + s2021.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show by Date + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s2021.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2021 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 2020 + s2020.e080701 - Mock Entry 20-3-thumb.jpg + s2020.e080701 - Mock Entry 20-3.info.json + s2020.e080701 - Mock Entry 20-3.mp4 + s2020.e080701 - Mock Entry 20-3.nfo + s2020.e080801 - Mock Entry 20-2-thumb.jpg + s2020.e080801 - Mock Entry 20-2.info.json + s2020.e080801 - Mock Entry 20-2.mp4 + s2020.e080801 - Mock Entry 20-2.nfo + s2020.e080802 - Mock Entry 20-1-thumb.jpg + s2020.e080802 - Mock Entry 20-1.info.json + s2020.e080802 - Mock Entry 20-1.mp4 + s2020.e080802 - Mock Entry 20-1.nfo +{output_directory}/Season 2021 + s2021.e080801 - Mock Entry 21-1-thumb.jpg + s2021.e080801 - Mock Entry 21-1.info.json + s2021.e080801 - Mock Entry 21-1.mp4 + s2021.e080801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt b/tests/resources/transaction_log_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/plex_tv_show_by_date/season_by_year__episode_by_month_day/is_yt_1_reformatted_to_season_by_year__episode_by_download_index.txt rename to tests/resources/transaction_log_summaries/integration/by-date/Plex/upload-year/upload-month-day/is_yt_1_reformatted_to_upload-year.txt diff --git a/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1.txt new file mode 100644 index 00000000..f1082093 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1.txt @@ -0,0 +1,138 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + season01-poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + namedseason: + attributes: + number: 1 + tag: Named Season 1 + title: Best Prebuilt TV Show Collection +{output_directory}/Season 01 + s01.e20080701 - Mock Entry 20-3-thumb.jpg + s01.e20080701 - Mock Entry 20-3.info.json + s01.e20080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 20080701 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e20080701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 20080701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 1 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e20080801 - Mock Entry 20-2-thumb.jpg + s01.e20080801 - Mock Entry 20-2.info.json + s01.e20080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 20080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e20080801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 20080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e20080802 - Mock Entry 20-1-thumb.jpg + s01.e20080802 - Mock Entry 20-1.info.json + s01.e20080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 20080802 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e20080802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 20080802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e21080801 - Mock Entry 21-1-thumb.jpg + s01.e21080801 - Mock Entry 21-1.info.json + s01.e21080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 21080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s01.e21080801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 21080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 1 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + season.nfo + NFO tags: + season: + title: Named Season 1 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1.txt new file mode 100644 index 00000000..6633a28b --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1.txt @@ -0,0 +1,265 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + season01-poster.jpg + season02-poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + namedseason: + - + attributes: + number: 1 + tag: Named Season 1 + - + attributes: + number: 2 + tag: Named Season 2 + title: Best Prebuilt TV Show Collection +{output_directory}/Season 01 + s01.e20060601 - Mock Entry 20-7-thumb.jpg + s01.e20060601 - Mock Entry 20-7.info.json + s01.e20060601 - Mock Entry 20-7.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-06-06 + episode_id: 20060601 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-7.com + + The Description + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e20060601 - Mock Entry 20-7.nfo + NFO tags: + episodedetails: + aired: 2020-06-06 + episode: 20060601 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-7.com + + The Description + season: 1 + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e20070601 - Mock Entry 20-6-thumb.jpg + s01.e20070601 - Mock Entry 20-6.info.json + s01.e20070601 - Mock Entry 20-6.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 20070601 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-6.com + + The Description + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e20070601 - Mock Entry 20-6.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 20070601 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-6.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e20070602 - Mock Entry 20-5-thumb.jpg + s01.e20070602 - Mock Entry 20-5.info.json + s01.e20070602 - Mock Entry 20-5.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 20070602 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-5.com + + The Description + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e20070602 - Mock Entry 20-5.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 20070602 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-5.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e20080601 - Mock Entry 20-4-thumb.jpg + s01.e20080601 - Mock Entry 20-4.info.json + s01.e20080601 - Mock Entry 20-4.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-06 + episode_id: 20080601 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-4.com + + The Description + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 + s01.e20080601 - Mock Entry 20-4.nfo + NFO tags: + episodedetails: + aired: 2020-08-06 + episode: 20080601 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-4.com + + The Description + season: 1 + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 + season.nfo + NFO tags: + season: + title: Named Season 1 +{output_directory}/Season 02 + s02.e20080701 - Mock Entry 20-3-thumb.jpg + s02.e20080701 - Mock Entry 20-3.info.json + s02.e20080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 20080701 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e20080701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 20080701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e20080801 - Mock Entry 20-2-thumb.jpg + s02.e20080801 - Mock Entry 20-2.info.json + s02.e20080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 20080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e20080801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 20080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e20080802 - Mock Entry 20-1-thumb.jpg + s02.e20080802 - Mock Entry 20-1.info.json + s02.e20080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 20080802 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e20080802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 20080802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e21080801 - Mock Entry 21-1-thumb.jpg + s02.e21080801 - Mock Entry 21-1.info.json + s02.e21080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 21080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s02.e21080801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 21080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + season.nfo + NFO tags: + season: + title: Named Season 2 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Emby/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/jellyfin_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Jellyfin/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1.txt new file mode 100644 index 00000000..7192b6c7 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1.txt @@ -0,0 +1 @@ +No new, modified, or removed files in '{output_directory}' \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..2d20fc29 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/does-not-exist/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,4 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index_reversed/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_playlist_index/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/playlist-index/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1.txt new file mode 100644 index 00000000..0e0cc670 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1.txt @@ -0,0 +1,134 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + season01-poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + namedseason: + attributes: + number: 1 + tag: Named Season 1 + title: Best Prebuilt TV Show Collection +{output_directory}/Season 01 + s01.e100052498 - Mock Entry 20-1-thumb.jpg + s01.e100052498 - Mock Entry 20-1.info.json + s01.e100052498 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 100052498 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s01.e100052498 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 100052498 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 1 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s01.e100052499 - Mock Entry 20-2-thumb.jpg + s01.e100052499 - Mock Entry 20-2.info.json + s01.e100052499 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 100052499 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s01.e100052499 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 100052499 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 1 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s01.e100052599 - Mock Entry 20-3-thumb.jpg + s01.e100052599 - Mock Entry 20-3.info.json + s01.e100052599 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 100052599 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s01.e100052599 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 100052599 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 1 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s01.e99052499 - Mock Entry 21-1-thumb.jpg + s01.e99052499 - Mock Entry 21-1.info.json + s01.e99052499 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 99052499 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s01.e99052499 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 99052499 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 1 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..a6ff607e --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,143 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json +{output_directory}/Season 01 + s01.e000001 - Mock Entry 20-3-thumb.jpg + s01.e000001 - Mock Entry 20-3.info.json + s01.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 1 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e000002 - Mock Entry 20-2-thumb.jpg + s01.e000002 - Mock Entry 20-2.info.json + s01.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e000003 - Mock Entry 20-1-thumb.jpg + s01.e000003 - Mock Entry 20-1.info.json + s01.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e000004 - Mock Entry 21-1-thumb.jpg + s01.e000004 - Mock Entry 21-1.info.json + s01.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s01.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 1 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 01 + s01.e100052498 - Mock Entry 20-1-thumb.jpg + s01.e100052498 - Mock Entry 20-1.info.json + s01.e100052498 - Mock Entry 20-1.mp4 + s01.e100052498 - Mock Entry 20-1.nfo + s01.e100052499 - Mock Entry 20-2-thumb.jpg + s01.e100052499 - Mock Entry 20-2.info.json + s01.e100052499 - Mock Entry 20-2.mp4 + s01.e100052499 - Mock Entry 20-2.nfo + s01.e100052599 - Mock Entry 20-3-thumb.jpg + s01.e100052599 - Mock Entry 20-3.info.json + s01.e100052599 - Mock Entry 20-3.mp4 + s01.e100052599 - Mock Entry 20-3.nfo + s01.e99052499 - Mock Entry 21-1-thumb.jpg + s01.e99052499 - Mock Entry 21-1.info.json + s01.e99052499 - Mock Entry 21-1.mp4 + s01.e99052499 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1.txt new file mode 100644 index 00000000..135d1231 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1.txt @@ -0,0 +1,257 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + season01-poster.jpg + season02-poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + namedseason: + - + attributes: + number: 1 + tag: Named Season 1 + - + attributes: + number: 2 + tag: Named Season 2 + title: Best Prebuilt TV Show Collection +{output_directory}/Season 01 + s01.e100052699 - Mock Entry 20-4-thumb.jpg + s01.e100052699 - Mock Entry 20-4.info.json + s01.e100052699 - Mock Entry 20-4.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-06 + episode_id: 100052699 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-4.com + + The Description + title: 2000-08-06 - Mock Entry 20-4 + year: 2000 + s01.e100052699 - Mock Entry 20-4.nfo + NFO tags: + episodedetails: + aired: 2000-08-06 + episode: 100052699 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-4.com + + The Description + season: 1 + title: 2000-08-06 - Mock Entry 20-4 + year: 2000 + s01.e100062698 - Mock Entry 20-5-thumb.jpg + s01.e100062698 - Mock Entry 20-5.info.json + s01.e100062698 - Mock Entry 20-5.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-07-06 + episode_id: 100062698 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-5.com + + The Description + title: 2000-07-06 - Mock Entry 20-5 + year: 2000 + s01.e100062698 - Mock Entry 20-5.nfo + NFO tags: + episodedetails: + aired: 2000-07-06 + episode: 100062698 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-5.com + + The Description + season: 1 + title: 2000-07-06 - Mock Entry 20-5 + year: 2000 + s01.e100062699 - Mock Entry 20-6-thumb.jpg + s01.e100062699 - Mock Entry 20-6.info.json + s01.e100062699 - Mock Entry 20-6.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-07-06 + episode_id: 100062699 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-6.com + + The Description + title: 2000-07-06 - Mock Entry 20-6 + year: 2000 + s01.e100062699 - Mock Entry 20-6.nfo + NFO tags: + episodedetails: + aired: 2000-07-06 + episode: 100062699 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-6.com + + The Description + season: 1 + title: 2000-07-06 - Mock Entry 20-6 + year: 2000 + s01.e100072599 - Mock Entry 20-7-thumb.jpg + s01.e100072599 - Mock Entry 20-7.info.json + s01.e100072599 - Mock Entry 20-7.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-06-06 + episode_id: 100072599 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-7.com + + The Description + title: 2000-06-06 - Mock Entry 20-7 + year: 2000 + s01.e100072599 - Mock Entry 20-7.nfo + NFO tags: + episodedetails: + aired: 2000-06-06 + episode: 100072599 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-7.com + + The Description + season: 1 + title: 2000-06-06 - Mock Entry 20-7 + year: 2000 +{output_directory}/Season 02 + s02.e100052498 - Mock Entry 20-1-thumb.jpg + s02.e100052498 - Mock Entry 20-1.info.json + s02.e100052498 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 100052498 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s02.e100052498 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 100052498 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s02.e100052499 - Mock Entry 20-2-thumb.jpg + s02.e100052499 - Mock Entry 20-2.info.json + s02.e100052499 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 100052499 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s02.e100052499 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 100052499 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s02.e100052599 - Mock Entry 20-3-thumb.jpg + s02.e100052599 - Mock Entry 20-3.info.json + s02.e100052599 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 100052599 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s02.e100052599 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 100052599 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s02.e99052499 - Mock Entry 21-1-thumb.jpg + s02.e99052499 - Mock Entry 21-1.info.json + s02.e99052499 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 99052499 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s02.e99052499 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 99052499 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..2d56f95a --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,277 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json +{output_directory}/Season 01 + s01.e000001 - Mock Entry 20-7-thumb.jpg + s01.e000001 - Mock Entry 20-7.info.json + s01.e000001 - Mock Entry 20-7.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-06-06 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-7.com + + The Description + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e000001 - Mock Entry 20-7.nfo + NFO tags: + episodedetails: + aired: 2020-06-06 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-7.com + + The Description + season: 1 + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e000002 - Mock Entry 20-6-thumb.jpg + s01.e000002 - Mock Entry 20-6.info.json + s01.e000002 - Mock Entry 20-6.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-6.com + + The Description + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e000002 - Mock Entry 20-6.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-6.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e000003 - Mock Entry 20-5-thumb.jpg + s01.e000003 - Mock Entry 20-5.info.json + s01.e000003 - Mock Entry 20-5.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-5.com + + The Description + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e000003 - Mock Entry 20-5.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-5.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e000004 - Mock Entry 20-4-thumb.jpg + s01.e000004 - Mock Entry 20-4.info.json + s01.e000004 - Mock Entry 20-4.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-06 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-4.com + + The Description + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 + s01.e000004 - Mock Entry 20-4.nfo + NFO tags: + episodedetails: + aired: 2020-08-06 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-4.com + + The Description + season: 1 + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 +{output_directory}/Season 02 + s02.e000001 - Mock Entry 20-3-thumb.jpg + s02.e000001 - Mock Entry 20-3.info.json + s02.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e000002 - Mock Entry 20-2-thumb.jpg + s02.e000002 - Mock Entry 20-2.info.json + s02.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e000003 - Mock Entry 20-1-thumb.jpg + s02.e000003 - Mock Entry 20-1.info.json + s02.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e000004 - Mock Entry 21-1-thumb.jpg + s02.e000004 - Mock Entry 21-1.info.json + s02.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s02.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 01 + s01.e100052699 - Mock Entry 20-4-thumb.jpg + s01.e100052699 - Mock Entry 20-4.info.json + s01.e100052699 - Mock Entry 20-4.mp4 + s01.e100052699 - Mock Entry 20-4.nfo + s01.e100062698 - Mock Entry 20-5-thumb.jpg + s01.e100062698 - Mock Entry 20-5.info.json + s01.e100062698 - Mock Entry 20-5.mp4 + s01.e100062698 - Mock Entry 20-5.nfo + s01.e100062699 - Mock Entry 20-6-thumb.jpg + s01.e100062699 - Mock Entry 20-6.info.json + s01.e100062699 - Mock Entry 20-6.mp4 + s01.e100062699 - Mock Entry 20-6.nfo + s01.e100072599 - Mock Entry 20-7-thumb.jpg + s01.e100072599 - Mock Entry 20-7.info.json + s01.e100072599 - Mock Entry 20-7.mp4 + s01.e100072599 - Mock Entry 20-7.nfo +{output_directory}/Season 02 + s02.e100052498 - Mock Entry 20-1-thumb.jpg + s02.e100052498 - Mock Entry 20-1.info.json + s02.e100052498 - Mock Entry 20-1.mp4 + s02.e100052498 - Mock Entry 20-1.nfo + s02.e100052499 - Mock Entry 20-2-thumb.jpg + s02.e100052499 - Mock Entry 20-2.info.json + s02.e100052499 - Mock Entry 20-2.mp4 + s02.e100052499 - Mock Entry 20-2.nfo + s02.e100052599 - Mock Entry 20-3-thumb.jpg + s02.e100052599 - Mock Entry 20-3.info.json + s02.e100052599 - Mock Entry 20-3.mp4 + s02.e100052599 - Mock Entry 20-3.nfo + s02.e99052499 - Mock Entry 21-1-thumb.jpg + s02.e99052499 - Mock Entry 21-1.info.json + s02.e99052499 - Mock Entry 21-1.mp4 + s02.e99052499 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1.txt new file mode 100644 index 00000000..cf5a8cd3 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1.txt @@ -0,0 +1,134 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + season01-poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + namedseason: + attributes: + number: 1 + tag: Named Season 1 + title: Best Prebuilt TV Show Collection +{output_directory}/Season 01 + s01.e00080701 - Mock Entry 20-3-thumb.jpg + s01.e00080701 - Mock Entry 20-3.info.json + s01.e00080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 0080701 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s01.e00080701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 0080701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 1 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s01.e00080801 - Mock Entry 20-2-thumb.jpg + s01.e00080801 - Mock Entry 20-2.info.json + s01.e00080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 0080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s01.e00080801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 0080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 1 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s01.e00080802 - Mock Entry 20-1-thumb.jpg + s01.e00080802 - Mock Entry 20-1.info.json + s01.e00080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 0080802 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s01.e00080802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 0080802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 1 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s01.e01080801 - Mock Entry 21-1-thumb.jpg + s01.e01080801 - Mock Entry 21-1.info.json + s01.e01080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 1080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s01.e01080801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 1080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 1 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..b2832b2a --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,143 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json +{output_directory}/Season 01 + s01.e000001 - Mock Entry 20-3-thumb.jpg + s01.e000001 - Mock Entry 20-3.info.json + s01.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 1 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e000002 - Mock Entry 20-2-thumb.jpg + s01.e000002 - Mock Entry 20-2.info.json + s01.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e000003 - Mock Entry 20-1-thumb.jpg + s01.e000003 - Mock Entry 20-1.info.json + s01.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e000004 - Mock Entry 21-1-thumb.jpg + s01.e000004 - Mock Entry 21-1.info.json + s01.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s01.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 1 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 01 + s01.e00080701 - Mock Entry 20-3-thumb.jpg + s01.e00080701 - Mock Entry 20-3.info.json + s01.e00080701 - Mock Entry 20-3.mp4 + s01.e00080701 - Mock Entry 20-3.nfo + s01.e00080801 - Mock Entry 20-2-thumb.jpg + s01.e00080801 - Mock Entry 20-2.info.json + s01.e00080801 - Mock Entry 20-2.mp4 + s01.e00080801 - Mock Entry 20-2.nfo + s01.e00080802 - Mock Entry 20-1-thumb.jpg + s01.e00080802 - Mock Entry 20-1.info.json + s01.e00080802 - Mock Entry 20-1.mp4 + s01.e00080802 - Mock Entry 20-1.nfo + s01.e01080801 - Mock Entry 21-1-thumb.jpg + s01.e01080801 - Mock Entry 21-1.info.json + s01.e01080801 - Mock Entry 21-1.mp4 + s01.e01080801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1.txt new file mode 100644 index 00000000..c6da76b6 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1.txt @@ -0,0 +1,257 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json + fanart.jpg + poster.jpg + season01-poster.jpg + season02-poster.jpg + tvshow.nfo + NFO tags: + tvshow: + genre: ytdl-sub + mpaa: TV-14 + namedseason: + - + attributes: + number: 1 + tag: Named Season 1 + - + attributes: + number: 2 + tag: Named Season 2 + title: Best Prebuilt TV Show Collection +{output_directory}/Season 01 + s01.e00060601 - Mock Entry 20-7-thumb.jpg + s01.e00060601 - Mock Entry 20-7.info.json + s01.e00060601 - Mock Entry 20-7.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-06-06 + episode_id: 0060601 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-7.com + + The Description + title: 2000-06-06 - Mock Entry 20-7 + year: 2000 + s01.e00060601 - Mock Entry 20-7.nfo + NFO tags: + episodedetails: + aired: 2000-06-06 + episode: 0060601 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-7.com + + The Description + season: 1 + title: 2000-06-06 - Mock Entry 20-7 + year: 2000 + s01.e00070601 - Mock Entry 20-6-thumb.jpg + s01.e00070601 - Mock Entry 20-6.info.json + s01.e00070601 - Mock Entry 20-6.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-07-06 + episode_id: 0070601 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-6.com + + The Description + title: 2000-07-06 - Mock Entry 20-6 + year: 2000 + s01.e00070601 - Mock Entry 20-6.nfo + NFO tags: + episodedetails: + aired: 2000-07-06 + episode: 0070601 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-6.com + + The Description + season: 1 + title: 2000-07-06 - Mock Entry 20-6 + year: 2000 + s01.e00070602 - Mock Entry 20-5-thumb.jpg + s01.e00070602 - Mock Entry 20-5.info.json + s01.e00070602 - Mock Entry 20-5.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-07-06 + episode_id: 0070602 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-5.com + + The Description + title: 2000-07-06 - Mock Entry 20-5 + year: 2000 + s01.e00070602 - Mock Entry 20-5.nfo + NFO tags: + episodedetails: + aired: 2000-07-06 + episode: 0070602 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-5.com + + The Description + season: 1 + title: 2000-07-06 - Mock Entry 20-5 + year: 2000 + s01.e00080601 - Mock Entry 20-4-thumb.jpg + s01.e00080601 - Mock Entry 20-4.info.json + s01.e00080601 - Mock Entry 20-4.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-06 + episode_id: 0080601 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-4.com + + The Description + title: 2000-08-06 - Mock Entry 20-4 + year: 2000 + s01.e00080601 - Mock Entry 20-4.nfo + NFO tags: + episodedetails: + aired: 2000-08-06 + episode: 0080601 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-4.com + + The Description + season: 1 + title: 2000-08-06 - Mock Entry 20-4 + year: 2000 +{output_directory}/Season 02 + s02.e00080701 - Mock Entry 20-3-thumb.jpg + s02.e00080701 - Mock Entry 20-3.info.json + s02.e00080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-07 + episode_id: 0080701 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s02.e00080701 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2000-08-07 + episode: 0080701 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2 + title: 2000-08-07 - Mock Entry 20-3 + year: 2000 + s02.e00080801 - Mock Entry 20-2-thumb.jpg + s02.e00080801 - Mock Entry 20-2.info.json + s02.e00080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 0080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s02.e00080801 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 0080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2 + title: 2000-08-08 - Mock Entry 20-2 + year: 2000 + s02.e00080802 - Mock Entry 20-1-thumb.jpg + s02.e00080802 - Mock Entry 20-1.info.json + s02.e00080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2000-08-08 + episode_id: 0080802 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s02.e00080802 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2000-08-08 + episode: 0080802 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2 + title: 2000-08-08 - Mock Entry 20-1 + year: 2000 + s02.e01080801 - Mock Entry 21-1-thumb.jpg + s02.e01080801 - Mock Entry 21-1.info.json + s02.e01080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2001-08-08 + episode_id: 1080801 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 + s02.e01080801 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2001-08-08 + episode: 1080801 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2 + title: 2001-08-08 - Mock Entry 21-1 + year: 2001 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..2fc7ab5e --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/release-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,277 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json +{output_directory}/Season 01 + s01.e000001 - Mock Entry 20-7-thumb.jpg + s01.e000001 - Mock Entry 20-7.info.json + s01.e000001 - Mock Entry 20-7.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-06-06 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-7.com + + The Description + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e000001 - Mock Entry 20-7.nfo + NFO tags: + episodedetails: + aired: 2020-06-06 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-7.com + + The Description + season: 1 + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e000002 - Mock Entry 20-6-thumb.jpg + s01.e000002 - Mock Entry 20-6.info.json + s01.e000002 - Mock Entry 20-6.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-6.com + + The Description + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e000002 - Mock Entry 20-6.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-6.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e000003 - Mock Entry 20-5-thumb.jpg + s01.e000003 - Mock Entry 20-5.info.json + s01.e000003 - Mock Entry 20-5.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-5.com + + The Description + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e000003 - Mock Entry 20-5.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-5.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e000004 - Mock Entry 20-4-thumb.jpg + s01.e000004 - Mock Entry 20-4.info.json + s01.e000004 - Mock Entry 20-4.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-06 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-4.com + + The Description + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 + s01.e000004 - Mock Entry 20-4.nfo + NFO tags: + episodedetails: + aired: 2020-08-06 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-4.com + + The Description + season: 1 + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 +{output_directory}/Season 02 + s02.e000001 - Mock Entry 20-3-thumb.jpg + s02.e000001 - Mock Entry 20-3.info.json + s02.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e000002 - Mock Entry 20-2-thumb.jpg + s02.e000002 - Mock Entry 20-2.info.json + s02.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e000003 - Mock Entry 20-1-thumb.jpg + s02.e000003 - Mock Entry 20-1.info.json + s02.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e000004 - Mock Entry 21-1-thumb.jpg + s02.e000004 - Mock Entry 21-1.info.json + s02.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s02.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 01 + s01.e00060601 - Mock Entry 20-7-thumb.jpg + s01.e00060601 - Mock Entry 20-7.info.json + s01.e00060601 - Mock Entry 20-7.mp4 + s01.e00060601 - Mock Entry 20-7.nfo + s01.e00070601 - Mock Entry 20-6-thumb.jpg + s01.e00070601 - Mock Entry 20-6.info.json + s01.e00070601 - Mock Entry 20-6.mp4 + s01.e00070601 - Mock Entry 20-6.nfo + s01.e00070602 - Mock Entry 20-5-thumb.jpg + s01.e00070602 - Mock Entry 20-5.info.json + s01.e00070602 - Mock Entry 20-5.mp4 + s01.e00070602 - Mock Entry 20-5.nfo + s01.e00080601 - Mock Entry 20-4-thumb.jpg + s01.e00080601 - Mock Entry 20-4.info.json + s01.e00080601 - Mock Entry 20-4.mp4 + s01.e00080601 - Mock Entry 20-4.nfo +{output_directory}/Season 02 + s02.e00080701 - Mock Entry 20-3-thumb.jpg + s02.e00080701 - Mock Entry 20-3.info.json + s02.e00080701 - Mock Entry 20-3.mp4 + s02.e00080701 - Mock Entry 20-3.nfo + s02.e00080801 - Mock Entry 20-2-thumb.jpg + s02.e00080801 - Mock Entry 20-2.info.json + s02.e00080801 - Mock Entry 20-2.mp4 + s02.e00080801 - Mock Entry 20-2.nfo + s02.e00080802 - Mock Entry 20-1-thumb.jpg + s02.e00080802 - Mock Entry 20-1.info.json + s02.e00080802 - Mock Entry 20-1.mp4 + s02.e00080802 - Mock Entry 20-1.nfo + s02.e01080801 - Mock Entry 21-1-thumb.jpg + s02.e01080801 - Mock Entry 21-1.info.json + s02.e01080801 - Mock Entry 21-1.mp4 + s02.e01080801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day_reversed/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day-reversed/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..8d6a047e --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,143 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json +{output_directory}/Season 01 + s01.e000001 - Mock Entry 20-3-thumb.jpg + s01.e000001 - Mock Entry 20-3.info.json + s01.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 1 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s01.e000002 - Mock Entry 20-2-thumb.jpg + s01.e000002 - Mock Entry 20-2.info.json + s01.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s01.e000003 - Mock Entry 20-1-thumb.jpg + s01.e000003 - Mock Entry 20-1.info.json + s01.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 1 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s01.e000004 - Mock Entry 21-1-thumb.jpg + s01.e000004 - Mock Entry 21-1.info.json + s01.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s01.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 1 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 01 + s01.e20080701 - Mock Entry 20-3-thumb.jpg + s01.e20080701 - Mock Entry 20-3.info.json + s01.e20080701 - Mock Entry 20-3.mp4 + s01.e20080701 - Mock Entry 20-3.nfo + s01.e20080801 - Mock Entry 20-2-thumb.jpg + s01.e20080801 - Mock Entry 20-2.info.json + s01.e20080801 - Mock Entry 20-2.mp4 + s01.e20080801 - Mock Entry 20-2.nfo + s01.e20080802 - Mock Entry 20-1-thumb.jpg + s01.e20080802 - Mock Entry 20-1.info.json + s01.e20080802 - Mock Entry 20-1.mp4 + s01.e20080802 - Mock Entry 20-1.nfo + s01.e21080801 - Mock Entry 21-1-thumb.jpg + s01.e21080801 - Mock Entry 21-1.info.json + s01.e21080801 - Mock Entry 21-1.mp4 + s01.e21080801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/kodi_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt new file mode 100644 index 00000000..af9f7f22 --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/collection/Kodi/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt @@ -0,0 +1,277 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json +{output_directory}/Season 01 + s01.e000001 - Mock Entry 20-7-thumb.jpg + s01.e000001 - Mock Entry 20-7.info.json + s01.e000001 - Mock Entry 20-7.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-06-06 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-7.com + + The Description + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e000001 - Mock Entry 20-7.nfo + NFO tags: + episodedetails: + aired: 2020-06-06 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-7.com + + The Description + season: 1 + title: 2020-06-06 - Mock Entry 20-7 + year: 2020 + s01.e000002 - Mock Entry 20-6-thumb.jpg + s01.e000002 - Mock Entry 20-6.info.json + s01.e000002 - Mock Entry 20-6.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-6.com + + The Description + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e000002 - Mock Entry 20-6.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-6.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-6 + year: 2020 + s01.e000003 - Mock Entry 20-5-thumb.jpg + s01.e000003 - Mock Entry 20-5.info.json + s01.e000003 - Mock Entry 20-5.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-07-06 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-5.com + + The Description + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e000003 - Mock Entry 20-5.nfo + NFO tags: + episodedetails: + aired: 2020-07-06 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-5.com + + The Description + season: 1 + title: 2020-07-06 - Mock Entry 20-5 + year: 2020 + s01.e000004 - Mock Entry 20-4-thumb.jpg + s01.e000004 - Mock Entry 20-4.info.json + s01.e000004 - Mock Entry 20-4.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-06 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-4.com + + The Description + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 + s01.e000004 - Mock Entry 20-4.nfo + NFO tags: + episodedetails: + aired: 2020-08-06 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-4.com + + The Description + season: 1 + title: 2020-08-06 - Mock Entry 20-4 + year: 2020 +{output_directory}/Season 02 + s02.e000001 - Mock Entry 20-3-thumb.jpg + s02.e000001 - Mock Entry 20-3.info.json + s02.e000001 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 1 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e000001 - Mock Entry 20-3.nfo + NFO tags: + episodedetails: + aired: 2020-08-07 + episode: 1 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-3.com + + The Description + season: 2 + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s02.e000002 - Mock Entry 20-2-thumb.jpg + s02.e000002 - Mock Entry 20-2.info.json + s02.e000002 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 2 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e000002 - Mock Entry 20-2.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 2 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-2.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s02.e000003 - Mock Entry 20-1-thumb.jpg + s02.e000003 - Mock Entry 20-1.info.json + s02.e000003 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 3 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e000003 - Mock Entry 20-1.nfo + NFO tags: + episodedetails: + aired: 2020-08-08 + episode: 3 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://20-1.com + + The Description + season: 2 + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 + s02.e000004 - Mock Entry 21-1-thumb.jpg + s02.e000004 - Mock Entry 21-1.info.json + s02.e000004 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 4 + genre: ytdl-sub + show: Best Prebuilt TV Show Collection + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + s02.e000004 - Mock Entry 21-1.nfo + NFO tags: + episodedetails: + aired: 2021-08-08 + episode: 4 + genre: ytdl-sub + mpaa: TV-14 + plot: + https://21-1.com + + The Description + season: 2 + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 + +Files removed: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 01 + s01.e20060601 - Mock Entry 20-7-thumb.jpg + s01.e20060601 - Mock Entry 20-7.info.json + s01.e20060601 - Mock Entry 20-7.mp4 + s01.e20060601 - Mock Entry 20-7.nfo + s01.e20070601 - Mock Entry 20-6-thumb.jpg + s01.e20070601 - Mock Entry 20-6.info.json + s01.e20070601 - Mock Entry 20-6.mp4 + s01.e20070601 - Mock Entry 20-6.nfo + s01.e20070602 - Mock Entry 20-5-thumb.jpg + s01.e20070602 - Mock Entry 20-5.info.json + s01.e20070602 - Mock Entry 20-5.mp4 + s01.e20070602 - Mock Entry 20-5.nfo + s01.e20080601 - Mock Entry 20-4-thumb.jpg + s01.e20080601 - Mock Entry 20-4.info.json + s01.e20080601 - Mock Entry 20-4.mp4 + s01.e20080601 - Mock Entry 20-4.nfo +{output_directory}/Season 02 + s02.e20080701 - Mock Entry 20-3-thumb.jpg + s02.e20080701 - Mock Entry 20-3.info.json + s02.e20080701 - Mock Entry 20-3.mp4 + s02.e20080701 - Mock Entry 20-3.nfo + s02.e20080801 - Mock Entry 20-2-thumb.jpg + s02.e20080801 - Mock Entry 20-2.info.json + s02.e20080801 - Mock Entry 20-2.mp4 + s02.e20080801 - Mock Entry 20-2.nfo + s02.e20080802 - Mock Entry 20-1-thumb.jpg + s02.e20080802 - Mock Entry 20-1.info.json + s02.e20080802 - Mock Entry 20-1.mp4 + s02.e20080802 - Mock Entry 20-1.nfo + s02.e21080801 - Mock Entry 21-1-thumb.jpg + s02.e21080801 - Mock Entry 21-1.info.json + s02.e21080801 - Mock Entry 21-1.mp4 + s02.e21080801 - Mock Entry 21-1.nfo \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_1/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_1/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.txt b/tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1.txt rename to tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1.txt diff --git a/tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt b/tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt similarity index 100% rename from tests/resources/transaction_log_summaries/unit/plex_tv_show_collection/season_by_collection__episode_by_year_month_day/s_2/is_yt_1_reformatted_to_season_by_collection__episode_by_playlist_index_reversed.txt rename to tests/resources/transaction_log_summaries/integration/collection/Plex/upload-year-month-day/s_2/is_yt_1_reformatted_to_playlist-index-reversed.txt diff --git a/tests/resources/transaction_log_summaries/integration/prebuilt_presets/filter_duration.txt b/tests/resources/transaction_log_summaries/integration/prebuilt_presets/filter_duration.txt new file mode 100644 index 00000000..8c3ebf9a --- /dev/null +++ b/tests/resources/transaction_log_summaries/integration/prebuilt_presets/filter_duration.txt @@ -0,0 +1,66 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/Season 2020 + s2020.e080701 - Mock Entry 20-3-thumb.jpg + s2020.e080701 - Mock Entry 20-3.info.json + s2020.e080701 - Mock Entry 20-3.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-07 + episode_id: 80701 + genre: ytdl-sub + show: subscription_test + synopsis: + https://20-3.com + + The Description + title: 2020-08-07 - Mock Entry 20-3 + year: 2020 + s2020.e080801 - Mock Entry 20-2-thumb.jpg + s2020.e080801 - Mock Entry 20-2.info.json + s2020.e080801 - Mock Entry 20-2.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 80801 + genre: ytdl-sub + show: subscription_test + synopsis: + https://20-2.com + + The Description + title: 2020-08-08 - Mock Entry 20-2 + year: 2020 + s2020.e080802 - Mock Entry 20-1-thumb.jpg + s2020.e080802 - Mock Entry 20-1.info.json + s2020.e080802 - Mock Entry 20-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2020-08-08 + episode_id: 80802 + genre: ytdl-sub + show: subscription_test + synopsis: + https://20-1.com + + The Description + title: 2020-08-08 - Mock Entry 20-1 + year: 2020 +{output_directory}/Season 2021 + s2021.e080801 - Mock Entry 21-1-thumb.jpg + s2021.e080801 - Mock Entry 21-1.info.json + s2021.e080801 - Mock Entry 21-1.mp4 + Video Tags: + contentRating: TV-14 + date: 2021-08-08 + episode_id: 80801 + genre: ytdl-sub + show: subscription_test + synopsis: + https://21-1.com + + The Description + title: 2021-08-08 - Mock Entry 21-1 + year: 2021 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/plugins/thumbnail/embedded_square_thumbnail.txt b/tests/resources/transaction_log_summaries/plugins/thumbnail/embedded_square_thumbnail.txt new file mode 100644 index 00000000..8859266c --- /dev/null +++ b/tests/resources/transaction_log_summaries/plugins/thumbnail/embedded_square_thumbnail.txt @@ -0,0 +1,85 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/JMC + Mock Entry 20-1.info.json + Mock Entry 20-1.jpg + Mock Entry 20-1.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + year: 2020 + Embedded thumbnail + Square thumbnail + Mock Entry 20-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + Mock Entry 20-2.info.json + Mock Entry 20-2.jpg + Mock Entry 20-2.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + year: 2020 + Embedded thumbnail + Square thumbnail + Mock Entry 20-2.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + Mock Entry 20-3.info.json + Mock Entry 20-3.jpg + Mock Entry 20-3.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + year: 2020 + Embedded thumbnail + Square thumbnail + Mock Entry 20-3.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + Mock Entry 21-1.info.json + Mock Entry 21-1.jpg + Mock Entry 21-1.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 + year: 2021 + Embedded thumbnail + Square thumbnail + Mock Entry 21-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/plugins/thumbnail/square_thumbnail.txt b/tests/resources/transaction_log_summaries/plugins/thumbnail/square_thumbnail.txt new file mode 100644 index 00000000..538acd06 --- /dev/null +++ b/tests/resources/transaction_log_summaries/plugins/thumbnail/square_thumbnail.txt @@ -0,0 +1,81 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/JMC + Mock Entry 20-1.info.json + Mock Entry 20-1.jpg + Mock Entry 20-1.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + year: 2020 + Square thumbnail + Mock Entry 20-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + Mock Entry 20-2.info.json + Mock Entry 20-2.jpg + Mock Entry 20-2.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + year: 2020 + Square thumbnail + Mock Entry 20-2.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + Mock Entry 20-3.info.json + Mock Entry 20-3.jpg + Mock Entry 20-3.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + year: 2020 + Square thumbnail + Mock Entry 20-3.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + Mock Entry 21-1.info.json + Mock Entry 21-1.jpg + Mock Entry 21-1.mp4 + Video Tags: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 + year: 2021 + Square thumbnail + Mock Entry 21-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_False.txt b/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_False.txt new file mode 100644 index 00000000..52e2c27d --- /dev/null +++ b/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_False.txt @@ -0,0 +1,77 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/subscription_test + Mock Entry 20-1.info.json + Mock Entry 20-1.jpg + Mock Entry 20-1.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + year: 2020 + Mock Entry 20-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + Mock Entry 20-2.info.json + Mock Entry 20-2.jpg + Mock Entry 20-2.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + year: 2020 + Mock Entry 20-2.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + Mock Entry 20-3.info.json + Mock Entry 20-3.jpg + Mock Entry 20-3.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + year: 2020 + Mock Entry 20-3.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + Mock Entry 21-1.info.json + Mock Entry 21-1.jpg + Mock Entry 21-1.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 + year: 2021 + Mock Entry 21-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_True.txt b/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_True.txt new file mode 100644 index 00000000..49e609e1 --- /dev/null +++ b/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos Categorized/multi_url_True.txt @@ -0,0 +1,95 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/subscription_test + Custom Title.info.json + Custom Title.jpg + Custom Title.mp4 + Video Tags: + album: Concerts + artist: subscription_test + genre: ytdl-sub + premiered: 2021-08-08 + title: Custom Title + year: 2021 + Custom Title.nfo + NFO tags: + musicvideo: + album: Concerts + artist: subscription_test + genre: ytdl-sub + premiered: 2021-08-08 + title: Custom Title + Mock Entry 20-4.info.json + Mock Entry 20-4.jpg + Mock Entry 20-4.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-06 + title: Mock Entry 20-4 + year: 2020 + Mock Entry 20-4.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-06 + title: Mock Entry 20-4 + Mock Entry 20-5.info.json + Mock Entry 20-5.jpg + Mock Entry 20-5.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-07-06 + title: Mock Entry 20-5 + year: 2020 + Mock Entry 20-5.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-07-06 + title: Mock Entry 20-5 + Mock Entry 20-6.info.json + Mock Entry 20-6.jpg + Mock Entry 20-6.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-07-06 + title: Mock Entry 20-6 + year: 2020 + Mock Entry 20-6.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-07-06 + title: Mock Entry 20-6 + Mock Entry 20-7.info.json + Mock Entry 20-7.jpg + Mock Entry 20-7.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-06-06 + title: Mock Entry 20-7 + year: 2020 + Mock Entry 20-7.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-06-06 + title: Mock Entry 20-7 \ No newline at end of file diff --git a/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos.txt b/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos.txt new file mode 100644 index 00000000..52e2c27d --- /dev/null +++ b/tests/resources/transaction_log_summaries/unit/music_videos/Emby Music Videos.txt @@ -0,0 +1,77 @@ +Files created: +---------------------------------------- +{output_directory} + .ytdl-sub-subscription_test-download-archive.json +{output_directory}/subscription_test + Mock Entry 20-1.info.json + Mock Entry 20-1.jpg + Mock Entry 20-1.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + year: 2020 + Mock Entry 20-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-1 + Mock Entry 20-2.info.json + Mock Entry 20-2.jpg + Mock Entry 20-2.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + year: 2020 + Mock Entry 20-2.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-08 + title: Mock Entry 20-2 + Mock Entry 20-3.info.json + Mock Entry 20-3.jpg + Mock Entry 20-3.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + year: 2020 + Mock Entry 20-3.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2020-08-07 + title: Mock Entry 20-3 + Mock Entry 21-1.info.json + Mock Entry 21-1.jpg + Mock Entry 21-1.mp4 + Video Tags: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 + year: 2021 + Mock Entry 21-1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: subscription_test + genre: ytdl-sub + premiered: 2021-08-08 + title: Mock Entry 21-1 \ No newline at end of file diff --git a/tests/unit/cli/test_output_summary.py b/tests/unit/cli/test_output_summary.py index 67422532..a31c9d58 100644 --- a/tests/unit/cli/test_output_summary.py +++ b/tests/unit/cli/test_output_summary.py @@ -36,7 +36,7 @@ def test_output_summary_no_errors(): ] ) - output_summary(subscriptions=mock_subscriptions) + output_summary(subscriptions=mock_subscriptions, suppress_colors=False) def test_output_summary_one_error(): @@ -50,7 +50,7 @@ def test_output_summary_one_error(): ] ) - output_summary(subscriptions=mock_subscriptions) + output_summary(subscriptions=mock_subscriptions, suppress_colors=False) def test_output_summary_multiple_errors(): @@ -64,4 +64,4 @@ def test_output_summary_multiple_errors(): ] ) - output_summary(subscriptions=mock_subscriptions) + output_summary(subscriptions=mock_subscriptions, suppress_colors=True) diff --git a/tests/unit/config/test_config_file.py b/tests/unit/config/test_config_file.py index 3bd98d6e..ae1d2231 100644 --- a/tests/unit/config/test_config_file.py +++ b/tests/unit/config/test_config_file.py @@ -55,6 +55,7 @@ class TestConfigFilePartiallyValidatesPresets: def test_success__empty_plugins(self, plugin: str): excluded_plugins = [ "embed_thumbnail", # value is bool, not dict + "square_thumbnail", "format", # value is string, not dict "filter_include", # is list "filter_exclude", # is list @@ -76,7 +77,7 @@ class TestConfigFilePartiallyValidatesPresets: expected_error_message="Validation error in partial_preset.download.1: " "'partial_preset.download.1' contains the field 'bad_key' which is not allowed. " "Allowed fields: download_reverse, include_sibling_metadata, playlist_thumbnails, " - "source_thumbnails, url, variables, ytdl_options", + "source_thumbnails, url, variables, webpage_url, ytdl_options", ) @pytest.mark.parametrize( diff --git a/tests/unit/config/test_preset.py b/tests/unit/config/test_preset.py index cb3c0b81..a42439be 100644 --- a/tests/unit/config/test_preset.py +++ b/tests/unit/config/test_preset.py @@ -6,7 +6,6 @@ import pytest from ytdl_sub.config.preset import Preset from ytdl_sub.plugins.nfo_tags import NfoTagsOptions -from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException from ytdl_sub.utils.exceptions import ValidationException @@ -120,6 +119,26 @@ class TestPreset: == "today-2months" ) + def test_preset_preserve_mtime_option(self, config_file, youtube_video, output_options): + # Test preserve_mtime defaults to False + preset_default = Preset( + config=config_file, + name="test_default", + value={"download": youtube_video, "output_options": output_options}, + ) + assert preset_default.output_options.preserve_mtime is False + + # Test preserve_mtime can be set to True + preset_enabled = Preset( + config=config_file, + name="test_enabled", + value={ + "download": youtube_video, + "output_options": dict(output_options, **{"preserve_mtime": True}), + }, + ) + assert preset_enabled.output_options.preserve_mtime is True + @pytest.mark.parametrize( "parent_preset", ["preset_self_loop", "preset_loop_0", "preset_loop_1"] ) @@ -137,100 +156,6 @@ class TestPreset: }, ) - def test_preset_error__source_variable_does_not_exist( - self, config_file, output_options, youtube_video - ): - with pytest.raises( - StringFormattingVariableNotFoundException, - match="contains the following variables that do not exist: dne_var", - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": youtube_video, - "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, - }, - ) - - def test_preset_error__override_variable_does_not_exist( - self, config_file, output_options, youtube_video - ): - with pytest.raises( - StringFormattingVariableNotFoundException, - match="contains the following variables that do not exist: dne_var", - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": youtube_video, - "output_options": {"output_directory": "{dne_var}", "file_name": "file"}, - }, - ) - - def test_preset_error__dict_source_variable_does_not_exist( - self, config_file, output_options, youtube_video - ): - with pytest.raises( - StringFormattingVariableNotFoundException, - match="contains the following variables that do not exist: dne_var", - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": youtube_video, - "output_options": {"output_directory": "dir", "file_name": "file"}, - "nfo_tags": { - "nfo_name": "the nfo name", - "nfo_root": "the root", - "tags": {"tag_a": "{dne_var}"}, - }, - }, - ) - - def test_preset_error__dict_override_variable_does_not_exist( - self, config_file, output_options, youtube_video - ): - with pytest.raises( - StringFormattingVariableNotFoundException, - match="contains the following variables that do not exist: dne_var", - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": youtube_video, - "output_options": output_options, - "output_directory_nfo_tags": { - "nfo_name": "the nfo name", - "nfo_root": "the root", - "tags": {"tag_a": "{dne_var}"}, - }, - }, - ) - - def test_preset_error__dict_override_variable_not_static( - self, config_file, output_options, youtube_video - ): - with pytest.raises( - StringFormattingVariableNotFoundException, - match="static formatters must contain variables that " - "have no dependency to entry variables", - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": youtube_video, - "output_options": { - "output_directory": "{title}", - "file_name": "{uid}", - }, - }, - ) - def test_preset_with_multi_url__contains_empty_url(self, config_file, output_options): _ = Preset( config=config_file, @@ -392,26 +317,3 @@ class TestPreset: "overrides": {name: "ack"}, }, ) - - def test_preset_error_added_url_variable_cannot_resolve(self, config_file, output_options): - with pytest.raises( - ValidationException, - match=re.escape( - "variable the_bad_one cannot use the variables subtitles_ext because it " - "depends on other variables that are computed later in execution" - ), - ): - _ = Preset( - config=config_file, - name="test", - value={ - "download": { - "url": "youtube.com/watch?v=123abc", - "variables": {"the_bad_one": "{subtitles_ext}"}, - }, - "subtitles": { - "embed_subtitles": True, - }, - "output_options": {"output_directory": "dir", "file_name": "acjk"}, - }, - ) diff --git a/tests/unit/config/test_subscription.py b/tests/unit/config/test_subscription.py index 8fbfac15..719ac9f4 100644 --- a/tests/unit/config/test_subscription.py +++ b/tests/unit/config/test_subscription.py @@ -1,4 +1,3 @@ -import json import re from contextlib import contextmanager from pathlib import Path @@ -6,11 +5,15 @@ from typing import Dict from unittest.mock import patch import pytest +import yaml from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.config.validators.variable_validation import ResolutionLevel +from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.plugins.nfo_tags import NfoTagsOptions from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.script import ScriptUtils @contextmanager @@ -472,23 +475,6 @@ def test_advanced_tv_show_subscriptions( overrides = subs[5].overrides assert overrides.script.get("subscription_name").native == "Gardening with Ciscoe" - assert ( - overrides.apply_formatter(overrides.dict["url"]) - == "https://www.youtube.com/@gardeningwithciscoe4430" - ) - assert ( - overrides.apply_formatter(overrides.dict["url2"]) - == "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_" - ) - - assert overrides.apply_formatter(overrides.dict["subscription_array"]) == json.dumps( - [ - "https://www.youtube.com/@gardeningwithciscoe4430", - "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_", - "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn", - "https://www.youtube.com/watch?v=2vq-vPubS5I", - ] - ) def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path): @@ -524,7 +510,7 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc ) assert jackson.get("subscription_indent_1").native == "Pop" assert ( - jackson.get("url").native + jackson.get("urls").native[0] == "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E" ) @@ -532,17 +518,7 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc gnr = subs[3].overrides.script assert gnr.get("subscription_name").native == "Guns N' Roses" - assert ( - gnr.get("url").native - == "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ" - ) + gnr_urls = gnr.get("urls").native + assert gnr_urls[0] == "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ" assert gnr.get("subscription_indent_1").native == "Rock" - assert gnr.get("url2").native == "https://www.youtube.com/watch?v=OldpIhHPsbs" - - -def test_default_docker_config_and_subscriptions(): - default_config = ConfigFile.from_file_path("docker/root/defaults/config.yaml") - default_subs = Subscription.from_file_path( - config=default_config, subscription_path=Path("docker/root/defaults/subscriptions.yaml") - ) - assert len(default_subs) == 15 + assert gnr_urls[1] == "https://www.youtube.com/watch?v=OldpIhHPsbs" diff --git a/tests/unit/config/test_subscription_resolution.py b/tests/unit/config/test_subscription_resolution.py new file mode 100644 index 00000000..bfa04d16 --- /dev/null +++ b/tests/unit/config/test_subscription_resolution.py @@ -0,0 +1,92 @@ +from pathlib import Path + +import pytest +import yaml +from resources import expected_json + +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.file_path import FilePathTruncater + + +def _ensure_resolved_yaml( + sub: Subscription, output_directory: str, preset_type: str, resolution_level: int +) -> None: + output_yaml = sub.resolved_yaml(resolution_level=resolution_level) + out = yaml.safe_load(output_yaml) + + expected_out_filename = ( + f"{preset_type}/inspect_sub_{ResolutionLevel.name_of(resolution_level)}.json" + ) + expected_out = expected_json(out, expected_out_filename) + + if resolution_level > ResolutionLevel.ORIGINAL: + output_path = Path(output_directory) + if "tv_show_directory" in expected_out["overrides"]: + output_path = output_path / sub.name + + expected_out["output_options"]["output_directory"] = FilePathTruncater.to_native_filepath( + str(output_path) + ) + + if "tv_show_directory" in expected_out["overrides"]: + expected_out["overrides"]["tv_show_directory"] = output_directory + if "music_directory" in expected_out["overrides"]: + expected_out["overrides"]["music_directory"] = output_directory + if "music_video_directory" in expected_out["overrides"]: + expected_out["overrides"]["music_video_directory"] = output_directory + + assert out == expected_out + + +@pytest.mark.parametrize("resolution_level", ResolutionLevel.all()) +class TestResolution: + + def test_resolution_tv_show( + self, + resolution_level: int, + default_config: ConfigFile, + tv_show_subscriptions_path: Path, + output_directory: str, + ): + _ensure_resolved_yaml( + sub=Subscription.from_file_path( + config=default_config, subscription_path=tv_show_subscriptions_path + )[0], + output_directory=output_directory, + preset_type="tv_show", + resolution_level=resolution_level, + ) + + def test_resolution_music( + self, + resolution_level: int, + default_config: ConfigFile, + music_subscriptions_path: Path, + output_directory: str, + ): + _ensure_resolved_yaml( + sub=Subscription.from_file_path( + config=default_config, subscription_path=music_subscriptions_path + )[0], + output_directory=output_directory, + preset_type="music", + resolution_level=resolution_level, + ) + + def test_resolution_music_video( + self, + resolution_level: int, + default_config: ConfigFile, + music_video_subscription_path: Path, + output_directory: str, + ): + _ensure_resolved_yaml( + sub=Subscription.from_file_path( + config=default_config, subscription_path=music_video_subscription_path + )[0], + output_directory=output_directory, + preset_type="music_video", + resolution_level=resolution_level, + ) diff --git a/tests/unit/config/test_subscription_validation.py b/tests/unit/config/test_subscription_validation.py new file mode 100644 index 00000000..3a6fd13d --- /dev/null +++ b/tests/unit/config/test_subscription_validation.py @@ -0,0 +1,172 @@ +import re + +import pytest + +from ytdl_sub.config.preset import Preset +from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException +from ytdl_sub.utils.exceptions import ValidationException + + +class TestSubscriptionValidation: + def test_preset_error__source_variable_does_not_exist( + self, config_file, output_options, youtube_video + ): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="contains the following variables that do not exist: dne_var", + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": youtube_video, + "output_options": {"output_directory": "dir", "file_name": "{dne_var}"}, + }, + ), + config=config_file, + ) + + def test_preset_error__override_variable_does_not_exist( + self, config_file, output_options, youtube_video + ): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="contains the following variables that do not exist: dne_var", + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": youtube_video, + "output_options": {"output_directory": "{dne_var}", "file_name": "file"}, + }, + ), + config=config_file, + ) + + def test_preset_error__dict_source_variable_does_not_exist( + self, config_file, output_options, youtube_video + ): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="contains the following variables that do not exist: dne_var", + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": youtube_video, + "output_options": {"output_directory": "dir", "file_name": "file"}, + "nfo_tags": { + "nfo_name": "the nfo name", + "nfo_root": "the root", + "tags": {"tag_a": "{dne_var}"}, + }, + }, + ), + config=config_file, + ) + + def test_preset_error__dict_override_variable_does_not_exist( + self, config_file, output_options, youtube_video + ): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="contains the following variables that do not exist: dne_var", + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": youtube_video, + "output_options": output_options, + "output_directory_nfo_tags": { + "nfo_name": "the nfo name", + "nfo_root": "the root", + "tags": {"tag_a": "{dne_var}"}, + }, + }, + ), + config=config_file, + ) + + def test_preset_error__dict_override_variable_not_static( + self, config_file, output_options, youtube_video + ): + with pytest.raises( + StringFormattingVariableNotFoundException, + match="static formatters must contain variables that " + "have no dependency to entry variables", + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": youtube_video, + "output_options": { + "output_directory": "{title}", + "file_name": "{uid}", + }, + }, + ), + config=config_file, + ) + + def test_preset_error_added_url_variable_cannot_resolve(self, config_file, output_options): + with pytest.raises( + ValidationException, + match=re.escape( + "variable the_bad_one cannot use the variables subtitles_ext because it " + "depends on other variables that are computed later in execution" + ), + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": { + "url": "youtube.com/watch?v=123abc", + "variables": {"the_bad_one": "{subtitles_ext}"}, + }, + "subtitles": { + "embed_subtitles": True, + }, + "output_options": {"output_directory": "dir", "file_name": "acjk"}, + }, + ), + config=config_file, + ) + + def test_preset_error_override_name_conflicts_with_plugin(self, config_file, output_options): + with pytest.raises( + ValidationException, + match=re.escape( + "Override variable with name throttle_protection 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." + ), + ): + _ = Subscription.from_preset( + preset=Preset( + config=config_file, + name="test", + value={ + "download": { + "url": "youtube.com/watch?v=123abc", + }, + "subtitles": { + "embed_subtitles": True, + }, + "output_options": {"output_directory": "dir", "file_name": "acjk"}, + "overrides": {"throttle_protection": "nope"}, + }, + ), + config=config_file, + ) diff --git a/tests/unit/entries/conftest.py b/tests/unit/entries/conftest.py index 9421d35e..e3302010 100644 --- a/tests/unit/entries/conftest.py +++ b/tests/unit/entries/conftest.py @@ -65,8 +65,8 @@ def mock_entry_to_dict( "epoch": 1596878400, "epoch_date": "20200808", "epoch_hour": "09", - "title": "entry {title}", - "title_sanitized": "entry {title}", + "title": "entry {title}", + "title_sanitized": "entry {title}", "ext": ext, "description": "", "creator": "abc123", @@ -113,8 +113,8 @@ def mock_entry_to_dict( "playlist_count": 1, "playlist_max_upload_year": 2021, "playlist_max_upload_year_truncated": 21, - "playlist_title": "entry {title}", - "playlist_title_sanitized": "entry {title}", + "playlist_title": "entry {title}", + "playlist_title_sanitized": "entry {title}", "playlist_description": "", "playlist_webpage_url": "https://yourname.here", "playlist_uid": "abc123", @@ -126,15 +126,15 @@ def mock_entry_to_dict( "source_description": "", "source_index": 1, "source_index_padded": "01", - "source_title": "entry {title}", - "source_title_sanitized": "entry {title}", + "source_title": "entry {title}", + "source_title_sanitized": "entry {title}", "source_webpage_url": "https://yourname.here", "source_uid": "abc123", "source_uploader": "abc123", "source_uploader_id": "abc123", "source_uploader_url": "https://yourname.here", "uid_sanitized_plex": "abc123", - "title_sanitized_plex": "entry {title}", + "title_sanitized_plex": "entry {title}", "release_date": upload_date, "release_date_standardized": "2021-01-12", "release_year": 2021, diff --git a/tests/unit/entries/test_entry.py b/tests/unit/entries/test_entry.py index 4797092d..07b7a5a3 100644 --- a/tests/unit/entries/test_entry.py +++ b/tests/unit/entries/test_entry.py @@ -58,3 +58,25 @@ class TestEntry(object): assert entry.get(v.upload_day_of_year_reversed, int) == day_year_rev assert entry.get(v.upload_day_of_year_padded, str) == day_year_pad assert entry.get(v.upload_day_of_year_reversed_padded, str) == day_year_rev_pad + + def test_entry_required_variables(self): + entry_dict = { + v.uid.metadata_key: "0", + v.extractor.metadata_key: "test", + v.extractor_key.metadata_key: "test", + v.epoch.metadata_key: 123, + v.webpage_url.metadata_key: "youtube.com/test_url", + v.ext.metadata_key: "mp4", + } + + assert set(entry_dict.keys()) == {_.metadata_key for _ in v.required_entry_variables()} + + # Ensure adding variables works + entry = Entry( + entry_dict=entry_dict, + working_directory=".", + ).initialize_script() + + entry.add({v.channel: "can add"}) + + assert entry.get(v.channel, str) == "can add" diff --git a/tests/unit/main/test_main.py b/tests/unit/main/test_main.py index b9a6b07d..32ff6d57 100644 --- a/tests/unit/main/test_main.py +++ b/tests/unit/main/test_main.py @@ -244,7 +244,9 @@ def test_no_positional_arg_command(mock_sys_exit, tv_show_config_path): main() assert mock_error.call_count == 1 - assert mock_error.call_args.args[0] == "Must provide one of the commands: sub, dl, view" + assert mock_error.call_args.args[0] == ( + "Must provide one of the commands: sub, dl, view, cli-to-sub" + ) def test_bad_config_path(mock_sys_exit): @@ -264,3 +266,18 @@ def test_bad_config_path(mock_sys_exit): "The config file 'does_not_exist.yaml' could not be found. " "Did you set --config correctly?" ) + + +def test_cli_to_sub(mock_sys_exit, capsys): + with ( + mock_sys_exit(expected_exit_code=0), + patch.object( + sys, + "argv", + ["ytdl-sub", "cli-to-sub", "--mark-watched", "--mtime"], + ), + ): + main() + + captured = capsys.readouterr() + assert captured.out == ("ytdl_options:\n mark_watched: true\n updatetime: true\n\n") diff --git a/tests/unit/plugins/__init__.py b/tests/unit/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/plugins/test_date_range.py b/tests/unit/plugins/test_date_range.py new file mode 100644 index 00000000..a024d8e2 --- /dev/null +++ b/tests/unit/plugins/test_date_range.py @@ -0,0 +1,108 @@ +import re +from typing import Any +from typing import Dict + +import pytest +from conftest import get_match_filters + +from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import ValidationException + + +@pytest.fixture +def preset_dict(output_directory) -> Dict[str, Any]: + return { + "download": "https://your.name.here", + "output_options": {"output_directory": output_directory, "file_name": "will_error.mp4"}, + } + + +class TestDateRange: + @pytest.mark.parametrize("date_range_type", ["upload_date", "release_date"]) + def test_date_range_type( + self, + default_config: ConfigFile, + preset_dict: Dict[str, Any], + output_directory: str, + date_range_type: str, + ): + preset_dict["date_range"] = { + "before": "20250530", + "after": "20250510", + "type": date_range_type, + } + sub = Subscription.from_dict( + config=default_config, + preset_name="test_date_range", + preset_dict=preset_dict, + ) + + metadata_filter, metadata_breaking_filter = get_match_filters( + subscription=sub, dry_run=False, download_filters=False + ) + assert metadata_filter == [ + f"!is_live & !is_upcoming & !post_live & {date_range_type} < 20250530" + ] + assert metadata_breaking_filter == [f"{date_range_type} >= 20250510"] + + download_filter, download_breaking_filter = get_match_filters( + subscription=sub, dry_run=False, download_filters=True + ) + assert not download_filter + assert not download_breaking_filter + + def test_date_range_breaks_false( + self, + default_config: ConfigFile, + preset_dict: Dict[str, Any], + output_directory: str, + ): + preset_dict["date_range"] = { + "before": "20250530", + "after": "20250510", + "breaks": False, + } + sub = Subscription.from_dict( + config=default_config, + preset_name="test_date_range", + preset_dict=preset_dict, + ) + + metadata_filter, metadata_breaking_filter = get_match_filters( + subscription=sub, dry_run=False, download_filters=False + ) + assert metadata_filter == [ + f"!is_live & !is_upcoming & !post_live & upload_date < 20250530 & upload_date >= 20250510" + ] + assert not metadata_breaking_filter + + download_filter, download_breaking_filter = get_match_filters( + subscription=sub, dry_run=False, download_filters=True + ) + assert not download_filter + assert not download_breaking_filter + + def test_date_range_invalid_type( + self, + default_config: ConfigFile, + preset_dict: Dict[str, Any], + output_directory: str, + ): + preset_dict["date_range"] = { + "before": "20250530", + "after": "20250510", + "type": "no", + } + + error_msg = ( + "Validation error in test_date_range.date_range.type: " + "Must be one of the following values: release_date, upload_date" + ) + + with pytest.raises(ValidationException, match=re.escape(error_msg)): + Subscription.from_dict( + config=default_config, + preset_name="test_date_range", + preset_dict=preset_dict, + ).download(dry_run=False) diff --git a/tests/unit/plugins/test_ytdl_options.py b/tests/unit/plugins/test_ytdl_options.py new file mode 100644 index 00000000..205e1750 --- /dev/null +++ b/tests/unit/plugins/test_ytdl_options.py @@ -0,0 +1,111 @@ +import re +from typing import Any +from typing import Dict + +import pytest +import yt_dlp +from conftest import assert_logs + +from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.downloaders.ytdlp import YTDLP +from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.ffmpeg import FFMPEG +from ytdl_sub.utils.file_path import FilePathTruncater + + +@pytest.fixture +def preset_dict(output_directory) -> Dict[str, Any]: + return { + "download": "https://your.name.here", + "output_options": {"output_directory": output_directory, "file_name": "will_error.mp4"}, + } + + +class TestYtdlOptions: + + def test_ytdl_options_are_strings( + self, + default_config: ConfigFile, + preset_dict: Dict[str, Any], + working_directory, + ): + expected_ytdl_options = { + "ignoreerrors": True, + "outtmpl": FilePathTruncater.to_native_filepath( + f"{working_directory}/test_ytdl_options/%(id)S.%(ext)s" + ), + "writethumbnail": False, + "ffmpeg_location": FFMPEG.ffmpeg_path(), + "match_filter": yt_dlp.utils.match_filter_func( + ["!is_live & !is_upcoming & !post_live"], [] + ), + "skip_download": True, + "writeinfojson": True, + "extract_flat": "discard", + } + + with ( + assert_logs( + logger=YTDLP.logger, + expected_message=f"ytdl_options: {str(expected_ytdl_options)}", + log_level="debug", + expected_occurrences=1, + ), + ): + _ = Subscription.from_dict( + config=default_config, + preset_name="test_ytdl_options", + preset_dict=preset_dict, + ).download(dry_run=True) + + def test_cookiefile_does_not_exist( + self, + default_config: ConfigFile, + preset_dict: Dict[str, Any], + ): + preset_dict["ytdl_options"] = { + "cookiefile": "/path/to/nowhere", + } + + error_msg = "Specified cookiefile /path/to/nowhere but it does not exist as a file." + + with pytest.raises(ValidationException, match=re.escape(error_msg)): + Subscription.from_dict( + config=default_config, + preset_name="test_ytdl_options", + preset_dict=preset_dict, + ).download(dry_run=False) + + def test_ytdl_option_types_preserved( + self, + default_config: ConfigFile, + output_directory: str, + ): + preset_dict = { + "download": "https://your.name.here", + "output_options": {"output_directory": output_directory, "file_name": "will_error.mp4"}, + "ytdl_options": { + "break_on_existing": True, + "js_runtimes": {"deno": {"path": "/usr/local/bin/{dnope}"}}, + "string_path": "verify overrides: {test_string}", + "list_test": ["hmmm"], + }, + "overrides": {"test_string": "hi", "dnope": "deno"}, + } + + sub = Subscription.from_dict( + config=default_config, + preset_name="test_ytdl_options", + preset_dict=preset_dict, + ) + + out = sub.ytdl_options.to_native_dict(sub.overrides) + expected = { + "break_on_existing": True, + "js_runtimes": {"deno": {"path": "/usr/local/bin/deno"}}, + "string_path": "verify overrides: hi", + "list_test": ["hmmm"], + } + + assert out == expected diff --git a/tests/unit/prebuilt_presets/__init__.py b/tests/unit/prebuilt_presets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/prebuilt_presets/test_tv_show_by_date.py b/tests/unit/prebuilt_presets/test_tv_show_by_date.py new file mode 100644 index 00000000..2b21e050 --- /dev/null +++ b/tests/unit/prebuilt_presets/test_tv_show_by_date.py @@ -0,0 +1,92 @@ +import re + +import pytest +import yaml + +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError +from ytdl_sub.subscriptions.subscription import Subscription + + +class TestTvShowByDatePreset: + + def test_s01_error_thrown(self, default_config): + with pytest.raises( + UserThrownRuntimeError, + match=re.escape( + "Provided `s01_url` or `s01_name` variable to TV Show by Date preset when it " + "expects `url`. Perhaps you meant to use the `TV Show Collection` preset?" + ), + ): + _ = Subscription.from_dict( + config=default_config, + preset_name="test", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "s01_url": "test"}, + }, + ) + + def test_backward_compatibility_single_download(self, default_config): + a = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "url": "test_1"}, + }, + ).resolved_yaml() + ) + + b = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "subscription_value": "test_1"}, + }, + ).resolved_yaml() + ) + + assert a["download"] == b["download"] + + def test_backward_compatibility_multi_download(self, default_config): + a = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "url": "test_1", "url2": "test_2"}, + }, + ).resolved_yaml() + ) + + b = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": { + "tv_show_directory": "abc", + "subscription_array": ["test_1", "test_2"], + }, + }, + ).resolved_yaml() + ) + + c = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "urls": ["test_1", "test_2"]}, + }, + ).resolved_yaml() + ) + + assert a["download"] == b["download"] + assert a["download"] == c["download"] diff --git a/tests/unit/prebuilt_presets/test_tv_show_collection.py b/tests/unit/prebuilt_presets/test_tv_show_collection.py new file mode 100644 index 00000000..0f7e9c5e --- /dev/null +++ b/tests/unit/prebuilt_presets/test_tv_show_collection.py @@ -0,0 +1,113 @@ +import re + +import pytest + +from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError +from ytdl_sub.subscriptions.subscription import Subscription + + +class TestTvShowCollectionPreset: + + def test_url_error_thrown(self, default_config): + with pytest.raises( + UserThrownRuntimeError, + match=re.escape( + "Provided `url` to TV Show Collection preset when it expects `s01_url`. " + "Perhaps you meant to use the `TV Show by Date` preset?" + ), + ): + _ = Subscription.from_dict( + config=default_config, + preset_name="test", + preset_dict={ + "preset": "Jellyfin TV Show Collection", + "overrides": {"tv_show_directory": "abc", "url": "test"}, + }, + ) + + def test_multi_url(self, default_config): + num_seasons = 40 # excluding season 0 + num_urls_per_season = 11 + + overrides = { + "tv_show_directory": "abc", + } + for season_num in range(0, num_seasons + 1): + overrides[f"s{season_num:02d}_name"] = f"The Season {season_num}" + overrides[f"s{season_num:02d}_url"] = [ + f"youtube.com/playlist?url_{season_num}_{i}" for i in range(num_urls_per_season) + ] + + sub = Subscription.from_dict( + config=default_config, + preset_name="test", + preset_dict={"preset": "Jellyfin TV Show Collection", "overrides": overrides}, + ) + + assert len(sub.downloader_options.urls.list) == (num_seasons + 1) * 3 + url_list = sub.downloader_options.urls.list + itr = 0 + + # loop twice for bilateral + for i in range(2): + for season_num in range(1, num_seasons + 2): + # Season 0 is placed last, adjust here + if season_num == num_seasons + 1: + season_num = 0 + + # is_bilateral + if i == 0: + url = sub.overrides.apply_formatter( + url_list[itr].url, + function_overrides={ + # mock so bilateral url gets enabled + "subscription_has_download_archive": "True" + }, + expected_type=list, + ) + assert url == [ + f"youtube.com/playlist?url_{season_num}_{i}" + for i in range(num_urls_per_season) + ] + variables = url_list[itr].variables.dict + assert ( + sub.overrides.apply_formatter(variables["collection_season_number"]) + == f"{season_num}" + ) + assert ( + sub.overrides.apply_formatter(variables["collection_season_name"]) + == f"The Season {season_num}" + ) + itr += 1 + # not bilateral + else: + for j in range(2): + url = sub.overrides.apply_formatter( + url_list[itr + j].url, + function_overrides={ + # mock so bilateral url gets enabled + "subscription_has_download_archive": "True" + }, + expected_type=list, + ) + + # First instance is the first url to get thumbnails + if j == 0: + assert url == [f"youtube.com/playlist?url_{season_num}_0"] + # Next one contains remaining urls + else: + assert url == [ + f"youtube.com/playlist?url_{season_num}_{i}" + for i in range(1, num_urls_per_season) + ] + + variables = url_list[itr].variables.dict + assert ( + sub.overrides.apply_formatter(variables["collection_season_number"]) + == f"{season_num}" + ) + assert ( + sub.overrides.apply_formatter(variables["collection_season_name"]) + == f"The Season {season_num}" + ) + itr += 2 diff --git a/tests/unit/script/functions/test_conditional_functions.py b/tests/unit/script/functions/test_conditional_functions.py index eff73214..d2078a80 100644 --- a/tests/unit/script/functions/test_conditional_functions.py +++ b/tests/unit/script/functions/test_conditional_functions.py @@ -3,6 +3,9 @@ import re import pytest from unit.script.conftest import single_variable_output +from ytdl_sub.script.script import Script +from ytdl_sub.script.types.syntax_tree import SyntaxTree +from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import FunctionRuntimeException @@ -135,3 +138,29 @@ class TestConditionalFunction: ): output = single_variable_output(function_str) assert output == expected_output + + def test_if_partial_resolve(self): + assert ( + Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%if( true, aa, bb )}", + } + ) + .resolve_partial(unresolvable={"bb"}) + .get("cc") + .native + == "a" + ) + + def test_if_partial_resolve_unresolved(self): + assert Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%if( false, aa, bb )}", + } + ).resolve_partial(unresolvable={"bb"}).definition_of("cc") == SyntaxTree( + ast=[Variable("bb")] + ) diff --git a/tests/unit/script/functions/test_numeric_functions.py b/tests/unit/script/functions/test_numeric_functions.py index c41f4c59..b05dbb70 100644 --- a/tests/unit/script/functions/test_numeric_functions.py +++ b/tests/unit/script/functions/test_numeric_functions.py @@ -76,3 +76,15 @@ class TestNumericFunctions: def test_pow(self, values: str, expected_output: float): output = single_variable_output(f"{{ %pow({values}) }}") assert output == expected_output + + @pytest.mark.parametrize( + "values, expected_output", + [ + ("5", [0, 1, 2, 3, 4]), + ("5, 1", [1, 2, 3, 4]), + ("5, 1, 2", [1, 3]), + ], + ) + def test_range(self, values: str, expected_output: float): + output = single_variable_output(f"{{ %range({values}) }}") + assert output == expected_output diff --git a/tests/unit/script/functions/test_print_functions.py b/tests/unit/script/functions/test_print_functions.py new file mode 100644 index 00000000..56500d16 --- /dev/null +++ b/tests/unit/script/functions/test_print_functions.py @@ -0,0 +1,61 @@ +import logging +from typing import Any +from typing import Optional +from unittest.mock import patch + +import pytest +from unit.script.conftest import single_variable_output + + +class TestPrintFunctions: + @pytest.mark.parametrize( + "function_str, expected_print, expected_output", + [ + # print + ("{%print('hi mom', True)}", "hi mom", True), + ("{%print('this is great', [1, 2, 3])}", "this is great", [1, 2, 3]), + ("{%print([1, 2], [3, 4])}", "[1, 2]", [3, 4]), + ("{%print('', True)}", None, True), + # print_if_true + ("{%print_if_true('hi mom', True)}", "hi mom", True), + ("{%print_if_true('hi mom', False)}", None, False), + ("{%print_if_true('', True)}", None, True), + # print_if_false + ("{%print_if_false('hi mom', True)}", None, True), + ("{%print_if_false('hi mom', False)}", "hi mom", False), + ("{%print_if_false('', True)}", None, True), + ], + ) + def test_print_functions( + self, function_str: str, expected_print: Optional[str], expected_output: Any + ): + with patch.object(logging.Logger, "info") as mock_logger: + output = single_variable_output(function_str) + assert output == expected_output + + if expected_print is not None: + assert mock_logger.call_count == 1 + assert mock_logger.call_args.args[0] == expected_print + else: + assert mock_logger.call_count == 0 + + @pytest.mark.parametrize( + "function_str, expected_print, expected_output", + [ + # print + ("{%print('hi mom', True, LEVEL)}", "hi mom", True), + # print_if_true + ("{%print_if_true('hi mom', True, LEVEL)}", "hi mom", True), + # print_if_false + ("{%print_if_false('hi mom', False, LEVEL)}", "hi mom", False), + ], + ) + @pytest.mark.parametrize("level", [-1, 0, 1, 2]) + def test_levels(self, function_str: str, expected_print: str, expected_output: str, level: int): + level_mapping = {-1: "debug", 0: "info", 1: "warning", 2: "error"} + with patch.object(logging.Logger, level_mapping[level]) as mock_logger: + output = single_variable_output(function_str.replace("LEVEL", str(level))) + assert output == expected_output + + assert mock_logger.call_count == 1 + assert mock_logger.call_args.args[0] == expected_print diff --git a/tests/unit/script/functions/test_string_functions.py b/tests/unit/script/functions/test_string_functions.py index f5c85741..bc5bfc32 100644 --- a/tests/unit/script/functions/test_string_functions.py +++ b/tests/unit/script/functions/test_string_functions.py @@ -152,3 +152,23 @@ class TestNumericFunctions: output = single_variable_output(f"{{%split('{input_string}', {split})}}") assert output == expected_output + + @pytest.mark.parametrize( + "value, expected_output", + [ + ("['a', 'b', 'c']", "a, b, c"), + ("['nope', [], {}]", "nope, [], {}"), + ("['a', 1, 3.14, True]", "a, 1, 3.14, true"), + ], + ) + def test_join(self, value, expected_output): + output = single_variable_output(f"{{%join(', ', {value})}}") + assert output == expected_output + + @pytest.mark.parametrize( + "value, expected_output", + [(" delete outer ", "delete outer"), (" delete me\n\n", "delete me")], + ) + def test_strip(self, value, expected_output): + output = single_variable_output(f"{{%strip('{value}')}}") + assert output == expected_output diff --git a/tests/unit/script/test_parser.py b/tests/unit/script/test_parser.py index b40e13ca..c6694562 100644 --- a/tests/unit/script/test_parser.py +++ b/tests/unit/script/test_parser.py @@ -16,6 +16,7 @@ from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Lambda from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import InvalidSyntaxException @@ -24,7 +25,7 @@ from ytdl_sub.script.utils.exceptions import InvalidSyntaxException class TestParser: def test_simple(self): parsed = parse("hello world") - assert parsed == SyntaxTree([String(value="hello world")]) + assert parsed == ResolvedSyntaxTree([String(value="hello world")]) assert parsed.variables == set() def test_single_function_one_arg(self): diff --git a/tests/unit/script/types/test_array.py b/tests/unit/script/types/test_array.py index f816b1bd..312ec325 100644 --- a/tests/unit/script/types/test_array.py +++ b/tests/unit/script/types/test_array.py @@ -10,6 +10,8 @@ from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.syntax_tree import SyntaxTree +from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import InvalidSyntaxException @@ -117,3 +119,29 @@ class TestArray: ).resolve() == ScriptOutput( {"aa": String("a"), "bb": String("b"), "cc": String('return ["a", "b"]')} ) + + def test_partial_resolve(self): + assert ( + Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%array_at( [aa, bb], 0 )}", + } + ) + .resolve_partial(unresolvable={"bb"}) + .get("cc") + .native + == "a" + ) + + def test_partial_resolve_unresolved(self): + assert Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%array_at( [aa, bb], 1 )}", + } + ).resolve_partial(unresolvable={"bb"}).definition_of("cc") == SyntaxTree( + ast=[Variable("bb")] + ) diff --git a/tests/unit/script/types/test_custom_function.py b/tests/unit/script/types/test_custom_function.py index 61096cc3..17919cc0 100644 --- a/tests/unit/script/types/test_custom_function.py +++ b/tests/unit/script/types/test_custom_function.py @@ -5,7 +5,9 @@ import pytest from ytdl_sub.script.parser import CUSTOM_FUNCTION_ARGUMENTS_ONLY_ARGS from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput +from ytdl_sub.script.types.function import CustomFunction from ytdl_sub.script.types.resolvable import Integer +from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArgumentName @@ -251,3 +253,30 @@ class TestCustomFunction: "output": "{%mul(%func1(1), 1)}", } ) + + def test_partial_resolve_custom_functions_any_order_via_init(self): + assert ( + Script( + { + "%custom_cubed": "{%mul(%custom_square($0),$0)}", + "%custom_square": "{%mul($0, $0)}", + "output": "{%custom_cubed(3)}", + } + ) + .resolve_partial() + .get("output") + .native + == 27 + ) + + def test_partial_resolve_unresolved(self): + assert Script( + { + "aa": "nope", + "%custom_cubed": "{%mul(%custom_square($0),$0)}", + "%custom_square": "{%mul($0, aa)}", + "output": "{%custom_cubed(3)}", + } + ).resolve_partial(unresolvable={"aa"}).definition_of("output") == SyntaxTree( + ast=[CustomFunction(name="custom_cubed", args=[Integer(3)])] + ) diff --git a/tests/unit/script/types/test_lambda_function.py b/tests/unit/script/types/test_lambda_function.py index 07105477..0474a1d3 100644 --- a/tests/unit/script/types/test_lambda_function.py +++ b/tests/unit/script/types/test_lambda_function.py @@ -6,6 +6,7 @@ from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.resolvable import Integer +from ytdl_sub.script.utils.exceptions import CycleDetected from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments @@ -76,6 +77,28 @@ class TestLambdaFunction: assert script.resolve().get("category_url_map").native == {1: 1, 2: 2, 3: 3} + def test_array_apply_custom_function(self): + output = ( + Script( + { + "the_array": "{ ['a', 'B', 'c', 'D'] }", + "output": "{ %array_apply(the_array, %custom_cap) }", + "should_lower": "{%bool(True)}", + "%custom_cap": """{ + %if( + %bool(should_lower), + %lower($0), + %upper($0) + ) + }""", + } + ) + .resolve(update=True) + .get("output") + .native + ) + assert output == ["a", "b", "c", "d"] + class TestLambdaFunctionIncompatibleNumArguments: @pytest.mark.parametrize( @@ -147,3 +170,27 @@ class TestLambdaFunctionIncompatibleNumArguments: "%output": f"{{%array_enumerate(array1, {lambda_value})}}", } ) + + def test_lambda_with_custom_function_cycle(self): + with pytest.raises( + CycleDetected, + match=re.escape("Cycle detected within these variables: two -> %times_two -> two"), + ): + Script({"%times_two": "{%mul($0, two)}", "two": "{%times_two(2)}"}) + + def test_partial_resolve_nested_lambda_custom_functions_within_custom_functions(self): + assert ( + Script( + { + "%nest4": "{%mul($0, 2)}", + "%nest3": "{%array_at(%array_apply([$0], %nest4), 0)}", + "%nest2": "{%array_at(%array_apply([$0], %nest3), 0)}", + "%nest1": "{%array_at(%array_apply([$0], %nest2), 0)}", + "output": "{%array_at(%array_apply([2], %nest1), 0)}", + } + ) + .resolve_partial() + .get("output") + .native + == 4 + ) diff --git a/tests/unit/utils/test_chapters.py b/tests/unit/utils/test_chapters.py index 0f7e753b..f695a4d0 100644 --- a/tests/unit/utils/test_chapters.py +++ b/tests/unit/utils/test_chapters.py @@ -86,6 +86,21 @@ def chapter_description_3() -> str: """ +@pytest.fixture +def chapter_duplicate_timecodes() -> str: + return """01. 00:00 Ocean +02:41 Dreams +02:41 Nightmares +05:16 Future Tales +08:50 Mind Travelling +11:05 Love Supreme +14:17 Reflections +14:17 Reflections VIP +16:32 Moonlight Fading +19:40 Between Two Worlds +""" + + class TestChapters: def test_chapters_from_str_1(self, chapter_description_1): chapters = Chapters.from_string(chapter_description_1) @@ -104,3 +119,8 @@ class TestChapters: chapters.titles[8] == "(Nightcore) Electro-Light & Jordan Kelvin James - Wait For You (feat. Anna Yvette)" ) + + def test_chapters_from_str_with_duplicate_timecodes(self, chapter_duplicate_timecodes): + chapters = Chapters.from_string(chapter_duplicate_timecodes) + assert len(chapters) == 8 + assert chapters.titles[5] == "Reflections // Reflections VIP" diff --git a/tests/unit/utils/test_file_handler.py b/tests/unit/utils/test_file_handler.py new file mode 100644 index 00000000..80505e24 --- /dev/null +++ b/tests/unit/utils/test_file_handler.py @@ -0,0 +1,20 @@ +import pytest + +from ytdl_sub.utils.file_handler import FileHandler +from ytdl_sub.utils.system import IS_WINDOWS + + +class TestFileHandler: + + def test_directory_exists(self): + if IS_WINDOWS: + return + + assert FileHandler.is_path_writable("/tmp") + assert FileHandler.is_path_writable("/tmp/") + assert FileHandler.is_path_writable("/tmp/non-existent") + assert FileHandler.is_path_writable("/tmp/non-existent/") + assert FileHandler.is_path_writable("/tmp/non-existent/nested") + + assert not FileHandler.is_path_writable("/lol-in-root") + assert not FileHandler.is_path_writable("/") diff --git a/tests/unit/utils/test_script_utils.py b/tests/unit/utils/test_script_utils.py index f9d9b395..792020ba 100644 --- a/tests/unit/utils/test_script_utils.py +++ b/tests/unit/utils/test_script_utils.py @@ -18,6 +18,9 @@ class TestScriptUtils: "string": "value", "quotes": "has '' and \"\"", "triple-single-quote": "right here! '''''''''''''''''''''''''''''' ack '''''''", + "has-double-quotes": 'i got "some double quotes" in here', + "has-single-quotes": "i got 'some single quotes' in here", + "has-both-quotes": "i got 'both quotes\" in here", "int": 1, "bool": True, "list": [1, 2, 3], @@ -60,7 +63,15 @@ class TestScriptUtils: def test_to_syntax_tree(self): out = ScriptUtils.to_native_script( - {"{var_a}": "{var_b}", "static_a": "string with {var_c} in it"} + { + "{var_a}": "{var_b}", + "static_a": "string with {var_c} in it", + "quotes": "has '' and \"\"", + "triple-single-quote": "right here! '''''''''''''''''''''''''''''' ack '''''''", + "has-double-quotes": 'i got "some double quotes" in here', + "has-single-quotes": "i got 'some single quotes' in here", + "has-both-quotes": "i got 'both quotes\" in here", + } ) assert parse(out) == SyntaxTree( ast=[ @@ -70,11 +81,24 @@ class TestScriptUtils: String(value="static_a"): BuiltInFunction( name="concat", args=[ - BuiltInFunction(name="string", args=[String(value="string with ")]), - BuiltInFunction(name="string", args=[Variable(name="var_c")]), - BuiltInFunction(name="string", args=[String(value=" in it")]), + String(value="string with "), + Variable(name="var_c"), + String(value=" in it"), ], ), + String(value="quotes"): String(value="has '' and \"\""), + String(value="triple-single-quote"): String( + value="right here! '''''''''''''''''''''''''''''' ack '''''''" + ), + String(value="has-double-quotes"): String( + value='i got "some double quotes" in here' + ), + String(value="has-single-quotes"): String( + value="i got 'some single quotes' in here" + ), + String(value="has-both-quotes"): String( + value="i got 'both quotes\" in here" + ), } ) ] diff --git a/tests/unit/validators/test_file_path_validators.py b/tests/unit/validators/test_file_path_validators.py index 467f7b80..e66a11c2 100644 --- a/tests/unit/validators/test_file_path_validators.py +++ b/tests/unit/validators/test_file_path_validators.py @@ -1,3 +1,4 @@ +import os import tempfile from pathlib import Path @@ -34,7 +35,8 @@ class TestStringFormatterFilePathValidator: Script({"file_name": formatter.format_string}).resolve().get_str("file_name") ) - assert truncated_file_path.count(".") == ext.count(".") + _, truncated_file_name = os.path.split(truncated_file_path) + assert truncated_file_name.count(".") == ext.count(".") assert str(Path(temp_dir)) in truncated_file_path assert ext in truncated_file_path diff --git a/tests/unit/validators/test_string_formatter_validator.py b/tests/unit/validators/test_string_formatter_validator.py index 4b2baef6..888dc58d 100644 --- a/tests/unit/validators/test_string_formatter_validator.py +++ b/tests/unit/validators/test_string_formatter_validator.py @@ -115,12 +115,12 @@ class TestUnstructuredDictFormatterValidator(object): assert len(validator.dict) == 8 assert all(isinstance(val, expected_formatter_class) for val in validator.dict.values()) assert validator.dict_with_format_strings == { - "key1": "{ %concat( %string( '''string with ''' ), %string( variable ) ) }", + "key1": '{ %concat( "string with ", variable ) }', "key2": "no variables", "key3": "{ %int(3) }", "key4": "{ %float(4.132) }", "key5": "{ %bool(True) }", - "key6": "{ { %concat( %string( variable ), %string( '''_key''' ) ): '''value''', '''static_key''': %concat( %string( variable ), %string( '''_value''' ) ) } }", - "key7": "{ [ '''list_1''', %concat( %string( '''list_''' ), %string( variable_2 ) ) ] }", - "key8": "{ %concat( %string( '''string ''' ), %string( variable1 ), %string( ''' with multiple ''' ), %string( variable2 ) ) }", + "key6": '{ { %concat( variable, "_key" ): "value", "static_key": %concat( variable, "_value" ) } }', + "key7": '{ [ "list_1", %concat( "list_", variable_2 ) ] }', + "key8": '{ %concat( "string ", variable1, " with multiple ", variable2 ) }', } diff --git a/tools/docgen/configuration.py b/tools/docgen/configuration.py index 7d9cb203..48c669dc 100644 --- a/tools/docgen/configuration.py +++ b/tools/docgen/configuration.py @@ -8,6 +8,9 @@ from ytdl_sub.config.config_validator import ConfigOptions class ConfigurationDocGen(DocGen): LOCATION = Path("docs/source/config_reference/config_yaml.rst") + DOCSTRING_LOCATION = ( + "The respective function docstrings within ytdl_sub/config/config_validator.py" + ) @classmethod def generate(cls): diff --git a/tools/docgen/docgen.py b/tools/docgen/docgen.py index a021e7ae..f609bf44 100644 --- a/tools/docgen/docgen.py +++ b/tools/docgen/docgen.py @@ -12,6 +12,21 @@ class DocGen: LOCATION: Path + # human-readable location of where to edit the underlying docstrings + DOCSTRING_LOCATION: str + + @classmethod + def _generate_warning(cls) -> str: + return ( + "..\n" + " WARNING: This RST file is generated from docstrings in:\n" + f" {cls.DOCSTRING_LOCATION}\n" + " In order to make a change to this file, edit the respective docstring\n" + " and run `make docs`. This will automatically sync the Python RST-based\n" + " docstrings into this file. If the docstrings and RST file are out of sync,\n" + " it will fail TestDocGen tests in GitHub CI.\n" + ) + @classmethod @abstractmethod def generate(cls) -> str: @@ -25,7 +40,7 @@ class DocGen: Maybe writes the docs to their file if the global is set to True, and returns the generated docs """ - contents = cls.generate() + contents = cls._generate_warning() + cls.generate() if REGENERATE_DOCS: with open(cls.LOCATION, "w", encoding="utf-8") as out: out.write(contents) diff --git a/tools/docgen/entry_variables.py b/tools/docgen/entry_variables.py index df53b13a..4f46c452 100644 --- a/tools/docgen/entry_variables.py +++ b/tools/docgen/entry_variables.py @@ -25,6 +25,7 @@ def _variable_class_to_name(obj: Type[Any]) -> str: class EntryVariablesDocGen(DocGen): LOCATION = Path("docs/source/config_reference/scripting/entry_variables.rst") + DOCSTRING_LOCATION = "src/ytdl_sub/entries/script/variable_definitions.py" @classmethod def generate(cls) -> str: diff --git a/tools/docgen/plugins.py b/tools/docgen/plugins.py index 5b33a39c..f0331acc 100644 --- a/tools/docgen/plugins.py +++ b/tools/docgen/plugins.py @@ -21,6 +21,7 @@ PLUGIN_NAMES_TO_SKIP_PROPERTIES: Set[str] = { "filter_include", "filter_exclude", "embed_thumbnail", + "square_thumbnail", "video_tags", "download", } @@ -29,6 +30,7 @@ PLUGIN_NAMES_TO_SKIP_PROPERTIES: Set[str] = { class PluginsDocGen(DocGen): LOCATION = Path("docs/source/config_reference/plugins.rst") + DOCSTRING_LOCATION = "The respective plugin files under src/ytdl_sub/plugins/" @classmethod def generate(cls): diff --git a/tools/docgen/scripting_functions.py b/tools/docgen/scripting_functions.py index 9ae519f9..78924ca4 100644 --- a/tools/docgen/scripting_functions.py +++ b/tools/docgen/scripting_functions.py @@ -56,6 +56,7 @@ def get_function_docstring( class ScriptingFunctionsDocGen(DocGen): LOCATION = Path("docs/source/config_reference/scripting/scripting_functions.rst") + DOCSTRING_LOCATION = "The respective function files under src/ytdl_sub/script/functions/" @classmethod def generate(cls) -> str: diff --git a/tools/docgen/static_variables.py b/tools/docgen/static_variables.py index 254179cc..b8b0c315 100644 --- a/tools/docgen/static_variables.py +++ b/tools/docgen/static_variables.py @@ -10,6 +10,7 @@ from ytdl_sub.entries.variables.override_variables import SubscriptionVariables class StaticVariablesDocGen(DocGen): LOCATION = Path("docs/source/config_reference/scripting/static_variables.rst") + DOCSTRING_LOCATION = "src/ytdl_sub/entries/variables/override_variables.py" @classmethod def generate(cls) -> str: