Compare commits
No commits in common. "master" and "2025.08.20" have entirely different histories.
master
...
2025.08.20
266 changed files with 3617 additions and 9385 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -146,11 +146,8 @@ docker/testing/volumes
|
|||
.local/
|
||||
|
||||
.ytdl-sub-working-directory
|
||||
.ytdl-sub-lock
|
||||
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
|
||||
tools/docgen/out
|
||||
|
||||
prof/
|
||||
tools/docgen/out
|
||||
|
|
@ -7,7 +7,6 @@ build:
|
|||
|
||||
sphinx:
|
||||
configuration: docs/source/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
python:
|
||||
install:
|
||||
|
|
@ -15,4 +14,4 @@ python:
|
|||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- docs
|
||||
11
Makefile
11
Makefile
|
|
@ -36,12 +36,12 @@ endif
|
|||
all: check_lint docs docker docker_ubuntu docker_gui
|
||||
|
||||
lint:
|
||||
python3 -m ruff format .
|
||||
python3 -m ruff check --fix .
|
||||
python3 -m isort .
|
||||
python3 -m black .
|
||||
python3 -m pylint src
|
||||
check_lint:
|
||||
ruff format --check . \
|
||||
&& ruff check . \
|
||||
isort . --check-only --diff \
|
||||
&& black . --check \
|
||||
&& pylint src/
|
||||
wheel: clean
|
||||
$(shell echo "__pypi_version__ = \"$(PYPI_VERSION)\"" > src/ytdl_sub/__init__.py)
|
||||
|
|
@ -63,8 +63,7 @@ executable: clean
|
|||
mv dist/ytdl-sub dist/ytdl-sub${EXEC_SUFFIX}
|
||||
docs:
|
||||
REGENERATE_DOCS=1 pytest tests/unit/docgen/test_docgen.py
|
||||
sphinx-build --write-all --fail-on-warning --nitpicky -b html \
|
||||
"./docs/source/" "./docs/build/"
|
||||
sphinx-build --fail-on-warning --nitpicky -b html docs/source/ docs/build/
|
||||
clean:
|
||||
rm -rf \
|
||||
.pytest_cache/ \
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ __preset__:
|
|||
|
||||
# Pass any arg directly to yt-dlp's Python API
|
||||
ytdl_options:
|
||||
cookiefile: "/config/ytdl-sub-configs/cookie.txt"
|
||||
cookiefile: "/config/cookie.txt"
|
||||
|
||||
###################################################################
|
||||
# TV Show Presets. Can replace Plex with Plex/Jellyfin/Emby/Kodi
|
||||
|
|
|
|||
|
|
@ -7,20 +7,16 @@ 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 -pv "${DEFAULT_WORKSPACE}" && \
|
||||
RUN mkdir -p /config && \
|
||||
apk update --no-cache && \
|
||||
apk upgrade --no-cache && \
|
||||
apk add --no-cache --repository=http://dl-3.alpinelinux.org/alpine/edge/main/ \
|
||||
vim \
|
||||
g++ \
|
||||
nano \
|
||||
unzip \
|
||||
make \
|
||||
deno \
|
||||
libffi-dev \
|
||||
"python3>=3.10" \
|
||||
py3-pip \
|
||||
|
|
@ -32,7 +28,6 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
"aria2>=1.36.0" && \
|
||||
ffmpeg -version && \
|
||||
aria2c --version && \
|
||||
deno --version && \
|
||||
# Install phantomjs if using x86_64, ensure it is properly installed
|
||||
if [[ $(uname -m) == "x86_64" ]]; then \
|
||||
echo "installing phantomjs" && \
|
||||
|
|
@ -46,30 +41,29 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
phantomjs --version && \
|
||||
cd -; \
|
||||
fi && \
|
||||
# Configure pip globally
|
||||
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
|
||||
# Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly
|
||||
python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \
|
||||
echo "hi" && \
|
||||
# Install ytdl-sub, ensure it is installed properly
|
||||
python3 -m pip install --break-system-packages --no-cache-dir ytdl_sub-*.whl && \
|
||||
ytdl-sub -h && \
|
||||
# Delete unneeded packages after install
|
||||
rm ytdl_sub-*.whl && \
|
||||
apk del \
|
||||
g++ \
|
||||
make \
|
||||
libffi-dev && \
|
||||
python3 -m pip --help
|
||||
|
||||
libffi-dev \
|
||||
py3-pip \
|
||||
py3-setuptools
|
||||
|
||||
###############################################################################
|
||||
# CONTAINER CONFIGS
|
||||
|
||||
ENV EDITOR="nano" \
|
||||
HOME="${DEFAULT_WORKSPACE}" \
|
||||
HOME="/config" \
|
||||
DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \
|
||||
CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \
|
||||
CRON_WRAPPER_SCRIPT="${DEFAULT_WORKSPACE}/.cron_wrapper" \
|
||||
LOGS_TO_STDOUT="${DEFAULT_WORKSPACE}/.cron.log" \
|
||||
DEFAULT_WORKSPACE=/config \
|
||||
CRON_SCRIPT="/config/cron" \
|
||||
CRON_WRAPPER_SCRIPT="/config/.cron_wrapper" \
|
||||
LOGS_TO_STDOUT=/config/.cron.log \
|
||||
LSIO_FIRST_PARTY=false
|
||||
|
||||
VOLUME "${DEFAULT_WORKSPACE}"
|
||||
WORKDIR "${DEFAULT_WORKSPACE}"
|
||||
VOLUME /config
|
||||
|
|
@ -4,8 +4,6 @@ FROM lscr.io/linuxserver/code-server:4.98.2
|
|||
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
|
||||
|
|
@ -23,7 +21,6 @@ RUN mkdir -p /config && \
|
|||
vim \
|
||||
g++ \
|
||||
nano \
|
||||
unzip \
|
||||
make \
|
||||
python3-pip \
|
||||
fontconfig \
|
||||
|
|
@ -62,13 +59,8 @@ RUN mkdir -p /config && \
|
|||
echo "Phantom JS version:" && \
|
||||
phantomjs --version ; \
|
||||
fi && \
|
||||
# Install Deno, required for YouTube downloads
|
||||
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y --no-modify-path && \
|
||||
deno --help && \
|
||||
# Configure pip globally
|
||||
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
|
||||
# Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly
|
||||
python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \
|
||||
# Install ytdl-sub, ensure it is installed properly
|
||||
pip install --no-cache-dir --break-system-packages ytdl_sub-*.whl && \
|
||||
ytdl-sub -h && \
|
||||
# Delete unneeded packages after install
|
||||
rm ytdl_sub-*.whl && \
|
||||
|
|
@ -76,11 +68,11 @@ RUN mkdir -p /config && \
|
|||
g++ \
|
||||
make \
|
||||
xz-utils \
|
||||
bzip2 && \
|
||||
bzip2 \
|
||||
python3-venv && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get purge -y --auto-remove && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
python3 -m pip --help
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
###############################################################################
|
||||
# CONTAINER CONFIGS
|
||||
|
|
@ -88,10 +80,10 @@ RUN mkdir -p /config && \
|
|||
ENV EDITOR="nano" \
|
||||
HOME="/config" \
|
||||
DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \
|
||||
CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \
|
||||
DEFAULT_WORKSPACE=/config/ytdl-sub-configs \
|
||||
CRON_SCRIPT="/config/ytdl-sub-configs/cron" \
|
||||
CRON_WRAPPER_SCRIPT="/config/.cron_wrapper" \
|
||||
LOGS_TO_STDOUT=/config/.cron.log \
|
||||
LSIO_FIRST_PARTY=false
|
||||
|
||||
VOLUME /config
|
||||
WORKDIR "${DEFAULT_WORKSPACE}"
|
||||
VOLUME /config
|
||||
|
|
@ -1 +0,0 @@
|
|||
Dockerfile
|
||||
|
|
@ -7,15 +7,13 @@ 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 -pv "${DEFAULT_WORKSPACE}" && \
|
||||
RUN mkdir -p /config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
|
|
@ -26,7 +24,6 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
vim \
|
||||
g++ \
|
||||
nano \
|
||||
unzip \
|
||||
make \
|
||||
python3-pip \
|
||||
fontconfig \
|
||||
|
|
@ -65,13 +62,8 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
echo "Phantom JS version:" && \
|
||||
phantomjs --version ; \
|
||||
fi && \
|
||||
# Install Deno, required for YouTube downloads
|
||||
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y --no-modify-path && \
|
||||
deno --help && \
|
||||
# Configure pip globally
|
||||
echo -e "[global]\nbreak-system-packages = true\nroot-user-action = ignore\nno-cache-dir = true" > /etc/pip.conf && \
|
||||
# Install ytdl-sub and yt-dlp dependencies, ensure they are installed properly
|
||||
python3 -m pip install ytdl_sub-*.whl curl-cffi yt-dlp-ejs && \
|
||||
# Install ytdl-sub, ensure it is installed properly
|
||||
pip install --no-cache-dir --break-system-packages ytdl_sub-*.whl && \
|
||||
ytdl-sub -h && \
|
||||
# Delete unneeded packages after install
|
||||
rm ytdl_sub-*.whl && \
|
||||
|
|
@ -79,22 +71,22 @@ RUN mkdir -pv "${DEFAULT_WORKSPACE}" && \
|
|||
g++ \
|
||||
make \
|
||||
xz-utils \
|
||||
bzip2 && \
|
||||
bzip2 \
|
||||
python3-venv && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get purge -y --auto-remove && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
python3 -m pip --help
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
###############################################################################
|
||||
# CONTAINER CONFIGS
|
||||
|
||||
ENV EDITOR="nano" \
|
||||
HOME="${DEFAULT_WORKSPACE}" \
|
||||
HOME="/config" \
|
||||
DOCKER_MODS=linuxserver/mods:universal-stdout-logs|linuxserver/mods:universal-cron \
|
||||
CRON_SCRIPT="${DEFAULT_WORKSPACE}/cron" \
|
||||
CRON_WRAPPER_SCRIPT="${DEFAULT_WORKSPACE}/.cron_wrapper" \
|
||||
LOGS_TO_STDOUT="${DEFAULT_WORKSPACE}/.cron.log" \
|
||||
DEFAULT_WORKSPACE=/config \
|
||||
CRON_SCRIPT="/config/cron" \
|
||||
CRON_WRAPPER_SCRIPT="/config/.cron_wrapper" \
|
||||
LOGS_TO_STDOUT=/config/.cron.log \
|
||||
LSIO_FIRST_PARTY=false
|
||||
|
||||
VOLUME "${DEFAULT_WORKSPACE}"
|
||||
WORKDIR "${DEFAULT_WORKSPACE}"
|
||||
VOLUME /config
|
||||
24
docker/root/custom-cont-init.d/defaults
Executable file → Normal file
24
docker/root/custom-cont-init.d/defaults
Executable file → Normal file
|
|
@ -17,28 +17,12 @@ echo "Starting ytdl-sub..."
|
|||
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
|
||||
# always create empty cron log file on start
|
||||
echo "" > "$LOGS_TO_STDOUT"
|
||||
|
||||
# set up cron
|
||||
if [ "$CRON_SCHEDULE" != "" ] ; then
|
||||
|
|
@ -47,11 +31,9 @@ if [ "$CRON_SCHEDULE" != "" ] ; then
|
|||
|
||||
# create cron script wrapper
|
||||
echo '#!/bin/bash' > "$CRON_WRAPPER_SCRIPT"
|
||||
# Echo commands for easier user debugging:
|
||||
echo "set -x" >> "$CRON_WRAPPER_SCRIPT"
|
||||
echo "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" >> "$CRON_WRAPPER_SCRIPT"
|
||||
echo "cd \"$DEFAULT_WORKSPACE\"" >> "$CRON_WRAPPER_SCRIPT"
|
||||
echo ". \"$CRON_SCRIPT\" >> \"$LOGS_TO_STDOUT\" 2>&1" >> "$CRON_WRAPPER_SCRIPT"
|
||||
echo ". \"$CRON_SCRIPT\" | tee -a \"$LOGS_TO_STDOUT\"" >> "$CRON_WRAPPER_SCRIPT"
|
||||
chmod +x "$CRON_WRAPPER_SCRIPT"
|
||||
chown abc:abc "$CRON_WRAPPER_SCRIPT"
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,4 @@
|
|||
# See the documentation above on how to build your own custom presets.
|
||||
#
|
||||
configuration:
|
||||
# Avoid unnecessarily long large file renames, set this to a path on the same
|
||||
# filesystem as the destination for downloaded files in the `overrides: /
|
||||
# *_directory:` paths:
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
|
|
|
|||
|
|
@ -1,15 +1,4 @@
|
|||
echo "Beginning cron job..."
|
||||
|
||||
# Place your ytdl-sub command(s) here.
|
||||
#
|
||||
# This script is executed in the same directory as this file which also contains the
|
||||
# default `./config.yaml` and `./subscriptions.yaml`, so you don't need to use the
|
||||
# `--config` CLI option or pass a `SUBPATH` to the `$ ytdl-sub sub` sub-command.
|
||||
#
|
||||
# Test your configuration and subscriptions carefully before automating downloads to
|
||||
# prevent triggering throttles or bans:
|
||||
#
|
||||
# https://ytdl-sub.readthedocs.io/en/latest/guides/getting_started/downloading.html
|
||||
#
|
||||
# Once you've tested your configuration and you're ready to download entries unattended,
|
||||
# remove the next line and un-comment the following line:
|
||||
echo "WARNING: Read /config/ytdl-sub-configs/cron and modify to automate downloads."
|
||||
# ytdl-sub sub
|
||||
# This script is executed in the same relative path as this file.
|
||||
|
|
@ -20,7 +20,7 @@ __preset__:
|
|||
|
||||
# Pass any arg directly to yt-dlp's Python API
|
||||
# ytdl_options:
|
||||
# cookiefile: "/config/ytdl-sub-configs/cookie.txt"
|
||||
# cookiefile: "/config/cookie.txt"
|
||||
|
||||
###################################################################
|
||||
# Subscriptions nested under this will use the
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Local building and testing of the Docker image variants.
|
||||
|
||||
# Defensive settings for make:
|
||||
# https://tech.davis-hansson.com/p/make/
|
||||
SHELL:=bash
|
||||
.ONESHELL:
|
||||
.SHELLFLAGS:=-eu -o pipefail -c
|
||||
.SILENT:
|
||||
.DELETE_ON_ERROR:
|
||||
MAKEFLAGS+=--warn-undefined-variables
|
||||
MAKEFLAGS+=--no-builtin-rules
|
||||
export PS1?=$$
|
||||
# Prefix echoed recipe commands with the recipe line number for debugging:
|
||||
export PS4?=:$$LINENO+
|
||||
|
||||
VARIANTS=headless gui ubuntu
|
||||
ROOT_PREREQS:=$(shell find ../root -type f)
|
||||
|
||||
# Finished with `$(shell)`, echo recipe commands going forward
|
||||
.SHELLFLAGS+= -x
|
||||
|
||||
|
||||
### Top-level targets:
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
.PHONY: build
|
||||
build: $(VARIANTS:%=./build/ytdl-sub-%.log)
|
||||
|
||||
.PHONY: run
|
||||
run: build $(VARIANTS:%=./volumes/ytdl-sub-%/)
|
||||
docker compose up
|
||||
|
||||
|
||||
### Real targets:
|
||||
|
||||
# Re-build the local images when changes require it.
|
||||
./build/ytdl-sub-%.log: ../Dockerfile.% $(ROOT_PREREQS)
|
||||
mkdir -pv "$(dir $(@))"
|
||||
docker compose build "$(@:build/ytdl-sub-%.log=ytdl-sub-%)" |&
|
||||
tee -a "$(@)"
|
||||
|
||||
# Ensure volumes are owned by the developer's normal user:
|
||||
./volumes/ytdl-sub-%/:
|
||||
mkdir -pv "$(@)"
|
||||
|
|
@ -1,50 +1,39 @@
|
|||
services:
|
||||
ytdl-sub-gui:
|
||||
build:
|
||||
context: "../"
|
||||
dockerfile: "./Dockerfile.gui"
|
||||
image: "ytdl-sub-gui:local"
|
||||
container_name: "ytdl-sub-gui"
|
||||
image: ytdl-sub-gui:local
|
||||
container_name: ytdl-sub-gui
|
||||
environment:
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
TZ: "America/Los_Angeles"
|
||||
CRON_SCHEDULE: '*/1 * * * *'
|
||||
CRON_RUN_ON_START: "true"
|
||||
UPDATE_YT_DLP_ON_START: "stable"
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Los_Angeles
|
||||
- CRON_SCHEDULE="*/1 * * * *"
|
||||
- CRON_RUN_ON_START=true
|
||||
volumes:
|
||||
- "./volumes/ytdl-sub-gui/:/config/"
|
||||
- ./volumes/ytdl-sub-gui:/config
|
||||
ports:
|
||||
- "8443:8443"
|
||||
restart: "unless-stopped"
|
||||
ytdl-sub-headless:
|
||||
build:
|
||||
context: "../"
|
||||
image: "ytdl-sub:local"
|
||||
container_name: "ytdl-sub-headless"
|
||||
- 8443:8443
|
||||
restart: unless-stopped
|
||||
ytdl-sub:
|
||||
image: ytdl-sub:local
|
||||
container_name: ytdl-sub
|
||||
environment:
|
||||
PUID: "1000"
|
||||
PGID: "1000"
|
||||
TZ: "America/Los_Angeles"
|
||||
CRON_SCHEDULE: '*/1 * * * *'
|
||||
CRON_RUN_ON_START: "true"
|
||||
UPDATE_YT_DLP_ON_START: "stable"
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Los_Angeles
|
||||
- CRON_SCHEDULE="*/1 * * * *"
|
||||
- CRON_RUN_ON_START=true
|
||||
volumes:
|
||||
- "./volumes/ytdl-sub-headless/:/config/"
|
||||
restart: "unless-stopped"
|
||||
- ./volumes/ytdl-sub:/config
|
||||
restart: unless-stopped
|
||||
ytdl-sub-ubuntu:
|
||||
build:
|
||||
context: "../"
|
||||
dockerfile: "./Dockerfile.ubuntu"
|
||||
image: "ytdl-sub-ubuntu:local"
|
||||
container_name: "ytdl-sub-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"
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=America/Los_Angeles
|
||||
- CRON_SCHEDULE="*/1 * * * *"
|
||||
- CRON_RUN_ON_START=true
|
||||
volumes:
|
||||
- "./volumes/ytdl-sub-ubuntu/:/config/"
|
||||
restart: "unless-stopped"
|
||||
- ./volumes/ytdl-sub:/config
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "ytdl-sub"
|
||||
copyright = "2026, Jesse Bannon"
|
||||
copyright = "2024, Jesse Bannon"
|
||||
author = "Jesse Bannon"
|
||||
release = ""
|
||||
|
||||
|
|
@ -15,8 +15,10 @@ release = ""
|
|||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx_copybutton",
|
||||
"sphinx_design",
|
||||
]
|
||||
|
|
@ -68,3 +70,19 @@ extlinks = {
|
|||
"lsio-gh": ("https://github.com/linuxserver/%s", "%s image"),
|
||||
"ytdl-sub-gh": ("https://github.com/jmbannon/ytdl-sub/%s", "src %s"),
|
||||
}
|
||||
|
||||
# -- Options for autodoc ----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration
|
||||
|
||||
# Automatically extract typehints when specified and place them in
|
||||
# descriptions of the relevant function/method.
|
||||
autodoc_default_options = {
|
||||
"autodoc_typehints_format": "short",
|
||||
"autodoc_class_signature": "separated",
|
||||
"add_module_names": False,
|
||||
# "add_class_names": False,
|
||||
}
|
||||
|
||||
python_use_unqualified_type_names = True
|
||||
napoleon_numpy_docstring = True
|
||||
napoleon_use_rtype = False
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
..
|
||||
WARNING: This RST file is generated from docstrings in:
|
||||
The respective function docstrings within ytdl_sub/config/config_validator.py
|
||||
In order to make a change to this file, edit the respective docstring
|
||||
and run `make docs`. This will automatically sync the Python RST-based
|
||||
docstrings into this file. If the docstrings and RST file are out of sync,
|
||||
it will fail TestDocGen tests in GitHub CI.
|
||||
|
||||
==================
|
||||
Configuration File
|
||||
==================
|
||||
-----------
|
||||
config.yaml
|
||||
-----------
|
||||
|
||||
ytdl-sub is configured using a ``config.yaml`` file.
|
||||
|
||||
The ``config.yaml`` is made up of two sections:
|
||||
|
|
@ -17,118 +14,86 @@ The ``config.yaml`` is made up of two sections:
|
|||
configuration:
|
||||
presets:
|
||||
|
||||
You can jump to any section and subsection of the config using the navigation
|
||||
section to the left.
|
||||
|
||||
Note for Windows users, paths can be represented with ``C:/forward/slashes/like/linux``.
|
||||
If you prefer to use a Windows backslash, note that it must have
|
||||
``C:\\double\\bashslash\\paths`` in order to escape the backslash character. This is due
|
||||
to it being a YAML escape character.
|
||||
If you wish to represent paths like Windows, you will need to ``C:\\double\\bashslash\\paths``
|
||||
in order to escape the backslash character.
|
||||
|
||||
configuration
|
||||
~~~~~~~~~~~~~
|
||||
The ``configuration`` section contains app-wide configs applied to all presets
|
||||
and subscriptions.
|
||||
|
||||
.. autoclass:: ytdl_sub.config.config_validator.ConfigOptions()
|
||||
:members:
|
||||
:member-order: bysource
|
||||
:exclude-members: subscription_value, persist_logs, experimental
|
||||
|
||||
persist_logs
|
||||
""""""""""""
|
||||
Within ``configuration``, define whether logs from subscription downloads
|
||||
should be persisted.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
configuration:
|
||||
dl_aliases:
|
||||
mv: "--preset music_video"
|
||||
u: "--download.url"
|
||||
|
||||
experimental:
|
||||
enable_update_with_info_json: True
|
||||
|
||||
ffmpeg_path: "/usr/bin/ffmpeg"
|
||||
ffprobe_path: "/usr/bin/ffprobe"
|
||||
|
||||
file_name_max_bytes: 255
|
||||
lock_directory: "/tmp"
|
||||
|
||||
persist_logs:
|
||||
keep_successful_logs: True
|
||||
logs_directory: "/var/log/ytdl-sub-logs"
|
||||
logs_directory: "/path/to/log/directory"
|
||||
|
||||
umask: "022"
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
Log files are stored as
|
||||
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
|
||||
|
||||
dl_aliases
|
||||
----------
|
||||
.. _dl_aliases:
|
||||
.. autoclass:: ytdl_sub.config.config_validator.PersistLogsValidator()
|
||||
:members:
|
||||
:member-order: bysource
|
||||
|
||||
Alias definitions to shorten :ref:`dl arguments <usage:Download Options>`. For example,
|
||||
presets
|
||||
~~~~~~~
|
||||
``presets`` define a `formula` for how to format downloaded media and metadata.
|
||||
|
||||
This section is work-in-progress!
|
||||
|
||||
preset
|
||||
""""""
|
||||
Presets support inheritance by defining a parent preset:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
configuration:
|
||||
dl_aliases:
|
||||
mv: "--preset music_video"
|
||||
u: "--download.url"
|
||||
presets:
|
||||
custom_preset:
|
||||
...
|
||||
parent_preset:
|
||||
...
|
||||
child_preset:
|
||||
preset: "parent_preset"
|
||||
|
||||
Simplifies
|
||||
In the example above, ``child_preset`` inherits all fields defined in ``parent_preset``.
|
||||
It is advantageous to use parent presets where possible to reduce duplicate yaml
|
||||
definitions.
|
||||
|
||||
.. code-block:: bash
|
||||
Presets also support inheritance from multiple presets:
|
||||
|
||||
ytdl-sub dl --preset "Jellyfin Music Videos" --download.url "youtube.com/watch?v=a1b2c3"
|
||||
.. code-block:: yaml
|
||||
|
||||
to
|
||||
child_preset:
|
||||
preset:
|
||||
- "custom_preset"
|
||||
- "parent_preset"
|
||||
|
||||
.. code-block:: bash
|
||||
In this example, ``child_preset`` will inherit all fields from ``custom_preset`` and
|
||||
``parent_preset`` in that order. The bottom-most preset has the highest priority. More
|
||||
specifically, presets are merged using `mergedeep`_ via `a TYPESAFE_ADDITIVE merge`_,
|
||||
which means:
|
||||
|
||||
ytdl-sub dl --mv --u "youtube.com/watch?v=a1b2c3"
|
||||
- if two conflicting keys arent lists or mappings, overwrite the higher priority one
|
||||
- otherwise, combine then re-evaluate
|
||||
|
||||
experimental
|
||||
------------
|
||||
Experimental flags reside under the ``experimental`` key.
|
||||
If you are only inheriting from one preset, the syntax ``preset: "parent_preset"`` is
|
||||
valid YAML. Inheriting from multiple presets require use of a list.
|
||||
|
||||
``enable_update_with_info_json``
|
||||
|
||||
Enables modifying subscription files using info.json files using the argument
|
||||
``--update-with-info-json``. This feature is still being tested and has the ability to
|
||||
destroy files. Ensure you have a full backup before usage. You have been warned!
|
||||
|
||||
ffmpeg_path
|
||||
-----------
|
||||
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux,
|
||||
``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows.
|
||||
|
||||
ffprobe_path
|
||||
------------
|
||||
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux,
|
||||
``./ffprobe.exe`` in the same directory as ytdl-sub for Windows.
|
||||
|
||||
file_name_max_bytes
|
||||
-------------------
|
||||
Max file name size in bytes. Most OS's typically default to 255 bytes.
|
||||
|
||||
lock_directory
|
||||
--------------
|
||||
The directory to temporarily store file locks, which prevents multiple instances
|
||||
of ``ytdl-sub`` from running. Note that file locks do not work on
|
||||
network-mounted directories. Ensure that this directory resides on the host
|
||||
machine. Defaults to ``/tmp``.
|
||||
|
||||
persist_logs
|
||||
------------
|
||||
By default, no logs are persisted. Specifying this key will enable persisted logs. The following
|
||||
options are available.
|
||||
|
||||
``keep_successful_logs``
|
||||
|
||||
Defaults to ``True``. When this key is ``False``, only write log files for failed
|
||||
subscriptions.
|
||||
|
||||
``logs_directory``
|
||||
|
||||
Required field. Write log files to this directory with names like
|
||||
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
|
||||
|
||||
umask
|
||||
-----
|
||||
Umask in octal format to apply to every created file. Defaults to ``022``.
|
||||
|
||||
working_directory
|
||||
-----------------
|
||||
The directory to temporarily store downloaded files before moving them into their final
|
||||
directory. Defaults to ``.ytdl-sub-working-directory``, created in the same directory
|
||||
that ytdl-sub is invoked from.
|
||||
|
||||
Presets
|
||||
=======
|
||||
Custom presets are defined in this section. Refer to the
|
||||
:ref:`Getting Started Guide<guides/getting_started/first_config:Basic Configuration>`
|
||||
on how to configure.
|
||||
.. _`mergedeep`:
|
||||
https://mergedeep.readthedocs.io/en/latest/
|
||||
.. _`a TYPESAFE_ADDITIVE merge`:
|
||||
https://mergedeep.readthedocs.io/en/latest/index.html#merge-strategies
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
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
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ Extracts audio from a video file.
|
|||
The codec to output after extracting the audio. Supported codecs are aac, flac, mp3, m4a,
|
||||
opus, vorbis, wav, and best to grab the best possible format at runtime.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -36,6 +37,7 @@ Extracts audio from a video file.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``quality``
|
||||
|
||||
:expected type: Float
|
||||
|
|
@ -43,6 +45,7 @@ Extracts audio from a video file.
|
|||
Optional. Specify ffmpeg audio quality. Insert a value between ``0`` (better) and ``9``
|
||||
(worse) for variable bitrate, or a specific bitrate like ``128`` for 128k.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
chapters
|
||||
|
|
@ -81,12 +84,14 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
Defaults to False. If chapters do not exist in the video/description itself, attempt to
|
||||
scrape comments to find the chapters.
|
||||
|
||||
|
||||
``embed_chapters``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Defaults to True. Embed chapters into the file.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -95,6 +100,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``force_key_frames``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
|
|
@ -102,6 +108,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
Defaults to False. Force keyframes at cuts when removing sections. This is slow due to
|
||||
needing a re-encode, but the resulting video may have fewer artifacts around the cuts.
|
||||
|
||||
|
||||
``remove_chapters_regex``
|
||||
|
||||
:expected type: Optional[List[RegexString]
|
||||
|
|
@ -109,6 +116,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
List of regex patterns to match chapter titles against and remove them from the
|
||||
entry.
|
||||
|
||||
|
||||
``remove_sponsorblock_categories``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
|
|
@ -117,6 +125,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
categories that are specified in ``sponsorblock_categories`` or "all", which removes
|
||||
everything specified in ``sponsorblock_categories``.
|
||||
|
||||
|
||||
``sponsorblock_categories``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
|
|
@ -125,6 +134,7 @@ chapters and remove specific ones. Can also remove chapters using regex.
|
|||
"intro", "outro", "selfpromo", "preview", "filler", "interaction", "music_offtopic",
|
||||
"poi_highlight", or "all" to include all categories.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
date_range
|
||||
|
|
@ -137,11 +147,9 @@ Dates must adhere to a yt-dlp datetime. From their docs:
|
|||
A string in the format YYYYMMDD or
|
||||
(now|today|yesterday|date)[+-][0-9](microsecond|second|minute|hour|day|week|month|year)(s)
|
||||
|
||||
Valid examples are ``now-2weeks`` or ``20200101``. Can use override variables in
|
||||
this. Note that yt-dlp will round times to the closest day, meaning that `day` is
|
||||
the lowest granularity possible. Also note that, considering time zones, it's best
|
||||
to include a margin of an extra day on either side to be sure it includes the
|
||||
intended download files.
|
||||
Valid examples are ``now-2weeks`` or ``20200101``. Can use override variables in this.
|
||||
Note that yt-dlp will round times to the closest day, meaning that `day` is the lowest
|
||||
granularity possible.
|
||||
|
||||
:Usage:
|
||||
|
||||
|
|
@ -157,13 +165,15 @@ intended download files.
|
|||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Only download videos after or on this datetime, inclusive.
|
||||
Only download videos after this datetime.
|
||||
|
||||
|
||||
``before``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Only download videos only before this datetime, not inclusive.
|
||||
Only download videos before this datetime.
|
||||
|
||||
|
||||
``breaks``
|
||||
|
||||
|
|
@ -172,6 +182,7 @@ intended download files.
|
|||
Toggle to enable breaking subsequent metadata downloads if an entry's upload date
|
||||
is out of range. Defaults to True.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -180,6 +191,7 @@ intended download files.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``type``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -187,6 +199,7 @@ intended download files.
|
|||
Which type of date to use. Must be either ``upload_date`` or ``release_date``.
|
||||
Defaults to ``upload_date``.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
download
|
||||
|
|
@ -290,6 +303,7 @@ Also supports custom ffmpeg conversions:
|
|||
- Video: avi, flv, mkv, mov, mp4, webm
|
||||
- Audio: aac, flac, mp3, m4a, opus, vorbis, wav
|
||||
|
||||
|
||||
``convert_with``
|
||||
|
||||
:expected type: Optional[String]
|
||||
|
|
@ -298,6 +312,7 @@ Also supports custom ffmpeg conversions:
|
|||
yt-dlp whereas ``ffmpeg`` specifies it will be converted using a custom command specified
|
||||
with ``ffmpeg_post_process_args``. Defaults to ``yt-dlp``.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -306,6 +321,7 @@ Also supports custom ffmpeg conversions:
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``ffmpeg_post_process_args``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -318,6 +334,7 @@ Also supports custom ffmpeg conversions:
|
|||
The output file will use the extension specified in ``convert_to``. Post-processing args
|
||||
can still be set with ``convert_with`` set to ``yt-dlp``.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
filter_exclude
|
||||
|
|
@ -453,6 +470,7 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``kodi_safe``
|
||||
|
||||
:expected type: OverridesBooleanFormatterValidator
|
||||
|
|
@ -461,12 +479,14 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
emojis and some foreign language characters. Setting this to True will replace those
|
||||
characters with '□'.
|
||||
|
||||
|
||||
``nfo_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The NFO file name.
|
||||
|
||||
|
||||
``nfo_root``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -479,6 +499,7 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
<episodedetails>
|
||||
</episodedetails>
|
||||
|
||||
|
||||
``tags``
|
||||
|
||||
:expected type: NfoTags
|
||||
|
|
@ -515,6 +536,7 @@ with a ``.nfo`` extension. You can add any values into the NFO.
|
|||
<genre>Comedy</genre>
|
||||
<genre>Drama</genre>
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
output_directory_nfo_tags
|
||||
|
|
@ -546,6 +568,7 @@ Usage:
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``kodi_safe``
|
||||
|
||||
:expected type: OverridesBooleanFormatterValidator
|
||||
|
|
@ -554,12 +577,14 @@ Usage:
|
|||
emojis and some foreign language characters. Setting this to True will replace those
|
||||
characters with '□'.
|
||||
|
||||
|
||||
``nfo_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The NFO file name.
|
||||
|
||||
|
||||
``nfo_root``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -572,6 +597,7 @@ Usage:
|
|||
<tvshow>
|
||||
</tvshow>
|
||||
|
||||
|
||||
``tags``
|
||||
|
||||
:expected type: NfoTags
|
||||
|
|
@ -606,6 +632,7 @@ Usage:
|
|||
<genre>Comedy</genre>
|
||||
<genre>Drama</genre>
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
output_options
|
||||
|
|
@ -640,6 +667,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
The file name to store a subscriptions download archive placed relative to
|
||||
the output directory. Defaults to ``.ytdl-sub-{subscription_name}-download-archive.json``
|
||||
|
||||
|
||||
``file_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -647,6 +675,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
The file name for the media file. This can include directories such as
|
||||
``"Season {upload_year}/{title}.{ext}"``, and will be placed in the output directory.
|
||||
|
||||
|
||||
``info_json_name``
|
||||
|
||||
:expected type: Optional[EntryFormatter]
|
||||
|
|
@ -655,6 +684,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
as ``"Season {upload_year}/{title}.{info_json_ext}"``, and will be placed in the output
|
||||
directory. Can be set to empty string or `null` to disable info json writes.
|
||||
|
||||
|
||||
``keep_files_after``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -666,6 +696,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
files after ``19000101``, which implies all files. Can be used in conjunction with
|
||||
``keep_max_files``.
|
||||
|
||||
|
||||
``keep_files_before``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -677,6 +708,7 @@ 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
|
||||
|
|
@ -686,6 +718,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
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]
|
||||
|
|
@ -695,6 +728,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
Only keeps N most recently uploaded videos. If set to <= 0, ``keep_max_files`` will not be
|
||||
applied. Can be used in conjunction with ``keep_files_before`` and ``keep_files_after``.
|
||||
|
||||
|
||||
``maintain_download_archive``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
|
|
@ -709,6 +743,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
|
||||
Defaults to False.
|
||||
|
||||
|
||||
``migrated_download_archive_name``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -718,19 +753,13 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
name first, and fallback to ``download_archive_name``. It will always save to this file
|
||||
and remove the original ``download_archive_name``.
|
||||
|
||||
|
||||
``output_directory``
|
||||
|
||||
:expected type: OverridesFormatter
|
||||
:description:
|
||||
The output directory to store all media files downloaded.
|
||||
|
||||
``preserve_mtime``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
:description:
|
||||
Preserve the video's original upload time as the file modification time.
|
||||
When True, sets the file's mtime to match the video's upload_date from
|
||||
yt-dlp metadata. Defaults to False.
|
||||
|
||||
``thumbnail_name``
|
||||
|
||||
|
|
@ -740,6 +769,7 @@ Defines where to output files and thumbnails after all post-processing has compl
|
|||
as ``"Season {upload_year}/{title}.{thumbnail_ext}"``, and will be placed in the output
|
||||
directory. Can be set to empty string or `null` to disable thumbnail writes.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
overrides
|
||||
|
|
@ -804,6 +834,7 @@ used with no modifications.
|
|||
If a file has no chapters and is set to "pass", then ``chapter_title`` is
|
||||
set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
square_thumbnail
|
||||
|
|
@ -850,6 +881,7 @@ Usage:
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``kodi_safe``
|
||||
|
||||
:expected type: OverridesBooleanFormatterValidator
|
||||
|
|
@ -858,12 +890,14 @@ Usage:
|
|||
emojis and some foreign language characters. Setting this to True will replace those
|
||||
characters with '□'.
|
||||
|
||||
|
||||
``nfo_name``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
:description:
|
||||
The NFO file name.
|
||||
|
||||
|
||||
``nfo_root``
|
||||
|
||||
:expected type: EntryFormatter
|
||||
|
|
@ -876,6 +910,7 @@ Usage:
|
|||
<season>
|
||||
</season>
|
||||
|
||||
|
||||
``tags``
|
||||
|
||||
:expected type: NfoTags
|
||||
|
|
@ -889,6 +924,7 @@ Usage:
|
|||
<title>My custom season name!</title>
|
||||
</season>
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
subtitles
|
||||
|
|
@ -916,6 +952,7 @@ It will set the respective language to the correct subtitle file.
|
|||
:description:
|
||||
Defaults to False. Whether to allow auto generated subtitles.
|
||||
|
||||
|
||||
``embed_subtitles``
|
||||
|
||||
:expected type: Optional[Boolean]
|
||||
|
|
@ -923,6 +960,7 @@ It will set the respective language to the correct subtitle file.
|
|||
Defaults to False. Whether to embed the subtitles into the video file. Note that
|
||||
webm files can only embed "vtt" subtitle types.
|
||||
|
||||
|
||||
``enable``
|
||||
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
|
|
@ -931,6 +969,7 @@ It will set the respective language to the correct subtitle file.
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``languages``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
|
|
@ -938,12 +977,6 @@ It will set the respective language to the correct subtitle file.
|
|||
Language code(s) to download for subtitles. Supports a single or list of multiple
|
||||
language codes. Defaults to only "en".
|
||||
|
||||
``languages_required``
|
||||
|
||||
:expected type: Optional[List[String]]
|
||||
:description:
|
||||
Language code(s) that are required to be present for downloads to continue. If missing,
|
||||
ytdl-sub will throw an error. NOTE: currently this only checks file-based subtitles.
|
||||
|
||||
``subtitles_name``
|
||||
|
||||
|
|
@ -954,12 +987,14 @@ It will set the respective language to the correct subtitle file.
|
|||
and will be placed in the output directory. ``lang`` is dynamic since you can download
|
||||
multiple subtitles. It will set the respective language to the correct subtitle file.
|
||||
|
||||
|
||||
``subtitles_type``
|
||||
|
||||
:expected type: Optional[String]
|
||||
:description:
|
||||
Defaults to "srt". One of the subtitle file types "srt", "vtt", "ass", "lrc".
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
throttle_protection
|
||||
|
|
@ -968,9 +1003,6 @@ Provides options to make ytdl-sub look more 'human-like' to protect from throttl
|
|||
range-based values, a random number will be chosen within the range to avoid sleeps looking
|
||||
scripted.
|
||||
|
||||
Range min and max values support static override variables within their definitions.
|
||||
``sleep_per_download_s`` supports both static and override variables.
|
||||
|
||||
:Usage:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
|
@ -1000,12 +1032,14 @@ Range min and max values support static override variables within their definiti
|
|||
this field can be set using an override variable to easily toggle whether this plugin
|
||||
is enabled or not via Boolean.
|
||||
|
||||
|
||||
``max_downloads_per_subscription``
|
||||
|
||||
:expected type: Optional[Range]
|
||||
:description:
|
||||
Number of downloads to perform per subscription.
|
||||
|
||||
|
||||
``sleep_per_download_s``
|
||||
|
||||
:expected type: Optional[Range]
|
||||
|
|
@ -1013,6 +1047,7 @@ Range min and max values support static override variables within their definiti
|
|||
Number in seconds to sleep between each download. Does not include time it takes for
|
||||
ytdl-sub to perform post-processing.
|
||||
|
||||
|
||||
``sleep_per_request_s``
|
||||
|
||||
:expected type: Optional[Range]
|
||||
|
|
@ -1022,12 +1057,14 @@ Range min and max values support static override variables within their definiti
|
|||
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]
|
||||
:description:
|
||||
Number in seconds to sleep between each subscription.
|
||||
|
||||
|
||||
``subscription_download_probability``
|
||||
|
||||
:expected type: Optional[Float]
|
||||
|
|
@ -1036,6 +1073,7 @@ Range min and max values support static override variables within their definiti
|
|||
recommended to set if you run ytdl-sub in a cron-job, that way you are statistically
|
||||
guaranteed over time to eventually download the subscription.
|
||||
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
video_tags
|
||||
|
|
|
|||
|
|
@ -4,30 +4,22 @@ Common
|
|||
|
||||
.. highlight:: yaml
|
||||
|
||||
|
||||
Filter Keywords
|
||||
---------------
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml
|
||||
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/filter_keywords.yaml
|
||||
|
||||
Filter Duration
|
||||
---------------
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml
|
||||
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/filter_duration.yaml
|
||||
|
||||
Media Quality
|
||||
-------------
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/helpers/media_quality.yaml
|
||||
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/media_quality.yaml
|
||||
|
||||
Only Recent Videos
|
||||
------------------
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/helpers/download_deletion_options.yaml
|
||||
|
|
@ -2,10 +2,11 @@
|
|||
Prebuilt Preset Reference
|
||||
=========================
|
||||
|
||||
This section contains the code for the prebuilt presets. If you just want to understand
|
||||
how to use the presets, check :doc:`this section instead</prebuilt_presets/index>`.
|
||||
This section contains the code for the prebuilt presets. If you just want to understand how to use the presets, check :doc:`this section instead</prebuilt_presets/index>`.
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
common
|
||||
tv_show
|
||||
music
|
||||
music
|
||||
|
|
@ -6,5 +6,4 @@ All audio music based presets inherit from ``_music_base``.
|
|||
|
||||
.. highlight:: yaml
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/music/singles.yaml
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/music/singles.yaml
|
||||
|
|
@ -6,5 +6,4 @@ All TV show based presets inherit from ``_episode_base``.
|
|||
|
||||
.. highlight:: yaml
|
||||
|
||||
.. literalinclude::
|
||||
/../../src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml
|
||||
.. literalinclude:: /../../src/ytdl_sub/prebuilt_presets/tv_show/episode.yaml
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
Scripting
|
||||
=========
|
||||
|
||||
``ytdl-sub`` fields (file-names, tags, etc) are defined using variables and scripts. The
|
||||
links below contain reference documentation for each built-in variable and scripting
|
||||
function.
|
||||
``ytdl-sub`` fields (file-names, tags, etc) are defined using variables and scripts. The links below
|
||||
contain reference documentation for each built-in variable and scripting function.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
|
@ -14,7 +13,6 @@ function.
|
|||
scripting_functions
|
||||
scripting_types
|
||||
|
||||
|
||||
How it Works
|
||||
------------
|
||||
|
||||
|
|
@ -46,22 +44,22 @@ We can use this instead of hard-coding it above:
|
|||
output_directory: "/path/to/tv_shows/{subscription_name}"
|
||||
|
||||
The syntax for variable usage is curly-braces with the variable name within it. Assuming
|
||||
our subscription is actually named "Custom YTDL-SUB TV Show", then ``ytdl-sub`` will
|
||||
actually write to that directory.
|
||||
our subscription is actually named "Custom YTDL-SUB TV Show", then ``ytdl-sub``
|
||||
will actually write to that directory.
|
||||
|
||||
Entry Variables
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
For context, an *entry* is a video or audio file downloaded from ``yt-dlp``. *Entry
|
||||
variables* are variables that are derived from an entry's ``info.json`` file. This file
|
||||
For context, an *entry* is a video or audio file downloaded from ``yt-dlp``.
|
||||
*Entry variables* are variables that are derived from an entry's ``info.json`` file. This file
|
||||
comes from ``yt-dlp`` and contains every piece of metadata that it scraped.
|
||||
|
||||
These variables are not considered static since they change per entry download. There
|
||||
are a few fields in ``ytdl-sub`` (i.e. ``output_directory``) that must be static. For
|
||||
others, we are free to use values that derive from an entry.
|
||||
These variables are not considered static since they change per entry download. There are a
|
||||
few fields in ``ytdl-sub`` (i.e. ``output_directory``) that must be static. For others,
|
||||
we are free to use values that derive from an entry.
|
||||
|
||||
Suppose we want to customize the name of an entry's output file and thumbnail to include
|
||||
its title in its name. We can do that using entry variables:
|
||||
Suppose we want to customize the name of an entry's output file and thumbnail to include its
|
||||
title in its name. We can do that using entry variables:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -76,8 +74,8 @@ Creating Custom Variables
|
|||
Suppose we want to include the date in our file names. This means we'd need to update
|
||||
both the ``file_name`` and ``thumbnail_name`` fields to include it.
|
||||
|
||||
Instead, we can create a custom *override variable*. This is ``ytdl-sub``'s method for
|
||||
creating and overriding custom variables.
|
||||
Instead, we can create a custom *override variable*. This is ``ytdl-sub``'s method
|
||||
for creating and overriding custom variables.
|
||||
|
||||
These are created in the ``overrides`` section. Let's take our above example and create
|
||||
a ``custom_file_name`` variable to use for the entry file and thumbnail fields:
|
||||
|
|
@ -99,9 +97,9 @@ For experienced ``yt-dlp`` scrapers, you may be thinking:
|
|||
|
||||
- What if the title has characters that do not play nice with my operating system?
|
||||
|
||||
``ytdl-sub`` is able to *sanitize* any variable, meaning it replaces any problematic
|
||||
characters with safe alternatives that can be used in file names. We can ensure our file
|
||||
names and directories are safe by using:
|
||||
``ytdl-sub`` is able to *sanitize* any variable, meaning it replaces any problematic characters
|
||||
with safe alternatives that can be used in file names. We can ensure our file names and directories
|
||||
are safe by using:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -118,16 +116,16 @@ Simply add a ``_sanitized`` suffix to any variable name to make it sanitized.
|
|||
.. note::
|
||||
|
||||
Make sure you do not sanitize custom variables that intentionally create directories,
|
||||
(i.e. sanitizing ``/path/to/tv_shows/``) otherwise they will... be sanitized and not
|
||||
resolve to directories!
|
||||
(i.e. sanitizing ``/path/to/tv_shows/``) otherwise they will... be sanitized and not resolve to
|
||||
directories!
|
||||
|
||||
|
||||
Using Scripting Functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Let's suppose you are an avid command-line user, and like all of your file names to be
|
||||
``snake_cased_with_no_spaces``. We can use the `replace
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#replace>`_
|
||||
``snake_cased_with_no_spaces``. We can use the
|
||||
`replace <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#replace>`_
|
||||
*scripting function* to create and use a snake-cased title.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
|
@ -145,48 +143,42 @@ Let's suppose you are an avid command-line user, and like all of your file names
|
|||
custom_file_name: "{upload_date_standardized}_{snake_cased_title_sanitized}"
|
||||
|
||||
Scripting functions are similar to variables - they must be used within curly-braces.
|
||||
It is good practice to use ``>-`` when defining variables that use functions. It is
|
||||
YAML's way of saying:
|
||||
It is good practice to use ``>-`` when defining variables that use functions. It is YAML's way of
|
||||
saying:
|
||||
|
||||
- Allow a string to be multi-lined, and do not include newlines before or after it.
|
||||
|
||||
See for yourself `here
|
||||
<https://yaml-online-parser.appspot.com/?yaml=output_options%3A%0A%20%20output_directory%3A%20%22%7Bsubscription_name_sanitized%7D%22%0A%20%20file_name%3A%20%22%7Bcustom_file_name%7D.%7Bext%7D%22%0A%20%20thumbnail_name%3A%20%22%7Bcustom_file_name%7D.%7Bthumbnail_ext%7D%22%0A%0Aoverrides%3A%0A%20%20snake_cased_title%3A%20%3E-%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%25replace%28%20title%2C%20%27%20%27%2C%20%27_%27%20%29%0A%20%20%20%20%7D%0A%20%20custom_file_name%3A%20%22%7Bupload_date_standardized%7D%20%7Bsnake_cased_title_sanitized%7D%22&type=json>`_.
|
||||
Any whitespace within curly-braces is okay since it will be parsed out. This is needed
|
||||
to make scripting function usage readable.
|
||||
See for yourself `here <https://yaml-online-parser.appspot.com/?yaml=output_options%3A%0A%20%20output_directory%3A%20%22%7Bsubscription_name_sanitized%7D%22%0A%20%20file_name%3A%20%22%7Bcustom_file_name%7D.%7Bext%7D%22%0A%20%20thumbnail_name%3A%20%22%7Bcustom_file_name%7D.%7Bthumbnail_ext%7D%22%0A%0Aoverrides%3A%0A%20%20snake_cased_title%3A%20%3E-%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%25replace%28%20title%2C%20%27%20%27%2C%20%27_%27%20%29%0A%20%20%20%20%7D%0A%20%20custom_file_name%3A%20%22%7Bupload_date_standardized%7D%20%7Bsnake_cased_title_sanitized%7D%22&type=json>`_.
|
||||
Any whitespace within curly-braces is okay since it will be parsed out. This is needed to make
|
||||
scripting function usage readable.
|
||||
|
||||
.. important::
|
||||
|
||||
It is important to use ``>-`` over other YAML new-line directives like ``>`` because
|
||||
they add newlines before or after curly-braces, and will be included in your
|
||||
variable's output string.
|
||||
|
||||
It is important to use ``>-`` over other YAML new-line directives like ``>`` because they
|
||||
add newlines before or after curly-braces, and will be included in your variable's output string.
|
||||
|
||||
Advanced Scripting
|
||||
------------------
|
||||
|
||||
Accessing ``info.json`` Fields
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The entirety of an entry's ``info.json`` file resides in the
|
||||
`Map <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_types.html#map>`_
|
||||
variable
|
||||
`entry_metadata <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/entry_variables.html#entry-metadata>`_.
|
||||
|
||||
The entirety of an entry's ``info.json`` file resides in the `Map
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_types.html#map>`_
|
||||
variable `entry_metadata
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/entry_variables.html#entry-metadata>`_.
|
||||
|
||||
Any field can be accessed by using the `map_get
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
|
||||
Any field can be accessed by using the
|
||||
`map_get <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
|
||||
function like so:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption:
|
||||
Fetches the 'artist' value from the .info.json, returns null if it does not exist.
|
||||
:caption: Fetches the 'artist' value from the .info.json, returns null if it does not exist.
|
||||
|
||||
artist: >-
|
||||
{ %map_get( entry_metadata, "artist", null ) }
|
||||
|
||||
Creating Custom Functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Custom functions can be created in the overrides section using the following syntax:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
|
@ -195,9 +187,9 @@ Custom functions can be created in the overrides section using the following syn
|
|||
"%get_entry_metadata_field": >-
|
||||
{ %map_get( entry_metadata, $0, null ) }
|
||||
|
||||
Custom function definitions must have ``%`` as a prefix to the function name, be
|
||||
surrounded by quotes to make YAML parsing happy, and can support arguments using ``$0``,
|
||||
``$1``, ... to indicate their first argument, second argument, etc.
|
||||
Custom function definitions must have ``%`` as a prefix to the function name, be surrounded by
|
||||
quotes to make YAML parsing happy, and can support arguments using ``$0``, ``$1``, ... to indicate
|
||||
their first argument, second argument, etc.
|
||||
|
||||
Using our new custom function, we can simply the ``artist`` variable definition above to:
|
||||
|
||||
|
|
|
|||
|
|
@ -521,13 +521,6 @@ pow
|
|||
:description:
|
||||
``**`` operator. Returns the exponential of the base and exponent value.
|
||||
|
||||
range
|
||||
~~~~~
|
||||
:spec: ``range(end: Integer, start: Optional[Integer], step: Optional[Integer]) -> Array``
|
||||
|
||||
:description:
|
||||
Returns the desired range of Integers in the form of an Array.
|
||||
|
||||
sub
|
||||
~~~
|
||||
:spec: ``sub(values: Numeric, ...) -> Numeric``
|
||||
|
|
@ -545,26 +538,27 @@ 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 the ``message`` and return ``passthrough``.
|
||||
Optionally can pass level, where < 0 is debug, 0 is info, 1 is warning, > 1 is error.
|
||||
Defaults to info.
|
||||
|
||||
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 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.
|
||||
Defaults to info.
|
||||
|
||||
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``)
|
||||
Print 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.
|
||||
Defaults to info.
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -837,7 +831,7 @@ behavior.
|
|||
|
||||
sanitize
|
||||
~~~~~~~~
|
||||
:spec: ``sanitize(value: AnyArgument, ...) -> String``
|
||||
:spec: ``sanitize(value: AnyArgument) -> String``
|
||||
|
||||
Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use
|
||||
for file/directory names on any OS.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
===============
|
||||
|
||||
Scripting Types
|
||||
===============
|
||||
|
||||
|
||||
Types
|
||||
-----
|
||||
|
||||
|
|
@ -17,9 +16,8 @@ Strings are a series of characters surrounded by quotes.
|
|||
|
||||
.. note::
|
||||
|
||||
For non-String types, they must be defined as parameters to scripting functions. This
|
||||
is because anything in a variable definition that is not within curly-braces gets
|
||||
evaluated as a String.
|
||||
For non-String types, they must be defined as parameters to scripting functions. This is because
|
||||
anything in a variable definition that is not within curly-braces gets evaluated as a String.
|
||||
|
||||
We can define Strings within curly-braces by setting them as parameters to a function:
|
||||
|
||||
|
|
@ -67,8 +65,8 @@ There are a few ways to make variables that use curly braces more compact, inclu
|
|||
|
||||
string_variable: "{ %string('This is a String variable') }"
|
||||
|
||||
In the case that you want to define a string variable that contains both single and
|
||||
double quotes, triple-quotes can be used to avoid *closing* the String.
|
||||
In the case that you want to define a string variable that contains both single and double quotes,
|
||||
triple-quotes can be used to avoid *closing* the String.
|
||||
|
||||
.. tab-set::
|
||||
|
||||
|
|
@ -90,8 +88,7 @@ double quotes, triple-quotes can be used to avoid *closing* the String.
|
|||
%string("""This has both " and ' in it.""")
|
||||
}
|
||||
|
||||
If you want a plain string that contains literal curly braces, you can escape them like
|
||||
so:
|
||||
If you want a plain string that contains literal curly braces, you can escape them like so:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -188,8 +185,8 @@ A type is considered boolean if it spells out ``True`` or ``False``, case-insens
|
|||
Array
|
||||
~~~~~
|
||||
|
||||
An Array contains multiple types of any kind, including nested Arrays and Maps. Arrays
|
||||
are defined using brackets (``[ ]``), and are accessed using zero-based indexing.
|
||||
An Array contains multiple types of any kind, including nested Arrays and Maps.
|
||||
Arrays are defined using brackets (``[ ]``), and are accessed using zero-based indexing.
|
||||
|
||||
.. tab-set::
|
||||
|
||||
|
|
@ -230,8 +227,8 @@ are defined using brackets (``[ ]``), and are accessed using zero-based indexing
|
|||
Map
|
||||
~~~
|
||||
|
||||
A Map is a key-value store, containing mappings between keys and values. Maps are
|
||||
defined using curly-braces (``{ }``), and are accessed using their keys.
|
||||
A Map is a key-value store, containing mappings between keys and values.
|
||||
Maps are defined using curly-braces (``{ }``), and are accessed using their keys.
|
||||
|
||||
.. tab-set::
|
||||
|
||||
|
|
@ -270,7 +267,6 @@ defined using curly-braces (``{ }``), and are accessed using their keys.
|
|||
|
||||
Null
|
||||
~~~~
|
||||
|
||||
Null is represented by an empty String, and can be conveyed by spelling out ``null``,
|
||||
case-insensitive.
|
||||
|
||||
|
|
@ -301,9 +297,7 @@ Function Type-Hints
|
|||
|
||||
AnyArgument
|
||||
~~~~~~~~~~~
|
||||
|
||||
AnyArgument means any of the above Types are valid as input or output to a scripting
|
||||
function.
|
||||
AnyArgument means any of the above Types are valid as input or output to a scripting function.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
@ -312,15 +306,13 @@ function.
|
|||
|
||||
Numeric
|
||||
~~~~~~~
|
||||
|
||||
Numeric refers to either an Integer or Float.
|
||||
|
||||
Optional
|
||||
~~~~~~~~
|
||||
|
||||
Optional means a particular scripting function argument can be either provided or not
|
||||
included. For example, the function `map_get
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
|
||||
Optional means a particular scripting function argument can be either provided or not included.
|
||||
For example, the function
|
||||
`map_get <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
|
||||
has an optional default value. Both of these usages are valid:
|
||||
|
||||
.. tab-set::
|
||||
|
|
@ -339,9 +331,8 @@ has an optional default value. Both of these usages are valid:
|
|||
|
||||
Lambda
|
||||
~~~~~~
|
||||
|
||||
Lambda parameters are a reference to a function, and will call that lambda function on
|
||||
the input. In this example,
|
||||
Lambda parameters are a reference to a function, and will call that lambda function
|
||||
on the input. In this example,
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -350,23 +341,20 @@ the input. In this example,
|
|||
%array_apply( [ 1, 2, 3, 4], %string )
|
||||
}
|
||||
|
||||
We apply ``%string`` as a lambda function to `array_apply
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-apply>`_,
|
||||
which is called on every element in the input array. The output becomes ``["1", "2",
|
||||
"3", "4"]``.
|
||||
We apply ``%string`` as a lambda function to
|
||||
`array_apply <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-apply>`_,
|
||||
which is called on every element in the input array. The output becomes ``["1", "2", "3", "4"]``.
|
||||
|
||||
This example has one input-argument being passed into the lambda. For other lambda-based
|
||||
functions like `array_enumerate
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-enumerate>`_,
|
||||
This example has one input-argument being passed into the lambda. For other lambda-based functions
|
||||
like `array_enumerate <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-enumerate>`_,
|
||||
it expects the lambda function to have two input arguments. These are denoted using
|
||||
``LambdaTwo``, ``LambdaThree``, etc within the function spec.
|
||||
|
||||
LambdaReduce
|
||||
~~~~~~~~~~~~
|
||||
|
||||
LambdaReduce parameters are a reference to a function that will perform a *reduce* - an
|
||||
operation that reduces an Array to a single value by calling the LambdaReduce function
|
||||
repeatedly on two elements in the Array until it is reduced to a single value.
|
||||
LambdaReduce parameters are a reference to a function that will perform a *reduce* - an operation
|
||||
that reduces an Array to a single value by calling the LambdaReduce function repeatedly on two
|
||||
elements in the Array until it is reduced to a single value.
|
||||
|
||||
In this example,
|
||||
|
||||
|
|
@ -377,12 +365,11 @@ In this example,
|
|||
%array_reduce( [ 1, 2, 3, 4], %add )
|
||||
}
|
||||
|
||||
We call `array_reduce
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-reduce>`_
|
||||
on the input array, using `add
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#add>`_
|
||||
as the LambdaReduce function. This will reduce the Array to a single value by internally
|
||||
calling
|
||||
We call
|
||||
`array_reduce <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#array-reduce>`_
|
||||
on the input array, using
|
||||
`add <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#add>`_
|
||||
as the LambdaReduce function. This will reduce the Array to a single value by internally calling
|
||||
|
||||
- *reduce-call 1*: ``%add(1, 2) = 3`` (first two elements)
|
||||
- *reduce-call 2*: ``%add(3, 3) = 6`` (output from first two and third element)
|
||||
|
|
@ -393,10 +380,9 @@ And evaluate to ``10``.
|
|||
ReturnableArguments
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Returnable arguments are used in conditional functions like `if
|
||||
<https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#if>`_,
|
||||
which implies the argument passed into the function is the function's output. For
|
||||
example,
|
||||
Returnable arguments are used in conditional functions like
|
||||
`if <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#if>`_,
|
||||
which implies the argument passed into the function is the function's output. For example,
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -405,4 +391,4 @@ example,
|
|||
%if( True, "Return this if True", "Return this if False" )
|
||||
}
|
||||
|
||||
is going to return ``"Return this if True"`` since the condition parameter is ``True``.
|
||||
is going to return ``"Return this if True"`` since the condition parameter is ``True``.
|
||||
|
|
@ -31,21 +31,16 @@ otherwise.
|
|||
|
||||
subscription_indent_i
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
For subscriptions where the ancestor keys contain the ``= ...`` prefix, the
|
||||
variables ``subscription_indent_1``, ``subscription_indent_2``, and so on get
|
||||
set to each subsequent value. For example, given the following subscriptions
|
||||
file snippet:
|
||||
For subscriptions in the form of
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Preset 1 | = Indent Value 1 | Preset 2:
|
||||
Preset 3 | = Indent Value 2 | Preset 4:
|
||||
Preset | = Indent Value 1:
|
||||
= Indent Value 2:
|
||||
"Subscription Name": "https://..."
|
||||
|
||||
The ``{subscription_indent_1}`` variable will be ``Indent Value 1`` and
|
||||
``{subscription_indent_2}`` will be ``Indent Value 2``. The most common use of
|
||||
these variables is to :doc:`set the genre and rating for subscriptions from the
|
||||
YAML keys <../prebuilt_presets/tv_show>`.
|
||||
``subscription_indent_1`` and ``subscription_indent_2`` get set to
|
||||
``Indent Value 1`` and ``Indent Value 2``.
|
||||
|
||||
subscription_map
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -2,22 +2,19 @@
|
|||
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 <guides/getting_started/index:Getting Started>`
|
||||
first before reviewing this section.
|
||||
|
||||
|
||||
File Preset
|
||||
-----------
|
||||
|
||||
Many examples show ``__preset__`` at the top. This is known as the *subscription file
|
||||
preset*. It is where a single :ref:`preset <guides/getting_started/first_config:Custom
|
||||
Preset Definition>` can be defined that gets applied to each subscription within the
|
||||
file.
|
||||
Many examples show ``__preset__`` at the top. This is known as the *subscription file preset*.
|
||||
It is where a single :ref:`preset <guides/getting_started/first_config:Custom Preset Definition>`
|
||||
can be defined that gets applied to each subscription within the file.
|
||||
|
||||
This is a good place to apply file-wide variables such as ``tv_show_directory`` or
|
||||
supply a cookies file path.
|
||||
|
|
@ -25,17 +22,15 @@ supply a cookies file path.
|
|||
.. code-block:: yaml
|
||||
|
||||
__preset__:
|
||||
# Variables that override defaults from `overrides:` for presets in YAML keys:
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
|
||||
# Directly set plugin options:
|
||||
ytdl_options:
|
||||
cookiefile: "/config/ytdl-sub-configs/cookie.txt"
|
||||
cookiefile: "/config/cookie.txt"
|
||||
|
||||
|
||||
Layout
|
||||
------
|
||||
|
||||
A subscription file is comprised of YAML keys and values. Keys can be either
|
||||
|
||||
- a preset
|
||||
|
|
@ -57,33 +52,32 @@ All three types of keys are used for the following:
|
|||
- ``= News`` - an override value for genre
|
||||
- ``Breaking News``, ``BBC News`` - The subscription names
|
||||
|
||||
The lowest level, most indented keys should always be the subscription name. It is good
|
||||
practice to put subscription names in quotes to differentiate between preset names and
|
||||
subscription names.
|
||||
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
|
||||
|
||||
|
|
@ -91,16 +85,16 @@ defined on a single line. The following is equivalent to the above 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 <prebuilt_presets/helpers:Only Recent>`
|
||||
preset to the above examples. But for ``BBC News`` specifically, we want to set the date
|
||||
range to be different than the default ``2months`` value to ``2weeks``.
|
||||
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:
|
||||
|
||||
|
|
@ -115,14 +109,13 @@ 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 <config_reference/scripting/static_variables: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 <config_reference/scripting/static_variables:Subscription Variables>`.
|
||||
|
||||
Map Mode
|
||||
--------
|
||||
|
||||
Map mode is for highly advanced presets that benefit from a more complex subscription
|
||||
definition. TODO: Show music video example here.
|
||||
Map mode is for highly advanced presets that benefit
|
||||
from a more complex subscription definition. TODO: Show music video
|
||||
example here.
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
Debugging
|
||||
=========
|
||||
|
||||
Run with ``--log-level debug`` to show all log messages, often too much information for
|
||||
normal operation but useful when investigating a specific problem.
|
||||
|
||||
:ref:`ytdl-sub builds on yt-dlp <introduction:motivation>`, which is in itself a complex
|
||||
tool. It performs an intricate and fragile task, web scraping, which in turn :ref:`is
|
||||
subject to the whims of external services <guides/getting_started/index:minimize the
|
||||
work to only what's necessary>` outside its control. Finally, because :ref:`ytdl-sub is
|
||||
a lower-level tool <guides/getting_started/index:prerequisite knowledge>`, many users,
|
||||
if not most, will have problems getting their configuration working and it can be
|
||||
difficult to determine when the root cause is their configuration, just a limit imposed
|
||||
by the services, or, least likely, a bug in one of the tools involved.
|
||||
|
||||
To expedite resolution and conserve the limited resources of both yourself and
|
||||
volunteers, do as much investigation yourself as you can:
|
||||
|
||||
#. Start by assuming the issue is your configuration:
|
||||
|
||||
Review :doc:`the guides <./guides/index>` to confirm your understanding. Increase
|
||||
output using the ``--log-level`` CLI option and read the output carefully for hints
|
||||
and clues. Use those clues to `search the docs`_. Read :doc:`the reference docs
|
||||
<./config_reference/index>` of the involved ``ytdl-sub`` components.
|
||||
|
||||
#. Try to determine if the issue is happening in ``yt-dlp`` or ``ytdl-sub``:
|
||||
|
||||
The user's configuration tells ``ytdl-sub`` how to run ``yt-dlp``. ``yt-dlp`` handles
|
||||
all the web scraping and downloading. ``ytdl-sub`` then assembles the files and metadata
|
||||
produced by ``yt-dlp`` and places them in your library.
|
||||
|
||||
If the issue is happening while scraping or downloading from the external service,
|
||||
then it's happening in the running of ``yt-dlp``. Look for output showing failed
|
||||
downloads, ``403`` errors, or signs of throttles. That doesn't mean it's a bug in
|
||||
``yt-dlp``, it could be in how your configuration tells ``ytdl-sub`` to run
|
||||
``yt-dlp`` or limits imposed by the service that are constantly changing, but you may
|
||||
be able to find answers from other ``yt-dlp`` users running into similar issues.
|
||||
|
||||
See `the yt-dlp known issues`_ and `search their issues`_ for clues and hints. Read
|
||||
the comments for more understanding, workarounds, and maybe even fixes. If you still
|
||||
don't understand the cause after reading everything you can find there, try to find
|
||||
help in `the yt-dlp Discord`_.
|
||||
|
||||
#. If the issue is happening in ``ytdl-sub``, reach out for help:
|
||||
|
||||
Once you've done everything you can to get your configuration working and you've
|
||||
determined that the issue isn't happening in ``yt-dlp``, look for answers in
|
||||
``ytdl-sub``:
|
||||
|
||||
#. Cut your configuration and subscriptions down to the minimum that reproduces the
|
||||
issue.
|
||||
|
||||
#. Run with the ``--log-level debug`` CLI option and copy the full output.
|
||||
|
||||
#. `Search the ytdl-sub issues`_ using clues and hints from the output.
|
||||
|
||||
#. `Open a support post in Discord`_ with those details and all other relevant
|
||||
details.
|
||||
|
||||
#. If someone from the Discord discussion directs you to, then `open a new issue`_
|
||||
with those same details.
|
||||
|
||||
|
||||
.. _`the yt-dlp known issues`:
|
||||
https://github.com/yt-dlp/yt-dlp/wiki/FAQ#known-issues
|
||||
.. _`search their issues`:
|
||||
https://github.com/yt-dlp/yt-dlp/issues
|
||||
.. _`the yt-dlp Discord`:
|
||||
https://discord.gg/H5MNcFW63r
|
||||
|
||||
.. _`search the docs`:
|
||||
https://ytdl-sub.readthedocs.io/en/latest/search.html
|
||||
.. _`search the ytdl-sub issues`:
|
||||
https://github.com/jmbannon/ytdl-sub/issues
|
||||
.. _`open a support post in Discord`:
|
||||
https://discord.com/channels/994270357957648404/1084886228266127460
|
||||
.. _`open a new issue`:
|
||||
https://github.com/jmbannon/ytdl-sub/issues/new
|
||||
|
|
@ -1,25 +1,14 @@
|
|||
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
|
||||
|
|
@ -51,16 +40,13 @@ created to replicate the plugin's behavior. See the following converted example:
|
|||
}
|
||||
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
|
||||
---------
|
||||
|
|
@ -68,9 +54,8 @@ July 2023
|
|||
music_tags
|
||||
~~~~~~~~~~
|
||||
|
||||
Music tags are getting simplified. ``tags`` will now reside directly under music_tags,
|
||||
and ``embed_thumbnail`` is getting moved to its own plugin (supports video files as
|
||||
well). Convert from:
|
||||
Music tags are getting simplified. ``tags`` will now reside directly under music_tags, and
|
||||
``embed_thumbnail`` is getting moved to its own plugin (supports video files as well). Convert from:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -94,8 +79,8 @@ The old format will be removed in October 2023.
|
|||
video_tags
|
||||
~~~~~~~~~~
|
||||
|
||||
Video tags are getting simplified as well. ``tags`` will now reside directly under
|
||||
video_tags. Convert from:
|
||||
Video tags are getting simplified as well. ``tags`` will now reside directly under video_tags.
|
||||
Convert from:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,45 @@
|
|||
FAQ
|
||||
===
|
||||
|
||||
Since ytdl-sub is relatively new to the public, there has not been many question asked
|
||||
yet. We will update this as more questions get asked.
|
||||
Since ytdl-sub is relatively new to the public, there has not been many question asked yet. We will update this as more questions get asked.
|
||||
|
||||
.. contents:: Frequently Asked Questions
|
||||
:depth: 3
|
||||
|
||||
|
||||
How do I...
|
||||
-----------
|
||||
|
||||
...remove the date in the video title?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`config_reference/prebuilt_presets/tv_show:TV Show` presets by default include
|
||||
the upload date in the ``episode_title`` override variable. This variable is used to set
|
||||
the title in things like the video metadata, NFO file, etc, which is subsequently read
|
||||
by media players. This can be overwritten as you see fit by redefining it:
|
||||
The :ref:`config_reference/prebuilt_presets/tv_show:TV Show` presets by default include the upload date in the ``episode_title``
|
||||
override variable. This variable is used to set the title in things like the video metadata, NFO file, etc, which is
|
||||
subsequently read by media players. This can be overwritten as you see fit by redefining it:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
overrides:
|
||||
episode_title: "{title}" # Only sets the video title
|
||||
|
||||
...get support or reach out to contribute?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you need support, you can:
|
||||
|
||||
* :ytdl-sub-gh:`Open an issue on GitHub <issues/new>`
|
||||
|
||||
* `Join our Discord <https://discord.gg/v8j9RAHb4k>`_
|
||||
|
||||
If you would like to contribute, we're happy to accept any help, even non-coders! To find out how you can help this project, you can:
|
||||
|
||||
* `Join our Discord <https://discord.gg/v8j9RAHb4k>`_ and leave a comment in #development with where you think you can assist or what skills you would like to contribute.
|
||||
|
||||
* If you just want to fix one thing, you're welcome to :ytdl-sub-gh:`submit a pull request <compare>` with information on what issue you're resolving and it will be reviewed as soon as possible.
|
||||
|
||||
...download age-restricted YouTube videos?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See `yt-dl's recommended way
|
||||
<https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl>`_ to
|
||||
download your YouTube cookie, then add it to your :ref:`ytdl options
|
||||
<config_reference/plugins:ytdl_options>` section of your config:
|
||||
See `yt-dl's recommended way <https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl>`_ to download your YouTube cookie, then add it to your :ref:`ytdl options <config_reference/plugins:ytdl_options>` section of your config:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -41,8 +50,7 @@ download your YouTube cookie, then add it to your :ref:`ytdl options
|
|||
...automate my downloads?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:doc:`This page </guides/getting_started/automating_downloads>` shows how to set up
|
||||
``ytdl-sub`` to run automatically on various platforms.
|
||||
:doc:`This page </guides/getting_started/automating_downloads>` shows how to set up ``ytdl-sub`` to run automatically on various platforms.
|
||||
|
||||
...download large channels?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -57,8 +65,7 @@ See the prebuilt preset :doc:`Filter Keywords </prebuilt_presets/helpers>`.
|
|||
...prevent creation of NFO file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Creation of NFO files is done by the NFO tags plugin. It, as any other plugin, can be
|
||||
disabled:
|
||||
Creation of NFO files is done by the NFO tags plugin. It, as any other plugin, can be disabled:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -68,9 +75,8 @@ disabled:
|
|||
...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:
|
||||
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
|
||||
|
||||
|
|
@ -82,11 +88,10 @@ overriding following variables:
|
|||
...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:
|
||||
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``.
|
||||
* 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``.
|
||||
|
|
@ -112,9 +117,7 @@ There are several solutions using ytdl-sub's scripting capabilities to override
|
|||
}
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption:
|
||||
Regex capture. Supports multiple capture strings and default values if captures
|
||||
are unsuccessful.
|
||||
:caption: Regex capture. Supports multiple capture strings and default values if captures are unsuccessful.
|
||||
|
||||
"~Nova PBS":
|
||||
url: "https://www.youtube.com/@novapbs"
|
||||
|
|
@ -129,132 +132,44 @@ There are several solutions using ytdl-sub's scripting capabilities to override
|
|||
episode_title: >-
|
||||
{ %array_at( captured_episode_title, 1 ) }
|
||||
|
||||
There is no single solution to this problem - it will vary case-by-case. See our full
|
||||
suite of :ref:`scripting functions
|
||||
<config_reference/scripting/scripting_functions:Scripting Functions>` to create your own
|
||||
clever scraping mechanisms.
|
||||
|
||||
...force ytdl-sub to re-download a file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sometimes users may wish to replace a file already in the archive, for example, if the
|
||||
current file is a lower resolution than desired, missing subtitles, corrupt, etc..
|
||||
|
||||
``ytdl-sub`` decides what files have already been downloaded by entries in :ref:`the
|
||||
download archive file <config_reference/plugins:output_options>`,
|
||||
``./.ytdl-sub-...-download-archive.json``, at the top of the subscription/series/show
|
||||
:ref:`output directory <config_reference/plugins:output_options>` in the appropriate
|
||||
``overrides: / ..._directory:`` library path, *and* the presence of the corresponding
|
||||
downloaded files under the same path. To force ``ytdl-sub`` to re-download an entry both
|
||||
need to be removed:
|
||||
|
||||
- Move aside the downloaded files:
|
||||
|
||||
Rename or move the downloaded files, including the associated files with the same
|
||||
base/stem name, such as ``./*.nfo``, ``./*.info-json``, etc..
|
||||
|
||||
- Ensure ``ytdl-sub`` is not running and won't run, such as by cron:
|
||||
|
||||
``ytdl-sub`` loads the ``./.ytdl-sub-...-download-archive.json`` file early, keeps it
|
||||
in memory, and writes it back out late. If it's running or starts running while you're
|
||||
modifying that file, then your changes will be overwritten when it exits.
|
||||
|
||||
- Remove the ``./.ytdl-sub-...-download-archive.json`` JSON array item:
|
||||
|
||||
Search for the stem name, the basename without any extension or suffix, common to all
|
||||
the downloaded files in this file and delete that whole entry, from the YouTube ID
|
||||
string to the closing curly braces. Be ware of JSON traling commas.
|
||||
|
||||
- Run ``$ ytdl-sub sub`` again with the appropriate CLI plugin options:
|
||||
|
||||
In normal operation, :ref:`yt-dlp minimizes requests and the files considered for
|
||||
download <guides/getting_started/index:minimize the work to only what's
|
||||
necessary>`. To re-download, those options must be disabled or modified. Disable
|
||||
:ref:`the 'break_on_existing' option <config_reference/plugins:ytdl_options>`, set
|
||||
:ref:`the 'date_range:' plugin <config_reference/plugins:date_range>`, and :ref:`limit
|
||||
the subscriptions <guides/getting_started/downloading:preview>` to
|
||||
download only the files that you've renamed in the steps above.
|
||||
|
||||
Set the appropriate dates, :ref:`including a sufficient margin
|
||||
<config_reference/plugins:date_range>`, and subscription name to include only the
|
||||
files you've renamed, and re-run. For example, if you've renamed all the files from
|
||||
2024 in the ``NOVA PBS`` subscription:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
ytdl-sub --match="NOVA PBS" sub -o "\
|
||||
--ytdl_options.break_on_existing False \
|
||||
--date_range.after 20240101 \
|
||||
--date_range.before 20250101 \
|
||||
"
|
||||
|
||||
...download a file missing from the archive
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The root causes are unknown, but sometimes even after successful, complete runs, some
|
||||
files will be missing from the archive. To attempt to download those missing files,
|
||||
use `the same CLI options as re-downloading a file`_
|
||||
|
||||
.. _`the same CLI options as re-downloading a file`:
|
||||
`...force ytdl-sub to re-download a file`_
|
||||
|
||||
...get support?
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
See :doc:`the debugging documentation <../debugging>`.
|
||||
|
||||
...reach out to contribute?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you would like to contribute, we're happy to accept any help, including from
|
||||
non-coders! To find out how you can help this project, you can:
|
||||
|
||||
- `Join our Discord <https://discord.gg/v8j9RAHb4k>`_ and leave a comment in
|
||||
#development with where you think you can assist or what skills you would like to
|
||||
contribute.
|
||||
|
||||
- If you just want to fix one thing, you're welcome to :ytdl-sub-gh:`submit a pull
|
||||
request <compare>` with information on what issue you're resolving and it will be
|
||||
reviewed as soon as possible.
|
||||
|
||||
There is no single solution to this problem - it will vary case-by-case. See
|
||||
our full suite of
|
||||
:ref:`scripting functions <config_reference/scripting/scripting_functions:Scripting Functions>`
|
||||
to create your own clever scraping mechanisms.
|
||||
|
||||
There is a bug where...
|
||||
-----------------------
|
||||
|
||||
...ytdl-sub is not downloading
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
..ytdl-sub is not downloading
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
...ytdl-sub is downloading at 360p or other lower quality
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Run with ``--log-level verbose`` to see all yt-dlp logs, to rule out whether it is a yt-dlp or ytdl-sub issue.
|
||||
|
||||
...ytdl-sub downloads 2-4 videos and then fails
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Any logs showing failed downloads, 403 errors, signs of throttles, etc, are a yt-dlp issue.
|
||||
A good strategy is to see if your same issue has been reported in
|
||||
`yt-dlp's GitHub issues <https://github.com/yt-dlp/yt-dlp/issues>`_, and search to see if there is a comment including
|
||||
a fix or workaround.
|
||||
|
||||
These are often just limits imposed by the external services that are not bugs. There
|
||||
may be little that can be done about them, but see :ref:`the '_throttle_protection'
|
||||
preset <prebuilt_presets/helpers:_throttle_protection>` for more information.
|
||||
If it looks like a ytdl-sub issue, run with ``--log-level debug`` and make a
|
||||
`GitHub issue in ytdl-sub <https://github.com/jmbannon/ytdl-sub/issues>`_
|
||||
containing these logs and other relevant info.
|
||||
|
||||
...date_range is not downloading older videos after I changed the range
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Your preset most likely has ``break_on_existing`` set to True, which will stop
|
||||
downloading additional metadata/videos if the video exists in your download archive. Set
|
||||
the following in your config to skip downloading videos that exist instead of stopping
|
||||
altogether.
|
||||
Your preset most likely has ``break_on_existing`` set to True, which will stop downloading additional metadata/videos if the video exists in your download archive. Set the following in your config to skip downloading videos that exist instead of stopping altogether.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
ytdl_options:
|
||||
break_on_existing: False
|
||||
|
||||
After you download your new date_range duration, re-enable ``break_on_existing`` to
|
||||
speed up successive downloads.
|
||||
After you download your new date_range duration, re-enable ``break_on_existing`` to speed up successive downloads.
|
||||
|
||||
...it is downloading non-English title and description metadata
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Most likely the video has a non-English language set to its 'native' language. You can
|
||||
tell yt-dlp to explicitly download English metadata using.
|
||||
Most likely the video has a non-English language set to its 'native' language. You can tell yt-dlp to explicitly download English metadata using.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -270,9 +185,7 @@ tell yt-dlp to explicitly download English metadata using.
|
|||
1. Set the following for your ytdl-sub library that has been added to Plex.
|
||||
|
||||
.. figure:: ../../images/plex_scanner_agent.png
|
||||
:alt:
|
||||
The Plex library editor, under the advanced settings, showing the required options
|
||||
for Plex to show the TV shows correctly.
|
||||
:alt: The Plex library editor, under the advanced settings, showing the required options for Plex to show the TV shows correctly.
|
||||
|
||||
- **Scanner:** Plex Series Scanner
|
||||
- **Agent:** Personal Media shows
|
||||
|
|
@ -280,15 +193,7 @@ tell yt-dlp to explicitly download English metadata using.
|
|||
- **Episode sorting:** Library default
|
||||
- **YES** Enable video preview thumbnails
|
||||
|
||||
2. Under **Settings** > **Agents**, confirm Plex Personal Media Shows/Movies scanner has
|
||||
**Local Media Assets** enabled.
|
||||
2. Under **Settings** > **Agents**, confirm Plex Personal Media Shows/Movies scanner has **Local Media Assets** enabled.
|
||||
|
||||
.. figure:: ../../images/plex_agent_sources.png
|
||||
:alt:
|
||||
The Plex Agents settings page has Local Media Assets enabled for Personal Media
|
||||
Shows and Movies tabs.
|
||||
|
||||
...ytdl-sub errors when downloading a 360p video with resolution assert
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:ref:`See how to either ignore this specific video or disable resolution assertion entirely here. <resolution assert handling>`
|
||||
:alt: The Plex Agents settings page has Local Media Assets enabled for Personal Media Shows and Movies tabs.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
Development and Contributing
|
||||
============================
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- python >= 3.10
|
||||
- ffmpeg/ffprobe 4.4.5 (test checksums rely on this version)
|
||||
- make
|
||||
|
|
@ -22,18 +20,15 @@ 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
|
||||
--------------------
|
||||
|
||||
|
|
@ -41,21 +36,18 @@ 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...
|
||||
|
||||
|
|
@ -63,36 +55,26 @@ 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
|
||||
:doc:`when using '--log-level debug' <../../debugging>`. This can be copy-pasted into
|
||||
the file ``resources/file_fixtures/repro.yaml``.
|
||||
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.
|
||||
Running the test ``e2e.test_debug_repro.TestReproduce.test_debug_log_repro``
|
||||
will fully reproduce that subscription in order to debug it.
|
||||
|
|
@ -1,132 +1,58 @@
|
|||
Automating
|
||||
==========
|
||||
Automating Downloads
|
||||
====================
|
||||
|
||||
Automate downloading your subscriptions by running the :ref:`'sub' sub-command
|
||||
<usage:subscriptions options>` periodically. There are various tools that can run
|
||||
commands on a schedule you may use any of them that work with your installation
|
||||
method. Most users use `cron`_ in `Docker containers <docker and unraid_>`_.
|
||||
:ref:`Guide for Docker and Unraid Containers <guides/getting_started/automating_downloads:docker and unraid>`
|
||||
|
||||
:ref:`Guide for Linux <guides/getting_started/automating_downloads:linux>`
|
||||
|
||||
:ref:`Guide for Windows <guides/getting_started/automating_downloads:windows>`
|
||||
|
||||
.. _cron scheduling syntax: https://crontab.guru/#0_*/6_*_*_*
|
||||
|
||||
.. _docker-unraid-setup:
|
||||
|
||||
Docker and Unraid
|
||||
-----------------
|
||||
|
||||
:doc:`The 'ytdl-sub' Docker container images <../install/docker>` provide optional cron
|
||||
support. Enable cron support by setting `a cron schedule`_ in the ``CRON_SCHEDULE``
|
||||
environment variable:
|
||||
Cron is preconfigured in every ytdl-sub docker container. Enable by adding the following
|
||||
ENV variables to your docker setup.
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: ./compose.yaml
|
||||
:emphasize-lines: 4
|
||||
|
||||
services:
|
||||
ytdl-sub:
|
||||
environment:
|
||||
CRON_SCHEDULE: "0 */6 * * *"
|
||||
# WARNING: See "Getting Started" -> "Automating" docs regarding throttles/bans:
|
||||
# CRON_RUN_ON_START: false
|
||||
- CRON_SCHEDULE="0 */6 * * *"
|
||||
- CRON_RUN_ON_START=false
|
||||
|
||||
Then recreate the container to apply the change and start it to generate the default
|
||||
``/config/ytdl-sub-configs/cron`` script. Read the comments in that script and edit as
|
||||
appropriate.
|
||||
|
||||
The container cron wrapper script will write output from the cron job to
|
||||
``/config/ytdl-sub-configs/.cron.log``. The default image ``ENTRYPOINT`` will ``$ tail
|
||||
...`` that file so you can monitor the cron job in the container's output and thus also
|
||||
in the Docker logs.
|
||||
- ``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.
|
||||
|
||||
You may also set the ``CRON_RUN_ON_START`` environment variable to ``true`` to have the
|
||||
image run your cron script whenever the container starts in addition to the cron
|
||||
schedule.
|
||||
|
||||
.. warning::
|
||||
|
||||
Using ``CRON_RUN_ON_START`` may cause your cron script to run too often and may
|
||||
trigger throttles and bans. When enabled, your cron script will run *whenever* the
|
||||
container starts including when the host reboots, when ``# dockerd`` restarts such as
|
||||
when upgrading Docker itself, when a new image is pulled, when something applies
|
||||
Compose changes, etc.. This may result in running ``ytdl-sub`` right before or after
|
||||
the next cron scheduled run.
|
||||
The cron script will reside in the main directory with the file name ``cron``.
|
||||
Cron logs should show when viewing the Docker logs.
|
||||
|
||||
|
||||
.. _linux-setup:
|
||||
|
||||
Linux, Mac OS X, BSD, or other UNIX's
|
||||
-------------------------------------
|
||||
|
||||
For installations on systems already running ``# crond``, you can also use cron to run
|
||||
``ytdl-sub`` periodically. Write a script to run ``ytdl-sub`` in the cron job. Be sure
|
||||
the script changes to the same directory as your configuration and uses the full path to
|
||||
``ytdl-sub``:
|
||||
Linux
|
||||
-----
|
||||
Must configure crontab manually, like so:
|
||||
|
||||
.. code-block:: shell
|
||||
:caption: ~/.local/bin/ytdl-sub-cron
|
||||
:emphasize-lines: 2,3
|
||||
|
||||
#!/bin/bash
|
||||
cd "~/.config/ytdl-sub/"
|
||||
~/.local/bin/ytdl-sub --dry-run sub -o '--ytdl_options.max_downloads 3' |&
|
||||
tee -a "~/.local/state/ytdl-sub/.cron.log"
|
||||
crontab -e
|
||||
0 */6 * * * /config/run_cron
|
||||
|
||||
Then tell ``# crond`` when to run the script:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
echo "0 */6 * * * ${HOME}/.local/bin/ytdl-sub-cron" | crontab "-"
|
||||
|
||||
Remove the ``--dry-run`` and ``-o ...`` CLI options from your cron script when you've
|
||||
tested your configuration and you're ready to download entries unattended.
|
||||
|
||||
|
||||
.. _windows-setup:
|
||||
|
||||
Windows
|
||||
-------
|
||||
To be tested (please contact code owner or join the discord server if you can test this out for us)
|
||||
|
||||
For most Windows users, the best way to run commands periodically is `the Task
|
||||
Scheduler`_:
|
||||
.. code-block:: powershell
|
||||
|
||||
.. attention::
|
||||
|
||||
These instructions are untested. Use at your own risk. If you use them, whether they
|
||||
work or not, please let us know how it went in `a support post in Discord`_ or `a new
|
||||
GitHub issue`_.
|
||||
|
||||
#. Open the Task Scheduler app.
|
||||
|
||||
#. Click ``Create Basic Task`` at the top of the right sidebar.
|
||||
|
||||
#. Set all the fields as appropriate until you get to the ``Action``...
|
||||
|
||||
#. For the ``Action``, select ``Start a program``...
|
||||
|
||||
#. Click ``Browse...`` to the installed ``ytdl-sub.exe`` executable...
|
||||
|
||||
#. Add CLI arguments to ``Add arguments (optional):``, for example ``--dry-run sub -o
|
||||
'--ytdl_options.max_downloads 3'``...
|
||||
|
||||
#. Set ``Start in (optional):`` to the directory containing your configuration.
|
||||
|
||||
#. Finish the rest of the ``Create Basic Task`` wizard.
|
||||
|
||||
|
||||
Next Steps
|
||||
----------
|
||||
|
||||
At this point, ``ytdl-sub`` should run periodically and keep your subscriptions current
|
||||
in your media library without your intervention. As your :doc:`subscriptions file
|
||||
<./subscriptions>` grows or you discover new use cases, it becomes worth while to
|
||||
simplify things by :doc:`defining your own custom presets <./first_config>`.
|
||||
|
||||
|
||||
|
||||
.. _`cron`:
|
||||
https://en.wikipedia.org/wiki/Cron
|
||||
.. _`a cron schedule`:
|
||||
https://crontab.cronhub.io/
|
||||
|
||||
.. _`the Task Scheduler`:
|
||||
https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page
|
||||
.. _`a support post in Discord`:
|
||||
https://discord.com/channels/994270357957648404/1084886228266127460
|
||||
.. _`a new GitHub issue`:
|
||||
https://github.com/jmbannon/ytdl-sub/issues/new
|
||||
ytdl-sub.exe --config \path\to\config\config.yaml sub \path\to\config\subscriptions.yaml
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
Downloading
|
||||
===========
|
||||
|
||||
Once you've :doc:`defined your subscriptions <./subscriptions>`, it's time to test your
|
||||
configuration and try your first download. As a web scraping tool, :ref:`it's important
|
||||
to minimize the requests sent to external services
|
||||
<guides/getting_started/index:minimize the work to only what's necessary>` to avoid
|
||||
triggering throttling or bans. Further, a full download of even one subscription can
|
||||
take significant time. Test each change to your subscriptions carefully and quickly as
|
||||
follows.
|
||||
|
||||
|
||||
Preview
|
||||
-------
|
||||
|
||||
Preview what ``ytdl-sub`` would do for this subscription. Run the :ref:`'sub'
|
||||
sub-command <usage:subscriptions options>` with CLI options to restrict requests as much
|
||||
as possible:
|
||||
|
||||
- Pull metadata and *simulate* a download without actually downloading any media files
|
||||
using the ``--dry-run`` CLI option.
|
||||
|
||||
- Limit requests by narrowing the run to one subscription by giving a
|
||||
subscription name to the ``--match`` CLI option.
|
||||
|
||||
- Stop after just a few downloads to further minimize requests and make testing faster
|
||||
using the ``max_downloads`` setting from ``yt-dlp``.
|
||||
|
||||
Change to the directory containing your ``./subscriptions.yaml`` file and run with those
|
||||
options:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
cd "/config/ytdl-sub-configs/"
|
||||
ytdl-sub --dry-run --match="NOVA PBS" sub -o '--ytdl_options.max_downloads 3'
|
||||
|
||||
Examine the output carefully, investigate anything that doesn't look right and repeat
|
||||
this step until everything looks right.
|
||||
|
||||
|
||||
Review
|
||||
------
|
||||
|
||||
Review the results of real downloads. Run it again without the ``--dry-run`` option to
|
||||
actually download media and place the files in your library:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
ytdl-sub --match="NOVA PBS" sub -o '--ytdl_options.max_downloads 3'
|
||||
|
||||
Examine the output carefully again. Then examine how the resulting downloads work in
|
||||
your library. Repeat with a larger value for ``max_downloads`` and examine the output
|
||||
and downloads again.
|
||||
|
||||
|
||||
Next Steps
|
||||
----------
|
||||
|
||||
Once you're `previewed <preview_>`_ and `reviewed <review_>`_ successful downloads of
|
||||
each of your subscriptions, you're ready to run a full download of all your
|
||||
subscriptions. Run the sub-command without the CLI options you used to limit what
|
||||
``ytdl-sub`` does while testing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
ytdl-sub sub
|
||||
|
||||
If you're ready to let ``ytdl-sub`` run unattended, it's time to :doc:`automate
|
||||
downloads <./automating_downloads>`.
|
||||
|
|
@ -3,39 +3,18 @@ Basic Configuration
|
|||
|
||||
A configuration file serves two purposes:
|
||||
|
||||
1. Set application-level functionality that is not specifiable in a subscription file.
|
||||
1. Set advanced functionality that is not specifiable in a subscription file, such as working directory location. These
|
||||
are set underneath ``configuration``.
|
||||
2. Create custom presets, which can drastically simplify your subscription file. These are defined underneath ``presets``.
|
||||
Presets are intended to be applicable and reusable across multiple subscriptions.
|
||||
|
||||
.. note::
|
||||
|
||||
ytdl-sub does not require a configuration file. However,
|
||||
certain application settings may be desirable for tweak, such as setting
|
||||
``working_directory`` to make ytdl-sub perform the initial download
|
||||
to an SSD drive.
|
||||
|
||||
2. Create custom presets.
|
||||
|
||||
.. note::
|
||||
|
||||
In the prior Initial Subscription examples, we leveraged the prebuilt preset
|
||||
``Jellyfin TV Show by Date``. This preset is entirely built using the same
|
||||
YAML configuration system offered to users by using a configuration file.
|
||||
|
||||
The following section attempts to demystify and explain how to...
|
||||
|
||||
- Set an application setting
|
||||
- Know whether or not custom presets are actually needed
|
||||
- How to create a custom preset
|
||||
- How to use a custom preset on subscriptions
|
||||
|
||||
-------------
|
||||
|
||||
how this works, and show-case how
|
||||
Below is a common configuration:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
configuration:
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
working_directory: '/mnt/ssd/.ytdl-sub-downloads'
|
||||
|
||||
presets:
|
||||
TV Show:
|
||||
|
|
@ -57,36 +36,28 @@ how this works, and show-case how
|
|||
max: 36
|
||||
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
tv_show_directory: "/ytdl_sub_tv_shows"
|
||||
|
||||
TV Show Only Recent:
|
||||
preset:
|
||||
- "TV Show"
|
||||
- "Only Recent"
|
||||
|
||||
|
||||
Configuration Section
|
||||
---------------------
|
||||
|
||||
The :ref:`configuration <config_reference/config_yaml:Configuration File>` section sets
|
||||
options for ytdl-sub execution. Most users should set the path where ``ytdl-sub``
|
||||
temporarily stores downloaded data before assembling it and moving it into your
|
||||
library. To avoid unnecessarily long large file renames, use a path on the same
|
||||
filesystem as your library in the ``overrides: / *_directory:`` paths:
|
||||
|
||||
The :ref:`configuration <config_reference/config_yaml:Configuration File>` section sets options for ytdl-sub execution.
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 1
|
||||
|
||||
configuration:
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
|
||||
working_directory: '/mnt/ssd/.ytdl-sub-downloads'
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -98,7 +69,6 @@ Show Only Recent``.
|
|||
|
||||
The indentation example above shows how to define multiple presets.
|
||||
|
||||
|
||||
Custom Preset Definition
|
||||
------------------------
|
||||
|
||||
|
|
@ -118,15 +88,12 @@ 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
|
||||
<config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` or other presets
|
||||
defined in your config.
|
||||
1. ``preset`` section, which can inherit :ref:`prebuilt presets <config_reference/prebuilt_presets/index:Prebuilt Preset Reference>`
|
||||
or other presets defined in your config.
|
||||
2. :ref:`Plugin definitions <config_reference/plugins:Plugins>`
|
||||
3. :ref:`overrides <config_reference/plugins:overrides>`, which can override inherited
|
||||
preset variables
|
||||
3. :ref:`overrides <config_reference/plugins:overrides>`, which can override inherited preset variables
|
||||
|
||||
Presets do not have to define all of these, as we'll see in the ``TV Show Only Recent``
|
||||
preset.
|
||||
Presets do not have to define all of these, as we'll see in the ``TV Show Only Recent`` preset.
|
||||
|
||||
Inheriting Presets
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -139,16 +106,14 @@ 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
|
||||
<config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` as a starting point
|
||||
for custom preset building, as they do the work of preset building to ensure things show
|
||||
as expected in their respective media players. Read on to see how to override prebuilt
|
||||
preset specifics such as title.
|
||||
It is highly advisable to use :ref:`prebuilt presets <config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` as
|
||||
a starting point for custom preset building, as they do the work of preset building to ensure things show as expected
|
||||
in their respective media players. Read on to see how to override prebuilt preset specifics such as title.
|
||||
|
||||
Defining Plugins
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
|
@ -169,18 +134,17 @@ Defining Plugins
|
|||
min: 10
|
||||
max: 36
|
||||
|
||||
Our ``TV Show`` sets two plugins, :ref:`throttle_protection
|
||||
<config_reference/plugins:throttle_protection>` and :ref:`embed_thumbnail
|
||||
<config_reference/plugins:embed_thumbnail>`. Each plugin's documentation shows the
|
||||
respective fields that they support.
|
||||
Our ``TV Show`` sets two plugins, :ref:`throttle_protection <config_reference/plugins:throttle_protection>` and
|
||||
:ref:`embed_thumbnail <config_reference/plugins:embed_thumbnail>`. Each plugin's documentation shows the respective
|
||||
fields that they support.
|
||||
|
||||
If an inherited preset defines the same plugin, the custom preset will use
|
||||
'merge-and-append' strategy to combine their definitions. What this means is:
|
||||
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
|
||||
|
||||
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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
@ -191,31 +155,27 @@ Setting Override Variables
|
|||
overrides:
|
||||
tv_show_directory: "/ytdl_sub_tv_shows"
|
||||
|
||||
All override variables reside underneath the :ref:`overrides
|
||||
<config_reference/plugins:overrides>` section.
|
||||
All override variables reside underneath the :ref:`overrides <config_reference/plugins:overrides>` section.
|
||||
|
||||
It is important to remember that individual subscriptions can override specific override
|
||||
variables. When defining variables in a preset, it is best practice to define them with
|
||||
the intention that
|
||||
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
|
||||
<config_reference/prebuilt_presets/index:Prebuilt Preset Reference>` to see all
|
||||
available variables that are overridable.
|
||||
See the :ref:`prebuilt preset reference <config_reference/prebuilt_presets/index:Prebuilt Preset Reference>`
|
||||
to see all available variables that are overridable.
|
||||
|
||||
|
||||
Using Custom Presets in Subscriptions
|
||||
--------------------------------------
|
||||
|
||||
Subscription files can use custom presets just like any other prebuilt preset. Below
|
||||
shows a complete subscription file using the above two custom presets.
|
||||
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
|
||||
|
||||
|
|
@ -233,31 +193,11 @@ 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.
|
||||
|
||||
Visualizing a subscription in Preset form
|
||||
-----------------------------------------
|
||||
|
||||
Subscription file syntax is designed to minimize boiler-plate when authoring new subscriptions.
|
||||
You can unpack any subscription using the ``inspect`` sub-command to see its boiler-plate *preset format*.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ytdl-sub inspect --match "BBC News" /path/to/subscriptions.yaml
|
||||
|
||||
This can be utilized for numerous purposes including:
|
||||
|
||||
* Ensuring your custom preset is getting applied correctly.
|
||||
* Figuring out which variables set things like file names, metadata, etc.
|
||||
* Understanding how subscription syntax translates to preset representation.
|
||||
|
||||
The default ``--level`` of inspect will fill in defined variables. Using ``--level original`` will
|
||||
present the subscription's raw layout with no fill.
|
||||
If you run ytdl-sub in the same directory, and the config file is named ``config.yaml``, it will
|
||||
use it by default.
|
||||
50
docs/source/guides/getting_started/first_download.rst
Normal file
50
docs/source/guides/getting_started/first_download.rst
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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
|
||||
126
docs/source/guides/getting_started/first_sub.rst
Normal file
126
docs/source/guides/getting_started/first_sub.rst
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
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__ <config_reference/subscription_yaml:File Preset>` section is where we
|
||||
can set modifications that apply to every subscription in this file.
|
||||
|
||||
This snippet specifically adds two :ref:`override <config_reference/plugins:Overrides>` 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 <config_reference/plugins:Plugins>` need to be
|
||||
set at the same indentation level as ``overrides``, not within it.
|
||||
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 11
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
|
||||
On line 11, we set the key to ``Jellyfin TV Show by Date``. This is a
|
||||
:ref:`prebuilt preset <prebuilt_presets/index:prebuilt presets>` that configures
|
||||
subscriptions to look like TV shows in the Jellyfin media player (can be changed to
|
||||
one of the presets outlined in the comment above). Setting it as a YAML key implies that all
|
||||
subscriptions underneath it will *inherit* this preset.
|
||||
|
||||
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 <config_reference/subscription_yaml:Subscription File>` and
|
||||
:ref:`subscription variables <config_reference/scripting/static_variables: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 <config_reference/prebuilt_presets/tv_show:TV Show>`,
|
||||
we can see that ``{subscription_name}`` is used to set the ``tv_show_name`` variable.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
:lineno-start: 11
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
= Documentaries:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
|
||||
= Kids | = TV-Y:
|
||||
"Jake Trains": "https://www.youtube.com/@JakeTrains"
|
||||
|
||||
Line 15 underneath ``Jellyfin TV Show by Date``, but at the same level as ``= Documentaries``.
|
||||
This means we'll inherit the TV show preset, but not the documentaries indent variable. We instead
|
||||
set the indent variables to ``= Kids | = TV-Y``. This sets two indent variables. We can set
|
||||
multiple presets and/or indent variables on the same key by using ``|`` as a separator.
|
||||
|
||||
Referring to the
|
||||
:ref:`TV show preset reference <config_reference/prebuilt_presets/tv_show:TV Show>`, the first
|
||||
two indent variables map to the TV show genre and TV show content rating.
|
||||
|
||||
The above info should be enough to understand the rest of the subscription file.
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
Getting Started
|
||||
===============
|
||||
|
||||
|
||||
Prerequisite Knowledge
|
||||
----------------------
|
||||
|
||||
|
|
@ -30,22 +29,21 @@ how ``ytdl-sub`` works, how it "thinks". So before you start configuring ``ytdl-
|
|||
.. _`YAML text files`: http://thomasloven.com/blog/2018/08/YAML-For-Nonprogrammers/
|
||||
|
||||
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
For most users, ``ytdl-sub`` works as follows:
|
||||
|
||||
Subscriptions use presets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Run ``$ ytdl-sub sub`` to read :doc:`a subscription file <./subscriptions>` that defines
|
||||
Run ``$ ytdl-sub sub`` to read :doc:`a subscription file <./first_sub>` that defines
|
||||
what subscriptions to download and place into your media library. Each subscription
|
||||
selects which :doc:`presets <../../prebuilt_presets/index>` to apply. Those presets
|
||||
configure how each subscription is downloaded and placed in the media library.
|
||||
|
||||
Presets configure plugins
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:doc:`A preset <../../prebuilt_presets/index>` is effectively a set of plugin
|
||||
configurations. Specifically, a preset consists of:
|
||||
|
|
@ -60,27 +58,27 @@ 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.
|
||||
metadata for those media, and 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.
|
||||
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
|
||||
|
|
@ -91,67 +89,47 @@ ancestor keys above that subscription describe the shared presets of that subscr
|
|||
and all the other descendant subscriptions under them.
|
||||
|
||||
Genres are also more often shared between subscriptions than not. To accommodate that
|
||||
reality, the ancestor keys of subscriptions may also use :ref:`the special '= ...'
|
||||
prefix to pass specific overrides
|
||||
<config_reference/scripting/static_variables:subscription_indent_i>` supported by the
|
||||
preset. By convention in the pre-built media type presets, the first ``= ...`` value
|
||||
specifies the genre for all descendant subscriptions.
|
||||
reality, the ancestor keys of subscriptions may also use the special ``= ...`` prefix to
|
||||
specify genres that also apply to all descendant subscriptions.
|
||||
|
||||
Finally, ancestor keys may use :ref:`the '... | ...' special character
|
||||
<config_reference/subscription_yaml:multi keys>` to combine multiple presets and/or
|
||||
genres for the descendant subscriptions beneath.
|
||||
Finally, ancestor keys may use 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
|
||||
they then use in most of their subscriptions. Most user-defines presets extend the
|
||||
:doc:`../../prebuilt_presets/index` provided by ``ytdl-sub``.
|
||||
|
||||
Minimize the work to only what's necessary
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Throttling and bans are a core problem for any web scraping tool, perhaps even more so
|
||||
for ``yt-dlp``, and no good actor *wants* to be an onerous burden on a
|
||||
service. Similarly, many web scraping use cases involve very large sets of data that are
|
||||
too big to process as a whole for performance. It's important to narrow the amount of
|
||||
data considered and minimize requests.
|
||||
|
||||
To these ends, most presets tell ``yt-dlp`` not to consider files before the most
|
||||
recently downloaded file using :ref:`the 'break_on_existing' option
|
||||
<config_reference/plugins:ytdl_options>`. Similarly, and particularly for huge channels
|
||||
or playlists, most users should use either :ref:`an 'Only Recent' preset
|
||||
<prebuilt_presets/helpers:only recent>` and/or :ref:`the 'Chunk Downloads' preset
|
||||
<prebuilt_presets/helpers:chunk downloads>` to restrict the number of downloads
|
||||
considered.
|
||||
|
||||
|
||||
Caveats
|
||||
~~~~~~~
|
||||
^^^^^^^
|
||||
|
||||
Some of these descriptions are not technically complete. For example, a subscription may
|
||||
use no preset at all and will just run ``yt-dlp`` without any customization or post
|
||||
processing. The subscriptions file has special support for :ref:`overriding the presets
|
||||
of all subscriptions in the file <config_reference/subscription_yaml:file preset>`. The
|
||||
configuration file supports :ref:`a few special options
|
||||
<config_reference/config_yaml:Configuration File>` that are not about defining presets. See
|
||||
<config_reference/config_yaml:configuration>` 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
|
||||
----------
|
||||
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.
|
||||
|
||||
With ``ytdl-sub`` installed and the above understood, the next step is to :doc:`start
|
||||
adding subscriptions <./subscriptions>`.
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
first_sub
|
||||
first_download
|
||||
automating_downloads
|
||||
first_config
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
subscriptions
|
||||
downloading
|
||||
automating_downloads
|
||||
first_config
|
||||
quick_start
|
||||
|
|
|
|||
|
|
@ -37,16 +37,34 @@ instructions to the letter.
|
|||
``"https://www.youtube.com/@novapbs"`` value to the URL of the channel or playlist
|
||||
for this subscription.
|
||||
|
||||
#. :ref:`Preview <guides/getting_started/downloading:preview>` and :ref:`Review
|
||||
<guides/getting_started/downloading:review>` the subscription.
|
||||
#. Preview what ``ytdl-sub`` would do for this subscription:
|
||||
|
||||
Run the :ref:`'sub' sub-command <usage:sub options>` but with the ``max_downloads``
|
||||
setting from ``yt-dlp`` along with the ``--dry-run`` and ``--match`` options from
|
||||
``ytdl-sub`` to minimize requests and prevent actual downloads. Be sure to update the
|
||||
``--match="..."`` value with the subscription name::
|
||||
|
||||
$ ytdl-sub --dry-run sub -o '--ytdl_options.max_downloads 3' --match="NOVA PBS"
|
||||
|
||||
Examine the output carefully.
|
||||
|
||||
#. 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::
|
||||
|
||||
$ ytdl-sub sub -o '--ytdl_options.max_downloads 3' --match="NOVA PBS"
|
||||
|
||||
Examine the output carefully, then examine how the downloads work in your
|
||||
library. Repeat with a larger value for ``max_downloads`` and examine the output and
|
||||
downloads again.
|
||||
|
||||
#. Add the rest of your subscriptions:
|
||||
|
||||
Repeat steps #3-6 for each of your subscriptions. Be sure to repeat the preview and
|
||||
Repeat steps #4-7 for each of your subscriptions. Be sure to repeat the preview and
|
||||
review steps for each subscription. In general, move slowly and carefully review
|
||||
everything. It's best to catch issues early :ref:`to avoid repeating downloads and to
|
||||
minimize requests <guides/getting_started/index:minimize the work to only what's
|
||||
necessary>`.
|
||||
everything. It's best to catch issues early to avoid repeating downloads and to
|
||||
minimize requests to avoid being throttled or banned by servers.
|
||||
|
||||
#. Automate downloads:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,167 +0,0 @@
|
|||
Subscriptions
|
||||
=============
|
||||
|
||||
Once you understand :ref:`how ytdl-sub works
|
||||
<guides/getting_started/index:architecture>`, it's time to start writing your
|
||||
:doc:`../../config_reference/subscription_yaml`.
|
||||
|
||||
|
||||
Media library paths
|
||||
-------------------
|
||||
|
||||
Everyone's media library may use different paths so ``ytdl-sub`` can't provide
|
||||
defaults. Tell ``ytdl-sub`` where to put your media using :ref:`overrides
|
||||
<guides/getting_started/index:presets and subscriptions accept overrides>`:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscriptions.yaml
|
||||
:emphasize-lines: 3-
|
||||
|
||||
__preset__:
|
||||
overrides:
|
||||
tv_show_directory: "/tv_shows"
|
||||
music_directory: "/music"
|
||||
music_video_directory: "/music_videos"
|
||||
|
||||
See the reference documentation for details about :ref:`the '__preset__:' special key
|
||||
<config_reference/subscription_yaml:file preset>`.
|
||||
|
||||
|
||||
Media library software and media types
|
||||
--------------------------------------
|
||||
|
||||
Different media library software, such as `Jellyfin`_, `Kodi`_, Plex, or Emby, have
|
||||
different requirements for where media files are placed, how those files are named, how
|
||||
metadata is formatted, and more. Those software also have different requirements for
|
||||
different types of media, such as shows/series, music, music videos, etc.. Use
|
||||
:doc:`prebuilt presets <../../prebuilt_presets/index>` in :ref:`YAML keys
|
||||
<guides/getting_started/index:subscriptions are grouped by indentation>` to tell
|
||||
``ytdl-sub`` which media library software and media type to process downloaded files
|
||||
for.
|
||||
|
||||
The actual subscription is defined in the lowest indentation level YAML keys. The
|
||||
example below defines a subscription named ``NOVA PBS`` to archive downloads from the
|
||||
entries in the ``https://www.youtube.com/@novapbs`` URL.
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscriptions.yaml
|
||||
:emphasize-lines: 1,4
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
|
||||
Bandcamp:
|
||||
"Emily Hopkins": "https://emilyharpist.bandcamp.com/"
|
||||
|
||||
.. _`Jellyfin`:
|
||||
https://jellyfin.org/
|
||||
.. _`Kodi`:
|
||||
https://kodi.tv/
|
||||
|
||||
|
||||
Which entries
|
||||
-------------
|
||||
|
||||
The :doc:`helper presets <../../prebuilt_presets/helpers>` also provide support for
|
||||
controlling which entries are downloaded and archived. These presets are intended to be
|
||||
combined with the library software and media type presets.
|
||||
|
||||
Combine presets using :ref:`the '.. | ...' special character
|
||||
<guides/getting_started/index:subscriptions are grouped by indentation>` in the YAML
|
||||
keys:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscriptions.yaml
|
||||
:emphasize-lines: 2,6
|
||||
|
||||
# Only download entries whose upload date is within the past 2 months:
|
||||
Kodi TV Show by Date | Only Recent:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
|
||||
# Only download 20 entries per run:
|
||||
Soundcloud Discography | Chunk Downloads:
|
||||
"UKNOWY": "https://soundcloud.com/uknowymunich"
|
||||
|
||||
|
||||
What format, quality, or resolution
|
||||
-----------------------------------
|
||||
|
||||
The :doc:`media quality presets <../../prebuilt_presets/media_quality>` provide support
|
||||
for controlling which ``yt-dlp`` media "format" to download, such as ``1080p`` video
|
||||
resolution or ``320k`` audio bitrate.
|
||||
|
||||
Users may also group and combine presets :ref:`using the YAML hierarchy
|
||||
<guides/getting_started/index:subscriptions are grouped by indentation>`. Subscriptions
|
||||
merge all the presets from their ancestor YAML keys. The hierarchy indentation depth may
|
||||
be as deep as needed to group your subscriptions for easy maintenance:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscriptions.yaml
|
||||
:emphasize-lines: 3,7,12
|
||||
|
||||
Jellyfin TV Show by Date | Only Recent:
|
||||
# Download the highest resolution available:
|
||||
Max Video Quality:
|
||||
"NOVA PBS": "https://www.youtube.com/@novapbs"
|
||||
"National Geographic": "https://www.youtube.com/@NatGeo"
|
||||
# Download the highest resolution available that is 720p or less:
|
||||
Max 720p:
|
||||
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
|
||||
|
||||
Soundcloud Discography | Chunk Downloads:
|
||||
# Only download audio using the Opus codec, not MP3 or other codecs:
|
||||
Max Opus Quality:
|
||||
"UKNOWY": "https://soundcloud.com/uknowymunich"
|
||||
|
||||
|
||||
Genre and rating metadata
|
||||
-------------------------
|
||||
|
||||
Presets may also support using arbitrary values from :ref:`YAML keys prefixed with '=
|
||||
...' <guides/getting_started/index:subscriptions are grouped by indentation>`. The ``=
|
||||
...`` prefix may be used at any indentation depth and may also be combined with presets
|
||||
and other ``= ...`` values using the ``... | ...`` special character to best group your
|
||||
subscriptions.
|
||||
|
||||
:ref:`By convention <config_reference/scripting/static_variables:subscription_indent_i>`
|
||||
in the built-in library software and media type presets, the first ``= ...`` value
|
||||
specifies the genre for all descendant subscriptions. For the ``TV Show ...`` presets,
|
||||
the second ``= ...`` value specifies the rating for all descendant subscriptions:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscriptions.yaml
|
||||
:emphasize-lines: 1,3
|
||||
|
||||
= Kids:
|
||||
|
||||
Jellyfin TV Show by Date | = TV-Y:
|
||||
"Jake Trains": "https://www.youtube.com/@JakeTrains"
|
||||
"Kids Toys Play": "https://www.youtube.com/@KidsToysPlayChannel"
|
||||
|
||||
Soundcloud Discography:
|
||||
"Foo Kids Band": "https://soundcloud.com/foo-kids-band"
|
||||
|
||||
|
||||
Override variables for one subscription
|
||||
---------------------------------------
|
||||
|
||||
Most variable overrides aren't actually specific to just one subscription and should be
|
||||
set in :doc:`your own custom presets <./first_config>`. But use :ref:`the override mode
|
||||
'~...' prefix <config_reference/subscription_yaml:override mode>` when an override is
|
||||
specific to only one subscription and will never be shared with another:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption: subscriptions.yaml
|
||||
:emphasize-lines: 2-
|
||||
|
||||
Jellyfin TV Show by Date:
|
||||
"~NOVA PBS":
|
||||
url: "https://www.youtube.com/@novapbs"
|
||||
tv_show_directory: "/media/Unique/Series/Path"
|
||||
|
||||
|
||||
Next Steps
|
||||
----------
|
||||
|
||||
Once you've defined your subscriptions, it's time to :doc:`test your configuration and
|
||||
try your first download <./downloading>`.
|
||||
|
|
@ -2,15 +2,13 @@
|
|||
Environment Agnostic
|
||||
====================
|
||||
|
||||
The PIP install method is not recommended; use of this method may cause unintended
|
||||
requirement conflicts if you have other locally installed apps that depend on ffmpeg.
|
||||
|
||||
The PIP install method is not recommended; use of this method may cause unintended requirement conflicts if you have other locally installed apps that depend on ffmpeg.
|
||||
|
||||
PIP Install
|
||||
--------------
|
||||
|
||||
You can install our `PyPI package <https://pypi.org/project/ytdl-sub/>`_. Both ffmpeg
|
||||
and Python 3.10 or greater are required.
|
||||
You can install our
|
||||
`PyPI package <https://pypi.org/project/ytdl-sub/>`_.
|
||||
Both ffmpeg and Python 3.10 or greater are required.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
@ -19,13 +17,10 @@ and Python 3.10 or greater are required.
|
|||
Install for Development
|
||||
=======================
|
||||
|
||||
These environment-agnostic methods of installing ``ytdl-sub`` are meant for local
|
||||
development of ``ytdl-sub``. If you want to contribute your changes, please read
|
||||
:doc:`/guides/development/index`.
|
||||
These environment-agnostic methods of installing ``ytdl-sub`` are meant for local development of ``ytdl-sub``. If you want to contribute your changes, please read :doc:`/guides/development/index`.
|
||||
|
||||
Local Install
|
||||
--------------
|
||||
|
||||
With a Python 3.10 virtual environment, you can clone and install the repo.
|
||||
|
||||
.. code-block:: bash
|
||||
|
|
@ -37,13 +32,12 @@ With a Python 3.10 virtual environment, you can clone and install the repo.
|
|||
|
||||
Local Docker Build
|
||||
-------------------
|
||||
|
||||
Run ``make docker`` in the root directory of this repo to build the image. This will
|
||||
build the python wheel and install it in the Dockerfile.
|
||||
Run ``make docker`` in the root directory of this repo to build the image. This
|
||||
will build the python wheel and install it in the Dockerfile.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/jmbannon/ytdl-sub.git
|
||||
cd ytdl-sub
|
||||
|
||||
make docker
|
||||
make docker
|
||||
|
|
@ -10,7 +10,6 @@ the recommended way of managing a ``ytdl-sub`` docker container. See :ref:`Auto
|
|||
Downloads <guides/getting_started/automating_downloads:docker and unraid>` for how to
|
||||
automate running ``ytdl-sub`` in a container running either variant.
|
||||
|
||||
|
||||
GUI Image
|
||||
---------
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ and other details. Once running, open `the web UI`_ to edit the `configuration`_
|
|||
|
||||
.. _`the web UI`: http://localhost:8443
|
||||
|
||||
|
||||
Headless Image
|
||||
--------------
|
||||
|
||||
|
|
@ -38,14 +36,6 @@ For example::
|
|||
|
||||
$ docker compose run --rm --user="${PUID}:${PGID}" --entrypoint="ytdl-sub" ytdl-sub sub
|
||||
|
||||
.. note::
|
||||
|
||||
In `the recommended GUI image <gui image_>`_, the ``DEFAULT_WORKSPACE`` directory is
|
||||
``/config/ytdl-sub-configs/`` which is used throughout the documentation and
|
||||
examples. In the headless images, that directory is just ``/config/``, so substitute
|
||||
that path if using a headless image.
|
||||
|
||||
|
||||
Install with Docker Compose
|
||||
---------------------------
|
||||
|
||||
|
|
@ -94,7 +84,6 @@ directory such as:
|
|||
# devices:
|
||||
# - capabilities: ["gpu"]
|
||||
|
||||
|
||||
Docker CLI
|
||||
----------
|
||||
|
||||
|
|
@ -120,27 +109,10 @@ host. The following command is for the gui image:
|
|||
See `the Docker reference <https://docs.docker.com/engine/reference/run/>`_ for further
|
||||
details.
|
||||
|
||||
Environment Variables
|
||||
---------------------
|
||||
``ytdl-sub`` docker images support the following environment variables.
|
||||
|
||||
.. csv-table:: Docker Environment Variables
|
||||
:header: "Name", "Supported Values", "Description"
|
||||
:widths: 15, 10, 60
|
||||
|
||||
"``PUID``", "integer", "User ID"
|
||||
"``PGID``", "integer", "Group ID"
|
||||
"``TZ``", "timezone", "Optional. Timezone to use in the logs. For supported values, see this `list <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List>`_. "
|
||||
"``CRON_SCHEDULE``", "cron schedule `format <https://crontab.guru/#0_*/6_*_*_*>`_", "Optional. Schedule to run the ``cron`` file in ytdl-sub's container. More info :ref:`here <guides/getting_started/automating_downloads:docker and unraid>`."
|
||||
"``CRON_RUN_ON_START``", "true/false", "Optional. Whether to run the cron script on container start."
|
||||
"``UPDATE_YT_DLP_ON_START``", "stable/nightly/master", "Optional. Whether to update yt-dlp to the latest configured version on container start."
|
||||
|
||||
For the GUI image, you can set LSIO's underlying code-server `env variables <https://docs.linuxserver.io/images/docker-code-server/#environment-variables-e>`_ as well."
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
In these examples, the configuration files will be at
|
||||
``<path/to/ytdl-sub/config>/config.yaml`` and
|
||||
``<path/to/ytdl-sub/config>/subscriptions.yaml``. Start the container the first time to
|
||||
populate those files with default examples.
|
||||
``<path/to/ytdl-sub/config>/subscriptions.yaml``. Starting the container the first time
|
||||
will populate those files with default examples.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
Install by Platform
|
||||
===================
|
||||
|
||||
``ytdl-sub`` can be installed on the following platforms.
|
||||
|
||||
All installations require a 64-bit CPU. 32-bit is not supported.
|
||||
|
|
@ -9,8 +8,7 @@ All installations require a 64-bit CPU. 32-bit is not supported.
|
|||
|
||||
.. tip::
|
||||
|
||||
The recommended install method of ``ytdl-sub`` is one of our :doc:`docker containers
|
||||
</guides/install/docker>`.
|
||||
The recommended install method of ``ytdl-sub`` is one of our :doc:`docker containers </guides/install/docker>`.
|
||||
|
||||
:doc:`/guides/install/docker`
|
||||
|
||||
|
|
@ -22,8 +20,9 @@ All installations require a 64-bit CPU. 32-bit is not supported.
|
|||
|
||||
:doc:`/guides/install/agnostic`
|
||||
|
||||
Once you've completed your installation, please refer to the
|
||||
:doc:`../getting_started/index` guide for next steps
|
||||
|
||||
|
||||
Once you've completed your installation, please refer to the :doc:`../getting_started/index` guide for next steps
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
Linux
|
||||
=====
|
||||
|
||||
``ytdl-sub`` should be installable using any Linux package manager, and requires ffmpeg
|
||||
to be installed.
|
||||
``ytdl-sub`` should be installable using any Linux package manager, and requires ffmpeg to be installed.
|
||||
|
||||
|
||||
.. tab-set::
|
||||
|
||||
|
|
@ -15,8 +15,7 @@ to be installed.
|
|||
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
|
||||
|
||||
|
|
@ -37,8 +36,7 @@ to be installed.
|
|||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,16 @@
|
|||
======
|
||||
Unraid
|
||||
======
|
||||
--------------
|
||||
You can install our :unraid:`unraid community apps <community/apps?q=ytdl-sub#r>` through the `Unraid Community Apps plugin <https://unraid.net/community/apps>`_.
|
||||
|
||||
You can install our :unraid:`unraid community apps <community/apps?q=ytdl-sub#r>`
|
||||
through the `Unraid Community Apps plugin <https://unraid.net/community/apps>`_.
|
||||
|
||||
If you installed the ``ytdl-sub-gui`` app, the code-server will be running at
|
||||
http://localhost:8443 (replace ``localhost`` with the IP of the computer running Unraid
|
||||
if you aren't trying to access ``ytdl-sub`` on that computer). Open this page in a
|
||||
browser to access and interact with ``ytdl-sub``.
|
||||
If you installed the ``ytdl-sub-gui`` app, the code-server will be running at http://localhost:8443 (replace ``localhost`` with the IP of the computer running Unraid if you aren't trying to access ``ytdl-sub`` on that computer). Open this page in a browser to access and interact with ``ytdl-sub``.
|
||||
|
||||
If you installed the ``ytdl-sub`` app (headless), open the normal app-specific console
|
||||
to access and interact with ``ytdl-sub``. Once open, you must first run ``su abc -s
|
||||
/bin/bash`` to change to the non-root user. You can confirm that this command worked by
|
||||
running ``whoami`` and verifying that the result is ``abc``.
|
||||
If you installed the ``ytdl-sub`` app (headless), open the normal app-specific console to access and interact with ``ytdl-sub``. Once open, you must first run ``su abc -s /bin/bash`` to change to the non-root user. You can confirm that this command worked by running ``whoami`` and verifying that the result is ``abc``.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you use the below option to access the ``ytdl-sub`` console, be sure to run ``su
|
||||
abc -s /bin/bash`` first thing. You can confirm that this command worked by running
|
||||
``whoami`` and verifying that the result is ``abc``. Do **NOT** run ``ytdl-sub`` as
|
||||
the root user! Running as root will set the owner of all modified files to root,
|
||||
which prevents most media managers and players from accessing the files.
|
||||
.. warning::
|
||||
|
||||
.. figure:: ../../../images/unraid_badconsole.png
|
||||
:alt:
|
||||
The Unraid community app plugin GUI, with an arrow pointing at the "Console"
|
||||
option in the dropdown after selecting ytdl-sub-gui
|
||||
If you use the below option to access the ``ytdl-sub`` console, be sure to run ``su abc -s /bin/bash`` first thing. You can confirm that this command worked by running ``whoami`` and verifying that the result is ``abc``. Do **NOT** run ``ytdl-sub`` as the root user! Running as root will set the owner of all modified files to root, which prevents most media managers and players from accessing the files.
|
||||
|
||||
.. figure:: ../../../images/unraid_badconsole.png
|
||||
:alt: The Unraid community app plugin GUI, with an arrow pointing at the "Console" option in the dropdown after selecting ytdl-sub-gui
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
=======
|
||||
Windows
|
||||
=======
|
||||
|
||||
--------------
|
||||
From powershell, run:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
|
@ -14,4 +12,4 @@ From powershell, run:
|
|||
|
||||
# Download ytdl-sub
|
||||
curl.exe -L -o ytdl-sub.exe https://github.com/jmbannon/ytdl-sub/releases/latest/download/ytdl-sub.exe
|
||||
ytdl-sub.exe -h
|
||||
ytdl-sub.exe -h
|
||||
|
|
@ -10,6 +10,5 @@ ytdl-sub User Guide
|
|||
prebuilt_presets/index
|
||||
usage
|
||||
config_reference/index
|
||||
debugging
|
||||
faq/index
|
||||
deprecation_notices
|
||||
|
|
|
|||
|
|
@ -17,13 +17,9 @@ and how it should appear in your media library servers and applications such as
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
|
||||
|
||||
|
||||
==============
|
||||
Helper Presets
|
||||
==============
|
||||
|
|
@ -9,13 +6,11 @@ Helper Presets
|
|||
|
||||
See how to apply helper presets :doc:`here </prebuilt_presets/index>`
|
||||
|
||||
|
||||
Only Recent
|
||||
-----------
|
||||
|
||||
To only download a recent number of videos, apply the ``Only Recent`` preset. Once a
|
||||
video's upload date is outside of the range, or you hit max files, older videos will be
|
||||
deleted automatically.
|
||||
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
|
||||
|
||||
|
|
@ -36,12 +31,9 @@ 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:
|
||||
|
||||
|
|
@ -79,7 +71,6 @@ Supports the following override variables:
|
|||
- "maple leafs"
|
||||
- "highlights"
|
||||
|
||||
|
||||
Filter Duration
|
||||
---------------
|
||||
|
||||
|
|
@ -109,19 +100,17 @@ Supports the following override variables:
|
|||
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
|
||||
|
||||
|
|
@ -140,98 +129,5 @@ default. You can change this number by setting the override variable
|
|||
= Documentaries:
|
||||
"Cosmos - What If": "https://www.youtube.com/playlist?list=PLZdXRHYAVxTJno6oFF9nLGuwXNGYHmE8U"
|
||||
|
||||
Once the entire channel is downloaded, remove the usage of this preset. It will then
|
||||
pull metadata from newest to oldest again, and stop once it reaches a video that has
|
||||
already been downloaded.
|
||||
|
||||
|
||||
_throttle_protection
|
||||
--------------------
|
||||
|
||||
.. note::
|
||||
|
||||
This preset is already a base preset of those higher-level presets that require it,
|
||||
so users seldom need to use it directly, for example, unless they're writing presets
|
||||
from scratch.
|
||||
|
||||
This preset is primarily a sensible default configuration of :ref:`the
|
||||
'throttle_protection' plugin <config_reference/plugins:throttle_protection>` along with
|
||||
an override to disable the plugin:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
overrides:
|
||||
# Disable throttle protection:
|
||||
enable_throttle_protection: false
|
||||
|
||||
In addition to throttling by denying download requests, some services also throttle
|
||||
downloads by only allowing downloads of the lowest resolution quality. At the time of
|
||||
writing, only YouTube does this by allowing only 360p downloads when throttled. To work
|
||||
around this kind of throttling, this preset includes :ref:`an assertion
|
||||
<config_reference/scripting/scripting_functions:error functions>` that will stop
|
||||
downloading when ``ytdl-sub`` downloads a video at 360p or lower. It supports the
|
||||
following overrides:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
overrides:
|
||||
# Disable resolution quality throttle protection:
|
||||
enable_resolution_assert: false
|
||||
# Change the resolution below which to assume downloading is throttled:
|
||||
resolution_assert_height_gte: 720
|
||||
|
||||
.. _resolution assert handling:
|
||||
|
||||
Handling Low Quality Videos
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A side effect from throttle protection's resolution assert is, if the only resolution available is 360p or lower, it will
|
||||
error. You can either disable resolution assert entirely (see above), or ignore specific titles in the subscription
|
||||
using the ``resolution_assert_ignore_titles`` variable. Add a subset of the title (case-sensitive) as a list entry
|
||||
to your subscription, like so:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# use tilda mode to set override variables to the subscription
|
||||
"~My Subscription":
|
||||
url: "https://youtube.com/@channel"
|
||||
resolution_assert_ignore_titles:
|
||||
- "This 360p Video Title"
|
||||
|
||||
_url
|
||||
----
|
||||
|
||||
All prebuilt presets share the same internal ``_multi_url`` preset which comes equipped with
|
||||
a few available customizations.
|
||||
|
||||
Sibling Metadata
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
*Sibling* refers to any entry within the same *playlist*. For channel downloads, this would
|
||||
imply **every** video that gets downloaded since yt-dlp treats the channel as the *playlist*.
|
||||
|
||||
Setting the variable ``include_sibling_metadata`` will include all sibling metadata within
|
||||
each individual entry's metadata. This is used specifically for music presets. When downloading
|
||||
a playlist as an album for example, it will take the max year amongst all the other sibling's metadata
|
||||
to have a consistent album year that can be used in file or directory naming.
|
||||
|
||||
Webpage URL
|
||||
~~~~~~~~~~~
|
||||
|
||||
``ytdl-sub`` performs downloads in two stages.
|
||||
|
||||
1. Metadata scrape from the original URL
|
||||
2. Individual entry downloads
|
||||
|
||||
For step 2, ``ytdl-sub`` will use the ``webpage_url`` variable by default for the input URL to yt-dlp.
|
||||
This can be modified in case it's not working as expected by using the variable ``modified_webpage_url``.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: yaml
|
||||
:caption:
|
||||
Removes yt-dlp smuggle data from the URL
|
||||
|
||||
overrides:
|
||||
modified_webpage_url: >-
|
||||
{ %regex_sub("#__youtubedl_smuggle=.*", "", webpage_url) }
|
||||
Once the entire channel is downloaded, remove 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.
|
||||
|
|
@ -7,13 +7,11 @@ 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:
|
||||
|
||||
|
|
@ -24,8 +22,9 @@ 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
|
||||
</config_reference/prebuilt_presets/index>`.
|
||||
|
||||
For advanced users, you can review the prebuilt preset
|
||||
definitions :doc:`here </config_reference/prebuilt_presets/index>`.
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
|
@ -34,4 +33,4 @@ For advanced users, you can review the prebuilt preset definitions :doc:`here
|
|||
music
|
||||
music_videos
|
||||
media_quality
|
||||
helpers
|
||||
helpers
|
||||
|
|
@ -6,10 +6,8 @@ Media Quality Presets
|
|||
|
||||
See how to apply media quality presets :doc:`here </prebuilt_presets/index>`
|
||||
|
||||
|
||||
Video
|
||||
-----
|
||||
|
||||
The following presets set video quality specifications to yt-dlp.
|
||||
|
||||
- ``Max Video Quality``
|
||||
|
|
@ -19,12 +17,10 @@ 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``
|
||||
|
|
|
|||
|
|
@ -2,16 +2,21 @@
|
|||
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.
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -21,8 +26,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
|
||||
|
||||
|
|
@ -32,13 +37,11 @@ separate values in the form 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.
|
||||
|
||||
|
|
@ -48,8 +51,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
|
||||
|
||||
|
|
@ -59,14 +62,11 @@ separate values in the form 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,10 +77,8 @@ 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
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
Music Video Presets
|
||||
===================
|
||||
|
||||
WIP
|
||||
WIP
|
||||
|
|
@ -2,57 +2,45 @@
|
|||
TV Show Presets
|
||||
===============
|
||||
|
||||
|
||||
Player-Specific Presets
|
||||
-----------------------
|
||||
=======================
|
||||
|
||||
``ytdl-sub`` provides player-specific versions of certain presets, which apply settings
|
||||
to optimize the downloads for that player.
|
||||
``ytdl-sub`` provides player-specific versions of certain presets, which apply settings to optimize the downloads for that player.
|
||||
|
||||
The following actions are taken based on the indicated player:
|
||||
|
||||
Kodi
|
||||
~~~~
|
||||
|
||||
--------
|
||||
* Everything that the Jellyfin version does
|
||||
* Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with
|
||||
``□``
|
||||
* Enables ``kodi_safe`` NFOs, replacing 4-byte unicode characters that break kodi with ``□``
|
||||
|
||||
Jellyfin
|
||||
~~~~~~~~
|
||||
|
||||
--------
|
||||
* Places any season-specific poster art in the main show folder
|
||||
* Generates NFO tags
|
||||
|
||||
Emby
|
||||
~~~~
|
||||
|
||||
----
|
||||
* Places any season-specific poster art in the main show folder
|
||||
* Generates NFO tags
|
||||
|
||||
* For named seasons, creates a ``season.nfo`` file per season
|
||||
|
||||
Plex
|
||||
~~~~~~~~
|
||||
|
||||
* :ref:`Special sanitization
|
||||
<config_reference/scripting/entry_variables:title_sanitized_plex>` of numbers so Plex
|
||||
doesn't recognize numbers that are part of the title as the episode number
|
||||
--------
|
||||
* :ref:`Special sanitization <config_reference/scripting/entry_variables:title_sanitized_plex>` of numbers so Plex doesn't recognize numbers that are part of the title as the episode number
|
||||
* Converts all downloaded videos to the mp4 format
|
||||
* Places any season-specific poster art into the season folder
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
TV Show by Date
|
||||
---------------
|
||||
===============
|
||||
|
||||
TV Show by Date will organize something like a YouTube channel or playlist into a tv
|
||||
show, where seasons and episodes are organized using upload date.
|
||||
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``
|
||||
|
|
@ -86,10 +74,9 @@ Must define ``tv_show_directory``. Available presets:
|
|||
- "https://www.youtube.com/@rickbeato240"
|
||||
|
||||
Advanced Usage
|
||||
~~~~~~~~~~~~~~
|
||||
--------------
|
||||
|
||||
If you prefer a different season/episode organization method, you can set the following
|
||||
override variables.
|
||||
If you prefer a different season/episode organization method, you can set the following override variables.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -108,11 +95,12 @@ Or for a specific preset
|
|||
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.
|
||||
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:
|
||||
|
||||
|
|
@ -121,24 +109,23 @@ Season Ordering
|
|||
* ``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.
|
||||
* 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.
|
||||
* 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:
|
||||
|
||||
|
|
@ -148,23 +135,21 @@ TV Show by Date presets use the following for defaults:
|
|||
tv_show_by_date_episode_ordering: "upload-month-day"
|
||||
|
||||
TV Show Collection
|
||||
------------------
|
||||
==================
|
||||
|
||||
TV Show Collections set each URL as its own season. If a video belongs to multiple URLs
|
||||
(i.e. a channel and a channel's playlist), the video will only download once and reside
|
||||
in the higher-numbered season.
|
||||
(i.e. a channel and a channel's playlist), the video will only download once and reside in
|
||||
the higher-numbered season.
|
||||
|
||||
Two main use cases of a collection are:
|
||||
1. Organize a YouTube channel TV show where Season 1 contains any video not in a
|
||||
'season playlist', Season 2 for 'Playlist A', Season 3 for 'Playlist B', etc.
|
||||
2. Organize one or more YouTube channels/playlists, where each season represents a
|
||||
separate channel/playlist.
|
||||
|
||||
Today, ytdl-supports up to 40 seasons with 11 URLs per season.
|
||||
1. Organize a YouTube channel TV show where Season 1 contains any video
|
||||
not in a 'season playlist', Season 2 for 'Playlist A', Season 3 for
|
||||
'Playlist B', etc.
|
||||
2. Organize one or more YouTube channels/playlists, where each season
|
||||
represents a separate channel/playlist.
|
||||
|
||||
Example
|
||||
~~~~~~~
|
||||
|
||||
-------
|
||||
Must define ``tv_show_directory``. Available presets:
|
||||
|
||||
* ``Kodi TV Show Collection``
|
||||
|
|
@ -187,34 +172,10 @@ Must define ``tv_show_directory``. Available presets:
|
|||
s02_name: "Covers"
|
||||
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
|
||||
|
||||
Other notable features include:
|
||||
|
||||
* TV show poster info is pulled from the first URL in s01.
|
||||
* Duplicate videos in different URLs (channel /videos vs playlist) will not download twice.
|
||||
|
||||
* The video will attributed to the season with the highest number.
|
||||
* Individual seasons support both single and multi URL.
|
||||
* s00 is supported for specials.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
"~Beyond the Guitar":
|
||||
s00_name: "Specials"
|
||||
s00_url:
|
||||
- "https://www.youtube.com/watch?v=vXzguOdulAI"
|
||||
- "https://www.youtube.com/watch?v=IGwYDvaGAz0"
|
||||
s01_name: "Videos"
|
||||
s01_url:
|
||||
- "https://www.youtube.com/c/BeyondTheGuitar"
|
||||
- "https://www.youtube.com/@BeyondTheGuitarAcademy"
|
||||
s02_name: "Covers"
|
||||
s02_url: "https://www.youtube.com/playlist?list=PLE62gWlWZk5NWVAVuf0Lm9jdv_-_KXs0W"
|
||||
|
||||
Advanced Usage
|
||||
~~~~~~~~~~~~~~
|
||||
--------------
|
||||
|
||||
If you prefer a different episode organization method, you can set the following
|
||||
override variables.
|
||||
If you prefer a different episode organization method, you can set the following override variables.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -237,8 +198,9 @@ Or for a specific preset
|
|||
|
||||
The following are supported.
|
||||
|
||||
|
||||
Episode Ordering
|
||||
""""""""""""""""
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``tv_show_collection_episode_ordering`` supports one of the following:
|
||||
|
||||
|
|
@ -248,8 +210,7 @@ Episode Ordering
|
|||
* ``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.
|
||||
* 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.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Usage
|
||||
=====
|
||||
=======
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
|
@ -7,12 +7,10 @@ Usage
|
|||
|
||||
For Windows users, it would be ``ytdl-sub.exe``
|
||||
|
||||
|
||||
General Options
|
||||
---------------
|
||||
|
||||
CLI options common to all sub-commands. Must be specified before the sub-command, for
|
||||
example ``$ ytdl-sub --dry-run sub ...``:
|
||||
General options must be specified before the command (i.e. ``sub``).
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
|
|
@ -22,30 +20,24 @@ example ``$ ytdl-sub --dry-run sub ...``:
|
|||
path to the config yaml, uses config.yaml if not provided
|
||||
-d, --dry-run preview what a download would output, does not perform any video downloads or writes to output directories
|
||||
-l quiet|info|verbose|debug, --log-level quiet|info|verbose|debug
|
||||
level of logs to print to console, defaults to verbose
|
||||
level of logs to print to console, defaults to info
|
||||
-t TRANSACTIONPATH, --transaction-log TRANSACTIONPATH
|
||||
path to store the transaction log output of all files added, modified, deleted
|
||||
-st, --suppress-transaction-log
|
||||
do not output transaction logs to console or file
|
||||
-nc, --suppress-colors
|
||||
do not use colors in ytdl-sub output
|
||||
-m MATCH [MATCH ...], --match MATCH [MATCH ...]
|
||||
match subscription names to one or more substrings, and only run those subscriptions
|
||||
|
||||
|
||||
Subscriptions Options
|
||||
---------------------
|
||||
|
||||
Download all subscriptions specified in each :doc:`subscriptions file
|
||||
<./guides/getting_started/subscriptions>`.
|
||||
Sub Options
|
||||
-----------
|
||||
Download all subscriptions specified in each ``SUBPATH``.
|
||||
|
||||
.. code-block::
|
||||
|
||||
ytdl-sub [GENERAL OPTIONS] sub [SUBPATH ...]
|
||||
|
||||
``SUBPATH`` is one or more paths to subscription files and defaults to
|
||||
``./subscriptions.yaml`` if none are given. It will use the config specified by
|
||||
``--config``, or ``./config.yaml``, if not provided.
|
||||
``SUBPATH`` is one or more paths to subscription files, uses ``subscriptions.yaml`` if not provided.
|
||||
It will use the config specified by ``--config``, or ``config.yaml`` if not provided.
|
||||
|
||||
.. code-block:: text
|
||||
:caption: Additional Options
|
||||
|
|
@ -55,19 +47,16 @@ Download all subscriptions specified in each :doc:`subscriptions file
|
|||
-o DL_OVERRIDE, --dl-override DL_OVERRIDE
|
||||
override all subscription config values using `dl` syntax, i.e. --dl-override='--ytdl_options.max_downloads 3'
|
||||
|
||||
|
||||
Download Options
|
||||
----------------
|
||||
|
||||
Download a single subscription in the form of CLI arguments instead of from :doc:`a
|
||||
subscriptions file <./guides/getting_started/subscriptions>`:
|
||||
-----------------
|
||||
Download a single subscription in the form of CLI arguments.
|
||||
|
||||
.. code-block::
|
||||
|
||||
ytdl-sub [GENERAL OPTIONS] dl [SUBSCRIPTION ARGUMENTS]
|
||||
|
||||
``SUBSCRIPTION ARGUMENTS`` are the same as YAML arguments, but use periods (``.``)
|
||||
instead of indents. For example, you can represent this subscription:
|
||||
``SUBSCRIPTION ARGUMENTS`` are exactly the same as YAML arguments, but use periods (``.``) instead
|
||||
of indents for specifying YAML from the CLI. For example, you can represent this subscription:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -87,14 +76,11 @@ Using the command:
|
|||
--overrides.tv_show_name "Rick A" \
|
||||
--overrides.url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw"
|
||||
|
||||
See how to shorten commands using :ref:`download aliases <config_reference/config_yaml:dl_aliases>`.
|
||||
|
||||
See how to shorten commands using
|
||||
`download aliases <https://ytdl-sub.readthedocs.io/en/latest/config_reference/config_yaml.html#ytdl_sub.config.config_validator.ConfigOptions.dl_aliases>`_.
|
||||
|
||||
View Options
|
||||
------------
|
||||
|
||||
Preview the source variables for a given URL. Helpful to create new subscriptions:
|
||||
|
||||
-----------------
|
||||
.. code-block::
|
||||
|
||||
ytdl-sub view [-sc] [URL]
|
||||
|
|
@ -105,42 +91,5 @@ Preview the source variables for a given URL. Helpful to create new subscription
|
|||
-sc, --split-chapters
|
||||
View source variables after splitting by chapters
|
||||
|
||||
CLI to SUB Options
|
||||
------------------
|
||||
Convert yt-dlp cli arguments to ytdl-sub `ytdl_options` arguments.
|
||||
|
||||
.. code-block::
|
||||
|
||||
ytdl-sub cli-to-sub [YT-DLP ARGS]
|
||||
|
||||
Inspect
|
||||
-------
|
||||
Inspect a single subscription's underlying preset representation.
|
||||
This can be utilized for numerous purposes including:
|
||||
|
||||
* Ensuring your custom preset is getting applied correctly.
|
||||
* Figuring out which variables set things like file names, metadata, etc.
|
||||
* Understanding how subscription syntax translates to preset representation.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ytdl-sub inspect --match "Game Chops" --mock 'title=Lets Play' examples/music_subscriptions.yaml
|
||||
|
||||
.. code-block:: text
|
||||
:caption: Additional Options
|
||||
|
||||
-l 0,1,2,3, --level 0,1,2,3
|
||||
level of inspection to perform:
|
||||
0 - original present the subscription as-is
|
||||
1 - fill fill in defined values
|
||||
2 - resolve resolve all possible variables (default)
|
||||
3 - internal resolve all variables to their internal representation
|
||||
|
||||
-m MATCH [MATCH ...], --match MATCH [MATCH ...]
|
||||
match subscription names to one or more substrings, and only run those subscriptions
|
||||
-o DL_OVERRIDE, --dl-override DL_OVERRIDE
|
||||
override all subscription config values using `dl` syntax, i.e. --dl-override='--ytdl_options.max_downloads 3'
|
||||
-k VAR=VALUE, --mock VAR=VALUE
|
||||
ability to mock one or more variable values, i.e. --mock 'title=Lets Play'
|
||||
Preview the source variables for a given URL. Helps when creating new configs.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
###############################################################################
|
||||
# Top-level configurations to apply umask and write log files
|
||||
# Top-level configurations to apply umask and persist error logs
|
||||
configuration:
|
||||
umask: "002"
|
||||
persist_logs:
|
||||
logs_directory: '/config/logs'
|
||||
logs_directory: './logs'
|
||||
keep_successful_logs: False
|
||||
|
||||
presets:
|
||||
|
|
@ -69,7 +69,7 @@ presets:
|
|||
# ytdl_options lets you pass any arg into yt-dlp's Python API
|
||||
ytdl_options:
|
||||
# Set the cookie file
|
||||
# cookiefile: "/config/ytdl-sub-configs/youtube_cookies.txt"
|
||||
# cookiefile: "/config/youtube_cookies.txt"
|
||||
|
||||
# For YouTube, get English metadata if multiple languages are present
|
||||
extractor_args:
|
||||
|
|
@ -98,4 +98,4 @@ presets:
|
|||
|
||||
overrides:
|
||||
only_recent_date_range: "2months"
|
||||
only_recent_max_files: 30
|
||||
only_recent_max_files: 30
|
||||
|
|
@ -24,6 +24,6 @@ TV Show Only Recent:
|
|||
# to set only for that subscriptions
|
||||
"~BBC News":
|
||||
url: "https://www.youtube.com/@BBCNews" # use url2, url3, ... for multi-url in this form
|
||||
only_recent_date_range: "2weeks"
|
||||
date_range: "2weeks"
|
||||
"Frontline PBS": "https://www.youtube.com/@frontline"
|
||||
"Whitehouse": "https://www.bitchute.com/channel/zWsYVmCOu4JA/" # Supports non-YT sites
|
||||
|
|
@ -15,7 +15,7 @@ classifiers = [
|
|||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
dependencies = [
|
||||
"yt-dlp[default]==2026.6.9",
|
||||
"yt-dlp[default]==2025.8.11",
|
||||
"colorama~=0.4",
|
||||
"mergedeep~=1.3",
|
||||
"mediafile~=0.12",
|
||||
|
|
@ -43,15 +43,16 @@ where = ["src"]
|
|||
[project.optional-dependencies]
|
||||
test = [
|
||||
"coverage[toml]>=6.3,<8.0",
|
||||
"pytest>=7.2,<10.0",
|
||||
"pytest-rerunfailures>=14,<17",
|
||||
"pytest>=7.2,<9.0",
|
||||
"pytest-rerunfailures>=14,<16",
|
||||
]
|
||||
lint = [
|
||||
"pylint==4.0.5",
|
||||
"ruff==0.15.16",
|
||||
"black==24.10.0",
|
||||
"isort==6.0.1",
|
||||
"pylint==3.3.8",
|
||||
]
|
||||
docs = [
|
||||
"sphinx>=7,<10",
|
||||
"sphinx>=7,<9",
|
||||
"sphinx-rtd-theme>=2,<4",
|
||||
"sphinx-book-theme~=1.0",
|
||||
"sphinx-copybutton~=0.5",
|
||||
|
|
@ -65,6 +66,15 @@ build = [
|
|||
[project.scripts]
|
||||
ytdl-sub = "ytdl_sub.main:main"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
force_single_line = true
|
||||
|
||||
[tool.black]
|
||||
line_length = 100
|
||||
target-version = ["py310"]
|
||||
|
||||
[tool.pylint.MASTER]
|
||||
disable = [
|
||||
"C0115", # Missing class docstring
|
||||
|
|
@ -90,27 +100,3 @@ include = [
|
|||
exclude_also = [
|
||||
"raise UNREACHABLE.*",
|
||||
]
|
||||
|
||||
# ruff
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.10
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = ["I"]
|
||||
|
||||
[tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
quote-style = "double"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
|
@ -1,31 +1,28 @@
|
|||
import gc
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.cli.output_summary import output_summary
|
||||
from ytdl_sub.cli.output_transaction_log import (
|
||||
_maybe_validate_transaction_log_file,
|
||||
output_transaction_log,
|
||||
)
|
||||
from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub
|
||||
from ytdl_sub.cli.output_transaction_log import _maybe_validate_transaction_log_file
|
||||
from ytdl_sub.cli.output_transaction_log import output_transaction_log
|
||||
from ytdl_sub.cli.parsers.dl import DownloadArgsParser
|
||||
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, parser
|
||||
from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME
|
||||
from ytdl_sub.cli.parsers.main import parser
|
||||
from ytdl_sub.config.config_file import ConfigFile
|
||||
from ytdl_sub.config.validators.variable_validation import ResolutionLevel
|
||||
from ytdl_sub.subscriptions.subscription import Subscription
|
||||
from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled, ValidationException
|
||||
from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.file_lock import working_directory_lock
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
|
||||
logger = Logger.get()
|
||||
|
||||
# View is a command to run a simple dry-run on a URL using the `_view` preset.
|
||||
|
|
@ -77,7 +74,6 @@ def _download_subscriptions_from_yaml_files(
|
|||
subscription_override_dict: Dict,
|
||||
update_with_info_json: bool,
|
||||
dry_run: bool,
|
||||
shuffle: bool,
|
||||
) -> List[Subscription]:
|
||||
"""
|
||||
Downloads all subscriptions from one or many subscription yaml files.
|
||||
|
|
@ -94,8 +90,6 @@ def _download_subscriptions_from_yaml_files(
|
|||
Whether to actually download or update using existing info json
|
||||
dry_run
|
||||
Whether to dry run or not
|
||||
shuffle
|
||||
Whether to shuffle the subscription download order
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -117,10 +111,6 @@ def _download_subscriptions_from_yaml_files(
|
|||
subscription_override_dict=subscription_override_dict,
|
||||
)
|
||||
|
||||
if shuffle:
|
||||
logger.info("Shuffling subscriptions")
|
||||
random.shuffle(subscriptions)
|
||||
|
||||
for subscription in subscriptions:
|
||||
with subscription.exception_handling():
|
||||
logger.info(
|
||||
|
|
@ -205,44 +195,6 @@ def _view_url_from_cli(config: ConfigFile, url: str, split_chapters: bool) -> Su
|
|||
return subscription
|
||||
|
||||
|
||||
def _parse_inspect_mocks(mocks: Optional[List[str]]) -> Dict[str, str]:
|
||||
out: Dict[str, str] = {}
|
||||
for mock in mocks or []:
|
||||
spl = mock.split("=", 1)
|
||||
if len(spl) == 1:
|
||||
raise ValidationException("inspect mock must be in the form of VAR=VALUE")
|
||||
out[spl[0].strip()] = spl[1]
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _inspect(
|
||||
config: ConfigFile,
|
||||
subscription_paths: List[str],
|
||||
subscription_matches: List[str],
|
||||
subscription_override_dict: Dict,
|
||||
inspection_level: int,
|
||||
mocks: Dict[str, str],
|
||||
) -> None:
|
||||
|
||||
subscriptions: List[Subscription] = []
|
||||
for path in subscription_paths:
|
||||
subscriptions += Subscription.from_file_path(
|
||||
config=config,
|
||||
subscription_path=path,
|
||||
subscription_matches=subscription_matches,
|
||||
subscription_override_dict=subscription_override_dict,
|
||||
)
|
||||
|
||||
if len(subscriptions) > 1:
|
||||
print(
|
||||
"inspect can only inspect a single subscription. Use --match to filter for a single one"
|
||||
)
|
||||
return
|
||||
|
||||
print(subscriptions[0].resolved_yaml(resolution_level=inspection_level, mocks=mocks))
|
||||
|
||||
|
||||
def main() -> List[Subscription]:
|
||||
"""
|
||||
Entrypoint for ytdl-sub, without the error handling
|
||||
|
|
@ -254,10 +206,6 @@ def main() -> List[Subscription]:
|
|||
|
||||
args, extra_args = parser.parse_known_args()
|
||||
|
||||
if args.subparser == "cli-to-sub":
|
||||
print_cli_to_sub(args=extra_args)
|
||||
return []
|
||||
|
||||
# Load the config
|
||||
if args.config:
|
||||
config = ConfigFile.from_file_path(args.config)
|
||||
|
|
@ -269,23 +217,6 @@ def main() -> List[Subscription]:
|
|||
|
||||
subscriptions: List[Subscription] = []
|
||||
|
||||
if args.subparser == "inspect":
|
||||
subscription_override_dict = {}
|
||||
if args.dl_override:
|
||||
subscription_override_dict = DownloadArgsParser.from_dl_override(
|
||||
override=args.dl_override, config=config
|
||||
).to_subscription_dict()
|
||||
|
||||
_inspect(
|
||||
config=config,
|
||||
subscription_paths=args.subscription_paths,
|
||||
subscription_matches=args.match,
|
||||
subscription_override_dict=subscription_override_dict,
|
||||
inspection_level=ResolutionLevel.level_number(args.inspection_level),
|
||||
mocks=_parse_inspect_mocks(args.mock),
|
||||
)
|
||||
return []
|
||||
|
||||
# If transaction log file is specified, make sure we can open it
|
||||
_maybe_validate_transaction_log_file(transaction_log_file_path=args.transaction_log)
|
||||
|
||||
|
|
@ -317,7 +248,6 @@ def main() -> List[Subscription]:
|
|||
subscription_override_dict=subscription_override_dict,
|
||||
update_with_info_json=args.update_with_info_json,
|
||||
dry_run=args.dry_run,
|
||||
shuffle=args.shuffle,
|
||||
)
|
||||
|
||||
# One-off download
|
||||
|
|
@ -333,7 +263,7 @@ def main() -> List[Subscription]:
|
|||
_view_url_from_cli(config=config, url=args.url, split_chapters=args.split_chapters)
|
||||
)
|
||||
else:
|
||||
raise ValidationException("Must provide one of the commands: sub, dl, view, cli-to-sub")
|
||||
raise ValidationException("Must provide one of the commands: sub, dl, view")
|
||||
|
||||
if not args.suppress_transaction_log:
|
||||
output_transaction_log(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Optional
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.subscriptions.subscription import Subscription
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
from typing import List
|
||||
|
||||
import yt_dlp
|
||||
import yt_dlp.options
|
||||
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
from ytdl_sub.utils.yaml import dump_yaml
|
||||
|
||||
logger = Logger.get()
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
##############################################################
|
||||
# --- BEGIN ----
|
||||
# Copy of https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py
|
||||
|
||||
create_parser = yt_dlp.options.create_parser
|
||||
|
||||
|
||||
def parse_patched_options(opts):
|
||||
patched_parser = create_parser()
|
||||
patched_parser.defaults.update(
|
||||
{
|
||||
"ignoreerrors": False,
|
||||
"retries": 0,
|
||||
"fragment_retries": 0,
|
||||
"extract_flat": False,
|
||||
"concat_playlist": "never",
|
||||
"update_self": False,
|
||||
}
|
||||
)
|
||||
yt_dlp.options.create_parser = lambda: patched_parser
|
||||
try:
|
||||
return yt_dlp.parse_options(opts)
|
||||
finally:
|
||||
yt_dlp.options.create_parser = create_parser
|
||||
|
||||
|
||||
default_opts = parse_patched_options([]).ydl_opts
|
||||
|
||||
|
||||
def cli_to_api(opts, cli_defaults=False):
|
||||
opts = (yt_dlp.parse_options if cli_defaults else parse_patched_options)(opts).ydl_opts
|
||||
|
||||
diff = {k: v for k, v in opts.items() if default_opts[k] != v}
|
||||
if "postprocessors" in diff:
|
||||
diff["postprocessors"] = [
|
||||
pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]
|
||||
]
|
||||
return diff
|
||||
|
||||
|
||||
# --- END ----
|
||||
##############################################################
|
||||
|
||||
|
||||
def print_cli_to_sub(args: List[str]) -> None:
|
||||
api_args = cli_to_api(args)
|
||||
if not api_args:
|
||||
logger.info("Does not resolve to any yt-dlp args")
|
||||
return
|
||||
|
||||
print(dump_yaml({"ytdl_options": api_args}))
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import hashlib
|
||||
import re
|
||||
import shlex
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
|
||||
from mergedeep import mergedeep
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import argparse
|
||||
import dataclasses
|
||||
from typing import Dict, List
|
||||
from typing import List
|
||||
|
||||
from ytdl_sub import __local_version__
|
||||
from ytdl_sub.utils.logger import LoggerLevels
|
||||
|
|
@ -172,10 +172,6 @@ class SubArguments:
|
|||
short="-o",
|
||||
long="--dl-override",
|
||||
)
|
||||
SHUFFLE = CLIArgument(
|
||||
short="-sh",
|
||||
long="--shuffle",
|
||||
)
|
||||
|
||||
|
||||
subscription_parser = subparsers.add_parser("sub")
|
||||
|
|
@ -201,13 +197,6 @@ subscription_parser.add_argument(
|
|||
help="override all subscription config values using `dl` syntax, "
|
||||
"i.e. --dl-override='--ytdl_options.max_downloads 3'",
|
||||
)
|
||||
subscription_parser.add_argument(
|
||||
SubArguments.SHUFFLE.short,
|
||||
SubArguments.SHUFFLE.long,
|
||||
action="store_true",
|
||||
help="shuffle subscription order when downloading",
|
||||
default=False,
|
||||
)
|
||||
|
||||
###################################################################################################
|
||||
# DOWNLOAD PARSER
|
||||
|
|
@ -232,81 +221,3 @@ view_parser.add_argument(
|
|||
help="View source variables after splitting by chapters",
|
||||
)
|
||||
view_parser.add_argument("url", help="URL to view source variables for")
|
||||
|
||||
###################################################################################################
|
||||
# CLI-TO-SUB PARSER
|
||||
cli_to_sub_parser = subparsers.add_parser("cli-to-sub")
|
||||
|
||||
###################################################################################################
|
||||
# INSPECT PARSER
|
||||
|
||||
|
||||
class InspectArguments:
|
||||
LEVEL = CLIArgument(
|
||||
short="-l",
|
||||
long="--level",
|
||||
)
|
||||
LevelChoices: Dict[str, str] = {
|
||||
"0": "original",
|
||||
"1": "fill",
|
||||
"2": "resolve",
|
||||
"3": "internal",
|
||||
}
|
||||
|
||||
MOCK = CLIArgument(
|
||||
short="-k",
|
||||
long="--mock",
|
||||
)
|
||||
|
||||
|
||||
inspect_parser = subparsers.add_parser("inspect", formatter_class=argparse.RawTextHelpFormatter)
|
||||
inspect_parser.add_argument(
|
||||
InspectArguments.LEVEL.short,
|
||||
InspectArguments.LEVEL.long,
|
||||
metavar=",".join(str(i) for i in range(4)),
|
||||
type=str,
|
||||
help="""level of inspection to perform:
|
||||
0 - original present the subscription as-is
|
||||
1 - fill fill in defined values
|
||||
2 - resolve resolve all possible variables (default)
|
||||
3 - internal resolve all variables to their internal representation
|
||||
""",
|
||||
default="resolve",
|
||||
choices=list(InspectArguments.LevelChoices.keys())
|
||||
+ list(InspectArguments.LevelChoices.values()),
|
||||
dest="inspection_level",
|
||||
)
|
||||
inspect_parser.add_argument(
|
||||
MainArguments.MATCH.short,
|
||||
MainArguments.MATCH.long,
|
||||
dest="match",
|
||||
nargs="+",
|
||||
action="extend",
|
||||
type=str,
|
||||
help="match subscription names to one or more substrings, and only run those subscriptions",
|
||||
default=[],
|
||||
)
|
||||
|
||||
inspect_parser.add_argument(
|
||||
"subscription_paths",
|
||||
metavar="SUBPATH",
|
||||
nargs="*",
|
||||
help="path to subscription files, uses subscriptions.yaml if not provided",
|
||||
default=["subscriptions.yaml"],
|
||||
)
|
||||
|
||||
inspect_parser.add_argument(
|
||||
SubArguments.OVERRIDE.short,
|
||||
SubArguments.OVERRIDE.long,
|
||||
type=str,
|
||||
help="override all subscription config values using `dl` syntax, "
|
||||
"i.e. --dl-override='--ytdl_options.max_downloads 3'",
|
||||
)
|
||||
|
||||
inspect_parser.add_argument(
|
||||
InspectArguments.MOCK.short,
|
||||
InspectArguments.MOCK.long,
|
||||
metavar="VAR=VALUE",
|
||||
action="append",
|
||||
help="ability to mock one or more variable values, i.e. --mock 'title=Lets Play'",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from ytdl_sub.config.config_validator import ConfigValidator
|
||||
from ytdl_sub.config.preset import Preset
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
import os
|
||||
import posixpath
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from mergedeep import mergedeep
|
||||
from yt_dlp.utils import datetime_from_str
|
||||
|
||||
from ytdl_sub.config.defaults import (
|
||||
DEFAULT_FFMPEG_PATH,
|
||||
DEFAULT_FFPROBE_PATH,
|
||||
DEFAULT_LOCK_DIRECTORY,
|
||||
MAX_FILE_NAME_BYTES,
|
||||
)
|
||||
from ytdl_sub.config.defaults import DEFAULT_FFMPEG_PATH
|
||||
from ytdl_sub.config.defaults import DEFAULT_FFPROBE_PATH
|
||||
from ytdl_sub.config.defaults import DEFAULT_LOCK_DIRECTORY
|
||||
from ytdl_sub.config.defaults import MAX_FILE_NAME_BYTES
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS
|
||||
from ytdl_sub.utils.exceptions import SubscriptionPermissionError
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.validators.file_path_validators import FFmpegFileValidator, FFprobeFileValidator
|
||||
from ytdl_sub.validators.file_path_validators import FFmpegFileValidator
|
||||
from ytdl_sub.validators.file_path_validators import FFprobeFileValidator
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.validators import (
|
||||
BoolValidator,
|
||||
IntValidator,
|
||||
LiteralDictValidator,
|
||||
StringValidator,
|
||||
)
|
||||
from ytdl_sub.validators.validators import BoolValidator
|
||||
from ytdl_sub.validators.validators import IntValidator
|
||||
from ytdl_sub.validators.validators import LiteralDictValidator
|
||||
from ytdl_sub.validators.validators import StringValidator
|
||||
|
||||
|
||||
class ExperimentalValidator(StrictDictValidator):
|
||||
"""
|
||||
Experimental flags reside under the ``experimental`` key.
|
||||
"""
|
||||
|
||||
_optional_keys = {"enable_update_with_info_json"}
|
||||
_allow_extra_keys = True
|
||||
|
||||
|
|
@ -50,11 +43,6 @@ class ExperimentalValidator(StrictDictValidator):
|
|||
|
||||
|
||||
class PersistLogsValidator(StrictDictValidator):
|
||||
"""
|
||||
By default, no logs are persisted. Specifying this key will enable persisted logs. The following
|
||||
options are available.
|
||||
"""
|
||||
|
||||
_required_keys = {"logs_directory"}
|
||||
_optional_keys = {"keep_logs_after", "keep_successful_logs"}
|
||||
|
||||
|
|
@ -79,8 +67,7 @@ class PersistLogsValidator(StrictDictValidator):
|
|||
@property
|
||||
def logs_directory(self) -> str:
|
||||
"""
|
||||
Required field. Write log files to this directory with names like
|
||||
``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``.
|
||||
Required. The directory to store the logs in.
|
||||
"""
|
||||
return self._logs_directory.value
|
||||
|
||||
|
|
@ -105,54 +92,12 @@ class PersistLogsValidator(StrictDictValidator):
|
|||
@property
|
||||
def keep_successful_logs(self) -> bool:
|
||||
"""
|
||||
Defaults to ``True``. When this key is ``False``, only write log files for failed
|
||||
subscriptions.
|
||||
Optional. Whether to store logs when downloading is successful. Defaults to True.
|
||||
"""
|
||||
return self._keep_successful_logs.value
|
||||
|
||||
|
||||
class ConfigOptions(StrictDictValidator):
|
||||
"""
|
||||
ytdl-sub is configured using a ``config.yaml`` file.
|
||||
|
||||
The ``config.yaml`` is made up of two sections:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
configuration:
|
||||
presets:
|
||||
|
||||
|
||||
Note for Windows users, paths can be represented with ``C:/forward/slashes/like/linux``.
|
||||
If you prefer to use a Windows backslash, note that it must have
|
||||
``C:\\\\double\\\\bashslash\\\\paths`` in order to escape the backslash character. This is due
|
||||
to it being a YAML escape character.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
configuration:
|
||||
dl_aliases:
|
||||
mv: "--preset music_video"
|
||||
u: "--download.url"
|
||||
|
||||
experimental:
|
||||
enable_update_with_info_json: True
|
||||
|
||||
ffmpeg_path: "/usr/bin/ffmpeg"
|
||||
ffprobe_path: "/usr/bin/ffprobe"
|
||||
|
||||
file_name_max_bytes: 255
|
||||
lock_directory: "/tmp"
|
||||
|
||||
persist_logs:
|
||||
keep_successful_logs: True
|
||||
logs_directory: "/var/log/ytdl-sub-logs"
|
||||
|
||||
umask: "022"
|
||||
working_directory: ".ytdl-sub-working-directory"
|
||||
|
||||
"""
|
||||
|
||||
_optional_keys = {
|
||||
"working_directory",
|
||||
"umask",
|
||||
|
|
@ -198,18 +143,11 @@ class ConfigOptions(StrictDictValidator):
|
|||
key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
|
||||
)
|
||||
|
||||
if not FileHandler.is_path_writable(self.working_directory):
|
||||
raise SubscriptionPermissionError(
|
||||
"ytdl-sub does not have permissions to the working directory: "
|
||||
f"{self.working_directory}"
|
||||
)
|
||||
|
||||
@property
|
||||
def working_directory(self) -> str:
|
||||
"""
|
||||
The directory to temporarily store downloaded files before moving them into their final
|
||||
directory. Defaults to ``.ytdl-sub-working-directory``, created in the same directory
|
||||
that ytdl-sub is invoked from.
|
||||
directory. Defaults to .ytdl-sub-working-directory
|
||||
"""
|
||||
# Expands tildas to actual paths, use native os sep
|
||||
return os.path.expanduser(self._working_directory.value.replace(posixpath.sep, os.sep))
|
||||
|
|
@ -217,7 +155,7 @@ class ConfigOptions(StrictDictValidator):
|
|||
@property
|
||||
def umask(self) -> Optional[str]:
|
||||
"""
|
||||
Umask in octal format to apply to every created file. Defaults to ``022``.
|
||||
Umask (octal format) to apply to every created file. Defaults to "022".
|
||||
"""
|
||||
return self._umask.value
|
||||
|
||||
|
|
@ -226,7 +164,7 @@ class ConfigOptions(StrictDictValidator):
|
|||
"""
|
||||
.. _dl_aliases:
|
||||
|
||||
Alias definitions to shorten :ref:`dl arguments <usage:Download Options>`. For example,
|
||||
Alias definitions to shorten ``ytdl-sub dl`` arguments. For example,
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
|
@ -276,25 +214,24 @@ class ConfigOptions(StrictDictValidator):
|
|||
def lock_directory(self) -> str:
|
||||
"""
|
||||
The directory to temporarily store file locks, which prevents multiple instances
|
||||
of ``ytdl-sub`` from running. Note that file locks do not work on
|
||||
network-mounted directories. Ensure that this directory resides on the host
|
||||
machine. Defaults to ``/tmp``.
|
||||
of ``ytdl-sub`` from running. Note that file locks do not work on network-mounted
|
||||
directories. Ensure that this directory resides on the host machine. Defaults to ``/tmp``.
|
||||
"""
|
||||
return self._lock_directory.value
|
||||
|
||||
@property
|
||||
def ffmpeg_path(self) -> str:
|
||||
"""
|
||||
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux,
|
||||
``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows.
|
||||
Path to ffmpeg executable. Defaults to ``/usr/bin/ffmpeg`` for Linux, and
|
||||
``ffmpeg.exe`` for Windows (in the same directory as ytdl-sub).
|
||||
"""
|
||||
return self._ffmpeg_path.value
|
||||
|
||||
@property
|
||||
def ffprobe_path(self) -> str:
|
||||
"""
|
||||
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux,
|
||||
``./ffprobe.exe`` in the same directory as ytdl-sub for Windows.
|
||||
Path to ffprobe executable. Defaults to ``/usr/bin/ffprobe`` for Linux, and
|
||||
``ffprobe.exe`` for Windows (in the same directory as ytdl-sub).
|
||||
"""
|
||||
return self._ffprobe_path.value
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import os
|
|||
|
||||
from ytdl_sub.utils.system import IS_WINDOWS
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _existing_path(*paths: str) -> str:
|
||||
"""
|
||||
|
|
@ -24,7 +22,7 @@ if IS_WINDOWS:
|
|||
|
||||
MAX_FILE_NAME_BYTES = 255
|
||||
else:
|
||||
DEFAULT_LOCK_DIRECTORY = ".ytdl-sub-lock"
|
||||
DEFAULT_LOCK_DIRECTORY = "/tmp"
|
||||
DEFAULT_FFMPEG_PATH = os.getenv(
|
||||
"YTDL_SUB_FFMPEG_PATH", _existing_path("/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
from typing import Any, Dict, Iterable, Optional, Set, Type, TypeVar
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
import mergedeep
|
||||
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.variables.override_variables import (
|
||||
REQUIRED_OVERRIDE_VARIABLE_NAMES,
|
||||
OverrideHelpers,
|
||||
)
|
||||
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||
from ytdl_sub.entries.variables.override_variables import OverrideHelpers
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.types.function import BuiltInFunction
|
||||
from ytdl_sub.script.types.resolvable import Resolvable, String
|
||||
from ytdl_sub.script.types.syntax_tree import SyntaxTree
|
||||
from ytdl_sub.script.types.resolvable import Resolvable
|
||||
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
||||
from ytdl_sub.utils.exceptions import (
|
||||
InvalidVariableNameException,
|
||||
StringFormattingException,
|
||||
ValidationException,
|
||||
)
|
||||
from ytdl_sub.utils.exceptions import InvalidVariableNameException
|
||||
from ytdl_sub.utils.exceptions import StringFormattingException
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.utils.scriptable import Scriptable
|
||||
from ytdl_sub.validators.string_formatter_validators import (
|
||||
StringFormatterValidator,
|
||||
UnstructuredDictFormatterValidator,
|
||||
)
|
||||
|
||||
ExpectedT = TypeVar("ExpectedT")
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator
|
||||
|
||||
|
||||
class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
||||
|
|
@ -92,24 +88,6 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
|
||||
return True
|
||||
|
||||
def ensure_variable_names_not_a_plugin(self, plugin_names: Iterable[str]) -> None:
|
||||
"""
|
||||
Throws an error if an override variable or function has the same name as a
|
||||
preset key. This is to avoid confusion when accidentally defining things in
|
||||
overrides that are meant to be in the preset.
|
||||
"""
|
||||
for name in self.keys:
|
||||
if name.startswith("%"):
|
||||
name = name[1:]
|
||||
|
||||
if name in plugin_names:
|
||||
raise self._validation_exception(
|
||||
f"Override variable with name {name} cannot be used since it is"
|
||||
" the name of a plugin. Perhaps you meant to define it as a plugin? If so,"
|
||||
" indent it left to make it at the same level as overrides.",
|
||||
exception_class=InvalidVariableNameException,
|
||||
)
|
||||
|
||||
def ensure_variable_name_valid(self, name: str) -> None:
|
||||
"""
|
||||
Ensures the variable name does not collide with any entry variables or built-in functions.
|
||||
|
|
@ -137,35 +115,29 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
)
|
||||
|
||||
def initial_variables(
|
||||
self, unresolved_variables: Optional[Dict[str, SyntaxTree]] = None
|
||||
) -> Dict[str, SyntaxTree]:
|
||||
self, unresolved_variables: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
Variables and format strings for all Override variables + additional variables (Optional)
|
||||
"""
|
||||
initial_variables: Dict[str, SyntaxTree] = self.dict_with_parsed_format_strings
|
||||
if unresolved_variables:
|
||||
initial_variables |= unresolved_variables
|
||||
return ScriptUtils.add_sanitized_parsed_variables(initial_variables)
|
||||
initial_variables: Dict[str, str] = {}
|
||||
mergedeep.merge(
|
||||
initial_variables,
|
||||
self.dict_with_format_strings,
|
||||
unresolved_variables if unresolved_variables else {},
|
||||
)
|
||||
return ScriptUtils.add_sanitized_variables(initial_variables)
|
||||
|
||||
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
|
||||
"""
|
||||
Initialize the override script with any unresolved variables
|
||||
"""
|
||||
self.script.add_parsed(
|
||||
self.script.add(
|
||||
self.initial_variables(
|
||||
unresolved_variables={
|
||||
var_name: SyntaxTree(
|
||||
ast=[
|
||||
BuiltInFunction(
|
||||
name="throw",
|
||||
args=[
|
||||
String(f"Plugin variable {var_name} has not been created yet")
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
var_name: f"{{%throw('Plugin variable {var_name} has not been created yet')}}"
|
||||
for var_name in unresolved_variables
|
||||
}
|
||||
)
|
||||
|
|
@ -186,15 +158,10 @@ 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(
|
||||
|
|
@ -209,8 +176,7 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
formatter: StringFormatterValidator,
|
||||
entry: Optional[Entry] = None,
|
||||
function_overrides: Optional[Dict[str, str]] = None,
|
||||
expected_type: Type[ExpectedT] = str,
|
||||
) -> ExpectedT:
|
||||
) -> str:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
|
|
@ -220,8 +186,6 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
Optional. Entry to add source variables to the formatter
|
||||
function_overrides
|
||||
Optional. Explicit values to override the overrides themselves and source variables
|
||||
expected_type
|
||||
The expected type that should return. Defaults to string.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
|
@ -232,15 +196,37 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable):
|
|||
StringFormattingException
|
||||
If the formatter that is trying to be resolved cannot
|
||||
"""
|
||||
out = formatter.post_process(
|
||||
self._apply_to_resolvable(
|
||||
formatter=formatter, entry=entry, function_overrides=function_overrides
|
||||
).native
|
||||
return formatter.post_process(
|
||||
str(
|
||||
self._apply_to_resolvable(
|
||||
formatter=formatter, entry=entry, function_overrides=function_overrides
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if not isinstance(out, expected_type):
|
||||
raise StringFormattingException(
|
||||
f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'"
|
||||
)
|
||||
def apply_overrides_formatter_to_native(
|
||||
self,
|
||||
formatter: OverridesStringFormatterValidator,
|
||||
) -> Any:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
formatter
|
||||
Overrides formatter to apply
|
||||
|
||||
return out
|
||||
Returns
|
||||
-------
|
||||
The native python form of the resolved variable
|
||||
"""
|
||||
return self._apply_to_resolvable(
|
||||
formatter=formatter, entry=None, function_overrides=None
|
||||
).native
|
||||
|
||||
def evaluate_boolean(
|
||||
self, formatter: StringFormatterValidator, entry: Optional[Entry] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Apply a formatter, and evaluate it to a boolean
|
||||
"""
|
||||
output = self.apply_formatter(formatter=formatter, entry=entry)
|
||||
return ScriptUtils.bool_formatter_output(output)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from functools import cached_property
|
||||
from typing import Dict, Generic, List, Optional, Tuple, Type
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT, ToggleableOptionsDictValidator
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import (
|
||||
DownloadArchiver,
|
||||
EnhancedDownloadArchive,
|
||||
)
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
|
@ -43,7 +48,7 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
|
|||
Returns True if enabled, False if disabled.
|
||||
"""
|
||||
if isinstance(self.plugin_options, ToggleableOptionsDictValidator):
|
||||
return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool)
|
||||
return self.overrides.evaluate_boolean(self.plugin_options.enable)
|
||||
return True
|
||||
|
||||
def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]:
|
||||
|
|
@ -114,17 +119,6 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC):
|
|||
"""
|
||||
return None
|
||||
|
||||
def post_completion_entry(self, file_metadata: FileMetadata) -> None:
|
||||
"""
|
||||
After the entry file is moved to its final location, run this hook.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file_metadata
|
||||
Metadata about the completed entry's file download
|
||||
"""
|
||||
return None
|
||||
|
||||
def post_process_subscription(self):
|
||||
"""
|
||||
After all downloaded files have been post-processed, apply a subscription-wide post process
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from typing import Dict, List, Optional, Tuple, Type
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin, SplitPlugin
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.plugin.plugin import SplitPlugin
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.downloaders.url.downloader import (
|
||||
UrlDownloaderCollectionVariablePlugin,
|
||||
UrlDownloaderThumbnailPlugin,
|
||||
)
|
||||
from ytdl_sub.downloaders.url.downloader import UrlDownloaderCollectionVariablePlugin
|
||||
from ytdl_sub.downloaders.url.downloader import UrlDownloaderThumbnailPlugin
|
||||
from ytdl_sub.plugins.audio_extract import AudioExtractPlugin
|
||||
from ytdl_sub.plugins.chapters import ChaptersPlugin
|
||||
from ytdl_sub.plugins.date_range import DateRangePlugin
|
||||
|
|
@ -89,12 +92,6 @@ class PluginMapping:
|
|||
EmbedThumbnailPlugin,
|
||||
]
|
||||
|
||||
_ORDER_POST_COMPLETION: List[Type[Plugin]] = [
|
||||
# Throttle protection should always be last
|
||||
# to not sleep over other logic
|
||||
ThrottleProtectionPlugin
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def _order_by(
|
||||
cls, plugin_types: List[Type[Plugin]], operation: PluginOperation
|
||||
|
|
@ -105,8 +102,6 @@ class PluginMapping:
|
|||
ordering = cls._ORDER_MODIFY_ENTRY
|
||||
elif operation == PluginOperation.POST_PROCESS:
|
||||
ordering = cls._ORDER_POST_PROCESS
|
||||
elif operation == PluginOperation.POST_COMPLETION:
|
||||
ordering = cls._ORDER_POST_COMPLETION
|
||||
else:
|
||||
raise ValueError("PluginOperation does not support ordering")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,3 @@ class PluginOperation(Enum):
|
|||
MODIFY_ENTRY_METADATA = 0
|
||||
MODIFY_ENTRY = 1
|
||||
POST_PROCESS = 2
|
||||
POST_COMPLETION = 3
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
from typing import Iterable, List, Optional, Set, Tuple, Type
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import OptionsValidator, OptionsValidatorT
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT
|
||||
|
||||
|
||||
class PresetPlugins:
|
||||
|
|
@ -40,34 +44,3 @@ class PresetPlugins:
|
|||
if plugin_type in plugin_option_types:
|
||||
return self.plugin_options[plugin_option_types.index(plugin_type)]
|
||||
return None
|
||||
|
||||
def get_added_and_modified_variables(
|
||||
self, additional_options: List[OptionsValidator]
|
||||
) -> Iterable[Tuple[OptionsValidator, Set[str], Set[str]]]:
|
||||
"""
|
||||
Iterates and returns the plugin options, added variables, modified variables
|
||||
"""
|
||||
for plugin_options in self.plugin_options + additional_options:
|
||||
added_variables: Set[str] = set()
|
||||
modified_variables: Set[str] = set()
|
||||
|
||||
for plugin_added_variables in plugin_options.added_variables(
|
||||
unresolved_variables=set(),
|
||||
).values():
|
||||
added_variables |= set(plugin_added_variables)
|
||||
|
||||
for plugin_modified_variables in plugin_options.modified_variables().values():
|
||||
modified_variables = plugin_modified_variables
|
||||
|
||||
yield plugin_options, added_variables, modified_variables
|
||||
|
||||
def get_all_variables(self, additional_options: List[OptionsValidator]) -> Set[str]:
|
||||
"""
|
||||
Returns set of all added and modified variables' names.
|
||||
"""
|
||||
all_variables: Set[str] = set()
|
||||
for _, added, modified in self.get_added_and_modified_variables(additional_options):
|
||||
all_variables.update(added)
|
||||
all_variables.update(modified)
|
||||
|
||||
return all_variables
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import copy
|
||||
from typing import Any, Dict, List, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
from mergedeep import mergedeep
|
||||
|
||||
|
|
@ -7,14 +9,18 @@ from ytdl_sub.config.config_validator import ConfigValidator
|
|||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
||||
from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
||||
from ytdl_sub.config.preset_options import OutputOptions, YTDLOptions
|
||||
from ytdl_sub.config.preset_options import OutputOptions
|
||||
from ytdl_sub.config.preset_options import YTDLOptions
|
||||
from ytdl_sub.config.validators.variable_validation import VariableValidation
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES, PUBLISHED_PRESET_NAMES
|
||||
from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES
|
||||
from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
from ytdl_sub.utils.yaml import dump_yaml
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.validators import StringListValidator, validation_exception
|
||||
from ytdl_sub.validators.validators import StringListValidator
|
||||
from ytdl_sub.validators.validators import validation_exception
|
||||
|
||||
PRESET_KEYS = {
|
||||
"preset",
|
||||
|
|
@ -49,12 +55,6 @@ class _PresetShell(StrictDictValidator):
|
|||
|
||||
|
||||
class Preset(_PresetShell):
|
||||
"""
|
||||
Custom presets are defined in this section. Refer to the
|
||||
:ref:`Getting Started Guide<guides/getting_started/first_config:Basic Configuration>`
|
||||
on how to configure.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def preset_partial_validate(cls, config: ConfigValidator, name: str, value: Any) -> None:
|
||||
"""
|
||||
|
|
@ -172,37 +172,6 @@ class Preset(_PresetShell):
|
|||
mergedeep.merge({}, *reversed(presets_to_merge), strategy=mergedeep.Strategy.ADDITIVE)
|
||||
)
|
||||
|
||||
def _initialize_overrides_script(self, overrides: Overrides) -> Overrides:
|
||||
"""
|
||||
Do some gymnastics to initialize the Overrides script.
|
||||
"""
|
||||
unresolved_variables: Set[str] = set()
|
||||
|
||||
for (
|
||||
plugin_options,
|
||||
added_variables,
|
||||
modified_variables,
|
||||
) in self.plugins.get_added_and_modified_variables(
|
||||
additional_options=[self.downloader_options, self.output_options]
|
||||
):
|
||||
for added_variable in added_variables:
|
||||
if not overrides.ensure_added_plugin_variable_valid(added_variable=added_variable):
|
||||
# pylint: disable=protected-access
|
||||
raise plugin_options._validation_exception(
|
||||
f"Cannot use the variable name {added_variable} because it exists as a"
|
||||
" built-in ytdl-sub variable name."
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
|
||||
# Set unresolved as variables that are added but do not exist as
|
||||
# entry/override variables since they are created at run-time
|
||||
unresolved_variables |= added_variables | modified_variables
|
||||
|
||||
# Initialize overrides with unresolved variables + modified variables to throw an error.
|
||||
# For modified variables, this is to prevent a resolve(update=True) to setting any
|
||||
# dependencies until it has been explicitly added
|
||||
return overrides.initialize_script(unresolved_variables=unresolved_variables)
|
||||
|
||||
def __init__(self, config: ConfigValidator, name: str, value: Any):
|
||||
super().__init__(name=name, value=value)
|
||||
|
||||
|
|
@ -223,10 +192,13 @@ class Preset(_PresetShell):
|
|||
)
|
||||
|
||||
self.plugins: PresetPlugins = self._validate_and_get_plugins()
|
||||
self.overrides = self._initialize_overrides_script(
|
||||
overrides=self._validate_key(key="overrides", validator=Overrides, default={})
|
||||
)
|
||||
self.overrides.ensure_variable_names_not_a_plugin(plugin_names=PRESET_KEYS)
|
||||
self.overrides = self._validate_key(key="overrides", validator=Overrides, default={})
|
||||
|
||||
VariableValidation(
|
||||
downloader_options=self.downloader_options,
|
||||
output_options=self.output_options,
|
||||
plugins=self.plugins,
|
||||
).initialize_preset_overrides(overrides=self.overrides).ensure_proper_usage()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
@ -255,18 +227,11 @@ class Preset(_PresetShell):
|
|||
"""
|
||||
return cls(config=config, name=preset_name, value=preset_dict)
|
||||
|
||||
def yaml(self, subscription_only: bool) -> str:
|
||||
@property
|
||||
def yaml(self) -> str:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
subscription_only:
|
||||
Only include the subscription contents, not the surrounding boiler-plate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Preset in YAML format
|
||||
"""
|
||||
if subscription_only:
|
||||
return dump_yaml(self._value)
|
||||
|
||||
return dump_yaml({"presets": {self._name: self._value}})
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
from typing import Any, Dict, Optional, Set
|
||||
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, ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.validators.file_path_validators import (
|
||||
OverridesStringFormatterFilePathValidator,
|
||||
StringFormatterFileNameValidator,
|
||||
)
|
||||
from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator
|
||||
from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator
|
||||
from ytdl_sub.validators.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 (
|
||||
OverridesIntegerFormatterValidator,
|
||||
OverridesStringFormatterValidator,
|
||||
StandardizedDateValidator,
|
||||
StringFormatterValidator,
|
||||
UnstructuredOverridesDictFormatterValidator,
|
||||
)
|
||||
from ytdl_sub.validators.validators import BoolValidator
|
||||
|
|
@ -58,24 +57,12 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator):
|
|||
def to_native_dict(self, overrides: Overrides) -> Dict:
|
||||
"""
|
||||
Materializes the entire ytdl-options dict from OverrideStringFormatters into
|
||||
native python.
|
||||
native python
|
||||
"""
|
||||
out = {
|
||||
key: overrides.apply_formatter(val, expected_type=object)
|
||||
return {
|
||||
key: overrides.apply_overrides_formatter_to_native(val)
|
||||
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
|
||||
|
|
@ -120,7 +107,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
"keep_max_files",
|
||||
"download_archive_standardized_date",
|
||||
"keep_files_date_eval",
|
||||
"preserve_mtime",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -184,10 +170,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
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
|
||||
) and not self.maintain_download_archive:
|
||||
|
|
@ -327,17 +309,6 @@ class OutputOptions(OptionsDictValidator):
|
|||
"""
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from abc import ABC
|
||||
from typing import Dict, Set, TypeVar
|
||||
from typing import Dict
|
||||
from typing import Set
|
||||
from typing import TypeVar
|
||||
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
|
|
@ -59,9 +61,9 @@ class ToggleableOptionsDictValidator(OptionsDictValidator):
|
|||
_optional_keys = {"enable"}
|
||||
|
||||
def __init__(self, name, value):
|
||||
assert "enable" in self._optional_keys, (
|
||||
f"{self.__class__.__name__} does not have enable as an optional field"
|
||||
)
|
||||
assert (
|
||||
"enable" in self._optional_keys
|
||||
), f"{self.__class__.__name__} does not have enable as an optional field"
|
||||
super().__init__(name, value)
|
||||
|
||||
self._enable = self._validate_key(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
from typing import Dict, List, Optional, Set
|
||||
import copy
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin_mapping import PluginMapping
|
||||
|
|
@ -7,212 +13,203 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
|
|||
from ytdl_sub.config.preset_options import OutputOptions
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES, VARIABLES
|
||||
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||
from ytdl_sub.script.script import Script
|
||||
from ytdl_sub.script.utils.name_validation import is_function
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.script.script import _is_function
|
||||
from ytdl_sub.utils.scriptable import BASE_SCRIPT
|
||||
from ytdl_sub.validators.string_formatter_validators import to_variable_dependency_format_string
|
||||
from ytdl_sub.validators.string_formatter_validators import validate_formatters
|
||||
|
||||
# Entry variables to mock during validation
|
||||
_DUMMY_ENTRY_VARIABLES: Dict[str, str] = {
|
||||
name: to_variable_dependency_format_string(
|
||||
# pylint: disable=protected-access
|
||||
script=BASE_SCRIPT,
|
||||
parsed_format_string=BASE_SCRIPT._variables[name],
|
||||
# pylint: enable=protected-access
|
||||
)
|
||||
for name in BASE_SCRIPT.variable_names
|
||||
}
|
||||
|
||||
class ResolutionLevel:
|
||||
ORIGINAL = 0
|
||||
FILL = 1
|
||||
RESOLVE = 2
|
||||
INTERNAL = 3
|
||||
|
||||
@classmethod
|
||||
def name_of(cls, resolution_level: int) -> str:
|
||||
"""
|
||||
Name of the resolution level.
|
||||
"""
|
||||
if resolution_level == cls.ORIGINAL:
|
||||
return "original"
|
||||
if resolution_level == cls.FILL:
|
||||
return "fill"
|
||||
if resolution_level == cls.RESOLVE:
|
||||
return "resolve"
|
||||
if resolution_level == cls.INTERNAL:
|
||||
return "internal"
|
||||
raise ValueError("Invalid resolution level")
|
||||
def _add_dummy_variables(variables: Iterable[str]) -> Dict[str, str]:
|
||||
dummy_variables: Dict[str, str] = {}
|
||||
for var in variables:
|
||||
dummy_variables[var] = ""
|
||||
dummy_variables[f"{var}_sanitized"] = ""
|
||||
|
||||
@classmethod
|
||||
def level_number(cls, resolution_arg: str) -> int:
|
||||
"""
|
||||
Numeric resolution level
|
||||
"""
|
||||
if resolution_arg in ("0", "original"):
|
||||
return 0
|
||||
if resolution_arg in ("1", "fill"):
|
||||
return 1
|
||||
if resolution_arg in ("2", "resolve"):
|
||||
return 2
|
||||
if resolution_arg in ("3", "internal"):
|
||||
return 3
|
||||
raise ValueError("Invalid resolution level")
|
||||
return dummy_variables
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> List[int]:
|
||||
"""
|
||||
All possible resolution levels.
|
||||
"""
|
||||
return [cls.ORIGINAL, cls.FILL, cls.RESOLVE, cls.INTERNAL]
|
||||
|
||||
def _add_dummy_overrides(overrides: Overrides) -> Dict[str, str]:
|
||||
# Have the dummy override variable contain all variable deps that it uses in the string
|
||||
dummy_overrides: Dict[str, str] = {}
|
||||
for override_name in _override_variables(overrides):
|
||||
if _is_function(override_name):
|
||||
continue
|
||||
|
||||
# pylint: disable=protected-access
|
||||
dummy_overrides[override_name] = to_variable_dependency_format_string(
|
||||
script=overrides.script, parsed_format_string=overrides.script._variables[override_name]
|
||||
)
|
||||
# pylint: enable=protected-access
|
||||
return dummy_overrides
|
||||
|
||||
|
||||
def _get_added_and_modified_variables(
|
||||
plugins: PresetPlugins, downloader_options: MultiUrlValidator, output_options: OutputOptions
|
||||
) -> 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)
|
||||
options.append(output_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()))
|
||||
|
||||
|
||||
class VariableValidation:
|
||||
def _get_resolve_partial_filter(self) -> Set[str]:
|
||||
# Exclude sanitized variables from partial validation. This lessens the work
|
||||
# and prevents double-evaluation, which can lead to bad behavior like double-prints.
|
||||
return {
|
||||
name
|
||||
for name in self.script.variable_names
|
||||
if name not in self.unresolved_variables and not name.endswith("_sanitized")
|
||||
}
|
||||
|
||||
def _apply_resolution_level(self, mocks: Optional[Dict[str, str]]) -> None:
|
||||
if self._resolution_level == ResolutionLevel.FILL:
|
||||
self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True)
|
||||
# Only partial resolve definitions that are already resolved
|
||||
self.unresolved_variables |= {
|
||||
name
|
||||
for name in self.overrides.keys
|
||||
if not is_function(name) and not self.script.definition_of(name).maybe_resolvable
|
||||
}
|
||||
elif self._resolution_level == ResolutionLevel.RESOLVE:
|
||||
# Partial resolve everything, but not including internal variables
|
||||
self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True)
|
||||
elif self._resolution_level == ResolutionLevel.INTERNAL:
|
||||
# Partial resolve everything including internal variables
|
||||
pass
|
||||
else:
|
||||
raise ValueError("Invalid resolution level for validation")
|
||||
|
||||
if mocks is not None:
|
||||
for mock_name in mocks.keys():
|
||||
if mock_name in self.unresolved_variables:
|
||||
self.unresolved_variables.remove(mock_name)
|
||||
|
||||
self.script.add(
|
||||
variables=mocks,
|
||||
unresolvable=self.unresolved_variables,
|
||||
)
|
||||
|
||||
self.script = self.script.resolve_partial(
|
||||
unresolvable=self.unresolved_variables,
|
||||
output_filter=self._get_resolve_partial_filter(),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
overrides: Overrides,
|
||||
downloader_options: MultiUrlValidator,
|
||||
output_options: OutputOptions,
|
||||
plugins: PresetPlugins,
|
||||
resolution_level: int = ResolutionLevel.RESOLVE,
|
||||
mocks: Optional[Dict[str, str]] = None,
|
||||
):
|
||||
self.overrides = overrides
|
||||
self.downloader_options = downloader_options
|
||||
self.output_options = output_options
|
||||
self.plugins = plugins
|
||||
|
||||
self.script: Script = self.overrides.script
|
||||
self.unresolved_variables = (
|
||||
self.plugins.get_all_variables(
|
||||
additional_options=[self.output_options, self.downloader_options]
|
||||
)
|
||||
| UNRESOLVED_VARIABLES
|
||||
)
|
||||
self.unresolved_runtime_variables = self.plugins.get_all_variables(
|
||||
additional_options=[self.output_options, self.downloader_options]
|
||||
)
|
||||
self._resolution_level = resolution_level
|
||||
self._apply_resolution_level(mocks=mocks)
|
||||
self.script: Optional[Script] = None
|
||||
self.resolved_variables: Set[str] = set()
|
||||
self.unresolved_variables: Set[str] = set()
|
||||
|
||||
def _add_runtime_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
|
||||
def initialize_preset_overrides(self, overrides: Overrides) -> "VariableValidation":
|
||||
"""
|
||||
Do some gymnastics to initialize the Overrides script.
|
||||
"""
|
||||
override_variables = set(list(overrides.initial_variables().keys()))
|
||||
|
||||
# Set resolved variables as all entry + override variables
|
||||
# at this point to generate every possible added/modified variable
|
||||
self.resolved_variables = set(_DUMMY_ENTRY_VARIABLES.keys()) | override_variables
|
||||
plugin_variables: Set[str] = set()
|
||||
|
||||
for (
|
||||
plugin_options,
|
||||
added_variables,
|
||||
modified_variables,
|
||||
) in _get_added_and_modified_variables(
|
||||
plugins=self.plugins,
|
||||
downloader_options=self.downloader_options,
|
||||
output_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
|
||||
self.unresolved_variables |= added_variables | modified_variables
|
||||
plugin_variables |= added_variables | modified_variables
|
||||
|
||||
# Then update resolved variables to reflect that
|
||||
self.resolved_variables -= self.unresolved_variables
|
||||
|
||||
# Initialize overrides with unresolved variables + modified variables to throw an error.
|
||||
# For modified variables, this is to prevent a resolve(update=True) to setting any
|
||||
# dependencies until it has been explicitly added
|
||||
overrides = overrides.initialize_script(unresolved_variables=self.unresolved_variables)
|
||||
|
||||
# copy the script and mock entry variables
|
||||
self.script = copy.deepcopy(overrides.script)
|
||||
self.script.add(
|
||||
variables=_add_dummy_overrides(overrides=overrides)
|
||||
| _add_dummy_variables(variables=plugin_variables)
|
||||
| _DUMMY_ENTRY_VARIABLES
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def _update_script(self) -> None:
|
||||
_ = self.script.resolve(unresolvable=self.unresolved_variables, update=True)
|
||||
|
||||
def _add_subscription_override_variables(self) -> None:
|
||||
"""
|
||||
Add dummy subscription variables for script validation
|
||||
"""
|
||||
self.resolved_variables |= REQUIRED_OVERRIDE_VARIABLE_NAMES
|
||||
|
||||
def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
|
||||
"""
|
||||
Add dummy variables for script validation
|
||||
"""
|
||||
added_variables = options.added_variables(
|
||||
unresolved_variables=self.unresolved_runtime_variables,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
).get(plugin_op, set())
|
||||
modified_variables = options.modified_variables().get(plugin_op, set())
|
||||
|
||||
self.unresolved_runtime_variables -= added_variables | modified_variables
|
||||
resolved_variables = added_variables | modified_variables
|
||||
|
||||
def _output_override_variables(self) -> Dict:
|
||||
output = {}
|
||||
for name in self.overrides.keys:
|
||||
value = self.script.definition_of(name)
|
||||
if name in self.script.function_names:
|
||||
# Keep custom functions as-is
|
||||
output[name] = self.overrides.dict_with_format_strings[name]
|
||||
elif resolved := value.maybe_resolvable:
|
||||
output[name] = resolved.native
|
||||
else:
|
||||
output[name] = ScriptUtils.to_native_script(value)
|
||||
self.resolved_variables |= resolved_variables
|
||||
self.unresolved_variables -= resolved_variables
|
||||
|
||||
return output
|
||||
|
||||
def ensure_proper_usage(self, partial_resolve_formatters: bool = False) -> Dict:
|
||||
def ensure_proper_usage(self) -> None:
|
||||
"""
|
||||
Validate variables resolve as plugins are executed, and return
|
||||
a mock script which contains actualized added variables from the plugins
|
||||
"""
|
||||
resolved_subscription: Dict = {}
|
||||
|
||||
self._add_runtime_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
|
||||
self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
|
||||
self._add_subscription_override_variables()
|
||||
|
||||
# Always add output options first
|
||||
self._add_runtime_variables(
|
||||
PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options
|
||||
)
|
||||
self._add_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_runtime_variables(
|
||||
PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options
|
||||
)
|
||||
self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options)
|
||||
|
||||
for plugin_options in PluginMapping.order_options_by(
|
||||
self.plugins.zipped(), PluginOperation.MODIFY_ENTRY
|
||||
):
|
||||
self._add_runtime_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
|
||||
self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options)
|
||||
|
||||
# Validate that any formatter in the plugin options can resolve
|
||||
resolved_subscription |= validate_formatters(
|
||||
validate_formatters(
|
||||
script=self.script,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
unresolved_runtime_variables=self.unresolved_runtime_variables,
|
||||
validator=plugin_options,
|
||||
partial_resolve_formatters=partial_resolve_formatters,
|
||||
)
|
||||
|
||||
resolved_subscription |= validate_formatters(
|
||||
validate_formatters(
|
||||
script=self.script,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
unresolved_runtime_variables=self.unresolved_runtime_variables,
|
||||
validator=self.output_options,
|
||||
partial_resolve_formatters=partial_resolve_formatters,
|
||||
)
|
||||
|
||||
# TODO: make this a function
|
||||
raw_download_output = validate_formatters(
|
||||
script=self.script,
|
||||
unresolved_variables=self.unresolved_variables,
|
||||
unresolved_runtime_variables=self.unresolved_runtime_variables,
|
||||
validator=self.downloader_options.urls,
|
||||
partial_resolve_formatters=partial_resolve_formatters,
|
||||
)
|
||||
resolved_subscription["download"] = []
|
||||
for url_output in raw_download_output["download"]:
|
||||
if isinstance(url_output["url"], list):
|
||||
url_output["url"] = [url for url in url_output["url"] if bool(url)]
|
||||
|
||||
if url_output["url"]:
|
||||
resolved_subscription["download"].append(url_output)
|
||||
|
||||
resolved_subscription["overrides"] = self._output_override_variables()
|
||||
|
||||
return resolved_subscription
|
||||
assert not self.unresolved_variables
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.validators.options import OptionsDictValidator
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePlugin
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import (
|
||||
VARIABLE_SCRIPTS,
|
||||
VARIABLES,
|
||||
VariableDefinitions,
|
||||
)
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.file_handler import FileHandler, get_file_extension
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import (
|
||||
DownloadMapping,
|
||||
EnhancedDownloadArchive,
|
||||
)
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.file_handler import get_file_extension
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadMapping
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import abc
|
||||
from abc import ABC
|
||||
from typing import Dict, Generic, Iterable, List, Optional, Type, final
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import final
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin import BasePlugin, Plugin
|
||||
from ytdl_sub.config.plugin.plugin import BasePlugin
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import OptionsValidatorT
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
|
|
|
|||
|
|
@ -1,29 +1,33 @@
|
|||
import contextlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from yt_dlp.utils import RejectedVideoReached
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePlugin, SourcePluginExtension
|
||||
from ytdl_sub.downloaders.url.validators import (
|
||||
MultiUrlValidator,
|
||||
UrlThumbnailListValidator,
|
||||
UrlValidator,
|
||||
)
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePlugin
|
||||
from ytdl_sub.downloaders.source_plugin import SourcePluginExtension
|
||||
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
|
||||
from ytdl_sub.downloaders.url.validators import UrlThumbnailListValidator
|
||||
from ytdl_sub.downloaders.url.validators import UrlValidator
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.downloaders.ytdlp import YTDLP
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.entry_parent import EntryParent
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
from ytdl_sub.utils.thumbnail import (
|
||||
ThumbnailTypes,
|
||||
download_and_convert_url_thumbnail,
|
||||
try_convert_download_thumbnail,
|
||||
)
|
||||
from ytdl_sub.utils.thumbnail import ThumbnailTypes
|
||||
from ytdl_sub.utils.thumbnail import download_and_convert_url_thumbnail
|
||||
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
|
@ -48,12 +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 entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list):
|
||||
if self.overrides.apply_formatter(validator.url) == entry_input_url:
|
||||
return validator
|
||||
|
||||
# Match the first validator based on the URL, if one exists
|
||||
for validator in self.plugin_options.urls.list:
|
||||
if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list):
|
||||
if self.overrides.apply_formatter(validator.url) == entry_input_url:
|
||||
return validator
|
||||
|
||||
# Return the first validator if none exist
|
||||
|
|
@ -93,6 +97,7 @@ class UrlDownloaderThumbnailPlugin(UrlDownloaderBasePluginExtension):
|
|||
|
||||
# If latest entry, always update the thumbnail on each entry
|
||||
if thumbnail_id == ThumbnailTypes.LATEST_ENTRY:
|
||||
|
||||
# always save in dry-run even if it doesn't exist...
|
||||
if self.is_dry_run or entry.is_thumbnail_downloaded():
|
||||
self.save_file(
|
||||
|
|
@ -252,16 +257,6 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
.to_dict()
|
||||
)
|
||||
|
||||
def webpage_url(self, entry: Entry) -> str:
|
||||
"""
|
||||
Returns
|
||||
-------
|
||||
The webpage_url to use for the actual download
|
||||
"""
|
||||
url_idx = entry.get(v.ytdl_sub_input_url_index, int)
|
||||
webpage_url_formatter = self.plugin_options.urls.list[url_idx].webpage_url
|
||||
return self.overrides.apply_formatter(webpage_url_formatter, entry=entry)
|
||||
|
||||
def metadata_ytdl_options(self, ytdl_option_overrides: Dict) -> Dict:
|
||||
"""
|
||||
Returns
|
||||
|
|
@ -363,7 +358,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
if (self.is_dry_run or not self.is_entry_thumbnails_enabled)
|
||||
else entry.is_thumbnail_downloaded_via_ytdlp
|
||||
),
|
||||
url=self.webpage_url(entry=entry),
|
||||
url=entry.webpage_url,
|
||||
)
|
||||
return Entry(
|
||||
download_entry_dict,
|
||||
|
|
@ -371,13 +366,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
)
|
||||
|
||||
def _iterate_child_entries(
|
||||
self, entries: List[Entry], validator: UrlValidator
|
||||
self, entries: List[Entry], download_reversed: bool
|
||||
) -> Iterator[Entry]:
|
||||
# Iterate a list of entries, and delete the entries after yielding
|
||||
entries_to_iter: List[Optional[Entry]] = entries
|
||||
|
||||
indices = list(range(len(entries_to_iter)))
|
||||
if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool):
|
||||
if download_reversed:
|
||||
indices = reversed(indices)
|
||||
|
||||
for idx in indices:
|
||||
|
|
@ -399,13 +394,17 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
entries_to_iter[idx] = None
|
||||
|
||||
def _iterate_parent_entry(
|
||||
self, parent: EntryParent, validator: UrlValidator
|
||||
self, parent: EntryParent, download_reversed: bool
|
||||
) -> Iterator[Entry]:
|
||||
yield from self._iterate_child_entries(entries=parent.entry_children(), validator=validator)
|
||||
yield from self._iterate_child_entries(
|
||||
entries=parent.entry_children(), download_reversed=download_reversed
|
||||
)
|
||||
|
||||
# Recursion the parent's parent entries
|
||||
for parent_child in reversed(parent.parent_children()):
|
||||
yield from self._iterate_parent_entry(parent=parent_child, validator=validator)
|
||||
yield from self._iterate_parent_entry(
|
||||
parent=parent_child, download_reversed=download_reversed
|
||||
)
|
||||
|
||||
def _download_url_metadata(
|
||||
self, url: str, include_sibling_metadata: bool, ytdl_options_overrides: Dict
|
||||
|
|
@ -439,7 +438,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
self,
|
||||
parents: List[EntryParent],
|
||||
orphans: List[Entry],
|
||||
validator: UrlValidator,
|
||||
download_reversed: bool,
|
||||
) -> Iterator[Entry]:
|
||||
"""
|
||||
Downloads the leaf entries from EntryParent trees
|
||||
|
|
@ -447,17 +446,21 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
# Delete info json files afterwards so other collection URLs do not use them
|
||||
with self._separate_download_archives(clear_info_json_files=True):
|
||||
for parent in parents:
|
||||
yield from self._iterate_parent_entry(parent=parent, validator=validator)
|
||||
yield from self._iterate_parent_entry(
|
||||
parent=parent, download_reversed=download_reversed
|
||||
)
|
||||
|
||||
yield from self._iterate_child_entries(entries=orphans, validator=validator)
|
||||
yield from self._iterate_child_entries(
|
||||
entries=orphans, download_reversed=download_reversed
|
||||
)
|
||||
|
||||
def _download_metadata(self, url: str, validator: UrlValidator) -> Iterable[Entry]:
|
||||
metadata_ytdl_options = self.metadata_ytdl_options(
|
||||
ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides)
|
||||
)
|
||||
|
||||
include_sibling_metadata = self.overrides.apply_formatter(
|
||||
validator.include_sibling_metadata, expected_type=bool
|
||||
download_reversed = self.overrides.evaluate_boolean(validator.download_reverse)
|
||||
include_sibling_metadata = self.overrides.evaluate_boolean(
|
||||
validator.include_sibling_metadata
|
||||
)
|
||||
|
||||
parents, orphan_entries = self._download_url_metadata(
|
||||
|
|
@ -466,15 +469,16 @@ 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,
|
||||
validator=validator,
|
||||
download_reversed=download_reversed,
|
||||
)
|
||||
|
||||
def download_metadata(self) -> Iterable[Entry]:
|
||||
|
|
@ -482,25 +486,19 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
|
|||
# download the bottom-most urls first since they are top-priority
|
||||
for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))):
|
||||
# URLs can be empty. If they are, then skip
|
||||
if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)):
|
||||
if not (url := self.overrides.apply_formatter(url_validator.url)):
|
||||
continue
|
||||
|
||||
for url in reversed(urls):
|
||||
assert isinstance(url, str)
|
||||
for entry in self._download_metadata(url=url, validator=url_validator):
|
||||
entry.initialize_script(self.overrides).add(
|
||||
{
|
||||
v.ytdl_sub_input_url: url,
|
||||
v.ytdl_sub_input_url_index: idx,
|
||||
v.ytdl_sub_input_url_count: len(self.collection.urls.list),
|
||||
}
|
||||
)
|
||||
|
||||
if not url:
|
||||
continue
|
||||
|
||||
for entry in self._download_metadata(url=url, validator=url_validator):
|
||||
entry.initialize_script(self.overrides).add(
|
||||
{
|
||||
v.ytdl_sub_input_url: url,
|
||||
v.ytdl_sub_input_url_index: idx,
|
||||
v.ytdl_sub_input_url_count: len(self.collection.urls.list),
|
||||
}
|
||||
)
|
||||
|
||||
yield entry
|
||||
yield entry
|
||||
|
||||
def download(self, entry: Entry) -> Optional[Entry]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.preset_options import YTDLOptions
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
from ytdl_sub.script.parser import parse
|
||||
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import (
|
||||
DictFormatterValidator,
|
||||
OverridesBooleanFormatterValidator,
|
||||
OverridesStringFormatterValidator,
|
||||
StringFormatterValidator,
|
||||
)
|
||||
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ class UrlThumbnailValidator(StrictDictValidator):
|
|||
def __init__(self, name, value):
|
||||
super().__init__(name, value)
|
||||
|
||||
self._thumb_name = self._validate_key(key="name", validator=StringFormatterValidator)
|
||||
self._name = self._validate_key(key="name", validator=StringFormatterValidator)
|
||||
self._uid = self._validate_key(key="uid", validator=OverridesStringFormatterValidator)
|
||||
|
||||
@property
|
||||
|
|
@ -28,7 +29,7 @@ class UrlThumbnailValidator(StrictDictValidator):
|
|||
"""
|
||||
File name for the thumbnail
|
||||
"""
|
||||
return self._thumb_name
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def uid(self) -> OverridesStringFormatterValidator:
|
||||
|
|
@ -42,19 +43,6 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]):
|
|||
_inner_list_type = UrlThumbnailValidator
|
||||
|
||||
|
||||
class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator):
|
||||
def post_process(self, resolved: Any) -> List[str]:
|
||||
if isinstance(resolved, str):
|
||||
return [resolved]
|
||||
if isinstance(resolved, list):
|
||||
for value in resolved:
|
||||
if not isinstance(value, str):
|
||||
raise self._validation_exception("Must be a string or an array of strings.")
|
||||
return resolved
|
||||
|
||||
raise self._validation_exception("Must be a string or an array of strings.")
|
||||
|
||||
|
||||
class UrlValidator(StrictDictValidator):
|
||||
_required_keys = {"url"}
|
||||
_optional_keys = {
|
||||
|
|
@ -64,7 +52,6 @@ class UrlValidator(StrictDictValidator):
|
|||
"download_reverse",
|
||||
"ytdl_options",
|
||||
"include_sibling_metadata",
|
||||
"webpage_url",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
|
@ -80,7 +67,7 @@ class UrlValidator(StrictDictValidator):
|
|||
super().__init__(name, value)
|
||||
|
||||
# TODO: url validate using yt-dlp IE
|
||||
self._url = self._validate_key(key="url", validator=OverridesOneOrManyUrlValidator)
|
||||
self._url = self._validate_key(key="url", validator=OverridesStringFormatterValidator)
|
||||
self._variables = self._validate_key_if_present(
|
||||
key="variables", validator=DictFormatterValidator, default={}
|
||||
)
|
||||
|
|
@ -102,9 +89,6 @@ 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:
|
||||
|
|
@ -196,19 +180,6 @@ 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 <config_reference/scripting/entry_variables:webpage_url>`. This
|
||||
can be overwritten by supplying parameter with a modification to ``webpage_url`` in the
|
||||
form of an override variable.
|
||||
|
||||
Defaults to ``{webpage_url}``.
|
||||
"""
|
||||
return self._webpage_url
|
||||
|
||||
|
||||
class UrlStringOrDictValidator(UrlValidator):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import copy
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
import mergedeep
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,15 @@ import os
|
|||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import yt_dlp as ytdl
|
||||
from yt_dlp.utils import ExistingVideoReached, MaxDownloadsReached, RejectedVideoReached
|
||||
from yt_dlp.utils import ExistingVideoReached
|
||||
from yt_dlp.utils import MaxDownloadsReached
|
||||
from yt_dlp.utils import RejectedVideoReached
|
||||
|
||||
from ytdl_sub.thread.log_entries_downloaded_listener import LogEntriesDownloadedListener
|
||||
from ytdl_sub.utils.exceptions import FileNotDownloadedException
|
||||
|
|
@ -219,7 +224,7 @@ class YTDLP:
|
|||
except RejectedVideoReached:
|
||||
cls.logger.debug(
|
||||
"RejectedVideoReached, stopping additional downloads "
|
||||
"(Can be disable by setting `date_range.breaks` to False)."
|
||||
"(Can be disable by setting `date_range.breaking` to False)."
|
||||
)
|
||||
except ExistingVideoReached:
|
||||
cls.logger.debug(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
# pylint: disable=protected-access
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Type, TypeVar, final
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import final
|
||||
|
||||
from yt_dlp.utils import LazyList, sanitize_filename
|
||||
from yt_dlp.utils import LazyList
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,24 @@ import copy
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Type, TypeVar, final
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import final
|
||||
|
||||
from ytdl_sub.entries.base_entry import BaseEntry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_types import ArrayVariable, StringVariable, Variable
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_types import ArrayVariable
|
||||
from ytdl_sub.entries.script.variable_types import StringVariable
|
||||
from ytdl_sub.entries.script.variable_types import Variable
|
||||
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.utils.scriptable import Scriptable
|
||||
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS, VIDEO_CODEC_EXTS
|
||||
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
|
||||
from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import math
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ytdl_sub.entries.base_entry import BaseEntry, BaseEntryT
|
||||
from ytdl_sub.entries.base_entry import BaseEntry
|
||||
from ytdl_sub.entries.base_entry import BaseEntryT
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_types import MetadataVariable
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ from yt_dlp.utils import sanitize_filename
|
|||
|
||||
from ytdl_sub.script.functions import Functions
|
||||
from ytdl_sub.script.types.map import Map
|
||||
from ytdl_sub.script.types.resolvable import AnyArgument, Integer, ReturnableArgument, String
|
||||
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.script.types.resolvable import String
|
||||
from ytdl_sub.script.utils.exceptions import RuntimeException
|
||||
from ytdl_sub.utils.file_path import FilePathTruncater
|
||||
|
||||
|
|
@ -46,12 +49,12 @@ class CustomFunctions:
|
|||
return String(FilePathTruncater.maybe_truncate_file_path(filepath.value))
|
||||
|
||||
@staticmethod
|
||||
def sanitize(*value: AnyArgument) -> String:
|
||||
def sanitize(value: AnyArgument) -> String:
|
||||
"""
|
||||
Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use
|
||||
for file/directory names on any OS.
|
||||
"""
|
||||
return String("".join(sanitize_filename(str(val)) for val in value))
|
||||
return String(sanitize_filename(str(value)))
|
||||
|
||||
@staticmethod
|
||||
def sanitize_plex_episode(string: String) -> String:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Dict
|
||||
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
from abc import ABC
|
||||
from functools import cache, cached_property
|
||||
from typing import Dict, Optional, Set
|
||||
from functools import cache
|
||||
from functools import cached_property
|
||||
from typing import Dict
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.entries.script.custom_functions import CustomFunctions
|
||||
from ytdl_sub.entries.script.variable_types import (
|
||||
ArrayMetadataVariable,
|
||||
IntegerMetadataVariable,
|
||||
IntegerVariable,
|
||||
MapMetadataVariable,
|
||||
MapVariable,
|
||||
MetadataVariable,
|
||||
StringDateMetadataVariable,
|
||||
StringDateVariable,
|
||||
StringMetadataVariable,
|
||||
StringVariable,
|
||||
Variable,
|
||||
)
|
||||
from ytdl_sub.entries.script.variable_types import ArrayMetadataVariable
|
||||
from ytdl_sub.entries.script.variable_types import IntegerMetadataVariable
|
||||
from ytdl_sub.entries.script.variable_types import IntegerVariable
|
||||
from ytdl_sub.entries.script.variable_types import MapMetadataVariable
|
||||
from ytdl_sub.entries.script.variable_types import MapVariable
|
||||
from ytdl_sub.entries.script.variable_types import MetadataVariable
|
||||
from ytdl_sub.entries.script.variable_types import StringDateMetadataVariable
|
||||
from ytdl_sub.entries.script.variable_types import StringDateVariable
|
||||
from ytdl_sub.entries.script.variable_types import StringMetadataVariable
|
||||
from ytdl_sub.entries.script.variable_types import StringVariable
|
||||
from ytdl_sub.entries.script.variable_types import Variable
|
||||
|
||||
# This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion
|
||||
# pylint: disable=no-member
|
||||
|
|
@ -1135,16 +1135,6 @@ 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]:
|
||||
"""
|
||||
|
|
@ -1172,7 +1162,6 @@ class VariableDefinitions(
|
|||
"""
|
||||
return {
|
||||
self.uid,
|
||||
self.extractor,
|
||||
self.extractor_key,
|
||||
self.epoch,
|
||||
self.webpage_url,
|
||||
|
|
@ -1215,14 +1204,6 @@ class VariableDefinitions(
|
|||
VARIABLES.entry_metadata,
|
||||
} | self.injected_variables()
|
||||
|
||||
def get(self, name: str) -> Optional[Variable]:
|
||||
"""
|
||||
Returns the variable attribute if it exists. None otherwise.
|
||||
"""
|
||||
if not hasattr(self, name):
|
||||
return None
|
||||
return getattr(self, name)
|
||||
|
||||
|
||||
# Singletons to use externally
|
||||
VARIABLES: VariableDefinitions = VariableDefinitions()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Type, TypeVar
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
from ytdl_sub.script.types.array import Array
|
||||
from ytdl_sub.script.types.map import Map
|
||||
from ytdl_sub.script.types.resolvable import Boolean, Integer, String
|
||||
from ytdl_sub.script.types.resolvable import Boolean
|
||||
from ytdl_sub.script.types.resolvable import Integer
|
||||
from ytdl_sub.script.types.resolvable import String
|
||||
|
||||
ENTRY_METADATA_VARIABLE_NAME = "entry_metadata"
|
||||
PLAYLIST_METADATA_VARIABLE_NAME = "playlist_metadata"
|
||||
|
|
@ -15,6 +22,7 @@ VariableT = TypeVar("VariableT", bound="Variable")
|
|||
|
||||
|
||||
def _get(
|
||||
cast: str,
|
||||
metadata_variable_name: str,
|
||||
metadata_key: str,
|
||||
variable_name: Optional[str],
|
||||
|
|
@ -39,7 +47,7 @@ def _get(
|
|||
return as_type(
|
||||
variable_name=variable_name or metadata_key,
|
||||
metadata_key=metadata_key,
|
||||
definition=f"{{ {out} }}",
|
||||
definition=f"{{ %legacy_bracket_safety(%{cast}({out})) }}",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -174,6 +182,7 @@ 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,
|
||||
|
|
@ -195,6 +204,7 @@ 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,
|
||||
|
|
@ -216,6 +226,7 @@ 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,
|
||||
|
|
@ -234,6 +245,7 @@ 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,
|
||||
|
|
@ -252,6 +264,7 @@ 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,
|
||||
|
|
@ -288,6 +301,7 @@ 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,
|
||||
|
|
@ -306,6 +320,7 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
from typing import Dict, Set
|
||||
from typing import Dict
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
|
||||
from ytdl_sub.entries.script.variable_types import (
|
||||
ArrayVariable,
|
||||
BooleanVariable,
|
||||
MapVariable,
|
||||
StringVariable,
|
||||
Variable,
|
||||
)
|
||||
from ytdl_sub.entries.script.variable_types import ArrayVariable
|
||||
from ytdl_sub.entries.script.variable_types import BooleanVariable
|
||||
from ytdl_sub.entries.script.variable_types import MapVariable
|
||||
from ytdl_sub.entries.script.variable_types import StringVariable
|
||||
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
|
||||
|
|
@ -53,21 +55,16 @@ class SubscriptionVariables:
|
|||
@staticmethod
|
||||
def subscription_indent_i(index: int) -> StringVariable:
|
||||
"""
|
||||
For subscriptions where the ancestor keys contain the ``= ...`` prefix, the
|
||||
variables ``subscription_indent_1``, ``subscription_indent_2``, and so on get
|
||||
set to each subsequent value. For example, given the following subscriptions
|
||||
file snippet:
|
||||
For subscriptions in the form of
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Preset 1 | = Indent Value 1 | Preset 2:
|
||||
Preset 3 | = Indent Value 2 | Preset 4:
|
||||
Preset | = Indent Value 1:
|
||||
= Indent Value 2:
|
||||
"Subscription Name": "https://..."
|
||||
|
||||
The ``{subscription_indent_1}`` variable will be ``Indent Value 1`` and
|
||||
``{subscription_indent_2}`` will be ``Indent Value 2``. The most common use of
|
||||
these variables is to :doc:`set the genre and rating for subscriptions from the
|
||||
YAML keys <../prebuilt_presets/tv_show>`.
|
||||
``subscription_indent_1`` and ``subscription_indent_2`` get set to
|
||||
``Indent Value 1`` and ``Indent Value 2``.
|
||||
"""
|
||||
return StringVariable(
|
||||
variable_name=f"subscription_indent_{index + 1}", definition="{ %string('') }"
|
||||
|
|
@ -161,7 +158,7 @@ class OverrideHelpers:
|
|||
True if the override name itself is valid. False otherwise.
|
||||
"""
|
||||
if name.startswith("%"):
|
||||
name = name[1:]
|
||||
return is_valid_name(name=name[1:])
|
||||
|
||||
return is_valid_name(name=name)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import sys
|
||||
|
||||
from ytdl_sub.cli.parsers.main import parser
|
||||
from ytdl_sub.utils.logger import Logger, LoggerLevels
|
||||
from ytdl_sub.utils.logger import Logger
|
||||
|
||||
|
||||
def _main() -> int:
|
||||
|
|
@ -10,11 +10,6 @@ def _main() -> int:
|
|||
args, _ = parser.parse_known_args()
|
||||
Logger.set_log_level(log_level_name=args.ytdl_sub_log_level)
|
||||
|
||||
# Suppress all logs during inspection since the output of the subcommand itself
|
||||
# is all that is necessary
|
||||
if args.subparser == "inspect":
|
||||
Logger.set_log_level(log_level_name=LoggerLevels.QUIET.name)
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import ytdl_sub.cli.entrypoint
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
import os.path
|
||||
from typing import Any, Dict, Optional, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
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, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.exceptions import FileNotDownloadedException
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.validators.audo_codec_validator import (
|
||||
AUDIO_CODEC_EXTS,
|
||||
AUDIO_CODEC_TYPES_EXTENSION_MAPPING,
|
||||
AudioTypeValidator,
|
||||
)
|
||||
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
|
||||
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_TYPES_EXTENSION_MAPPING
|
||||
from ytdl_sub.validators.audo_codec_validator import AudioTypeValidator
|
||||
from ytdl_sub.validators.validators import FloatValidator
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
import collections
|
||||
import re
|
||||
from typing import Dict, List, Optional, Set
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
|
||||
from ytdl_sub.entries.entry import (
|
||||
Entry,
|
||||
ytdl_sub_chapters_from_comments,
|
||||
ytdl_sub_split_by_chapters_parent_uid,
|
||||
)
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.entry import ytdl_sub_chapters_from_comments
|
||||
from ytdl_sub.entries.entry import ytdl_sub_split_by_chapters_parent_uid
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.chapters import Chapters
|
||||
from ytdl_sub.utils.ffmpeg import set_ffmpeg_metadata_chapters
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.validators.regex_validator import RegexListValidator
|
||||
from ytdl_sub.validators.string_select_validator import StringSelectValidator
|
||||
from ytdl_sub.validators.validators import BoolValidator, ListValidator
|
||||
from ytdl_sub.validators.validators import BoolValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
from typing import List, Optional, Set, Tuple
|
||||
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
|
||||
|
|
@ -22,11 +25,9 @@ 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. Also note that, considering time zones, it's best
|
||||
to include a margin of an extra day on either side to be sure it includes the
|
||||
intended download files.
|
||||
Valid examples are ``now-2weeks`` or ``20200101``. Can use override variables in this.
|
||||
Note that yt-dlp will round times to the closest day, meaning that `day` is the lowest
|
||||
granularity possible.
|
||||
|
||||
:Usage:
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ class DateRangeOptions(ToggleableOptionsDictValidator):
|
|||
"""
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Only download videos only before this datetime, not inclusive.
|
||||
Only download videos before this datetime.
|
||||
"""
|
||||
return self._before
|
||||
|
||||
|
|
@ -64,7 +65,7 @@ class DateRangeOptions(ToggleableOptionsDictValidator):
|
|||
"""
|
||||
:expected type: Optional[OverridesFormatter]
|
||||
:description:
|
||||
Only download videos after or on this datetime, inclusive.
|
||||
Only download videos after this datetime.
|
||||
"""
|
||||
return self._after
|
||||
|
||||
|
|
@ -113,7 +114,7 @@ class DateRangePlugin(Plugin[DateRangeOptions]):
|
|||
date_validator=self.plugin_options.after, overrides=self.overrides
|
||||
)
|
||||
after_filter = f"{date_type} >= {after_str}"
|
||||
if self.overrides.apply_formatter(self.plugin_options.breaks, expected_type=bool):
|
||||
if self.overrides.evaluate_boolean(self.plugin_options.breaks):
|
||||
breaking_match_filters.append(after_filter)
|
||||
else:
|
||||
match_filters.append(after_filter)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List, Optional
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
import mediafile
|
||||
|
||||
|
|
@ -6,7 +7,8 @@ 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, FileMetadata
|
||||
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.string_formatter_validators import OverridesBooleanFormatterValidator
|
||||
|
|
@ -31,7 +33,7 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]):
|
|||
|
||||
@property
|
||||
def _embed_thumbnail(self) -> bool:
|
||||
return self.overrides.apply_formatter(self.plugin_options, expected_type=bool)
|
||||
return self.overrides.evaluate_boolean(self.plugin_options)
|
||||
|
||||
@classmethod
|
||||
def _embed_video_thumbnail(cls, entry: Entry) -> None:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
import os
|
||||
from subprocess import CalledProcessError
|
||||
from typing import Any, Dict, Optional, Set
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.utils.exceptions import FileNotDownloadedException, ValidationException
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.exceptions import FileNotDownloadedException
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
from ytdl_sub.utils.ffmpeg import FFMPEG
|
||||
from ytdl_sub.utils.file_handler import FileHandler, FileMetadata
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.validators.audo_codec_validator import FileTypeValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
|
||||
from ytdl_sub.validators.string_select_validator import StringSelectValidator
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
|
|
@ -6,14 +7,13 @@ 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.validators.string_formatter_validators import BooleanFormatterValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
logger = Logger.get("filter-exclude")
|
||||
|
||||
|
||||
class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator):
|
||||
class FilterExcludeOptions(ListFormatterValidator, 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.
|
||||
|
|
@ -29,8 +29,6 @@ class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsVali
|
|||
{ %contains( %lower(description), '#short' ) }
|
||||
"""
|
||||
|
||||
_inner_list_type = BooleanFormatterValidator
|
||||
|
||||
|
||||
class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
|
||||
plugin_options_type = FilterExcludeOptions
|
||||
|
|
@ -54,9 +52,7 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
|
|||
return entry
|
||||
|
||||
for formatter in self.plugin_options.list:
|
||||
should_exclude = self.overrides.apply_formatter(
|
||||
formatter=formatter, entry=entry, expected_type=bool
|
||||
)
|
||||
should_exclude = self.overrides.evaluate_boolean(formatter=formatter, entry=entry)
|
||||
|
||||
if should_exclude:
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.config.overrides import Overrides
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
|
|
@ -6,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.validators.string_formatter_validators import BooleanFormatterValidator
|
||||
from ytdl_sub.validators.validators import ListValidator
|
||||
from ytdl_sub.utils.script import ScriptUtils
|
||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
|
||||
|
||||
logger = Logger.get("filter-include")
|
||||
|
||||
|
||||
class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator):
|
||||
class FilterIncludeOptions(ListFormatterValidator, 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.
|
||||
|
|
@ -37,8 +38,6 @@ class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsVali
|
|||
}
|
||||
"""
|
||||
|
||||
_inner_list_type = BooleanFormatterValidator
|
||||
|
||||
|
||||
class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
|
||||
plugin_options_type = FilterIncludeOptions
|
||||
|
|
@ -62,8 +61,8 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
|
|||
return entry
|
||||
|
||||
for formatter in self.plugin_options.list:
|
||||
should_exclude = self.overrides.apply_formatter(
|
||||
formatter=formatter, entry=entry, expected_type=bool
|
||||
should_exclude = ScriptUtils.bool_formatter_output(
|
||||
self.overrides.apply_formatter(formatter=formatter, entry=entry)
|
||||
)
|
||||
if not should_exclude:
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import OptionsValidator
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import copy
|
||||
from typing import Any, List, Tuple
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
import mediafile
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import OptionsDictValidator
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions
|
||||
from ytdl_sub.entries.script.variable_definitions import VARIABLES
|
||||
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
|
||||
from ytdl_sub.utils.exceptions import ValidationException
|
||||
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.string_formatter_validators import (
|
||||
ListFormatterValidator,
|
||||
StringFormatterValidator,
|
||||
)
|
||||
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
|
||||
v: VariableDefinitions = VARIABLES
|
||||
|
||||
|
|
@ -146,7 +147,8 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
|
|||
else:
|
||||
if len(tag_value) > 1:
|
||||
logger.warning(
|
||||
"Music tag '%s' does not support lists. Only setting the first element",
|
||||
"Music tag '%s' does not support lists. "
|
||||
"Only setting the first element",
|
||||
tag_name,
|
||||
)
|
||||
setattr(audio_file, tag_name, tag_value[0])
|
||||
|
|
|
|||
|
|
@ -2,25 +2,24 @@ import os
|
|||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
from ytdl_sub.config.plugin.plugin import Plugin
|
||||
from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator
|
||||
from ytdl_sub.entries.entry import Entry
|
||||
from ytdl_sub.utils.file_handler import FileHandler, FileMetadata
|
||||
from ytdl_sub.utils.xml import (
|
||||
XmlElement,
|
||||
to_max_3_byte_utf8_dict,
|
||||
to_max_3_byte_utf8_string,
|
||||
to_xml,
|
||||
)
|
||||
from ytdl_sub.utils.file_handler import FileHandler
|
||||
from ytdl_sub.utils.file_handler import FileMetadata
|
||||
from ytdl_sub.utils.xml import XmlElement
|
||||
from ytdl_sub.utils.xml import to_max_3_byte_utf8_dict
|
||||
from ytdl_sub.utils.xml import to_max_3_byte_utf8_string
|
||||
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,
|
||||
OverridesBooleanFormatterValidator,
|
||||
StringFormatterValidator,
|
||||
)
|
||||
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator
|
||||
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
|
||||
|
||||
|
||||
class SharedNfoTagsOptions(ToggleableOptionsDictValidator):
|
||||
|
|
@ -141,7 +140,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC):
|
|||
if not nfo_tags:
|
||||
return
|
||||
|
||||
if self.overrides.apply_formatter(self.plugin_options.kodi_safe, expected_type=bool):
|
||||
if self.overrides.evaluate_boolean(self.plugin_options.kodi_safe):
|
||||
nfo_root = to_max_3_byte_utf8_string(nfo_root)
|
||||
nfo_tags = {
|
||||
to_max_3_byte_utf8_string(key): [
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue