diff --git a/Makefile b/Makefile index 0be80e53..9cd35d89 100644 --- a/Makefile +++ b/Makefile @@ -36,12 +36,12 @@ endif all: check_lint docs docker docker_ubuntu docker_gui lint: - python3 -m isort . - python3 -m black . + python3 -m ruff format . + python3 -m ruff check --fix . python3 -m pylint src check_lint: - isort . --check-only --diff \ - && black . --check \ + ruff format --check . \ + && ruff check . \ && pylint src/ wheel: clean $(shell echo "__pypi_version__ = \"$(PYPI_VERSION)\"" > src/ytdl_sub/__init__.py) diff --git a/README.md b/README.md index 3ba8ab5f..d9203837 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ __preset__: # Pass any arg directly to yt-dlp's Python API ytdl_options: - cookiefile: "/config/cookie.txt" + cookiefile: "/config/ytdl-sub-configs/cookie.txt" ################################################################### # TV Show Presets. Can replace Plex with Plex/Jellyfin/Emby/Kodi diff --git a/docker/root/custom-cont-init.d/defaults b/docker/root/custom-cont-init.d/defaults index f3bfd62e..af6b8350 100755 --- a/docker/root/custom-cont-init.d/defaults +++ b/docker/root/custom-cont-init.d/defaults @@ -47,6 +47,8 @@ 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" diff --git a/docker/root/defaults/cron b/docker/root/defaults/cron index 6fdbea71..0deb3568 100644 --- a/docker/root/defaults/cron +++ b/docker/root/defaults/cron @@ -1,4 +1,15 @@ -echo "Beginning cron job..." - # Place your ytdl-sub command(s) here. -# This script is executed in the same relative path as this file. \ No newline at end of file +# +# 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 diff --git a/docker/root/defaults/subscriptions.yaml b/docker/root/defaults/subscriptions.yaml index 98535e27..4fd02a39 100644 --- a/docker/root/defaults/subscriptions.yaml +++ b/docker/root/defaults/subscriptions.yaml @@ -20,7 +20,7 @@ __preset__: # Pass any arg directly to yt-dlp's Python API # ytdl_options: - # cookiefile: "/config/cookie.txt" + # cookiefile: "/config/ytdl-sub-configs/cookie.txt" ################################################################### # Subscriptions nested under this will use the diff --git a/docs/source/conf.py b/docs/source/conf.py index f754815a..c226b589 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,7 +7,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "ytdl-sub" -copyright = "2024, Jesse Bannon" +copyright = "2026, Jesse Bannon" author = "Jesse Bannon" release = "" @@ -15,10 +15,8 @@ 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", ] @@ -70,19 +68,3 @@ 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 diff --git a/docs/source/config_reference/config_yaml.rst b/docs/source/config_reference/config_yaml.rst index 6955c386..ccfa901b 100644 --- a/docs/source/config_reference/config_yaml.rst +++ b/docs/source/config_reference/config_yaml.rst @@ -1,7 +1,13 @@ -================== +.. + WARNING: This RST file is generated from docstrings in: + The respective function docstrings within ytdl_sub/config/config_validator.py + In order to make a change to this file, edit the respective docstring + and run `make docs`. This will automatically sync the Python RST-based + docstrings into this file. If the docstrings and RST file are out of sync, + it will fail TestDocGen tests in GitHub CI. + Configuration File ================== - ytdl-sub is configured using a ``config.yaml`` file. The ``config.yaml`` is made up of two sections: @@ -11,113 +17,118 @@ 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 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 -~~~~~~~~~~~~ - -Without this key, ``ytdl-sub`` only prints output to it's ``stdout`` and ``stderr``. If -your configuration includes the ``persist_logs:`` key, then ``ytdl-sub`` also writes log -files to disk. - -.. warning:: - - The log files grow rapidly if ``keep_successful_logs:`` is ``true``, the default, and - may fill up disk space. Set ``keep_successful_logs: false`` or prune the log files - regularly. - -For example: +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: - logs_directory: "/path/to/log/directory" + keep_successful_logs: True + logs_directory: "/var/log/ytdl-sub-logs" -.. autoclass:: ytdl_sub.config.config_validator.PersistLogsValidator() - :members: - :member-order: bysource + umask: "022" + working_directory: ".ytdl-sub-working-directory" +dl_aliases +---------- +.. _dl_aliases: -presets -------- - -Each key under ``presets:`` defines a `formula` for how to format downloaded media and -metadata. The key is the name of the preset and the value is a mapping that defines the -preset. - -.. note:: - - The ``presets:`` key at the top of the configuration file contains multiple - user-defined presets, but *each preset* itself may include a ``preset:`` key that - defines *that preset's* base presets. For example: - - .. code-block:: yaml - - presets: - Foo Preset: - preset: - - "Jellyfin TV Show by Date" - - "Only Recent" - -preset -~~~~~~ - -Presets support inheritance by defining one or more parent presets: +Alias definitions to shorten :ref:`dl arguments `. For example, .. code-block:: yaml - presets: - custom_preset: - ... - parent_preset: - ... - child_preset: - preset: - - "parent_preset" + configuration: + dl_aliases: + mv: "--preset music_video" + u: "--download.url" -In the example above, ``child_preset`` inherits all fields defined in ``parent_preset``. -Use parent presets where possible to reduce duplicate yaml definitions. +Simplifies -Presets also support inheritance from multiple presets: +.. code-block:: bash -.. code-block:: yaml + ytdl-sub dl --preset "Jellyfin Music Videos" --download.url "youtube.com/watch?v=a1b2c3" - child_preset: - preset: - - "custom_preset" - - "parent_preset" +to -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: +.. code-block:: bash -- if two conflicting keys arent lists or mappings, overwrite the higher priority one -- otherwise, combine then re-evaluate + ytdl-sub dl --mv --u "youtube.com/watch?v=a1b2c3" -If you are only inheriting from one preset, using a single string instead of a list is -valid, for example ``preset: "parent_preset"``, but we recommend always using a list for -consistent readability between presets. +experimental +------------ +Experimental flags reside under the ``experimental`` key. -.. _`mergedeep`: - https://mergedeep.readthedocs.io/en/latest/ -.. _`a TYPESAFE_ADDITIVE merge`: - https://mergedeep.readthedocs.io/en/latest/index.html#merge-strategies +``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` +on how to configure. diff --git a/docs/source/config_reference/plugins.rst b/docs/source/config_reference/plugins.rst index 6b8229e4..75496323 100644 --- a/docs/source/config_reference/plugins.rst +++ b/docs/source/config_reference/plugins.rst @@ -28,7 +28,6 @@ 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] @@ -37,7 +36,6 @@ 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 @@ -45,7 +43,6 @@ 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 @@ -84,14 +81,12 @@ 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] @@ -100,7 +95,6 @@ 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] @@ -108,7 +102,6 @@ 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] @@ -116,7 +109,6 @@ 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]] @@ -125,7 +117,6 @@ 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]] @@ -134,7 +125,6 @@ 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 @@ -169,14 +159,12 @@ intended download files. :description: Only download videos after or on this datetime, inclusive. - ``before`` :expected type: Optional[OverridesFormatter] :description: Only download videos only before this datetime, not inclusive. - ``breaks`` :expected type: Optional[OverridesFormatter] @@ -184,7 +172,6 @@ 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] @@ -193,7 +180,6 @@ intended download files. this field can be set using an override variable to easily toggle whether this plugin is enabled or not via Boolean. - ``type`` :expected type: Optional[OverridesFormatter] @@ -201,7 +187,6 @@ intended download files. Which type of date to use. Must be either ``upload_date`` or ``release_date``. Defaults to ``upload_date``. - ---------------------------------------------------------------------------------------------------- download @@ -305,7 +290,6 @@ 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] @@ -314,7 +298,6 @@ 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] @@ -323,7 +306,6 @@ 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] @@ -336,7 +318,6 @@ 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 @@ -472,7 +453,6 @@ 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 @@ -481,14 +461,12 @@ 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 @@ -501,7 +479,6 @@ with a ``.nfo`` extension. You can add any values into the NFO. - ``tags`` :expected type: NfoTags @@ -538,7 +515,6 @@ with a ``.nfo`` extension. You can add any values into the NFO. Comedy Drama - ---------------------------------------------------------------------------------------------------- output_directory_nfo_tags @@ -570,7 +546,6 @@ 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 @@ -579,14 +554,12 @@ 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 @@ -599,7 +572,6 @@ Usage: - ``tags`` :expected type: NfoTags @@ -634,7 +606,6 @@ Usage: Comedy Drama - ---------------------------------------------------------------------------------------------------- output_options @@ -669,7 +640,6 @@ 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 @@ -677,7 +647,6 @@ 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] @@ -686,7 +655,6 @@ 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] @@ -698,7 +666,6 @@ 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] @@ -710,7 +677,6 @@ Defines where to output files and thumbnails after all post-processing has compl files before ``now``, which implies all files. Can be used in conjunction with ``keep_max_files``. - ``keep_files_date_eval`` :expected type: str @@ -720,7 +686,6 @@ 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] @@ -730,7 +695,6 @@ 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] @@ -745,7 +709,6 @@ Defines where to output files and thumbnails after all post-processing has compl Defaults to False. - ``migrated_download_archive_name`` :expected type: Optional[OverridesFormatter] @@ -755,14 +718,12 @@ 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] @@ -771,7 +732,6 @@ Defines where to output files and thumbnails after all post-processing has compl When True, sets the file's mtime to match the video's upload_date from yt-dlp metadata. Defaults to False. - ``thumbnail_name`` :expected type: Optional[EntryFormatter] @@ -780,7 +740,6 @@ 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 @@ -845,7 +804,6 @@ used with no modifications. If a file has no chapters and is set to "pass", then ``chapter_title`` is set to the entry's title and ``chapter_index``, ``chapter_count`` are both set to 1. - ---------------------------------------------------------------------------------------------------- square_thumbnail @@ -892,7 +850,6 @@ 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 @@ -901,14 +858,12 @@ 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 @@ -921,7 +876,6 @@ Usage: - ``tags`` :expected type: NfoTags @@ -935,7 +889,6 @@ Usage: My custom season name! - ---------------------------------------------------------------------------------------------------- subtitles @@ -963,7 +916,6 @@ 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] @@ -971,7 +923,6 @@ 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] @@ -980,7 +931,6 @@ 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]] @@ -988,7 +938,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]] @@ -996,7 +945,6 @@ It will set the respective language to the correct subtitle file. 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`` :expected type: Optional[EntryFormatter] @@ -1006,14 +954,12 @@ 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 @@ -1054,14 +1000,12 @@ 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] @@ -1069,7 +1013,6 @@ Range min and max values support static override variables within their definiti Number in seconds to sleep between each download. Does not include time it takes for ytdl-sub to perform post-processing. - ``sleep_per_request_s`` :expected type: Optional[Range] @@ -1079,14 +1022,12 @@ 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] @@ -1095,7 +1036,6 @@ 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 diff --git a/docs/source/config_reference/subscription_yaml.rst b/docs/source/config_reference/subscription_yaml.rst index 298456f8..5d1ad0fb 100644 --- a/docs/source/config_reference/subscription_yaml.rst +++ b/docs/source/config_reference/subscription_yaml.rst @@ -31,7 +31,7 @@ supply a cookies file path. # Directly set plugin options: ytdl_options: - cookiefile: "/config/cookie.txt" + cookiefile: "/config/ytdl-sub-configs/cookie.txt" Layout ------ diff --git a/docs/source/guides/getting_started/automating_downloads.rst b/docs/source/guides/getting_started/automating_downloads.rst index 80e86902..094a5f1f 100644 --- a/docs/source/guides/getting_started/automating_downloads.rst +++ b/docs/source/guides/getting_started/automating_downloads.rst @@ -1,51 +1,80 @@ -Automating Downloads -==================== +Automating +========== -:ref:`Guide for Docker and Unraid Containers ` +Automate downloading your subscriptions by running the :ref:`'sub' sub-command +` 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 `_. -:ref:`Guide for Linux ` - -:ref:`Guide for Windows ` - -.. _cron scheduling syntax: https://crontab.guru/#0_*/6_*_*_* - - -.. _docker-unraid-setup: Docker and Unraid ----------------- -Cron is preconfigured in every ytdl-sub docker container. Enable by adding the following -ENV variables to your docker setup. +:doc:`The 'ytdl-sub' Docker container images <../install/docker>` provide optional cron +support. Enable cron support by setting `a cron schedule`_ in the ``CRON_SCHEDULE`` +environment variable: .. code-block:: yaml + :caption: ./compose.yaml + :emphasize-lines: 4 services: ytdl-sub: environment: - - CRON_SCHEDULE="0 */6 * * *" - - CRON_RUN_ON_START=false + CRON_SCHEDULE: "0 */6 * * *" + # WARNING: See "Getting Started" -> "Automating" docs regarding throttles/bans: + # CRON_RUN_ON_START: false +Then recreate the container to apply the change and start it to generate the default +``/config/ytdl-sub-configs/cron`` script. Read the comments in that script and edit as +appropriate. -- ``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. +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. -The cron script will reside in the main directory with the file name ``cron``. Cron -logs should show when viewing the Docker logs. +You may also set the ``CRON_RUN_ON_START`` environment variable to ``true`` to have the +image run your cron script whenever the container starts in addition to the cron +schedule. + +.. warning:: + + Using ``CRON_RUN_ON_START`` may cause your cron script to run too often and may + trigger throttles and bans. When enabled, your cron script will run *whenever* the + container starts including when the host reboots, when ``# dockerd`` restarts such as + when upgrading Docker itself, when a new image is pulled, when something applies + Compose changes, etc.. This may result in running ``ytdl-sub`` right before or after + the next cron scheduled run. .. _linux-setup: -Linux ------ -Must configure crontab manually, like so: +Linux, Mac OS X, BSD, or other UNIX's +------------------------------------- + +For installations on systems already running ``# crond``, you can also use cron to run +``ytdl-sub`` periodically. Write a script to run ``ytdl-sub`` in the cron job. Be sure +the script changes to the same directory as your configuration and uses the full path to +``ytdl-sub``: .. code-block:: shell + :caption: ~/.local/bin/ytdl-sub-cron + :emphasize-lines: 2,3 - crontab -e - 0 */6 * * * /config/run_cron + #!/bin/bash + cd "~/.config/ytdl-sub/" + ~/.local/bin/ytdl-sub --dry-run sub -o '--ytdl_options.max_downloads 3' |& + tee -a "~/.local/state/ytdl-sub/.cron.log" + +Then tell ``# crond`` when to run the script: + +.. code-block:: console + + echo "0 */6 * * * ${HOME}/.local/bin/ytdl-sub-cron" | crontab "-" + +Remove the ``--dry-run`` and ``-o ...`` CLI options from your cron script when you've +tested your configuration and you're ready to download entries unattended. .. _windows-setup: @@ -53,17 +82,51 @@ Must configure crontab manually, like so: 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:: - ytdl-sub.exe --config \path\to\config\config.yaml sub \path\to\config\subscriptions.yaml + 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 ---------- -Once you have a significant quantity of subscriptions or have use cases not served using -:doc:`YAML keys and the special characters <./subscriptions>`, it's time to start -:doc:`defining your own custom presets <./first_config>`. +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 diff --git a/docs/source/guides/getting_started/first_config.rst b/docs/source/guides/getting_started/first_config.rst index 34dabdb2..0f98b84e 100644 --- a/docs/source/guides/getting_started/first_config.rst +++ b/docs/source/guides/getting_started/first_config.rst @@ -3,13 +3,33 @@ Basic Configuration A configuration file serves two purposes: -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. +1. Set application-level functionality that is not specifiable in a subscription file. -Below is a common configuration: + .. note:: + + ytdl-sub does not require a configuration file. However, + certain application settings may be desirable for tweak, such as setting + ``working_directory`` to make ytdl-sub perform the initial download + to an SSD drive. + +2. Create custom presets. + + .. note:: + + In the prior Initial Subscription examples, we leveraged the prebuilt preset + ``Jellyfin TV Show by Date``. This preset is entirely built using the same + YAML configuration system offered to users by using a configuration file. + +The following section attempts to demystify and explain how to... + +- Set an application setting +- Know whether or not custom presets are actually needed +- How to create a custom preset +- How to use a custom preset on subscriptions + +------------- + +how this works, and show-case how .. code-block:: yaml :linenos: diff --git a/docs/source/guides/getting_started/index.rst b/docs/source/guides/getting_started/index.rst index 1288380d..5c04e47f 100644 --- a/docs/source/guides/getting_started/index.rst +++ b/docs/source/guides/getting_started/index.rst @@ -134,7 +134,7 @@ use no preset at all and will just run ``yt-dlp`` without any customization or p processing. The subscriptions file has special support for :ref:`overriding the presets of all subscriptions in the file `. The configuration file supports :ref:`a few special options -` that are not about defining presets. See +` 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. diff --git a/docs/source/guides/install/docker.rst b/docs/source/guides/install/docker.rst index aa7bfd5f..93df8119 100644 --- a/docs/source/guides/install/docker.rst +++ b/docs/source/guides/install/docker.rst @@ -38,6 +38,13 @@ For example:: $ docker compose run --rm --user="${PUID}:${PGID}" --entrypoint="ytdl-sub" ytdl-sub sub +.. note:: + + In `the recommended 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 --------------------------- diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 0db65064..cb5b827e 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -87,8 +87,7 @@ Using the command: --overrides.tv_show_name "Rick A" \ --overrides.url: "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw" -See how to shorten commands using `download aliases -`_. +See how to shorten commands using :ref:`download aliases `. View Options diff --git a/examples/advanced/tv_show_config.yaml b/examples/advanced/tv_show_config.yaml index d2019b9a..4b6c5db7 100644 --- a/examples/advanced/tv_show_config.yaml +++ b/examples/advanced/tv_show_config.yaml @@ -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/youtube_cookies.txt" + # cookiefile: "/config/ytdl-sub-configs/youtube_cookies.txt" # For YouTube, get English metadata if multiple languages are present extractor_args: diff --git a/pyproject.toml b/pyproject.toml index 94e18a34..c59b7b16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dependencies = [ - "yt-dlp[default]==2025.12.8", + "yt-dlp[default]==2026.3.3", "colorama~=0.4", "mergedeep~=1.3", "mediafile~=0.12", @@ -43,13 +43,12 @@ where = ["src"] [project.optional-dependencies] test = [ "coverage[toml]>=6.3,<8.0", - "pytest>=7.2,<9.0", + "pytest>=7.2,<10.0", "pytest-rerunfailures>=14,<17", ] lint = [ - "black==24.10.0", - "isort==7.0.0", - "pylint==4.0.1", + "pylint==4.0.5", + "ruff==0.15.5", ] docs = [ "sphinx>=7,<10", @@ -66,15 +65,6 @@ 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 @@ -100,3 +90,27 @@ 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" \ No newline at end of file diff --git a/src/ytdl_sub/cli/entrypoint.py b/src/ytdl_sub/cli/entrypoint.py index 1f46552a..2b0d4793 100644 --- a/src/ytdl_sub/cli/entrypoint.py +++ b/src/ytdl_sub/cli/entrypoint.py @@ -1,26 +1,24 @@ import gc import os +import random import sys from datetime import datetime from pathlib import Path -from typing import Dict -from typing import List -from typing import Optional +from typing import Dict, List, Optional from yt_dlp.utils import sanitize_filename from ytdl_sub.cli.output_summary import output_summary -from ytdl_sub.cli.output_transaction_log import _maybe_validate_transaction_log_file -from ytdl_sub.cli.output_transaction_log import output_transaction_log +from ytdl_sub.cli.output_transaction_log import ( + _maybe_validate_transaction_log_file, + output_transaction_log, +) from ytdl_sub.cli.parsers.cli_to_sub import print_cli_to_sub from ytdl_sub.cli.parsers.dl import DownloadArgsParser -from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME -from ytdl_sub.cli.parsers.main import InspectArguments -from ytdl_sub.cli.parsers.main import parser +from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME, parser, InspectArguments from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription -from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled -from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled, ValidationException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_lock import working_directory_lock from ytdl_sub.utils.logger import Logger @@ -78,6 +76,7 @@ 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,6 +93,8 @@ 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 ------- @@ -115,6 +116,10 @@ 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( @@ -320,6 +325,7 @@ 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 diff --git a/src/ytdl_sub/cli/output_transaction_log.py b/src/ytdl_sub/cli/output_transaction_log.py index ce46de0c..0457ea35 100644 --- a/src/ytdl_sub/cli/output_transaction_log.py +++ b/src/ytdl_sub/cli/output_transaction_log.py @@ -1,5 +1,4 @@ -from typing import List -from typing import Optional +from typing import List, Optional from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ValidationException diff --git a/src/ytdl_sub/cli/parsers/cli_to_sub.py b/src/ytdl_sub/cli/parsers/cli_to_sub.py index d4190042..2827c705 100644 --- a/src/ytdl_sub/cli/parsers/cli_to_sub.py +++ b/src/ytdl_sub/cli/parsers/cli_to_sub.py @@ -18,7 +18,6 @@ create_parser = yt_dlp.options.create_parser def parse_patched_options(opts): - patched_parser = create_parser() patched_parser.defaults.update( { diff --git a/src/ytdl_sub/cli/parsers/dl.py b/src/ytdl_sub/cli/parsers/dl.py index 7fbf7ac4..dd445179 100644 --- a/src/ytdl_sub/cli/parsers/dl.py +++ b/src/ytdl_sub/cli/parsers/dl.py @@ -1,10 +1,7 @@ import hashlib import re import shlex -from typing import Any -from typing import Dict -from typing import List -from typing import Tuple +from typing import Any, Dict, List, Tuple from mergedeep import mergedeep diff --git a/src/ytdl_sub/cli/parsers/main.py b/src/ytdl_sub/cli/parsers/main.py index 423443a8..f0afe246 100644 --- a/src/ytdl_sub/cli/parsers/main.py +++ b/src/ytdl_sub/cli/parsers/main.py @@ -173,6 +173,10 @@ class SubArguments: short="-o", long="--dl-override", ) + SHUFFLE = CLIArgument( + short="-sh", + long="--shuffle", + ) subscription_parser = subparsers.add_parser("sub") @@ -198,6 +202,13 @@ 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 diff --git a/src/ytdl_sub/config/config_file.py b/src/ytdl_sub/config/config_file.py index d0c117ef..42563da4 100644 --- a/src/ytdl_sub/config/config_file.py +++ b/src/ytdl_sub/config/config_file.py @@ -1,6 +1,5 @@ import os -from typing import Any -from typing import Dict +from typing import Any, Dict from ytdl_sub.config.config_validator import ConfigValidator from ytdl_sub.config.preset import Preset diff --git a/src/ytdl_sub/config/config_validator.py b/src/ytdl_sub/config/config_validator.py index 073eb23a..8f3961e6 100644 --- a/src/ytdl_sub/config/config_validator.py +++ b/src/ytdl_sub/config/config_validator.py @@ -1,29 +1,34 @@ import os import posixpath -from typing import Any -from typing import Dict -from typing import Optional +from typing import Any, Dict, Optional from mergedeep import mergedeep from yt_dlp.utils import datetime_from_str -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.config.defaults import ( + DEFAULT_FFMPEG_PATH, + DEFAULT_FFPROBE_PATH, + DEFAULT_LOCK_DIRECTORY, + MAX_FILE_NAME_BYTES, +) from ytdl_sub.prebuilt_presets import PREBUILT_PRESETS from ytdl_sub.utils.exceptions import SubscriptionPermissionError from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.validators.file_path_validators import FFmpegFileValidator -from ytdl_sub.validators.file_path_validators import FFprobeFileValidator +from ytdl_sub.validators.file_path_validators import FFmpegFileValidator, FFprobeFileValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator -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 +from ytdl_sub.validators.validators import ( + BoolValidator, + IntValidator, + LiteralDictValidator, + StringValidator, +) class ExperimentalValidator(StrictDictValidator): + """ + Experimental flags reside under the ``experimental`` key. + """ + _optional_keys = {"enable_update_with_info_json"} _allow_extra_keys = True @@ -45,6 +50,11 @@ 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"} @@ -69,8 +79,8 @@ class PersistLogsValidator(StrictDictValidator): @property def logs_directory(self) -> str: """ - Write log files to this directory with names like - ``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``. (required) + Required field. Write log files to this directory with names like + ``YYYY-mm-dd-HHMMSS.subscription_name.(success|error).log``. """ return self._logs_directory.value @@ -95,15 +105,54 @@ class PersistLogsValidator(StrictDictValidator): @property def keep_successful_logs(self) -> bool: """ - If the ``persist_logs:`` key is in the configuration, then ``ytdl-sub`` *always* - writes log files for the subscription both for successful downloads and when it - encounters an error while downloading. When this key is ``False``, only write - log files for errors. (default ``True``) + Defaults to ``True``. When this key is ``False``, only write log files for failed + subscriptions. """ 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", @@ -159,7 +208,8 @@ class ConfigOptions(StrictDictValidator): def working_directory(self) -> str: """ The directory to temporarily store downloaded files before moving them into their final - directory. (default ``./.ytdl-sub-working-directory``) + directory. Defaults to ``.ytdl-sub-working-directory``, created in the same directory + that ytdl-sub is invoked from. """ # Expands tildas to actual paths, use native os sep return os.path.expanduser(self._working_directory.value.replace(posixpath.sep, os.sep)) @@ -167,7 +217,7 @@ class ConfigOptions(StrictDictValidator): @property def umask(self) -> Optional[str]: """ - Umask in octal format to apply to every created file. (default ``022``) + Umask in octal format to apply to every created file. Defaults to ``022``. """ return self._umask.value @@ -176,7 +226,7 @@ class ConfigOptions(StrictDictValidator): """ .. _dl_aliases: - Alias definitions to shorten ``ytdl-sub dl`` arguments. For example, + Alias definitions to shorten :ref:`dl arguments `. For example, .. code-block:: yaml @@ -228,23 +278,23 @@ class ConfigOptions(StrictDictValidator): 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. (default ``/tmp``) + machine. Defaults to ``/tmp``. """ return self._lock_directory.value @property def ffmpeg_path(self) -> str: """ - Path to ffmpeg executable. (default ``/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, + ``./ffmpeg.exe`` in the same directory as ytdl-sub for Windows. """ return self._ffmpeg_path.value @property def ffprobe_path(self) -> str: """ - Path to ffprobe executable. (default ``/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, + ``./ffprobe.exe`` in the same directory as ytdl-sub for Windows. """ return self._ffprobe_path.value diff --git a/src/ytdl_sub/config/overrides.py b/src/ytdl_sub/config/overrides.py index 623fc06d..1d34ff35 100644 --- a/src/ytdl_sub/config/overrides.py +++ b/src/ytdl_sub/config/overrides.py @@ -1,28 +1,30 @@ -from typing import Any -from typing import Dict -from typing import Iterable -from typing import Optional -from typing import Set +from typing import Any, Dict, Iterable, Optional, Set, Type, TypeVar 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 -from ytdl_sub.entries.variables.override_variables import OverrideHelpers +from ytdl_sub.entries.variables.override_variables import ( + REQUIRED_OVERRIDE_VARIABLE_NAMES, + OverrideHelpers, +) from ytdl_sub.script.parser import parse from ytdl_sub.script.script import Script from ytdl_sub.script.types.function import BuiltInFunction -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Resolvable, String from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved -from ytdl_sub.utils.exceptions import InvalidVariableNameException -from ytdl_sub.utils.exceptions import StringFormattingException -from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.exceptions import ( + InvalidVariableNameException, + StringFormattingException, + ValidationException, +) from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.scriptable import Scriptable -from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator -from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator -from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator +from ytdl_sub.validators.string_formatter_validators import ( + StringFormatterValidator, + UnstructuredDictFormatterValidator, +) + +ExpectedT = TypeVar("ExpectedT") class Overrides(UnstructuredDictFormatterValidator, Scriptable): @@ -207,7 +209,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): formatter: StringFormatterValidator, entry: Optional[Entry] = None, function_overrides: Optional[Dict[str, str]] = None, - ) -> str: + expected_type: Type[ExpectedT] = str, + ) -> ExpectedT: """ Parameters ---------- @@ -217,6 +220,8 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): Optional. Entry to add source variables to the formatter function_overrides Optional. Explicit values to override the overrides themselves and source variables + expected_type + The expected type that should return. Defaults to string. Returns ------- @@ -227,42 +232,15 @@ class Overrides(UnstructuredDictFormatterValidator, Scriptable): StringFormattingException If the formatter that is trying to be resolved cannot """ - return formatter.post_process( - str( - self._apply_to_resolvable( - formatter=formatter, entry=entry, function_overrides=function_overrides - ) - ) - ) - - def apply_overrides_formatter_to_native( - self, - formatter: OverridesStringFormatterValidator, - function_overrides: Optional[Dict[str, str]] = None, - ) -> Any: - """ - Parameters - ---------- - formatter - Overrides formatter to apply - function_overrides - Optional. Explicit values to override the overrides themselves and source variables - - Returns - ------- - The native python form of the resolved variable - """ - return formatter.post_process_native( + out = formatter.post_process( self._apply_to_resolvable( - formatter=formatter, entry=None, function_overrides=function_overrides + formatter=formatter, entry=entry, function_overrides=function_overrides ).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) + if not isinstance(out, expected_type): + raise StringFormattingException( + f"Expected type {expected_type.__name__}, but received '{out.__class__.__name__}'" + ) + + return out diff --git a/src/ytdl_sub/config/plugin/plugin.py b/src/ytdl_sub/config/plugin/plugin.py index 0cd02b53..73556656 100644 --- a/src/ytdl_sub/config/plugin/plugin.py +++ b/src/ytdl_sub/config/plugin/plugin.py @@ -1,20 +1,15 @@ -from abc import ABC -from abc import abstractmethod +from abc import ABC, abstractmethod from functools import cached_property -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 typing import Dict, Generic, List, Optional, Tuple, Type from ytdl_sub.config.overrides import Overrides -from ytdl_sub.config.validators.options import OptionsValidatorT -from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator +from ytdl_sub.config.validators.options import OptionsValidatorT, 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 -from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive +from ytdl_sub.ytdl_additions.enhanced_download_archive import ( + DownloadArchiver, + EnhancedDownloadArchive, +) # pylint: disable=unused-argument @@ -48,7 +43,7 @@ class Plugin(BasePlugin[OptionsValidatorT], Generic[OptionsValidatorT], ABC): Returns True if enabled, False if disabled. """ if isinstance(self.plugin_options, ToggleableOptionsDictValidator): - return self.overrides.evaluate_boolean(self.plugin_options.enable) + return self.overrides.apply_formatter(self.plugin_options.enable, expected_type=bool) return True def ytdl_options_match_filters(self) -> Tuple[List[str], List[str]]: diff --git a/src/ytdl_sub/config/plugin/plugin_mapping.py b/src/ytdl_sub/config/plugin/plugin_mapping.py index bf458d6a..6f40e9fc 100644 --- a/src/ytdl_sub/config/plugin/plugin_mapping.py +++ b/src/ytdl_sub/config/plugin/plugin_mapping.py @@ -1,15 +1,12 @@ -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple -from typing import Type +from typing import Dict, List, Optional, Tuple, Type -from ytdl_sub.config.plugin.plugin import Plugin -from ytdl_sub.config.plugin.plugin import SplitPlugin +from ytdl_sub.config.plugin.plugin import Plugin, 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 -from ytdl_sub.downloaders.url.downloader import UrlDownloaderThumbnailPlugin +from ytdl_sub.downloaders.url.downloader import ( + UrlDownloaderCollectionVariablePlugin, + UrlDownloaderThumbnailPlugin, +) from ytdl_sub.plugins.audio_extract import AudioExtractPlugin from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.date_range import DateRangePlugin diff --git a/src/ytdl_sub/config/plugin/preset_plugins.py b/src/ytdl_sub/config/plugin/preset_plugins.py index 61305a37..bd6ca390 100644 --- a/src/ytdl_sub/config/plugin/preset_plugins.py +++ b/src/ytdl_sub/config/plugin/preset_plugins.py @@ -1,13 +1,7 @@ -from typing import Iterable -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple -from typing import Type +from typing import Iterable, List, Optional, Set, Tuple, Type from ytdl_sub.config.plugin.plugin import Plugin -from ytdl_sub.config.validators.options import OptionsValidator -from ytdl_sub.config.validators.options import OptionsValidatorT +from ytdl_sub.config.validators.options import OptionsValidator, OptionsValidatorT class PresetPlugins: diff --git a/src/ytdl_sub/config/preset.py b/src/ytdl_sub/config/preset.py index d8380839..4734eea4 100644 --- a/src/ytdl_sub/config/preset.py +++ b/src/ytdl_sub/config/preset.py @@ -1,8 +1,5 @@ import copy -from typing import Any -from typing import Dict -from typing import List -from typing import Set +from typing import Any, Dict, List, Set from mergedeep import mergedeep @@ -10,17 +7,14 @@ 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 -from ytdl_sub.config.preset_options import YTDLOptions +from ytdl_sub.config.preset_options import OutputOptions, YTDLOptions from ytdl_sub.downloaders.url.validators import MultiUrlValidator -from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES -from ytdl_sub.prebuilt_presets import PUBLISHED_PRESET_NAMES +from ytdl_sub.prebuilt_presets import PREBUILT_PRESET_NAMES, 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 -from ytdl_sub.validators.validators import validation_exception +from ytdl_sub.validators.validators import StringListValidator, validation_exception PRESET_KEYS = { "preset", @@ -55,6 +49,12 @@ class _PresetShell(StrictDictValidator): class Preset(_PresetShell): + """ + Custom presets are defined in this section. Refer to the + :ref:`Getting Started Guide` + on how to configure. + """ + @classmethod def preset_partial_validate(cls, config: ConfigValidator, name: str, value: Any) -> None: """ @@ -255,11 +255,18 @@ class Preset(_PresetShell): """ return cls(config=config, name=preset_name, value=preset_dict) - @property - def yaml(self) -> str: + def yaml(self, subscription_only: bool) -> str: """ + Parameters + ---------- + subscription_only: + Only include the subscription contents, not the surrounding boiler-plate. + Returns ------- Preset in YAML format """ + if subscription_only: + return dump_yaml(self._value) + return dump_yaml({"presets": {self._name: self._value}}) diff --git a/src/ytdl_sub/config/preset_options.py b/src/ytdl_sub/config/preset_options.py index 93f36453..112514cd 100644 --- a/src/ytdl_sub/config/preset_options.py +++ b/src/ytdl_sub/config/preset_options.py @@ -1,24 +1,22 @@ -from typing import Any -from typing import Dict -from typing import Optional -from typing import Set +from typing import Any, Dict, Optional, Set from ytdl_sub.config.defaults import DEFAULT_DOWNLOAD_ARCHIVE_NAME from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin_operation import PluginOperation from ytdl_sub.config.validators.options import OptionsDictValidator from ytdl_sub.entries.script.variable_definitions import VARIABLES as v -from ytdl_sub.utils.exceptions import SubscriptionPermissionError -from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.exceptions import SubscriptionPermissionError, ValidationException from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.validators.file_path_validators import OverridesStringFormatterFilePathValidator -from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator +from ytdl_sub.validators.file_path_validators import ( + OverridesStringFormatterFilePathValidator, + 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 @@ -63,7 +61,7 @@ class YTDLOptions(UnstructuredOverridesDictFormatterValidator): native python. """ out = { - key: overrides.apply_overrides_formatter_to_native(val) + key: overrides.apply_formatter(val, expected_type=object) for key, val in self.dict.items() } if "cookiefile" in out: diff --git a/src/ytdl_sub/config/validators/options.py b/src/ytdl_sub/config/validators/options.py index 5fbdc14d..21477643 100644 --- a/src/ytdl_sub/config/validators/options.py +++ b/src/ytdl_sub/config/validators/options.py @@ -1,7 +1,5 @@ from abc import ABC -from typing import Dict -from typing import Set -from typing import TypeVar +from typing import Dict, Set, TypeVar from ytdl_sub.config.plugin.plugin_operation import PluginOperation from ytdl_sub.utils.exceptions import ValidationException @@ -61,9 +59,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( diff --git a/src/ytdl_sub/config/validators/variable_validation.py b/src/ytdl_sub/config/validators/variable_validation.py index 1f53c1f6..9e4db801 100644 --- a/src/ytdl_sub/config/validators/variable_validation.py +++ b/src/ytdl_sub/config/validators/variable_validation.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, List, Set from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin_mapping import PluginMapping @@ -7,80 +7,163 @@ 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.script.script import Script +from ytdl_sub.script.utils.name_validation import is_function from ytdl_sub.validators.string_formatter_validators import validate_formatters +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") + + @classmethod + def all(cls) -> List[int]: + """ + All possible resolution levels. + """ + return [cls.ORIGINAL, cls.FILL, cls.RESOLVE, cls.INTERNAL] + + class VariableValidation: + def _get_resolve_partial_filter(self) -> Set[str]: + # Exclude sanitized variables from partial validation. This lessens the work + # and prevents double-evaluation, which can lead to bad behavior like double-prints. + return { + name + for name in self.script.variable_names + if name not in self.unresolved_variables and not name.endswith("_sanitized") + } + + def _apply_resolution_level(self) -> None: + if self._resolution_level == ResolutionLevel.FILL: + self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True) + # Only partial resolve definitions that are already resolved + self.unresolved_variables |= { + name + for name in self.overrides.keys + if not is_function(name) and not self.script.definition_of(name).maybe_resolvable + } + elif self._resolution_level == ResolutionLevel.RESOLVE: + # Partial resolve everything, but not including internal variables + self.unresolved_variables |= VARIABLES.variable_names(include_sanitized=True) + elif self._resolution_level == ResolutionLevel.INTERNAL: + # Partial resolve everything including internal variables + pass + else: + raise ValueError("Invalid resolution level for validation") + + self.script = self.script.resolve_partial( + unresolvable=self.unresolved_variables, + output_filter=self._get_resolve_partial_filter(), + ) + def __init__( self, overrides: Overrides, downloader_options: MultiUrlValidator, output_options: OutputOptions, plugins: PresetPlugins, + resolution_level: int = ResolutionLevel.RESOLVE, ): self.overrides = overrides self.downloader_options = downloader_options self.output_options = output_options self.plugins = plugins - self.script = self.overrides.script - self.unresolved_variables = self.plugins.get_all_variables( + self.script: Script = self.overrides.script + self.unresolved_variables = ( + self.plugins.get_all_variables( + additional_options=[self.output_options, self.downloader_options] + ) + | UNRESOLVED_VARIABLES + ) + self.unresolved_runtime_variables = self.plugins.get_all_variables( additional_options=[self.output_options, self.downloader_options] ) + self._resolution_level = resolution_level - def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None: + self._apply_resolution_level() + + def _add_runtime_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None: """ Add dummy variables for script validation """ added_variables = options.added_variables( - unresolved_variables=self.unresolved_variables, + unresolved_variables=self.unresolved_runtime_variables, ).get(plugin_op, set()) modified_variables = options.modified_variables().get(plugin_op, set()) - self.unresolved_variables -= added_variables | modified_variables + self.unresolved_runtime_variables -= added_variables | modified_variables - def ensure_proper_usage(self) -> Dict: + def ensure_proper_usage(self, partial_resolve_formatters: bool = False) -> Dict: """ Validate variables resolve as plugins are executed, and return a mock script which contains actualized added variables from the plugins """ - resolved_subscription: Dict = {} - self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options) + self._add_runtime_variables(PluginOperation.DOWNLOADER, options=self.downloader_options) # Always add output options first - self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options) + self._add_runtime_variables( + PluginOperation.MODIFY_ENTRY_METADATA, options=self.output_options + ) # Metadata variables to be added for plugin_options in PluginMapping.order_options_by( self.plugins.zipped(), PluginOperation.MODIFY_ENTRY_METADATA ): - self._add_variables(PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options) + self._add_runtime_variables( + PluginOperation.MODIFY_ENTRY_METADATA, options=plugin_options + ) for plugin_options in PluginMapping.order_options_by( self.plugins.zipped(), PluginOperation.MODIFY_ENTRY ): - self._add_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options) + self._add_runtime_variables(PluginOperation.MODIFY_ENTRY, options=plugin_options) # Validate that any formatter in the plugin options can resolve resolved_subscription |= validate_formatters( script=self.script, unresolved_variables=self.unresolved_variables, + unresolved_runtime_variables=self.unresolved_runtime_variables, validator=plugin_options, + partial_resolve_formatters=partial_resolve_formatters, ) resolved_subscription |= validate_formatters( script=self.script, unresolved_variables=self.unresolved_variables, + unresolved_runtime_variables=self.unresolved_runtime_variables, validator=self.output_options, + partial_resolve_formatters=partial_resolve_formatters, ) # 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"]: diff --git a/src/ytdl_sub/downloaders/info_json/info_json_downloader.py b/src/ytdl_sub/downloaders/info_json/info_json_downloader.py index 582962f5..68d2e9e0 100644 --- a/src/ytdl_sub/downloaders/info_json/info_json_downloader.py +++ b/src/ytdl_sub/downloaders/info_json/info_json_downloader.py @@ -1,24 +1,24 @@ import copy import json from pathlib import Path -from typing import Dict -from typing import Iterable -from typing import List -from typing import Optional +from typing import Dict, Iterable, List, 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 -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import ( + VARIABLE_SCRIPTS, + VARIABLES, + VariableDefinitions, +) from ytdl_sub.utils.exceptions import ValidationException -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 +from ytdl_sub.utils.file_handler import FileHandler, get_file_extension +from ytdl_sub.ytdl_additions.enhanced_download_archive import ( + DownloadMapping, + EnhancedDownloadArchive, +) v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/downloaders/source_plugin.py b/src/ytdl_sub/downloaders/source_plugin.py index 70127c63..030c4458 100644 --- a/src/ytdl_sub/downloaders/source_plugin.py +++ b/src/ytdl_sub/downloaders/source_plugin.py @@ -1,16 +1,9 @@ import abc from abc import ABC -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 typing import Dict, Generic, Iterable, List, Optional, Type, final from ytdl_sub.config.overrides import Overrides -from ytdl_sub.config.plugin.plugin import BasePlugin -from ytdl_sub.config.plugin.plugin import Plugin +from ytdl_sub.config.plugin.plugin import BasePlugin, 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 diff --git a/src/ytdl_sub/downloaders/url/downloader.py b/src/ytdl_sub/downloaders/url/downloader.py index acb35895..66ef2f60 100644 --- a/src/ytdl_sub/downloaders/url/downloader.py +++ b/src/ytdl_sub/downloaders/url/downloader.py @@ -1,33 +1,29 @@ import contextlib import os from pathlib import Path -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 typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple from yt_dlp.utils import RejectedVideoReached from ytdl_sub.config.overrides import Overrides -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.source_plugin import SourcePlugin, SourcePluginExtension +from ytdl_sub.downloaders.url.validators import ( + MultiUrlValidator, + UrlThumbnailListValidator, + 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 -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.logger import Logger -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.utils.thumbnail import ( + ThumbnailTypes, + download_and_convert_url_thumbnail, + try_convert_download_thumbnail, +) from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive v: VariableDefinitions = VARIABLES @@ -52,12 +48,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_overrides_formatter_to_native(validator.url): + if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list): return validator # Match the first validator based on the URL, if one exists for validator in self.plugin_options.urls.list: - if entry_input_url in self.overrides.apply_overrides_formatter_to_native(validator.url): + if entry_input_url in self.overrides.apply_formatter(validator.url, expected_type=list): return validator # Return the first validator if none exist @@ -97,7 +93,6 @@ 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( @@ -382,7 +377,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): entries_to_iter: List[Optional[Entry]] = entries indices = list(range(len(entries_to_iter))) - if self.overrides.evaluate_boolean(validator.download_reverse): + if self.overrides.apply_formatter(validator.download_reverse, expected_type=bool): indices = reversed(indices) for idx in indices: @@ -461,8 +456,8 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]): ytdl_option_overrides=validator.ytdl_options.to_native_dict(self.overrides) ) - include_sibling_metadata = self.overrides.evaluate_boolean( - validator.include_sibling_metadata + include_sibling_metadata = self.overrides.apply_formatter( + validator.include_sibling_metadata, expected_type=bool ) parents, orphan_entries = self._download_url_metadata( @@ -487,11 +482,9 @@ 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_overrides_formatter_to_native(url_validator.url)): + if not (urls := self.overrides.apply_formatter(url_validator.url, expected_type=list)): continue - assert isinstance(urls, list) - for url in reversed(urls): assert isinstance(url, str) diff --git a/src/ytdl_sub/downloaders/url/validators.py b/src/ytdl_sub/downloaders/url/validators.py index 551d7169..13fc321b 100644 --- a/src/ytdl_sub/downloaders/url/validators.py +++ b/src/ytdl_sub/downloaders/url/validators.py @@ -1,17 +1,16 @@ -from typing import Any -from typing import Dict -from typing import Optional -from typing import Set +from typing import Any, Dict, List, Optional, 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 -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.string_formatter_validators import ( + DictFormatterValidator, + OverridesBooleanFormatterValidator, + OverridesStringFormatterValidator, + StringFormatterValidator, +) from ytdl_sub.validators.validators import ListValidator @@ -44,7 +43,7 @@ class UrlThumbnailListValidator(ListValidator[UrlThumbnailValidator]): class OverridesOneOrManyUrlValidator(OverridesStringFormatterValidator): - def post_process_native(self, resolved: Any) -> Any: + def post_process(self, resolved: Any) -> List[str]: if isinstance(resolved, str): return [resolved] if isinstance(resolved, list): diff --git a/src/ytdl_sub/downloaders/ytdl_options_builder.py b/src/ytdl_sub/downloaders/ytdl_options_builder.py index 9f389b51..208cd48c 100644 --- a/src/ytdl_sub/downloaders/ytdl_options_builder.py +++ b/src/ytdl_sub/downloaders/ytdl_options_builder.py @@ -1,6 +1,5 @@ import copy -from typing import Dict -from typing import Optional +from typing import Dict, Optional import mergedeep diff --git a/src/ytdl_sub/downloaders/ytdlp.py b/src/ytdl_sub/downloaders/ytdlp.py index c01376e4..8161ca73 100644 --- a/src/ytdl_sub/downloaders/ytdlp.py +++ b/src/ytdl_sub/downloaders/ytdlp.py @@ -5,15 +5,10 @@ import os import time from contextlib import contextmanager from pathlib import Path -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional +from typing import Callable, Dict, List, Optional import yt_dlp as ytdl -from yt_dlp.utils import ExistingVideoReached -from yt_dlp.utils import MaxDownloadsReached -from yt_dlp.utils import RejectedVideoReached +from yt_dlp.utils import ExistingVideoReached, MaxDownloadsReached, RejectedVideoReached from ytdl_sub.thread.log_entries_downloaded_listener import LogEntriesDownloadedListener from ytdl_sub.utils.exceptions import FileNotDownloadedException diff --git a/src/ytdl_sub/entries/base_entry.py b/src/ytdl_sub/entries/base_entry.py index c8321774..13d32ff9 100644 --- a/src/ytdl_sub/entries/base_entry.py +++ b/src/ytdl_sub/entries/base_entry.py @@ -1,18 +1,11 @@ # pylint: disable=protected-access from abc import ABC from pathlib import Path -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 typing import Any, Dict, Optional, Type, TypeVar, final -from yt_dlp.utils import LazyList -from yt_dlp.utils import sanitize_filename +from yt_dlp.utils import LazyList, sanitize_filename -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/entries/entry.py b/src/ytdl_sub/entries/entry.py index f97e98f5..5135bb1e 100644 --- a/src/ytdl_sub/entries/entry.py +++ b/src/ytdl_sub/entries/entry.py @@ -3,24 +3,15 @@ import copy import json import os from pathlib import Path -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 typing import Any, Dict, Optional, Type, TypeVar, final from ytdl_sub.entries.base_entry import BaseEntry -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.entries.script.variable_definitions import VARIABLES, VariableDefinitions +from ytdl_sub.entries.script.variable_types import ArrayVariable, StringVariable, 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 -from ytdl_sub.validators.audo_codec_validator import VIDEO_CODEC_EXTS +from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS, VIDEO_CODEC_EXTS v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/entries/entry_parent.py b/src/ytdl_sub/entries/entry_parent.py index ae32bcc4..33bb9f70 100644 --- a/src/ytdl_sub/entries/entry_parent.py +++ b/src/ytdl_sub/entries/entry_parent.py @@ -1,16 +1,10 @@ import math -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set +from typing import Any, Dict, List, Optional, Set from urllib.parse import urlparse -from ytdl_sub.entries.base_entry import BaseEntry -from ytdl_sub.entries.base_entry import BaseEntryT +from ytdl_sub.entries.base_entry import BaseEntry, BaseEntryT from ytdl_sub.entries.entry import Entry -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions from ytdl_sub.entries.script.variable_types import MetadataVariable v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/entries/script/custom_functions.py b/src/ytdl_sub/entries/script/custom_functions.py index ded8c54f..0bfc90ec 100644 --- a/src/ytdl_sub/entries/script/custom_functions.py +++ b/src/ytdl_sub/entries/script/custom_functions.py @@ -5,10 +5,7 @@ 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 -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.types.resolvable import AnyArgument, Integer, ReturnableArgument, String from ytdl_sub.script.utils.exceptions import RuntimeException from ytdl_sub.utils.file_path import FilePathTruncater diff --git a/src/ytdl_sub/entries/script/function_scripts.py b/src/ytdl_sub/entries/script/function_scripts.py index ab4ba121..7feeb0bb 100644 --- a/src/ytdl_sub/entries/script/function_scripts.py +++ b/src/ytdl_sub/entries/script/function_scripts.py @@ -1,7 +1,6 @@ from typing import Dict -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/entries/script/variable_definitions.py b/src/ytdl_sub/entries/script/variable_definitions.py index 67505473..a436060d 100644 --- a/src/ytdl_sub/entries/script/variable_definitions.py +++ b/src/ytdl_sub/entries/script/variable_definitions.py @@ -1,21 +1,21 @@ from abc import ABC -from functools import cache -from functools import cached_property -from typing import Dict -from typing import Set +from functools import cache, cached_property +from typing import Dict, Set from ytdl_sub.entries.script.custom_functions import CustomFunctions -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 +from ytdl_sub.entries.script.variable_types import ( + ArrayMetadataVariable, + IntegerMetadataVariable, + IntegerVariable, + MapMetadataVariable, + MapVariable, + MetadataVariable, + StringDateMetadataVariable, + StringDateVariable, + StringMetadataVariable, + StringVariable, + Variable, +) # This file contains mixins to a BaseEntry subclass. Ignore pylint's "no kwargs member" suggestion # pylint: disable=no-member @@ -1135,6 +1135,16 @@ class VariableDefinitions( ] } + @cache + def variable_names(self, include_sanitized: bool): + """ + Returns all variable names, and can include sanitized. + """ + var_names: Set[str] = self.scripts().keys() + if include_sanitized: + var_names |= {f"{name}_sanitized" for name in var_names} + return var_names + @cache def injected_variables(self) -> Set[MetadataVariable]: """ diff --git a/src/ytdl_sub/entries/script/variable_types.py b/src/ytdl_sub/entries/script/variable_types.py index bbf994e9..ea476cca 100644 --- a/src/ytdl_sub/entries/script/variable_types.py +++ b/src/ytdl_sub/entries/script/variable_types.py @@ -1,17 +1,10 @@ -from abc import ABC -from abc import abstractmethod +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Dict -from typing import List -from typing import Optional -from typing import Type -from typing import TypeVar +from typing import Dict, List, Optional, Type, 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 -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Boolean, Integer, String ENTRY_METADATA_VARIABLE_NAME = "entry_metadata" PLAYLIST_METADATA_VARIABLE_NAME = "playlist_metadata" diff --git a/src/ytdl_sub/entries/variables/override_variables.py b/src/ytdl_sub/entries/variables/override_variables.py index 8dd2081c..c33068e7 100644 --- a/src/ytdl_sub/entries/variables/override_variables.py +++ b/src/ytdl_sub/entries/variables/override_variables.py @@ -1,13 +1,14 @@ -from typing import Dict -from typing import Set +from typing import Dict, 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 -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.entries.script.variable_types import ( + ArrayVariable, + BooleanVariable, + MapVariable, + StringVariable, + Variable, +) from ytdl_sub.script.functions import Functions from ytdl_sub.script.utils.name_validation import is_valid_name diff --git a/src/ytdl_sub/plugins/audio_extract.py b/src/ytdl_sub/plugins/audio_extract.py index 2fbd1d6b..ee7c39e4 100644 --- a/src/ytdl_sub/plugins/audio_extract.py +++ b/src/ytdl_sub/plugins/audio_extract.py @@ -1,21 +1,19 @@ import os.path -from typing import Any -from typing import Dict -from typing import Optional -from typing import Set +from typing import Any, Dict, Optional, 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 -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, 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 -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.audo_codec_validator import ( + AUDIO_CODEC_EXTS, + AUDIO_CODEC_TYPES_EXTENSION_MAPPING, + AudioTypeValidator, +) from ytdl_sub.validators.validators import FloatValidator v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/plugins/chapters.py b/src/ytdl_sub/plugins/chapters.py index 0394a0fd..b75fe733 100644 --- a/src/ytdl_sub/plugins/chapters.py +++ b/src/ytdl_sub/plugins/chapters.py @@ -1,26 +1,23 @@ import collections import re -from typing import Dict -from typing import List -from typing import Optional -from typing import Set +from typing import Dict, List, Optional, 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.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.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.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 -from ytdl_sub.validators.validators import ListValidator +from ytdl_sub.validators.validators import BoolValidator, ListValidator v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/plugins/date_range.py b/src/ytdl_sub/plugins/date_range.py index 478cf938..8bd6be26 100644 --- a/src/ytdl_sub/plugins/date_range.py +++ b/src/ytdl_sub/plugins/date_range.py @@ -1,7 +1,4 @@ -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple +from typing import List, Optional, Set, Tuple from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator @@ -116,7 +113,7 @@ class DateRangePlugin(Plugin[DateRangeOptions]): date_validator=self.plugin_options.after, overrides=self.overrides ) after_filter = f"{date_type} >= {after_str}" - if self.overrides.evaluate_boolean(self.plugin_options.breaks): + if self.overrides.apply_formatter(self.plugin_options.breaks, expected_type=bool): breaking_match_filters.append(after_filter) else: match_filters.append(after_filter) diff --git a/src/ytdl_sub/plugins/embed_thumbnail.py b/src/ytdl_sub/plugins/embed_thumbnail.py index e7d19b8b..3984c7e7 100644 --- a/src/ytdl_sub/plugins/embed_thumbnail.py +++ b/src/ytdl_sub/plugins/embed_thumbnail.py @@ -1,5 +1,4 @@ -from typing import List -from typing import Optional +from typing import List, Optional import mediafile @@ -7,8 +6,7 @@ from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.ffmpeg import FFMPEG -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandler, 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 @@ -33,7 +31,7 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]): @property def _embed_thumbnail(self) -> bool: - return self.overrides.evaluate_boolean(self.plugin_options) + return self.overrides.apply_formatter(self.plugin_options, expected_type=bool) @classmethod def _embed_video_thumbnail(cls, entry: Entry) -> None: diff --git a/src/ytdl_sub/plugins/file_convert.py b/src/ytdl_sub/plugins/file_convert.py index 1b9ecf1e..2e3ad8bc 100644 --- a/src/ytdl_sub/plugins/file_convert.py +++ b/src/ytdl_sub/plugins/file_convert.py @@ -1,22 +1,16 @@ import os from subprocess import CalledProcessError -from typing import Any -from typing import Dict -from typing import Optional -from typing import Set +from typing import Any, Dict, Optional, 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 -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.entries.script.variable_definitions import VARIABLES, VariableDefinitions +from ytdl_sub.utils.exceptions import FileNotDownloadedException, ValidationException from ytdl_sub.utils.ffmpeg import FFMPEG -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandler, 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 diff --git a/src/ytdl_sub/plugins/filter_exclude.py b/src/ytdl_sub/plugins/filter_exclude.py index 7d256998..b5b4db7d 100644 --- a/src/ytdl_sub/plugins/filter_exclude.py +++ b/src/ytdl_sub/plugins/filter_exclude.py @@ -1,5 +1,4 @@ -from typing import Dict -from typing import Optional +from typing import Dict, Optional from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin import Plugin @@ -7,13 +6,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 ListFormatterValidator +from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator +from ytdl_sub.validators.validators import ListValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-exclude") -class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): +class FilterExcludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): """ Applies a conditional OR on any number of filters comprised of either variables or scripts. If any filter evaluates to True, the entry will be excluded. @@ -29,6 +29,8 @@ class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): { %contains( %lower(description), '#short' ) } """ + _inner_list_type = BooleanFormatterValidator + class FilterExcludePlugin(Plugin[FilterExcludeOptions]): plugin_options_type = FilterExcludeOptions @@ -52,7 +54,9 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]): return entry for formatter in self.plugin_options.list: - should_exclude = self.overrides.evaluate_boolean(formatter=formatter, entry=entry) + should_exclude = self.overrides.apply_formatter( + formatter=formatter, entry=entry, expected_type=bool + ) if should_exclude: logger.info( diff --git a/src/ytdl_sub/plugins/filter_include.py b/src/ytdl_sub/plugins/filter_include.py index c2bc5217..bd872300 100644 --- a/src/ytdl_sub/plugins/filter_include.py +++ b/src/ytdl_sub/plugins/filter_include.py @@ -1,5 +1,4 @@ -from typing import Dict -from typing import Optional +from typing import Dict, Optional from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin import Plugin @@ -7,14 +6,14 @@ from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.script import ScriptUtils -from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator +from ytdl_sub.validators.string_formatter_validators import BooleanFormatterValidator +from ytdl_sub.validators.validators import ListValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive logger = Logger.get("filter-include") -class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): +class FilterIncludeOptions(ListValidator[BooleanFormatterValidator], OptionsValidator): """ Applies a conditional AND on any number of filters comprised of either variables or scripts. If all filters evaluate to True, the entry will be included. @@ -38,6 +37,8 @@ class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): } """ + _inner_list_type = BooleanFormatterValidator + class FilterIncludePlugin(Plugin[FilterIncludeOptions]): plugin_options_type = FilterIncludeOptions @@ -61,8 +62,8 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]): return entry for formatter in self.plugin_options.list: - should_exclude = ScriptUtils.bool_formatter_output( - self.overrides.apply_formatter(formatter=formatter, entry=entry) + should_exclude = self.overrides.apply_formatter( + formatter=formatter, entry=entry, expected_type=bool ) if not should_exclude: logger.info( diff --git a/src/ytdl_sub/plugins/format.py b/src/ytdl_sub/plugins/format.py index 6e2d545d..a4e92065 100644 --- a/src/ytdl_sub/plugins/format.py +++ b/src/ytdl_sub/plugins/format.py @@ -1,5 +1,4 @@ -from typing import Dict -from typing import Optional +from typing import Dict, Optional from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import OptionsValidator diff --git a/src/ytdl_sub/plugins/match_filters.py b/src/ytdl_sub/plugins/match_filters.py index 6a777054..b7483b6a 100644 --- a/src/ytdl_sub/plugins/match_filters.py +++ b/src/ytdl_sub/plugins/match_filters.py @@ -1,7 +1,5 @@ import copy -from typing import Any -from typing import List -from typing import Tuple +from typing import Any, List, Tuple from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import ToggleableOptionsDictValidator diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index d7804b67..7f050dd6 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -1,22 +1,21 @@ from collections import defaultdict from datetime import datetime -from typing import Any -from typing import Dict -from typing import List +from typing import Any, Dict, 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 -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, 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 -from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator +from ytdl_sub.validators.string_formatter_validators import ( + ListFormatterValidator, + StringFormatterValidator, +) v: VariableDefinitions = VARIABLES @@ -147,8 +146,7 @@ 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]) diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index 24c07c72..ff698ed7 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -2,24 +2,25 @@ import os from abc import ABC from collections import defaultdict from pathlib import Path -from typing import Any -from typing import Dict -from typing import List +from typing import Any, Dict, 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 -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.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.validators.file_path_validators import StringFormatterFileNameValidator from ytdl_sub.validators.nfo_validators import NfoTagsValidator -from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator -from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator -from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator +from ytdl_sub.validators.string_formatter_validators import ( + DictFormatterValidator, + OverridesBooleanFormatterValidator, + StringFormatterValidator, +) class SharedNfoTagsOptions(ToggleableOptionsDictValidator): @@ -140,7 +141,7 @@ class SharedNfoTagsPlugin(Plugin[SharedNfoTagsOptions], ABC): if not nfo_tags: return - if self.overrides.evaluate_boolean(self.plugin_options.kodi_safe): + if self.overrides.apply_formatter(self.plugin_options.kodi_safe, expected_type=bool): nfo_root = to_max_3_byte_utf8_string(nfo_root) nfo_tags = { to_max_3_byte_utf8_string(key): [ diff --git a/src/ytdl_sub/plugins/output_directory_nfo_tags.py b/src/ytdl_sub/plugins/output_directory_nfo_tags.py index aae44103..40c14322 100644 --- a/src/ytdl_sub/plugins/output_directory_nfo_tags.py +++ b/src/ytdl_sub/plugins/output_directory_nfo_tags.py @@ -1,8 +1,6 @@ from ytdl_sub.config.overrides import Overrides from ytdl_sub.entries.entry import Entry -from ytdl_sub.plugins.nfo_tags import NfoTagsValidator -from ytdl_sub.plugins.nfo_tags import SharedNfoTagsOptions -from ytdl_sub.plugins.nfo_tags import SharedNfoTagsPlugin +from ytdl_sub.plugins.nfo_tags import NfoTagsValidator, SharedNfoTagsOptions, SharedNfoTagsPlugin from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive diff --git a/src/ytdl_sub/plugins/split_by_chapters.py b/src/ytdl_sub/plugins/split_by_chapters.py index 20722672..caff76b6 100644 --- a/src/ytdl_sub/plugins/split_by_chapters.py +++ b/src/ytdl_sub/plugins/split_by_chapters.py @@ -1,23 +1,14 @@ -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from ytdl_sub.config.plugin.plugin import SplitPlugin from ytdl_sub.config.plugin.plugin_operation import PluginOperation from ytdl_sub.config.validators.options import OptionsDictValidator -from ytdl_sub.entries.entry import Entry -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.chapters import Timestamp +from ytdl_sub.entries.entry import Entry, ytdl_sub_split_by_chapters_parent_uid +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions +from ytdl_sub.utils.chapters import Chapters, Timestamp from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.ffmpeg import FFMPEG -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandler, FileMetadata from ytdl_sub.validators.string_select_validator import StringSelectValidator v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/plugins/square_thumbnail.py b/src/ytdl_sub/plugins/square_thumbnail.py index 030f5a09..451d3102 100644 --- a/src/ytdl_sub/plugins/square_thumbnail.py +++ b/src/ytdl_sub/plugins/square_thumbnail.py @@ -1,12 +1,10 @@ -from typing import List -from typing import Optional +from typing import List, Optional from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.ffmpeg import FFMPEG -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandler, FileMetadata from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator @@ -31,7 +29,7 @@ class SquareThumbnailPlugin(Plugin[SquareThumbnailOptions]): @property def _square_thumbnail(self) -> bool: - return self.overrides.evaluate_boolean(self.plugin_options) + return self.overrides.apply_formatter(self.plugin_options, expected_type=bool) @classmethod def _convert_to_square_thumbnail(cls, entry: Entry) -> None: diff --git a/src/ytdl_sub/plugins/static_nfo_tags.py b/src/ytdl_sub/plugins/static_nfo_tags.py index 3476bda5..6dd233df 100644 --- a/src/ytdl_sub/plugins/static_nfo_tags.py +++ b/src/ytdl_sub/plugins/static_nfo_tags.py @@ -1,7 +1,5 @@ from ytdl_sub.entries.entry import Entry -from ytdl_sub.plugins.nfo_tags import NfoTagsValidator -from ytdl_sub.plugins.nfo_tags import SharedNfoTagsOptions -from ytdl_sub.plugins.nfo_tags import SharedNfoTagsPlugin +from ytdl_sub.plugins.nfo_tags import NfoTagsValidator, SharedNfoTagsOptions, SharedNfoTagsPlugin from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator diff --git a/src/ytdl_sub/plugins/subtitles.py b/src/ytdl_sub/plugins/subtitles.py index 54318512..ae7e1014 100644 --- a/src/ytdl_sub/plugins/subtitles.py +++ b/src/ytdl_sub/plugins/subtitles.py @@ -1,28 +1,21 @@ import os from pathlib import Path -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple +from typing import Dict, List, Optional, Set, Tuple 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 -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandler, FileMetadata from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.subtitles import SUBTITLE_EXTENSIONS from ytdl_sub.validators.file_path_validators import StringFormatterFileNameValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator from ytdl_sub.validators.string_select_validator import StringSelectValidator -from ytdl_sub.validators.validators import BoolValidator -from ytdl_sub.validators.validators import StringListValidator +from ytdl_sub.validators.validators import BoolValidator, StringListValidator v: VariableDefinitions = VARIABLES diff --git a/src/ytdl_sub/plugins/throttle_protection.py b/src/ytdl_sub/plugins/throttle_protection.py index 7acad676..0b5b99c2 100644 --- a/src/ytdl_sub/plugins/throttle_protection.py +++ b/src/ytdl_sub/plugins/throttle_protection.py @@ -1,10 +1,7 @@ import random import time from abc import ABC -from typing import Dict -from typing import Optional -from typing import Type -from typing import TypeVar +from typing import Dict, Optional, Type, TypeVar from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin import Plugin @@ -13,8 +10,10 @@ from ytdl_sub.entries.entry import Entry from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.strict_dict_validator import StrictDictValidator -from ytdl_sub.validators.string_formatter_validators import FloatFormatterValidator -from ytdl_sub.validators.string_formatter_validators import OverridesFloatFormatterValidator +from ytdl_sub.validators.string_formatter_validators import ( + FloatFormatterValidator, + OverridesFloatFormatterValidator, +) from ytdl_sub.validators.validators import ProbabilityValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -42,8 +41,8 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC): ) def _randomized_float(self, overrides: Overrides, entry: Optional[Entry] = None) -> float: - actualized_min = float(overrides.apply_formatter(self._min, entry=entry)) - actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) + actualized_min = overrides.apply_formatter(self._min, entry=entry, expected_type=float) + actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) if actualized_min < 0: raise self._validation_exception( @@ -70,7 +69,7 @@ class _RandomizedRangeValidator(StrictDictValidator, ABC): ------- Max possible value """ - actualized_max = float(overrides.apply_formatter(self._max, entry=entry)) + actualized_max = overrides.apply_formatter(self._max, entry=entry, expected_type=float) if actualized_max < 0: raise self._validation_exception( f"max must be greater than zero, received {actualized_max}" diff --git a/src/ytdl_sub/plugins/video_tags.py b/src/ytdl_sub/plugins/video_tags.py index 16b7d949..00c5d460 100644 --- a/src/ytdl_sub/plugins/video_tags.py +++ b/src/ytdl_sub/plugins/video_tags.py @@ -34,7 +34,7 @@ class VideoTagsPlugin(Plugin[VideoTagsOptions]): Tags the entry's audio file using values defined in the metadata options """ tags_to_write: Dict[str, str] = {} - for tag_name, tag_formatter in self.plugin_options.dict.items(): + for tag_name, tag_formatter in sorted(self.plugin_options.dict.items()): tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) tags_to_write[tag_name] = tag_value diff --git a/src/ytdl_sub/prebuilt_presets/__init__.py b/src/ytdl_sub/prebuilt_presets/__init__.py index d1ed554b..bc3701c3 100644 --- a/src/ytdl_sub/prebuilt_presets/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/__init__.py @@ -1,8 +1,6 @@ import os import pathlib -from typing import Any -from typing import Dict -from typing import Set +from typing import Any, Dict, Set import mergedeep diff --git a/src/ytdl_sub/prebuilt_presets/music/__init__.py b/src/ytdl_sub/prebuilt_presets/music/__init__.py index e8c8cb23..c490fdd4 100644 --- a/src/ytdl_sub/prebuilt_presets/music/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/music/__init__.py @@ -1,5 +1,4 @@ -from ytdl_sub.prebuilt_presets import PrebuiltPresets -from ytdl_sub.prebuilt_presets import get_prebuilt_preset_package_name +from ytdl_sub.prebuilt_presets import PrebuiltPresets, get_prebuilt_preset_package_name PREBUILT_PRESET_PACKAGE_NAME = get_prebuilt_preset_package_name(__file__) diff --git a/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml b/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml index 32eb964a..966be5f8 100644 --- a/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml +++ b/src/ytdl_sub/prebuilt_presets/music/soundcloud.yaml @@ -8,7 +8,7 @@ presets: urls: # The first URL will be all the artist's tracks. # Treat these as singles - an album with a single track - - url: "{url}/tracks" + - url: "{url}" include_sibling_metadata: False variables: sc_track_album: "{title}" diff --git a/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py b/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py index 76b18443..79eb90d0 100644 --- a/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/music_videos/__init__.py @@ -1,5 +1,4 @@ -from ytdl_sub.prebuilt_presets import PrebuiltPresets -from ytdl_sub.prebuilt_presets import get_prebuilt_preset_package_name +from ytdl_sub.prebuilt_presets import PrebuiltPresets, get_prebuilt_preset_package_name PREBUILT_PRESET_PACKAGE_NAME = get_prebuilt_preset_package_name(__file__) diff --git a/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py b/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py index 0e10c448..bc05a466 100644 --- a/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py +++ b/src/ytdl_sub/prebuilt_presets/tv_show/__init__.py @@ -1,5 +1,4 @@ -from ytdl_sub.prebuilt_presets import PrebuiltPresets -from ytdl_sub.prebuilt_presets import get_prebuilt_preset_package_name +from ytdl_sub.prebuilt_presets import PrebuiltPresets, get_prebuilt_preset_package_name PREBUILT_PRESET_PACKAGE_NAME = get_prebuilt_preset_package_name(__file__) diff --git a/src/ytdl_sub/script/functions/__init__.py b/src/ytdl_sub/script/functions/__init__.py index e43f7e28..0153e992 100644 --- a/src/ytdl_sub/script/functions/__init__.py +++ b/src/ytdl_sub/script/functions/__init__.py @@ -1,5 +1,4 @@ -from typing import Callable -from typing import Dict +from typing import Callable, Dict from ytdl_sub.script.functions.array_functions import ArrayFunctions from ytdl_sub.script.functions.boolean_functions import BooleanFunctions diff --git a/src/ytdl_sub/script/functions/array_functions.py b/src/ytdl_sub/script/functions/array_functions.py index a07585e7..b2c200f8 100644 --- a/src/ytdl_sub/script/functions/array_functions.py +++ b/src/ytdl_sub/script/functions/array_functions.py @@ -1,18 +1,21 @@ import itertools -from typing import List -from typing import Optional +from typing import List, Optional from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import LambdaReduce -from ytdl_sub.script.types.resolvable import LambdaTwo -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.utils.exceptions import UNREACHABLE -from ytdl_sub.script.utils.exceptions import ArrayValueDoesNotExist -from ytdl_sub.script.utils.exceptions import FunctionRuntimeException +from ytdl_sub.script.types.resolvable import ( + AnyArgument, + Boolean, + Integer, + Lambda, + LambdaReduce, + LambdaTwo, + Resolvable, +) +from ytdl_sub.script.utils.exceptions import ( + UNREACHABLE, + ArrayValueDoesNotExist, + FunctionRuntimeException, +) class ArrayFunctions: diff --git a/src/ytdl_sub/script/functions/boolean_functions.py b/src/ytdl_sub/script/functions/boolean_functions.py index f2c89e28..3a407deb 100644 --- a/src/ytdl_sub/script/functions/boolean_functions.py +++ b/src/ytdl_sub/script/functions/boolean_functions.py @@ -1,10 +1,6 @@ from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.map import Map -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import AnyArgument, Boolean, Float, Integer, String # pylint: disable=invalid-name diff --git a/src/ytdl_sub/script/functions/conditional_functions.py b/src/ytdl_sub/script/functions/conditional_functions.py index 896ddd59..f04df860 100644 --- a/src/ytdl_sub/script/functions/conditional_functions.py +++ b/src/ytdl_sub/script/functions/conditional_functions.py @@ -1,9 +1,11 @@ from typing import Union -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import ReturnableArgumentA -from ytdl_sub.script.types.resolvable import ReturnableArgumentB +from ytdl_sub.script.types.resolvable import ( + AnyArgument, + Boolean, + ReturnableArgumentA, + ReturnableArgumentB, +) from ytdl_sub.script.utils.exceptions import FunctionRuntimeException diff --git a/src/ytdl_sub/script/functions/date_functions.py b/src/ytdl_sub/script/functions/date_functions.py index fea1095c..65b8b20b 100644 --- a/src/ytdl_sub/script/functions/date_functions.py +++ b/src/ytdl_sub/script/functions/date_functions.py @@ -1,7 +1,6 @@ from datetime import datetime -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Integer, String class DateFunctions: diff --git a/src/ytdl_sub/script/functions/error_functions.py b/src/ytdl_sub/script/functions/error_functions.py index 34b92a74..a5181b82 100644 --- a/src/ytdl_sub/script/functions/error_functions.py +++ b/src/ytdl_sub/script/functions/error_functions.py @@ -1,6 +1,4 @@ -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import ReturnableArgument -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import AnyArgument, ReturnableArgument, String from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError diff --git a/src/ytdl_sub/script/functions/json_functions.py b/src/ytdl_sub/script/functions/json_functions.py index c706a86f..b29d9218 100644 --- a/src/ytdl_sub/script/functions/json_functions.py +++ b/src/ytdl_sub/script/functions/json_functions.py @@ -3,12 +3,14 @@ from typing import Any from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.map import Map -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import ( + AnyArgument, + Boolean, + Float, + Integer, + Resolvable, + String, +) from ytdl_sub.script.utils.exceptions import UNREACHABLE diff --git a/src/ytdl_sub/script/functions/map_functions.py b/src/ytdl_sub/script/functions/map_functions.py index 6d93d31b..7bda27e6 100644 --- a/src/ytdl_sub/script/functions/map_functions.py +++ b/src/ytdl_sub/script/functions/map_functions.py @@ -1,18 +1,21 @@ -from typing import Dict -from typing import Optional +from typing import Dict, Optional from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.map import Map -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Hashable -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import LambdaThree -from ytdl_sub.script.types.resolvable import LambdaTwo -from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.utils.exceptions import FunctionRuntimeException -from ytdl_sub.script.utils.exceptions import KeyDoesNotExistRuntimeException -from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException +from ytdl_sub.script.types.resolvable import ( + AnyArgument, + Boolean, + Hashable, + Integer, + LambdaThree, + LambdaTwo, + String, +) +from ytdl_sub.script.utils.exceptions import ( + FunctionRuntimeException, + KeyDoesNotExistRuntimeException, + KeyNotHashableRuntimeException, +) class MapFunctions: diff --git a/src/ytdl_sub/script/functions/numeric_functions.py b/src/ytdl_sub/script/functions/numeric_functions.py index 003c8837..d11728f2 100644 --- a/src/ytdl_sub/script/functions/numeric_functions.py +++ b/src/ytdl_sub/script/functions/numeric_functions.py @@ -2,10 +2,7 @@ import math from typing import Optional from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Numeric +from ytdl_sub.script.types.resolvable import AnyArgument, Float, Integer, Numeric def _to_numeric(value: int | float) -> Numeric: diff --git a/src/ytdl_sub/script/functions/print_functions.py b/src/ytdl_sub/script/functions/print_functions.py index 47084085..9ef6573d 100644 --- a/src/ytdl_sub/script/functions/print_functions.py +++ b/src/ytdl_sub/script/functions/print_functions.py @@ -1,8 +1,6 @@ from typing import Optional -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import ReturnableArgument +from ytdl_sub.script.types.resolvable import AnyArgument, Integer, ReturnableArgument from ytdl_sub.utils.logger import Logger logger = Logger.get(name="preset") diff --git a/src/ytdl_sub/script/functions/regex_functions.py b/src/ytdl_sub/script/functions/regex_functions.py index b41ead02..bf38a061 100644 --- a/src/ytdl_sub/script/functions/regex_functions.py +++ b/src/ytdl_sub/script/functions/regex_functions.py @@ -1,15 +1,9 @@ import re -from typing import AnyStr -from typing import List -from typing import Match -from typing import Optional +from typing import AnyStr, List, Match, Optional from ytdl_sub.script.functions.array_functions import ArrayFunctions from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Boolean, Float, Integer, String from ytdl_sub.script.utils.exceptions import FunctionRuntimeException diff --git a/src/ytdl_sub/script/functions/string_functions.py b/src/ytdl_sub/script/functions/string_functions.py index a1f4567e..7f3be4f6 100644 --- a/src/ytdl_sub/script/functions/string_functions.py +++ b/src/ytdl_sub/script/functions/string_functions.py @@ -1,12 +1,7 @@ from typing import Optional from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.resolvable import AnyArgument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Numeric -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import AnyArgument, Boolean, Float, Integer, Numeric, String class StringFunctions: diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 73dbb3f4..b87f79b2 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -1,39 +1,31 @@ import json from enum import Enum -from typing import Dict -from typing import List -from typing import Optional -from typing import Set +from typing import Dict, List, Optional, Set from ytdl_sub.script.functions import Functions from ytdl_sub.script.types.array import UnresolvedArray -from ytdl_sub.script.types.function import Argument -from ytdl_sub.script.types.function import BuiltInFunction -from ytdl_sub.script.types.function import CustomFunction -from ytdl_sub.script.types.function import Function +from ytdl_sub.script.types.function import Argument, BuiltInFunction, CustomFunction, Function from ytdl_sub.script.types.map import UnresolvedMap -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import NonHashable -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Boolean, Float, Integer, Lambda, NonHashable, String from ytdl_sub.script.types.syntax_tree import SyntaxTree -from ytdl_sub.script.types.variable import FunctionArgument -from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.types.variable import FunctionArgument, Variable from ytdl_sub.script.utils.exception_formatters import ParserExceptionFormatter -from ytdl_sub.script.utils.exceptions import UNREACHABLE -from ytdl_sub.script.utils.exceptions import CycleDetected -from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist -from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments -from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArgumentName -from ytdl_sub.script.utils.exceptions import InvalidSyntaxException -from ytdl_sub.script.utils.exceptions import InvalidVariableName -from ytdl_sub.script.utils.exceptions import UserException -from ytdl_sub.script.utils.exceptions import VariableDoesNotExist -from ytdl_sub.script.utils.name_validation import is_function -from ytdl_sub.script.utils.name_validation import to_function_name -from ytdl_sub.script.utils.name_validation import validate_variable_name +from ytdl_sub.script.utils.exceptions import ( + UNREACHABLE, + CycleDetected, + FunctionDoesNotExist, + IncompatibleFunctionArguments, + InvalidCustomFunctionArgumentName, + InvalidSyntaxException, + InvalidVariableName, + UserException, + VariableDoesNotExist, +) +from ytdl_sub.script.utils.name_validation import ( + is_function, + to_function_name, + validate_variable_name, +) # pylint: disable=invalid-name # pylint: disable=too-many-branches @@ -580,7 +572,6 @@ class _Parser: return True def _parse(self) -> SyntaxTree: - while ch := self._read(): continue_parse = self._parse_main_loop(ch) if not continue_parse: diff --git a/src/ytdl_sub/script/script.py b/src/ytdl_sub/script/script.py index 2e436e81..5652a0c6 100644 --- a/src/ytdl_sub/script/script.py +++ b/src/ytdl_sub/script/script.py @@ -1,32 +1,28 @@ # pylint: disable=missing-raises-doc -import copy -from typing import Dict -from typing import List -from typing import Optional -from typing import Set +from collections import defaultdict +from typing import Dict, List, Optional, Set from ytdl_sub.script.functions import Functions from ytdl_sub.script.parser import parse from ytdl_sub.script.script_output import ScriptOutput -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import BuiltInFunctionType -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree -from ytdl_sub.script.types.syntax_tree import SyntaxTree -from ytdl_sub.script.types.variable import FunctionArgument -from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.types.resolvable import Argument, BuiltInFunctionType, Lambda, Resolvable +from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree, SyntaxTree +from ytdl_sub.script.types.variable import FunctionArgument, Variable from ytdl_sub.script.types.variable_dependency import VariableDependency -from ytdl_sub.script.utils.exceptions import UNREACHABLE -from ytdl_sub.script.utils.exceptions import CycleDetected -from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments -from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments -from ytdl_sub.script.utils.exceptions import RuntimeException -from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved -from ytdl_sub.script.utils.name_validation import is_function -from ytdl_sub.script.utils.name_validation import to_function_definition_name -from ytdl_sub.script.utils.name_validation import to_function_name -from ytdl_sub.script.utils.name_validation import validate_variable_name +from ytdl_sub.script.utils.exceptions import ( + UNREACHABLE, + CycleDetected, + IncompatibleFunctionArguments, + InvalidCustomFunctionArguments, + RuntimeException, + ScriptVariableNotResolved, +) +from ytdl_sub.script.utils.name_validation import ( + is_function, + to_function_definition_name, + to_function_name, + validate_variable_name, +) from ytdl_sub.script.utils.type_checking import FunctionSpec @@ -38,59 +34,71 @@ class Script: ``{ %custom_function: syntax }`` """ - def _ensure_no_cycle( + def _throw_cycle_error( self, name: str, dep: str, deps: List[str], definitions: Dict[str, SyntaxTree] ): - if dep not in definitions: - return # does not exist, will throw downstream in parser + type_name, pre = ( + ("custom functions", "%") if definitions is self._functions else ("variables", "") + ) + cycle_deps = [name] + deps + [dep] + cycle_deps_str = " -> ".join([f"{pre}{name}" for name in cycle_deps]) - if name in deps + [dep]: - type_name, pre = ( - ("custom functions", "%") if definitions is self._functions else ("variables", "") - ) - cycle_deps = [name] + deps + [dep] - cycle_deps_str = " -> ".join([f"{pre}{name}" for name in cycle_deps]) - - raise CycleDetected(f"Cycle detected within these {type_name}: {cycle_deps_str}") + raise CycleDetected(f"Cycle detected within these {type_name}: {cycle_deps_str}") def _traverse_variable_dependencies( self, variable_name: str, variable_dependency: SyntaxTree, deps: List[str], + ensured: Dict[str, Set[str]], ) -> None: for dep in variable_dependency.variables: - self._ensure_no_cycle( - name=variable_name, dep=dep.name, deps=deps, definitions=self._variables - ) + if variable_name == dep.name: + self._throw_cycle_error( + name=variable_name, dep=dep.name, deps=deps, definitions=self._variables + ) + + if dep.name in ensured[variable_name]: + continue + self._traverse_variable_dependencies( variable_name=variable_name, variable_dependency=self._variables[dep.name], deps=deps + [dep.name], + ensured=ensured, ) + ensured[variable_name].add(dep.name) for custom_func in variable_dependency.custom_function_dependencies( custom_function_definitions=self._functions ): for dep in self._functions[custom_func.name].variables: - self._ensure_no_cycle( - name=variable_name, - dep=dep.name, - deps=deps + [custom_func.definition_name()], - definitions=self._variables, - ) + if variable_name == dep.name: + self._throw_cycle_error( + name=variable_name, + dep=dep.name, + deps=deps + [custom_func.definition_name()], + definitions=self._variables, + ) + if dep.name in ensured[variable_name]: + continue + self._traverse_variable_dependencies( variable_name=variable_name, variable_dependency=self._variables[dep.name], deps=deps + [custom_func.definition_name(), dep.name], + ensured=ensured, ) + ensured[variable_name].add(dep.name) def _ensure_no_variable_cycles(self, variables: Dict[str, SyntaxTree]): + ensured: Dict[str, Set[str]] = defaultdict(set) for variable_name, variable_definition in variables.items(): self._traverse_variable_dependencies( variable_name=variable_name, variable_dependency=variable_definition, deps=[], + ensured=ensured, ) def _traverse_custom_function_dependencies( @@ -100,9 +108,10 @@ class Script: deps: List[str], ) -> None: for dep in custom_function_dependency.custom_functions: - self._ensure_no_cycle( - name=custom_function_name, dep=dep.name, deps=deps, definitions=self._functions - ) + if custom_function_name == dep.name: + self._throw_cycle_error( + name=custom_function_name, dep=dep.name, deps=deps, definitions=self._functions + ) self._traverse_custom_function_dependencies( custom_function_name=custom_function_name, custom_function_dependency=self._functions[dep.name], @@ -679,6 +688,18 @@ class Script: raise RuntimeException(f"Tried to get unresolved variable {variable_name}") + def definition_of(self, name: str) -> SyntaxTree: + """ + Returns + ------- + The definition of the variable or function. + """ + if name.startswith("%") and name[1:] in self._functions: + return self._functions[name[1:]] + if name in self._variables: + return self._variables[name] + raise RuntimeException(f"Tried to get non-existent definition with name {name}") + @property def variable_names(self) -> Set[str]: """ @@ -699,10 +720,56 @@ class Script: """ return set(to_function_definition_name(name) for name in self._functions.keys()) - def resolve_partial( + def _resolve_partial_loop( + self, + output_filter: Optional[Set[str]], + resolved: Dict[Variable, Resolvable], + unresolved: Dict[Variable, Argument], + unresolvable: Optional[Set[str]], + ): + to_partially_resolve: Set[Variable] = ( + {Variable(name) for name in output_filter} if output_filter else set(unresolved.keys()) + ) + + partially_resolved = True + while partially_resolved: + partially_resolved = False + + for variable in list(to_partially_resolve): + definition = unresolved[variable] + maybe_resolved = definition + + if isinstance(definition, Variable) and definition.name not in unresolvable: + if definition in resolved: + maybe_resolved = resolved[definition] + elif definition in unresolved: + maybe_resolved = unresolved[definition] + else: + raise UNREACHABLE + elif isinstance(definition, VariableDependency): + maybe_resolved = definition.partial_resolve( + resolved_variables=resolved, + unresolved_variables=unresolved, + custom_functions=self._functions, + ) + + if isinstance(maybe_resolved, Resolvable): + resolved[variable] = maybe_resolved + del unresolved[variable] + to_partially_resolve.remove(variable) + partially_resolved = True + else: + unresolved[variable] = maybe_resolved + + # If the definition changed, then the script changed + # which means we can iterate again + partially_resolved |= definition != maybe_resolved + + def _resolve_partial( self, unresolvable: Optional[Set[str]] = None, - ) -> "Script": + output_filter: Optional[Set[str]] = None, + ) -> Dict[str, SyntaxTree]: """ Returns ------- @@ -717,40 +784,54 @@ class Script: if name not in unresolvable } - partially_resolved = True - while partially_resolved: - - partially_resolved = False - - for variable in list(unresolved.keys()): - definition = unresolved[variable] - - maybe_resolved = definition - if isinstance(definition, Variable) and definition.name not in unresolvable: - maybe_resolved = resolved.get(definition, unresolved[definition]) - elif isinstance(definition, VariableDependency): - maybe_resolved = definition.partial_resolve( - resolved_variables=resolved, - unresolved_variables=unresolved, - custom_functions=self._functions, - ) - - if isinstance(maybe_resolved, Resolvable): - resolved[variable] = maybe_resolved - del unresolved[variable] - partially_resolved = True - else: - unresolved[variable] = maybe_resolved - - # If the definition changed, then the script changed - # which means we can iterate again - partially_resolved |= definition != maybe_resolved - - return copy.deepcopy(self).add_parsed( - {var_name: self._variables[var_name] for var_name in unresolvable} - | { - var.name: ResolvedSyntaxTree(ast=[definition]) - for var, definition in resolved.items() - } - | {var.name: SyntaxTree(ast=[definition]) for var, definition in unresolved.items()} + self._resolve_partial_loop( + output_filter=output_filter, + resolved=resolved, + unresolved=unresolved, + unresolvable=unresolvable, ) + + if output_filter: + out: Dict[str, SyntaxTree] = {} + for name in output_filter: + variable_name = Variable(name) + if variable_name in resolved: + out[name] = ResolvedSyntaxTree(ast=[resolved[variable_name]]) + else: + out[name] = SyntaxTree(ast=[unresolved[variable_name]]) + + return out + + return { + var.name: ResolvedSyntaxTree(ast=[definition]) for var, definition in resolved.items() + } | {var.name: SyntaxTree(ast=[definition]) for var, definition in unresolved.items()} + + def resolve_partial( + self, + unresolvable: Optional[Set[str]] = None, + output_filter: Optional[Set[str]] = None, + ) -> "Script": + """ + Updates the internal script to resolve as much as possible. + """ + out = self._resolve_partial(unresolvable=unresolvable, output_filter=output_filter) + for var_name, definition in out.items(): + self._variables[var_name] = definition + + return self + + def resolve_partial_once( + self, variable_definitions: Dict[str, SyntaxTree], unresolvable: Optional[Set[str]] = None + ) -> Dict[str, SyntaxTree]: + """ + Partially resolves the input variable definitions as much as possible. + """ + try: + self.add_parsed(variable_definitions) + return self._resolve_partial( + unresolvable=unresolvable, + output_filter=set(list(variable_definitions.keys())), + ) + finally: + for name in variable_definitions.keys(): + self._variables.pop(name, None) diff --git a/src/ytdl_sub/script/script_output.py b/src/ytdl_sub/script/script_output.py index d8275e6e..7e3cc782 100644 --- a/src/ytdl_sub/script/script_output.py +++ b/src/ytdl_sub/script/script_output.py @@ -1,6 +1,5 @@ from dataclasses import dataclass -from typing import Any -from typing import Dict +from typing import Any, Dict from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved diff --git a/src/ytdl_sub/script/types/array.py b/src/ytdl_sub/script/types/array.py index 204697fb..4ec3a444 100644 --- a/src/ytdl_sub/script/types/array.py +++ b/src/ytdl_sub/script/types/array.py @@ -1,15 +1,14 @@ from abc import ABC from dataclasses import dataclass -from typing import Any -from typing import Dict -from typing import List -from typing import Type +from typing import Any, Dict, List, Type -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import FutureResolvable -from ytdl_sub.script.types.resolvable import NonHashable -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import ResolvableToJson +from ytdl_sub.script.types.resolvable import ( + Argument, + FutureResolvable, + NonHashable, + Resolvable, + ResolvableToJson, +) from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable_dependency import VariableDependency diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index d34c0a47..0fa108e8 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -1,38 +1,34 @@ import functools from abc import ABC from dataclasses import dataclass -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional -from typing import Type -from typing import Union +from typing import Callable, Dict, List, Optional, Type, Union from ytdl_sub.script.functions import Functions -from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.array import UnresolvedArray -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import BuiltInFunctionType -from ytdl_sub.script.types.resolvable import FunctionType -from ytdl_sub.script.types.resolvable import FutureResolvable -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import NamedCustomFunction -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import ReturnableArgument -from ytdl_sub.script.types.resolvable import ReturnableArgumentA -from ytdl_sub.script.types.resolvable import ReturnableArgumentB -from ytdl_sub.script.types.variable import FunctionArgument -from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.types.array import Array, UnresolvedArray +from ytdl_sub.script.types.resolvable import ( + Argument, + Boolean, + BuiltInFunctionType, + FunctionType, + FutureResolvable, + Integer, + Lambda, + NamedCustomFunction, + Resolvable, + ReturnableArgument, + ReturnableArgumentA, + ReturnableArgumentB, +) +from ytdl_sub.script.types.variable import FunctionArgument, Variable from ytdl_sub.script.types.variable_dependency import VariableDependency from ytdl_sub.script.utils.exception_formatters import FunctionArgumentsExceptionFormatter -from ytdl_sub.script.utils.exceptions import UNREACHABLE -from ytdl_sub.script.utils.exceptions import FunctionRuntimeException -from ytdl_sub.script.utils.exceptions import RuntimeException -from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError -from ytdl_sub.script.utils.type_checking import FunctionSpec -from ytdl_sub.script.utils.type_checking import is_union +from ytdl_sub.script.utils.exceptions import ( + UNREACHABLE, + FunctionRuntimeException, + RuntimeException, + UserThrownRuntimeError, +) +from ytdl_sub.script.utils.type_checking import FunctionSpec, is_union @dataclass(frozen=True) @@ -371,13 +367,6 @@ class BuiltInFunction(Function, BuiltInFunctionType): If the conditional partially resolvable enough to warrant evaluation, perform it here. """ - if self.is_subset_of( - variables=resolved_variables, custom_function_definitions=custom_functions - ): - return self.resolve( - resolved_variables=resolved_variables, - custom_functions=custom_functions, - ) if self.name == "if": maybe_resolvable_arg, is_resolvable = VariableDependency.try_partial_resolve( diff --git a/src/ytdl_sub/script/types/map.py b/src/ytdl_sub/script/types/map.py index d5e2eb27..5efeff75 100644 --- a/src/ytdl_sub/script/types/map.py +++ b/src/ytdl_sub/script/types/map.py @@ -1,17 +1,16 @@ import itertools from abc import ABC from dataclasses import dataclass -from typing import Any -from typing import Dict -from typing import List -from typing import Type +from typing import Any, Dict, List, Type -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import FutureResolvable -from ytdl_sub.script.types.resolvable import Hashable -from ytdl_sub.script.types.resolvable import NonHashable -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import ResolvableToJson +from ytdl_sub.script.types.resolvable import ( + Argument, + FutureResolvable, + Hashable, + NonHashable, + Resolvable, + ResolvableToJson, +) from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable_dependency import VariableDependency from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index ad9ff300..8edd4424 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -1,12 +1,7 @@ import json -from abc import ABC -from abc import abstractmethod +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any -from typing import Generic -from typing import List -from typing import Type -from typing import TypeVar +from typing import Any, Generic, List, Type, TypeVar T = TypeVar("T") NumericT = TypeVar("NumericT", bound=int | float) diff --git a/src/ytdl_sub/script/types/syntax_tree.py b/src/ytdl_sub/script/types/syntax_tree.py index 2a970300..e38822ff 100644 --- a/src/ytdl_sub/script/types/syntax_tree.py +++ b/src/ytdl_sub/script/types/syntax_tree.py @@ -1,12 +1,8 @@ from dataclasses import dataclass -from typing import Dict -from typing import List -from typing import Optional +from typing import Dict, List, Optional from ytdl_sub.script.types.function import BuiltInFunction -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Argument, Resolvable, String from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable_dependency import VariableDependency @@ -55,6 +51,10 @@ class SyntaxTree(VariableDependency): custom_functions=custom_functions, ) + # If no arguments, must be empty string + if len(maybe_resolvable_values) == 0: + return String(value="") + # Mimic the above resolve behavior if len(maybe_resolvable_values) > 1: return BuiltInFunction(name="concat", args=maybe_resolvable_values) diff --git a/src/ytdl_sub/script/types/variable_dependency.py b/src/ytdl_sub/script/types/variable_dependency.py index 5cfee863..3599d65c 100644 --- a/src/ytdl_sub/script/types/variable_dependency.py +++ b/src/ytdl_sub/script/types/variable_dependency.py @@ -1,24 +1,17 @@ -from abc import ABC -from abc import abstractmethod +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Dict -from typing import Iterable -from typing import List -from typing import Set -from typing import Tuple -from typing import Type -from typing import TypeVar -from typing import final +from typing import Dict, Iterable, List, Set, Tuple, Type, TypeVar, final -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import BuiltInFunctionType -from ytdl_sub.script.types.resolvable import FunctionType -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import NamedCustomFunction -from ytdl_sub.script.types.resolvable import ParsedCustomFunction -from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.variable import FunctionArgument -from ytdl_sub.script.types.variable import Variable +from ytdl_sub.script.types.resolvable import ( + Argument, + BuiltInFunctionType, + FunctionType, + Lambda, + NamedCustomFunction, + ParsedCustomFunction, + Resolvable, +) +from ytdl_sub.script.types.variable import FunctionArgument, Variable from ytdl_sub.script.utils.exceptions import UNREACHABLE TypeT = TypeVar("TypeT") @@ -44,7 +37,7 @@ class VariableDependency(ABC): output.append(arg) elif instance and isinstance(arg, ttype): output.append(arg) - elif type(arg) == ttype: # pylint: disable=unidiomatic-typecheck + elif type(arg) is ttype: # pylint: disable=unidiomatic-typecheck output.append(arg) if isinstance(arg, VariableDependency): @@ -279,8 +272,11 @@ class VariableDependency(ABC): if not isinstance(maybe_resolvable_args[-1], Resolvable): is_resolvable = False elif isinstance(arg, Variable): - if arg not in resolved_variables: + if arg in resolved_variables: + maybe_resolvable_args[-1] = resolved_variables[arg] + else: is_resolvable = False + # Could be un unresolvable if arg in unresolved_variables: maybe_resolvable_args[-1] = unresolved_variables[arg] diff --git a/src/ytdl_sub/script/utils/exception_formatters.py b/src/ytdl_sub/script/utils/exception_formatters.py index b2cab76d..63a0a275 100644 --- a/src/ytdl_sub/script/utils/exception_formatters.py +++ b/src/ytdl_sub/script/utils/exception_formatters.py @@ -1,12 +1,9 @@ import sys -from typing import List -from typing import TypeVar +from typing import List, TypeVar from ytdl_sub.script.types.resolvable import BuiltInFunctionType -from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments -from ytdl_sub.script.utils.exceptions import UserException -from ytdl_sub.script.utils.type_checking import FunctionSpec -from ytdl_sub.script.utils.type_checking import is_union +from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments, UserException +from ytdl_sub.script.utils.type_checking import FunctionSpec, is_union UserExceptionT = TypeVar("UserExceptionT", bound=UserException) diff --git a/src/ytdl_sub/script/utils/name_validation.py b/src/ytdl_sub/script/utils/name_validation.py index 5b2597ce..708c51bf 100644 --- a/src/ytdl_sub/script/utils/name_validation.py +++ b/src/ytdl_sub/script/utils/name_validation.py @@ -1,8 +1,7 @@ import re from ytdl_sub.script.functions import Functions -from ytdl_sub.script.utils.exceptions import InvalidFunctionName -from ytdl_sub.script.utils.exceptions import InvalidVariableName +from ytdl_sub.script.utils.exceptions import InvalidFunctionName, InvalidVariableName _NAME_REGEX_VALIDATOR = re.compile(r"^[a-z][a-z0-9_]*$") diff --git a/src/ytdl_sub/script/utils/type_checking.py b/src/ytdl_sub/script/utils/type_checking.py index 7598a526..656c1c46 100644 --- a/src/ytdl_sub/script/utils/type_checking.py +++ b/src/ytdl_sub/script/utils/type_checking.py @@ -3,24 +3,20 @@ import inspect from dataclasses import dataclass from inspect import FullArgSpec from types import NoneType -from typing import Callable -from typing import List -from typing import Optional -from typing import Type -from typing import TypeVar -from typing import Union -from typing import get_origin +from typing import Callable, List, Optional, Type, TypeVar, Union, get_origin -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import BuiltInFunctionType -from ytdl_sub.script.types.resolvable import FutureResolvable -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import LambdaReduce -from ytdl_sub.script.types.resolvable import LambdaThree -from ytdl_sub.script.types.resolvable import LambdaTwo -from ytdl_sub.script.types.resolvable import NamedCustomFunction -from ytdl_sub.script.types.resolvable import NamedType -from ytdl_sub.script.types.resolvable import Resolvable +from ytdl_sub.script.types.resolvable import ( + Argument, + BuiltInFunctionType, + FutureResolvable, + Lambda, + LambdaReduce, + LambdaThree, + LambdaTwo, + NamedCustomFunction, + NamedType, + Resolvable, +) from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import UNREACHABLE diff --git a/src/ytdl_sub/subscriptions/base_subscription.py b/src/ytdl_sub/subscriptions/base_subscription.py index 656e98ed..55b96240 100644 --- a/src/ytdl_sub/subscriptions/base_subscription.py +++ b/src/ytdl_sub/subscriptions/base_subscription.py @@ -6,14 +6,12 @@ from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.preset_plugins import PresetPlugins from ytdl_sub.config.preset import Preset -from ytdl_sub.config.preset_options import OutputOptions -from ytdl_sub.config.preset_options import YTDLOptions -from ytdl_sub.config.validators.variable_validation import VariableValidation +from ytdl_sub.config.preset_options import OutputOptions, YTDLOptions +from ytdl_sub.config.validators.variable_validation import ResolutionLevel, VariableValidation from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.entries.variables.override_variables import SubscriptionVariables from ytdl_sub.utils.exceptions import SubscriptionPermissionError -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileHandlerTransactionLog +from ytdl_sub.utils.file_handler import FileHandler, FileHandlerTransactionLog from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.yaml import dump_yaml from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive @@ -79,7 +77,7 @@ class BaseSubscription(ABC): ) # Validate after adding the subscription name - self._validated_dict = VariableValidation( + _ = VariableValidation( overrides=self.overrides, downloader_options=self.downloader_options, output_options=self.output_options, @@ -254,12 +252,22 @@ class BaseSubscription(ABC): ------- Subscription in yaml format """ - return self._preset_options.yaml + return self._preset_options.yaml(subscription_only=False) - def resolved_yaml(self) -> str: + def resolved_yaml(self, resolution_level: int = ResolutionLevel.RESOLVE) -> str: """ Returns ------- Human-readable, condensed YAML definition of the subscription. """ - return dump_yaml(self._validated_dict) + if resolution_level == ResolutionLevel.ORIGINAL: + return self._preset_options.yaml(subscription_only=True) + + out = VariableValidation( + overrides=self.overrides, + downloader_options=self.downloader_options, + output_options=self.output_options, + plugins=self.plugins, + resolution_level=resolution_level, + ).ensure_proper_usage(partial_resolve_formatters=True) + return dump_yaml(out) diff --git a/src/ytdl_sub/subscriptions/subscription.py b/src/ytdl_sub/subscriptions/subscription.py index e40c2663..731ab969 100644 --- a/src/ytdl_sub/subscriptions/subscription.py +++ b/src/ytdl_sub/subscriptions/subscription.py @@ -1,9 +1,6 @@ import copy from pathlib import Path -from typing import Any -from typing import Dict -from typing import List -from typing import Optional +from typing import Any, Dict, List, Optional from mergedeep import mergedeep @@ -143,7 +140,6 @@ class Subscription(SubscriptionDownload): } for subscription_name, subscription_object in subscriptions_dicts.items(): - # Hard-override subscriptions here mergedeep.merge( subscription_object, diff --git a/src/ytdl_sub/subscriptions/subscription_download.py b/src/ytdl_sub/subscriptions/subscription_download.py index c9a8b6f7..f4696f8b 100644 --- a/src/ytdl_sub/subscriptions/subscription_download.py +++ b/src/ytdl_sub/subscriptions/subscription_download.py @@ -4,15 +4,15 @@ import os import shutil from abc import ABC from pathlib import Path -from typing import List -from typing import Optional +from typing import List, Optional -from ytdl_sub.config.plugin.plugin import Plugin -from ytdl_sub.config.plugin.plugin import SplitPlugin +from ytdl_sub.config.plugin.plugin import Plugin, SplitPlugin from ytdl_sub.config.plugin.plugin_mapping import PluginMapping from ytdl_sub.config.plugin.plugin_operation import PluginOperation -from ytdl_sub.downloaders.info_json.info_json_downloader import InfoJsonDownloader -from ytdl_sub.downloaders.info_json.info_json_downloader import InfoJsonDownloaderOptions +from ytdl_sub.downloaders.info_json.info_json_downloader import ( + InfoJsonDownloader, + InfoJsonDownloaderOptions, +) from ytdl_sub.downloaders.source_plugin import SourcePlugin from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder @@ -22,9 +22,7 @@ from ytdl_sub.subscriptions.base_subscription import BaseSubscription from ytdl_sub.subscriptions.subscription_ytdl_options import SubscriptionYTDLOptions from ytdl_sub.utils.datetime import to_date_range from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileHandlerTransactionLog -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandler, FileHandlerTransactionLog, FileMetadata from ytdl_sub.utils.logger import Logger logger: logging.Logger = Logger.get() @@ -155,8 +153,8 @@ class SubscriptionDownload(BaseSubscription, ABC): keep_max_files: Optional[int] = None if self.output_options.keep_max_files: # validated it can be cast to int within the validator - keep_max_files = int( - self.overrides.apply_formatter(self.output_options.keep_max_files) + keep_max_files = self.overrides.apply_formatter( + self.output_options.keep_max_files, expected_type=int ) if date_range_to_keep or self.output_options.keep_max_files is not None: diff --git a/src/ytdl_sub/subscriptions/subscription_validators.py b/src/ytdl_sub/subscriptions/subscription_validators.py index 9287a15b..6dd391dd 100644 --- a/src/ytdl_sub/subscriptions/subscription_validators.py +++ b/src/ytdl_sub/subscriptions/subscription_validators.py @@ -1,22 +1,20 @@ import copy import dataclasses -from abc import ABC -from abc import abstractmethod -from typing import Dict -from typing import List -from typing import Optional -from typing import final +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, final from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.overrides import Overrides from ytdl_sub.entries.variables.override_variables import SubscriptionVariables from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.validators.string_formatter_validators import UnstructuredDictFormatterValidator -from ytdl_sub.validators.validators import DictValidator -from ytdl_sub.validators.validators import LiteralDictValidator -from ytdl_sub.validators.validators import StringListValidator -from ytdl_sub.validators.validators import StringValidator -from ytdl_sub.validators.validators import Validator +from ytdl_sub.validators.validators import ( + DictValidator, + LiteralDictValidator, + StringListValidator, + StringValidator, + Validator, +) class SubscriptionOutput(Validator, ABC): diff --git a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py index b5772197..8a4526b6 100644 --- a/src/ytdl_sub/subscriptions/subscription_ytdl_options.py +++ b/src/ytdl_sub/subscriptions/subscription_ytdl_options.py @@ -1,9 +1,5 @@ from pathlib import Path -from typing import Dict -from typing import List -from typing import Optional -from typing import Type -from typing import TypeVar +from typing import Dict, List, Optional, Type, TypeVar from yt_dlp import match_filter_func @@ -15,9 +11,7 @@ from ytdl_sub.plugins.audio_extract import AudioExtractPlugin from ytdl_sub.plugins.chapters import ChaptersPlugin from ytdl_sub.plugins.file_convert import FileConvertPlugin from ytdl_sub.plugins.format import FormatPlugin -from ytdl_sub.plugins.match_filters import MatchFiltersPlugin -from ytdl_sub.plugins.match_filters import combine_filters -from ytdl_sub.plugins.match_filters import default_filters +from ytdl_sub.plugins.match_filters import MatchFiltersPlugin, combine_filters, default_filters from ytdl_sub.plugins.subtitles import SubtitlesPlugin from ytdl_sub.plugins.throttle_protection import ThrottleProtectionPlugin from ytdl_sub.utils.ffmpeg import FFMPEG @@ -94,8 +88,8 @@ class SubscriptionYTDLOptions: self._enhanced_download_archive.working_ytdl_file_path ) if self._preset.output_options.keep_max_files: - keep_max_files = int( - self._overrides.apply_formatter(self._preset.output_options.keep_max_files) + keep_max_files = self._overrides.apply_formatter( + self._preset.output_options.keep_max_files, expected_type=int ) if keep_max_files > 0: # yt-dlp has a weird bug with max_downloads=1, set to 2 for safe measure diff --git a/src/ytdl_sub/thread/log_entries_downloaded_listener.py b/src/ytdl_sub/thread/log_entries_downloaded_listener.py index ffa9c3b9..1c4ededb 100644 --- a/src/ytdl_sub/thread/log_entries_downloaded_listener.py +++ b/src/ytdl_sub/thread/log_entries_downloaded_listener.py @@ -4,8 +4,7 @@ import threading import time from json import JSONDecodeError from pathlib import Path -from typing import Optional -from typing import Set +from typing import Optional, Set from ytdl_sub.utils.logger import Logger diff --git a/src/ytdl_sub/utils/chapters.py b/src/ytdl_sub/utils/chapters.py index bdd52f34..22df783a 100644 --- a/src/ytdl_sub/utils/chapters.py +++ b/src/ytdl_sub/utils/chapters.py @@ -1,19 +1,14 @@ import re -from typing import Dict -from typing import List -from typing import Tuple +from typing import Dict, List, Tuple -from ytdl_sub.entries.entry import Entry -from ytdl_sub.entries.entry import ytdl_sub_chapters_from_comments -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.entry import Entry, ytdl_sub_chapters_from_comments +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions from ytdl_sub.utils.file_handler import FileMetadata v: VariableDefinitions = VARIABLES class Timestamp: - # Captures the following formats: # 0:00 title # 00:00 title diff --git a/src/ytdl_sub/utils/ffmpeg.py b/src/ytdl_sub/utils/ffmpeg.py index 52374464..6896b03b 100644 --- a/src/ytdl_sub/utils/ffmpeg.py +++ b/src/ytdl_sub/utils/ffmpeg.py @@ -1,8 +1,6 @@ import subprocess import tempfile -from typing import Dict -from typing import List -from typing import Optional +from typing import Dict, List, Optional from ytdl_sub.utils.chapters import Chapters from ytdl_sub.utils.exceptions import ValidationException diff --git a/src/ytdl_sub/utils/file_handler.py b/src/ytdl_sub/utils/file_handler.py index 1db4bb6c..6fe51b1b 100644 --- a/src/ytdl_sub/utils/file_handler.py +++ b/src/ytdl_sub/utils/file_handler.py @@ -4,12 +4,7 @@ import os import shutil from collections import defaultdict from pathlib import Path -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Union +from typing import Any, Dict, List, Optional, Set, Union from ytdl_sub.utils.subtitles import SUBTITLE_EXTENSIONS diff --git a/src/ytdl_sub/utils/logger.py b/src/ytdl_sub/utils/logger.py index fcd734d5..10f98173 100644 --- a/src/ytdl_sub/utils/logger.py +++ b/src/ytdl_sub/utils/logger.py @@ -5,8 +5,7 @@ import sys import tempfile from dataclasses import dataclass from pathlib import Path -from typing import List -from typing import Optional +from typing import List, Optional from ytdl_sub import __local_version__ from ytdl_sub.utils.exceptions import ValidationException @@ -84,7 +83,6 @@ class StreamToLogger(io.StringIO): class Logger: - # The level set via CLI arguments _LOGGER_LEVEL: LoggerLevel = LoggerLevels.DEBUG diff --git a/src/ytdl_sub/utils/retry.py b/src/ytdl_sub/utils/retry.py index 4704a2e5..15e44bf3 100644 --- a/src/ytdl_sub/utils/retry.py +++ b/src/ytdl_sub/utils/retry.py @@ -1,8 +1,5 @@ from time import sleep -from typing import Any -from typing import Optional -from typing import Tuple -from typing import Type +from typing import Any, Optional, Tuple, Type from ytdl_sub.utils.logger import Logger diff --git a/src/ytdl_sub/utils/script.py b/src/ytdl_sub/utils/script.py index 22fc6487..5c9e7707 100644 --- a/src/ytdl_sub/utils/script.py +++ b/src/ytdl_sub/utils/script.py @@ -1,22 +1,12 @@ import json import re -from typing import Any -from typing import Dict -from typing import Optional +from typing import Any, Dict, Optional from ytdl_sub.script.parser import parse -from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.array import UnresolvedArray -from ytdl_sub.script.types.function import BuiltInFunction -from ytdl_sub.script.types.function import Function -from ytdl_sub.script.types.map import Map -from ytdl_sub.script.types.map import UnresolvedMap -from ytdl_sub.script.types.resolvable import Argument -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.array import Array, UnresolvedArray +from ytdl_sub.script.types.function import BuiltInFunction, Function +from ytdl_sub.script.types.map import Map, UnresolvedMap +from ytdl_sub.script.types.resolvable import Argument, Boolean, Float, Integer, Lambda, String from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import UNREACHABLE @@ -87,9 +77,8 @@ class ScriptUtils: ast = parse(text=value).ast if len(ast) == 1: return ast[0] - return BuiltInFunction( - name="concat", args=[BuiltInFunction(name="string", args=[arg]) for arg in ast] - ) + + return BuiltInFunction(name="concat", args=ast) if isinstance(value, bool): return Boolean(value) if isinstance(value, int): @@ -177,7 +166,6 @@ class ScriptUtils: @classmethod def _syntax_tree_to_native_script(cls, tree: SyntaxTree) -> str: - if (output := cls._is_top_level_string(tree)) is not None: return output diff --git a/src/ytdl_sub/utils/scriptable.py b/src/ytdl_sub/utils/scriptable.py index 38780024..c05d1d66 100644 --- a/src/ytdl_sub/utils/scriptable.py +++ b/src/ytdl_sub/utils/scriptable.py @@ -1,13 +1,9 @@ import copy from abc import ABC -from typing import Any -from typing import Dict -from typing import Optional -from typing import Set +from typing import Any, Dict, Optional, Set from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS -from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES -from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS +from ytdl_sub.entries.script.variable_definitions import UNRESOLVED_VARIABLES, VARIABLE_SCRIPTS from ytdl_sub.entries.script.variable_types import Variable from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_DEFINITIONS from ytdl_sub.script.script import Script diff --git a/src/ytdl_sub/utils/xml.py b/src/ytdl_sub/utils/xml.py index 1e5e3179..223f7532 100644 --- a/src/ytdl_sub/utils/xml.py +++ b/src/ytdl_sub/utils/xml.py @@ -1,9 +1,6 @@ import xml.etree.ElementTree as et from dataclasses import dataclass -from typing import Any -from typing import Dict -from typing import List -from typing import Union +from typing import Any, Dict, List, Union @dataclass diff --git a/src/ytdl_sub/utils/yaml.py b/src/ytdl_sub/utils/yaml.py index f0f04d5c..1c3153f4 100644 --- a/src/ytdl_sub/utils/yaml.py +++ b/src/ytdl_sub/utils/yaml.py @@ -6,8 +6,7 @@ from typing import Dict import yaml from yaml import YAMLError -from ytdl_sub.utils.exceptions import FileNotFoundException -from ytdl_sub.utils.exceptions import InvalidYamlException +from ytdl_sub.utils.exceptions import FileNotFoundException, InvalidYamlException from ytdl_sub.utils.logger import Logger logger = Logger.get(name="yaml") diff --git a/src/ytdl_sub/validators/audo_codec_validator.py b/src/ytdl_sub/validators/audo_codec_validator.py index 959d697a..ae01185c 100644 --- a/src/ytdl_sub/validators/audo_codec_validator.py +++ b/src/ytdl_sub/validators/audo_codec_validator.py @@ -1,5 +1,4 @@ -from typing import Dict -from typing import Set +from typing import Dict, Set from ytdl_sub.validators.string_select_validator import StringSelectValidator diff --git a/src/ytdl_sub/validators/file_path_validators.py b/src/ytdl_sub/validators/file_path_validators.py index f5a69f61..f0076d5a 100644 --- a/src/ytdl_sub/validators/file_path_validators.py +++ b/src/ytdl_sub/validators/file_path_validators.py @@ -3,8 +3,10 @@ from pathlib import Path from typing import Any from ytdl_sub.utils.file_path import FilePathTruncater -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 ( + OverridesStringFormatterValidator, + StringFormatterValidator, +) from ytdl_sub.validators.validators import StringValidator diff --git a/src/ytdl_sub/validators/nfo_validators.py b/src/ytdl_sub/validators/nfo_validators.py index d5cdd819..ee2d1f3d 100644 --- a/src/ytdl_sub/validators/nfo_validators.py +++ b/src/ytdl_sub/validators/nfo_validators.py @@ -1,18 +1,17 @@ from abc import ABC from collections import defaultdict -from typing import Dict -from typing import List +from typing import Dict, List from ytdl_sub.validators.strict_dict_validator import StrictDictValidator -from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator -from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator -from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator -from ytdl_sub.validators.validators import DictValidator -from ytdl_sub.validators.validators import ListValidator +from ytdl_sub.validators.string_formatter_validators import ( + DictFormatterValidator, + ListFormatterValidator, + StringFormatterValidator, +) +from ytdl_sub.validators.validators import DictValidator, ListValidator class NfoTagsWithAttributesValidator(StrictDictValidator): - _required_keys = {"attributes", "tag"} def __init__(self, name, value): diff --git a/src/ytdl_sub/validators/regex_validator.py b/src/ytdl_sub/validators/regex_validator.py index f7af98c7..7f1034c2 100644 --- a/src/ytdl_sub/validators/regex_validator.py +++ b/src/ytdl_sub/validators/regex_validator.py @@ -1,9 +1,7 @@ import re -from typing import List -from typing import Optional +from typing import List, Optional -from ytdl_sub.validators.validators import ListValidator -from ytdl_sub.validators.validators import StringValidator +from ytdl_sub.validators.validators import ListValidator, StringValidator class RegexValidator(StringValidator): diff --git a/src/ytdl_sub/validators/source_variable_validator.py b/src/ytdl_sub/validators/source_variable_validator.py index ba17c895..851fbd12 100644 --- a/src/ytdl_sub/validators/source_variable_validator.py +++ b/src/ytdl_sub/validators/source_variable_validator.py @@ -1,7 +1,6 @@ from ytdl_sub.script.utils.name_validation import is_valid_name from ytdl_sub.utils.exceptions import InvalidVariableNameException -from ytdl_sub.validators.validators import ListValidator -from ytdl_sub.validators.validators import StringValidator +from ytdl_sub.validators.validators import ListValidator, StringValidator class SourceVariableNameValidator(StringValidator): diff --git a/src/ytdl_sub/validators/strict_dict_validator.py b/src/ytdl_sub/validators/strict_dict_validator.py index e76f4eec..8eb90803 100644 --- a/src/ytdl_sub/validators/strict_dict_validator.py +++ b/src/ytdl_sub/validators/strict_dict_validator.py @@ -1,5 +1,4 @@ -from typing import List -from typing import Set +from typing import List, Set from ytdl_sub.validators.validators import DictValidator diff --git a/src/ytdl_sub/validators/string_formatter_validators.py b/src/ytdl_sub/validators/string_formatter_validators.py index 1581c702..780976ba 100644 --- a/src/ytdl_sub/validators/string_formatter_validators.py +++ b/src/ytdl_sub/validators/string_formatter_validators.py @@ -1,25 +1,23 @@ from datetime import datetime -from typing import Any -from typing import Dict -from typing import Set -from typing import Union -from typing import final +from typing import Any, Dict, Set, Union, final -from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.script.parser import parse from ytdl_sub.script.script import Script from ytdl_sub.script.types.syntax_tree import SyntaxTree -from ytdl_sub.script.utils.exceptions import RuntimeException -from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved -from ytdl_sub.script.utils.exceptions import UserException -from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError +from ytdl_sub.script.utils.exceptions import ( + RuntimeException, + ScriptVariableNotResolved, + UserException, +) from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException from ytdl_sub.utils.script import ScriptUtils -from ytdl_sub.validators.validators import DictValidator -from ytdl_sub.validators.validators import ListValidator -from ytdl_sub.validators.validators import LiteralDictValidator -from ytdl_sub.validators.validators import StringValidator -from ytdl_sub.validators.validators import Validator +from ytdl_sub.validators.validators import ( + DictValidator, + ListValidator, + LiteralDictValidator, + StringValidator, + Validator, +) # pylint: disable=protected-access @@ -82,35 +80,27 @@ class StringFormatterValidator(StringValidator): """ return self._parsed - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: Any) -> Any: """ Returns ------- - Apply any post processing to the resolved value + Apply any post processing to the resolved value. Defaults to casting it to string. """ - return resolved - - def post_process_native(self, resolved: Any) -> Any: - """ - Returns - ------- - Apply any post processing to the resolved native value. - """ - return resolved + return str(resolved) class FloatFormatterValidator(StringFormatterValidator): _expected_value_type_name = "float" - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: str) -> float: try: - float(resolved) + out = float(resolved) except Exception as exc: raise self._validation_exception( f"Expected a float, but received '{resolved}'" ) from exc - return resolved + return out class StandardizedDateValidator(StringFormatterValidator): @@ -127,6 +117,13 @@ class StandardizedDateValidator(StringFormatterValidator): return resolved +class BooleanFormatterValidator(StringFormatterValidator): + _expected_value_type_name = "boolean" + + def post_process(self, resolved: Any) -> bool: + return ScriptUtils.bool_formatter_output(output=str(resolved)) + + # pylint: disable=line-too-long class OverridesStringFormatterValidator(StringFormatterValidator): """ @@ -146,15 +143,14 @@ class OverridesStringFormatterValidator(StringFormatterValidator): class OverridesIntegerFormatterValidator(OverridesStringFormatterValidator): _expected_value_type_name = "integer" - def post_process(self, resolved: str) -> str: + def post_process(self, resolved: str) -> int: try: - int(resolved) + out = int(resolved) except Exception as exc: raise self._validation_exception( f"Expected an integer, but received '{resolved}'" ) from exc - - return resolved + return out class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringFormatterValidator): @@ -163,9 +159,14 @@ class OverridesFloatFormatterValidator(FloatFormatterValidator, OverridesStringF """ -class OverridesBooleanFormatterValidator(OverridesStringFormatterValidator): +class OverridesBooleanFormatterValidator( + BooleanFormatterValidator, OverridesStringFormatterValidator +): _expected_value_type_name = "boolean" + def post_process(self, resolved: Any) -> bool: + return ScriptUtils.bool_formatter_output(output=str(resolved)) + class ListFormatterValidator(ListValidator[StringFormatterValidator]): _inner_list_type = StringFormatterValidator @@ -183,12 +184,13 @@ class DictFormatterValidator(LiteralDictValidator): super().__init__(name, value) for key in self._keys: - self._value[key] = self._validate_key(key=key, validator=self._key_validator) + # Gets stored in __validator_dict + _ = self._validate_key(key=key, validator=self._key_validator) @property def dict(self) -> Dict[str, StringFormatterValidator]: """Returns dict with string formatter values""" - return self._value + return self._validator_dict @property def dict_with_format_strings(self) -> Dict[str, str]: @@ -210,7 +212,22 @@ class OverridesDictFormatterValidator(DictFormatterValidator): _key_validator = OverridesStringFormatterValidator +class AnyFormatterValidator(StringFormatterValidator): + """ + Applies no post-processing. + """ + + def post_process(self, resolved: Any) -> Any: + return resolved + + +class AnyOverridesFormatterValidator(AnyFormatterValidator, OverridesStringFormatterValidator): + pass + + class UnstructuredDictFormatterValidator(DictFormatterValidator): + _key_validator = AnyFormatterValidator + def __init__(self, name, value): # Convert the unstructured-ness into a script if isinstance(value, dict): @@ -219,33 +236,21 @@ class UnstructuredDictFormatterValidator(DictFormatterValidator): class UnstructuredOverridesDictFormatterValidator(UnstructuredDictFormatterValidator): - _key_validator = OverridesStringFormatterValidator - - -def to_variable_dependency_format_string(script: Script, parsed_format_string: SyntaxTree) -> str: - """ - Create a dummy format string that contains all variable deps as a string. - """ - dummy_format_string = "" - for var in parsed_format_string.variables: - dummy_format_string += f"{{ {var.name} }}" - for variable_dependency in script._variables[var.name].variables: - dummy_format_string += f"{{ {variable_dependency.name} }}" - return dummy_format_string + _key_validator = AnyOverridesFormatterValidator def _validate_formatter( mock_script: Script, unresolved_variables: Set[str], + unresolved_runtime_variables: Set[str], formatter_validator: Union[StringFormatterValidator, OverridesStringFormatterValidator], -) -> str: + partial_resolve_entry_formatters: bool, +) -> Any: parsed = formatter_validator.parsed if resolved := parsed.maybe_resolvable: - return resolved.native + return formatter_validator.post_process(resolved.native) is_static_formatter = isinstance(formatter_validator, OverridesStringFormatterValidator) - if is_static_formatter: - unresolved_variables = unresolved_variables.union({VARIABLES.entry_metadata.variable_name}) variable_names = {var.name for var in parsed.variables} custom_function_names = {f"%{func.name}" for func in parsed.custom_functions} @@ -265,20 +270,32 @@ def _validate_formatter( "contains the following custom functions that do not exist: " f"{', '.join(sorted(custom_function_names - mock_script.function_names))}" ) - if unresolved := variable_names.intersection(unresolved_variables): + if unresolved := variable_names.intersection(unresolved_runtime_variables): raise StringFormattingVariableNotFoundException( "contains the following variables that are unresolved when executing this " f"formatter: {', '.join(sorted(unresolved))}" ) + + if partial_resolve_entry_formatters and not is_static_formatter: + parsed = mock_script.resolve_partial_once( + variable_definitions={"tmp_var": formatter_validator.parsed}, + unresolvable=unresolved_variables, + )["tmp_var"] + try: if is_static_formatter: - return mock_script.resolve_once_parsed( - {"tmp_var": formatter_validator.parsed}, - unresolvable=unresolved_variables, - update=True, - )["tmp_var"].native + return formatter_validator.post_process( + mock_script.resolve_once_parsed( + {"tmp_var": formatter_validator.parsed}, + unresolvable=unresolved_variables, + update=True, + )["tmp_var"].native + ) - return formatter_validator.format_string + if maybe_resolved := parsed.maybe_resolvable: + return formatter_validator.post_process(maybe_resolved) + + return ScriptUtils.to_native_script(parsed) except RuntimeException as exc: if isinstance(exc, ScriptVariableNotResolved) and is_static_formatter: raise StringFormattingVariableNotFoundException( @@ -286,18 +303,14 @@ def _validate_formatter( "entry variables" ) from exc raise StringFormattingVariableNotFoundException(exc) from exc - except UserThrownRuntimeError as exc: - # Errors are expected for non-static formatters due to missing entry - # data. Raise otherwise. - if not is_static_formatter: - return formatter_validator.format_string - raise exc def validate_formatters( script: Script, unresolved_variables: Set[str], + unresolved_runtime_variables: Set[str], validator: Validator, + partial_resolve_formatters: bool, ) -> Dict: """ Ensure all OverridesStringFormatterValidator's only contain variables from the overrides @@ -313,7 +326,9 @@ def validate_formatters( resolved_dict[validator.leaf_name] |= validate_formatters( script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, validator=validator_value, + partial_resolve_formatters=partial_resolve_formatters, ) elif isinstance(validator, ListValidator): resolved_dict[validator.leaf_name] = [] @@ -321,7 +336,9 @@ def validate_formatters( list_output = validate_formatters( script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, validator=list_value, + partial_resolve_formatters=partial_resolve_formatters, ) assert len(list_output) == 1 resolved_dict[validator.leaf_name].append(list(list_output.values())[0]) @@ -329,7 +346,9 @@ def validate_formatters( resolved_dict[validator.leaf_name] = _validate_formatter( mock_script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, formatter_validator=validator, + partial_resolve_entry_formatters=partial_resolve_formatters, ) elif isinstance(validator, (DictFormatterValidator, OverridesDictFormatterValidator)): resolved_dict[validator.leaf_name] = {} @@ -337,7 +356,9 @@ def validate_formatters( resolved_dict[validator.leaf_name] |= _validate_formatter( mock_script=script, unresolved_variables=unresolved_variables, + unresolved_runtime_variables=unresolved_runtime_variables, formatter_validator=validator_value, + partial_resolve_entry_formatters=partial_resolve_formatters, ) else: resolved_dict[validator.leaf_name] = validator._value diff --git a/src/ytdl_sub/validators/validators.py b/src/ytdl_sub/validators/validators.py index 6c1356ae..462da271 100644 --- a/src/ytdl_sub/validators/validators.py +++ b/src/ytdl_sub/validators/validators.py @@ -1,13 +1,6 @@ import copy from abc import ABC -from typing import Any -from typing import Dict -from typing import Generic -from typing import List -from typing import Optional -from typing import Type -from typing import TypeVar -from typing import final +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, final from ytdl_sub.utils.exceptions import ValidationException @@ -173,7 +166,7 @@ class ListValidator(Validator, ABC, Generic[ValidatorT]): super().__init__(name, value) self._list: List[ValidatorT] = [ - self._inner_list_type(name=f"{name}.{i+1}", value=val) + self._inner_list_type(name=f"{name}.{i + 1}", value=val) for i, val in enumerate(self._value) ] diff --git a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py index 50b12f42..73dba9e0 100644 --- a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +++ b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py @@ -5,22 +5,14 @@ import time from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set +from typing import Any, Dict, List, Optional, Set from yt_dlp import DateRange from yt_dlp.utils import make_archive_id -from ytdl_sub.entries.entry import Entry -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.file_handler import FileHandler -from ytdl_sub.utils.file_handler import FileHandlerTransactionLog -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.entries.entry import Entry, ytdl_sub_split_by_chapters_parent_uid +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions +from ytdl_sub.utils.file_handler import FileHandler, FileHandlerTransactionLog, FileMetadata from ytdl_sub.utils.logger import Logger logger = Logger.get("archive") diff --git a/tests/conftest.py b/tests/conftest.py index 087ac147..dbf7378e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,19 +7,12 @@ import shutil import sys import tempfile from pathlib import Path -from typing import Any -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional -from typing import Tuple -from typing import Union +from typing import Any, Callable, Dict, List, Optional, Tuple from unittest.mock import patch import pytest from expected_download import _get_files_in_directory -from resources import copy_file_fixture -from resources import file_fixture_path +from resources import copy_file_fixture, file_fixture_path from yt_dlp.utils import sanitize_filename from ytdl_sub.cli.entrypoint import main @@ -28,8 +21,7 @@ from ytdl_sub.entries.script.custom_functions import CustomFunctions from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.subscriptions.subscription_download import SubscriptionDownload from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.logger import LoggerLevels +from ytdl_sub.utils.logger import Logger, LoggerLevels from ytdl_sub.utils.yaml import load_yaml @@ -117,12 +109,13 @@ def assert_logs( yield for call_args in patched_debug.call_args_list: - occurrences += int(expected_message in call_args.args[0]) + full_print = call_args.args[0] % call_args.args[1:] + occurrences += int(expected_message in full_print) if expected_occurrences is not None: - assert ( - occurrences == expected_occurrences - ), f"{expected_message} was expected {expected_occurrences} times, got {occurrences}" + assert occurrences == expected_occurrences, ( + f"{expected_message} was expected {expected_occurrences} times, got {occurrences}" + ) else: assert occurrences > 0, f"{expected_message} was not found in a logger.debug call" diff --git a/tests/e2e/cli/test_view.py b/tests/e2e/cli/test_view.py index 19a8e289..b918cbfb 100644 --- a/tests/e2e/cli/test_view.py +++ b/tests/e2e/cli/test_view.py @@ -15,10 +15,9 @@ class TestView: output_directory, split_chapters, ): - - args = f"view " + args = "view " args += "--split-chapters " if split_chapters else "" - args += f"https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i" + args += "https://www.youtube.com/playlist?list=PLBsm_SagFMmdWnCnrNtLjA9kzfrRkto4i" subscriptions = mock_run_from_cli(args=args) assert len(subscriptions) == 1 diff --git a/tests/e2e/plugins/test_subtitles.py b/tests/e2e/plugins/test_subtitles.py index bead12b1..110d374c 100644 --- a/tests/e2e/plugins/test_subtitles.py +++ b/tests/e2e/plugins/test_subtitles.py @@ -26,9 +26,9 @@ def single_video_subs_embed_preset_dict(output_directory): @pytest.fixture def test_single_video_subs_embed_and_file_preset_dict(single_video_subs_embed_preset_dict): - single_video_subs_embed_preset_dict["subtitles"][ - "subtitles_name" - ] = "{music_video_file_name}.{lang}.{subtitles_ext}" + single_video_subs_embed_preset_dict["subtitles"]["subtitles_name"] = ( + "{music_video_file_name}.{lang}.{subtitles_ext}" + ) return single_video_subs_embed_preset_dict diff --git a/tests/e2e/presets/test_bilateral.py b/tests/e2e/presets/test_bilateral.py index 8b83e83b..8799c2e9 100644 --- a/tests/e2e/presets/test_bilateral.py +++ b/tests/e2e/presets/test_bilateral.py @@ -53,7 +53,6 @@ def tv_show_collection_bilateral_dict(output_directory): @pytest.mark.skipif(DISABLE_YOUTUBE_TESTS, reason="YouTube tests cannot run in GH") class TestBilateral: - def test_tv_show_by_date_downloads_bilateral( self, tv_show_by_date_bilateral_dict: Dict, diff --git a/tests/e2e/youtube/test_channel.py b/tests/e2e/youtube/test_channel.py index 7ef62d09..dc73af1f 100644 --- a/tests/e2e/youtube/test_channel.py +++ b/tests/e2e/youtube/test_channel.py @@ -1,5 +1,4 @@ -from typing import Callable -from typing import Dict +from typing import Callable, Dict import pytest from expected_download import assert_expected_downloads diff --git a/tests/expected_download.py b/tests/expected_download.py index bdb97e14..9df67923 100644 --- a/tests/expected_download.py +++ b/tests/expected_download.py @@ -2,11 +2,9 @@ import json import os.path from dataclasses import dataclass from pathlib import Path -from typing import List -from typing import Optional +from typing import List, Optional -from resources import REGENERATE_FIXTURES -from resources import RESOURCE_PATH +from resources import REGENERATE_FIXTURES, RESOURCE_PATH from ytdl_sub.utils.file_handler import get_file_md5_hash from ytdl_sub.utils.system import IS_WINDOWS @@ -131,9 +129,9 @@ def assert_expected_downloads( ): if dry_run: output_directory_contents = list(Path(output_directory).rglob("*")) - assert ( - len(output_directory_contents) == 0 - ), f"Expected output directory to be empty after a dry-run, but found {output_directory_contents}" + assert len(output_directory_contents) == 0, ( + f"Expected output directory to be empty after a dry-run, but found {output_directory_contents}" + ) return summary_full_path = _EXPECTED_DOWNLOADS_SUMMARY_PATH / expected_download_summary_file_name diff --git a/tests/expected_transaction_log.py b/tests/expected_transaction_log.py index 3eddf176..05dc9dac 100644 --- a/tests/expected_transaction_log.py +++ b/tests/expected_transaction_log.py @@ -2,8 +2,7 @@ import os from pathlib import Path from typing import List -from resources import REGENERATE_FIXTURES -from resources import RESOURCE_PATH +from resources import REGENERATE_FIXTURES, RESOURCE_PATH from ytdl_sub.utils.file_handler import FileHandlerTransactionLog @@ -48,9 +47,9 @@ def assert_transaction_log_matches( # Split, ensure there are the same number of new lines summary_lines: List[str] = summary.split("\n") expected_summary_lines: List[str] = expected_summary.split("\n") - assert len(summary_lines) == len( - expected_summary_lines - ), f"Summary number of lines differ: {len(summary_lines) != len(expected_summary_lines)}" + assert len(summary_lines) == len(expected_summary_lines), ( + f"Summary number of lines differ: {len(summary_lines) != len(expected_summary_lines)}" + ) # Ensure each line equals for idx in range(len(summary_lines)): diff --git a/tests/integration/cli/conftest.py b/tests/integration/cli/conftest.py index b8eb4b29..4f60dbb7 100644 --- a/tests/integration/cli/conftest.py +++ b/tests/integration/cli/conftest.py @@ -1,8 +1,6 @@ -import datetime import os import shutil import tempfile -import time from typing import Callable from unittest.mock import patch @@ -11,8 +9,7 @@ import pytest from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription -from ytdl_sub.utils.file_handler import FileHandlerTransactionLog -from ytdl_sub.utils.file_handler import FileMetadata +from ytdl_sub.utils.file_handler import FileHandlerTransactionLog, FileMetadata from ytdl_sub.utils.logger import Logger diff --git a/tests/integration/cli/test_dl.py b/tests/integration/cli/test_dl.py index 013ad4a6..36b8064f 100644 --- a/tests/integration/cli/test_dl.py +++ b/tests/integration/cli/test_dl.py @@ -2,8 +2,7 @@ from typing import Dict from unittest.mock import patch import pytest -from conftest import mock_run_from_cli -from conftest import preset_dict_to_dl_args +from conftest import mock_run_from_cli, preset_dict_to_dl_args from expected_download import assert_expected_downloads from expected_transaction_log import assert_transaction_log_matches diff --git a/tests/integration/cli/test_entrypoint.py b/tests/integration/cli/test_entrypoint.py index 084df414..c989b87e 100644 --- a/tests/integration/cli/test_entrypoint.py +++ b/tests/integration/cli/test_entrypoint.py @@ -1,14 +1,13 @@ import re import sys from pathlib import Path -from typing import Callable -from typing import List +from typing import Callable, List from unittest.mock import patch import pytest -from ytdl_sub.cli.entrypoint import _download_subscriptions_from_yaml_files -from ytdl_sub.cli.entrypoint import main +from ytdl_sub.cli.entrypoint import _download_subscriptions_from_yaml_files, main +from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ExperimentalFeatureNotEnabled @@ -59,6 +58,7 @@ def test_subscription_logs_write_to_file( subscription_override_dict={}, update_with_info_json=False, dry_run=dry_run, + shuffle=False, ) except ValueError: assert not mock_success_output @@ -120,3 +120,39 @@ def test_update_with_info_json_requires_experimental_flag( pytest.raises(ExperimentalFeatureNotEnabled), ): _ = main() + + +def test_subscription_shuffle( + default_config: ConfigFile, + mock_subscription_download_factory: Callable, + music_video_subscription_path: Path, +): + subscription_paths = [str(music_video_subscription_path)] + + with ( + patch.object( + Subscription, + "download", + new=mock_subscription_download_factory(mock_success_output=True), + ), + ): + out1 = _download_subscriptions_from_yaml_files( + config=default_config, + subscription_paths=subscription_paths, + subscription_matches=[], + subscription_override_dict={}, + update_with_info_json=False, + dry_run=True, + shuffle=True, + ) + out2 = _download_subscriptions_from_yaml_files( + config=default_config, + subscription_paths=subscription_paths, + subscription_matches=[], + subscription_override_dict={}, + update_with_info_json=False, + dry_run=True, + shuffle=True, + ) + + assert [sub.name for sub in out1] != [sub.name for sub in out2] diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b0aab494..335d3706 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,11 +1,7 @@ import contextlib import os from pathlib import Path -from typing import Any -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional +from typing import Any, Callable, Dict, List, Optional from unittest.mock import patch import pytest @@ -14,8 +10,7 @@ from resources import copy_file_fixture from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader from ytdl_sub.downloaders.ytdlp import YTDLP -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions from ytdl_sub.plugins.throttle_protection import ThrottleProtectionPlugin v: VariableDefinitions = VARIABLES diff --git a/tests/integration/plugins/conftest.py b/tests/integration/plugins/conftest.py index 48ac4a3f..7188fb2a 100644 --- a/tests/integration/plugins/conftest.py +++ b/tests/integration/plugins/conftest.py @@ -1,4 +1,3 @@ -import tempfile from unittest.mock import patch import pytest diff --git a/tests/integration/plugins/test_file_convert.py b/tests/integration/plugins/test_file_convert.py index 06980b22..afeede56 100644 --- a/tests/integration/plugins/test_file_convert.py +++ b/tests/integration/plugins/test_file_convert.py @@ -52,7 +52,7 @@ class TestFileConvert: assert_transaction_log_matches( output_directory=output_directory, transaction_log=transaction_log, - transaction_log_summary_file_name=f"plugins/file_convert/custom_ffmpeg_args.txt", + transaction_log_summary_file_name="plugins/file_convert/custom_ffmpeg_args.txt", ) assert_expected_downloads( output_directory=output_directory, diff --git a/tests/integration/plugins/test_filter.py b/tests/integration/plugins/test_filter.py index 9f9e9883..fd958b76 100644 --- a/tests/integration/plugins/test_filter.py +++ b/tests/integration/plugins/test_filter.py @@ -1,5 +1,4 @@ -from typing import Any -from typing import Dict +from typing import Any, Dict import mergedeep import pytest diff --git a/tests/integration/plugins/test_output_options.py b/tests/integration/plugins/test_output_options.py index 458416b4..4314440a 100644 --- a/tests/integration/plugins/test_output_options.py +++ b/tests/integration/plugins/test_output_options.py @@ -36,7 +36,6 @@ def output_options_subscription_dict(output_directory) -> Dict: class TestOutputOptions: - @classmethod def _ensure_subscription_migrates( cls, @@ -114,9 +113,9 @@ class TestOutputOptions: expected_download_summary_file_name="plugins/output_options/pre_migration.json", ) - output_options_subscription_dict["output_options"][ - "migrated_download_archive_name" - ] = ".ytdl-sub-{tv_show_name_sanitized}-migrated-download-archive.json" + output_options_subscription_dict["output_options"]["migrated_download_archive_name"] = ( + ".ytdl-sub-{tv_show_name_sanitized}-migrated-download-archive.json" + ) subscription = Subscription.from_dict( config=config, preset_name=subscription_name, @@ -236,24 +235,14 @@ class TestOutputOptions: ): output_options_subscription_dict["output_options"]["keep_files_date_eval"] = "nope" - subscription = Subscription.from_dict( - config=config, - preset_name=subscription_name, - preset_dict=output_options_subscription_dict, - ) - expected_error_msg = ( "Validation error in subscription_test.output_options.keep_files_date_eval: " "Expected a standardized date in the form of YYYY-MM-DD, but received 'nope'" ) - with ( - mock_download_collection_entries( - is_youtube_channel=False, - num_urls=1, - is_extracted_audio=False, - is_dry_run=True, - ), - pytest.raises(ValidationException, match=re.escape(expected_error_msg)), - ): - subscription.download(dry_run=True) + with pytest.raises(ValidationException, match=re.escape(expected_error_msg)): + _ = Subscription.from_dict( + config=config, + preset_name=subscription_name, + preset_dict=output_options_subscription_dict, + ) diff --git a/tests/integration/plugins/test_throttle_protection.py b/tests/integration/plugins/test_throttle_protection.py index 6154f70a..bd56f652 100644 --- a/tests/integration/plugins/test_throttle_protection.py +++ b/tests/integration/plugins/test_throttle_protection.py @@ -71,7 +71,7 @@ class TestThrottleProtectionPlugin: ), assert_logs( logger=throttle_protection_logger, - expected_message="Sleeping between subscriptions for %0.2f seconds", + expected_message="Sleeping between subscriptions for 0.02 seconds", log_level="info", expected_occurrences=1, ), @@ -139,7 +139,7 @@ class TestThrottleProtectionPlugin: ), assert_logs( logger=throttle_protection_logger, - expected_message="Reached subscription max downloads of %d", + expected_message="Reached subscription max downloads of 0 for throttle protection", log_level="info", expected_occurrences=1, ), diff --git a/tests/integration/prebuilt_presets/test_filter_duration.py b/tests/integration/prebuilt_presets/test_filter_duration.py index 7faf123f..0529a2e3 100644 --- a/tests/integration/prebuilt_presets/test_filter_duration.py +++ b/tests/integration/prebuilt_presets/test_filter_duration.py @@ -17,7 +17,6 @@ def filter_subscription_dict(output_directory): class TestFilterKeywords: - def test_no_overrides( self, config, @@ -40,7 +39,7 @@ class TestFilterKeywords: assert_transaction_log_matches( output_directory=output_directory, transaction_log=transaction_log, - transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_keywords_empty.txt", + transaction_log_summary_file_name="integration/prebuilt_presets/filter_keywords_empty.txt", ) @pytest.mark.parametrize( @@ -81,7 +80,7 @@ class TestFilterKeywords: assert_transaction_log_matches( output_directory=output_directory, transaction_log=transaction_log, - transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_duration.txt", + transaction_log_summary_file_name="integration/prebuilt_presets/filter_duration.txt", ) @pytest.mark.parametrize( @@ -106,6 +105,6 @@ class TestFilterKeywords: with ( mock_download_collection_entries(is_youtube_channel=False, num_urls=1, is_dry_run=True), - pytest.raises(UserThrownRuntimeError, match=f"filter_duration args must be numeric"), + pytest.raises(UserThrownRuntimeError, match="filter_duration args must be numeric"), ): _ = subscription.download(dry_run=True) diff --git a/tests/integration/prebuilt_presets/test_filter_keywords.py b/tests/integration/prebuilt_presets/test_filter_keywords.py index 733f27b6..947c7d9e 100644 --- a/tests/integration/prebuilt_presets/test_filter_keywords.py +++ b/tests/integration/prebuilt_presets/test_filter_keywords.py @@ -17,7 +17,6 @@ def filter_subscription_dict(output_directory): class TestFilterKeywords: - def test_no_overrides( self, config, @@ -40,7 +39,7 @@ class TestFilterKeywords: assert_transaction_log_matches( output_directory=output_directory, transaction_log=transaction_log, - transaction_log_summary_file_name=f"integration/prebuilt_presets/filter_keywords_empty.txt", + transaction_log_summary_file_name="integration/prebuilt_presets/filter_keywords_empty.txt", ) @pytest.mark.parametrize("filter_mode", ["include", "exclude"]) diff --git a/tests/integration/prebuilt_presets/test_music.py b/tests/integration/prebuilt_presets/test_music.py index 6ef0ec0e..d4465b24 100644 --- a/tests/integration/prebuilt_presets/test_music.py +++ b/tests/integration/prebuilt_presets/test_music.py @@ -10,7 +10,6 @@ from ytdl_sub.subscriptions.subscription import Subscription @pytest.mark.usefixtures(mock_chapters_class.__name__) @pytest.mark.parametrize("music_preset", MusicPresets.preset_names) class TestPrebuiltMusicPresets: - def test_presets_run( self, config, diff --git a/tests/integration/prebuilt_presets/test_music_videos.py b/tests/integration/prebuilt_presets/test_music_videos.py index e4e2399d..526a2a22 100644 --- a/tests/integration/prebuilt_presets/test_music_videos.py +++ b/tests/integration/prebuilt_presets/test_music_videos.py @@ -57,7 +57,6 @@ class TestPrebuiltMusicVideoPresets: @pytest.mark.parametrize("music_video_preset", MusicVideoPresets.preset_names) @pytest.mark.parametrize("multi_url", [True, False]) class TestPrebuiltMusicVideoPresetsWithCategories: - def _preset_dict( self, output_directory: Path, diff --git a/tests/integration/prebuilt_presets/test_tv_show_by_date.py b/tests/integration/prebuilt_presets/test_tv_show_by_date.py index e886e228..0909ad5b 100644 --- a/tests/integration/prebuilt_presets/test_tv_show_by_date.py +++ b/tests/integration/prebuilt_presets/test_tv_show_by_date.py @@ -28,7 +28,6 @@ VALID_ORDERING_COMBOS = [ class TestPrebuiltTVShowPresets: - @staticmethod def run( config: ConfigFile, @@ -40,7 +39,6 @@ class TestPrebuiltTVShowPresets: is_youtube_channel: bool = True, is_many_urls: bool = False, ): - expected_summary_name = "integration/by-date/{}/{}/{}/is_yt_{}{}".format( tv_show_preset.split(" ")[0], season_ordering, @@ -136,7 +134,6 @@ class TestPrebuiltTVShowPresets: mock_download_collection_entries, tv_show_preset: str, ): - with mock_download_collection_entries(is_youtube_channel=True): self.run( config=config, @@ -160,7 +157,6 @@ class TestPrebuiltTVShowPresets: season_ordering: str, episode_ordering: str, ): - with mock_download_collection_entries( is_youtube_channel=True, ): diff --git a/tests/integration/prebuilt_presets/test_tv_show_collection.py b/tests/integration/prebuilt_presets/test_tv_show_collection.py index 2061efd1..05db4981 100644 --- a/tests/integration/prebuilt_presets/test_tv_show_collection.py +++ b/tests/integration/prebuilt_presets/test_tv_show_collection.py @@ -1,6 +1,5 @@ import re -from typing import Dict -from typing import List +from typing import Dict, List import pytest from expected_download import assert_expected_downloads @@ -15,7 +14,6 @@ DEFAULT_EPISODE_ORDERING = "upload-year-month-day" class TestPrebuiltTvShowCollectionPresets: - @staticmethod def run( config: ConfigFile, @@ -124,7 +122,6 @@ class TestPrebuiltTvShowCollectionPresets: media_player_preset: str, season_indices: List[int], ): - with mock_download_collection_entries( is_youtube_channel=True, num_urls=len(season_indices) ): @@ -158,7 +155,6 @@ class TestPrebuiltTvShowCollectionPresets: episode_ordering: str, season_indices: List[int], ): - with mock_download_collection_entries( is_youtube_channel=True, num_urls=len(season_indices) ): diff --git a/tests/integration/prebuilt_presets/test_url.py b/tests/integration/prebuilt_presets/test_url.py index 0da8d4b5..2391bba2 100644 --- a/tests/integration/prebuilt_presets/test_url.py +++ b/tests/integration/prebuilt_presets/test_url.py @@ -1,12 +1,9 @@ import pytest -from conftest import assert_logs from ytdl_sub.downloaders.url.downloader import MultiUrlDownloader from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder -from ytdl_sub.downloaders.ytdlp import YTDLP from ytdl_sub.entries.entry import Entry -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions from ytdl_sub.subscriptions.subscription import Subscription v: VariableDefinitions = VARIABLES @@ -29,7 +26,6 @@ def subscription_dict(output_directory): class TestUrl: - def test_modified_url_when_downloading( self, config, diff --git a/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json b/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json index 642ea323..13bbe1fa 100644 --- a/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json +++ b/tests/resources/expected_downloads_summaries/plugins/file_convert/custom_ffmpeg_args.json @@ -2,18 +2,18 @@ ".ytdl-sub-subscription_test-download-archive.json": "19cf39d57914ba9cbd1e57ba6f1e0683", "JMC/Mock Entry 20-1.info.json": "INFO_JSON", "JMC/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-1.mkv": "eb9a8ff61701ab673c2f06147fe29a4e", + "JMC/Mock Entry 20-1.mkv": "cbc8c05ea5cf4deefd735b93af9c0259", "JMC/Mock Entry 20-1.nfo": "fefcf0b3e4f4ff80ad636584d50dadec", "JMC/Mock Entry 20-2.info.json": "INFO_JSON", "JMC/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-2.mkv": "65d047562c61a068e8a92bd8df8801fa", + "JMC/Mock Entry 20-2.mkv": "85cc274346abc0c71ab7702b0d61abde", "JMC/Mock Entry 20-2.nfo": "025c0b631da5ff5470382b38fce78d2d", "JMC/Mock Entry 20-3.info.json": "INFO_JSON", "JMC/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 20-3.mkv": "98582526fcb3bea10ab7b3df7a5bc65d", + "JMC/Mock Entry 20-3.mkv": "c5a9ce268475c3b8fa9d4d0e3c0c25ae", "JMC/Mock Entry 20-3.nfo": "618b0ff948d9de2e10cf1da8c0dd6615", "JMC/Mock Entry 21-1.info.json": "INFO_JSON", "JMC/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7", - "JMC/Mock Entry 21-1.mkv": "3fe368f6d4bd9662a24abca9215b4d26", + "JMC/Mock Entry 21-1.mkv": "ebdd8026e72625c56dd2aa8a2e19e814", "JMC/Mock Entry 21-1.nfo": "e5c715749efc1603a6e2f59244d87aba" } \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_full_overrides.json b/tests/resources/expected_json/music/inspect_full_overrides.json deleted file mode 100644 index ac17630f..00000000 --- a/tests/resources/expected_json/music/inspect_full_overrides.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "album_cover_path": "Lester Young/{ %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }/folder.jpg", - "album_dir": "[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "artist_dir": "Lester Young", - "avatar_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name": "", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "include_sibling_metadata": "{ %bool(True) }", - "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "music_directory": "tv_show_directory_path", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_readable": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }x{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "subscription_array": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }", - "subscription_indent_1": "Jazz", - "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "track_album": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "track_album_artist": "Lester Young", - "track_artist": "Lester Young", - "track_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "track_file_name": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }", - "track_full_path": "Lester Young/{ %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }/{ %concat( %string( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ), \" - \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), \".\", %string( ext ) ) }", - "track_genre": "Jazz", - "track_genre_default": "Unset", - "track_number": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "track_number_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", - "track_original_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "track_title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "track_total": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "track_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", - "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_full_variables.json b/tests/resources/expected_json/music/inspect_full_variables.json deleted file mode 100644 index ca5513bf..00000000 --- a/tests/resources/expected_json/music/inspect_full_variables.json +++ /dev/null @@ -1,482 +0,0 @@ -{ - "album_cover_path": "Lester Young/{ %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }/folder.jpg", - "album_cover_path_sanitized": "{ %sanitize( %concat( \"Lester Young\", \"/\", %string( %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ), \"/folder.\", \"jpg\" ) ) }", - "album_dir": "[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "album_dir_sanitized": "{ %sanitize( %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ) }", - "artist_dir": "Lester Young", - "artist_dir_sanitized": "Lester Young", - "avatar_uncropped_thumbnail_file_name": "", - "avatar_uncropped_thumbnail_file_name_sanitized": "", - "banner_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name_sanitized": "", - "channel": "{ %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "channel_id": "{ %map_get_non_empty( entry_metadata, \"channel_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "channel_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"channel_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "channel_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "chapters": "{ [ ] }", - "chapters_sanitized": "[]", - "comments": "{ [ ] }", - "comments_sanitized": "[]", - "creator": "{ %map_get_non_empty( entry_metadata, \"creator\", %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "creator_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"creator\", %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "description": "{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "description_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"description\", '' ) ) }", - "download_index": "{ %int(1) }", - "download_index_padded6": "000001", - "download_index_padded6_sanitized": "000001", - "download_index_sanitized": "1", - "duration": "{ %map_get_non_empty( entry_metadata, \"duration\", 0 ) }", - "duration_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"duration\", 0 ) ) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_resolution_assert_sanitized": "true", - "enable_throttle_protection": "{ %bool(True) }", - "enable_throttle_protection_sanitized": "true", - "entry_metadata": "{ { } }", - "entry_metadata_sanitized": "{ %sanitize( entry_metadata ) }", - "epoch": "{ %map_get( entry_metadata, \"epoch\" ) }", - "epoch_date": "{ %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) }", - "epoch_date_sanitized": "{ %sanitize( %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) }", - "epoch_hour": "{ %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%H\" ) }", - "epoch_hour_sanitized": "{ %sanitize( %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%H\" ) ) }", - "epoch_sanitized": "{ %sanitize( %map_get( entry_metadata, \"epoch\" ) ) }", - "ext": "{ %throw( \"Plugin variable ext has not been created yet\" ) }", - "ext_sanitized": "{ %sanitize( ext ) }", - "extractor": "{ %map_get( entry_metadata, \"extractor\" ) }", - "extractor_key": "{ %map_get( entry_metadata, \"extractor_key\" ) }", - "extractor_key_sanitized": "{ %sanitize( %map_get( entry_metadata, \"extractor_key\" ) ) }", - "extractor_sanitized": "{ %sanitize( %map_get( entry_metadata, \"extractor\" ) ) }", - "height": "{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "height_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"height\", 0 ) ) }", - "ie_key": "{ %map_get_non_empty( entry_metadata, \"ie_key\", %map_get( entry_metadata, \"extractor_key\" ) ) }", - "ie_key_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"ie_key\", %map_get( entry_metadata, \"extractor_key\" ) ) ) }", - "include_sibling_metadata": "{ %bool(True) }", - "include_sibling_metadata_sanitized": "true", - "info_json_ext": "info.json", - "info_json_ext_sanitized": "info.json", - "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "modified_webpage_url_sanitized": "{ %sanitize( %map_get( entry_metadata, \"webpage_url\" ) ) }", - "music_directory": "tv_show_directory_path", - "music_directory_sanitized": "tv_show_directory_path", - "playlist_count": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "playlist_count_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) ) }", - "playlist_description": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) }", - "playlist_description_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) }", - "playlist_index": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "playlist_index_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", - "playlist_index_padded6": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 6 ) }", - "playlist_index_padded6_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 6 ) ) }", - "playlist_index_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ) }", - "playlist_index_reversed": "{ %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ) }", - "playlist_index_reversed_padded": "{ %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 2 ) }", - "playlist_index_reversed_padded6": "{ %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 6 ) }", - "playlist_index_reversed_padded6_sanitized": "{ %sanitize( %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 6 ) ) }", - "playlist_index_reversed_padded_sanitized": "{ %sanitize( %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 2 ) ) }", - "playlist_index_reversed_sanitized": "{ %sanitize( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ) ) }", - "playlist_index_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) ) }", - "playlist_max_upload_date": "{ %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) }", - "playlist_max_upload_date_sanitized": "{ %sanitize( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ) }", - "playlist_max_upload_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", - "playlist_max_upload_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ) }", - "playlist_max_upload_year_truncated": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year_truncated\" ) ) }", - "playlist_max_upload_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year_truncated\" ) ) ) }", - "playlist_metadata": "{ %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) }", - "playlist_metadata_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) ) }", - "playlist_title": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uid": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) }", - "playlist_uid_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_uploader": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uploader_id": "{ %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uploader_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "playlist_uploader_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "playlist_uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "playlist_webpage_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "playlist_webpage_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "release_date": "{ %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) }", - "release_date_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ) }", - "release_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"date_standardized\" ) ) }", - "release_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"date_standardized\" ) ) ) }", - "release_day": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day\" ) ) }", - "release_day_of_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year\" ) ) }", - "release_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_padded\" ) ) }", - "release_day_of_year_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_padded\" ) ) ) }", - "release_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed\" ) ) }", - "release_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed_padded\" ) ) }", - "release_day_of_year_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed_padded\" ) ) ) }", - "release_day_of_year_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed\" ) ) ) }", - "release_day_of_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year\" ) ) ) }", - "release_day_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_padded\" ) ) }", - "release_day_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_padded\" ) ) ) }", - "release_day_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed\" ) ) }", - "release_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed_padded\" ) ) }", - "release_day_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed_padded\" ) ) ) }", - "release_day_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed\" ) ) ) }", - "release_day_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day\" ) ) ) }", - "release_month": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month\" ) ) }", - "release_month_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_padded\" ) ) }", - "release_month_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_padded\" ) ) ) }", - "release_month_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed\" ) ) }", - "release_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed_padded\" ) ) }", - "release_month_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed_padded\" ) ) ) }", - "release_month_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed\" ) ) ) }", - "release_month_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month\" ) ) ) }", - "release_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year\" ) ) }", - "release_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year\" ) ) ) }", - "release_year_truncated": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated\" ) ) }", - "release_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated_reversed\" ) ) }", - "release_year_truncated_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated_reversed\" ) ) ) }", - "release_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated\" ) ) ) }", - "requested_subtitles": "{ { } }", - "requested_subtitles_sanitized": "{}", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_height_gte_sanitized": "361", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_ignore_titles_sanitized": "[]", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) }", - "resolution_assert_is_ignored_sanitized": "{ %sanitize( %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_assert_print_sanitized": "true", - "resolution_assert_sanitized": "{ %sanitize( %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) ) }", - "resolution_readable": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }x{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "resolution_readable_sanitized": "{ %sanitize( %concat( %string( %map_get_non_empty( entry_metadata, \"width\", 0 ) ), \"x\", %string( %map_get_non_empty( entry_metadata, \"height\", 0 ) ) ) ) }", - "sibling_metadata": "{ %map_get_non_empty( entry_metadata, \"sibling_metadata\", [ ] ) }", - "sibling_metadata_sanitized": "{ %sanitize( sibling_metadata ) }", - "source_count": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_count\", 1 ) }", - "source_count_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_count\", 1 ) ) }", - "source_description": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"description\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) }", - "source_description_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"description\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) ) }", - "source_index": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ) }", - "source_index_padded": "{ %pad_zero( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ), 2 ) }", - "source_index_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ), 2 ) ) }", - "source_index_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ) ) }", - "source_metadata": "{ %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) }", - "source_metadata_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) ) }", - "source_title": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"title\", %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_title_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"title\", %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uid": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"id\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "source_uid_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"id\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_uploader": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uploader_id": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_id\", %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_id\", %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uploader_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "source_uploader_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) }", - "source_uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) ) }", - "source_webpage_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "source_webpage_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) }", - "sponsorblock_chapters": "{ [ ] }", - "sponsorblock_chapters_sanitized": "[]", - "subscription_array": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }", - "subscription_array_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists\uff02]", - "subscription_has_download_archive": "{ %bool(False) }", - "subscription_has_download_archive_sanitized": "false", - "subscription_indent_1": "Jazz", - "subscription_indent_1_sanitized": "Jazz", - "subscription_name": "Lester Young", - "subscription_name_sanitized": "Lester Young", - "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "subscription_value_1_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists", - "subscription_value_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists", - "thumbnail_ext": "jpg", - "thumbnail_ext_sanitized": "jpg", - "title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "title_sanitized_plex": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "title_sanitized_plex_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "track_album": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "track_album_artist": "Lester Young", - "track_album_artist_sanitized": "Lester Young", - "track_album_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "track_artist": "Lester Young", - "track_artist_sanitized": "Lester Young", - "track_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "track_date_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "track_file_name": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }", - "track_file_name_sanitized": "{ %sanitize( %concat( %string( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ), \" - \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), \".\", %string( ext ) ) ) }", - "track_full_path": "Lester Young/{ %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }/{ %concat( %string( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ), \" - \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), \".\", %string( ext ) ) }", - "track_full_path_sanitized": "{ %sanitize( %concat( \"Lester Young\", \"/\", %string( %concat( \"[\", %string( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ), \"] \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ), \"/\", %string( %concat( %string( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ), \" - \", %string( %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), \".\", %string( ext ) ) ) ) ) }", - "track_genre": "Jazz", - "track_genre_default": "Unset", - "track_genre_default_sanitized": "Unset", - "track_genre_sanitized": "Jazz", - "track_number": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "track_number_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", - "track_number_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ) }", - "track_number_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) ) }", - "track_original_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "track_original_date_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "track_title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "track_title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "track_total": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "track_total_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) ) }", - "track_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", - "track_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ) }", - "uid": "{ %map_get( entry_metadata, \"id\" ) }", - "uid_sanitized": "{ %sanitize( %map_get( entry_metadata, \"id\" ) ) }", - "uid_sanitized_plex": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "uid_sanitized_plex_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) ) }", - "upload_date": "{ %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) }", - "upload_date_index": "{ %int(1) }", - "upload_date_index_padded": "01", - "upload_date_index_padded_sanitized": "01", - "upload_date_index_reversed": "{ %int(99) }", - "upload_date_index_reversed_padded": "99", - "upload_date_index_reversed_padded_sanitized": "99", - "upload_date_index_reversed_sanitized": "99", - "upload_date_index_sanitized": "1", - "upload_date_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) }", - "upload_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "upload_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "upload_day": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day\" ) ) }", - "upload_day_of_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year\" ) ) }", - "upload_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_padded\" ) ) }", - "upload_day_of_year_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_padded\" ) ) ) }", - "upload_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed\" ) ) }", - "upload_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed_padded\" ) ) }", - "upload_day_of_year_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed_padded\" ) ) ) }", - "upload_day_of_year_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed\" ) ) ) }", - "upload_day_of_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year\" ) ) ) }", - "upload_day_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ) }", - "upload_day_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ) ) }", - "upload_day_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed\" ) ) }", - "upload_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed_padded\" ) ) }", - "upload_day_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed_padded\" ) ) ) }", - "upload_day_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed\" ) ) ) }", - "upload_day_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day\" ) ) ) }", - "upload_month": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }", - "upload_month_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_padded\" ) ) }", - "upload_month_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_padded\" ) ) ) }", - "upload_month_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed\" ) ) }", - "upload_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed_padded\" ) ) }", - "upload_month_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed_padded\" ) ) ) }", - "upload_month_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed\" ) ) ) }", - "upload_month_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) ) }", - "upload_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "upload_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) }", - "upload_year_truncated": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated\" ) ) }", - "upload_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated_reversed\" ) ) }", - "upload_year_truncated_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated_reversed\" ) ) ) }", - "upload_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated\" ) ) ) }", - "uploader": "{ %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "uploader_id": "{ %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) }", - "uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "uploader_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "uploader_url": "{ %map_get_non_empty( entry_metadata, \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "url10": "", - "url100": "", - "url100_sanitized": "", - "url10_sanitized": "", - "url11": "", - "url11_sanitized": "", - "url12": "", - "url12_sanitized": "", - "url13": "", - "url13_sanitized": "", - "url14": "", - "url14_sanitized": "", - "url15": "", - "url15_sanitized": "", - "url16": "", - "url16_sanitized": "", - "url17": "", - "url17_sanitized": "", - "url18": "", - "url18_sanitized": "", - "url19": "", - "url19_sanitized": "", - "url2": "", - "url20": "", - "url20_sanitized": "", - "url21": "", - "url21_sanitized": "", - "url22": "", - "url22_sanitized": "", - "url23": "", - "url23_sanitized": "", - "url24": "", - "url24_sanitized": "", - "url25": "", - "url25_sanitized": "", - "url26": "", - "url26_sanitized": "", - "url27": "", - "url27_sanitized": "", - "url28": "", - "url28_sanitized": "", - "url29": "", - "url29_sanitized": "", - "url2_sanitized": "", - "url3": "", - "url30": "", - "url30_sanitized": "", - "url31": "", - "url31_sanitized": "", - "url32": "", - "url32_sanitized": "", - "url33": "", - "url33_sanitized": "", - "url34": "", - "url34_sanitized": "", - "url35": "", - "url35_sanitized": "", - "url36": "", - "url36_sanitized": "", - "url37": "", - "url37_sanitized": "", - "url38": "", - "url38_sanitized": "", - "url39": "", - "url39_sanitized": "", - "url3_sanitized": "", - "url4": "", - "url40": "", - "url40_sanitized": "", - "url41": "", - "url41_sanitized": "", - "url42": "", - "url42_sanitized": "", - "url43": "", - "url43_sanitized": "", - "url44": "", - "url44_sanitized": "", - "url45": "", - "url45_sanitized": "", - "url46": "", - "url46_sanitized": "", - "url47": "", - "url47_sanitized": "", - "url48": "", - "url48_sanitized": "", - "url49": "", - "url49_sanitized": "", - "url4_sanitized": "", - "url5": "", - "url50": "", - "url50_sanitized": "", - "url51": "", - "url51_sanitized": "", - "url52": "", - "url52_sanitized": "", - "url53": "", - "url53_sanitized": "", - "url54": "", - "url54_sanitized": "", - "url55": "", - "url55_sanitized": "", - "url56": "", - "url56_sanitized": "", - "url57": "", - "url57_sanitized": "", - "url58": "", - "url58_sanitized": "", - "url59": "", - "url59_sanitized": "", - "url5_sanitized": "", - "url6": "", - "url60": "", - "url60_sanitized": "", - "url61": "", - "url61_sanitized": "", - "url62": "", - "url62_sanitized": "", - "url63": "", - "url63_sanitized": "", - "url64": "", - "url64_sanitized": "", - "url65": "", - "url65_sanitized": "", - "url66": "", - "url66_sanitized": "", - "url67": "", - "url67_sanitized": "", - "url68": "", - "url68_sanitized": "", - "url69": "", - "url69_sanitized": "", - "url6_sanitized": "", - "url7": "", - "url70": "", - "url70_sanitized": "", - "url71": "", - "url71_sanitized": "", - "url72": "", - "url72_sanitized": "", - "url73": "", - "url73_sanitized": "", - "url74": "", - "url74_sanitized": "", - "url75": "", - "url75_sanitized": "", - "url76": "", - "url76_sanitized": "", - "url77": "", - "url77_sanitized": "", - "url78": "", - "url78_sanitized": "", - "url79": "", - "url79_sanitized": "", - "url7_sanitized": "", - "url8": "", - "url80": "", - "url80_sanitized": "", - "url81": "", - "url81_sanitized": "", - "url82": "", - "url82_sanitized": "", - "url83": "", - "url83_sanitized": "", - "url84": "", - "url84_sanitized": "", - "url85": "", - "url85_sanitized": "", - "url86": "", - "url86_sanitized": "", - "url87": "", - "url87_sanitized": "", - "url88": "", - "url88_sanitized": "", - "url89": "", - "url89_sanitized": "", - "url8_sanitized": "", - "url9": "", - "url90": "", - "url90_sanitized": "", - "url91": "", - "url91_sanitized": "", - "url92": "", - "url92_sanitized": "", - "url93": "", - "url93_sanitized": "", - "url94": "", - "url94_sanitized": "", - "url95": "", - "url95_sanitized": "", - "url96": "", - "url96_sanitized": "", - "url97": "", - "url97_sanitized": "", - "url98": "", - "url98_sanitized": "", - "url99": "", - "url99_sanitized": "", - "url9_sanitized": "", - "url_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists", - "urls": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }", - "urls_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists\uff02]", - "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "webpage_url_sanitized": "{ %sanitize( %map_get( entry_metadata, \"webpage_url\" ) ) }", - "width": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }", - "width_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"width\", 0 ) ) }", - "ytdl_sub_entry_date_eval": "{ %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "ytdl_sub_entry_date_eval_sanitized": "{ %sanitize( %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) ) }", - "ytdl_sub_input_url": "", - "ytdl_sub_input_url_count": "{ %int(0) }", - "ytdl_sub_input_url_count_sanitized": "0", - "ytdl_sub_input_url_index": "{ %int(-1) }", - "ytdl_sub_input_url_index_sanitized": "-1", - "ytdl_sub_input_url_sanitized": "" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_overrides.json b/tests/resources/expected_json/music/inspect_overrides.json deleted file mode 100644 index 19490f00..00000000 --- a/tests/resources/expected_json/music/inspect_overrides.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "album_cover_path": "Lester Young/{ %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) }/folder.{ thumbnail_ext }", - "album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }", - "artist_dir": "Lester Young", - "avatar_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name": "", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "include_sibling_metadata": "{ %bool(True) }", - "modified_webpage_url": "{ webpage_url }", - "music_directory": "tv_show_directory_path", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_readable": "{ width }x{ height }", - "subscription_array": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }", - "subscription_indent_1": "Jazz", - "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "track_album": "{ playlist_title }", - "track_album_artist": "Lester Young", - "track_artist": "Lester Young", - "track_date": "{ upload_date_standardized }", - "track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }", - "track_full_path": "Lester Young/{ %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) }/{ %concat( %string( playlist_index_padded ), \" - \", %string( %sanitize( title ) ), \".\", %string( ext ) ) }", - "track_genre": "Jazz", - "track_genre_default": "Unset", - "track_number": "{ playlist_index }", - "track_number_padded": "{ playlist_index_padded }", - "track_original_date": "{ upload_date_standardized }", - "track_title": "{ title }", - "track_total": "{ playlist_count }", - "track_year": "{ playlist_max_upload_year }", - "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_fill.json b/tests/resources/expected_json/music/inspect_sub_fill.json new file mode 100644 index 00000000..b66bfe9b --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_fill.json @@ -0,0 +1,242 @@ +{ + "audio_extract": { + "codec": "best", + "enable": true + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": true, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "variables": {}, + "webpage_url": "{ modified_webpage_url }", + "ytdl_options": {} + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": [ + "{ track_album }" + ], + "albumartist": [ + "Lester Young" + ], + "albumartists": [ + "Lester Young" + ], + "artist": [ + "Lester Young" + ], + "artists": [ + "Lester Young" + ], + "date": [ + "{ track_date }" + ], + "genres": [ + "Jazz" + ], + "original_date": [ + "{ track_original_date }" + ], + "title": [ + "{ track_title }" + ], + "track": [ + "{ track_number }" + ], + "tracktotal": [ + "{ track_total }" + ], + "year": [ + "{ track_year }" + ] + }, + "output_options": { + "download_archive_name": ".ytdl-sub-Lester Young-download-archive.json", + "file_name": "{ track_full_path }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpzald2h7x", + "preserve_mtime": false, + "thumbnail_name": "{ album_cover_path }" + }, + "overrides": { + "album_cover_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }", + "album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }", + "artist_dir": "Lester Young", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{ webpage_url }", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpzald2h7x", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{ playlist_title }", + "track_album_artist": "Lester Young", + "track_artist": "Lester Young", + "track_date": "{ upload_date_standardized }", + "track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }", + "track_full_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }", + "track_genre": "Jazz", + "track_genre_default": "Unset", + "track_number": "{ playlist_index }", + "track_number_padded": "{ playlist_index_padded }", + "track_original_date": "{ upload_date_standardized }", + "track_title": "{ title }", + "track_total": "{ playlist_count }", + "track_year": "{ playlist_max_upload_year }", + "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_internal.json b/tests/resources/expected_json/music/inspect_sub_internal.json new file mode 100644 index 00000000..2409b16d --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_internal.json @@ -0,0 +1,242 @@ +{ + "audio_extract": { + "codec": "best", + "enable": true + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": true, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "variables": {}, + "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "ytdl_options": {} + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": [ + "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }" + ], + "albumartist": [ + "Lester Young" + ], + "albumartists": [ + "Lester Young" + ], + "artist": [ + "Lester Young" + ], + "artists": [ + "Lester Young" + ], + "date": [ + "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }" + ], + "genres": [ + "Jazz" + ], + "original_date": [ + "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }" + ], + "title": [ + "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }" + ], + "track": [ + "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }" + ], + "tracktotal": [ + "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }" + ], + "year": [ + "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }" + ] + }, + "output_options": { + "download_archive_name": ".ytdl-sub-Lester Young-download-archive.json", + "file_name": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/{ %concat( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ), \" - \", %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ), \".\", ext ) }", + "keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpdu1vad67", + "preserve_mtime": false, + "thumbnail_name": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/folder.jpg" + }, + "overrides": { + "album_cover_path": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/folder.jpg", + "album_dir": "[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", + "artist_dir": "Lester Young", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpdu1vad67", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "track_album_artist": "Lester Young", + "track_artist": "Lester Young", + "track_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "track_file_name": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }", + "track_full_path": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/{ %concat( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ), \" - \", %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ), \".\", ext ) }", + "track_genre": "Jazz", + "track_genre_default": "Unset", + "track_number": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", + "track_number_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", + "track_original_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "track_title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", + "track_total": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", + "track_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", + "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_original.json b/tests/resources/expected_json/music/inspect_sub_original.json new file mode 100644 index 00000000..988e58a9 --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_original.json @@ -0,0 +1,227 @@ +{ + "audio_extract": { + "codec": "best" + }, + "download": [ + { + "include_sibling_metadata": "{include_sibling_metadata}", + "playlist_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "url": "{ %array_at(urls, 0) }", + "webpage_url": "{modified_webpage_url}" + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "url": "{ %array_slice(urls, 1) }", + "webpage_url": "{modified_webpage_url}" + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": "{track_album}", + "albumartist": "{track_album_artist}", + "albumartists": [ + "{track_album_artist}" + ], + "artist": "{track_artist}", + "artists": [ + "{track_artist}" + ], + "date": "{track_date}", + "genres": [ + "{track_genre}" + ], + "original_date": "{track_original_date}", + "title": "{track_title}", + "track": "{track_number}", + "tracktotal": "{track_total}", + "year": "{track_year}" + }, + "output_options": { + "file_name": "{track_full_path}", + "maintain_download_archive": true, + "output_directory": "{music_directory}", + "thumbnail_name": "{album_cover_path}" + }, + "overrides": { + "album_cover_path": "{artist_dir}/{album_dir}/folder.{thumbnail_ext}", + "album_dir": "[{track_year}] {track_album_sanitized}", + "artist_dir": "{track_artist_sanitized}", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{webpage_url}", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpk6coazyn", + "resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [] }", + "resolution_assert_is_ignored": "{\n %print_if_true(\n %concat(title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\"),\n %contains_any(title, resolution_assert_ignore_titles)\n )\n}", + "resolution_assert_print": "{\n %print(\n %if(\n enable_resolution_assert,\n \"Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`\",\n \"Resolution assert is disabled. Use at your own risk!\"\n ),\n enable_resolution_assert,\n -1\n )\n}", + "resolution_readable": "{width}x{height}", + "subscription_array": "{%from_json('''[\"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\"]''')}", + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{playlist_title}", + "track_album_artist": "{track_artist}", + "track_artist": "{subscription_name}", + "track_date": "{upload_date_standardized}", + "track_file_name": "{track_number_padded} - {track_title_sanitized}.{ext}", + "track_full_path": "{artist_dir}/{album_dir}/{track_file_name}", + "track_genre": "{subscription_indent_1}", + "track_genre_default": "Unset", + "track_number": "{playlist_index}", + "track_number_padded": "{playlist_index_padded}", + "track_original_date": "{track_date}", + "track_title": "{title}", + "track_total": "{playlist_count}", + "track_year": "{playlist_max_upload_year}", + "url": "{subscription_value}", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": "{subscription_array}" + }, + "preset": [ + "_music_base", + "_multi_url", + "_albums_from_playlists", + "_throttle_protection", + "YouTube Releases", + "__preset__" + ], + "throttle_protection": { + "enable": "{\n %print(\n %if(\n enable_throttle_protection,\n \"Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`\",\n \"Throttle protection is disabled. Use at your own risk!\"\n ),\n enable_throttle_protection\n )\n}", + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "ytdl_options": { + "break_on_existing": true + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_sub_resolve.json b/tests/resources/expected_json/music/inspect_sub_resolve.json new file mode 100644 index 00000000..347a61ac --- /dev/null +++ b/tests/resources/expected_json/music/inspect_sub_resolve.json @@ -0,0 +1,242 @@ +{ + "audio_extract": { + "codec": "best", + "enable": true + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": true, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "variables": {}, + "webpage_url": "{ webpage_url }", + "ytdl_options": {} + } + ], + "format": "ba[ext=webm]/ba", + "music_tags": { + "album": [ + "{ playlist_title }" + ], + "albumartist": [ + "Lester Young" + ], + "albumartists": [ + "Lester Young" + ], + "artist": [ + "Lester Young" + ], + "artists": [ + "Lester Young" + ], + "date": [ + "{ upload_date_standardized }" + ], + "genres": [ + "Jazz" + ], + "original_date": [ + "{ upload_date_standardized }" + ], + "title": [ + "{ title }" + ], + "track": [ + "{ playlist_index }" + ], + "tracktotal": [ + "{ playlist_count }" + ], + "year": [ + "{ playlist_max_upload_year }" + ] + }, + "output_options": { + "download_archive_name": ".ytdl-sub-Lester Young-download-archive.json", + "file_name": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpd5oeacb3", + "preserve_mtime": false, + "thumbnail_name": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }" + }, + "overrides": { + "album_cover_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }", + "album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }", + "artist_dir": "Lester Young", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "include_sibling_metadata": true, + "modified_webpage_url": "{ webpage_url }", + "music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpd5oeacb3", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ], + "subscription_indent_1": "Jazz", + "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "track_album": "{ playlist_title }", + "track_album_artist": "Lester Young", + "track_artist": "Lester Young", + "track_date": "{ upload_date_standardized }", + "track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }", + "track_full_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }", + "track_genre": "Jazz", + "track_genre_default": "Unset", + "track_number": "{ playlist_index }", + "track_number_padded": "{ playlist_index_padded }", + "track_original_date": "{ upload_date_standardized }", + "track_title": "{ title }", + "track_total": "{ playlist_count }", + "track_year": "{ playlist_max_upload_year }", + "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music/inspect_variables.json b/tests/resources/expected_json/music/inspect_variables.json deleted file mode 100644 index 131629d8..00000000 --- a/tests/resources/expected_json/music/inspect_variables.json +++ /dev/null @@ -1,482 +0,0 @@ -{ - "album_cover_path": "Lester Young/{ %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) }/folder.{ thumbnail_ext }", - "album_cover_path_sanitized": "{ %sanitize( %concat( \"Lester Young\", \"/\", %string( %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) ), \"/folder.\", %string( thumbnail_ext ) ) ) }", - "album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }", - "album_dir_sanitized": "{ %sanitize( %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) ) }", - "artist_dir": "Lester Young", - "artist_dir_sanitized": "Lester Young", - "avatar_uncropped_thumbnail_file_name": "", - "avatar_uncropped_thumbnail_file_name_sanitized": "", - "banner_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name_sanitized": "", - "channel": "{ %map_get_non_empty( entry_metadata, \"channel\", uploader ) }", - "channel_id": "{ %map_get_non_empty( entry_metadata, \"channel_id\", uploader_id ) }", - "channel_id_sanitized": "{ %sanitize( channel_id ) }", - "channel_sanitized": "{ %sanitize( channel ) }", - "chapters": "{ [ ] }", - "chapters_sanitized": "{ %sanitize( chapters ) }", - "comments": "{ [ ] }", - "comments_sanitized": "{ %sanitize( comments ) }", - "creator": "{ %map_get_non_empty( entry_metadata, \"creator\", channel ) }", - "creator_sanitized": "{ %sanitize( creator ) }", - "description": "{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "description_sanitized": "{ %sanitize( description ) }", - "download_index": "{ %int( 1 ) }", - "download_index_padded6": "{ %pad_zero( download_index, 6 ) }", - "download_index_padded6_sanitized": "{ %sanitize( download_index_padded6 ) }", - "download_index_sanitized": "{ %sanitize( download_index ) }", - "duration": "{ %map_get_non_empty( entry_metadata, \"duration\", 0 ) }", - "duration_sanitized": "{ %sanitize( duration ) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_resolution_assert_sanitized": "true", - "enable_throttle_protection": "{ %bool(True) }", - "enable_throttle_protection_sanitized": "true", - "entry_metadata": "{ { } }", - "entry_metadata_sanitized": "{ %sanitize( entry_metadata ) }", - "epoch": "{ %map_get( entry_metadata, \"epoch\" ) }", - "epoch_date": "{ %datetime_strftime( epoch, \"%Y%m%d\" ) }", - "epoch_date_sanitized": "{ %sanitize( epoch_date ) }", - "epoch_hour": "{ %datetime_strftime( epoch, \"%H\" ) }", - "epoch_hour_sanitized": "{ %sanitize( epoch_hour ) }", - "epoch_sanitized": "{ %sanitize( epoch ) }", - "ext": "{ %throw( \"Plugin variable ext has not been created yet\" ) }", - "ext_sanitized": "{ %sanitize( ext ) }", - "extractor": "{ %map_get( entry_metadata, \"extractor\" ) }", - "extractor_key": "{ %map_get( entry_metadata, \"extractor_key\" ) }", - "extractor_key_sanitized": "{ %sanitize( extractor_key ) }", - "extractor_sanitized": "{ %sanitize( extractor ) }", - "height": "{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "height_sanitized": "{ %sanitize( height ) }", - "ie_key": "{ %map_get_non_empty( entry_metadata, \"ie_key\", extractor_key ) }", - "ie_key_sanitized": "{ %sanitize( ie_key ) }", - "include_sibling_metadata": "{ %bool(True) }", - "include_sibling_metadata_sanitized": "true", - "info_json_ext": "info.json", - "info_json_ext_sanitized": "info.json", - "modified_webpage_url": "{ webpage_url }", - "modified_webpage_url_sanitized": "{ %sanitize( webpage_url ) }", - "music_directory": "tv_show_directory_path", - "music_directory_sanitized": "tv_show_directory_path", - "playlist_count": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "playlist_count_sanitized": "{ %sanitize( playlist_count ) }", - "playlist_description": "{ %map_get_non_empty( playlist_metadata, \"description\", description ) }", - "playlist_description_sanitized": "{ %sanitize( playlist_description ) }", - "playlist_index": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "playlist_index_padded": "{ %pad_zero( playlist_index, 2 ) }", - "playlist_index_padded6": "{ %pad_zero( playlist_index, 6 ) }", - "playlist_index_padded6_sanitized": "{ %sanitize( playlist_index_padded6 ) }", - "playlist_index_padded_sanitized": "{ %sanitize( playlist_index_padded ) }", - "playlist_index_reversed": "{ %sub( playlist_count, playlist_index, -1 ) }", - "playlist_index_reversed_padded": "{ %pad_zero( playlist_index_reversed, 2 ) }", - "playlist_index_reversed_padded6": "{ %pad_zero( playlist_index_reversed, 6 ) }", - "playlist_index_reversed_padded6_sanitized": "{ %sanitize( playlist_index_reversed_padded6 ) }", - "playlist_index_reversed_padded_sanitized": "{ %sanitize( playlist_index_reversed_padded ) }", - "playlist_index_reversed_sanitized": "{ %sanitize( playlist_index_reversed ) }", - "playlist_index_sanitized": "{ %sanitize( playlist_index ) }", - "playlist_max_upload_date": "{ %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) }", - "playlist_max_upload_date_sanitized": "{ %sanitize( playlist_max_upload_date ) }", - "playlist_max_upload_year": "{ %int( %map_get( %to_date_metadata( playlist_max_upload_date ), \"year\" ) ) }", - "playlist_max_upload_year_sanitized": "{ %sanitize( playlist_max_upload_year ) }", - "playlist_max_upload_year_truncated": "{ %int( %map_get( %to_date_metadata( playlist_max_upload_date ), \"year_truncated\" ) ) }", - "playlist_max_upload_year_truncated_sanitized": "{ %sanitize( playlist_max_upload_year_truncated ) }", - "playlist_metadata": "{ %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) }", - "playlist_metadata_sanitized": "{ %sanitize( playlist_metadata ) }", - "playlist_title": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", title ) }", - "playlist_title_sanitized": "{ %sanitize( playlist_title ) }", - "playlist_uid": "{ %map_get_non_empty( playlist_metadata, \"playlist_id\", uid ) }", - "playlist_uid_sanitized": "{ %sanitize( playlist_uid ) }", - "playlist_uploader": "{ %map_get_non_empty( playlist_metadata, \"uploader\", uploader ) }", - "playlist_uploader_id": "{ %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", uploader_id ) }", - "playlist_uploader_id_sanitized": "{ %sanitize( playlist_uploader_id ) }", - "playlist_uploader_sanitized": "{ %sanitize( playlist_uploader ) }", - "playlist_uploader_url": "{ %map_get_non_empty( playlist_metadata, \"uploader_url\", webpage_url ) }", - "playlist_uploader_url_sanitized": "{ %sanitize( playlist_uploader_url ) }", - "playlist_webpage_url": "{ %map_get_non_empty( playlist_metadata, \"webpage_url\", webpage_url ) }", - "playlist_webpage_url_sanitized": "{ %sanitize( playlist_webpage_url ) }", - "release_date": "{ %map_get_non_empty( entry_metadata, \"release_date\", upload_date ) }", - "release_date_sanitized": "{ %sanitize( release_date ) }", - "release_date_standardized": "{ %string( %map_get( %to_date_metadata( release_date ), \"date_standardized\" ) ) }", - "release_date_standardized_sanitized": "{ %sanitize( release_date_standardized ) }", - "release_day": "{ %int( %map_get( %to_date_metadata( release_date ), \"day\" ) ) }", - "release_day_of_year": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_of_year\" ) ) }", - "release_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_of_year_padded\" ) ) }", - "release_day_of_year_padded_sanitized": "{ %sanitize( release_day_of_year_padded ) }", - "release_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_of_year_reversed\" ) ) }", - "release_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_of_year_reversed_padded\" ) ) }", - "release_day_of_year_reversed_padded_sanitized": "{ %sanitize( release_day_of_year_reversed_padded ) }", - "release_day_of_year_reversed_sanitized": "{ %sanitize( release_day_of_year_reversed ) }", - "release_day_of_year_sanitized": "{ %sanitize( release_day_of_year ) }", - "release_day_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_padded\" ) ) }", - "release_day_padded_sanitized": "{ %sanitize( release_day_padded ) }", - "release_day_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_reversed\" ) ) }", - "release_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_reversed_padded\" ) ) }", - "release_day_reversed_padded_sanitized": "{ %sanitize( release_day_reversed_padded ) }", - "release_day_reversed_sanitized": "{ %sanitize( release_day_reversed ) }", - "release_day_sanitized": "{ %sanitize( release_day ) }", - "release_month": "{ %int( %map_get( %to_date_metadata( release_date ), \"month\" ) ) }", - "release_month_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"month_padded\" ) ) }", - "release_month_padded_sanitized": "{ %sanitize( release_month_padded ) }", - "release_month_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"month_reversed\" ) ) }", - "release_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"month_reversed_padded\" ) ) }", - "release_month_reversed_padded_sanitized": "{ %sanitize( release_month_reversed_padded ) }", - "release_month_reversed_sanitized": "{ %sanitize( release_month_reversed ) }", - "release_month_sanitized": "{ %sanitize( release_month ) }", - "release_year": "{ %int( %map_get( %to_date_metadata( release_date ), \"year\" ) ) }", - "release_year_sanitized": "{ %sanitize( release_year ) }", - "release_year_truncated": "{ %int( %map_get( %to_date_metadata( release_date ), \"year_truncated\" ) ) }", - "release_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"year_truncated_reversed\" ) ) }", - "release_year_truncated_reversed_sanitized": "{ %sanitize( release_year_truncated_reversed ) }", - "release_year_truncated_sanitized": "{ %sanitize( release_year_truncated ) }", - "requested_subtitles": "{ { } }", - "requested_subtitles_sanitized": "{ %sanitize( requested_subtitles ) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_height_gte_sanitized": "361", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_ignore_titles_sanitized": "[]", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) }", - "resolution_assert_is_ignored_sanitized": "{ %sanitize( %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_assert_print_sanitized": "true", - "resolution_assert_sanitized": "{ %sanitize( %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) ) }", - "resolution_readable": "{ width }x{ height }", - "resolution_readable_sanitized": "{ %sanitize( %concat( %string( width ), \"x\", %string( height ) ) ) }", - "sibling_metadata": "{ %map_get_non_empty( entry_metadata, \"sibling_metadata\", [ ] ) }", - "sibling_metadata_sanitized": "{ %sanitize( sibling_metadata ) }", - "source_count": "{ %map_get_non_empty( playlist_metadata, \"playlist_count\", 1 ) }", - "source_count_sanitized": "{ %sanitize( source_count ) }", - "source_description": "{ %map_get_non_empty( source_metadata, \"description\", playlist_description ) }", - "source_description_sanitized": "{ %sanitize( source_description ) }", - "source_index": "{ %map_get_non_empty( playlist_metadata, \"playlist_index\", 1 ) }", - "source_index_padded": "{ %pad_zero( source_index, 2 ) }", - "source_index_padded_sanitized": "{ %sanitize( source_index_padded ) }", - "source_index_sanitized": "{ %sanitize( source_index ) }", - "source_metadata": "{ %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) }", - "source_metadata_sanitized": "{ %sanitize( source_metadata ) }", - "source_title": "{ %map_get_non_empty( source_metadata, \"title\", playlist_title ) }", - "source_title_sanitized": "{ %sanitize( source_title ) }", - "source_uid": "{ %map_get_non_empty( source_metadata, \"id\", playlist_uid ) }", - "source_uid_sanitized": "{ %sanitize( source_uid ) }", - "source_uploader": "{ %map_get_non_empty( source_metadata, \"uploader\", playlist_uploader ) }", - "source_uploader_id": "{ %map_get_non_empty( source_metadata, \"uploader_id\", playlist_uploader_id ) }", - "source_uploader_id_sanitized": "{ %sanitize( source_uploader_id ) }", - "source_uploader_sanitized": "{ %sanitize( source_uploader ) }", - "source_uploader_url": "{ %map_get_non_empty( source_metadata, \"uploader_url\", source_webpage_url ) }", - "source_uploader_url_sanitized": "{ %sanitize( source_uploader_url ) }", - "source_webpage_url": "{ %map_get_non_empty( source_metadata, \"webpage_url\", playlist_webpage_url ) }", - "source_webpage_url_sanitized": "{ %sanitize( source_webpage_url ) }", - "sponsorblock_chapters": "{ [ ] }", - "sponsorblock_chapters_sanitized": "{ %sanitize( sponsorblock_chapters ) }", - "subscription_array": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }", - "subscription_array_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists\uff02]", - "subscription_has_download_archive": "{ %bool(False) }", - "subscription_has_download_archive_sanitized": "false", - "subscription_indent_1": "Jazz", - "subscription_indent_1_sanitized": "Jazz", - "subscription_name": "Lester Young", - "subscription_name_sanitized": "Lester Young", - "subscription_value": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "subscription_value_1": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "subscription_value_1_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists", - "subscription_value_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists", - "thumbnail_ext": "jpg", - "thumbnail_ext_sanitized": "jpg", - "title": "{ %map_get_non_empty( entry_metadata, \"title\", uid ) }", - "title_sanitized": "{ %sanitize( title ) }", - "title_sanitized_plex": "{ %sanitize_plex_episode( title ) }", - "title_sanitized_plex_sanitized": "{ %sanitize( title_sanitized_plex ) }", - "track_album": "{ playlist_title }", - "track_album_artist": "Lester Young", - "track_album_artist_sanitized": "Lester Young", - "track_album_sanitized": "{ %sanitize( playlist_title ) }", - "track_artist": "Lester Young", - "track_artist_sanitized": "Lester Young", - "track_date": "{ upload_date_standardized }", - "track_date_sanitized": "{ %sanitize( upload_date_standardized ) }", - "track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }", - "track_file_name_sanitized": "{ %sanitize( %concat( %string( playlist_index_padded ), \" - \", %string( %sanitize( title ) ), \".\", %string( ext ) ) ) }", - "track_full_path": "Lester Young/{ %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) }/{ %concat( %string( playlist_index_padded ), \" - \", %string( %sanitize( title ) ), \".\", %string( ext ) ) }", - "track_full_path_sanitized": "{ %sanitize( %concat( \"Lester Young\", \"/\", %string( %concat( \"[\", %string( playlist_max_upload_year ), \"] \", %string( %sanitize( playlist_title ) ) ) ), \"/\", %string( %concat( %string( playlist_index_padded ), \" - \", %string( %sanitize( title ) ), \".\", %string( ext ) ) ) ) ) }", - "track_genre": "Jazz", - "track_genre_default": "Unset", - "track_genre_default_sanitized": "Unset", - "track_genre_sanitized": "Jazz", - "track_number": "{ playlist_index }", - "track_number_padded": "{ playlist_index_padded }", - "track_number_padded_sanitized": "{ %sanitize( playlist_index_padded ) }", - "track_number_sanitized": "{ %sanitize( playlist_index ) }", - "track_original_date": "{ upload_date_standardized }", - "track_original_date_sanitized": "{ %sanitize( upload_date_standardized ) }", - "track_title": "{ title }", - "track_title_sanitized": "{ %sanitize( title ) }", - "track_total": "{ playlist_count }", - "track_total_sanitized": "{ %sanitize( playlist_count ) }", - "track_year": "{ playlist_max_upload_year }", - "track_year_sanitized": "{ %sanitize( playlist_max_upload_year ) }", - "uid": "{ %map_get( entry_metadata, \"id\" ) }", - "uid_sanitized": "{ %sanitize( uid ) }", - "uid_sanitized_plex": "{ %sanitize_plex_episode( uid ) }", - "uid_sanitized_plex_sanitized": "{ %sanitize( uid_sanitized_plex ) }", - "upload_date": "{ %map_get_non_empty( entry_metadata, \"upload_date\", epoch_date ) }", - "upload_date_index": "{ %int( 1 ) }", - "upload_date_index_padded": "{ %pad_zero( upload_date_index, 2 ) }", - "upload_date_index_padded_sanitized": "{ %sanitize( upload_date_index_padded ) }", - "upload_date_index_reversed": "{ %sub( 100, upload_date_index ) }", - "upload_date_index_reversed_padded": "{ %pad_zero( upload_date_index_reversed, 2 ) }", - "upload_date_index_reversed_padded_sanitized": "{ %sanitize( upload_date_index_reversed_padded ) }", - "upload_date_index_reversed_sanitized": "{ %sanitize( upload_date_index_reversed ) }", - "upload_date_index_sanitized": "{ %sanitize( upload_date_index ) }", - "upload_date_sanitized": "{ %sanitize( upload_date ) }", - "upload_date_standardized": "{ %string( %map_get( %to_date_metadata( upload_date ), \"date_standardized\" ) ) }", - "upload_date_standardized_sanitized": "{ %sanitize( upload_date_standardized ) }", - "upload_day": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day\" ) ) }", - "upload_day_of_year": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_of_year\" ) ) }", - "upload_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_of_year_padded\" ) ) }", - "upload_day_of_year_padded_sanitized": "{ %sanitize( upload_day_of_year_padded ) }", - "upload_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_of_year_reversed\" ) ) }", - "upload_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_of_year_reversed_padded\" ) ) }", - "upload_day_of_year_reversed_padded_sanitized": "{ %sanitize( upload_day_of_year_reversed_padded ) }", - "upload_day_of_year_reversed_sanitized": "{ %sanitize( upload_day_of_year_reversed ) }", - "upload_day_of_year_sanitized": "{ %sanitize( upload_day_of_year ) }", - "upload_day_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_padded\" ) ) }", - "upload_day_padded_sanitized": "{ %sanitize( upload_day_padded ) }", - "upload_day_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_reversed\" ) ) }", - "upload_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_reversed_padded\" ) ) }", - "upload_day_reversed_padded_sanitized": "{ %sanitize( upload_day_reversed_padded ) }", - "upload_day_reversed_sanitized": "{ %sanitize( upload_day_reversed ) }", - "upload_day_sanitized": "{ %sanitize( upload_day ) }", - "upload_month": "{ %int( %map_get( %to_date_metadata( upload_date ), \"month\" ) ) }", - "upload_month_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"month_padded\" ) ) }", - "upload_month_padded_sanitized": "{ %sanitize( upload_month_padded ) }", - "upload_month_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"month_reversed\" ) ) }", - "upload_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"month_reversed_padded\" ) ) }", - "upload_month_reversed_padded_sanitized": "{ %sanitize( upload_month_reversed_padded ) }", - "upload_month_reversed_sanitized": "{ %sanitize( upload_month_reversed ) }", - "upload_month_sanitized": "{ %sanitize( upload_month ) }", - "upload_year": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year\" ) ) }", - "upload_year_sanitized": "{ %sanitize( upload_year ) }", - "upload_year_truncated": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year_truncated\" ) ) }", - "upload_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year_truncated_reversed\" ) ) }", - "upload_year_truncated_reversed_sanitized": "{ %sanitize( upload_year_truncated_reversed ) }", - "upload_year_truncated_sanitized": "{ %sanitize( upload_year_truncated ) }", - "uploader": "{ %map_get_non_empty( entry_metadata, \"uploader\", uploader_id ) }", - "uploader_id": "{ %map_get_non_empty( entry_metadata, \"uploader_id\", uid ) }", - "uploader_id_sanitized": "{ %sanitize( uploader_id ) }", - "uploader_sanitized": "{ %sanitize( uploader ) }", - "uploader_url": "{ %map_get_non_empty( entry_metadata, \"uploader_url\", webpage_url ) }", - "uploader_url_sanitized": "{ %sanitize( uploader_url ) }", - "url": "https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists", - "url10": "", - "url100": "", - "url100_sanitized": "", - "url10_sanitized": "", - "url11": "", - "url11_sanitized": "", - "url12": "", - "url12_sanitized": "", - "url13": "", - "url13_sanitized": "", - "url14": "", - "url14_sanitized": "", - "url15": "", - "url15_sanitized": "", - "url16": "", - "url16_sanitized": "", - "url17": "", - "url17_sanitized": "", - "url18": "", - "url18_sanitized": "", - "url19": "", - "url19_sanitized": "", - "url2": "", - "url20": "", - "url20_sanitized": "", - "url21": "", - "url21_sanitized": "", - "url22": "", - "url22_sanitized": "", - "url23": "", - "url23_sanitized": "", - "url24": "", - "url24_sanitized": "", - "url25": "", - "url25_sanitized": "", - "url26": "", - "url26_sanitized": "", - "url27": "", - "url27_sanitized": "", - "url28": "", - "url28_sanitized": "", - "url29": "", - "url29_sanitized": "", - "url2_sanitized": "", - "url3": "", - "url30": "", - "url30_sanitized": "", - "url31": "", - "url31_sanitized": "", - "url32": "", - "url32_sanitized": "", - "url33": "", - "url33_sanitized": "", - "url34": "", - "url34_sanitized": "", - "url35": "", - "url35_sanitized": "", - "url36": "", - "url36_sanitized": "", - "url37": "", - "url37_sanitized": "", - "url38": "", - "url38_sanitized": "", - "url39": "", - "url39_sanitized": "", - "url3_sanitized": "", - "url4": "", - "url40": "", - "url40_sanitized": "", - "url41": "", - "url41_sanitized": "", - "url42": "", - "url42_sanitized": "", - "url43": "", - "url43_sanitized": "", - "url44": "", - "url44_sanitized": "", - "url45": "", - "url45_sanitized": "", - "url46": "", - "url46_sanitized": "", - "url47": "", - "url47_sanitized": "", - "url48": "", - "url48_sanitized": "", - "url49": "", - "url49_sanitized": "", - "url4_sanitized": "", - "url5": "", - "url50": "", - "url50_sanitized": "", - "url51": "", - "url51_sanitized": "", - "url52": "", - "url52_sanitized": "", - "url53": "", - "url53_sanitized": "", - "url54": "", - "url54_sanitized": "", - "url55": "", - "url55_sanitized": "", - "url56": "", - "url56_sanitized": "", - "url57": "", - "url57_sanitized": "", - "url58": "", - "url58_sanitized": "", - "url59": "", - "url59_sanitized": "", - "url5_sanitized": "", - "url6": "", - "url60": "", - "url60_sanitized": "", - "url61": "", - "url61_sanitized": "", - "url62": "", - "url62_sanitized": "", - "url63": "", - "url63_sanitized": "", - "url64": "", - "url64_sanitized": "", - "url65": "", - "url65_sanitized": "", - "url66": "", - "url66_sanitized": "", - "url67": "", - "url67_sanitized": "", - "url68": "", - "url68_sanitized": "", - "url69": "", - "url69_sanitized": "", - "url6_sanitized": "", - "url7": "", - "url70": "", - "url70_sanitized": "", - "url71": "", - "url71_sanitized": "", - "url72": "", - "url72_sanitized": "", - "url73": "", - "url73_sanitized": "", - "url74": "", - "url74_sanitized": "", - "url75": "", - "url75_sanitized": "", - "url76": "", - "url76_sanitized": "", - "url77": "", - "url77_sanitized": "", - "url78": "", - "url78_sanitized": "", - "url79": "", - "url79_sanitized": "", - "url7_sanitized": "", - "url8": "", - "url80": "", - "url80_sanitized": "", - "url81": "", - "url81_sanitized": "", - "url82": "", - "url82_sanitized": "", - "url83": "", - "url83_sanitized": "", - "url84": "", - "url84_sanitized": "", - "url85": "", - "url85_sanitized": "", - "url86": "", - "url86_sanitized": "", - "url87": "", - "url87_sanitized": "", - "url88": "", - "url88_sanitized": "", - "url89": "", - "url89_sanitized": "", - "url8_sanitized": "", - "url9": "", - "url90": "", - "url90_sanitized": "", - "url91": "", - "url91_sanitized": "", - "url92": "", - "url92_sanitized": "", - "url93": "", - "url93_sanitized": "", - "url94": "", - "url94_sanitized": "", - "url95": "", - "url95_sanitized": "", - "url96": "", - "url96_sanitized": "", - "url97": "", - "url97_sanitized": "", - "url98": "", - "url98_sanitized": "", - "url99": "", - "url99_sanitized": "", - "url9_sanitized": "", - "url_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists", - "urls": "{ [ \"https://www.youtube.com/channel/UCsItMF6_fP754ihIsSRLk5A/playlists\" ] }", - "urls_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8channel\u29f8UCsItMF6_fP754ihIsSRLk5A\u29f8playlists\uff02]", - "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "webpage_url_sanitized": "{ %sanitize( webpage_url ) }", - "width": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }", - "width_sanitized": "{ %sanitize( width ) }", - "ytdl_sub_entry_date_eval": "{ %string( upload_date_standardized ) }", - "ytdl_sub_entry_date_eval_sanitized": "{ %sanitize( ytdl_sub_entry_date_eval ) }", - "ytdl_sub_input_url": "{ %string( '' ) }", - "ytdl_sub_input_url_count": "{ %int( 0 ) }", - "ytdl_sub_input_url_count_sanitized": "{ %sanitize( ytdl_sub_input_url_count ) }", - "ytdl_sub_input_url_index": "{ %int( -1 ) }", - "ytdl_sub_input_url_index_sanitized": "{ %sanitize( ytdl_sub_input_url_index ) }", - "ytdl_sub_input_url_sanitized": "{ %sanitize( ytdl_sub_input_url ) }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_full_overrides.json b/tests/resources/expected_json/music_video/inspect_full_overrides.json deleted file mode 100644 index 0ad15d9a..00000000 --- a/tests/resources/expected_json/music_video/inspect_full_overrides.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "avatar_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name": "", - "category_url_array": "{ [ ] }", - "category_url_map": "{ [ ] }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "include_sibling_metadata": "{ %bool(False) }", - "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "music_video_album": "{ %get_url_field( \"category\", music_video_album_default ) }", - "music_video_album_default": "Music Videos", - "music_video_artist": "Rick Astley", - "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", - "music_video_directory": "tv_show_directory_path", - "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "music_video_file_name_suffix": "", - "music_video_genre": "Pop", - "music_video_genre_default": "ytdl-sub", - "music_video_title": "{ %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_readable": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }x{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "subscription_array": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\" ] }", - "subscription_indent_1": "Pop", - "subscription_map": "{ { } }", - "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' ] }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_full_variables.json b/tests/resources/expected_json/music_video/inspect_full_variables.json deleted file mode 100644 index fa8b5c85..00000000 --- a/tests/resources/expected_json/music_video/inspect_full_variables.json +++ /dev/null @@ -1,480 +0,0 @@ -{ - "avatar_uncropped_thumbnail_file_name": "", - "avatar_uncropped_thumbnail_file_name_sanitized": "", - "banner_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name_sanitized": "", - "category_url_array": "{ [ ] }", - "category_url_array_sanitized": "[]", - "category_url_map": "{ [ ] }", - "category_url_map_sanitized": "[]", - "channel": "{ %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "channel_id": "{ %map_get_non_empty( entry_metadata, \"channel_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "channel_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"channel_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "channel_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "chapters": "{ [ ] }", - "chapters_sanitized": "[]", - "comments": "{ [ ] }", - "comments_sanitized": "[]", - "creator": "{ %map_get_non_empty( entry_metadata, \"creator\", %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "creator_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"creator\", %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "description": "{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "description_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"description\", '' ) ) }", - "download_index": "{ %int(1) }", - "download_index_padded6": "000001", - "download_index_padded6_sanitized": "000001", - "download_index_sanitized": "1", - "duration": "{ %map_get_non_empty( entry_metadata, \"duration\", 0 ) }", - "duration_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"duration\", 0 ) ) }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_bilateral_scraping_sanitized": "true", - "enable_resolution_assert": "{ %bool(True) }", - "enable_resolution_assert_sanitized": "true", - "enable_throttle_protection": "{ %bool(True) }", - "enable_throttle_protection_sanitized": "true", - "entry_metadata": "{ { } }", - "entry_metadata_sanitized": "{ %sanitize( entry_metadata ) }", - "epoch": "{ %map_get( entry_metadata, \"epoch\" ) }", - "epoch_date": "{ %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) }", - "epoch_date_sanitized": "{ %sanitize( %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) }", - "epoch_hour": "{ %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%H\" ) }", - "epoch_hour_sanitized": "{ %sanitize( %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%H\" ) ) }", - "epoch_sanitized": "{ %sanitize( %map_get( entry_metadata, \"epoch\" ) ) }", - "ext": "{ %map_get( entry_metadata, \"ext\" ) }", - "ext_sanitized": "{ %sanitize( %map_get( entry_metadata, \"ext\" ) ) }", - "extractor": "{ %map_get( entry_metadata, \"extractor\" ) }", - "extractor_key": "{ %map_get( entry_metadata, \"extractor_key\" ) }", - "extractor_key_sanitized": "{ %sanitize( %map_get( entry_metadata, \"extractor_key\" ) ) }", - "extractor_sanitized": "{ %sanitize( %map_get( entry_metadata, \"extractor\" ) ) }", - "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "file_title_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "file_uid_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) ) }", - "height": "{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "height_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"height\", 0 ) ) }", - "ie_key": "{ %map_get_non_empty( entry_metadata, \"ie_key\", %map_get( entry_metadata, \"extractor_key\" ) ) }", - "ie_key_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"ie_key\", %map_get( entry_metadata, \"extractor_key\" ) ) ) }", - "include_sibling_metadata": "{ %bool(False) }", - "include_sibling_metadata_sanitized": "false", - "info_json_ext": "info.json", - "info_json_ext_sanitized": "info.json", - "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "modified_webpage_url_sanitized": "{ %sanitize( %map_get( entry_metadata, \"webpage_url\" ) ) }", - "music_video_album": "{ %get_url_field( \"category\", music_video_album_default ) }", - "music_video_album_default": "Music Videos", - "music_video_album_default_sanitized": "Music Videos", - "music_video_album_sanitized": "{ %sanitize( %get_url_field( \"category\", music_video_album_default ) ) }", - "music_video_artist": "Rick Astley", - "music_video_artist_sanitized": "Rick Astley", - "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", - "music_video_date_sanitized": "{ %sanitize( %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) ) }", - "music_video_directory": "tv_show_directory_path", - "music_video_directory_sanitized": "tv_show_directory_path", - "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "music_video_file_name_sanitized": "{ %sanitize( %concat( \"Rick Astley\", \"/\", %string( %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ), '' ) ) }", - "music_video_file_name_suffix": "", - "music_video_file_name_suffix_sanitized": "", - "music_video_genre": "Pop", - "music_video_genre_default": "ytdl-sub", - "music_video_genre_default_sanitized": "ytdl-sub", - "music_video_genre_sanitized": "Pop", - "music_video_title": "{ %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "music_video_title_sanitized": "{ %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", - "music_video_year_sanitized": "{ %sanitize( %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) ) }", - "playlist_count": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "playlist_count_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) ) }", - "playlist_description": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) }", - "playlist_description_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) }", - "playlist_index": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "playlist_index_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", - "playlist_index_padded6": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 6 ) }", - "playlist_index_padded6_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 6 ) ) }", - "playlist_index_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ) }", - "playlist_index_reversed": "{ %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ) }", - "playlist_index_reversed_padded": "{ %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 2 ) }", - "playlist_index_reversed_padded6": "{ %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 6 ) }", - "playlist_index_reversed_padded6_sanitized": "{ %sanitize( %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 6 ) ) }", - "playlist_index_reversed_padded_sanitized": "{ %sanitize( %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 2 ) ) }", - "playlist_index_reversed_sanitized": "{ %sanitize( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ) ) }", - "playlist_index_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) ) }", - "playlist_max_upload_date": "{ %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) }", - "playlist_max_upload_date_sanitized": "{ %sanitize( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ) }", - "playlist_max_upload_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", - "playlist_max_upload_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ) }", - "playlist_max_upload_year_truncated": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year_truncated\" ) ) }", - "playlist_max_upload_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year_truncated\" ) ) ) }", - "playlist_metadata": "{ %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) }", - "playlist_metadata_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) ) }", - "playlist_title": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uid": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) }", - "playlist_uid_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_uploader": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uploader_id": "{ %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uploader_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "playlist_uploader_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "playlist_uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "playlist_webpage_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "playlist_webpage_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "release_date": "{ %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) }", - "release_date_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ) }", - "release_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"date_standardized\" ) ) }", - "release_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"date_standardized\" ) ) ) }", - "release_day": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day\" ) ) }", - "release_day_of_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year\" ) ) }", - "release_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_padded\" ) ) }", - "release_day_of_year_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_padded\" ) ) ) }", - "release_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed\" ) ) }", - "release_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed_padded\" ) ) }", - "release_day_of_year_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed_padded\" ) ) ) }", - "release_day_of_year_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed\" ) ) ) }", - "release_day_of_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year\" ) ) ) }", - "release_day_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_padded\" ) ) }", - "release_day_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_padded\" ) ) ) }", - "release_day_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed\" ) ) }", - "release_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed_padded\" ) ) }", - "release_day_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed_padded\" ) ) ) }", - "release_day_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed\" ) ) ) }", - "release_day_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day\" ) ) ) }", - "release_month": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month\" ) ) }", - "release_month_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_padded\" ) ) }", - "release_month_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_padded\" ) ) ) }", - "release_month_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed\" ) ) }", - "release_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed_padded\" ) ) }", - "release_month_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed_padded\" ) ) ) }", - "release_month_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed\" ) ) ) }", - "release_month_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month\" ) ) ) }", - "release_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year\" ) ) }", - "release_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year\" ) ) ) }", - "release_year_truncated": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated\" ) ) }", - "release_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated_reversed\" ) ) }", - "release_year_truncated_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated_reversed\" ) ) ) }", - "release_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated\" ) ) ) }", - "requested_subtitles": "{ { } }", - "requested_subtitles_sanitized": "{}", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_height_gte_sanitized": "361", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_ignore_titles_sanitized": "[]", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) }", - "resolution_assert_is_ignored_sanitized": "{ %sanitize( %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_assert_print_sanitized": "true", - "resolution_assert_sanitized": "{ %sanitize( %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) ) }", - "resolution_readable": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }x{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "resolution_readable_sanitized": "{ %sanitize( %concat( %string( %map_get_non_empty( entry_metadata, \"width\", 0 ) ), \"x\", %string( %map_get_non_empty( entry_metadata, \"height\", 0 ) ) ) ) }", - "sibling_metadata": "{ %map_get_non_empty( entry_metadata, \"sibling_metadata\", [ ] ) }", - "sibling_metadata_sanitized": "{ %sanitize( sibling_metadata ) }", - "source_count": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_count\", 1 ) }", - "source_count_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_count\", 1 ) ) }", - "source_description": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"description\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) }", - "source_description_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"description\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) ) }", - "source_index": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ) }", - "source_index_padded": "{ %pad_zero( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ), 2 ) }", - "source_index_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ), 2 ) ) }", - "source_index_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ) ) }", - "source_metadata": "{ %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) }", - "source_metadata_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) ) }", - "source_title": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"title\", %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_title_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"title\", %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uid": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"id\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "source_uid_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"id\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_uploader": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uploader_id": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_id\", %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_id\", %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uploader_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "source_uploader_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) }", - "source_uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) ) }", - "source_webpage_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "source_webpage_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) }", - "sponsorblock_chapters": "{ [ ] }", - "sponsorblock_chapters_sanitized": "[]", - "subscription_array": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\" ] }", - "subscription_array_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\uff02]", - "subscription_has_download_archive": "{ %bool(False) }", - "subscription_has_download_archive_sanitized": "false", - "subscription_indent_1": "Pop", - "subscription_indent_1_sanitized": "Pop", - "subscription_map": "{ { } }", - "subscription_map_sanitized": "{}", - "subscription_name": "Rick Astley", - "subscription_name_sanitized": "Rick Astley", - "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_1_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "thumbnail_ext": "jpg", - "thumbnail_ext_sanitized": "jpg", - "title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "title_sanitized_plex": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "title_sanitized_plex_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "uid": "{ %map_get( entry_metadata, \"id\" ) }", - "uid_sanitized": "{ %sanitize( %map_get( entry_metadata, \"id\" ) ) }", - "uid_sanitized_plex": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "uid_sanitized_plex_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) ) }", - "upload_date": "{ %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) }", - "upload_date_index": "{ %int(1) }", - "upload_date_index_padded": "01", - "upload_date_index_padded_sanitized": "01", - "upload_date_index_reversed": "{ %int(99) }", - "upload_date_index_reversed_padded": "99", - "upload_date_index_reversed_padded_sanitized": "99", - "upload_date_index_reversed_sanitized": "99", - "upload_date_index_sanitized": "1", - "upload_date_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) }", - "upload_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "upload_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "upload_day": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day\" ) ) }", - "upload_day_of_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year\" ) ) }", - "upload_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_padded\" ) ) }", - "upload_day_of_year_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_padded\" ) ) ) }", - "upload_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed\" ) ) }", - "upload_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed_padded\" ) ) }", - "upload_day_of_year_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed_padded\" ) ) ) }", - "upload_day_of_year_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed\" ) ) ) }", - "upload_day_of_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year\" ) ) ) }", - "upload_day_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ) }", - "upload_day_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ) ) }", - "upload_day_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed\" ) ) }", - "upload_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed_padded\" ) ) }", - "upload_day_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed_padded\" ) ) ) }", - "upload_day_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed\" ) ) ) }", - "upload_day_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day\" ) ) ) }", - "upload_month": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }", - "upload_month_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_padded\" ) ) }", - "upload_month_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_padded\" ) ) ) }", - "upload_month_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed\" ) ) }", - "upload_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed_padded\" ) ) }", - "upload_month_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed_padded\" ) ) ) }", - "upload_month_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed\" ) ) ) }", - "upload_month_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) ) }", - "upload_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "upload_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) }", - "upload_year_truncated": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated\" ) ) }", - "upload_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated_reversed\" ) ) }", - "upload_year_truncated_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated_reversed\" ) ) ) }", - "upload_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated\" ) ) ) }", - "uploader": "{ %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "uploader_id": "{ %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) }", - "uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "uploader_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "uploader_url": "{ %map_get_non_empty( entry_metadata, \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "url10": "", - "url100": "", - "url100_sanitized": "", - "url10_sanitized": "", - "url11": "", - "url11_sanitized": "", - "url12": "", - "url12_sanitized": "", - "url13": "", - "url13_sanitized": "", - "url14": "", - "url14_sanitized": "", - "url15": "", - "url15_sanitized": "", - "url16": "", - "url16_sanitized": "", - "url17": "", - "url17_sanitized": "", - "url18": "", - "url18_sanitized": "", - "url19": "", - "url19_sanitized": "", - "url2": "", - "url20": "", - "url20_sanitized": "", - "url21": "", - "url21_sanitized": "", - "url22": "", - "url22_sanitized": "", - "url23": "", - "url23_sanitized": "", - "url24": "", - "url24_sanitized": "", - "url25": "", - "url25_sanitized": "", - "url26": "", - "url26_sanitized": "", - "url27": "", - "url27_sanitized": "", - "url28": "", - "url28_sanitized": "", - "url29": "", - "url29_sanitized": "", - "url2_sanitized": "", - "url3": "", - "url30": "", - "url30_sanitized": "", - "url31": "", - "url31_sanitized": "", - "url32": "", - "url32_sanitized": "", - "url33": "", - "url33_sanitized": "", - "url34": "", - "url34_sanitized": "", - "url35": "", - "url35_sanitized": "", - "url36": "", - "url36_sanitized": "", - "url37": "", - "url37_sanitized": "", - "url38": "", - "url38_sanitized": "", - "url39": "", - "url39_sanitized": "", - "url3_sanitized": "", - "url4": "", - "url40": "", - "url40_sanitized": "", - "url41": "", - "url41_sanitized": "", - "url42": "", - "url42_sanitized": "", - "url43": "", - "url43_sanitized": "", - "url44": "", - "url44_sanitized": "", - "url45": "", - "url45_sanitized": "", - "url46": "", - "url46_sanitized": "", - "url47": "", - "url47_sanitized": "", - "url48": "", - "url48_sanitized": "", - "url49": "", - "url49_sanitized": "", - "url4_sanitized": "", - "url5": "", - "url50": "", - "url50_sanitized": "", - "url51": "", - "url51_sanitized": "", - "url52": "", - "url52_sanitized": "", - "url53": "", - "url53_sanitized": "", - "url54": "", - "url54_sanitized": "", - "url55": "", - "url55_sanitized": "", - "url56": "", - "url56_sanitized": "", - "url57": "", - "url57_sanitized": "", - "url58": "", - "url58_sanitized": "", - "url59": "", - "url59_sanitized": "", - "url5_sanitized": "", - "url6": "", - "url60": "", - "url60_sanitized": "", - "url61": "", - "url61_sanitized": "", - "url62": "", - "url62_sanitized": "", - "url63": "", - "url63_sanitized": "", - "url64": "", - "url64_sanitized": "", - "url65": "", - "url65_sanitized": "", - "url66": "", - "url66_sanitized": "", - "url67": "", - "url67_sanitized": "", - "url68": "", - "url68_sanitized": "", - "url69": "", - "url69_sanitized": "", - "url6_sanitized": "", - "url7": "", - "url70": "", - "url70_sanitized": "", - "url71": "", - "url71_sanitized": "", - "url72": "", - "url72_sanitized": "", - "url73": "", - "url73_sanitized": "", - "url74": "", - "url74_sanitized": "", - "url75": "", - "url75_sanitized": "", - "url76": "", - "url76_sanitized": "", - "url77": "", - "url77_sanitized": "", - "url78": "", - "url78_sanitized": "", - "url79": "", - "url79_sanitized": "", - "url7_sanitized": "", - "url8": "", - "url80": "", - "url80_sanitized": "", - "url81": "", - "url81_sanitized": "", - "url82": "", - "url82_sanitized": "", - "url83": "", - "url83_sanitized": "", - "url84": "", - "url84_sanitized": "", - "url85": "", - "url85_sanitized": "", - "url86": "", - "url86_sanitized": "", - "url87": "", - "url87_sanitized": "", - "url88": "", - "url88_sanitized": "", - "url89": "", - "url89_sanitized": "", - "url8_sanitized": "", - "url9": "", - "url90": "", - "url90_sanitized": "", - "url91": "", - "url91_sanitized": "", - "url92": "", - "url92_sanitized": "", - "url93": "", - "url93_sanitized": "", - "url94": "", - "url94_sanitized": "", - "url95": "", - "url95_sanitized": "", - "url96": "", - "url96_sanitized": "", - "url97": "", - "url97_sanitized": "", - "url98": "", - "url98_sanitized": "", - "url99": "", - "url99_sanitized": "", - "url9_sanitized": "", - "url_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "urls": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' ] }", - "urls_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02]", - "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "webpage_url_sanitized": "{ %sanitize( %map_get( entry_metadata, \"webpage_url\" ) ) }", - "width": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }", - "width_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"width\", 0 ) ) }", - "ytdl_sub_entry_date_eval": "{ %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "ytdl_sub_entry_date_eval_sanitized": "{ %sanitize( %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) ) }", - "ytdl_sub_input_url": "", - "ytdl_sub_input_url_count": "{ %int(0) }", - "ytdl_sub_input_url_count_sanitized": "0", - "ytdl_sub_input_url_index": "{ %int(-1) }", - "ytdl_sub_input_url_index_sanitized": "-1", - "ytdl_sub_input_url_sanitized": "" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_overrides.json b/tests/resources/expected_json/music_video/inspect_overrides.json deleted file mode 100644 index a0b3c3a1..00000000 --- a/tests/resources/expected_json/music_video/inspect_overrides.json +++ /dev/null @@ -1,136 +0,0 @@ -{ - "avatar_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name": "", - "category_url_array": "{ [ ] }", - "category_url_map": "{ [ ] }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "file_title": "{ title_sanitized_plex }", - "file_uid": "{ uid_sanitized_plex }", - "include_sibling_metadata": "{ %bool(False) }", - "modified_webpage_url": "{ webpage_url }", - "music_video_album": "{ %get_url_field( \"category\", music_video_album_default ) }", - "music_video_album_default": "Music Videos", - "music_video_artist": "Rick Astley", - "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", - "music_video_directory": "tv_show_directory_path", - "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }", - "music_video_file_name_suffix": "", - "music_video_genre": "Pop", - "music_video_genre_default": "ytdl-sub", - "music_video_title": "{ %get_url_field( \"title\", title ) }", - "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_readable": "{ width }x{ height }", - "subscription_array": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\" ] }", - "subscription_indent_1": "Pop", - "subscription_map": "{ { } }", - "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' ] }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_fill.json b/tests/resources/expected_json/music_video/inspect_sub_fill.json new file mode 100644 index 00000000..5999a01f --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_fill.json @@ -0,0 +1,314 @@ +{ + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "variables": {}, + "webpage_url": "{ modified_webpage_url }", + "ytdl_options": {} + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-Rick Astley-download-archive.json", + "file_name": "{ music_video_file_name }.{ ext }", + "info_json_name": "{ music_video_file_name }.{ info_json_ext }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpyukbh6ta", + "preserve_mtime": false, + "thumbnail_name": "{ music_video_file_name }.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, '' ) ) ) }", + "%flat_array__category_to_map_format": "{ %array_apply_fixed( %assert_then( %is_array( $1 ), $1, \"If using album categories, each category must map to an array\" ), $0, %flat_array__url_to_map_format ) }", + "%flat_array__url_keyed_map_format": "{ { %map_get( $0, \"url\" ): $0 } }", + "%flat_array__url_to_map_format": "{ %elif( %is_map( $0 ), %map_extend( $0, { \"category\": $1 } ), %is_string( $0 ), { \"url\": $0, \"category\": $1 }, %throw( \"If using album categories, each URL must be either a string or map with metadata fields\" ) ) }", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, { } ) ), $0, $1 ) }", + "%get_url_i": "{ %map_get( %map( %array_at( category_url_array, %int( %sub( $0, 1 ) ), { } ) ), \"url\", %array_at( subscription_array, %int( %sub( $0, 1 ) ), '' ) ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ [ ] }", + "category_url_map": "{ [ ] }", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "music_video_album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "Rick Astley", + "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpyukbh6ta", + "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }", + "music_video_file_name_suffix": "", + "music_video_genre": "Pop", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field( \"title\", title ) }", + "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "subscription_indent_1": "Pop", + "subscription_map": {}, + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{ music_video_album }", + "artist": "Rick Astley", + "genre": "Pop", + "premiered": "{ music_video_date }", + "title": "{ music_video_title }", + "year": "{ music_video_year }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_internal.json b/tests/resources/expected_json/music_video/inspect_sub_internal.json new file mode 100644 index 00000000..3cd8a4cd --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_internal.json @@ -0,0 +1,314 @@ +{ + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "variables": {}, + "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "ytdl_options": {} + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-Rick Astley-download-archive.json", + "file_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), '' ) }.{ %map_get( entry_metadata, \"ext\" ) }", + "info_json_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), '' ) }.info.json", + "keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmput7fc_rs", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ), '' ) }.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, '' ) ) ) }", + "%flat_array__category_to_map_format": "{ %array_apply_fixed( %assert_then( %is_array( $1 ), $1, \"If using album categories, each category must map to an array\" ), $0, %flat_array__url_to_map_format ) }", + "%flat_array__url_keyed_map_format": "{ { %map_get( $0, \"url\" ): $0 } }", + "%flat_array__url_to_map_format": "{ %elif( %is_map( $0 ), %map_extend( $0, { \"category\": $1 } ), %is_string( $0 ), { \"url\": $0, \"category\": $1 }, %throw( \"If using album categories, each URL must be either a string or map with metadata fields\" ) ) }", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, { } ) ), $0, $1 ) }", + "%get_url_i": "{ %map_get( %map( %array_at( category_url_array, %int( %sub( $0, 1 ) ), { } ) ), \"url\", %array_at( subscription_array, %int( %sub( $0, 1 ) ), '' ) ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ [ ] }", + "category_url_map": "{ [ ] }", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "music_video_album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "Rick Astley", + "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmput7fc_rs", + "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", + "music_video_file_name_suffix": "", + "music_video_genre": "Pop", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "subscription_indent_1": "Pop", + "subscription_map": {}, + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "artist": "Rick Astley", + "genre": "Pop", + "premiered": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "title": "{ %get_url_field( \"title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_original.json b/tests/resources/expected_json/music_video/inspect_sub_original.json new file mode 100644 index 00000000..cdd52c9f --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_original.json @@ -0,0 +1,235 @@ +{ + "download": [ + { + "download_reverse": false, + "url": "{ %array_apply(urls, %bilateral_url) }", + "webpage_url": "{modified_webpage_url}", + "ytdl_options": { + "playlist_items": "-1:0:-1" + } + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "playlist_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "url": "{ %array_at(urls, 0) }", + "webpage_url": "{modified_webpage_url}" + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "url": "{ %array_slice(urls, 1) }", + "webpage_url": "{modified_webpage_url}" + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "file_name": "{music_video_file_name}.{ext}", + "info_json_name": "{music_video_file_name}.{info_json_ext}", + "maintain_download_archive": true, + "output_directory": "{music_video_directory}", + "thumbnail_name": "{music_video_file_name}.jpg" + }, + "overrides": { + "%bilateral_url": "{ \n %if(\n %and(\n enable_bilateral_scraping,\n subscription_has_download_archive,\n %is_bilateral_url($0)\n ),\n $0,\n \"\"\n )\n}", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, null ) ) ) }", + "%flat_array__category_to_map_format": "{ \n %array_apply_fixed(\n %assert_then(\n %is_array( $1 ),\n $1,\n \"If using album categories, each category must map to an array\"\n ),\n $0,\n %flat_array__url_to_map_format \n )\n}", + "%flat_array__url_keyed_map_format": "{ { %map_get($0, \"url\"): $0 } }", + "%flat_array__url_to_map_format": "{\n %elif(\n %is_map($0),\n %map_extend( $0, { \"category\": $1 } ),\n %is_string($0),\n { \"url\": $0, \"category\": $1 },\n %throw(\"If using album categories, each URL must be either a string or map with metadata fields\")\n )\n}", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, {} ) ), $0, $1 ) }", + "%get_url_i": "{ \n %map_get(\n %map( %array_at( category_url_array, %int( %sub($0, 1) ), {} ) ),\n \"url\",\n %array_at(subscription_array, %int( %sub($0, 1) ), null)\n )\n}", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ %array_flatten( %map_apply( subscription_map, %flat_array__category_to_map_format ) ) }", + "category_url_map": "{\n %array_reduce(\n %array_apply( category_url_array, %flat_array__url_keyed_map_format ),\n %map_extend\n )\n}", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{title_sanitized_plex}", + "file_uid": "{uid_sanitized_plex}", + "include_sibling_metadata": false, + "modified_webpage_url": "{webpage_url}", + "music_video_album": "{ %get_url_field(\"category\", music_video_album_default) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "{subscription_name}", + "music_video_date": "{ \n %elif(\n %contains_url_field(\"date\"),\n %get_url_field(\"date\", upload_date_standardized),\n\n %contains_url_field(\"year\"),\n %concat( %get_url_field(\"date\", upload_year), \"-01-01\"),\n\n upload_date_standardized\n )\n}", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp3bmucwsg", + "music_video_file_name": "{music_video_artist_sanitized}/{music_video_title_sanitized}{music_video_file_name_suffix}", + "music_video_file_name_suffix": "", + "music_video_genre": "{subscription_indent_1}", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field(\"title\", title) }", + "music_video_year": "{\n %int(%elif(\n %contains_url_field(\"date\"),\n %slice( %get_url_field(\"date\", upload_date_standardized), 0, 4 ),\n\n %contains_url_field(\"year\"),\n %get_url_field(\"date\", upload_year),\n\n upload_year\n ))\n}", + "resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [] }", + "resolution_assert_is_ignored": "{\n %print_if_true(\n %concat(title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\"),\n %contains_any(title, resolution_assert_ignore_titles)\n )\n}", + "resolution_assert_print": "{\n %print(\n %if(\n enable_resolution_assert,\n \"Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`\",\n \"Resolution assert is disabled. Use at your own risk!\"\n ),\n enable_resolution_assert,\n -1\n )\n}", + "resolution_readable": "{width}x{height}", + "subscription_array": "{%from_json('''[\"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\"]''')}", + "subscription_indent_1": "Pop", + "subscription_map": "{ {} }", + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "{subscription_value}", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": "{ %array_apply(%range(100, 1), %get_url_i) }" + }, + "preset": [ + "_base", + "_plex_base", + "_url_bilateral_overrides", + "_multi_url_bilateral_inner", + "_multi_url", + "_multi_url_bilateral", + "_throttle_protection", + "_url_categorized", + "_plex_video_base", + "_music_video_base", + "_music_video_tags", + "Plex Music Videos", + "__preset__" + ], + "throttle_protection": { + "enable": "{\n %print(\n %if(\n enable_throttle_protection,\n \"Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`\",\n \"Throttle protection is disabled. Use at your own risk!\"\n ),\n enable_throttle_protection\n )\n}", + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{music_video_album}", + "artist": "{music_video_artist}", + "genre": "{music_video_genre}", + "premiered": "{music_video_date}", + "title": "{music_video_title}", + "year": "{music_video_year}" + }, + "ytdl_options": { + "break_on_existing": true + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_sub_resolve.json b/tests/resources/expected_json/music_video/inspect_sub_resolve.json new file mode 100644 index 00000000..f3594e9c --- /dev/null +++ b/tests/resources/expected_json/music_video/inspect_sub_resolve.json @@ -0,0 +1,314 @@ +{ + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "", + "uid": "avatar_uncropped" + }, + { + "name": "", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "variables": {}, + "webpage_url": "{ webpage_url }", + "ytdl_options": {} + } + ], + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-Rick Astley-download-archive.json", + "file_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", title ) ), '' ) }.{ ext }", + "info_json_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", title ) ), '' ) }.{ info_json_ext }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpstvsa5ot", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( \"Rick Astley\", \"/\", %sanitize( %get_url_field( \"title\", title ) ), '' ) }.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%contains_url_field": "{ %not( %is_null( %get_url_field( $0, '' ) ) ) }", + "%flat_array__category_to_map_format": "{ %array_apply_fixed( %assert_then( %is_array( $1 ), $1, \"If using album categories, each category must map to an array\" ), $0, %flat_array__url_to_map_format ) }", + "%flat_array__url_keyed_map_format": "{ { %map_get( $0, \"url\" ): $0 } }", + "%flat_array__url_to_map_format": "{ %elif( %is_map( $0 ), %map_extend( $0, { \"category\": $1 } ), %is_string( $0 ), { \"url\": $0, \"category\": $1 }, %throw( \"If using album categories, each URL must be either a string or map with metadata fields\" ) ) }", + "%get_url_field": "{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, { } ) ), $0, $1 ) }", + "%get_url_i": "{ %map_get( %map( %array_at( category_url_array, %int( %sub( $0, 1 ) ), { } ) ), \"url\", %array_at( subscription_array, %int( %sub( $0, 1 ) ), '' ) ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "avatar_uncropped_thumbnail_file_name": "", + "banner_uncropped_thumbnail_file_name": "", + "category_url_array": "{ [ ] }", + "category_url_map": "{ [ ] }", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "music_video_album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "music_video_album_default": "Music Videos", + "music_video_artist": "Rick Astley", + "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpstvsa5ot", + "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }", + "music_video_file_name_suffix": "", + "music_video_genre": "Pop", + "music_video_genre_default": "ytdl-sub", + "music_video_title": "{ %get_url_field( \"title\", title ) }", + "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "subscription_array": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc" + ], + "subscription_indent_1": "Pop", + "subscription_map": {}, + "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "album": "{ %get_url_field( \"category\", \"Music Videos\" ) }", + "artist": "Rick Astley", + "genre": "Pop", + "premiered": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", + "title": "{ %get_url_field( \"title\", title ) }", + "year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/music_video/inspect_variables.json b/tests/resources/expected_json/music_video/inspect_variables.json deleted file mode 100644 index 318f2685..00000000 --- a/tests/resources/expected_json/music_video/inspect_variables.json +++ /dev/null @@ -1,480 +0,0 @@ -{ - "avatar_uncropped_thumbnail_file_name": "", - "avatar_uncropped_thumbnail_file_name_sanitized": "", - "banner_uncropped_thumbnail_file_name": "", - "banner_uncropped_thumbnail_file_name_sanitized": "", - "category_url_array": "{ [ ] }", - "category_url_array_sanitized": "[]", - "category_url_map": "{ [ ] }", - "category_url_map_sanitized": "[]", - "channel": "{ %map_get_non_empty( entry_metadata, \"channel\", uploader ) }", - "channel_id": "{ %map_get_non_empty( entry_metadata, \"channel_id\", uploader_id ) }", - "channel_id_sanitized": "{ %sanitize( channel_id ) }", - "channel_sanitized": "{ %sanitize( channel ) }", - "chapters": "{ [ ] }", - "chapters_sanitized": "{ %sanitize( chapters ) }", - "comments": "{ [ ] }", - "comments_sanitized": "{ %sanitize( comments ) }", - "creator": "{ %map_get_non_empty( entry_metadata, \"creator\", channel ) }", - "creator_sanitized": "{ %sanitize( creator ) }", - "description": "{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "description_sanitized": "{ %sanitize( description ) }", - "download_index": "{ %int( 1 ) }", - "download_index_padded6": "{ %pad_zero( download_index, 6 ) }", - "download_index_padded6_sanitized": "{ %sanitize( download_index_padded6 ) }", - "download_index_sanitized": "{ %sanitize( download_index ) }", - "duration": "{ %map_get_non_empty( entry_metadata, \"duration\", 0 ) }", - "duration_sanitized": "{ %sanitize( duration ) }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_bilateral_scraping_sanitized": "true", - "enable_resolution_assert": "{ %bool(True) }", - "enable_resolution_assert_sanitized": "true", - "enable_throttle_protection": "{ %bool(True) }", - "enable_throttle_protection_sanitized": "true", - "entry_metadata": "{ { } }", - "entry_metadata_sanitized": "{ %sanitize( entry_metadata ) }", - "epoch": "{ %map_get( entry_metadata, \"epoch\" ) }", - "epoch_date": "{ %datetime_strftime( epoch, \"%Y%m%d\" ) }", - "epoch_date_sanitized": "{ %sanitize( epoch_date ) }", - "epoch_hour": "{ %datetime_strftime( epoch, \"%H\" ) }", - "epoch_hour_sanitized": "{ %sanitize( epoch_hour ) }", - "epoch_sanitized": "{ %sanitize( epoch ) }", - "ext": "{ %map_get( entry_metadata, \"ext\" ) }", - "ext_sanitized": "{ %sanitize( ext ) }", - "extractor": "{ %map_get( entry_metadata, \"extractor\" ) }", - "extractor_key": "{ %map_get( entry_metadata, \"extractor_key\" ) }", - "extractor_key_sanitized": "{ %sanitize( extractor_key ) }", - "extractor_sanitized": "{ %sanitize( extractor ) }", - "file_title": "{ title_sanitized_plex }", - "file_title_sanitized": "{ %sanitize( title_sanitized_plex ) }", - "file_uid": "{ uid_sanitized_plex }", - "file_uid_sanitized": "{ %sanitize( uid_sanitized_plex ) }", - "height": "{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "height_sanitized": "{ %sanitize( height ) }", - "ie_key": "{ %map_get_non_empty( entry_metadata, \"ie_key\", extractor_key ) }", - "ie_key_sanitized": "{ %sanitize( ie_key ) }", - "include_sibling_metadata": "{ %bool(False) }", - "include_sibling_metadata_sanitized": "false", - "info_json_ext": "info.json", - "info_json_ext_sanitized": "info.json", - "modified_webpage_url": "{ webpage_url }", - "modified_webpage_url_sanitized": "{ %sanitize( webpage_url ) }", - "music_video_album": "{ %get_url_field( \"category\", music_video_album_default ) }", - "music_video_album_default": "Music Videos", - "music_video_album_default_sanitized": "Music Videos", - "music_video_album_sanitized": "{ %sanitize( %get_url_field( \"category\", music_video_album_default ) ) }", - "music_video_artist": "Rick Astley", - "music_video_artist_sanitized": "Rick Astley", - "music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }", - "music_video_date_sanitized": "{ %sanitize( %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) ) }", - "music_video_directory": "tv_show_directory_path", - "music_video_directory_sanitized": "tv_show_directory_path", - "music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }", - "music_video_file_name_sanitized": "{ %sanitize( %concat( \"Rick Astley\", \"/\", %string( %sanitize( %get_url_field( \"title\", title ) ) ), '' ) ) }", - "music_video_file_name_suffix": "", - "music_video_file_name_suffix_sanitized": "", - "music_video_genre": "Pop", - "music_video_genre_default": "ytdl-sub", - "music_video_genre_default_sanitized": "ytdl-sub", - "music_video_genre_sanitized": "Pop", - "music_video_title": "{ %get_url_field( \"title\", title ) }", - "music_video_title_sanitized": "{ %sanitize( %get_url_field( \"title\", title ) ) }", - "music_video_year": "{ %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) }", - "music_video_year_sanitized": "{ %sanitize( %int( %elif( %contains_url_field( \"date\" ), %slice( %get_url_field( \"date\", upload_date_standardized ), 0, 4 ), %contains_url_field( \"year\" ), %get_url_field( \"date\", upload_year ), upload_year ) ) ) }", - "playlist_count": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "playlist_count_sanitized": "{ %sanitize( playlist_count ) }", - "playlist_description": "{ %map_get_non_empty( playlist_metadata, \"description\", description ) }", - "playlist_description_sanitized": "{ %sanitize( playlist_description ) }", - "playlist_index": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "playlist_index_padded": "{ %pad_zero( playlist_index, 2 ) }", - "playlist_index_padded6": "{ %pad_zero( playlist_index, 6 ) }", - "playlist_index_padded6_sanitized": "{ %sanitize( playlist_index_padded6 ) }", - "playlist_index_padded_sanitized": "{ %sanitize( playlist_index_padded ) }", - "playlist_index_reversed": "{ %sub( playlist_count, playlist_index, -1 ) }", - "playlist_index_reversed_padded": "{ %pad_zero( playlist_index_reversed, 2 ) }", - "playlist_index_reversed_padded6": "{ %pad_zero( playlist_index_reversed, 6 ) }", - "playlist_index_reversed_padded6_sanitized": "{ %sanitize( playlist_index_reversed_padded6 ) }", - "playlist_index_reversed_padded_sanitized": "{ %sanitize( playlist_index_reversed_padded ) }", - "playlist_index_reversed_sanitized": "{ %sanitize( playlist_index_reversed ) }", - "playlist_index_sanitized": "{ %sanitize( playlist_index ) }", - "playlist_max_upload_date": "{ %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) }", - "playlist_max_upload_date_sanitized": "{ %sanitize( playlist_max_upload_date ) }", - "playlist_max_upload_year": "{ %int( %map_get( %to_date_metadata( playlist_max_upload_date ), \"year\" ) ) }", - "playlist_max_upload_year_sanitized": "{ %sanitize( playlist_max_upload_year ) }", - "playlist_max_upload_year_truncated": "{ %int( %map_get( %to_date_metadata( playlist_max_upload_date ), \"year_truncated\" ) ) }", - "playlist_max_upload_year_truncated_sanitized": "{ %sanitize( playlist_max_upload_year_truncated ) }", - "playlist_metadata": "{ %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) }", - "playlist_metadata_sanitized": "{ %sanitize( playlist_metadata ) }", - "playlist_title": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", title ) }", - "playlist_title_sanitized": "{ %sanitize( playlist_title ) }", - "playlist_uid": "{ %map_get_non_empty( playlist_metadata, \"playlist_id\", uid ) }", - "playlist_uid_sanitized": "{ %sanitize( playlist_uid ) }", - "playlist_uploader": "{ %map_get_non_empty( playlist_metadata, \"uploader\", uploader ) }", - "playlist_uploader_id": "{ %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", uploader_id ) }", - "playlist_uploader_id_sanitized": "{ %sanitize( playlist_uploader_id ) }", - "playlist_uploader_sanitized": "{ %sanitize( playlist_uploader ) }", - "playlist_uploader_url": "{ %map_get_non_empty( playlist_metadata, \"uploader_url\", webpage_url ) }", - "playlist_uploader_url_sanitized": "{ %sanitize( playlist_uploader_url ) }", - "playlist_webpage_url": "{ %map_get_non_empty( playlist_metadata, \"webpage_url\", webpage_url ) }", - "playlist_webpage_url_sanitized": "{ %sanitize( playlist_webpage_url ) }", - "release_date": "{ %map_get_non_empty( entry_metadata, \"release_date\", upload_date ) }", - "release_date_sanitized": "{ %sanitize( release_date ) }", - "release_date_standardized": "{ %string( %map_get( %to_date_metadata( release_date ), \"date_standardized\" ) ) }", - "release_date_standardized_sanitized": "{ %sanitize( release_date_standardized ) }", - "release_day": "{ %int( %map_get( %to_date_metadata( release_date ), \"day\" ) ) }", - "release_day_of_year": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_of_year\" ) ) }", - "release_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_of_year_padded\" ) ) }", - "release_day_of_year_padded_sanitized": "{ %sanitize( release_day_of_year_padded ) }", - "release_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_of_year_reversed\" ) ) }", - "release_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_of_year_reversed_padded\" ) ) }", - "release_day_of_year_reversed_padded_sanitized": "{ %sanitize( release_day_of_year_reversed_padded ) }", - "release_day_of_year_reversed_sanitized": "{ %sanitize( release_day_of_year_reversed ) }", - "release_day_of_year_sanitized": "{ %sanitize( release_day_of_year ) }", - "release_day_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_padded\" ) ) }", - "release_day_padded_sanitized": "{ %sanitize( release_day_padded ) }", - "release_day_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_reversed\" ) ) }", - "release_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_reversed_padded\" ) ) }", - "release_day_reversed_padded_sanitized": "{ %sanitize( release_day_reversed_padded ) }", - "release_day_reversed_sanitized": "{ %sanitize( release_day_reversed ) }", - "release_day_sanitized": "{ %sanitize( release_day ) }", - "release_month": "{ %int( %map_get( %to_date_metadata( release_date ), \"month\" ) ) }", - "release_month_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"month_padded\" ) ) }", - "release_month_padded_sanitized": "{ %sanitize( release_month_padded ) }", - "release_month_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"month_reversed\" ) ) }", - "release_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"month_reversed_padded\" ) ) }", - "release_month_reversed_padded_sanitized": "{ %sanitize( release_month_reversed_padded ) }", - "release_month_reversed_sanitized": "{ %sanitize( release_month_reversed ) }", - "release_month_sanitized": "{ %sanitize( release_month ) }", - "release_year": "{ %int( %map_get( %to_date_metadata( release_date ), \"year\" ) ) }", - "release_year_sanitized": "{ %sanitize( release_year ) }", - "release_year_truncated": "{ %int( %map_get( %to_date_metadata( release_date ), \"year_truncated\" ) ) }", - "release_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"year_truncated_reversed\" ) ) }", - "release_year_truncated_reversed_sanitized": "{ %sanitize( release_year_truncated_reversed ) }", - "release_year_truncated_sanitized": "{ %sanitize( release_year_truncated ) }", - "requested_subtitles": "{ { } }", - "requested_subtitles_sanitized": "{ %sanitize( requested_subtitles ) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_height_gte_sanitized": "361", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_ignore_titles_sanitized": "[]", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) }", - "resolution_assert_is_ignored_sanitized": "{ %sanitize( %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_assert_print_sanitized": "true", - "resolution_assert_sanitized": "{ %sanitize( %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) ) }", - "resolution_readable": "{ width }x{ height }", - "resolution_readable_sanitized": "{ %sanitize( %concat( %string( width ), \"x\", %string( height ) ) ) }", - "sibling_metadata": "{ %map_get_non_empty( entry_metadata, \"sibling_metadata\", [ ] ) }", - "sibling_metadata_sanitized": "{ %sanitize( sibling_metadata ) }", - "source_count": "{ %map_get_non_empty( playlist_metadata, \"playlist_count\", 1 ) }", - "source_count_sanitized": "{ %sanitize( source_count ) }", - "source_description": "{ %map_get_non_empty( source_metadata, \"description\", playlist_description ) }", - "source_description_sanitized": "{ %sanitize( source_description ) }", - "source_index": "{ %map_get_non_empty( playlist_metadata, \"playlist_index\", 1 ) }", - "source_index_padded": "{ %pad_zero( source_index, 2 ) }", - "source_index_padded_sanitized": "{ %sanitize( source_index_padded ) }", - "source_index_sanitized": "{ %sanitize( source_index ) }", - "source_metadata": "{ %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) }", - "source_metadata_sanitized": "{ %sanitize( source_metadata ) }", - "source_title": "{ %map_get_non_empty( source_metadata, \"title\", playlist_title ) }", - "source_title_sanitized": "{ %sanitize( source_title ) }", - "source_uid": "{ %map_get_non_empty( source_metadata, \"id\", playlist_uid ) }", - "source_uid_sanitized": "{ %sanitize( source_uid ) }", - "source_uploader": "{ %map_get_non_empty( source_metadata, \"uploader\", playlist_uploader ) }", - "source_uploader_id": "{ %map_get_non_empty( source_metadata, \"uploader_id\", playlist_uploader_id ) }", - "source_uploader_id_sanitized": "{ %sanitize( source_uploader_id ) }", - "source_uploader_sanitized": "{ %sanitize( source_uploader ) }", - "source_uploader_url": "{ %map_get_non_empty( source_metadata, \"uploader_url\", source_webpage_url ) }", - "source_uploader_url_sanitized": "{ %sanitize( source_uploader_url ) }", - "source_webpage_url": "{ %map_get_non_empty( source_metadata, \"webpage_url\", playlist_webpage_url ) }", - "source_webpage_url_sanitized": "{ %sanitize( source_webpage_url ) }", - "sponsorblock_chapters": "{ [ ] }", - "sponsorblock_chapters_sanitized": "{ %sanitize( sponsorblock_chapters ) }", - "subscription_array": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\" ] }", - "subscription_array_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\uff02]", - "subscription_has_download_archive": "{ %bool(False) }", - "subscription_has_download_archive_sanitized": "false", - "subscription_indent_1": "Pop", - "subscription_indent_1_sanitized": "Pop", - "subscription_map": "{ { } }", - "subscription_map_sanitized": "{}", - "subscription_name": "Rick Astley", - "subscription_name_sanitized": "Rick Astley", - "subscription_value": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_1": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_1_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "subscription_value_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "thumbnail_ext": "jpg", - "thumbnail_ext_sanitized": "jpg", - "title": "{ %map_get_non_empty( entry_metadata, \"title\", uid ) }", - "title_sanitized": "{ %sanitize( title ) }", - "title_sanitized_plex": "{ %sanitize_plex_episode( title ) }", - "title_sanitized_plex_sanitized": "{ %sanitize( title_sanitized_plex ) }", - "uid": "{ %map_get( entry_metadata, \"id\" ) }", - "uid_sanitized": "{ %sanitize( uid ) }", - "uid_sanitized_plex": "{ %sanitize_plex_episode( uid ) }", - "uid_sanitized_plex_sanitized": "{ %sanitize( uid_sanitized_plex ) }", - "upload_date": "{ %map_get_non_empty( entry_metadata, \"upload_date\", epoch_date ) }", - "upload_date_index": "{ %int( 1 ) }", - "upload_date_index_padded": "{ %pad_zero( upload_date_index, 2 ) }", - "upload_date_index_padded_sanitized": "{ %sanitize( upload_date_index_padded ) }", - "upload_date_index_reversed": "{ %sub( 100, upload_date_index ) }", - "upload_date_index_reversed_padded": "{ %pad_zero( upload_date_index_reversed, 2 ) }", - "upload_date_index_reversed_padded_sanitized": "{ %sanitize( upload_date_index_reversed_padded ) }", - "upload_date_index_reversed_sanitized": "{ %sanitize( upload_date_index_reversed ) }", - "upload_date_index_sanitized": "{ %sanitize( upload_date_index ) }", - "upload_date_sanitized": "{ %sanitize( upload_date ) }", - "upload_date_standardized": "{ %string( %map_get( %to_date_metadata( upload_date ), \"date_standardized\" ) ) }", - "upload_date_standardized_sanitized": "{ %sanitize( upload_date_standardized ) }", - "upload_day": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day\" ) ) }", - "upload_day_of_year": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_of_year\" ) ) }", - "upload_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_of_year_padded\" ) ) }", - "upload_day_of_year_padded_sanitized": "{ %sanitize( upload_day_of_year_padded ) }", - "upload_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_of_year_reversed\" ) ) }", - "upload_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_of_year_reversed_padded\" ) ) }", - "upload_day_of_year_reversed_padded_sanitized": "{ %sanitize( upload_day_of_year_reversed_padded ) }", - "upload_day_of_year_reversed_sanitized": "{ %sanitize( upload_day_of_year_reversed ) }", - "upload_day_of_year_sanitized": "{ %sanitize( upload_day_of_year ) }", - "upload_day_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_padded\" ) ) }", - "upload_day_padded_sanitized": "{ %sanitize( upload_day_padded ) }", - "upload_day_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_reversed\" ) ) }", - "upload_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_reversed_padded\" ) ) }", - "upload_day_reversed_padded_sanitized": "{ %sanitize( upload_day_reversed_padded ) }", - "upload_day_reversed_sanitized": "{ %sanitize( upload_day_reversed ) }", - "upload_day_sanitized": "{ %sanitize( upload_day ) }", - "upload_month": "{ %int( %map_get( %to_date_metadata( upload_date ), \"month\" ) ) }", - "upload_month_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"month_padded\" ) ) }", - "upload_month_padded_sanitized": "{ %sanitize( upload_month_padded ) }", - "upload_month_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"month_reversed\" ) ) }", - "upload_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"month_reversed_padded\" ) ) }", - "upload_month_reversed_padded_sanitized": "{ %sanitize( upload_month_reversed_padded ) }", - "upload_month_reversed_sanitized": "{ %sanitize( upload_month_reversed ) }", - "upload_month_sanitized": "{ %sanitize( upload_month ) }", - "upload_year": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year\" ) ) }", - "upload_year_sanitized": "{ %sanitize( upload_year ) }", - "upload_year_truncated": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year_truncated\" ) ) }", - "upload_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year_truncated_reversed\" ) ) }", - "upload_year_truncated_reversed_sanitized": "{ %sanitize( upload_year_truncated_reversed ) }", - "upload_year_truncated_sanitized": "{ %sanitize( upload_year_truncated ) }", - "uploader": "{ %map_get_non_empty( entry_metadata, \"uploader\", uploader_id ) }", - "uploader_id": "{ %map_get_non_empty( entry_metadata, \"uploader_id\", uid ) }", - "uploader_id_sanitized": "{ %sanitize( uploader_id ) }", - "uploader_sanitized": "{ %sanitize( uploader ) }", - "uploader_url": "{ %map_get_non_empty( entry_metadata, \"uploader_url\", webpage_url ) }", - "uploader_url_sanitized": "{ %sanitize( uploader_url ) }", - "url": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "url10": "", - "url100": "", - "url100_sanitized": "", - "url10_sanitized": "", - "url11": "", - "url11_sanitized": "", - "url12": "", - "url12_sanitized": "", - "url13": "", - "url13_sanitized": "", - "url14": "", - "url14_sanitized": "", - "url15": "", - "url15_sanitized": "", - "url16": "", - "url16_sanitized": "", - "url17": "", - "url17_sanitized": "", - "url18": "", - "url18_sanitized": "", - "url19": "", - "url19_sanitized": "", - "url2": "", - "url20": "", - "url20_sanitized": "", - "url21": "", - "url21_sanitized": "", - "url22": "", - "url22_sanitized": "", - "url23": "", - "url23_sanitized": "", - "url24": "", - "url24_sanitized": "", - "url25": "", - "url25_sanitized": "", - "url26": "", - "url26_sanitized": "", - "url27": "", - "url27_sanitized": "", - "url28": "", - "url28_sanitized": "", - "url29": "", - "url29_sanitized": "", - "url2_sanitized": "", - "url3": "", - "url30": "", - "url30_sanitized": "", - "url31": "", - "url31_sanitized": "", - "url32": "", - "url32_sanitized": "", - "url33": "", - "url33_sanitized": "", - "url34": "", - "url34_sanitized": "", - "url35": "", - "url35_sanitized": "", - "url36": "", - "url36_sanitized": "", - "url37": "", - "url37_sanitized": "", - "url38": "", - "url38_sanitized": "", - "url39": "", - "url39_sanitized": "", - "url3_sanitized": "", - "url4": "", - "url40": "", - "url40_sanitized": "", - "url41": "", - "url41_sanitized": "", - "url42": "", - "url42_sanitized": "", - "url43": "", - "url43_sanitized": "", - "url44": "", - "url44_sanitized": "", - "url45": "", - "url45_sanitized": "", - "url46": "", - "url46_sanitized": "", - "url47": "", - "url47_sanitized": "", - "url48": "", - "url48_sanitized": "", - "url49": "", - "url49_sanitized": "", - "url4_sanitized": "", - "url5": "", - "url50": "", - "url50_sanitized": "", - "url51": "", - "url51_sanitized": "", - "url52": "", - "url52_sanitized": "", - "url53": "", - "url53_sanitized": "", - "url54": "", - "url54_sanitized": "", - "url55": "", - "url55_sanitized": "", - "url56": "", - "url56_sanitized": "", - "url57": "", - "url57_sanitized": "", - "url58": "", - "url58_sanitized": "", - "url59": "", - "url59_sanitized": "", - "url5_sanitized": "", - "url6": "", - "url60": "", - "url60_sanitized": "", - "url61": "", - "url61_sanitized": "", - "url62": "", - "url62_sanitized": "", - "url63": "", - "url63_sanitized": "", - "url64": "", - "url64_sanitized": "", - "url65": "", - "url65_sanitized": "", - "url66": "", - "url66_sanitized": "", - "url67": "", - "url67_sanitized": "", - "url68": "", - "url68_sanitized": "", - "url69": "", - "url69_sanitized": "", - "url6_sanitized": "", - "url7": "", - "url70": "", - "url70_sanitized": "", - "url71": "", - "url71_sanitized": "", - "url72": "", - "url72_sanitized": "", - "url73": "", - "url73_sanitized": "", - "url74": "", - "url74_sanitized": "", - "url75": "", - "url75_sanitized": "", - "url76": "", - "url76_sanitized": "", - "url77": "", - "url77_sanitized": "", - "url78": "", - "url78_sanitized": "", - "url79": "", - "url79_sanitized": "", - "url7_sanitized": "", - "url8": "", - "url80": "", - "url80_sanitized": "", - "url81": "", - "url81_sanitized": "", - "url82": "", - "url82_sanitized": "", - "url83": "", - "url83_sanitized": "", - "url84": "", - "url84_sanitized": "", - "url85": "", - "url85_sanitized": "", - "url86": "", - "url86_sanitized": "", - "url87": "", - "url87_sanitized": "", - "url88": "", - "url88_sanitized": "", - "url89": "", - "url89_sanitized": "", - "url8_sanitized": "", - "url9": "", - "url90": "", - "url90_sanitized": "", - "url91": "", - "url91_sanitized": "", - "url92": "", - "url92_sanitized": "", - "url93": "", - "url93_sanitized": "", - "url94": "", - "url94_sanitized": "", - "url95": "", - "url95_sanitized": "", - "url96": "", - "url96_sanitized": "", - "url97": "", - "url97_sanitized": "", - "url98": "", - "url98_sanitized": "", - "url99": "", - "url99_sanitized": "", - "url9_sanitized": "", - "url_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc", - "urls": "{ [ \"https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\", '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' ] }", - "urls_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8playlist\uff1flist=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02, \uff02\uff02]", - "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "webpage_url_sanitized": "{ %sanitize( webpage_url ) }", - "width": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }", - "width_sanitized": "{ %sanitize( width ) }", - "ytdl_sub_entry_date_eval": "{ %string( upload_date_standardized ) }", - "ytdl_sub_entry_date_eval_sanitized": "{ %sanitize( ytdl_sub_entry_date_eval ) }", - "ytdl_sub_input_url": "{ %string( '' ) }", - "ytdl_sub_input_url_count": "{ %int( 0 ) }", - "ytdl_sub_input_url_count_sanitized": "{ %sanitize( ytdl_sub_input_url_count ) }", - "ytdl_sub_input_url_index": "{ %int( -1 ) }", - "ytdl_sub_input_url_index_sanitized": "{ %sanitize( ytdl_sub_input_url_index ) }", - "ytdl_sub_input_url_sanitized": "{ %sanitize( ytdl_sub_input_url ) }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_full_overrides.json b/tests/resources/expected_json/tv_show/inspect_full_overrides.json deleted file mode 100644 index 179d4e11..00000000 --- a/tests/resources/expected_json/tv_show/inspect_full_overrides.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "assert_not_collection": "{ %bool(True) }", - "avatar_uncropped_thumbnail_file_name": "poster.jpg", - "banner_uncropped_thumbnail_file_name": "fanart.jpg", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "episode_content_rating": "TV-14", - "episode_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "episode_file_name": "s{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.e{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) } - { %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "episode_file_path": "{ %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) }/{ %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "episode_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) }{ upload_date_index_padded }", - "episode_number_and_padded_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ), 6 ] }", - "episode_number_padded": "{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) }", - "episode_plot": "{ %map_get( entry_metadata, \"webpage_url\" ) }\n\n{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "episode_title": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) } - { %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "episode_year": "{ %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) }", - "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "include_sibling_metadata": "{ %bool(False) }", - "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "only_recent_date_range": "2months", - "only_recent_max_files": "{ %int(30) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_readable": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }x{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "s01_name": "", - "s01_url": "", - "season_directory_name": "Season { %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "season_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "season_number_padded": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "season_poster_file_name": "{ %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) }/Season{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.jpg", - "subscription_array": "{ [ \"https://www.youtube.com/@novapbs\" ] }", - "subscription_indent_1": "Documentaries", - "subscription_indent_2": "TV-14", - "subscription_value": "https://www.youtube.com/@novapbs", - "subscription_value_1": "https://www.youtube.com/@novapbs", - "thumbnail_file_name": "{ %concat( %string( %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ) ) }-thumb.jpg", - "tv_show_by_date_episode_ordering": "upload-month-day", - "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ), 6 ] }", - "tv_show_by_date_season_ordering": "upload-year", - "tv_show_content_rating": "TV-14", - "tv_show_content_rating_default": "TV-14", - "tv_show_date_range_type": "upload_date", - "tv_show_directory": "tv_show_directory_path", - "tv_show_fanart_file_name": "fanart.jpg", - "tv_show_genre": "Documentaries", - "tv_show_genre_default": "ytdl-sub", - "tv_show_name": "NOVA PBS", - "tv_show_poster_file_name": "poster.jpg", - "url": "https://www.youtube.com/@novapbs", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ [ \"https://www.youtube.com/@novapbs\" ] }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_full_variables.json b/tests/resources/expected_json/tv_show/inspect_full_variables.json deleted file mode 100644 index bfb834bf..00000000 --- a/tests/resources/expected_json/tv_show/inspect_full_variables.json +++ /dev/null @@ -1,520 +0,0 @@ -{ - "assert_not_collection": "{ %bool(True) }", - "assert_not_collection_sanitized": "true", - "avatar_uncropped_thumbnail_file_name": "poster.jpg", - "avatar_uncropped_thumbnail_file_name_sanitized": "poster.jpg", - "banner_uncropped_thumbnail_file_name": "fanart.jpg", - "banner_uncropped_thumbnail_file_name_sanitized": "fanart.jpg", - "channel": "{ %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "channel_id": "{ %map_get_non_empty( entry_metadata, \"channel_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "channel_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"channel_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "channel_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "chapters": "{ [ ] }", - "chapters_sanitized": "[]", - "comments": "{ [ ] }", - "comments_sanitized": "[]", - "creator": "{ %map_get_non_empty( entry_metadata, \"creator\", %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "creator_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"creator\", %map_get_non_empty( entry_metadata, \"channel\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "description": "{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "description_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"description\", '' ) ) }", - "download_index": "{ %int(1) }", - "download_index_padded6": "000001", - "download_index_padded6_sanitized": "000001", - "download_index_sanitized": "1", - "duration": "{ %map_get_non_empty( entry_metadata, \"duration\", 0 ) }", - "duration_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"duration\", 0 ) ) }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_bilateral_scraping_sanitized": "true", - "enable_resolution_assert": "{ %bool(True) }", - "enable_resolution_assert_sanitized": "true", - "enable_throttle_protection": "{ %bool(True) }", - "enable_throttle_protection_sanitized": "true", - "entry_metadata": "{ { } }", - "entry_metadata_sanitized": "{ %sanitize( entry_metadata ) }", - "episode_content_rating": "TV-14", - "episode_content_rating_sanitized": "TV-14", - "episode_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "episode_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "episode_file_name": "s{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.e{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) } - { %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "episode_file_name_sanitized": "{ %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "episode_file_path": "{ %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) }/{ %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "episode_file_path_sanitized": "{ %sanitize( %concat( %string( %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ) ) ) }", - "episode_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) }{ upload_date_index_padded }", - "episode_number_and_padded_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ), 6 ] }", - "episode_number_and_padded__sanitized": "{ %sanitize( [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ), 6 ] ) }", - "episode_number_padded": "{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) }", - "episode_number_padded_sanitized": "{ %sanitize( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ) }", - "episode_number_sanitized": "{ %sanitize( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ) }", - "episode_plot": "{ %map_get( entry_metadata, \"webpage_url\" ) }\n\n{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "episode_plot_sanitized": "{ %sanitize( %concat( %string( %map_get( entry_metadata, \"webpage_url\" ) ), \"\n\n\", %string( %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) ) }", - "episode_title": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) } - { %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "episode_title_sanitized": "{ %sanitize( %concat( %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ), \" - \", %string( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "episode_year": "{ %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) }", - "episode_year_sanitized": "{ %sanitize( %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) ) }", - "epoch": "{ %map_get( entry_metadata, \"epoch\" ) }", - "epoch_date": "{ %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) }", - "epoch_date_sanitized": "{ %sanitize( %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) }", - "epoch_hour": "{ %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%H\" ) }", - "epoch_hour_sanitized": "{ %sanitize( %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%H\" ) ) }", - "epoch_sanitized": "{ %sanitize( %map_get( entry_metadata, \"epoch\" ) ) }", - "ext": "{ %throw( \"Plugin variable ext has not been created yet\" ) }", - "ext_sanitized": "{ %sanitize( ext ) }", - "extractor": "{ %map_get( entry_metadata, \"extractor\" ) }", - "extractor_key": "{ %map_get( entry_metadata, \"extractor_key\" ) }", - "extractor_key_sanitized": "{ %sanitize( %map_get( entry_metadata, \"extractor_key\" ) ) }", - "extractor_sanitized": "{ %sanitize( %map_get( entry_metadata, \"extractor\" ) ) }", - "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "file_title_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "file_uid_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) ) }", - "height": "{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "height_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"height\", 0 ) ) }", - "ie_key": "{ %map_get_non_empty( entry_metadata, \"ie_key\", %map_get( entry_metadata, \"extractor_key\" ) ) }", - "ie_key_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"ie_key\", %map_get( entry_metadata, \"extractor_key\" ) ) ) }", - "include_sibling_metadata": "{ %bool(False) }", - "include_sibling_metadata_sanitized": "false", - "info_json_ext": "info.json", - "info_json_ext_sanitized": "info.json", - "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "modified_webpage_url_sanitized": "{ %sanitize( %map_get( entry_metadata, \"webpage_url\" ) ) }", - "only_recent_date_range": "2months", - "only_recent_date_range_sanitized": "2months", - "only_recent_max_files": "{ %int(30) }", - "only_recent_max_files_sanitized": "30", - "playlist_count": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "playlist_count_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) ) }", - "playlist_description": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) }", - "playlist_description_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) }", - "playlist_index": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "playlist_index_padded": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) }", - "playlist_index_padded6": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 6 ) }", - "playlist_index_padded6_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 6 ) ) }", - "playlist_index_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) ) }", - "playlist_index_reversed": "{ %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ) }", - "playlist_index_reversed_padded": "{ %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 2 ) }", - "playlist_index_reversed_padded6": "{ %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 6 ) }", - "playlist_index_reversed_padded6_sanitized": "{ %sanitize( %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 6 ) ) }", - "playlist_index_reversed_padded_sanitized": "{ %sanitize( %pad_zero( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ), 2 ) ) }", - "playlist_index_reversed_sanitized": "{ %sanitize( %sub( %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ), %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), -1 ) ) }", - "playlist_index_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) ) }", - "playlist_max_upload_date": "{ %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) }", - "playlist_max_upload_date_sanitized": "{ %sanitize( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ) }", - "playlist_max_upload_year": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }", - "playlist_max_upload_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) ) }", - "playlist_max_upload_year_truncated": "{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year_truncated\" ) ) }", - "playlist_max_upload_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year_truncated\" ) ) ) }", - "playlist_metadata": "{ %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) }", - "playlist_metadata_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) ) }", - "playlist_title": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uid": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) }", - "playlist_uid_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_uploader": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uploader_id": "{ %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "playlist_uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "playlist_uploader_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "playlist_uploader_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "playlist_uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "playlist_webpage_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "playlist_webpage_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "release_date": "{ %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) }", - "release_date_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ) }", - "release_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"date_standardized\" ) ) }", - "release_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"date_standardized\" ) ) ) }", - "release_day": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day\" ) ) }", - "release_day_of_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year\" ) ) }", - "release_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_padded\" ) ) }", - "release_day_of_year_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_padded\" ) ) ) }", - "release_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed\" ) ) }", - "release_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed_padded\" ) ) }", - "release_day_of_year_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed_padded\" ) ) ) }", - "release_day_of_year_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year_reversed\" ) ) ) }", - "release_day_of_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_of_year\" ) ) ) }", - "release_day_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_padded\" ) ) }", - "release_day_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_padded\" ) ) ) }", - "release_day_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed\" ) ) }", - "release_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed_padded\" ) ) }", - "release_day_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed_padded\" ) ) ) }", - "release_day_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day_reversed\" ) ) ) }", - "release_day_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"day\" ) ) ) }", - "release_month": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month\" ) ) }", - "release_month_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_padded\" ) ) }", - "release_month_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_padded\" ) ) ) }", - "release_month_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed\" ) ) }", - "release_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed_padded\" ) ) }", - "release_month_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed_padded\" ) ) ) }", - "release_month_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month_reversed\" ) ) ) }", - "release_month_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"month\" ) ) ) }", - "release_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year\" ) ) }", - "release_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year\" ) ) ) }", - "release_year_truncated": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated\" ) ) }", - "release_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated_reversed\" ) ) }", - "release_year_truncated_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated_reversed\" ) ) ) }", - "release_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"release_date\", %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) ), \"year_truncated\" ) ) ) }", - "requested_subtitles": "{ { } }", - "requested_subtitles_sanitized": "{}", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_height_gte_sanitized": "361", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_ignore_titles_sanitized": "[]", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) }", - "resolution_assert_is_ignored_sanitized": "{ %sanitize( %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), resolution_assert_ignore_titles ) ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_assert_print_sanitized": "true", - "resolution_assert_sanitized": "{ %sanitize( %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) ) }", - "resolution_readable": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }x{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "resolution_readable_sanitized": "{ %sanitize( %concat( %string( %map_get_non_empty( entry_metadata, \"width\", 0 ) ), \"x\", %string( %map_get_non_empty( entry_metadata, \"height\", 0 ) ) ) ) }", - "s01_name": "", - "s01_name_sanitized": "", - "s01_url": "", - "s01_url_sanitized": "", - "season_directory_name": "Season { %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "season_directory_name_sanitized": "{ %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) }", - "season_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "season_number_padded": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "season_number_padded_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) }", - "season_number_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) }", - "season_poster_file_name": "{ %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) }/Season{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.jpg", - "season_poster_file_name_sanitized": "{ %sanitize( %concat( %string( %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) ), \"/Season\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".jpg\" ) ) }", - "sibling_metadata": "{ %map_get_non_empty( entry_metadata, \"sibling_metadata\", [ ] ) }", - "sibling_metadata_sanitized": "{ %sanitize( sibling_metadata ) }", - "source_count": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_count\", 1 ) }", - "source_count_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_count\", 1 ) ) }", - "source_description": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"description\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) }", - "source_description_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"description\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"description\", %map_get_non_empty( entry_metadata, \"description\", '' ) ) ) ) }", - "source_index": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ) }", - "source_index_padded": "{ %pad_zero( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ), 2 ) }", - "source_index_padded_sanitized": "{ %sanitize( %pad_zero( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ), 2 ) ) }", - "source_index_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_index\", 1 ) ) }", - "source_metadata": "{ %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) }", - "source_metadata_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) ) }", - "source_title": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"title\", %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_title_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"title\", %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uid": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"id\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "source_uid_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"id\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"playlist_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_uploader": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uploader_id": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_id\", %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "source_uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_id\", %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", - "source_uploader_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"uploader\", %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }", - "source_uploader_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) }", - "source_uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"uploader_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) ) }", - "source_webpage_url": "{ %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "source_webpage_url_sanitized": "{ %sanitize( %map_get_non_empty( %map_get_non_empty( entry_metadata, \"source_metadata\", { } ), \"webpage_url\", %map_get_non_empty( %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ), \"webpage_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) ) }", - "sponsorblock_chapters": "{ [ ] }", - "sponsorblock_chapters_sanitized": "[]", - "subscription_array": "{ [ \"https://www.youtube.com/@novapbs\" ] }", - "subscription_array_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs\uff02]", - "subscription_has_download_archive": "{ %bool(False) }", - "subscription_has_download_archive_sanitized": "false", - "subscription_indent_1": "Documentaries", - "subscription_indent_1_sanitized": "Documentaries", - "subscription_indent_2": "TV-14", - "subscription_indent_2_sanitized": "TV-14", - "subscription_name": "NOVA PBS", - "subscription_name_sanitized": "NOVA PBS", - "subscription_value": "https://www.youtube.com/@novapbs", - "subscription_value_1": "https://www.youtube.com/@novapbs", - "subscription_value_1_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs", - "subscription_value_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs", - "thumbnail_ext": "jpg", - "thumbnail_ext_sanitized": "jpg", - "thumbnail_file_name": "{ %concat( %string( %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ) ) }-thumb.jpg", - "thumbnail_file_name_sanitized": "{ %sanitize( %concat( %string( %concat( %string( %sanitize( %concat( \"Season \", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ), \".e\", %string( %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ) ), 6 ) ), \" - \", %string( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) ) ) ), \"-thumb.jpg\" ) ) }", - "title": "{ %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", - "title_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "title_sanitized_plex": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", - "title_sanitized_plex_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "tv_show_by_date_episode_ordering": "upload-month-day", - "tv_show_by_date_episode_ordering_sanitized": "upload-month-day", - "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ), 6 ] }", - "tv_show_by_date_ordering_pair_validation__sanitized": "{ %sanitize( [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), upload_date_index_padded ), 6 ] ) }", - "tv_show_by_date_season_ordering": "upload-year", - "tv_show_by_date_season_ordering_sanitized": "upload-year", - "tv_show_content_rating": "TV-14", - "tv_show_content_rating_default": "TV-14", - "tv_show_content_rating_default_sanitized": "TV-14", - "tv_show_content_rating_sanitized": "TV-14", - "tv_show_date_range_type": "upload_date", - "tv_show_date_range_type_sanitized": "upload_date", - "tv_show_directory": "tv_show_directory_path", - "tv_show_directory_sanitized": "tv_show_directory_path", - "tv_show_fanart_file_name": "fanart.jpg", - "tv_show_fanart_file_name_sanitized": "fanart.jpg", - "tv_show_genre": "Documentaries", - "tv_show_genre_default": "ytdl-sub", - "tv_show_genre_default_sanitized": "ytdl-sub", - "tv_show_genre_sanitized": "Documentaries", - "tv_show_name": "NOVA PBS", - "tv_show_name_sanitized": "NOVA PBS", - "tv_show_poster_file_name": "poster.jpg", - "tv_show_poster_file_name_sanitized": "poster.jpg", - "uid": "{ %map_get( entry_metadata, \"id\" ) }", - "uid_sanitized": "{ %sanitize( %map_get( entry_metadata, \"id\" ) ) }", - "uid_sanitized_plex": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", - "uid_sanitized_plex_sanitized": "{ %sanitize( %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) ) }", - "upload_date": "{ %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) }", - "upload_date_index": "{ %int(1) }", - "upload_date_index_padded": "01", - "upload_date_index_padded_sanitized": "01", - "upload_date_index_reversed": "{ %int(99) }", - "upload_date_index_reversed_padded": "99", - "upload_date_index_reversed_padded_sanitized": "99", - "upload_date_index_reversed_sanitized": "99", - "upload_date_index_sanitized": "1", - "upload_date_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ) }", - "upload_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", - "upload_date_standardized_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "upload_day": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day\" ) ) }", - "upload_day_of_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year\" ) ) }", - "upload_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_padded\" ) ) }", - "upload_day_of_year_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_padded\" ) ) ) }", - "upload_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed\" ) ) }", - "upload_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed_padded\" ) ) }", - "upload_day_of_year_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed_padded\" ) ) ) }", - "upload_day_of_year_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year_reversed\" ) ) ) }", - "upload_day_of_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_of_year\" ) ) ) }", - "upload_day_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ) }", - "upload_day_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ) ) }", - "upload_day_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed\" ) ) }", - "upload_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed_padded\" ) ) }", - "upload_day_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed_padded\" ) ) ) }", - "upload_day_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_reversed\" ) ) ) }", - "upload_day_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day\" ) ) ) }", - "upload_month": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }", - "upload_month_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_padded\" ) ) }", - "upload_month_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_padded\" ) ) ) }", - "upload_month_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed\" ) ) }", - "upload_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed_padded\" ) ) }", - "upload_month_reversed_padded_sanitized": "{ %sanitize( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed_padded\" ) ) ) }", - "upload_month_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month_reversed\" ) ) ) }", - "upload_month_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) ) }", - "upload_year": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", - "upload_year_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) }", - "upload_year_truncated": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated\" ) ) }", - "upload_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated_reversed\" ) ) }", - "upload_year_truncated_reversed_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated_reversed\" ) ) ) }", - "upload_year_truncated_sanitized": "{ %sanitize( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year_truncated\" ) ) ) }", - "uploader": "{ %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "uploader_id": "{ %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) }", - "uploader_id_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) }", - "uploader_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader\", %map_get_non_empty( entry_metadata, \"uploader_id\", %map_get( entry_metadata, \"id\" ) ) ) ) }", - "uploader_url": "{ %map_get_non_empty( entry_metadata, \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) }", - "uploader_url_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"uploader_url\", %map_get( entry_metadata, \"webpage_url\" ) ) ) }", - "url": "https://www.youtube.com/@novapbs", - "url10": "", - "url100": "", - "url100_sanitized": "", - "url10_sanitized": "", - "url11": "", - "url11_sanitized": "", - "url12": "", - "url12_sanitized": "", - "url13": "", - "url13_sanitized": "", - "url14": "", - "url14_sanitized": "", - "url15": "", - "url15_sanitized": "", - "url16": "", - "url16_sanitized": "", - "url17": "", - "url17_sanitized": "", - "url18": "", - "url18_sanitized": "", - "url19": "", - "url19_sanitized": "", - "url2": "", - "url20": "", - "url20_sanitized": "", - "url21": "", - "url21_sanitized": "", - "url22": "", - "url22_sanitized": "", - "url23": "", - "url23_sanitized": "", - "url24": "", - "url24_sanitized": "", - "url25": "", - "url25_sanitized": "", - "url26": "", - "url26_sanitized": "", - "url27": "", - "url27_sanitized": "", - "url28": "", - "url28_sanitized": "", - "url29": "", - "url29_sanitized": "", - "url2_sanitized": "", - "url3": "", - "url30": "", - "url30_sanitized": "", - "url31": "", - "url31_sanitized": "", - "url32": "", - "url32_sanitized": "", - "url33": "", - "url33_sanitized": "", - "url34": "", - "url34_sanitized": "", - "url35": "", - "url35_sanitized": "", - "url36": "", - "url36_sanitized": "", - "url37": "", - "url37_sanitized": "", - "url38": "", - "url38_sanitized": "", - "url39": "", - "url39_sanitized": "", - "url3_sanitized": "", - "url4": "", - "url40": "", - "url40_sanitized": "", - "url41": "", - "url41_sanitized": "", - "url42": "", - "url42_sanitized": "", - "url43": "", - "url43_sanitized": "", - "url44": "", - "url44_sanitized": "", - "url45": "", - "url45_sanitized": "", - "url46": "", - "url46_sanitized": "", - "url47": "", - "url47_sanitized": "", - "url48": "", - "url48_sanitized": "", - "url49": "", - "url49_sanitized": "", - "url4_sanitized": "", - "url5": "", - "url50": "", - "url50_sanitized": "", - "url51": "", - "url51_sanitized": "", - "url52": "", - "url52_sanitized": "", - "url53": "", - "url53_sanitized": "", - "url54": "", - "url54_sanitized": "", - "url55": "", - "url55_sanitized": "", - "url56": "", - "url56_sanitized": "", - "url57": "", - "url57_sanitized": "", - "url58": "", - "url58_sanitized": "", - "url59": "", - "url59_sanitized": "", - "url5_sanitized": "", - "url6": "", - "url60": "", - "url60_sanitized": "", - "url61": "", - "url61_sanitized": "", - "url62": "", - "url62_sanitized": "", - "url63": "", - "url63_sanitized": "", - "url64": "", - "url64_sanitized": "", - "url65": "", - "url65_sanitized": "", - "url66": "", - "url66_sanitized": "", - "url67": "", - "url67_sanitized": "", - "url68": "", - "url68_sanitized": "", - "url69": "", - "url69_sanitized": "", - "url6_sanitized": "", - "url7": "", - "url70": "", - "url70_sanitized": "", - "url71": "", - "url71_sanitized": "", - "url72": "", - "url72_sanitized": "", - "url73": "", - "url73_sanitized": "", - "url74": "", - "url74_sanitized": "", - "url75": "", - "url75_sanitized": "", - "url76": "", - "url76_sanitized": "", - "url77": "", - "url77_sanitized": "", - "url78": "", - "url78_sanitized": "", - "url79": "", - "url79_sanitized": "", - "url7_sanitized": "", - "url8": "", - "url80": "", - "url80_sanitized": "", - "url81": "", - "url81_sanitized": "", - "url82": "", - "url82_sanitized": "", - "url83": "", - "url83_sanitized": "", - "url84": "", - "url84_sanitized": "", - "url85": "", - "url85_sanitized": "", - "url86": "", - "url86_sanitized": "", - "url87": "", - "url87_sanitized": "", - "url88": "", - "url88_sanitized": "", - "url89": "", - "url89_sanitized": "", - "url8_sanitized": "", - "url9": "", - "url90": "", - "url90_sanitized": "", - "url91": "", - "url91_sanitized": "", - "url92": "", - "url92_sanitized": "", - "url93": "", - "url93_sanitized": "", - "url94": "", - "url94_sanitized": "", - "url95": "", - "url95_sanitized": "", - "url96": "", - "url96_sanitized": "", - "url97": "", - "url97_sanitized": "", - "url98": "", - "url98_sanitized": "", - "url99": "", - "url99_sanitized": "", - "url9_sanitized": "", - "url_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs", - "urls": "{ [ \"https://www.youtube.com/@novapbs\" ] }", - "urls_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs\uff02]", - "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "webpage_url_sanitized": "{ %sanitize( %map_get( entry_metadata, \"webpage_url\" ) ) }", - "width": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }", - "width_sanitized": "{ %sanitize( %map_get_non_empty( entry_metadata, \"width\", 0 ) ) }", - "ytdl_sub_chapters_from_comments": "{ %throw( \"Plugin variable ytdl_sub_chapters_from_comments has not been created yet\" ) }", - "ytdl_sub_chapters_from_comments_sanitized": "{ %sanitize( ytdl_sub_chapters_from_comments ) }", - "ytdl_sub_entry_date_eval": "{ %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) }", - "ytdl_sub_entry_date_eval_sanitized": "{ %sanitize( %string( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) ) ) }", - "ytdl_sub_input_url": "", - "ytdl_sub_input_url_count": "{ %int(0) }", - "ytdl_sub_input_url_count_sanitized": "0", - "ytdl_sub_input_url_index": "{ %int(-1) }", - "ytdl_sub_input_url_index_sanitized": "-1", - "ytdl_sub_input_url_sanitized": "" -} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_overrides.json b/tests/resources/expected_json/tv_show/inspect_overrides.json deleted file mode 100644 index faf71281..00000000 --- a/tests/resources/expected_json/tv_show/inspect_overrides.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "assert_not_collection": "{ %bool(True) }", - "avatar_uncropped_thumbnail_file_name": "poster.jpg", - "banner_uncropped_thumbnail_file_name": "fanart.jpg", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_resolution_assert": "{ %bool(True) }", - "enable_throttle_protection": "{ %bool(True) }", - "episode_content_rating": "TV-14", - "episode_date_standardized": "{ upload_date_standardized }", - "episode_file_name": "s{ upload_year }.e{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) } - { title_sanitized_plex }", - "episode_file_path": "{ %sanitize( %concat( \"Season \", %string( upload_year ) ) ) }/{ %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) }", - "episode_number": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", - "episode_number_and_padded_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", - "episode_number_padded": "{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) }", - "episode_plot": "{ webpage_url }\n\n{ description }", - "episode_title": "{ upload_date_standardized } - { title }", - "episode_year": "{ %slice( upload_date_standardized, 0, 4 ) }", - "file_title": "{ title_sanitized_plex }", - "file_uid": "{ uid_sanitized_plex }", - "include_sibling_metadata": "{ %bool(False) }", - "modified_webpage_url": "{ webpage_url }", - "only_recent_date_range": "2months", - "only_recent_max_files": "{ %int(30) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_readable": "{ width }x{ height }", - "s01_name": "", - "s01_url": "", - "season_directory_name": "Season { upload_year }", - "season_number": "{ upload_year }", - "season_number_padded": "{ upload_year }", - "season_poster_file_name": "{ %sanitize( %concat( \"Season \", %string( upload_year ) ) ) }/Season{ upload_year }.jpg", - "subscription_array": "{ [ \"https://www.youtube.com/@novapbs\" ] }", - "subscription_indent_1": "Documentaries", - "subscription_indent_2": "TV-14", - "subscription_value": "https://www.youtube.com/@novapbs", - "subscription_value_1": "https://www.youtube.com/@novapbs", - "thumbnail_file_name": "{ %concat( %string( %sanitize( %concat( \"Season \", %string( upload_year ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) ) ) }-thumb.jpg", - "tv_show_by_date_episode_ordering": "upload-month-day", - "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", - "tv_show_by_date_season_ordering": "upload-year", - "tv_show_content_rating": "TV-14", - "tv_show_content_rating_default": "TV-14", - "tv_show_date_range_type": "upload_date", - "tv_show_directory": "tv_show_directory_path", - "tv_show_fanart_file_name": "fanart.jpg", - "tv_show_genre": "Documentaries", - "tv_show_genre_default": "ytdl-sub", - "tv_show_name": "NOVA PBS", - "tv_show_poster_file_name": "poster.jpg", - "url": "https://www.youtube.com/@novapbs", - "url10": "", - "url100": "", - "url11": "", - "url12": "", - "url13": "", - "url14": "", - "url15": "", - "url16": "", - "url17": "", - "url18": "", - "url19": "", - "url2": "", - "url20": "", - "url21": "", - "url22": "", - "url23": "", - "url24": "", - "url25": "", - "url26": "", - "url27": "", - "url28": "", - "url29": "", - "url3": "", - "url30": "", - "url31": "", - "url32": "", - "url33": "", - "url34": "", - "url35": "", - "url36": "", - "url37": "", - "url38": "", - "url39": "", - "url4": "", - "url40": "", - "url41": "", - "url42": "", - "url43": "", - "url44": "", - "url45": "", - "url46": "", - "url47": "", - "url48": "", - "url49": "", - "url5": "", - "url50": "", - "url51": "", - "url52": "", - "url53": "", - "url54": "", - "url55": "", - "url56": "", - "url57": "", - "url58": "", - "url59": "", - "url6": "", - "url60": "", - "url61": "", - "url62": "", - "url63": "", - "url64": "", - "url65": "", - "url66": "", - "url67": "", - "url68": "", - "url69": "", - "url7": "", - "url70": "", - "url71": "", - "url72": "", - "url73": "", - "url74": "", - "url75": "", - "url76": "", - "url77": "", - "url78": "", - "url79": "", - "url8": "", - "url80": "", - "url81": "", - "url82": "", - "url83": "", - "url84": "", - "url85": "", - "url86": "", - "url87": "", - "url88": "", - "url89": "", - "url9": "", - "url90": "", - "url91": "", - "url92": "", - "url93": "", - "url94": "", - "url95": "", - "url96": "", - "url97": "", - "url98": "", - "url99": "", - "urls": "{ [ \"https://www.youtube.com/@novapbs\" ] }" -} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_fill.json b/tests/resources/expected_json/tv_show/inspect_sub_fill.json new file mode 100644 index 00000000..6d1a8662 --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_fill.json @@ -0,0 +1,250 @@ +{ + "chapters": { + "allow_chapters_from_comments": false, + "embed_chapters": true, + "enable": true, + "force_key_frames": false + }, + "date_range": { + "breaks": true, + "enable": true, + "type": "upload_date" + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/@novapbs" + ], + "variables": {}, + "webpage_url": "{ modified_webpage_url }", + "ytdl_options": {} + } + ], + "file_convert": { + "convert_to": "mp4", + "convert_with": "yt-dlp", + "enable": true + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", + "file_name": "{ episode_file_path }.{ ext }", + "info_json_name": "{ episode_file_path }.{ info_json_ext }", + "keep_files_date_eval": "{ episode_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpc_p6gjjw/NOVA PBS", + "preserve_mtime": false, + "thumbnail_name": "{ thumbnail_file_name }" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", + "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", + "assert_not_collection": true, + "avatar_uncropped_thumbnail_file_name": "poster.jpg", + "banner_uncropped_thumbnail_file_name": "fanart.jpg", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "TV-14", + "episode_date_standardized": "{ upload_date_standardized }", + "episode_file_name": "s{ upload_year }.e{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) } - { title_sanitized_plex }", + "episode_file_path": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/{ %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) }", + "episode_number": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", + "episode_number_and_padded_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "episode_number_padded": "{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) }", + "episode_plot": "{ webpage_url }\n\n{ description }", + "episode_title": "{ upload_date_standardized } - { title }", + "episode_year": "{ %slice( upload_date_standardized, 0, 4 ) }", + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season { upload_year }", + "season_number": "{ upload_year }", + "season_number_padded": "{ upload_year }", + "season_poster_file_name": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/Season{ upload_year }.jpg", + "subscription_array": [ + "https://www.youtube.com/@novapbs" + ], + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "TV-14", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "TV-14", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "upload_date", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpc_p6gjjw", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "Documentaries", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "NOVA PBS", + "tv_show_poster_file_name": "poster.jpg", + "url": "https://www.youtube.com/@novapbs", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/@novapbs" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "TV-14", + "date": "{ episode_date_standardized }", + "episode_id": "{ episode_number }", + "genre": "Documentaries", + "show": "NOVA PBS", + "synopsis": "{ episode_plot }", + "title": "{ episode_title }", + "year": "{ episode_year }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_internal.json b/tests/resources/expected_json/tv_show/inspect_sub_internal.json new file mode 100644 index 00000000..3f1c1eaf --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_internal.json @@ -0,0 +1,250 @@ +{ + "chapters": { + "allow_chapters_from_comments": false, + "embed_chapters": true, + "enable": true, + "force_key_frames": false + }, + "date_range": { + "breaks": true, + "enable": true, + "type": "upload_date" + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/@novapbs" + ], + "variables": {}, + "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "ytdl_options": {} + } + ], + "file_convert": { + "convert_to": "mp4", + "convert_with": "yt-dlp", + "enable": true + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", + "file_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }.{ ext }", + "info_json_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }.info.json", + "keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpps6shcpt/NOVA PBS", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }-thumb.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", + "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", + "assert_not_collection": true, + "avatar_uncropped_thumbnail_file_name": "poster.jpg", + "banner_uncropped_thumbnail_file_name": "fanart.jpg", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "TV-14", + "episode_date_standardized": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "episode_file_name": "s{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.e{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ) } - { %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "episode_file_path": "{ %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) }/{ %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }", + "episode_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) }{ %pad_zero( upload_date_index, 2 ) }", + "episode_number_and_padded_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ), 6 ] }", + "episode_number_padded": "{ %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ) }", + "episode_plot": "{ %map_get( entry_metadata, \"webpage_url\" ) }\n\n{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", + "episode_title": "{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) } - { %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", + "episode_year": "{ %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) }", + "file_title": "{ %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }", + "file_uid": "{ %sanitize_plex_episode( %map_get( entry_metadata, \"id\" ) ) }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ), [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season { %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", + "season_number": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", + "season_number_padded": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }", + "season_poster_file_name": "{ %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ) }/Season{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) }.jpg", + "subscription_array": [ + "https://www.youtube.com/@novapbs" + ], + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "TV-14", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{ %concat( %sanitize( %concat( \"Season \", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ) ) ), \"/\", %sanitize( %concat( \"s\", %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"year\" ) ), \".e\", %pad_zero( %int( %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ) ), 6 ), \" - \", %sanitize_plex_episode( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) ) }-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ), %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) ), %pad_zero( upload_date_index, 2 ) ), 6 ] }", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "TV-14", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "upload_date", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpps6shcpt", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "Documentaries", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "NOVA PBS", + "tv_show_poster_file_name": "poster.jpg", + "url": "https://www.youtube.com/@novapbs", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/@novapbs" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "TV-14", + "date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }", + "episode_id": "{ %int( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"month\" ) ) }{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"day_padded\" ) }{ %pad_zero( upload_date_index, 2 ) }", + "genre": "Documentaries", + "show": "NOVA PBS", + "synopsis": "{ %map_get( entry_metadata, \"webpage_url\" ) }\n\n{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", + "title": "{ %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) } - { %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) }", + "year": "{ %slice( %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ), 0, 4 ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_original.json b/tests/resources/expected_json/tv_show/inspect_sub_original.json new file mode 100644 index 00000000..0a7540d3 --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_original.json @@ -0,0 +1,255 @@ +{ + "chapters": { + "embed_chapters": true + }, + "date_range": { + "type": "{tv_show_date_range_type}" + }, + "download": [ + { + "download_reverse": false, + "url": "{ %array_apply(urls, %bilateral_url) }", + "webpage_url": "{modified_webpage_url}", + "ytdl_options": { + "playlist_items": "-1:0:-1" + } + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "playlist_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "{avatar_uncropped_thumbnail_file_name}", + "uid": "avatar_uncropped" + }, + { + "name": "{banner_uncropped_thumbnail_file_name}", + "uid": "banner_uncropped" + } + ], + "url": "{ %array_at(urls, 0) }", + "webpage_url": "{modified_webpage_url}" + }, + { + "include_sibling_metadata": "{include_sibling_metadata}", + "url": "{ %array_slice(urls, 1) }", + "webpage_url": "{modified_webpage_url}" + } + ], + "file_convert": { + "convert_to": "mp4" + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "file_name": "{episode_file_path}.{ext}", + "info_json_name": "{episode_file_path}.{info_json_ext}", + "keep_files_date_eval": "{episode_date_standardized}", + "maintain_download_archive": true, + "output_directory": "{tv_show_directory}/{tv_show_name_sanitized}", + "thumbnail_name": "{thumbnail_file_name}" + }, + "overrides": { + "%bilateral_url": "{ \n %if(\n %and(\n enable_bilateral_scraping,\n subscription_has_download_archive,\n %is_bilateral_url($0)\n ),\n $0,\n \"\"\n )\n}", + "%episode_ordering_": "{ %eq( %lower(tv_show_by_date_episode_ordering), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{\n %eq([tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering], [$0, $1])\n}", + "%season_ordering_": "{ %eq( %lower(tv_show_by_date_season_ordering), $0 ) }", + "assert_not_collection": "{\n %assert(\n %and(\n %not( %bool(s01_url) ),\n %not( %bool(s01_name) )\n ),\n \"Provided `s01_url` or `s01_name` variable to TV Show by Date preset when it expects `url`. Perhaps you meant to use the `TV Show Collection` preset?\"\n )\n}", + "avatar_uncropped_thumbnail_file_name": "{tv_show_poster_file_name}", + "banner_uncropped_thumbnail_file_name": "{tv_show_fanart_file_name}", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "{tv_show_content_rating}", + "episode_date_standardized": "{\n %if(\n %contains(tv_show_by_date_season_ordering, \"release\"),\n release_date_standardized,\n upload_date_standardized\n )\n}", + "episode_file_name": "s{season_number_padded}.e{episode_number_padded} - {file_title}", + "episode_file_path": "{season_directory_name_sanitized}/{episode_file_name_sanitized}", + "episode_number": "{ %array_at(episode_number_and_padded_, 0) }", + "episode_number_and_padded_": "{\n %elif(\n %episode_ordering_( \"upload-day\" ), [ %concat(upload_day, upload_date_index_padded), 4 ],\n %episode_ordering_( \"upload-month-day\" ), [ %concat(upload_month, upload_day_padded, upload_date_index_padded), 6],\n %episode_ordering_( \"upload-month-day-reversed\" ), [ %concat(upload_day_of_year_reversed, upload_date_index_reversed_padded), 5],\n %episode_ordering_( \"release-day\" ), [ %concat(release_day, upload_date_index_padded), 4 ],\n %episode_ordering_( \"release-month-day\" ), [ %concat(release_month, release_day_padded, upload_date_index_padded), 6],\n %episode_ordering_( \"release-month-day-reversed\" ), [ %concat(release_day_of_year_reversed, upload_date_index_reversed_padded), 5],\n %episode_ordering_( \"download-index\" ), [ download_index, 6 ],\n %throw(\n 'tv_show_by_date_episode_ordering must be one of the following: \"upload-day\", \"upload-month-day\", \"upload-month-day-reversed\", \"release-day\", \"release-month-day\", \"release-month-day-reversed\", \"download-index\"'\n )\n )\n}", + "episode_number_padded": "{ %pad_zero( %int(episode_number), %int(%array_at(episode_number_and_padded_, 1))) }", + "episode_plot": "{webpage_url}\n\n{description}", + "episode_title": "{episode_date_standardized} - {title}", + "episode_year": "{%slice(episode_date_standardized, 0, 4)}", + "file_title": "{title_sanitized_plex}", + "file_uid": "{uid_sanitized_plex}", + "include_sibling_metadata": false, + "modified_webpage_url": "{webpage_url}", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [] }", + "resolution_assert_is_ignored": "{\n %print_if_true(\n %concat(title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\"),\n %contains_any(title, resolution_assert_ignore_titles)\n )\n}", + "resolution_assert_print": "{\n %print(\n %if(\n enable_resolution_assert,\n \"Resolution assert is enabled, will fail on low-quality video downloads and presume throttle. Disable using the override variable `enable_resolution_assert: False`\",\n \"Resolution assert is disabled. Use at your own risk!\"\n ),\n enable_resolution_assert,\n -1\n )\n}", + "resolution_readable": "{width}x{height}", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season {season_number_padded}", + "season_number": "{\n %elif(\n %season_ordering_( \"upload-year\" ), upload_year,\n %season_ordering_( \"upload-year-month\" ), %concat(upload_year, upload_month_padded),\n %season_ordering_( \"release-year\" ), release_year,\n %season_ordering_( \"release-year-month\" ), %concat(release_year, release_month_padded),\n %throw(\n 'tv_show_by_date_season_ordering must be one of the following: \"upload-year\", \"upload-year-month\", \"release-year\", \"release-year-month\"'\n )\n )\n}", + "season_number_padded": "{season_number}", + "season_poster_file_name": "{season_directory_name_sanitized}/Season{season_number_padded}.jpg", + "subscription_array": "{%from_json('''[\"https://www.youtube.com/@novapbs\"]''')}", + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "{tv_show_content_rating_default}", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{episode_file_path}-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{\n %assert_then(\n %or(\n %ordering_pair_eq(\"upload-year\", \"upload-month-day\"),\n %ordering_pair_eq(\"upload-year\", \"upload-month-day-reversed\"),\n %ordering_pair_eq(\"upload-year\", \"download-index\"),\n %ordering_pair_eq(\"upload-year-month\", \"upload-day\"),\n %ordering_pair_eq(\"release-year\", \"release-month-day\"),\n %ordering_pair_eq(\"release-year\", \"release-month-day-reversed\"),\n %ordering_pair_eq(\"release-year\", \"download-index\"),\n %ordering_pair_eq(\"release-year-month\", \"release-day\")\n ),\n episode_number_and_padded_,\n \"Detected incompatibility between tv_show_by_date_season_ordering and tv_show_by_date_episode_ordering. Ensure you are not using both upload and release date, and that the year/month/day are included in the combined season and episode.\"\n )\n}", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "{subscription_indent_2}", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "{\n %if(\n %contains(tv_show_by_date_season_ordering, \"release\"),\n \"release_date\",\n \"upload_date\"\n )\n}", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp5yx9y73k", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "{subscription_indent_1}", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "{subscription_name}", + "tv_show_poster_file_name": "poster.jpg", + "url": "{subscription_value}", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": "{subscription_array}" + }, + "preset": [ + "_season_by_year", + "plex_tv_show_by_date", + "season_by_year__episode_by_month_day", + "Plex TV Show by Date", + "__preset__" + ], + "throttle_protection": { + "enable": "{\n %print(\n %if(\n enable_throttle_protection,\n \"Throttle protection is enabled. Disable using the override variable `enable_throttle_protection: False`\",\n \"Throttle protection is disabled. Use at your own risk!\"\n ),\n enable_throttle_protection\n )\n}", + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "{episode_content_rating}", + "date": "{episode_date_standardized}", + "episode_id": "{episode_number}", + "genre": "{tv_show_genre}", + "show": "{tv_show_name}", + "synopsis": "{episode_plot}", + "title": "{episode_title}", + "year": "{episode_year}" + }, + "ytdl_options": { + "break_on_existing": true + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_sub_resolve.json b/tests/resources/expected_json/tv_show/inspect_sub_resolve.json new file mode 100644 index 00000000..3c2898ea --- /dev/null +++ b/tests/resources/expected_json/tv_show/inspect_sub_resolve.json @@ -0,0 +1,250 @@ +{ + "chapters": { + "allow_chapters_from_comments": false, + "embed_chapters": true, + "enable": true, + "force_key_frames": false + }, + "date_range": { + "breaks": true, + "enable": true, + "type": "upload_date" + }, + "download": [ + { + "download_reverse": true, + "include_sibling_metadata": false, + "playlist_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "source_thumbnails": [ + { + "name": "poster.jpg", + "uid": "avatar_uncropped" + }, + { + "name": "fanart.jpg", + "uid": "banner_uncropped" + } + ], + "url": [ + "https://www.youtube.com/@novapbs" + ], + "variables": {}, + "webpage_url": "{ webpage_url }", + "ytdl_options": {} + } + ], + "file_convert": { + "convert_to": "mp4", + "convert_with": "yt-dlp", + "enable": true + }, + "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", + "output_options": { + "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", + "file_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }.{ ext }", + "info_json_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }.{ info_json_ext }", + "keep_files_date_eval": "{ upload_date_standardized }", + "maintain_download_archive": true, + "output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp3j0cm_tw/NOVA PBS", + "preserve_mtime": false, + "thumbnail_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }-thumb.jpg" + }, + "overrides": { + "%bilateral_url": "{ %if( %and( enable_bilateral_scraping, subscription_has_download_archive, %is_bilateral_url( $0 ) ), $0, '' ) }", + "%episode_ordering_": "{ %eq( %lower( tv_show_by_date_episode_ordering ), $0 ) }", + "%is_bilateral_url": "{ %contains( $0, \"youtube.com/playlist\" ) }", + "%ordering_pair_eq": "{ %eq( [ tv_show_by_date_season_ordering, tv_show_by_date_episode_ordering ], [ $0, $1 ] ) }", + "%season_ordering_": "{ %eq( %lower( tv_show_by_date_season_ordering ), $0 ) }", + "assert_not_collection": true, + "avatar_uncropped_thumbnail_file_name": "poster.jpg", + "banner_uncropped_thumbnail_file_name": "fanart.jpg", + "enable_bilateral_scraping": true, + "enable_resolution_assert": true, + "enable_throttle_protection": true, + "episode_content_rating": "TV-14", + "episode_date_standardized": "{ upload_date_standardized }", + "episode_file_name": "s{ upload_year }.e{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) } - { title_sanitized_plex }", + "episode_file_path": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/{ %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) }", + "episode_number": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", + "episode_number_and_padded_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "episode_number_padded": "{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) }", + "episode_plot": "{ webpage_url }\n\n{ description }", + "episode_title": "{ upload_date_standardized } - { title }", + "episode_year": "{ %slice( upload_date_standardized, 0, 4 ) }", + "file_title": "{ title_sanitized_plex }", + "file_uid": "{ uid_sanitized_plex }", + "include_sibling_metadata": false, + "modified_webpage_url": "{ webpage_url }", + "only_recent_date_range": "2months", + "only_recent_max_files": 30, + "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", + "resolution_assert_height_gte": 361, + "resolution_assert_ignore_titles": "{ [ ] }", + "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, [ ] ) ) }", + "resolution_assert_print": true, + "resolution_readable": "{ width }x{ height }", + "s01_name": "", + "s01_url": "", + "season_directory_name": "Season { upload_year }", + "season_number": "{ upload_year }", + "season_number_padded": "{ upload_year }", + "season_poster_file_name": "{ %sanitize( %concat( \"Season \", upload_year ) ) }/Season{ upload_year }.jpg", + "subscription_array": [ + "https://www.youtube.com/@novapbs" + ], + "subscription_indent_1": "Documentaries", + "subscription_indent_2": "TV-14", + "subscription_value": "https://www.youtube.com/@novapbs", + "subscription_value_1": "https://www.youtube.com/@novapbs", + "thumbnail_file_name": "{ %concat( %sanitize( %concat( \"Season \", upload_year ) ), \"/\", %sanitize( %concat( \"s\", upload_year, \".e\", %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ), \" - \", title_sanitized_plex ) ) ) }-thumb.jpg", + "tv_show_by_date_episode_ordering": "upload-month-day", + "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", + "tv_show_by_date_season_ordering": "upload-year", + "tv_show_content_rating": "TV-14", + "tv_show_content_rating_default": "TV-14", + "tv_show_date_range_type": "upload_date", + "tv_show_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp3j0cm_tw", + "tv_show_fanart_file_name": "fanart.jpg", + "tv_show_genre": "Documentaries", + "tv_show_genre_default": "ytdl-sub", + "tv_show_name": "NOVA PBS", + "tv_show_poster_file_name": "poster.jpg", + "url": "https://www.youtube.com/@novapbs", + "url10": "", + "url100": "", + "url11": "", + "url12": "", + "url13": "", + "url14": "", + "url15": "", + "url16": "", + "url17": "", + "url18": "", + "url19": "", + "url2": "", + "url20": "", + "url21": "", + "url22": "", + "url23": "", + "url24": "", + "url25": "", + "url26": "", + "url27": "", + "url28": "", + "url29": "", + "url3": "", + "url30": "", + "url31": "", + "url32": "", + "url33": "", + "url34": "", + "url35": "", + "url36": "", + "url37": "", + "url38": "", + "url39": "", + "url4": "", + "url40": "", + "url41": "", + "url42": "", + "url43": "", + "url44": "", + "url45": "", + "url46": "", + "url47": "", + "url48": "", + "url49": "", + "url5": "", + "url50": "", + "url51": "", + "url52": "", + "url53": "", + "url54": "", + "url55": "", + "url56": "", + "url57": "", + "url58": "", + "url59": "", + "url6": "", + "url60": "", + "url61": "", + "url62": "", + "url63": "", + "url64": "", + "url65": "", + "url66": "", + "url67": "", + "url68": "", + "url69": "", + "url7": "", + "url70": "", + "url71": "", + "url72": "", + "url73": "", + "url74": "", + "url75": "", + "url76": "", + "url77": "", + "url78": "", + "url79": "", + "url8": "", + "url80": "", + "url81": "", + "url82": "", + "url83": "", + "url84": "", + "url85": "", + "url86": "", + "url87": "", + "url88": "", + "url89": "", + "url9": "", + "url90": "", + "url91": "", + "url92": "", + "url93": "", + "url94": "", + "url95": "", + "url96": "", + "url97": "", + "url98": "", + "url99": "", + "urls": [ + "https://www.youtube.com/@novapbs" + ] + }, + "throttle_protection": { + "enable": true, + "sleep_per_download_s": { + "max": 28.4, + "min": 13.8 + }, + "sleep_per_request_s": { + "max": 0.75, + "min": 0.0 + }, + "sleep_per_subscription_s": { + "max": 26.1, + "min": 16.3 + } + }, + "video_tags": { + "contentRating": "TV-14", + "date": "{ upload_date_standardized }", + "episode_id": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", + "genre": "Documentaries", + "show": "NOVA PBS", + "synopsis": "{ webpage_url }\n\n{ description }", + "title": "{ upload_date_standardized } - { title }", + "year": "{ %slice( upload_date_standardized, 0, 4 ) }" + } +} \ No newline at end of file diff --git a/tests/resources/expected_json/tv_show/inspect_variables.json b/tests/resources/expected_json/tv_show/inspect_variables.json deleted file mode 100644 index 77659a4b..00000000 --- a/tests/resources/expected_json/tv_show/inspect_variables.json +++ /dev/null @@ -1,520 +0,0 @@ -{ - "assert_not_collection": "{ %bool(True) }", - "assert_not_collection_sanitized": "true", - "avatar_uncropped_thumbnail_file_name": "poster.jpg", - "avatar_uncropped_thumbnail_file_name_sanitized": "poster.jpg", - "banner_uncropped_thumbnail_file_name": "fanart.jpg", - "banner_uncropped_thumbnail_file_name_sanitized": "fanart.jpg", - "channel": "{ %map_get_non_empty( entry_metadata, \"channel\", uploader ) }", - "channel_id": "{ %map_get_non_empty( entry_metadata, \"channel_id\", uploader_id ) }", - "channel_id_sanitized": "{ %sanitize( channel_id ) }", - "channel_sanitized": "{ %sanitize( channel ) }", - "chapters": "{ [ ] }", - "chapters_sanitized": "{ %sanitize( chapters ) }", - "comments": "{ [ ] }", - "comments_sanitized": "{ %sanitize( comments ) }", - "creator": "{ %map_get_non_empty( entry_metadata, \"creator\", channel ) }", - "creator_sanitized": "{ %sanitize( creator ) }", - "description": "{ %map_get_non_empty( entry_metadata, \"description\", '' ) }", - "description_sanitized": "{ %sanitize( description ) }", - "download_index": "{ %int( 1 ) }", - "download_index_padded6": "{ %pad_zero( download_index, 6 ) }", - "download_index_padded6_sanitized": "{ %sanitize( download_index_padded6 ) }", - "download_index_sanitized": "{ %sanitize( download_index ) }", - "duration": "{ %map_get_non_empty( entry_metadata, \"duration\", 0 ) }", - "duration_sanitized": "{ %sanitize( duration ) }", - "enable_bilateral_scraping": "{ %bool(True) }", - "enable_bilateral_scraping_sanitized": "true", - "enable_resolution_assert": "{ %bool(True) }", - "enable_resolution_assert_sanitized": "true", - "enable_throttle_protection": "{ %bool(True) }", - "enable_throttle_protection_sanitized": "true", - "entry_metadata": "{ { } }", - "entry_metadata_sanitized": "{ %sanitize( entry_metadata ) }", - "episode_content_rating": "TV-14", - "episode_content_rating_sanitized": "TV-14", - "episode_date_standardized": "{ upload_date_standardized }", - "episode_date_standardized_sanitized": "{ %sanitize( upload_date_standardized ) }", - "episode_file_name": "s{ upload_year }.e{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) } - { title_sanitized_plex }", - "episode_file_name_sanitized": "{ %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) }", - "episode_file_path": "{ %sanitize( %concat( \"Season \", %string( upload_year ) ) ) }/{ %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) }", - "episode_file_path_sanitized": "{ %sanitize( %concat( %string( %sanitize( %concat( \"Season \", %string( upload_year ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) ) ) ) }", - "episode_number": "{ upload_month }{ upload_day_padded }{ upload_date_index_padded }", - "episode_number_and_padded_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", - "episode_number_and_padded__sanitized": "{ %sanitize( [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] ) }", - "episode_number_padded": "{ %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) }", - "episode_number_padded_sanitized": "{ %sanitize( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ) }", - "episode_number_sanitized": "{ %sanitize( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ) }", - "episode_plot": "{ webpage_url }\n\n{ description }", - "episode_plot_sanitized": "{ %sanitize( %concat( %string( webpage_url ), \"\n\n\", %string( description ) ) ) }", - "episode_title": "{ upload_date_standardized } - { title }", - "episode_title_sanitized": "{ %sanitize( %concat( %string( upload_date_standardized ), \" - \", %string( title ) ) ) }", - "episode_year": "{ %slice( upload_date_standardized, 0, 4 ) }", - "episode_year_sanitized": "{ %sanitize( %slice( upload_date_standardized, 0, 4 ) ) }", - "epoch": "{ %map_get( entry_metadata, \"epoch\" ) }", - "epoch_date": "{ %datetime_strftime( epoch, \"%Y%m%d\" ) }", - "epoch_date_sanitized": "{ %sanitize( epoch_date ) }", - "epoch_hour": "{ %datetime_strftime( epoch, \"%H\" ) }", - "epoch_hour_sanitized": "{ %sanitize( epoch_hour ) }", - "epoch_sanitized": "{ %sanitize( epoch ) }", - "ext": "{ %throw( \"Plugin variable ext has not been created yet\" ) }", - "ext_sanitized": "{ %sanitize( ext ) }", - "extractor": "{ %map_get( entry_metadata, \"extractor\" ) }", - "extractor_key": "{ %map_get( entry_metadata, \"extractor_key\" ) }", - "extractor_key_sanitized": "{ %sanitize( extractor_key ) }", - "extractor_sanitized": "{ %sanitize( extractor ) }", - "file_title": "{ title_sanitized_plex }", - "file_title_sanitized": "{ %sanitize( title_sanitized_plex ) }", - "file_uid": "{ uid_sanitized_plex }", - "file_uid_sanitized": "{ %sanitize( uid_sanitized_plex ) }", - "height": "{ %map_get_non_empty( entry_metadata, \"height\", 0 ) }", - "height_sanitized": "{ %sanitize( height ) }", - "ie_key": "{ %map_get_non_empty( entry_metadata, \"ie_key\", extractor_key ) }", - "ie_key_sanitized": "{ %sanitize( ie_key ) }", - "include_sibling_metadata": "{ %bool(False) }", - "include_sibling_metadata_sanitized": "false", - "info_json_ext": "info.json", - "info_json_ext_sanitized": "info.json", - "modified_webpage_url": "{ webpage_url }", - "modified_webpage_url_sanitized": "{ %sanitize( webpage_url ) }", - "only_recent_date_range": "2months", - "only_recent_date_range_sanitized": "2months", - "only_recent_max_files": "{ %int(30) }", - "only_recent_max_files_sanitized": "30", - "playlist_count": "{ %map_get_non_empty( entry_metadata, \"playlist_count\", 1 ) }", - "playlist_count_sanitized": "{ %sanitize( playlist_count ) }", - "playlist_description": "{ %map_get_non_empty( playlist_metadata, \"description\", description ) }", - "playlist_description_sanitized": "{ %sanitize( playlist_description ) }", - "playlist_index": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }", - "playlist_index_padded": "{ %pad_zero( playlist_index, 2 ) }", - "playlist_index_padded6": "{ %pad_zero( playlist_index, 6 ) }", - "playlist_index_padded6_sanitized": "{ %sanitize( playlist_index_padded6 ) }", - "playlist_index_padded_sanitized": "{ %sanitize( playlist_index_padded ) }", - "playlist_index_reversed": "{ %sub( playlist_count, playlist_index, -1 ) }", - "playlist_index_reversed_padded": "{ %pad_zero( playlist_index_reversed, 2 ) }", - "playlist_index_reversed_padded6": "{ %pad_zero( playlist_index_reversed, 6 ) }", - "playlist_index_reversed_padded6_sanitized": "{ %sanitize( playlist_index_reversed_padded6 ) }", - "playlist_index_reversed_padded_sanitized": "{ %sanitize( playlist_index_reversed_padded ) }", - "playlist_index_reversed_sanitized": "{ %sanitize( playlist_index_reversed ) }", - "playlist_index_sanitized": "{ %sanitize( playlist_index ) }", - "playlist_max_upload_date": "{ %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) }", - "playlist_max_upload_date_sanitized": "{ %sanitize( playlist_max_upload_date ) }", - "playlist_max_upload_year": "{ %int( %map_get( %to_date_metadata( playlist_max_upload_date ), \"year\" ) ) }", - "playlist_max_upload_year_sanitized": "{ %sanitize( playlist_max_upload_year ) }", - "playlist_max_upload_year_truncated": "{ %int( %map_get( %to_date_metadata( playlist_max_upload_date ), \"year_truncated\" ) ) }", - "playlist_max_upload_year_truncated_sanitized": "{ %sanitize( playlist_max_upload_year_truncated ) }", - "playlist_metadata": "{ %map_get_non_empty( entry_metadata, \"playlist_metadata\", { } ) }", - "playlist_metadata_sanitized": "{ %sanitize( playlist_metadata ) }", - "playlist_title": "{ %map_get_non_empty( entry_metadata, \"playlist_title\", title ) }", - "playlist_title_sanitized": "{ %sanitize( playlist_title ) }", - "playlist_uid": "{ %map_get_non_empty( playlist_metadata, \"playlist_id\", uid ) }", - "playlist_uid_sanitized": "{ %sanitize( playlist_uid ) }", - "playlist_uploader": "{ %map_get_non_empty( playlist_metadata, \"uploader\", uploader ) }", - "playlist_uploader_id": "{ %map_get_non_empty( entry_metadata, \"playlist_uploader_id\", uploader_id ) }", - "playlist_uploader_id_sanitized": "{ %sanitize( playlist_uploader_id ) }", - "playlist_uploader_sanitized": "{ %sanitize( playlist_uploader ) }", - "playlist_uploader_url": "{ %map_get_non_empty( playlist_metadata, \"uploader_url\", webpage_url ) }", - "playlist_uploader_url_sanitized": "{ %sanitize( playlist_uploader_url ) }", - "playlist_webpage_url": "{ %map_get_non_empty( playlist_metadata, \"webpage_url\", webpage_url ) }", - "playlist_webpage_url_sanitized": "{ %sanitize( playlist_webpage_url ) }", - "release_date": "{ %map_get_non_empty( entry_metadata, \"release_date\", upload_date ) }", - "release_date_sanitized": "{ %sanitize( release_date ) }", - "release_date_standardized": "{ %string( %map_get( %to_date_metadata( release_date ), \"date_standardized\" ) ) }", - "release_date_standardized_sanitized": "{ %sanitize( release_date_standardized ) }", - "release_day": "{ %int( %map_get( %to_date_metadata( release_date ), \"day\" ) ) }", - "release_day_of_year": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_of_year\" ) ) }", - "release_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_of_year_padded\" ) ) }", - "release_day_of_year_padded_sanitized": "{ %sanitize( release_day_of_year_padded ) }", - "release_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_of_year_reversed\" ) ) }", - "release_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_of_year_reversed_padded\" ) ) }", - "release_day_of_year_reversed_padded_sanitized": "{ %sanitize( release_day_of_year_reversed_padded ) }", - "release_day_of_year_reversed_sanitized": "{ %sanitize( release_day_of_year_reversed ) }", - "release_day_of_year_sanitized": "{ %sanitize( release_day_of_year ) }", - "release_day_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_padded\" ) ) }", - "release_day_padded_sanitized": "{ %sanitize( release_day_padded ) }", - "release_day_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"day_reversed\" ) ) }", - "release_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"day_reversed_padded\" ) ) }", - "release_day_reversed_padded_sanitized": "{ %sanitize( release_day_reversed_padded ) }", - "release_day_reversed_sanitized": "{ %sanitize( release_day_reversed ) }", - "release_day_sanitized": "{ %sanitize( release_day ) }", - "release_month": "{ %int( %map_get( %to_date_metadata( release_date ), \"month\" ) ) }", - "release_month_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"month_padded\" ) ) }", - "release_month_padded_sanitized": "{ %sanitize( release_month_padded ) }", - "release_month_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"month_reversed\" ) ) }", - "release_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( release_date ), \"month_reversed_padded\" ) ) }", - "release_month_reversed_padded_sanitized": "{ %sanitize( release_month_reversed_padded ) }", - "release_month_reversed_sanitized": "{ %sanitize( release_month_reversed ) }", - "release_month_sanitized": "{ %sanitize( release_month ) }", - "release_year": "{ %int( %map_get( %to_date_metadata( release_date ), \"year\" ) ) }", - "release_year_sanitized": "{ %sanitize( release_year ) }", - "release_year_truncated": "{ %int( %map_get( %to_date_metadata( release_date ), \"year_truncated\" ) ) }", - "release_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( release_date ), \"year_truncated_reversed\" ) ) }", - "release_year_truncated_reversed_sanitized": "{ %sanitize( release_year_truncated_reversed ) }", - "release_year_truncated_sanitized": "{ %sanitize( release_year_truncated ) }", - "requested_subtitles": "{ { } }", - "requested_subtitles_sanitized": "{ %sanitize( requested_subtitles ) }", - "resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }", - "resolution_assert_height_gte": "{ %int(361) }", - "resolution_assert_height_gte_sanitized": "361", - "resolution_assert_ignore_titles": "{ [ ] }", - "resolution_assert_ignore_titles_sanitized": "[]", - "resolution_assert_is_ignored": "{ %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) }", - "resolution_assert_is_ignored_sanitized": "{ %sanitize( %print_if_true( %concat( title, \" has a match in resolution_assert_ignore_titles, skipping resolution assert.\" ), %contains_any( title, resolution_assert_ignore_titles ) ) ) }", - "resolution_assert_print": "{ %bool(True) }", - "resolution_assert_print_sanitized": "true", - "resolution_assert_sanitized": "{ %sanitize( %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) ) }", - "resolution_readable": "{ width }x{ height }", - "resolution_readable_sanitized": "{ %sanitize( %concat( %string( width ), \"x\", %string( height ) ) ) }", - "s01_name": "", - "s01_name_sanitized": "", - "s01_url": "", - "s01_url_sanitized": "", - "season_directory_name": "Season { upload_year }", - "season_directory_name_sanitized": "{ %sanitize( %concat( \"Season \", %string( upload_year ) ) ) }", - "season_number": "{ upload_year }", - "season_number_padded": "{ upload_year }", - "season_number_padded_sanitized": "{ %sanitize( upload_year ) }", - "season_number_sanitized": "{ %sanitize( upload_year ) }", - "season_poster_file_name": "{ %sanitize( %concat( \"Season \", %string( upload_year ) ) ) }/Season{ upload_year }.jpg", - "season_poster_file_name_sanitized": "{ %sanitize( %concat( %string( %sanitize( %concat( \"Season \", %string( upload_year ) ) ) ), \"/Season\", %string( upload_year ), \".jpg\" ) ) }", - "sibling_metadata": "{ %map_get_non_empty( entry_metadata, \"sibling_metadata\", [ ] ) }", - "sibling_metadata_sanitized": "{ %sanitize( sibling_metadata ) }", - "source_count": "{ %map_get_non_empty( playlist_metadata, \"playlist_count\", 1 ) }", - "source_count_sanitized": "{ %sanitize( source_count ) }", - "source_description": "{ %map_get_non_empty( source_metadata, \"description\", playlist_description ) }", - "source_description_sanitized": "{ %sanitize( source_description ) }", - "source_index": "{ %map_get_non_empty( playlist_metadata, \"playlist_index\", 1 ) }", - "source_index_padded": "{ %pad_zero( source_index, 2 ) }", - "source_index_padded_sanitized": "{ %sanitize( source_index_padded ) }", - "source_index_sanitized": "{ %sanitize( source_index ) }", - "source_metadata": "{ %map_get_non_empty( entry_metadata, \"source_metadata\", { } ) }", - "source_metadata_sanitized": "{ %sanitize( source_metadata ) }", - "source_title": "{ %map_get_non_empty( source_metadata, \"title\", playlist_title ) }", - "source_title_sanitized": "{ %sanitize( source_title ) }", - "source_uid": "{ %map_get_non_empty( source_metadata, \"id\", playlist_uid ) }", - "source_uid_sanitized": "{ %sanitize( source_uid ) }", - "source_uploader": "{ %map_get_non_empty( source_metadata, \"uploader\", playlist_uploader ) }", - "source_uploader_id": "{ %map_get_non_empty( source_metadata, \"uploader_id\", playlist_uploader_id ) }", - "source_uploader_id_sanitized": "{ %sanitize( source_uploader_id ) }", - "source_uploader_sanitized": "{ %sanitize( source_uploader ) }", - "source_uploader_url": "{ %map_get_non_empty( source_metadata, \"uploader_url\", source_webpage_url ) }", - "source_uploader_url_sanitized": "{ %sanitize( source_uploader_url ) }", - "source_webpage_url": "{ %map_get_non_empty( source_metadata, \"webpage_url\", playlist_webpage_url ) }", - "source_webpage_url_sanitized": "{ %sanitize( source_webpage_url ) }", - "sponsorblock_chapters": "{ [ ] }", - "sponsorblock_chapters_sanitized": "{ %sanitize( sponsorblock_chapters ) }", - "subscription_array": "{ [ \"https://www.youtube.com/@novapbs\" ] }", - "subscription_array_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs\uff02]", - "subscription_has_download_archive": "{ %bool(False) }", - "subscription_has_download_archive_sanitized": "false", - "subscription_indent_1": "Documentaries", - "subscription_indent_1_sanitized": "Documentaries", - "subscription_indent_2": "TV-14", - "subscription_indent_2_sanitized": "TV-14", - "subscription_name": "NOVA PBS", - "subscription_name_sanitized": "NOVA PBS", - "subscription_value": "https://www.youtube.com/@novapbs", - "subscription_value_1": "https://www.youtube.com/@novapbs", - "subscription_value_1_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs", - "subscription_value_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs", - "thumbnail_ext": "jpg", - "thumbnail_ext_sanitized": "jpg", - "thumbnail_file_name": "{ %concat( %string( %sanitize( %concat( \"Season \", %string( upload_year ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) ) ) }-thumb.jpg", - "thumbnail_file_name_sanitized": "{ %sanitize( %concat( %string( %concat( %string( %sanitize( %concat( \"Season \", %string( upload_year ) ) ) ), \"/\", %string( %sanitize( %concat( \"s\", %string( upload_year ), \".e\", %string( %pad_zero( %int( %concat( upload_month, upload_day_padded, upload_date_index_padded ) ), 6 ) ), \" - \", %string( title_sanitized_plex ) ) ) ) ) ), \"-thumb.jpg\" ) ) }", - "title": "{ %map_get_non_empty( entry_metadata, \"title\", uid ) }", - "title_sanitized": "{ %sanitize( title ) }", - "title_sanitized_plex": "{ %sanitize_plex_episode( title ) }", - "title_sanitized_plex_sanitized": "{ %sanitize( title_sanitized_plex ) }", - "tv_show_by_date_episode_ordering": "upload-month-day", - "tv_show_by_date_episode_ordering_sanitized": "upload-month-day", - "tv_show_by_date_ordering_pair_validation_": "{ [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] }", - "tv_show_by_date_ordering_pair_validation__sanitized": "{ %sanitize( [ %concat( upload_month, upload_day_padded, upload_date_index_padded ), 6 ] ) }", - "tv_show_by_date_season_ordering": "upload-year", - "tv_show_by_date_season_ordering_sanitized": "upload-year", - "tv_show_content_rating": "TV-14", - "tv_show_content_rating_default": "TV-14", - "tv_show_content_rating_default_sanitized": "TV-14", - "tv_show_content_rating_sanitized": "TV-14", - "tv_show_date_range_type": "upload_date", - "tv_show_date_range_type_sanitized": "upload_date", - "tv_show_directory": "tv_show_directory_path", - "tv_show_directory_sanitized": "tv_show_directory_path", - "tv_show_fanart_file_name": "fanart.jpg", - "tv_show_fanart_file_name_sanitized": "fanart.jpg", - "tv_show_genre": "Documentaries", - "tv_show_genre_default": "ytdl-sub", - "tv_show_genre_default_sanitized": "ytdl-sub", - "tv_show_genre_sanitized": "Documentaries", - "tv_show_name": "NOVA PBS", - "tv_show_name_sanitized": "NOVA PBS", - "tv_show_poster_file_name": "poster.jpg", - "tv_show_poster_file_name_sanitized": "poster.jpg", - "uid": "{ %map_get( entry_metadata, \"id\" ) }", - "uid_sanitized": "{ %sanitize( uid ) }", - "uid_sanitized_plex": "{ %sanitize_plex_episode( uid ) }", - "uid_sanitized_plex_sanitized": "{ %sanitize( uid_sanitized_plex ) }", - "upload_date": "{ %map_get_non_empty( entry_metadata, \"upload_date\", epoch_date ) }", - "upload_date_index": "{ %int( 1 ) }", - "upload_date_index_padded": "{ %pad_zero( upload_date_index, 2 ) }", - "upload_date_index_padded_sanitized": "{ %sanitize( upload_date_index_padded ) }", - "upload_date_index_reversed": "{ %sub( 100, upload_date_index ) }", - "upload_date_index_reversed_padded": "{ %pad_zero( upload_date_index_reversed, 2 ) }", - "upload_date_index_reversed_padded_sanitized": "{ %sanitize( upload_date_index_reversed_padded ) }", - "upload_date_index_reversed_sanitized": "{ %sanitize( upload_date_index_reversed ) }", - "upload_date_index_sanitized": "{ %sanitize( upload_date_index ) }", - "upload_date_sanitized": "{ %sanitize( upload_date ) }", - "upload_date_standardized": "{ %string( %map_get( %to_date_metadata( upload_date ), \"date_standardized\" ) ) }", - "upload_date_standardized_sanitized": "{ %sanitize( upload_date_standardized ) }", - "upload_day": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day\" ) ) }", - "upload_day_of_year": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_of_year\" ) ) }", - "upload_day_of_year_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_of_year_padded\" ) ) }", - "upload_day_of_year_padded_sanitized": "{ %sanitize( upload_day_of_year_padded ) }", - "upload_day_of_year_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_of_year_reversed\" ) ) }", - "upload_day_of_year_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_of_year_reversed_padded\" ) ) }", - "upload_day_of_year_reversed_padded_sanitized": "{ %sanitize( upload_day_of_year_reversed_padded ) }", - "upload_day_of_year_reversed_sanitized": "{ %sanitize( upload_day_of_year_reversed ) }", - "upload_day_of_year_sanitized": "{ %sanitize( upload_day_of_year ) }", - "upload_day_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_padded\" ) ) }", - "upload_day_padded_sanitized": "{ %sanitize( upload_day_padded ) }", - "upload_day_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"day_reversed\" ) ) }", - "upload_day_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"day_reversed_padded\" ) ) }", - "upload_day_reversed_padded_sanitized": "{ %sanitize( upload_day_reversed_padded ) }", - "upload_day_reversed_sanitized": "{ %sanitize( upload_day_reversed ) }", - "upload_day_sanitized": "{ %sanitize( upload_day ) }", - "upload_month": "{ %int( %map_get( %to_date_metadata( upload_date ), \"month\" ) ) }", - "upload_month_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"month_padded\" ) ) }", - "upload_month_padded_sanitized": "{ %sanitize( upload_month_padded ) }", - "upload_month_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"month_reversed\" ) ) }", - "upload_month_reversed_padded": "{ %string( %map_get( %to_date_metadata( upload_date ), \"month_reversed_padded\" ) ) }", - "upload_month_reversed_padded_sanitized": "{ %sanitize( upload_month_reversed_padded ) }", - "upload_month_reversed_sanitized": "{ %sanitize( upload_month_reversed ) }", - "upload_month_sanitized": "{ %sanitize( upload_month ) }", - "upload_year": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year\" ) ) }", - "upload_year_sanitized": "{ %sanitize( upload_year ) }", - "upload_year_truncated": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year_truncated\" ) ) }", - "upload_year_truncated_reversed": "{ %int( %map_get( %to_date_metadata( upload_date ), \"year_truncated_reversed\" ) ) }", - "upload_year_truncated_reversed_sanitized": "{ %sanitize( upload_year_truncated_reversed ) }", - "upload_year_truncated_sanitized": "{ %sanitize( upload_year_truncated ) }", - "uploader": "{ %map_get_non_empty( entry_metadata, \"uploader\", uploader_id ) }", - "uploader_id": "{ %map_get_non_empty( entry_metadata, \"uploader_id\", uid ) }", - "uploader_id_sanitized": "{ %sanitize( uploader_id ) }", - "uploader_sanitized": "{ %sanitize( uploader ) }", - "uploader_url": "{ %map_get_non_empty( entry_metadata, \"uploader_url\", webpage_url ) }", - "uploader_url_sanitized": "{ %sanitize( uploader_url ) }", - "url": "https://www.youtube.com/@novapbs", - "url10": "", - "url100": "", - "url100_sanitized": "", - "url10_sanitized": "", - "url11": "", - "url11_sanitized": "", - "url12": "", - "url12_sanitized": "", - "url13": "", - "url13_sanitized": "", - "url14": "", - "url14_sanitized": "", - "url15": "", - "url15_sanitized": "", - "url16": "", - "url16_sanitized": "", - "url17": "", - "url17_sanitized": "", - "url18": "", - "url18_sanitized": "", - "url19": "", - "url19_sanitized": "", - "url2": "", - "url20": "", - "url20_sanitized": "", - "url21": "", - "url21_sanitized": "", - "url22": "", - "url22_sanitized": "", - "url23": "", - "url23_sanitized": "", - "url24": "", - "url24_sanitized": "", - "url25": "", - "url25_sanitized": "", - "url26": "", - "url26_sanitized": "", - "url27": "", - "url27_sanitized": "", - "url28": "", - "url28_sanitized": "", - "url29": "", - "url29_sanitized": "", - "url2_sanitized": "", - "url3": "", - "url30": "", - "url30_sanitized": "", - "url31": "", - "url31_sanitized": "", - "url32": "", - "url32_sanitized": "", - "url33": "", - "url33_sanitized": "", - "url34": "", - "url34_sanitized": "", - "url35": "", - "url35_sanitized": "", - "url36": "", - "url36_sanitized": "", - "url37": "", - "url37_sanitized": "", - "url38": "", - "url38_sanitized": "", - "url39": "", - "url39_sanitized": "", - "url3_sanitized": "", - "url4": "", - "url40": "", - "url40_sanitized": "", - "url41": "", - "url41_sanitized": "", - "url42": "", - "url42_sanitized": "", - "url43": "", - "url43_sanitized": "", - "url44": "", - "url44_sanitized": "", - "url45": "", - "url45_sanitized": "", - "url46": "", - "url46_sanitized": "", - "url47": "", - "url47_sanitized": "", - "url48": "", - "url48_sanitized": "", - "url49": "", - "url49_sanitized": "", - "url4_sanitized": "", - "url5": "", - "url50": "", - "url50_sanitized": "", - "url51": "", - "url51_sanitized": "", - "url52": "", - "url52_sanitized": "", - "url53": "", - "url53_sanitized": "", - "url54": "", - "url54_sanitized": "", - "url55": "", - "url55_sanitized": "", - "url56": "", - "url56_sanitized": "", - "url57": "", - "url57_sanitized": "", - "url58": "", - "url58_sanitized": "", - "url59": "", - "url59_sanitized": "", - "url5_sanitized": "", - "url6": "", - "url60": "", - "url60_sanitized": "", - "url61": "", - "url61_sanitized": "", - "url62": "", - "url62_sanitized": "", - "url63": "", - "url63_sanitized": "", - "url64": "", - "url64_sanitized": "", - "url65": "", - "url65_sanitized": "", - "url66": "", - "url66_sanitized": "", - "url67": "", - "url67_sanitized": "", - "url68": "", - "url68_sanitized": "", - "url69": "", - "url69_sanitized": "", - "url6_sanitized": "", - "url7": "", - "url70": "", - "url70_sanitized": "", - "url71": "", - "url71_sanitized": "", - "url72": "", - "url72_sanitized": "", - "url73": "", - "url73_sanitized": "", - "url74": "", - "url74_sanitized": "", - "url75": "", - "url75_sanitized": "", - "url76": "", - "url76_sanitized": "", - "url77": "", - "url77_sanitized": "", - "url78": "", - "url78_sanitized": "", - "url79": "", - "url79_sanitized": "", - "url7_sanitized": "", - "url8": "", - "url80": "", - "url80_sanitized": "", - "url81": "", - "url81_sanitized": "", - "url82": "", - "url82_sanitized": "", - "url83": "", - "url83_sanitized": "", - "url84": "", - "url84_sanitized": "", - "url85": "", - "url85_sanitized": "", - "url86": "", - "url86_sanitized": "", - "url87": "", - "url87_sanitized": "", - "url88": "", - "url88_sanitized": "", - "url89": "", - "url89_sanitized": "", - "url8_sanitized": "", - "url9": "", - "url90": "", - "url90_sanitized": "", - "url91": "", - "url91_sanitized": "", - "url92": "", - "url92_sanitized": "", - "url93": "", - "url93_sanitized": "", - "url94": "", - "url94_sanitized": "", - "url95": "", - "url95_sanitized": "", - "url96": "", - "url96_sanitized": "", - "url97": "", - "url97_sanitized": "", - "url98": "", - "url98_sanitized": "", - "url99": "", - "url99_sanitized": "", - "url9_sanitized": "", - "url_sanitized": "https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs", - "urls": "{ [ \"https://www.youtube.com/@novapbs\" ] }", - "urls_sanitized": "[\uff02https\uff1a\u29f8\u29f8www.youtube.com\u29f8@novapbs\uff02]", - "webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }", - "webpage_url_sanitized": "{ %sanitize( webpage_url ) }", - "width": "{ %map_get_non_empty( entry_metadata, \"width\", 0 ) }", - "width_sanitized": "{ %sanitize( width ) }", - "ytdl_sub_chapters_from_comments": "{ %throw( \"Plugin variable ytdl_sub_chapters_from_comments has not been created yet\" ) }", - "ytdl_sub_chapters_from_comments_sanitized": "{ %sanitize( ytdl_sub_chapters_from_comments ) }", - "ytdl_sub_entry_date_eval": "{ %string( upload_date_standardized ) }", - "ytdl_sub_entry_date_eval_sanitized": "{ %sanitize( ytdl_sub_entry_date_eval ) }", - "ytdl_sub_input_url": "{ %string( '' ) }", - "ytdl_sub_input_url_count": "{ %int( 0 ) }", - "ytdl_sub_input_url_count_sanitized": "{ %sanitize( ytdl_sub_input_url_count ) }", - "ytdl_sub_input_url_index": "{ %int( -1 ) }", - "ytdl_sub_input_url_index_sanitized": "{ %sanitize( ytdl_sub_input_url_index ) }", - "ytdl_sub_input_url_sanitized": "{ %sanitize( ytdl_sub_input_url ) }" -} \ No newline at end of file diff --git a/tests/unit/cli/test_download_args_parser.py b/tests/unit/cli/test_download_args_parser.py index 675f4066..9e4cad03 100644 --- a/tests/unit/cli/test_download_args_parser.py +++ b/tests/unit/cli/test_download_args_parser.py @@ -1,14 +1,10 @@ import shlex -from typing import Callable -from typing import Dict -from typing import List -from typing import Optional +from typing import Callable, Dict, List, Optional import pytest from ytdl_sub.cli.parsers.dl import DownloadArgsParser -from ytdl_sub.cli.parsers.main import MainArguments -from ytdl_sub.cli.parsers.main import parser +from ytdl_sub.cli.parsers.main import MainArguments, parser from ytdl_sub.config.config_validator import ConfigOptions from ytdl_sub.utils.exceptions import InvalidDlArguments @@ -164,7 +160,7 @@ class TestDownloadArgsParser: with pytest.raises( InvalidDlArguments, - match=f"Invalid dl argument --parameter.using.list: " + match="Invalid dl argument --parameter.using.list: " "Cannot specify an argument to be two different types", ): DownloadArgsParser( diff --git a/tests/unit/cli/test_output_summary.py b/tests/unit/cli/test_output_summary.py index a31c9d58..3278eb54 100644 --- a/tests/unit/cli/test_output_summary.py +++ b/tests/unit/cli/test_output_summary.py @@ -1,14 +1,11 @@ -from typing import List -from typing import Optional -from typing import Tuple -from unittest.mock import MagicMock -from unittest.mock import Mock +from typing import List, Optional, Tuple +from unittest.mock import MagicMock, Mock from ytdl_sub.cli.output_summary import output_summary def _to_mock_subscriptions( - subscription_values: List[Tuple[str, int, int, int, int, Optional[Exception]]] + subscription_values: List[Tuple[str, int, int, int, int, Optional[Exception]]], ) -> List[MagicMock]: mock_subscriptions: List[MagicMock] = [] for values in subscription_values: diff --git a/tests/unit/config/test_config_file.py b/tests/unit/config/test_config_file.py index ae1d2231..39560388 100644 --- a/tests/unit/config/test_config_file.py +++ b/tests/unit/config/test_config_file.py @@ -1,8 +1,7 @@ import os.path import re from pathlib import Path -from typing import Dict -from typing import Optional +from typing import Dict, Optional import pytest diff --git a/tests/unit/config/test_preset.py b/tests/unit/config/test_preset.py index a42439be..f36d9290 100644 --- a/tests/unit/config/test_preset.py +++ b/tests/unit/config/test_preset.py @@ -201,7 +201,7 @@ class TestPreset: with pytest.raises( ValidationException, match=re.escape( - f"Override variable with name subtitles_ext cannot be used since" + "Override variable with name subtitles_ext cannot be used since" " it is added by a plugin." ), ): diff --git a/tests/unit/config/test_subscription.py b/tests/unit/config/test_subscription.py index 35942f8d..a961d8ab 100644 --- a/tests/unit/config/test_subscription.py +++ b/tests/unit/config/test_subscription.py @@ -5,14 +5,11 @@ from typing import Dict from unittest.mock import patch import pytest -import yaml from ytdl_sub.config.config_file import ConfigFile -from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.plugins.nfo_tags import NfoTagsOptions from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.utils.script import ScriptUtils @contextmanager @@ -427,7 +424,7 @@ def test_subscription_file_invalid_form(config_file: ConfigFile): mock_load_yaml(preset_dict={"sub_name": 4332}), pytest.raises( ValidationException, - match=re.escape(f"Subscription value should either be a string, list, or object"), + match=re.escape("Subscription value should either be a string, list, or object"), ), ): _ = Subscription.from_file_path(config=config_file, subscription_path="mocked") @@ -475,19 +472,6 @@ def test_advanced_tv_show_subscriptions( assert overrides.script.get("subscription_name").native == "Gardening with Ciscoe" - assert overrides.apply_overrides_formatter_to_native(overrides.dict["subscription_array"]) == [ - "https://www.youtube.com/@gardeningwithciscoe4430", - "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_", - "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn", - "https://www.youtube.com/watch?v=2vq-vPubS5I", - ] - assert overrides.apply_overrides_formatter_to_native(overrides.dict["urls"]) == [ - "https://www.youtube.com/@gardeningwithciscoe4430", - "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_", - "https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn", - "https://www.youtube.com/watch?v=2vq-vPubS5I", - ] - def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path): subs = Subscription.from_file_path( @@ -534,69 +518,3 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc assert gnr_urls[0] == "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ" assert gnr.get("subscription_indent_1").native == "Rock" assert gnr_urls[1] == "https://www.youtube.com/watch?v=OldpIhHPsbs" - - -def test_default_docker_config_and_subscriptions( - docker_default_subscription_path: Path, output_directory: str -): - default_config = ConfigFile.from_file_path("docker/root/defaults/config.yaml") - default_subs = Subscription.from_file_path( - config=default_config, subscription_path=docker_default_subscription_path - ) - assert len(default_subs) == 1 - - resolved_yaml_as_json = yaml.safe_load(default_subs[0].resolved_yaml()) - - # Since this creates random values, ignore it for this test - assert "throttle_protection" in resolved_yaml_as_json - del resolved_yaml_as_json["throttle_protection"] - - assert resolved_yaml_as_json == { - "chapters": { - "allow_chapters_from_comments": False, - "embed_chapters": True, - "enable": "True", - "force_key_frames": False, - }, - "date_range": {"breaks": "True", "enable": "True", "type": "upload_date"}, - "download": [ - { - "download_reverse": "True", - "include_sibling_metadata": False, - "playlist_thumbnails": [ - {"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"}, - {"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"}, - ], - "source_thumbnails": [ - {"name": "{avatar_uncropped_thumbnail_file_name}", "uid": "avatar_uncropped"}, - {"name": "{banner_uncropped_thumbnail_file_name}", "uid": "banner_uncropped"}, - ], - "url": "https://www.youtube.com/@novapbs", - "variables": {}, - "webpage_url": "{modified_webpage_url}", - "ytdl_options": {}, - } - ], - "file_convert": {"convert_to": "mp4", "convert_with": "yt-dlp", "enable": "True"}, - "format": "(bv*[ext=mp4][vcodec~='^((he|a)vc|h26[45])']+ba[ext=m4a]) / (bv[ext=mp4]*+ba[ext=m4a]/b)", - "output_options": { - "download_archive_name": ".ytdl-sub-NOVA PBS-download-archive.json", - "file_name": "{episode_file_path}.{ext}", - "info_json_name": "{episode_file_path}.{info_json_ext}", - "keep_files_date_eval": "{episode_date_standardized}", - "maintain_download_archive": True, - "output_directory": f"{output_directory}/NOVA PBS", - "preserve_mtime": False, - "thumbnail_name": "{thumbnail_file_name}", - }, - "video_tags": { - "contentRating": "{episode_content_rating}", - "date": "{episode_date_standardized}", - "episode_id": "{episode_number}", - "genre": "{tv_show_genre}", - "show": "{tv_show_name}", - "synopsis": "{episode_plot}", - "title": "{episode_title}", - "year": "{episode_year}", - }, - } diff --git a/tests/unit/config/test_subscription_partial_resolve.py b/tests/unit/config/test_subscription_partial_resolve.py deleted file mode 100644 index 0a901fec..00000000 --- a/tests/unit/config/test_subscription_partial_resolve.py +++ /dev/null @@ -1,118 +0,0 @@ -from pathlib import Path - -from resources import expected_json - -from ytdl_sub.config.config_file import ConfigFile -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.subscriptions.subscription import Subscription -from ytdl_sub.utils.script import ScriptUtils - - -def _ensure_partial_resolve( - sub: Subscription, preset_type: str, should_fully_resolve: bool -) -> None: - unresolvable = sub.plugins.get_all_variables( - additional_options=[sub.downloader_options, sub.output_options] - ) - unresolvable.add("entry_metadata") - unresolvable.add("sibling_metadata") - - if not should_fully_resolve: - unresolvable.update(VARIABLES.scripts().keys()) - - script = sub.overrides.script.add( - { - f"{preset_type}_directory": "tv_show_directory_path", - f"{preset_type}_directory_sanitized": "tv_show_directory_path", - } - ) - partial_script = script.resolve_partial(unresolvable=unresolvable) - - # Assert only overrides - out = { - name: ScriptUtils.to_native_script(partial_script._variables[name]) - for name in sub.overrides.keys - if not name.startswith("%") - } - - expected_out_filename = f"{preset_type}/inspect_overrides.json" - if should_fully_resolve: - expected_out_filename = f"{preset_type}/inspect_full_overrides.json" - - assert out == expected_json(out, expected_out_filename) - - # Assert all variables - out = { - name: ScriptUtils.to_native_script(partial_script._variables[name]) - for name in partial_script.variable_names - } - - expected_out_filename = f"{preset_type}/inspect_variables.json" - if should_fully_resolve: - expected_out_filename = f"{preset_type}/inspect_full_variables.json" - - assert out == expected_json(out, expected_out_filename) - - -def test_partial_resolve_tv_show(config_file: ConfigFile, tv_show_subscriptions_path: Path): - _ensure_partial_resolve( - sub=Subscription.from_file_path( - config=config_file, subscription_path=tv_show_subscriptions_path - )[0], - preset_type="tv_show", - should_fully_resolve=True, - ) - - -def test_partial_resolve_tv_show_full(config_file: ConfigFile, tv_show_subscriptions_path: Path): - _ensure_partial_resolve( - sub=Subscription.from_file_path( - config=config_file, subscription_path=tv_show_subscriptions_path - )[0], - preset_type="tv_show", - should_fully_resolve=False, - ) - - -def test_partial_resolve_music(default_config: ConfigFile, music_subscriptions_path: Path): - _ensure_partial_resolve( - sub=Subscription.from_file_path( - config=default_config, subscription_path=music_subscriptions_path - )[0], - preset_type="music", - should_fully_resolve=True, - ) - - -def test_partial_resolve_music_full(default_config: ConfigFile, music_subscriptions_path: Path): - _ensure_partial_resolve( - sub=Subscription.from_file_path( - config=default_config, subscription_path=music_subscriptions_path - )[0], - preset_type="music", - should_fully_resolve=False, - ) - - -def test_partial_resolve_music_video( - default_config: ConfigFile, music_video_subscription_path: Path -): - _ensure_partial_resolve( - sub=Subscription.from_file_path( - config=default_config, subscription_path=music_video_subscription_path - )[0], - preset_type="music_video", - should_fully_resolve=False, - ) - - -def test_partial_resolve_music_video_full( - default_config: ConfigFile, music_video_subscription_path: Path -): - _ensure_partial_resolve( - sub=Subscription.from_file_path( - config=default_config, subscription_path=music_video_subscription_path - )[0], - preset_type="music_video", - should_fully_resolve=True, - ) diff --git a/tests/unit/config/test_subscription_resolution.py b/tests/unit/config/test_subscription_resolution.py new file mode 100644 index 00000000..12781bb8 --- /dev/null +++ b/tests/unit/config/test_subscription_resolution.py @@ -0,0 +1,91 @@ +from pathlib import Path + +import pytest +import yaml +from resources import expected_json + +from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.config.validators.variable_validation import ResolutionLevel +from ytdl_sub.subscriptions.subscription import Subscription +from ytdl_sub.utils.file_path import FilePathTruncater + + +def _ensure_resolved_yaml( + sub: Subscription, output_directory: str, preset_type: str, resolution_level: int +) -> None: + output_yaml = sub.resolved_yaml(resolution_level=resolution_level) + out = yaml.safe_load(output_yaml) + + expected_out_filename = ( + f"{preset_type}/inspect_sub_{ResolutionLevel.name_of(resolution_level)}.json" + ) + expected_out = expected_json(out, expected_out_filename) + + if resolution_level > ResolutionLevel.ORIGINAL: + output_path = Path(output_directory) + if "tv_show_directory" in expected_out["overrides"]: + output_path = output_path / sub.name + + expected_out["output_options"]["output_directory"] = FilePathTruncater.to_native_filepath( + str(output_path) + ) + + if "tv_show_directory" in expected_out["overrides"]: + expected_out["overrides"]["tv_show_directory"] = output_directory + if "music_directory" in expected_out["overrides"]: + expected_out["overrides"]["music_directory"] = output_directory + if "music_video_directory" in expected_out["overrides"]: + expected_out["overrides"]["music_video_directory"] = output_directory + + assert out == expected_out + + +@pytest.mark.parametrize("resolution_level", ResolutionLevel.all()) +class TestResolution: + def test_resolution_tv_show( + self, + resolution_level: int, + default_config: ConfigFile, + tv_show_subscriptions_path: Path, + output_directory: str, + ): + _ensure_resolved_yaml( + sub=Subscription.from_file_path( + config=default_config, subscription_path=tv_show_subscriptions_path + )[0], + output_directory=output_directory, + preset_type="tv_show", + resolution_level=resolution_level, + ) + + def test_resolution_music( + self, + resolution_level: int, + default_config: ConfigFile, + music_subscriptions_path: Path, + output_directory: str, + ): + _ensure_resolved_yaml( + sub=Subscription.from_file_path( + config=default_config, subscription_path=music_subscriptions_path + )[0], + output_directory=output_directory, + preset_type="music", + resolution_level=resolution_level, + ) + + def test_resolution_music_video( + self, + resolution_level: int, + default_config: ConfigFile, + music_video_subscription_path: Path, + output_directory: str, + ): + _ensure_resolved_yaml( + sub=Subscription.from_file_path( + config=default_config, subscription_path=music_video_subscription_path + )[0], + output_directory=output_directory, + preset_type="music_video", + resolution_level=resolution_level, + ) diff --git a/tests/unit/config/test_subscription_validation.py b/tests/unit/config/test_subscription_validation.py index 3a6fd13d..a333fbb4 100644 --- a/tests/unit/config/test_subscription_validation.py +++ b/tests/unit/config/test_subscription_validation.py @@ -4,8 +4,7 @@ import pytest from ytdl_sub.config.preset import Preset from ytdl_sub.subscriptions.subscription import Subscription -from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException -from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.exceptions import StringFormattingVariableNotFoundException, ValidationException class TestSubscriptionValidation: diff --git a/tests/unit/docgen/test_docgen.py b/tests/unit/docgen/test_docgen.py index 5d326839..5cfb8f92 100644 --- a/tests/unit/docgen/test_docgen.py +++ b/tests/unit/docgen/test_docgen.py @@ -1,5 +1,6 @@ from typing import Type +from tools.docgen.configuration import ConfigurationDocGen from tools.docgen.docgen import DocGen from tools.docgen.entry_variables import EntryVariablesDocGen from tools.docgen.plugins import PluginsDocGen @@ -28,3 +29,6 @@ class TestDocGen: def test_plugins_generated(self): _test_doc_gen(PluginsDocGen) + + def test_configuration_generated(self): + _test_doc_gen(ConfigurationDocGen) diff --git a/tests/unit/entries/conftest.py b/tests/unit/entries/conftest.py index e3302010..76b9c7c6 100644 --- a/tests/unit/entries/conftest.py +++ b/tests/unit/entries/conftest.py @@ -1,7 +1,6 @@ import pytest from ytdl_sub.entries.entry import Entry -from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator @pytest.fixture diff --git a/tests/unit/entries/test_entry.py b/tests/unit/entries/test_entry.py index 07b7a5a3..8f525797 100644 --- a/tests/unit/entries/test_entry.py +++ b/tests/unit/entries/test_entry.py @@ -1,8 +1,7 @@ import pytest from ytdl_sub.entries.entry import Entry -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions v: VariableDefinitions = VARIABLES @@ -32,7 +31,6 @@ class TestEntry(object): month_rev_pad, day_rev_pad, ): - mock_entry_kwargs["upload_date"] = upload_date entry = Entry(entry_dict=mock_entry_kwargs, working_directory=".").initialize_script() assert entry.get(v.upload_year_truncated_reversed, int) == year_rev diff --git a/tests/unit/main/test_main.py b/tests/unit/main/test_main.py index 32ff6d57..3ec5a025 100644 --- a/tests/unit/main/test_main.py +++ b/tests/unit/main/test_main.py @@ -12,16 +12,15 @@ from ytdl_sub.cli.parsers.main import DEFAULT_CONFIG_FILE_NAME from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.file_handler import FileHandler -from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.logger import LoggerLevels +from ytdl_sub.utils.logger import Logger, LoggerLevels @pytest.fixture def expected_uncaught_error_message(): return ( - f"Version %s\nPlease upload the error log file '%s' and make a " - f"Github issue at https://github.com/jmbannon/ytdl-sub/issues with your config and " - f"command/subscription yaml file to reproduce. Thanks for trying ytdl-sub!" + "Version %s\nPlease upload the error log file '%s' and make a " + "Github issue at https://github.com/jmbannon/ytdl-sub/issues with your config and " + "command/subscription yaml file to reproduce. Thanks for trying ytdl-sub!" ) diff --git a/tests/unit/plugins/test_date_range.py b/tests/unit/plugins/test_date_range.py index a024d8e2..81e5fc2e 100644 --- a/tests/unit/plugins/test_date_range.py +++ b/tests/unit/plugins/test_date_range.py @@ -1,6 +1,5 @@ import re -from typing import Any -from typing import Dict +from typing import Any, Dict import pytest from conftest import get_match_filters @@ -73,7 +72,7 @@ class TestDateRange: subscription=sub, dry_run=False, download_filters=False ) assert metadata_filter == [ - f"!is_live & !is_upcoming & !post_live & upload_date < 20250530 & upload_date >= 20250510" + "!is_live & !is_upcoming & !post_live & upload_date < 20250530 & upload_date >= 20250510" ] assert not metadata_breaking_filter diff --git a/tests/unit/plugins/test_ytdl_options.py b/tests/unit/plugins/test_ytdl_options.py index bfeb0ef1..959d9b74 100644 --- a/tests/unit/plugins/test_ytdl_options.py +++ b/tests/unit/plugins/test_ytdl_options.py @@ -1,13 +1,16 @@ import re -from typing import Any -from typing import Dict +from typing import Any, Dict import pytest -from conftest import get_match_filters +import yt_dlp +from conftest import assert_logs from ytdl_sub.config.config_file import ConfigFile +from ytdl_sub.downloaders.ytdlp import YTDLP from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.ffmpeg import FFMPEG +from ytdl_sub.utils.file_path import FilePathTruncater @pytest.fixture @@ -19,6 +22,41 @@ def preset_dict(output_directory) -> Dict[str, Any]: class TestYtdlOptions: + def test_ytdl_options_are_strings( + self, + default_config: ConfigFile, + preset_dict: Dict[str, Any], + working_directory, + ): + expected_ytdl_options = { + "ignoreerrors": True, + "outtmpl": FilePathTruncater.to_native_filepath( + f"{working_directory}/test_ytdl_options/%(id)S.%(ext)s" + ), + "writethumbnail": False, + "ffmpeg_location": FFMPEG.ffmpeg_path(), + "match_filter": yt_dlp.utils.match_filter_func( + ["!is_live & !is_upcoming & !post_live"], [] + ), + "skip_download": True, + "writeinfojson": True, + "extract_flat": "discard", + } + + with ( + assert_logs( + logger=YTDLP.logger, + expected_message=f"ytdl_options: {str(expected_ytdl_options)}", + log_level="debug", + expected_occurrences=1, + ), + ): + _ = Subscription.from_dict( + config=default_config, + preset_name="test_ytdl_options", + preset_dict=preset_dict, + ).download(dry_run=True) + def test_cookiefile_does_not_exist( self, default_config: ConfigFile, @@ -36,3 +74,36 @@ class TestYtdlOptions: preset_name="test_ytdl_options", preset_dict=preset_dict, ).download(dry_run=False) + + def test_ytdl_option_types_preserved( + self, + default_config: ConfigFile, + output_directory: str, + ): + preset_dict = { + "download": "https://your.name.here", + "output_options": {"output_directory": output_directory, "file_name": "will_error.mp4"}, + "ytdl_options": { + "break_on_existing": True, + "js_runtimes": {"deno": {"path": "/usr/local/bin/{dnope}"}}, + "string_path": "verify overrides: {test_string}", + "list_test": ["hmmm"], + }, + "overrides": {"test_string": "hi", "dnope": "deno"}, + } + + sub = Subscription.from_dict( + config=default_config, + preset_name="test_ytdl_options", + preset_dict=preset_dict, + ) + + out = sub.ytdl_options.to_native_dict(sub.overrides) + expected = { + "break_on_existing": True, + "js_runtimes": {"deno": {"path": "/usr/local/bin/deno"}}, + "string_path": "verify overrides: hi", + "list_test": ["hmmm"], + } + + assert out == expected diff --git a/tests/unit/prebuilt_presets/test_tv_show_by_date.py b/tests/unit/prebuilt_presets/test_tv_show_by_date.py index 115a2a61..7f8d7042 100644 --- a/tests/unit/prebuilt_presets/test_tv_show_by_date.py +++ b/tests/unit/prebuilt_presets/test_tv_show_by_date.py @@ -1,13 +1,13 @@ import re import pytest +import yaml from ytdl_sub.script.utils.exceptions import UserThrownRuntimeError from ytdl_sub.subscriptions.subscription import Subscription class TestTvShowByDatePreset: - def test_s01_error_thrown(self, default_config): with pytest.raises( UserThrownRuntimeError, @@ -25,57 +25,67 @@ class TestTvShowByDatePreset: }, ) - def test_backward_compatibility_single(self, default_config): - a = Subscription.from_dict( - config=default_config, - preset_name="a", - preset_dict={ - "preset": "Jellyfin TV Show by Date", - "overrides": {"tv_show_directory": "abc", "url": "test_1"}, - }, - ) - - b = Subscription.from_dict( - config=default_config, - preset_name="a", - preset_dict={ - "preset": "Jellyfin TV Show by Date", - "overrides": {"tv_show_directory": "abc", "subscription_value": "test_1"}, - }, - ) - - assert a.resolved_yaml() == b.resolved_yaml() - - def test_backward_compatibility_multi(self, default_config): - a = Subscription.from_dict( - config=default_config, - preset_name="a", - preset_dict={ - "preset": "Jellyfin TV Show by Date", - "overrides": {"tv_show_directory": "abc", "url": "test_1", "url2": "test_2"}, - }, - ) - - b = Subscription.from_dict( - config=default_config, - preset_name="a", - preset_dict={ - "preset": "Jellyfin TV Show by Date", - "overrides": { - "tv_show_directory": "abc", - "subscription_array": ["test_1", "test_2"], + def test_backward_compatibility_single_download(self, default_config): + a = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "url": "test_1"}, }, - }, + ).resolved_yaml() ) - c = Subscription.from_dict( - config=default_config, - preset_name="a", - preset_dict={ - "preset": "Jellyfin TV Show by Date", - "overrides": {"tv_show_directory": "abc", "urls": ["test_1", "test_2"]}, - }, + b = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "subscription_value": "test_1"}, + }, + ).resolved_yaml() ) - assert a.resolved_yaml() == b.resolved_yaml() - assert a.resolved_yaml() == c.resolved_yaml() + assert a["download"] == b["download"] + + def test_backward_compatibility_multi_download(self, default_config): + a = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "url": "test_1", "url2": "test_2"}, + }, + ).resolved_yaml() + ) + + b = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": { + "tv_show_directory": "abc", + "subscription_array": ["test_1", "test_2"], + }, + }, + ).resolved_yaml() + ) + + c = yaml.safe_load( + Subscription.from_dict( + config=default_config, + preset_name="a", + preset_dict={ + "preset": "Jellyfin TV Show by Date", + "overrides": {"tv_show_directory": "abc", "urls": ["test_1", "test_2"]}, + }, + ).resolved_yaml() + ) + + assert a["download"] == b["download"] + assert a["download"] == c["download"] diff --git a/tests/unit/prebuilt_presets/test_tv_show_collection.py b/tests/unit/prebuilt_presets/test_tv_show_collection.py index 07e30006..f8ecb2da 100644 --- a/tests/unit/prebuilt_presets/test_tv_show_collection.py +++ b/tests/unit/prebuilt_presets/test_tv_show_collection.py @@ -7,7 +7,6 @@ from ytdl_sub.subscriptions.subscription import Subscription class TestTvShowCollectionPreset: - def test_url_error_thrown(self, default_config): with pytest.raises( UserThrownRuntimeError, @@ -57,12 +56,13 @@ class TestTvShowCollectionPreset: # is_bilateral if i == 0: - url = sub.overrides.apply_overrides_formatter_to_native( + url = sub.overrides.apply_formatter( url_list[itr].url, function_overrides={ # mock so bilateral url gets enabled "subscription_has_download_archive": "True" }, + expected_type=list, ) assert url == [ f"youtube.com/playlist?url_{season_num}_{i}" @@ -81,12 +81,13 @@ class TestTvShowCollectionPreset: # not bilateral else: for j in range(2): - url = sub.overrides.apply_overrides_formatter_to_native( + url = sub.overrides.apply_formatter( url_list[itr + j].url, function_overrides={ # mock so bilateral url gets enabled "subscription_has_download_archive": "True" }, + expected_type=list, ) # First instance is the first url to get thumbnails diff --git a/tests/unit/script/functions/test_conditional_functions.py b/tests/unit/script/functions/test_conditional_functions.py index eff73214..d2078a80 100644 --- a/tests/unit/script/functions/test_conditional_functions.py +++ b/tests/unit/script/functions/test_conditional_functions.py @@ -3,6 +3,9 @@ import re import pytest from unit.script.conftest import single_variable_output +from ytdl_sub.script.script import Script +from ytdl_sub.script.types.syntax_tree import SyntaxTree +from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import FunctionRuntimeException @@ -135,3 +138,29 @@ class TestConditionalFunction: ): output = single_variable_output(function_str) assert output == expected_output + + def test_if_partial_resolve(self): + assert ( + Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%if( true, aa, bb )}", + } + ) + .resolve_partial(unresolvable={"bb"}) + .get("cc") + .native + == "a" + ) + + def test_if_partial_resolve_unresolved(self): + assert Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%if( false, aa, bb )}", + } + ).resolve_partial(unresolvable={"bb"}).definition_of("cc") == SyntaxTree( + ast=[Variable("bb")] + ) diff --git a/tests/unit/script/functions/test_date_functions.py b/tests/unit/script/functions/test_date_functions.py index 1c826ea4..e9406e35 100644 --- a/tests/unit/script/functions/test_date_functions.py +++ b/tests/unit/script/functions/test_date_functions.py @@ -1,8 +1,6 @@ import pytest from unit.script.conftest import single_variable_output -from ytdl_sub.script.script import Script - class TestNumericFunctions: @pytest.mark.parametrize( diff --git a/tests/unit/script/functions/test_map_functions.py b/tests/unit/script/functions/test_map_functions.py index befc611e..750bc02f 100644 --- a/tests/unit/script/functions/test_map_functions.py +++ b/tests/unit/script/functions/test_map_functions.py @@ -4,9 +4,11 @@ import pytest from unit.script.conftest import single_variable_output from ytdl_sub.script.script import Script -from ytdl_sub.script.utils.exceptions import FunctionRuntimeException -from ytdl_sub.script.utils.exceptions import KeyDoesNotExistRuntimeException -from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException +from ytdl_sub.script.utils.exceptions import ( + FunctionRuntimeException, + KeyDoesNotExistRuntimeException, + KeyNotHashableRuntimeException, +) class TestMapFunctions: diff --git a/tests/unit/script/functions/test_print_functions.py b/tests/unit/script/functions/test_print_functions.py index 56500d16..a85d15e7 100644 --- a/tests/unit/script/functions/test_print_functions.py +++ b/tests/unit/script/functions/test_print_functions.py @@ -1,6 +1,5 @@ import logging -from typing import Any -from typing import Optional +from typing import Any, Optional from unittest.mock import patch import pytest diff --git a/tests/unit/script/functions/test_regex_functions.py b/tests/unit/script/functions/test_regex_functions.py index b284b865..1f78ae82 100644 --- a/tests/unit/script/functions/test_regex_functions.py +++ b/tests/unit/script/functions/test_regex_functions.py @@ -66,7 +66,7 @@ class TestRegexFunctions: match="no regex strings were captured for input string the string", ): single_variable_output( - f"""{{ + """{ %regex_capture_many( "the string", [ @@ -74,7 +74,7 @@ class TestRegexFunctions: "not here either (.*)" ] ) - }}""" + }""" ) def test_regex_capture_many_fails_unequal_capture_groups(self): @@ -83,7 +83,7 @@ class TestRegexFunctions: match="regex_array elements must contain the same number of capture groups", ): single_variable_output( - f"""{{ + """{ %regex_capture_many( "the string", [ @@ -91,5 +91,5 @@ class TestRegexFunctions: "(.*) not equal to 1 (.*)" ] ) - }}""" + }""" ) diff --git a/tests/unit/script/functions/test_string_functions.py b/tests/unit/script/functions/test_string_functions.py index bc5bfc32..02aafad1 100644 --- a/tests/unit/script/functions/test_string_functions.py +++ b/tests/unit/script/functions/test_string_functions.py @@ -1,5 +1,4 @@ -from typing import List -from typing import Optional +from typing import List, Optional import pytest from unit.script.conftest import single_variable_output diff --git a/tests/unit/script/test_parser.py b/tests/unit/script/test_parser.py index c6694562..2174129b 100644 --- a/tests/unit/script/test_parser.py +++ b/tests/unit/script/test_parser.py @@ -1,23 +1,19 @@ import re -from typing import Optional -from typing import Union +from typing import Optional, Union import pytest -from ytdl_sub.script.parser import _UNEXPECTED_CHAR_ARGUMENT -from ytdl_sub.script.parser import BRACKET_INVALID_CHAR -from ytdl_sub.script.parser import BRACKET_NOT_CLOSED -from ytdl_sub.script.parser import ParsedArgType -from ytdl_sub.script.parser import parse +from ytdl_sub.script.parser import ( + _UNEXPECTED_CHAR_ARGUMENT, + BRACKET_INVALID_CHAR, + BRACKET_NOT_CLOSED, + ParsedArgType, + parse, +) from ytdl_sub.script.types.array import UnresolvedArray from ytdl_sub.script.types.function import BuiltInFunction -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import Lambda -from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree -from ytdl_sub.script.types.syntax_tree import SyntaxTree +from ytdl_sub.script.types.resolvable import Boolean, Float, Integer, Lambda, String +from ytdl_sub.script.types.syntax_tree import ResolvedSyntaxTree, SyntaxTree from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import InvalidSyntaxException diff --git a/tests/unit/script/types/test_array.py b/tests/unit/script/types/test_array.py index f816b1bd..6de29d94 100644 --- a/tests/unit/script/types/test_array.py +++ b/tests/unit/script/types/test_array.py @@ -2,14 +2,17 @@ import re import pytest -from ytdl_sub.script.parser import _UNEXPECTED_CHAR_ARGUMENT -from ytdl_sub.script.parser import _UNEXPECTED_COMMA_ARGUMENT -from ytdl_sub.script.parser import ParsedArgType +from ytdl_sub.script.parser import ( + _UNEXPECTED_CHAR_ARGUMENT, + _UNEXPECTED_COMMA_ARGUMENT, + ParsedArgType, +) from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.array import Array -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Float, String +from ytdl_sub.script.types.syntax_tree import SyntaxTree +from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import InvalidSyntaxException @@ -95,7 +98,7 @@ class TestArray: @pytest.mark.parametrize( "array", [ - "{]}" "{ ]}", + "{]}{ ]}", "{\n]}", ], ) @@ -117,3 +120,29 @@ class TestArray: ).resolve() == ScriptOutput( {"aa": String("a"), "bb": String("b"), "cc": String('return ["a", "b"]')} ) + + def test_partial_resolve(self): + assert ( + Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%array_at( [aa, bb], 0 )}", + } + ) + .resolve_partial(unresolvable={"bb"}) + .get("cc") + .native + == "a" + ) + + def test_partial_resolve_unresolved(self): + assert Script( + { + "aa": "a", + "bb": "unresolvable!", + "cc": "{%array_at( [aa, bb], 1 )}", + } + ).resolve_partial(unresolvable={"bb"}).definition_of("cc") == SyntaxTree( + ast=[Variable("bb")] + ) diff --git a/tests/unit/script/types/test_bool.py b/tests/unit/script/types/test_bool.py index 5de7ddb1..9dd4316a 100644 --- a/tests/unit/script/types/test_bool.py +++ b/tests/unit/script/types/test_bool.py @@ -5,8 +5,7 @@ import pytest from ytdl_sub.script.parser import BOOLEAN_ONLY_ARGS from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Boolean, String from ytdl_sub.script.utils.exceptions import InvalidSyntaxException diff --git a/tests/unit/script/types/test_custom_function.py b/tests/unit/script/types/test_custom_function.py index 61096cc3..89987eca 100644 --- a/tests/unit/script/types/test_custom_function.py +++ b/tests/unit/script/types/test_custom_function.py @@ -5,12 +5,16 @@ import pytest from ytdl_sub.script.parser import CUSTOM_FUNCTION_ARGUMENTS_ONLY_ARGS from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput +from ytdl_sub.script.types.function import CustomFunction from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.utils.exceptions import CycleDetected -from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist -from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArgumentName -from ytdl_sub.script.utils.exceptions import InvalidCustomFunctionArguments -from ytdl_sub.script.utils.exceptions import InvalidSyntaxException +from ytdl_sub.script.types.syntax_tree import SyntaxTree +from ytdl_sub.script.utils.exceptions import ( + CycleDetected, + FunctionDoesNotExist, + InvalidCustomFunctionArgumentName, + InvalidCustomFunctionArguments, + InvalidSyntaxException, +) class TestCustomFunction: @@ -251,3 +255,30 @@ class TestCustomFunction: "output": "{%mul(%func1(1), 1)}", } ) + + def test_partial_resolve_custom_functions_any_order_via_init(self): + assert ( + Script( + { + "%custom_cubed": "{%mul(%custom_square($0),$0)}", + "%custom_square": "{%mul($0, $0)}", + "output": "{%custom_cubed(3)}", + } + ) + .resolve_partial() + .get("output") + .native + == 27 + ) + + def test_partial_resolve_unresolved(self): + assert Script( + { + "aa": "nope", + "%custom_cubed": "{%mul(%custom_square($0),$0)}", + "%custom_square": "{%mul($0, aa)}", + "output": "{%custom_cubed(3)}", + } + ).resolve_partial(unresolvable={"aa"}).definition_of("output") == SyntaxTree( + ast=[CustomFunction(name="custom_cubed", args=[Integer(3)])] + ) diff --git a/tests/unit/script/types/test_float.py b/tests/unit/script/types/test_float.py index e285ce17..7f4ab8f8 100644 --- a/tests/unit/script/types/test_float.py +++ b/tests/unit/script/types/test_float.py @@ -2,12 +2,10 @@ import re import pytest -from ytdl_sub.script.parser import NUMERICS_INVALID_CHAR -from ytdl_sub.script.parser import NUMERICS_ONLY_ARGS +from ytdl_sub.script.parser import NUMERICS_INVALID_CHAR, NUMERICS_ONLY_ARGS from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Float, String from ytdl_sub.script.utils.exceptions import InvalidSyntaxException diff --git a/tests/unit/script/types/test_function.py b/tests/unit/script/types/test_function.py index e13828f4..519078f3 100644 --- a/tests/unit/script/types/test_function.py +++ b/tests/unit/script/types/test_function.py @@ -7,10 +7,12 @@ from ytdl_sub.script.functions import Functions from ytdl_sub.script.parser import FUNCTION_INVALID_CHAR from ytdl_sub.script.script import Script from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist -from ytdl_sub.script.utils.exceptions import FunctionRuntimeException -from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments -from ytdl_sub.script.utils.exceptions import InvalidSyntaxException +from ytdl_sub.script.utils.exceptions import ( + FunctionDoesNotExist, + FunctionRuntimeException, + IncompatibleFunctionArguments, + InvalidSyntaxException, +) def _incompatible_arguments_match(expected: str, recieved: str) -> str: @@ -95,7 +97,7 @@ class TestFunction: def test_register_function(self): try: Functions.register_function(function=mock_register_function) - output = single_variable_output(f"{{%mock_register_function(10)}}") + output = single_variable_output("{%mock_register_function(10)}") assert output == 110 finally: del Functions._custom_functions[mock_register_function.__name__] diff --git a/tests/unit/script/types/test_integer.py b/tests/unit/script/types/test_integer.py index 18747e11..4f686849 100644 --- a/tests/unit/script/types/test_integer.py +++ b/tests/unit/script/types/test_integer.py @@ -2,13 +2,10 @@ import re import pytest -from ytdl_sub.script.parser import NUMERICS_INVALID_CHAR -from ytdl_sub.script.parser import NUMERICS_ONLY_ARGS +from ytdl_sub.script.parser import NUMERICS_INVALID_CHAR, NUMERICS_ONLY_ARGS from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput -from ytdl_sub.script.types.resolvable import Boolean -from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.types.resolvable import Integer, String from ytdl_sub.script.utils.exceptions import InvalidSyntaxException diff --git a/tests/unit/script/types/test_lambda_function.py b/tests/unit/script/types/test_lambda_function.py index cd054811..f40e7d82 100644 --- a/tests/unit/script/types/test_lambda_function.py +++ b/tests/unit/script/types/test_lambda_function.py @@ -6,8 +6,7 @@ from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.resolvable import Integer -from ytdl_sub.script.utils.exceptions import CycleDetected -from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments +from ytdl_sub.script.utils.exceptions import CycleDetected, IncompatibleFunctionArguments class TestLambdaFunction: @@ -177,3 +176,20 @@ class TestLambdaFunctionIncompatibleNumArguments: match=re.escape("Cycle detected within these variables: two -> %times_two -> two"), ): Script({"%times_two": "{%mul($0, two)}", "two": "{%times_two(2)}"}) + + def test_partial_resolve_nested_lambda_custom_functions_within_custom_functions(self): + assert ( + Script( + { + "%nest4": "{%mul($0, 2)}", + "%nest3": "{%array_at(%array_apply([$0], %nest4), 0)}", + "%nest2": "{%array_at(%array_apply([$0], %nest3), 0)}", + "%nest1": "{%array_at(%array_apply([$0], %nest2), 0)}", + "output": "{%array_at(%array_apply([2], %nest1), 0)}", + } + ) + .resolve_partial() + .get("output") + .native + == 4 + ) diff --git a/tests/unit/script/types/test_map.py b/tests/unit/script/types/test_map.py index 576eba35..8a29d3c0 100644 --- a/tests/unit/script/types/test_map.py +++ b/tests/unit/script/types/test_map.py @@ -2,20 +2,20 @@ import re import pytest -from ytdl_sub.script.parser import _UNEXPECTED_COMMA_ARGUMENT -from ytdl_sub.script.parser import BRACKET_NOT_CLOSED -from ytdl_sub.script.parser import MAP_KEY_MULTIPLE_VALUES -from ytdl_sub.script.parser import MAP_KEY_NOT_HASHABLE -from ytdl_sub.script.parser import MAP_KEY_WITH_NO_VALUE -from ytdl_sub.script.parser import MAP_MISSING_KEY -from ytdl_sub.script.parser import ParsedArgType +from ytdl_sub.script.parser import ( + _UNEXPECTED_COMMA_ARGUMENT, + BRACKET_NOT_CLOSED, + MAP_KEY_MULTIPLE_VALUES, + MAP_KEY_NOT_HASHABLE, + MAP_KEY_WITH_NO_VALUE, + MAP_MISSING_KEY, + ParsedArgType, +) from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.map import Map -from ytdl_sub.script.types.resolvable import Float -from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.utils.exceptions import InvalidSyntaxException -from ytdl_sub.script.utils.exceptions import KeyNotHashableRuntimeException +from ytdl_sub.script.types.resolvable import Float, String +from ytdl_sub.script.utils.exceptions import InvalidSyntaxException, KeyNotHashableRuntimeException class TestMap: diff --git a/tests/unit/script/types/test_string.py b/tests/unit/script/types/test_string.py index 11ad9065..28a4cb62 100644 --- a/tests/unit/script/types/test_string.py +++ b/tests/unit/script/types/test_string.py @@ -3,8 +3,7 @@ import re import pytest from unit.script.conftest import single_variable_output -from ytdl_sub.script.parser import STRINGS_NOT_CLOSED -from ytdl_sub.script.parser import STRINGS_ONLY_ARGS +from ytdl_sub.script.parser import STRINGS_NOT_CLOSED, STRINGS_ONLY_ARGS from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.resolvable import String diff --git a/tests/unit/script/types/test_variable.py b/tests/unit/script/types/test_variable.py index 526e1476..166c8339 100644 --- a/tests/unit/script/types/test_variable.py +++ b/tests/unit/script/types/test_variable.py @@ -5,9 +5,11 @@ import pytest from ytdl_sub.script.script import Script from ytdl_sub.script.script_output import ScriptOutput from ytdl_sub.script.types.resolvable import String -from ytdl_sub.script.utils.exceptions import CycleDetected -from ytdl_sub.script.utils.exceptions import InvalidVariableName -from ytdl_sub.script.utils.exceptions import VariableDoesNotExist +from ytdl_sub.script.utils.exceptions import ( + CycleDetected, + InvalidVariableName, + VariableDoesNotExist, +) class TestVariable: diff --git a/tests/unit/utils/test_chapters.py b/tests/unit/utils/test_chapters.py index f695a4d0..c7cd5936 100644 --- a/tests/unit/utils/test_chapters.py +++ b/tests/unit/utils/test_chapters.py @@ -1,7 +1,6 @@ import pytest -from ytdl_sub.utils.chapters import Chapters -from ytdl_sub.utils.chapters import Timestamp +from ytdl_sub.utils.chapters import Chapters, Timestamp class TestTimestamp: diff --git a/tests/unit/utils/test_file_handler.py b/tests/unit/utils/test_file_handler.py index 80505e24..44cc01af 100644 --- a/tests/unit/utils/test_file_handler.py +++ b/tests/unit/utils/test_file_handler.py @@ -1,11 +1,8 @@ -import pytest - from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.system import IS_WINDOWS class TestFileHandler: - def test_directory_exists(self): if IS_WINDOWS: return diff --git a/tests/unit/utils/test_logger.py b/tests/unit/utils/test_logger.py index 1d39de5b..6148eaac 100644 --- a/tests/unit/utils/test_logger.py +++ b/tests/unit/utils/test_logger.py @@ -3,8 +3,7 @@ import time import pytest -from ytdl_sub.utils.logger import Logger -from ytdl_sub.utils.logger import LoggerLevels +from ytdl_sub.utils.logger import Logger, LoggerLevels class TestLogger: diff --git a/tests/unit/utils/test_script_utils.py b/tests/unit/utils/test_script_utils.py index 608066b5..792020ba 100644 --- a/tests/unit/utils/test_script_utils.py +++ b/tests/unit/utils/test_script_utils.py @@ -81,9 +81,9 @@ class TestScriptUtils: String(value="static_a"): BuiltInFunction( name="concat", args=[ - BuiltInFunction(name="string", args=[String(value="string with ")]), - BuiltInFunction(name="string", args=[Variable(name="var_c")]), - BuiltInFunction(name="string", args=[String(value=" in it")]), + String(value="string with "), + Variable(name="var_c"), + String(value=" in it"), ], ), String(value="quotes"): String(value="has '' and \"\""), diff --git a/tests/unit/utils/test_yaml.py b/tests/unit/utils/test_yaml.py index 7d0fd284..41553e5b 100644 --- a/tests/unit/utils/test_yaml.py +++ b/tests/unit/utils/test_yaml.py @@ -1,11 +1,9 @@ -import os import re import tempfile import pytest -from ytdl_sub.utils.exceptions import FileNotFoundException -from ytdl_sub.utils.exceptions import InvalidYamlException +from ytdl_sub.utils.exceptions import FileNotFoundException, InvalidYamlException from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.yaml import load_yaml diff --git a/tests/unit/validators/test_dict_validator.py b/tests/unit/validators/test_dict_validator.py index e1eb2aeb..5437522e 100644 --- a/tests/unit/validators/test_dict_validator.py +++ b/tests/unit/validators/test_dict_validator.py @@ -1,10 +1,12 @@ import pytest from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.validators.validators import BoolValidator -from ytdl_sub.validators.validators import DictValidator -from ytdl_sub.validators.validators import LiteralDictValidator -from ytdl_sub.validators.validators import StringValidator +from ytdl_sub.validators.validators import ( + BoolValidator, + DictValidator, + LiteralDictValidator, + StringValidator, +) class TestDictValidator: diff --git a/tests/unit/validators/test_regex_validator.py b/tests/unit/validators/test_regex_validator.py index c1398cd1..170c57fb 100644 --- a/tests/unit/validators/test_regex_validator.py +++ b/tests/unit/validators/test_regex_validator.py @@ -1,8 +1,7 @@ import pytest from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.validators.regex_validator import RegexListValidator -from ytdl_sub.validators.regex_validator import RegexValidator +from ytdl_sub.validators.regex_validator import RegexListValidator, RegexValidator @pytest.mark.parametrize( diff --git a/tests/unit/validators/test_string_formatter_validator.py b/tests/unit/validators/test_string_formatter_validator.py index 9bb4d1e0..a1a05b85 100644 --- a/tests/unit/validators/test_string_formatter_validator.py +++ b/tests/unit/validators/test_string_formatter_validator.py @@ -1,14 +1,12 @@ import pytest -from yt_dlp.utils import sanitize_filename -from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import ValidationException -from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator -from ytdl_sub.validators.string_formatter_validators import OverridesDictFormatterValidator -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 from ytdl_sub.validators.string_formatter_validators import ( + DictFormatterValidator, + OverridesDictFormatterValidator, + OverridesStringFormatterValidator, + StringFormatterValidator, + UnstructuredDictFormatterValidator, UnstructuredOverridesDictFormatterValidator, ) @@ -115,12 +113,12 @@ class TestUnstructuredDictFormatterValidator(object): assert len(validator.dict) == 8 assert all(isinstance(val, expected_formatter_class) for val in validator.dict.values()) assert validator.dict_with_format_strings == { - "key1": '{ %concat( %string( "string with " ), %string( variable ) ) }', + "key1": '{ %concat( "string with ", variable ) }', "key2": "no variables", "key3": "{ %int(3) }", "key4": "{ %float(4.132) }", "key5": "{ %bool(True) }", - "key6": '{ { %concat( %string( variable ), %string( "_key" ) ): "value", "static_key": %concat( %string( variable ), %string( "_value" ) ) } }', - "key7": '{ [ "list_1", %concat( %string( "list_" ), %string( variable_2 ) ) ] }', - "key8": '{ %concat( %string( "string " ), %string( variable1 ), %string( " with multiple " ), %string( variable2 ) ) }', + "key6": '{ { %concat( variable, "_key" ): "value", "static_key": %concat( variable, "_value" ) } }', + "key7": '{ [ "list_1", %concat( "list_", variable_2 ) ] }', + "key8": '{ %concat( "string ", variable1, " with multiple ", variable2 ) }', } diff --git a/tools/docgen/configuration.py b/tools/docgen/configuration.py new file mode 100644 index 00000000..9f3b6c24 --- /dev/null +++ b/tools/docgen/configuration.py @@ -0,0 +1,35 @@ +from pathlib import Path + +from tools.docgen.docgen import DocGen +from tools.docgen.utils import generate_options_validator_docs +from ytdl_sub.config.config_validator import ConfigOptions +from ytdl_sub.config.preset import Preset + + +class ConfigurationDocGen(DocGen): + LOCATION = Path("docs/source/config_reference/config_yaml.rst") + DOCSTRING_LOCATION = ( + "The respective function docstrings within ytdl_sub/config/config_validator.py" + ) + + @classmethod + def generate(cls): + docs = generate_options_validator_docs( + name="Configuration File", + options=ConfigOptions, + offset=0, + skip_properties=False, + recurse_property_options=True, + property_sections=True, + ) + + docs += generate_options_validator_docs( + name="Presets", + options=Preset, + offset=0, + skip_properties=True, + recurse_property_options=False, + property_sections=False, + ) + + return docs diff --git a/tools/docgen/entry_variables.py b/tools/docgen/entry_variables.py index 4f46c452..f19f5a2f 100644 --- a/tools/docgen/entry_variables.py +++ b/tools/docgen/entry_variables.py @@ -1,16 +1,15 @@ from pathlib import Path -from typing import Any -from typing import Dict -from typing import Type +from typing import Any, Dict, Type from tools.docgen.docgen import DocGen -from tools.docgen.utils import cached_properties -from tools.docgen.utils import camel_case_to_human -from tools.docgen.utils import get_function_docs -from tools.docgen.utils import line_section -from tools.docgen.utils import section -from ytdl_sub.entries.script.variable_definitions import VARIABLES -from ytdl_sub.entries.script.variable_definitions import VariableDefinitions +from tools.docgen.utils import ( + cached_properties, + camel_case_to_human, + get_function_docs, + line_section, + section, +) +from ytdl_sub.entries.script.variable_definitions import VARIABLES, VariableDefinitions def _variable_class_to_name(obj: Type[Any]) -> str: @@ -23,7 +22,6 @@ def _variable_class_to_name(obj: Type[Any]) -> str: class EntryVariablesDocGen(DocGen): - LOCATION = Path("docs/source/config_reference/scripting/entry_variables.rst") DOCSTRING_LOCATION = "src/ytdl_sub/entries/script/variable_definitions.py" diff --git a/tools/docgen/plugins.py b/tools/docgen/plugins.py index d2be80d3..73ccdc93 100644 --- a/tools/docgen/plugins.py +++ b/tools/docgen/plugins.py @@ -1,77 +1,28 @@ -import inspect from pathlib import Path -from typing import Any -from typing import Dict -from typing import Type +from typing import Dict, Set, Type from tools.docgen.docgen import DocGen -from tools.docgen.utils import line_section -from tools.docgen.utils import properties -from tools.docgen.utils import section +from tools.docgen.utils import generate_options_validator_docs, line_section, section from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.plugin.plugin_mapping import PluginMapping -from ytdl_sub.config.preset_options import OutputOptions -from ytdl_sub.config.preset_options import YTDLOptions +from ytdl_sub.config.preset_options import OutputOptions, YTDLOptions from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.downloaders.url.validators import MultiUrlValidator - -def should_filter_all_properties(plugin_name: str) -> bool: - return plugin_name in ( - "format", - "match_filters", - "music_tags", - "filter_include", - "filter_exclude", - "embed_thumbnail", - "square_thumbnail", - "video_tags", - "download", - ) - - -def should_filter_property(property_name: str) -> bool: - return property_name.startswith("_") or property_name in ( - "value", - "source_variable_capture_dict", - "dict", - "keys", - "dict_with_format_strings", - "dict_with_parsed_format_strings", - "subscription_name", - "list", - "script", - "unresolvable", - "leaf_name", - ) - - -def get_function_docs(function_name: str, obj: Any, level: int) -> str: - docs = f"\n``{function_name}``\n\n" - docs += inspect.cleandoc(getattr(obj, function_name).__doc__) - docs += "\n\n" - return docs - - -def generate_plugin_docs(name: str, options: Type[OptionsValidator], offset: int) -> str: - docs = "" - docs += section(name, level=offset + 0) - - docs += inspect.cleandoc(options.__doc__) - docs += "\n" - - if should_filter_all_properties(name): - return docs - - property_names = [prop for prop in properties(options) if not should_filter_property(prop)] - for property_name in sorted(property_names): - docs += get_function_docs(function_name=property_name, obj=options, level=offset + 1) - - return docs +PLUGIN_NAMES_TO_SKIP_PROPERTIES: Set[str] = { + "format", + "match_filters", + "music_tags", + "filter_include", + "filter_exclude", + "embed_thumbnail", + "square_thumbnail", + "video_tags", + "download", +} class PluginsDocGen(DocGen): - LOCATION = Path("docs/source/config_reference/plugins.rst") DOCSTRING_LOCATION = "The respective plugin files under src/ytdl_sub/plugins/" @@ -91,6 +42,11 @@ class PluginsDocGen(DocGen): docs = section("Plugins", level=0) for idx, name in enumerate(sorted(options_dict.keys())): docs += line_section(section_idx=idx) - docs += generate_plugin_docs(name, options_dict[name], offset=1) + docs += generate_options_validator_docs( + name=name, + options=options_dict[name], + offset=1, + skip_properties=name in PLUGIN_NAMES_TO_SKIP_PROPERTIES, + ) return docs diff --git a/tools/docgen/scripting_functions.py b/tools/docgen/scripting_functions.py index 78924ca4..b54a653d 100644 --- a/tools/docgen/scripting_functions.py +++ b/tools/docgen/scripting_functions.py @@ -1,15 +1,9 @@ import inspect from pathlib import Path -from typing import Any -from typing import Dict -from typing import Optional -from typing import Type +from typing import Any, Dict, Optional, Type from tools.docgen.docgen import DocGen -from tools.docgen.utils import camel_case_to_human -from tools.docgen.utils import line_section -from tools.docgen.utils import section -from tools.docgen.utils import static_methods +from tools.docgen.utils import camel_case_to_human, line_section, section, static_methods from ytdl_sub.entries.script.custom_functions import CustomFunctions from ytdl_sub.script.functions import Functions from ytdl_sub.script.utils.type_checking import FunctionSpec @@ -54,7 +48,6 @@ def get_function_docstring( class ScriptingFunctionsDocGen(DocGen): - LOCATION = Path("docs/source/config_reference/scripting/scripting_functions.rst") DOCSTRING_LOCATION = "The respective function files under src/ytdl_sub/script/functions/" diff --git a/tools/docgen/static_variables.py b/tools/docgen/static_variables.py index b8b0c315..fd5d1d09 100644 --- a/tools/docgen/static_variables.py +++ b/tools/docgen/static_variables.py @@ -1,14 +1,11 @@ from pathlib import Path from tools.docgen.docgen import DocGen -from tools.docgen.utils import get_function_docs -from tools.docgen.utils import section -from tools.docgen.utils import static_methods +from tools.docgen.utils import get_function_docs, section, static_methods from ytdl_sub.entries.variables.override_variables import SubscriptionVariables class StaticVariablesDocGen(DocGen): - LOCATION = Path("docs/source/config_reference/scripting/static_variables.rst") DOCSTRING_LOCATION = "src/ytdl_sub/entries/variables/override_variables.py" diff --git a/tools/docgen/utils.py b/tools/docgen/utils.py index 81ddc3fa..e74372af 100644 --- a/tools/docgen/utils.py +++ b/tools/docgen/utils.py @@ -1,17 +1,51 @@ import inspect from functools import cached_property -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Type +from typing import Any, Dict, List, Optional, Type + +from ytdl_sub.script.utils.type_checking import get_optional_type, is_optional +from ytdl_sub.validators.validators import Validator LEVEL_CHARS: Dict[int, str] = {0: "=", 1: "-", 2: "~", 3: "^"} -def section(name: str, level: int, as_code: bool = False) -> str: - if as_code: - name = f"``{name}``" +def _should_filter_property(property_name: str) -> bool: + return property_name.startswith("_") or property_name in ( + "value", + "source_variable_capture_dict", + "dict", + "keys", + "dict_with_format_strings", + "subscription_name", + "list", + "script", + "unresolvable", + "dict_with_parsed_format_strings", + "leaf_name", + ) + + +def _is_validator_property( + options: Type[Validator], property_name: str +) -> Optional[Type[Validator]]: + property_return_type = inspect.getfullargspec(getattr(options, property_name).fget).annotations[ + "return" + ] + if is_optional(property_return_type): + property_return_type = get_optional_type(property_return_type) + + try: + if issubclass(property_return_type, Validator): + return property_return_type + except TypeError: + return None + + return None + + +def section(name: str, level: Optional[int]) -> str: + if level is None: + return f"\n{name}\n\n" + return f"\n{name}\n{len(name) * LEVEL_CHARS[level]}\n" @@ -40,10 +74,20 @@ def camel_case_to_human(string: str) -> str: return output_str +def line() -> str: + return "\n" + ("-" * 100) + "\n" + + +def line_section(section_idx: int) -> str: + if section_idx > 0: + return line() + return "" + + def get_function_docs( function_name: str, obj: Any, - level: int, + level: Optional[int], display_function_name: Optional[str] = None, pre_docstring: Optional[str] = None, ) -> str: @@ -56,11 +100,42 @@ def get_function_docs( return docs -def line() -> str: - return "\n" + ("-" * 100) + "\n" +def generate_options_validator_docs( + name: str, + options: Type[Validator], + offset: int, + skip_properties: bool, + recurse_property_options: bool = False, + property_sections: bool = False, +) -> str: + docs = "" + docs += section(name, level=offset + 0) + docs += inspect.cleandoc(options.__doc__) + docs += "\n" -def line_section(section_idx: int) -> str: - if section_idx > 0: - return line() - return "" + if skip_properties: + return docs + + property_names = [prop for prop in properties(options) if not _should_filter_property(prop)] + for property_name in sorted(property_names): + maybe_validator_property = ( + _is_validator_property(options, property_name) if recurse_property_options else None + ) + if maybe_validator_property: + docs += generate_options_validator_docs( + name=property_name, + options=maybe_validator_property, + offset=offset + 1, + skip_properties=False, + recurse_property_options=False, + ) + else: + docs += get_function_docs( + function_name=property_name, + obj=options, + level=(offset + 1) if property_sections else None, + display_function_name=f"``{property_name}``" if not property_sections else None, + ) + + return docs diff --git a/tools/linter b/tools/linter deleted file mode 100755 index b3a4fb7f..00000000 --- a/tools/linter +++ /dev/null @@ -1,11 +0,0 @@ -# Run within root directory - -if [[ $1 = "check" ]]; then - isort . --check-only --diff \ - && black . --check \ - && pylint src/ -else - isort . - black . - pylint src/ -fi \ No newline at end of file diff --git a/tools/test b/tools/test deleted file mode 100755 index 445b441c..00000000 --- a/tools/test +++ /dev/null @@ -1,12 +0,0 @@ -# Run within root directory - -if [[ $1 = "html" ]]; then - coverage run -m pytest && coverage report && coverage html -elif [[ $1 = "xml" ]]; then - coverage run -m pytest && coverage report && coverage xml -elif [[ $1 = "unit" ]]; then - coverage run -m pytest tests/unit && coverage report -else - coverage run -m pytest && coverage report -fi -