Merge branch 'master' into j/music-extras

This commit is contained in:
Jesse Bannon 2024-01-05 16:37:00 -08:00
commit bf83361dfc
33 changed files with 656 additions and 219 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -9,7 +9,7 @@ contain reference documentation for each built-in variable and scripting functio
:maxdepth: 1 :maxdepth: 1
entry_variables entry_variables
override_variables static_variables
scripting_functions scripting_functions
scripting_types scripting_types
@ -30,7 +30,7 @@ considered *static* because it does not depend on anything from an entry.
.. code-block:: yaml .. code-block:: yaml
output_options: output_options:
output_directory: "Custom YTDL-SUB TV Show" output_directory: "/path/to/tv_shows/Custom YTDL-SUB TV Show"
Static Variables Static Variables
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
@ -41,7 +41,7 @@ We can use this instead of hard-coding it above:
.. code-block:: yaml .. code-block:: yaml
output_options: output_options:
output_directory: "{subscription_name}" output_directory: "/path/to/tv_shows/{subscription_name}"
The syntax for variable usage is curly-braces with the variable name within it. Assuming The syntax for variable usage is curly-braces with the variable name within it. Assuming
our subscription is actually named "Custom YTDL-SUB TV Show", then ``ytdl-sub`` our subscription is actually named "Custom YTDL-SUB TV Show", then ``ytdl-sub``
@ -64,7 +64,7 @@ title in its name. We can do that using entry variables:
.. code-block:: yaml .. code-block:: yaml
output_options: output_options:
output_directory: "{subscription_name}" output_directory: "/path/to/tv_shows/{subscription_name}"
file_name: "{title}.{ext}" file_name: "{title}.{ext}"
thumbnail_name: "{title}.{thumbnail_ext}" thumbnail_name: "{title}.{thumbnail_ext}"
@ -83,7 +83,7 @@ a ``custom_file_name`` variable to use for the entry file and thumbnail fields:
.. code-block:: yaml .. code-block:: yaml
output_options: output_options:
output_directory: "{subscription_name}" output_directory: "/path/to/tv_shows/{subscription_name}"
file_name: "{custom_file_name}.{ext}" file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}" thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
@ -104,7 +104,7 @@ are safe by using:
.. code-block:: yaml .. code-block:: yaml
output_options: output_options:
output_directory: "{subscription_name_sanitized}" output_directory: "/path/to/tv_shows/{subscription_name_sanitized}"
file_name: "{custom_file_name}.{ext}" file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}" thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
@ -115,8 +115,9 @@ Simply add a ``_sanitized`` suffix to any variable name to make it sanitized.
.. note:: .. note::
Make sure you do not sanitize custom variables that intentionally create directories, otherwise Make sure you do not sanitize custom variables that intentionally create directories,
they will... be sanitized and not resolve to directories! (i.e. sanitizing ``/path/to/tv_shows/``) otherwise they will... be sanitized and not resolve to
directories!
Using Scripting Functions Using Scripting Functions
@ -130,7 +131,7 @@ Let's suppose you are an avid command-line user, and like all of your file names
.. code-block:: yaml .. code-block:: yaml
output_options: output_options:
output_directory: "{subscription_name_sanitized}" output_directory: "/path/to/tv_shows/{subscription_name_sanitized}"
file_name: "{custom_file_name}.{ext}" file_name: "{custom_file_name}.{ext}"
thumbnail_name: "{custom_file_name}.{thumbnail_ext}" thumbnail_name: "{custom_file_name}.{thumbnail_ext}"
@ -147,7 +148,7 @@ saying:
- Allow a string to be multi-lined, and do not include newlines before or after it. - Allow a string to be multi-lined, and do not include newlines before or after it.
See for yourself `here <https://yaml-online-parser.appspot.com/?yaml=output_options%3A%0A%20%20output_directory%3A%20%22%7Bsubscription_name_sanitized%7D%22%0A%20%20file_name%3A%20%22%7Bcustom_file_name%7D.%7Bext%7D%22%0A%20%20thumbnail_name%3A%20%22%7Bcustom_file_name%7D.%7Bthumbnail_ext%7D%22%0A%0Aoverrides%3A%0A%20%20snake_cased_title%3A%20%3E-%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%25replace%28%20title%2C%20%27%20%27%2C%20%27_%27%20%29%0A%20%20%20%20%7D%0A%20%20custom_file_name%3A%20%22%7Bupload_date_standardized%7D%20%7Bsnake_cased_title_sanitized%7D%22&type=canonical_yaml>`_. See for yourself `here <https://yaml-online-parser.appspot.com/?yaml=output_options%3A%0A%20%20output_directory%3A%20%22%7Bsubscription_name_sanitized%7D%22%0A%20%20file_name%3A%20%22%7Bcustom_file_name%7D.%7Bext%7D%22%0A%20%20thumbnail_name%3A%20%22%7Bcustom_file_name%7D.%7Bthumbnail_ext%7D%22%0A%0Aoverrides%3A%0A%20%20snake_cased_title%3A%20%3E-%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%25replace%28%20title%2C%20%27%20%27%2C%20%27_%27%20%29%0A%20%20%20%20%7D%0A%20%20custom_file_name%3A%20%22%7Bupload_date_standardized%7D%20%7Bsnake_cased_title_sanitized%7D%22&type=json>`_.
Any whitespace within curly-braces is okay since it will be parsed out. This is needed to make Any whitespace within curly-braces is okay since it will be parsed out. This is needed to make
scripting function usage readable. scripting function usage readable.
@ -161,12 +162,41 @@ Advanced Scripting
Accessing ``info.json`` Fields Accessing ``info.json`` Fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
WIP The entirety of an entry's ``info.json`` file resides in the
`Map <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_types.html#map>`_
variable
`entry_metadata <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/entry_variables.html#entry-metadata>`_.
Any field can be accessed by using the
`map_get <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#map-get>`_
function like so:
.. code-block:: yaml
:caption: Fetches the 'artist' value from the .info.json, returns null if it does not exist.
artist: >-
{ %map_get( entry_metadata, "artist", null ) }
Creating Custom Functions Creating Custom Functions
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
WIP Custom functions can be created in the overrides section using the following syntax:
Parsing Maps and Arrays .. code-block:: yaml
~~~~~~~~~~~~~~~~~~~~~~~
WIP overrides:
"%get_entry_metadata_field": >-
{ %map_get( entry_metadata, $0, null ) }
Custom function definitions must have ``%`` as a prefix to the function name, be surrounded by
quotes to make YAML parsing happy, and can support arguments using ``$0``, ``$1``, ... to indicate
their first argument, second argument, etc.
Using our new custom function, we can simply the ``artist`` variable definition above to:
.. code-block:: yaml
overrides:
"%get_entry_metadata_field": >-
{ %map_get( entry_metadata, $0, null ) }
artist: >-
{ get_entry_metadata_field("artist") }

View file

@ -8,23 +8,22 @@ Types
String String
~~~~~~ ~~~~~~
Strings are a series of characters surrounded by quotes and can be defined in a few ways, including: Strings are a series of characters surrounded by quotes.
.. tab-set:: .. code-block:: yaml
.. tab-item:: Literal
.. code-block:: yaml
string_variable: "This is a String variable" string_variable: "This is a String variable"
.. tab-item:: In-Line .. note::
.. code-block:: yaml For non-String types, they must be defined as parameters to scripting functions. This is because
anything in a variable definition that is not within curly-braces gets evaluated as a String.
string_variable: "{ %string('This is a String variable') }" We can define Strings within curly-braces by setting them as parameters to a function:
.. tab-item:: Single Quote .. tab-set::
.. tab-item:: Multi-Line Single Quote
.. code-block:: yaml .. code-block:: yaml
@ -33,7 +32,7 @@ Strings are a series of characters surrounded by quotes and can be defined in a
%string('This is a String variable') %string('This is a String variable')
} }
.. tab-item:: Double Quote .. tab-item:: Multi-Line Double Quote
.. code-block:: yaml .. code-block:: yaml
@ -42,13 +41,42 @@ Strings are a series of characters surrounded by quotes and can be defined in a
%string("This is a String variable") %string("This is a String variable")
} }
.. tab-item:: Triple Quote There are a few ways to make variables that use curly braces more compact, including:
.. tab-set::
.. tab-item:: New-Line Single Quote
.. code-block:: yaml
string_variable: >-
{ %string('This is a String variable') }
.. tab-item:: New-Line Double Quote
.. code-block:: yaml
string_variable: >-
{ %string("This is a String variable") }
.. tab-item:: Same-Line
.. code-block:: yaml
string_variable: "{ %string('This is a String variable') }"
In the case that you want to define a string variable that contains both single and double quotes,
triple-quotes can be used to avoid *closing* the String.
.. tab-set::
.. tab-item:: Triple-Single Quote
.. code-block:: yaml .. code-block:: yaml
string_variable: >- string_variable: >-
{ {
%string('''This is a String variable''') %string('''This has both " and ' in it.''')
} }
.. tab-item:: Triple-Double Quote .. tab-item:: Triple-Double Quote
@ -57,14 +85,9 @@ Strings are a series of characters surrounded by quotes and can be defined in a
string_variable: >- string_variable: >-
{ {
%string("""This is a String variable""") %string("""This has both " and ' in it.""")
} }
.. note::
For non-String types, they must be defined as parameters to scripting functions. This is because
anything in a variable definition that is not within curly-braces gets evaluated as a String.
Integer Integer
~~~~~~~ ~~~~~~~
@ -72,7 +95,7 @@ Integers are whole numbers with no decimal.
.. tab-set:: .. tab-set::
.. tab-item:: Literal .. tab-item:: Multi-Line
.. code-block:: yaml .. code-block:: yaml
@ -81,7 +104,15 @@ Integers are whole numbers with no decimal.
%int(2022) %int(2022)
} }
.. tab-item:: In-Line .. tab-item:: New-Line
.. code-block:: yaml
int_variable: >-
{ %int(2022) }
.. tab-item:: Same-Line
.. code-block:: yaml .. code-block:: yaml
@ -94,7 +125,7 @@ Floats are floating-point decimals numbers.
.. tab-set:: .. tab-set::
.. tab-item:: Literal .. tab-item:: Multi-Line
.. code-block:: yaml .. code-block:: yaml
@ -103,7 +134,14 @@ Floats are floating-point decimals numbers.
%float(3.14) %float(3.14)
} }
.. tab-item:: In-Line .. tab-item:: New-Line
.. code-block:: yaml
float_variable: >-
{ %float(3.14) }
.. tab-item:: Same-Line
.. code-block:: yaml .. code-block:: yaml
@ -116,7 +154,7 @@ A type is considered boolean if it spells out ``True`` or ``False``, case-insens
.. tab-set:: .. tab-set::
.. tab-item:: Literal .. tab-item:: Multi-Line
.. code-block:: yaml .. code-block:: yaml
@ -125,7 +163,14 @@ A type is considered boolean if it spells out ``True`` or ``False``, case-insens
%bool(True) %bool(True)
} }
.. tab-item:: In-Line .. tab-item:: New-Line
.. code-block:: yaml
bool_variable: >-
{ %bool(True) }
.. tab-item:: Same-Line
.. code-block:: yaml .. code-block:: yaml
@ -139,7 +184,7 @@ Arrays are defined using brackets (``[ ]``), and are accessed using zero-based i
.. tab-set:: .. tab-set::
.. tab-item:: Literal .. tab-item:: Multi-Line
.. code-block:: yaml .. code-block:: yaml
@ -157,7 +202,16 @@ Arrays are defined using brackets (``[ ]``), and are accessed using zero-based i
%array_at(array_variable, 0) %array_at(array_variable, 0)
} }
.. tab-item:: In-Line .. tab-item:: New-Line
.. code-block:: yaml
array_variable: >-
{ ["element with index 0", 1, 2.0, ["Nested Array 3"]] }
element_0: >-
{ %array_at(array_variable, 0) }
.. tab-item:: Same-Line
.. code-block:: yaml .. code-block:: yaml
@ -168,11 +222,11 @@ Map
~~~ ~~~
A Map is a key-value store, containing mappings between keys and values. A Map is a key-value store, containing mappings between keys and values.
Maps are defined using curley-braces (``{ }``), and are accessed using their keys. Maps are defined using curly-braces (``{ }``), and are accessed using their keys.
.. tab-set:: .. tab-set::
.. tab-item:: Literal .. tab-item:: Multi-Line
.. code-block:: yaml .. code-block:: yaml
@ -189,7 +243,16 @@ Maps are defined using curley-braces (``{ }``), and are accessed using their key
%map_get(map_variable, "string_key") %map_get(map_variable, "string_key")
} }
.. tab-item:: In-Line .. tab-item:: New-Line
.. code-block:: yaml
map_variable: >-
{ {"string_key": "string_value", 1: "int_key", "list_value": ["elem0", 1, 2.0]} }
string_value: >-
{ %map_get(map_variable, "string_key") }
.. tab-item:: Same-Line
.. code-block:: yaml .. code-block:: yaml
@ -209,7 +272,14 @@ case-insensitive.
null_variable: "" null_variable: ""
.. tab-item:: In-Line .. tab-item:: New-Line
.. code-block:: yaml
null_variable: >-
{ %string(null) }
.. tab-item:: Same-Line
.. code-block:: yaml .. code-block:: yaml
@ -276,8 +346,9 @@ it expects the lambda function to have two input arguments. These are denoted us
LambdaReduce LambdaReduce
~~~~~~~~~~~~ ~~~~~~~~~~~~
LambdaReduce is special type of lambda that reduces an Array to a single value by calling the LambdaReduce parameters are a reference to a function that will perform a *reduce* - an operation
LabmdaReduce function repeatedly on two elements in the Array until it is reduced to a single value. that reduces an Array to a single value by calling the LambdaReduce function repeatedly on two
elements in the Array until it is reduced to a single value.
In this example, In this example,
@ -294,11 +365,9 @@ on the input array, using
`add <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#add>`_ `add <https://ytdl-sub.readthedocs.io/en/latest/config_reference/scripting/scripting_functions.html#add>`_
as the LambdaReduce function. This will reduce the Array to a single value by internally calling as the LambdaReduce function. This will reduce the Array to a single value by internally calling
.. code-block:: - *reduce-call 1*: ``%add(1, 2) = 3`` (first two elements)
- *reduce-call 2*: ``%add(3, 3) = 6`` (output from first two and third element)
- %add(1, 2) = 3 - *reduce-call 3*: ``%add(6, 4) = 10`` (output from first three elements and fourth element)
- %add(3, 3) = 6
- %add(6, 4) = 10
And evaluate to ``10``. And evaluate to ``10``.

View file

@ -1,9 +1,12 @@
Override Variables Static Variables
================== ================
Subscription Variables
----------------------
subscription_indent_i subscription_indent_i
--------------------- ~~~~~~~~~~~~~~~~~~~~~
For subscriptions in the form of For subscriptions in the form of
.. code-block:: yaml .. code-block:: yaml
@ -16,7 +19,7 @@ For subscriptions in the form of
``Indent Value 1`` and ``Indent Value 2``. ``Indent Value 1`` and ``Indent Value 2``.
subscription_map subscription_map
---------------- ~~~~~~~~~~~~~~~~
For subscriptions in the form of For subscriptions in the form of
.. code-block:: yaml .. code-block:: yaml
@ -42,11 +45,12 @@ Stores all the contents under the subscription name into the override variable
} }
subscription_name subscription_name
----------------- ~~~~~~~~~~~~~~~~~
Name of the subscription Name of the subscription. For subscriptions types that use a prefix (``~``, ``+``),
the prefix and all whitespace afterwards is stripped from the subscription name.
subscription_value subscription_value
------------------ ~~~~~~~~~~~~~~~~~~
For subscriptions in the form of For subscriptions in the form of
.. code-block:: yaml .. code-block:: yaml
@ -56,7 +60,7 @@ For subscriptions in the form of
``subscription_value`` gets set to ``https://...``. ``subscription_value`` gets set to ``https://...``.
subscription_value_i subscription_value_i
-------------------- ~~~~~~~~~~~~~~~~~~~~
For subscriptions in the form of For subscriptions in the form of
.. code-block:: yaml .. code-block:: yaml

View file

@ -1,8 +1,8 @@
===
FAQ FAQ
=== ===
Since ytdl-sub is relatively new to the public, there has not been many question asked yet. We will update this as Since ytdl-sub is relatively new to the public, there has not been many question asked yet. We will update this as more questions get asked.
more questions get asked.
.. contents:: Frequently Asked Questions .. contents:: Frequently Asked Questions
:depth: 3 :depth: 3
@ -10,12 +10,25 @@ more questions get asked.
How do I... How do I...
----------- -----------
...get support or reach out to contribute?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need support, you can:
* :ytdl-sub-gh:`Open an issue on GitHub <issues/new>`
* `Join our Discord <https://discord.gg/v8j9RAHb4k>`_
If you would like to contribute, we're happy to accept any help, even non-coders! To find out how you can help this project, you can:
* `Join our Discord <https://discord.gg/v8j9RAHb4k>`_ and leave a comment in #development with where you think you can assist or what skills you would like to contribute.
* If you just want to fix one thing, you're welcome to :ytdl-sub-gh:`submit a pull request <compare>` with information on what issue you're resolving and it will be reviewed as soon as possible.
...download age-restricted YouTube videos? ...download age-restricted YouTube videos?
'''''''''''''''''''''''''''''''''''''''''' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
See
`ytdls recommended way <https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl>`_ See `yt-dl's recommended way <https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl>`_ to download your YouTube cookie, then add it to your :ref:`ytdl options <config_reference/plugins:ytdl_options>` section of your config:
to download your YouTube cookie, then add it to your
`ytdl options <https://ytdl-sub.readthedocs.io/en/latest/config.html#ytdl-options>`_ section of your config:
.. code-block:: yaml .. code-block:: yaml
@ -23,14 +36,16 @@ to download your YouTube cookie, then add it to your
cookiefile: "/path/to/cookies/file.txt" cookiefile: "/path/to/cookies/file.txt"
...automate my downloads? ...automate my downloads?
''''''''''''''''''''''''' ~~~~~~~~~~~~~~~~~~~~~~~~~
`This part of the wiki <https://github.com/jmbannon/ytdl-sub/wiki/7.-Automate-Downloading-New-Content-Using-Your-Configs>`_ shows how to set up ``ytdl-sub`` to run in a cron job within Docker.
:doc:`This page </guides/getting_started/automating_downloads>` shows how to set up ``ytdl-sub`` to run automatically on various platforms.
There is a bug where... There is a bug where...
----------------------- -----------------------
...date_range is not downloading older videos after I changed the range ...date_range is not downloading older videos after I changed the range
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Your preset most likely has ``break_on_existing`` set to True, which will stop downloading additional metadata/videos if the video exists in your download archive. Set the following in your config to skip downloading videos that exist instead of stopping altogether. Your preset most likely has ``break_on_existing`` set to True, which will stop downloading additional metadata/videos if the video exists in your download archive. Set the following in your config to skip downloading videos that exist instead of stopping altogether.
.. code-block:: yaml .. code-block:: yaml
@ -38,11 +53,12 @@ Your preset most likely has ``break_on_existing`` set to True, which will stop d
ytdl_options: ytdl_options:
break_on_existing: False break_on_existing: False
After your download your new date_range duration, re-enable ``break_on_existing`` to speed up successive downloads. After you download your new date_range duration, re-enable ``break_on_existing`` to speed up successive downloads.
...it is downloading non-English title and description metadata ...it is downloading non-English title and description metadata
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most likely the video has a non-English language set to its 'native' language. You can tell yt-dlp to explicitly download English metadata using
Most likely the video has a non-English language set to its 'native' language. You can tell yt-dlp to explicitly download English metadata using.
.. code-block:: yaml .. code-block:: yaml
@ -53,7 +69,19 @@ Most likely the video has a non-English language set to its 'native' language. Y
- "en" - "en"
...Plex is not showing my TV shows correctly ...Plex is not showing my TV shows correctly
'''''''''''''''''''''''''''''''''''''''''''' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Set the following
`Scanner and Agent <https://i.imgur.com/zdZhCLZ.png>`_ Set the following for your ytdl-sub library that has been added to Plex.
for your library.
.. figure:: ../../images/plex_scanner_agent.png
:alt: The Plex library editor, under the advanced settings, showing the required options for Plex to show the TV shows correctly.
**Scanner:** Plex Series Scanner
**Agent:** Personal Media shows
**Visibility:** Exclude from home screen and global search
**Episode sorting:** Library default
**YES** Enable video preview thumbnails

View file

@ -17,14 +17,14 @@ The ``ytdl-sub`` Docker images use :lsio:`LSIO-based images <\ >` and install yt
GUI Image GUI Image
--------- ---------
The GUI image uses LSIO's :lsio-gh:`docker-code-server image` for its base image. More info on other code-server environment variables can be found within its documentation. The GUI image uses LSIO's :lsio-gh:`docker-code-server image <\ >` for its base image. More info on other code-server environment variables can be found within its documentation.
After starting, code-server will be running at http://localhost:8443. Open this page in a browser to access and interact with ``ytdl-sub``. After starting, the code-server will be running at http://localhost:8443. Open this page in a browser to access and interact with ``ytdl-sub``.
Headless Image Headless Image
-------------- --------------
The headless image uses LSIO's :lsio-gh:`docker-baseimage-alpine image` for its base image. Execute the following command to access and interact with ``ytdl-sub``: The headless image uses LSIO's :lsio-gh:`docker-baseimage-alpine image <\ >` for its base image. Execute the following command to access and interact with ``ytdl-sub``:
.. code-block:: bash .. code-block:: bash

View file

@ -1,3 +1,16 @@
Unraid Unraid
-------------- --------------
You can install our :unraid:`unraid community apps <community/apps?q=ytdl-sub#r>` through the `Unraid Community Apps plugin <https://unraid.net/community/apps>`_. Uses Docker under the hood. You can install our :unraid:`unraid community apps <community/apps?q=ytdl-sub#r>` through the `Unraid Community Apps plugin <https://unraid.net/community/apps>`_.
If you installed the ``ytdl-sub-gui`` app, the code-server will be running at http://localhost:8443 (replace ``localhost`` with the IP of the computer running Unraid if you aren't trying to access ``ytdl-sub`` on that computer). Open this page in a browser to access and interact with ``ytdl-sub``.
If you installed the ``ytdl-sub`` app (headless), open the normal app-specific console to access and interact with ``ytdl-sub``. Once open, you must first run ``su abc -s /bin/bash`` to change to the non-root user. You can confirm that this command worked by running ``whoami`` and verifying that the result is ``abc``.
.. warning::
If you use the below option to access the ``ytdl-sub`` console, be sure to run ``su abc -s /bin/bash`` first thing. You can confirm that this command worked by running ``whoami`` and verifying that the result is ``abc``. Do **NOT** run ``ytdl-sub`` as the root user! Running as root will set the owner of all modified files to root, which prevents most media managers and players from accessing the files.
.. figure:: ../../../images/unraid_badconsole.png
:alt: The Unraid community app plugin GUI, with an arrow pointing at the "Console" option in the dropdown after selecting ytdl-sub-gui

View file

@ -22,24 +22,6 @@ disable = [
load-plugins = "pylint.extensions.docparams" load-plugins = "pylint.extensions.docparams"
[tool.pydocstyle]
inherit = false
match = "[^test_].*\\.py"
ignore = [
"D100", # docstring in public module
"D101", # Missing docstring in public class (covered by pylint)
"D104", # docstring in public package
"D107", # docstring in init
"D200", # One-line should fit on one line
"D203", # 1 blank line before class docstring
"D205", # 1 blank line between summary and description
"D212", # Multi-line should start at first line
"D400", # Should end with a period
"D401", # Return vs Returns
"D413", # Missing blank line after last section
"D415", # Should end with a period
]
[tool.coverage.run] [tool.coverage.run]
include = [ include = [
"src/*" "src/*"

View file

@ -27,7 +27,7 @@ package_dir =
packages=find: packages=find:
install_requires = install_requires =
yt-dlp==2023.11.16 yt-dlp==2023.12.30
argparse==1.4.0 argparse==1.4.0
colorama==0.4.6 colorama==0.4.6
mergedeep==1.3.4 mergedeep==1.3.4

View file

@ -8,7 +8,7 @@ import mergedeep
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.variables.override_variables import OverrideHelpers from ytdl_sub.entries.variables.override_variables import OverrideHelpers
from ytdl_sub.entries.variables.override_variables import OverrideVariables from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.script.parser import parse from ytdl_sub.script.parser import parse
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
@ -56,7 +56,7 @@ class Overrides(DictFormatterValidator, Scriptable):
def __init__(self, name, value): def __init__(self, name, value):
DictFormatterValidator.__init__(self, name, value) DictFormatterValidator.__init__(self, name, value)
Scriptable.__init__(self) Scriptable.__init__(self, initialize_base_script=True)
for key in self._keys: for key in self._keys:
self.ensure_variable_name_valid(key) self.ensure_variable_name_valid(key)
@ -135,7 +135,7 @@ class Overrides(DictFormatterValidator, Scriptable):
""" """
self.script.add( self.script.add(
ScriptUtils.add_sanitized_variables( ScriptUtils.add_sanitized_variables(
{OverrideVariables.subscription_name(): subscription_name} {SubscriptionVariables.subscription_name(): subscription_name}
) )
) )
self.script.add( self.script.add(

View file

@ -14,7 +14,7 @@ from ytdl_sub.config.preset_options import OutputOptions
from ytdl_sub.config.validators.options import OptionsValidator from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.downloaders.url.validators import MultiUrlValidator from ytdl_sub.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.variables.override_variables import OverrideVariables from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.script.script import Script from ytdl_sub.script.script import Script
from ytdl_sub.validators.string_formatter_validators import validate_formatters from ytdl_sub.validators.string_formatter_validators import validate_formatters
@ -67,7 +67,9 @@ def _get_added_and_modified_variables(
def _override_variables(overrides: Overrides) -> Set[str]: def _override_variables(overrides: Overrides) -> Set[str]:
return set(list(overrides.initial_variables().keys())) | {OverrideVariables.subscription_name()} return set(list(overrides.initial_variables().keys())) | {
SubscriptionVariables.subscription_name()
}
def _entry_variables() -> Set[str]: def _entry_variables() -> Set[str]:

View file

@ -152,7 +152,9 @@ class InfoJsonDownloader(SourcePlugin[InfoJsonDownloaderOptions]):
for file_name in entry_file_names: for file_name in entry_file_names:
ext = get_file_extension(file_name) ext = get_file_extension(file_name)
file_path = Path(self.output_directory) / file_name file_path = Path(self.output_directory) / file_name
working_directory_file_path = Path(self.working_directory) / f"{entry.uid}.{ext}" working_directory_file_path = Path(self.working_directory) / entry.base_filename(
ext=ext
)
# NFO files will always get rewritten, so ignore # NFO files will always get rewritten, so ignore
if ext == "nfo": if ext == "nfo":

View file

@ -349,39 +349,43 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
) )
def _iterate_child_entries( def _iterate_child_entries(
self, url_validator: UrlValidator, entries: List[Entry] self, entries: List[Entry], download_reversed: bool
) -> Iterator[Entry]: ) -> Iterator[Entry]:
entries_to_iterate = entries # Iterate a list of entries, and delete the entries after yielding
if url_validator.download_reverse: indices = list(range(len(entries)))
entries_to_iterate = reversed(entries) if download_reversed:
indices = reversed(indices)
for entry in entries_to_iterate: for idx in indices:
self._url_state.entries_downloaded += 1 self._url_state.entries_downloaded += 1
if self._is_downloaded(entry): if self._is_downloaded(entries[idx]):
download_logger.info( download_logger.info(
"Already downloaded entry %d/%d: %s", "Already downloaded entry %d/%d: %s",
self._url_state.entries_downloaded, self._url_state.entries_downloaded,
self._url_state.entries_total, self._url_state.entries_total,
entry.title, entries[idx].title,
) )
del entries[idx]
continue continue
yield entry yield entries[idx]
self._mark_downloaded(entry) self._mark_downloaded(entries[idx])
del entries[idx]
def _iterate_parent_entry( def _iterate_parent_entry(
self, url_validator: UrlValidator, parent: EntryParent self, parent: EntryParent, download_reversed: bool
) -> Iterator[Entry]: ) -> Iterator[Entry]:
for entry_child in self._iterate_child_entries( for entry_child in self._iterate_child_entries(
url_validator=url_validator, entries=parent.entry_children() entries=parent.entry_children(), download_reversed=download_reversed
): ):
yield entry_child yield entry_child
# Recursion the parent's parent entries # Recursion the parent's parent entries
for parent_child in reversed(parent.parent_children()): for parent_child in reversed(parent.parent_children()):
for entry_child in self._iterate_parent_entry( for entry_child in self._iterate_parent_entry(
url_validator=url_validator, parent=parent_child parent=parent_child, download_reversed=download_reversed
): ):
yield entry_child yield entry_child
@ -415,9 +419,9 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
def _iterate_entries( def _iterate_entries(
self, self,
url_validator: UrlValidator,
parents: List[EntryParent], parents: List[EntryParent],
orphans: List[Entry], orphans: List[Entry],
download_reversed: bool,
) -> Iterator[Entry]: ) -> Iterator[Entry]:
""" """
Downloads the leaf entries from EntryParent trees Downloads the leaf entries from EntryParent trees
@ -426,11 +430,13 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
with self._separate_download_archives(clear_info_json_files=True): with self._separate_download_archives(clear_info_json_files=True):
for parent in parents: for parent in parents:
for entry_child in self._iterate_parent_entry( for entry_child in self._iterate_parent_entry(
url_validator=url_validator, parent=parent parent=parent, download_reversed=download_reversed
): ):
yield entry_child yield entry_child
for orphan in self._iterate_child_entries(url_validator=url_validator, entries=orphans): for orphan in self._iterate_child_entries(
entries=orphans, download_reversed=download_reversed
):
yield orphan yield orphan
def download_metadata(self) -> Iterable[Entry]: def download_metadata(self) -> Iterable[Entry]:
@ -454,7 +460,9 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
"Beginning downloads for %s", self.overrides.apply_formatter(collection_url.url) "Beginning downloads for %s", self.overrides.apply_formatter(collection_url.url)
) )
for entry in self._iterate_entries( for entry in self._iterate_entries(
url_validator=collection_url, parents=parents, orphans=orphan_entries parents=parents,
orphans=orphan_entries,
download_reversed=collection_url.download_reverse,
): ):
entry.initialize_script(self.overrides).add( entry.initialize_script(self.overrides).add(
{v.ytdl_sub_input_url: self.overrides.apply_formatter(collection_url.url)} {v.ytdl_sub_input_url: self.overrides.apply_formatter(collection_url.url)}

View file

@ -8,6 +8,8 @@ from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import final from typing import final
from yt_dlp.utils import sanitize_filename
from ytdl_sub.entries.script.variable_definitions import VARIABLES 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 VariableDefinitions
@ -45,6 +47,19 @@ class BaseEntry(ABC):
""" """
return str(self._kwargs[v.uid.metadata_key]) return str(self._kwargs[v.uid.metadata_key])
@property
def uid_sanitized(self) -> str:
"""
Sanitized version, used in filenames
"""
return sanitize_filename(self.uid)
def base_filename(self, ext: str):
"""
The base filename of all yt-dlp downloaded entry files
"""
return f"{self.uid_sanitized}.{ext}"
@property @property
def download_archive_extractor(self) -> str: def download_archive_extractor(self) -> str:
""" """
@ -101,30 +116,13 @@ class BaseEntry(ABC):
""" """
return self._working_directory return self._working_directory
def add_kwargs(self, variables_to_add: Dict[str, Any]) -> "BaseEntry":
"""
Adds variables to kwargs. Use with caution since yt-dlp data can be overwritten.
Plugins should use ``add_variables``.
Parameters
----------
variables_to_add
Variables to add to kwargs
Returns
-------
self
"""
self._kwargs = dict(self._kwargs, **variables_to_add)
return self
def get_download_info_json_name(self) -> str: def get_download_info_json_name(self) -> str:
""" """
Returns Returns
------- -------
The download info json's file name The download info json's file name
""" """
return f"{self.uid}.{self.info_json_ext}" return self.base_filename(ext=self.info_json_ext)
def get_download_info_json_path(self) -> str: def get_download_info_json_path(self) -> str:
""" """

View file

@ -44,12 +44,6 @@ class Entry(BaseEntry, Scriptable):
BaseEntry.__init__(self, entry_dict=entry_dict, working_directory=working_directory) BaseEntry.__init__(self, entry_dict=entry_dict, working_directory=working_directory)
Scriptable.__init__(self) Scriptable.__init__(self)
def _add_entry_kwargs_to_script(self) -> None:
# Add entry metadata, but avoid the `.add()` helper since it also adds sanitized
self.unresolvable.remove(v.entry_metadata.variable_name)
self.script.add({v.entry_metadata.variable_name: ScriptUtils.to_script(self._kwargs)})
self.update_script()
def initialize_script(self, other: Optional[Scriptable] = None) -> "Entry": def initialize_script(self, other: Optional[Scriptable] = None) -> "Entry":
""" """
Initializes the entry script using the Overrides script, then adding Initializes the entry script using the Overrides script, then adding
@ -57,12 +51,20 @@ class Entry(BaseEntry, Scriptable):
""" """
# Overrides contains added variables that are unresolvable, add them here # Overrides contains added variables that are unresolvable, add them here
if other: if other:
self.script = copy.deepcopy(other.script) self._script = copy.deepcopy(other.script)
self.unresolvable = copy.deepcopy(other.unresolvable) self._unresolvable = copy.deepcopy(other.unresolvable)
else:
self.initialize_base_script()
self._add_entry_kwargs_to_script() self._add_entry_kwargs_to_script()
return self return self
def _add_entry_kwargs_to_script(self) -> None:
# Add entry metadata, but avoid the `.add()` helper since it also adds sanitized
self.unresolvable.remove(v.entry_metadata.variable_name)
self.script.add({v.entry_metadata.variable_name: ScriptUtils.to_script(self._kwargs)})
self.update_script()
def get(self, variable: Variable, expected_type: Type[TypeT]) -> TypeT: def get(self, variable: Variable, expected_type: Type[TypeT]) -> TypeT:
""" """
Gets a variable of an expected type. Will error if it does not exist or is not resolved. Gets a variable of an expected type. Will error if it does not exist or is not resolved.
@ -113,7 +115,8 @@ class Entry(BaseEntry, Scriptable):
""" """
ext = self.try_get(v.ext, str) or self._kwargs[v.ext.metadata_key] ext = self.try_get(v.ext, str) or self._kwargs[v.ext.metadata_key]
for possible_ext in [ext, "mkv"]: for possible_ext in [ext, "mkv"]:
file_path = str(Path(self.working_directory()) / f"{self.uid}.{possible_ext}") file_name = self.base_filename(ext=possible_ext)
file_path = str(Path(self.working_directory()) / file_name)
if os.path.isfile(file_path): if os.path.isfile(file_path):
return possible_ext return possible_ext
@ -125,7 +128,7 @@ class Entry(BaseEntry, Scriptable):
------- -------
The entry's file name The entry's file name
""" """
return f"{self.uid}.{self.ext}" return self.base_filename(ext=self.ext)
def get_download_file_path(self) -> str: def get_download_file_path(self) -> str:
"""Returns the entry's file path to where it was downloaded""" """Returns the entry's file path to where it was downloaded"""
@ -137,7 +140,7 @@ class Entry(BaseEntry, Scriptable):
------- -------
The download thumbnail's file name The download thumbnail's file name
""" """
return f"{self.uid}.{self.get(v.thumbnail_ext, str)}" return self.base_filename(ext=self.get(v.thumbnail_ext, str))
def get_download_thumbnail_path(self) -> str: def get_download_thumbnail_path(self) -> str:
"""Returns the entry's thumbnail's file path to where it was downloaded""" """Returns the entry's thumbnail's file path to where it was downloaded"""
@ -155,7 +158,10 @@ class Entry(BaseEntry, Scriptable):
possible_thumbnail_exts.add(thumbnail["url"].split(".")[-1]) possible_thumbnail_exts.add(thumbnail["url"].split(".")[-1])
for ext in possible_thumbnail_exts: for ext in possible_thumbnail_exts:
possible_thumbnail_path = str(Path(self.working_directory()) / f"{self.uid}.{ext}") possible_thumbnail_filename = self.base_filename(ext=ext)
possible_thumbnail_path = str(
Path(self.working_directory()) / possible_thumbnail_filename
)
if os.path.isfile(possible_thumbnail_path): if os.path.isfile(possible_thumbnail_path):
return possible_thumbnail_path return possible_thumbnail_path
@ -202,7 +208,7 @@ class Entry(BaseEntry, Scriptable):
# HACK: yt-dlp does not record extracted/converted extensions anywhere. If the file is not # HACK: yt-dlp does not record extracted/converted extensions anywhere. If the file is not
# found, try it using all possible extensions # found, try it using all possible extensions
if not file_exists: if not file_exists:
for ext in AUDIO_CODEC_EXTS.union(VIDEO_CODEC_EXTS): for ext in AUDIO_CODEC_EXTS | VIDEO_CODEC_EXTS:
if os.path.isfile(self.get_download_file_path().removesuffix(self.ext) + ext): if os.path.isfile(self.get_download_file_path().removesuffix(self.ext) + ext):
file_exists = True file_exists = True
break break

View file

@ -7,11 +7,12 @@ from ytdl_sub.script.utils.name_validation import is_valid_name
SUBSCRIPTION_ARRAY = "subscription_array" SUBSCRIPTION_ARRAY = "subscription_array"
class OverrideVariables: class SubscriptionVariables:
@staticmethod @staticmethod
def subscription_name() -> str: def subscription_name() -> str:
""" """
Name of the subscription Name of the subscription. For subscriptions types that use a prefix (``~``, ``+``),
the prefix and all whitespace afterwards is stripped from the subscription name.
""" """
return "subscription_name" return "subscription_name"

View file

@ -13,7 +13,7 @@ from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
from ytdl_sub.validators.validators import BoolValidator from ytdl_sub.validators.validators import BoolValidator
logger = Logger.get("embed_thumbnail") logger = Logger.get("embed-thumbnail")
class EmbedThumbnailOptions(BoolValidator, OptionsValidator): class EmbedThumbnailOptions(BoolValidator, OptionsValidator):

View file

@ -11,7 +11,7 @@ from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
logger = Logger.get("conditional") logger = Logger.get("filter-exclude")
class FilterExcludeOptions(ListFormatterValidator, OptionsValidator): class FilterExcludeOptions(ListFormatterValidator, OptionsValidator):
@ -55,6 +55,11 @@ class FilterExcludePlugin(Plugin[FilterExcludeOptions]):
for formatter in self.plugin_options.list: for formatter in self.plugin_options.list:
out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry)) out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry))
if bool(out): if bool(out):
logger.info(
"Filtering '%s' from the filter %s evaluating to True",
entry.title,
formatter.format_string,
)
return None return None
return entry return entry

View file

@ -11,7 +11,7 @@ from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
logger = Logger.get("conditional") logger = Logger.get("filter-include")
class FilterIncludeOptions(ListFormatterValidator, OptionsValidator): class FilterIncludeOptions(ListFormatterValidator, OptionsValidator):
@ -63,6 +63,11 @@ class FilterIncludePlugin(Plugin[FilterIncludeOptions]):
for formatter in self.plugin_options.list: for formatter in self.plugin_options.list:
out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry)) out = json.loads(self.overrides.apply_formatter(formatter=formatter, entry=entry))
if not bool(out): if not bool(out):
logger.info(
"Filtering '%s' from the filter %s evaluating to False",
entry.title,
formatter.format_string,
)
return None return None
return entry return entry

View file

@ -8,7 +8,7 @@ from ytdl_sub.config.validators.options import OptionsDictValidator
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.validators import StringListValidator from ytdl_sub.validators.validators import StringListValidator
logger = Logger.get("match_filters") logger = Logger.get("match-filters")
def default_filters() -> Tuple[List[str], List[str]]: def default_filters() -> Tuple[List[str], List[str]]:

View file

@ -21,7 +21,7 @@ from ytdl_sub.validators.validators import BoolValidator
v: VariableDefinitions = VARIABLES v: VariableDefinitions = VARIABLES
logger = Logger.get("music_tags") logger = Logger.get("music-tags")
def _is_multi_field(tag_name: str) -> bool: def _is_multi_field(tag_name: str) -> bool:

View file

@ -208,7 +208,6 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
file_metadata = FileMetadata(f"Embedded subtitles with lang(s) {', '.join(langs)}") file_metadata = FileMetadata(f"Embedded subtitles with lang(s) {', '.join(langs)}")
if self.plugin_options.subtitles_name: if self.plugin_options.subtitles_name:
for lang in langs: for lang in langs:
subtitle_file_name = f"{entry.uid}.{lang}.{self.plugin_options.subtitles_type}"
output_subtitle_file_name = self.overrides.apply_formatter( output_subtitle_file_name = self.overrides.apply_formatter(
formatter=self.plugin_options.subtitles_name, formatter=self.plugin_options.subtitles_name,
entry=entry, entry=entry,
@ -216,7 +215,9 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
) )
self.save_file( self.save_file(
file_name=subtitle_file_name, file_name=entry.base_filename(
ext=f"{lang}.{self.plugin_options.subtitles_type}"
),
output_file_name=output_subtitle_file_name, output_file_name=output_subtitle_file_name,
entry=entry, entry=entry,
) )
@ -225,9 +226,8 @@ class SubtitlesPlugin(Plugin[SubtitleOptions]):
# Can happen for both file and embedded subs # Can happen for both file and embedded subs
for lang in langs: for lang in langs:
for possible_ext in SUBTITLE_EXTENSIONS: for possible_ext in SUBTITLE_EXTENSIONS:
possible_subs_file = ( possible_subs_filename = entry.base_filename(ext=f"{lang}.{possible_ext}")
Path(self.working_directory) / f"{entry.uid}.{lang}.{possible_ext}" possible_subs_file = Path(self.working_directory) / possible_subs_filename
)
FileHandler.delete(possible_subs_file) FileHandler.delete(possible_subs_file)
return file_metadata return file_metadata

View file

@ -10,7 +10,7 @@ from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
logger = Logger.get("video_tags") logger = Logger.get("video-tags")
class VideoTagsOptions(OptionsDictValidator): class VideoTagsOptions(OptionsDictValidator):

View file

@ -1,5 +1,3 @@
presets: presets:
################################################################################## ##################################################################################
@ -12,7 +10,7 @@ presets:
# #
# "Subscription Name": "url" # "Subscription Name": "url"
# #
# where url tries to grab channel avatar + banner # where the first url tries to grab channel avatar + banner
# #
_multi_url: _multi_url:
download: download:
@ -41,9 +39,91 @@ presets:
- url: "{url18}" - url: "{url18}"
- url: "{url19}" - url: "{url19}"
- url: "{url20}" - url: "{url20}"
- url: "{url21}"
- url: "{url22}"
- url: "{url23}"
- url: "{url24}"
- url: "{url25}"
- url: "{url26}"
- url: "{url27}"
- url: "{url28}"
- url: "{url29}"
- url: "{url30}"
- url: "{url31}"
- url: "{url32}"
- url: "{url33}"
- url: "{url34}"
- url: "{url35}"
- url: "{url36}"
- url: "{url37}"
- url: "{url38}"
- url: "{url39}"
- url: "{url40}"
- url: "{url41}"
- url: "{url42}"
- url: "{url43}"
- url: "{url44}"
- url: "{url45}"
- url: "{url46}"
- url: "{url47}"
- url: "{url48}"
- url: "{url49}"
- url: "{url50}"
- url: "{url51}"
- url: "{url52}"
- url: "{url53}"
- url: "{url54}"
- url: "{url55}"
- url: "{url56}"
- url: "{url57}"
- url: "{url58}"
- url: "{url59}"
- url: "{url60}"
- url: "{url61}"
- url: "{url62}"
- url: "{url63}"
- url: "{url64}"
- url: "{url65}"
- url: "{url66}"
- url: "{url67}"
- url: "{url68}"
- url: "{url69}"
- url: "{url70}"
- url: "{url71}"
- url: "{url72}"
- url: "{url73}"
- url: "{url74}"
- url: "{url75}"
- url: "{url76}"
- url: "{url77}"
- url: "{url78}"
- url: "{url79}"
- url: "{url80}"
- url: "{url81}"
- url: "{url82}"
- url: "{url83}"
- url: "{url84}"
- url: "{url85}"
- url: "{url86}"
- url: "{url87}"
- url: "{url88}"
- url: "{url89}"
- url: "{url90}"
- url: "{url91}"
- url: "{url92}"
- url: "{url93}"
- url: "{url94}"
- url: "{url95}"
- url: "{url96}"
- url: "{url97}"
- url: "{url98}"
- url: "{url99}"
- url: "{url100}"
overrides: overrides:
avatar_uncropped_thumbnail_file_name: "" avatar_uncropped_thumbnail_file_name: ""
banner_uncropped_thumbnail_file_name: "" banner_uncropped_thumbnail_file_name: ""
subscription_value: "" subscription_value: ""
subscription_value_2: "" subscription_value_2: ""
subscription_value_3: "" subscription_value_3: ""
@ -64,6 +144,87 @@ presets:
subscription_value_18: "" subscription_value_18: ""
subscription_value_19: "" subscription_value_19: ""
subscription_value_20: "" subscription_value_20: ""
subscription_value_21: ""
subscription_value_22: ""
subscription_value_23: ""
subscription_value_24: ""
subscription_value_25: ""
subscription_value_26: ""
subscription_value_27: ""
subscription_value_28: ""
subscription_value_29: ""
subscription_value_30: ""
subscription_value_31: ""
subscription_value_32: ""
subscription_value_33: ""
subscription_value_34: ""
subscription_value_35: ""
subscription_value_36: ""
subscription_value_37: ""
subscription_value_38: ""
subscription_value_39: ""
subscription_value_40: ""
subscription_value_41: ""
subscription_value_42: ""
subscription_value_43: ""
subscription_value_44: ""
subscription_value_45: ""
subscription_value_46: ""
subscription_value_47: ""
subscription_value_48: ""
subscription_value_49: ""
subscription_value_50: ""
subscription_value_51: ""
subscription_value_52: ""
subscription_value_53: ""
subscription_value_54: ""
subscription_value_55: ""
subscription_value_56: ""
subscription_value_57: ""
subscription_value_58: ""
subscription_value_59: ""
subscription_value_60: ""
subscription_value_61: ""
subscription_value_62: ""
subscription_value_63: ""
subscription_value_64: ""
subscription_value_65: ""
subscription_value_66: ""
subscription_value_67: ""
subscription_value_68: ""
subscription_value_69: ""
subscription_value_70: ""
subscription_value_71: ""
subscription_value_72: ""
subscription_value_73: ""
subscription_value_74: ""
subscription_value_75: ""
subscription_value_76: ""
subscription_value_77: ""
subscription_value_78: ""
subscription_value_79: ""
subscription_value_80: ""
subscription_value_81: ""
subscription_value_82: ""
subscription_value_83: ""
subscription_value_84: ""
subscription_value_85: ""
subscription_value_86: ""
subscription_value_87: ""
subscription_value_88: ""
subscription_value_89: ""
subscription_value_90: ""
subscription_value_91: ""
subscription_value_92: ""
subscription_value_93: ""
subscription_value_94: ""
subscription_value_95: ""
subscription_value_96: ""
subscription_value_97: ""
subscription_value_98: ""
subscription_value_99: ""
subscription_value_100: ""
url: "{subscription_value}" url: "{subscription_value}"
url2: "{subscription_value_2}" url2: "{subscription_value_2}"
url3: "{subscription_value_3}" url3: "{subscription_value_3}"
@ -84,3 +245,83 @@ presets:
url18: "{subscription_value_18}" url18: "{subscription_value_18}"
url19: "{subscription_value_19}" url19: "{subscription_value_19}"
url20: "{subscription_value_20}" url20: "{subscription_value_20}"
url21: "{subscription_value_21}"
url22: "{subscription_value_22}"
url23: "{subscription_value_23}"
url24: "{subscription_value_24}"
url25: "{subscription_value_25}"
url26: "{subscription_value_26}"
url27: "{subscription_value_27}"
url28: "{subscription_value_28}"
url29: "{subscription_value_29}"
url30: "{subscription_value_30}"
url31: "{subscription_value_31}"
url32: "{subscription_value_32}"
url33: "{subscription_value_33}"
url34: "{subscription_value_34}"
url35: "{subscription_value_35}"
url36: "{subscription_value_36}"
url37: "{subscription_value_37}"
url38: "{subscription_value_38}"
url39: "{subscription_value_39}"
url40: "{subscription_value_40}"
url41: "{subscription_value_41}"
url42: "{subscription_value_42}"
url43: "{subscription_value_43}"
url44: "{subscription_value_44}"
url45: "{subscription_value_45}"
url46: "{subscription_value_46}"
url47: "{subscription_value_47}"
url48: "{subscription_value_48}"
url49: "{subscription_value_49}"
url50: "{subscription_value_50}"
url51: "{subscription_value_51}"
url52: "{subscription_value_52}"
url53: "{subscription_value_53}"
url54: "{subscription_value_54}"
url55: "{subscription_value_55}"
url56: "{subscription_value_56}"
url57: "{subscription_value_57}"
url58: "{subscription_value_58}"
url59: "{subscription_value_59}"
url60: "{subscription_value_60}"
url61: "{subscription_value_61}"
url62: "{subscription_value_62}"
url63: "{subscription_value_63}"
url64: "{subscription_value_64}"
url65: "{subscription_value_65}"
url66: "{subscription_value_66}"
url67: "{subscription_value_67}"
url68: "{subscription_value_68}"
url69: "{subscription_value_69}"
url70: "{subscription_value_70}"
url71: "{subscription_value_71}"
url72: "{subscription_value_72}"
url73: "{subscription_value_73}"
url74: "{subscription_value_74}"
url75: "{subscription_value_75}"
url76: "{subscription_value_76}"
url77: "{subscription_value_77}"
url78: "{subscription_value_78}"
url79: "{subscription_value_79}"
url80: "{subscription_value_80}"
url81: "{subscription_value_81}"
url82: "{subscription_value_82}"
url83: "{subscription_value_83}"
url84: "{subscription_value_84}"
url85: "{subscription_value_85}"
url86: "{subscription_value_86}"
url87: "{subscription_value_87}"
url88: "{subscription_value_88}"
url89: "{subscription_value_89}"
url90: "{subscription_value_90}"
url91: "{subscription_value_91}"
url92: "{subscription_value_92}"
url93: "{subscription_value_93}"
url94: "{subscription_value_94}"
url95: "{subscription_value_95}"
url96: "{subscription_value_96}"
url97: "{subscription_value_97}"
url98: "{subscription_value_98}"
url99: "{subscription_value_99}"
url100: "{subscription_value_100}"

View file

@ -343,6 +343,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
preset=self._preset_options, preset=self._preset_options,
plugins=plugins, plugins=plugins,
enhanced_download_archive=self._enhanced_download_archive, enhanced_download_archive=self._enhanced_download_archive,
overrides=self.overrides,
working_directory=self.working_directory, working_directory=self.working_directory,
dry_run=dry_run, dry_run=dry_run,
) )
@ -395,6 +396,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
preset=self._preset_options, preset=self._preset_options,
plugins=plugins, plugins=plugins,
enhanced_download_archive=self._enhanced_download_archive, enhanced_download_archive=self._enhanced_download_archive,
overrides=self.overrides,
working_directory=self.working_directory, working_directory=self.working_directory,
dry_run=dry_run, dry_run=dry_run,
) )

View file

@ -9,7 +9,7 @@ from typing import final
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
from ytdl_sub.config.overrides import Overrides from ytdl_sub.config.overrides import Overrides
from ytdl_sub.entries.variables.override_variables import OverrideVariables from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.validators import DictValidator from ytdl_sub.validators.validators import DictValidator
@ -32,7 +32,7 @@ class SubscriptionOutput(Validator, ABC):
indent overrides to merge with the preset dict's overrides indent overrides to merge with the preset dict's overrides
""" """
return { return {
OverrideVariables.subscription_indent_i(i): self._indent_overrides[i] SubscriptionVariables.subscription_indent_i(i): self._indent_overrides[i]
for i in range(len(self._indent_overrides)) for i in range(len(self._indent_overrides))
} }
@ -143,7 +143,7 @@ class SubscriptionValueValidator(SubscriptionLeafValidator, StringValidator):
presets=presets, presets=presets,
indent_overrides=indent_overrides, indent_overrides=indent_overrides,
) )
self._overrides_to_add[OverrideVariables.subscription_value()] = self.value self._overrides_to_add[SubscriptionVariables.subscription_value()] = self.value
class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator): class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator):
@ -168,10 +168,12 @@ class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValid
for idx, list_value in enumerate(self.list): for idx, list_value in enumerate(self.list):
# Write the first list value into subscription_value as well # Write the first list value into subscription_value as well
if idx == 0: if idx == 0:
self._overrides_to_add[OverrideVariables.subscription_value()] = list_value.value self._overrides_to_add[
SubscriptionVariables.subscription_value()
] = list_value.value
self._overrides_to_add[ self._overrides_to_add[
OverrideVariables.subscription_value_i(index=idx) SubscriptionVariables.subscription_value_i(index=idx)
] = list_value.value ] = list_value.value
@ -215,7 +217,7 @@ class SubscriptionMapValidator(SubscriptionLeafValidator, LiteralDictValidator):
presets=presets, presets=presets,
indent_overrides=indent_overrides, indent_overrides=indent_overrides,
) )
self._overrides_to_add[OverrideVariables.subscription_map()] = ScriptUtils.to_script( self._overrides_to_add[SubscriptionVariables.subscription_map()] = ScriptUtils.to_script(
self.dict self.dict
) )

View file

@ -7,6 +7,7 @@ from typing import TypeVar
from yt_dlp import match_filter_func from yt_dlp import match_filter_func
from ytdl_sub.config.overrides import Overrides
from ytdl_sub.config.plugin.plugin import Plugin from ytdl_sub.config.plugin.plugin import Plugin
from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset import Preset
from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder from ytdl_sub.downloaders.ytdl_options_builder import YTDLOptionsBuilder
@ -33,12 +34,14 @@ class SubscriptionYTDLOptions:
preset: Preset, preset: Preset,
plugins: List[Plugin], plugins: List[Plugin],
enhanced_download_archive: EnhancedDownloadArchive, enhanced_download_archive: EnhancedDownloadArchive,
overrides: Overrides,
working_directory: str, working_directory: str,
dry_run: bool, dry_run: bool,
): ):
self._preset = preset self._preset = preset
self._plugins = plugins self._plugins = plugins
self._enhanced_download_archive = enhanced_download_archive self._enhanced_download_archive = enhanced_download_archive
self._overrides = overrides
self._working_directory = working_directory self._working_directory = working_directory
self._dry_run = dry_run self._dry_run = dry_run
@ -56,8 +59,8 @@ class SubscriptionYTDLOptions:
ytdl-options to apply to every run no matter what ytdl-options to apply to every run no matter what
""" """
ytdl_options = { ytdl_options = {
# Download all files in the format of {id}.{ext} # Download all files in the format of {id}.{ext}, where id is sanitized
"outtmpl": str(Path(self._working_directory) / "%(id)s.%(ext)s"), "outtmpl": str(Path(self._working_directory) / "%(id)S.%(ext)s"),
# Always write thumbnails # Always write thumbnails
"writethumbnail": True, "writethumbnail": True,
"ffmpeg_location": FFMPEG.ffmpeg_path(), "ffmpeg_location": FFMPEG.ffmpeg_path(),
@ -78,6 +81,7 @@ class SubscriptionYTDLOptions:
"skip_download": True, "skip_download": True,
"writethumbnail": False, "writethumbnail": False,
"writeinfojson": True, "writeinfojson": True,
"extract_flat": "discard", # do not store info.json in mem since its in file
} }
@property @property
@ -90,6 +94,11 @@ class SubscriptionYTDLOptions:
if self._preset.output_options.maintain_download_archive: if self._preset.output_options.maintain_download_archive:
ytdl_options["download_archive"] = self._enhanced_download_archive.working_file_path ytdl_options["download_archive"] = self._enhanced_download_archive.working_file_path
if self._preset.output_options.keep_max_files:
# yt-dlp has a weird bug with max_downloads=1, set to 2 for safe measure
ytdl_options["max_downloads"] = max(
int(self._overrides.apply_formatter(self._preset.output_options.keep_max_files)), 2
)
return ytdl_options return ytdl_options

View file

@ -2,6 +2,7 @@ import copy
from abc import ABC from abc import ABC
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Optional
from typing import Set from typing import Set
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
@ -13,21 +14,47 @@ from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.script import ScriptUtils from ytdl_sub.utils.script import ScriptUtils
_BASE_SCRIPT: Script = Script(
ScriptUtils.add_sanitized_variables(
dict(copy.deepcopy(VARIABLE_SCRIPTS), **copy.deepcopy(CUSTOM_FUNCTION_SCRIPTS))
)
)
class Scriptable(ABC): class Scriptable(ABC):
""" """
Shared class between Entry and Overrides to manage their underlying Script. Shared class between Entry and Overrides to manage their underlying Script.
""" """
_BASE_SCRIPT: Script = Script( def __init__(self, initialize_base_script: bool = False):
ScriptUtils.add_sanitized_variables( self._script: Optional[Script] = None
dict(copy.deepcopy(VARIABLE_SCRIPTS), **copy.deepcopy(CUSTOM_FUNCTION_SCRIPTS)) self._unresolvable: Optional[Set[str]] = None
)
)
def __init__(self): if initialize_base_script:
self.script = copy.deepcopy(Scriptable._BASE_SCRIPT) self.initialize_base_script()
self.unresolvable: Set[str] = copy.deepcopy(UNRESOLVED_VARIABLES)
def initialize_base_script(self):
"""
Initializes with base values
"""
self._script = copy.deepcopy(_BASE_SCRIPT)
self._unresolvable = copy.deepcopy(UNRESOLVED_VARIABLES)
@property
def script(self) -> Script:
"""
Initialized script
"""
assert self._script is not None, "Not initialized"
return self._script
@property
def unresolvable(self) -> Set[str]:
"""
Initialized unresolvable variables
"""
assert self._unresolvable is not None, "Not initialized"
return self._unresolvable
def update_script(self) -> None: def update_script(self) -> None:
""" """
@ -45,7 +72,7 @@ class Scriptable(ABC):
for var, definition in values.items() for var, definition in values.items()
} }
self.unresolvable -= set(list(values_as_str.keys())) self._unresolvable -= set(list(values_as_str.keys()))
self.script.add( self.script.add(
ScriptUtils.add_sanitized_variables( ScriptUtils.add_sanitized_variables(
{ {

View file

@ -2,9 +2,9 @@ from typing import Type
from tools.docgen.docgen import DocGen from tools.docgen.docgen import DocGen
from tools.docgen.entry_variables import EntryVariablesDocGen from tools.docgen.entry_variables import EntryVariablesDocGen
from tools.docgen.override_variables import OverrideVariablesDocGen
from tools.docgen.plugins import PluginsDocGen from tools.docgen.plugins import PluginsDocGen
from tools.docgen.scripting_functions import ScriptingFunctionsDocGen from tools.docgen.scripting_functions import ScriptingFunctionsDocGen
from tools.docgen.static_variables import StaticVariablesDocGen
from ytdl_sub.utils.file_handler import get_md5_hash from ytdl_sub.utils.file_handler import get_md5_hash
@ -20,8 +20,8 @@ class TestDocGen:
def test_entry_variables_generated(self): def test_entry_variables_generated(self):
_test_doc_gen(EntryVariablesDocGen) _test_doc_gen(EntryVariablesDocGen)
def test_override_variables_generated(self): def test_static_variables_generated(self):
_test_doc_gen(OverrideVariablesDocGen) _test_doc_gen(StaticVariablesDocGen)
def test_scripting_functions_generated(self): def test_scripting_functions_generated(self):
_test_doc_gen(ScriptingFunctionsDocGen) _test_doc_gen(ScriptingFunctionsDocGen)

View file

@ -1,25 +0,0 @@
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 ytdl_sub.entries.variables.override_variables import OverrideVariables
class OverrideVariablesDocGen(DocGen):
LOCATION = Path("docs/source/config_reference/scripting/override_variables.rst")
@classmethod
def generate(cls) -> str:
docs = section("Override Variables", level=0)
for name in static_methods(OverrideVariables):
docs += get_function_docs(
function_name=name,
obj=OverrideVariables,
level=1,
)
return docs

View file

@ -39,6 +39,8 @@ def should_filter_property(property_name: str) -> bool:
"dict_with_format_strings", "dict_with_format_strings",
"subscription_name", "subscription_name",
"list", "list",
"script",
"unresolvable",
) )

View file

@ -0,0 +1,26 @@
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 ytdl_sub.entries.variables.override_variables import SubscriptionVariables
class StaticVariablesDocGen(DocGen):
LOCATION = Path("docs/source/config_reference/scripting/static_variables.rst")
@classmethod
def generate(cls) -> str:
docs = section("Static Variables", level=0)
docs += section("Subscription Variables", level=1)
for name in static_methods(SubscriptionVariables):
docs += get_function_docs(
function_name=name,
obj=SubscriptionVariables,
level=2,
)
return docs