[FEATURE] Automatically handle playlists ordered in reverse (#948)

Playlists have always been a pain-point with ytdl-sub. If an author adds new videos to the end of a playlist, as opposed to the front, it breaks ytdl-sub's intuition of incremental scraping by breaking on the first (oldest) video. This update now makes it possible to handle this in the prebuilt TV Show presets:
- Add each URL variable (`url`, `url2`, ...) into the `download` portion of the subscription twice
- First definition is what we all know and use, simply scrapes first-to-last, then downloads last-to-first
- Second definition does the following:
  - Check to see if a URL is a YouTube playlist URL, if so...
    - Set the field to download, but with modifications to scrape last-to-first, then download first-to-last
  - Otherwise...
    - Set the field to an empty string, which means ytdl-sub will skip it
This commit is contained in:
Jesse Bannon 2024-04-01 04:24:41 -07:00 committed by GitHub
parent 083db0d9dc
commit 017db953bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 2015 additions and 162 deletions

View file

@ -647,3 +647,15 @@ ytdl_sub_input_url
:type: ``String``
:description:
The input URL used in ytdl-sub to create this entry.
ytdl_sub_input_url_count
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The total number of input URLs as defined in the subscription.
ytdl_sub_input_url_index
~~~~~~~~~~~~~~~~~~~~~~~~
:type: ``Integer``
:description:
The index of the input URL as defined in the subscription, top-most being the 0th index.

View file

@ -5,6 +5,11 @@ Static Variables
Subscription Variables
----------------------
subscription_has_download_archive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns True if the subscription has any entries recorded in a download archive. False
otherwise.
subscription_indent_i
~~~~~~~~~~~~~~~~~~~~~
For subscriptions in the form of

View file

@ -7,8 +7,8 @@ import mergedeep
from ytdl_sub.entries.entry import Entry
from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
from ytdl_sub.entries.variables.override_variables import OverrideHelpers
from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.script.parser import parse
from ytdl_sub.script.script import Script
from ytdl_sub.script.utils.exceptions import ScriptVariableNotResolved
@ -62,6 +62,7 @@ class Overrides(DictFormatterValidator, Scriptable):
self.ensure_variable_name_valid(key)
self.unresolvable.add(VARIABLES.entry_metadata.variable_name)
self.unresolvable.update(REQUIRED_OVERRIDE_VARIABLE_NAMES)
def ensure_added_plugin_variable_valid(self, added_variable: str) -> bool:
"""
@ -127,17 +128,10 @@ class Overrides(DictFormatterValidator, Scriptable):
)
return ScriptUtils.add_sanitized_variables(initial_variables)
def initialize_script(
self, subscription_name: str, unresolved_variables: Set[str]
) -> "Overrides":
def initialize_script(self, unresolved_variables: Set[str]) -> "Overrides":
"""
Initialize the override script with override variables + any unresolved variables
Initialize the override script with any unresolved variables
"""
self.script.add(
ScriptUtils.add_sanitized_variables(
{SubscriptionVariables.subscription_name(): subscription_name}
)
)
self.script.add(
self.initial_variables(
unresolved_variables={

View file

@ -198,9 +198,7 @@ class Preset(_PresetShell):
downloader_options=self.downloader_options,
output_options=self.output_options,
plugins=self.plugins,
).initialize_overrides(
subscription_name=self.name, overrides=self.overrides
).ensure_proper_usage()
).initialize_preset_overrides(overrides=self.overrides).ensure_proper_usage()
@property
def name(self) -> str:

View file

@ -13,13 +13,24 @@ from ytdl_sub.config.plugin.preset_plugins import PresetPlugins
from ytdl_sub.config.preset_options import OutputOptions
from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_NAMES
from ytdl_sub.script.script import Script
from ytdl_sub.script.script import _is_function
from ytdl_sub.utils.scriptable import BASE_SCRIPT
from ytdl_sub.validators.string_formatter_validators import to_variable_dependency_format_string
from ytdl_sub.validators.string_formatter_validators import validate_formatters
# Entry variables to mock during validation
_DUMMY_ENTRY_VARIABLES: Dict[str, str] = {
name: to_variable_dependency_format_string(
# pylint: disable=protected-access
script=BASE_SCRIPT,
parsed_format_string=BASE_SCRIPT._variables[name]
# pylint: enable=protected-access
)
for name in BASE_SCRIPT.variable_names
}
def _add_dummy_variables(variables: Iterable[str]) -> Dict[str, str]:
dummy_variables: Dict[str, str] = {}
@ -72,20 +83,7 @@ def _get_added_and_modified_variables(
def _override_variables(overrides: Overrides) -> Set[str]:
return set(list(overrides.initial_variables().keys())) | {
SubscriptionVariables.subscription_name()
}
_DUMMY_ENTRY_VARIABLES: Dict[str, str] = {
name: to_variable_dependency_format_string(
# pylint: disable=protected-access
script=BASE_SCRIPT,
parsed_format_string=BASE_SCRIPT._variables[name]
# pylint: enable=protected-access
)
for name in BASE_SCRIPT.variable_names
}
return set(list(overrides.initial_variables().keys()))
class VariableValidation:
@ -103,13 +101,11 @@ class VariableValidation:
self.resolved_variables: Set[str] = set()
self.unresolved_variables: Set[str] = set()
def initialize_overrides(
self, subscription_name: str, overrides: Overrides
) -> "VariableValidation":
def initialize_preset_overrides(self, overrides: Overrides) -> "VariableValidation":
"""
Do some gymnastics to initialize the Overrides script.
"""
override_variables = _override_variables(overrides)
override_variables = set(list(overrides.initial_variables().keys()))
# Set resolved variables as all entry + override variables
# at this point to generate every possible added/modified variable
@ -145,9 +141,7 @@ class VariableValidation:
# Initialize overrides with unresolved variables + modified variables to throw an error.
# For modified variables, this is to prevent a resolve(update=True) to setting any
# dependencies until it has been explicitly added
overrides = overrides.initialize_script(
subscription_name=subscription_name, unresolved_variables=self.unresolved_variables
)
overrides = overrides.initialize_script(unresolved_variables=self.unresolved_variables)
# copy the script and mock entry variables
self.script = copy.deepcopy(overrides.script)
@ -162,7 +156,16 @@ class VariableValidation:
def _update_script(self) -> None:
_ = self.script.resolve(unresolvable=self.unresolved_variables, update=True)
def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> Set[str]:
def _add_subscription_override_variables(self) -> None:
"""
Add dummy subscription variables for script validation
"""
self.resolved_variables |= REQUIRED_OVERRIDE_VARIABLE_NAMES
def _add_variables(self, plugin_op: PluginOperation, options: OptionsValidator) -> None:
"""
Add dummy variables for script validation
"""
added_variables = options.added_variables(
resolved_variables=self.resolved_variables,
unresolved_variables=self.unresolved_variables,
@ -175,14 +178,14 @@ class VariableValidation:
self.resolved_variables |= resolved_variables
self.unresolved_variables -= resolved_variables
return added_variables
def ensure_proper_usage(self) -> None:
"""
Validate variables resolve as plugins are executed, and return
a mock script which contains actualized added variables from the plugins
"""
self._add_variables(PluginOperation.DOWNLOADER, options=self.downloader_options)
self._add_subscription_override_variables()
# Metadata variables to be added
for plugin_options in PluginMapping.order_options_by(

View file

@ -118,6 +118,7 @@ class InfoJsonDownloader(SourcePlugin[InfoJsonDownloaderOptions]):
)
entries.append(entry)
# TODO: MATCH A URL TO A URL_VALIDATOR !!!
for entry in sorted(entries, key=lambda ent: ent.get(v.download_index, int)):
# Remove each entry from the live download archive since it will get re-added
# unless it is filtered

View file

@ -17,7 +17,7 @@ from ytdl_sub.entries.entry import Entry
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
class SourcePluginExtension(Plugin[TOptionsValidator], ABC):
class SourcePluginExtension(Plugin[TOptionsValidator], Generic[TOptionsValidator], ABC):
"""
Plugins that get added automatically by using a downloader. Downloader options
are the plugin options.

View file

@ -25,6 +25,7 @@ from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.script import ScriptUtils
from ytdl_sub.utils.thumbnail import ThumbnailTypes
from ytdl_sub.utils.thumbnail import download_and_convert_url_thumbnail
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
@ -41,7 +42,30 @@ class URLDownloadState:
self.entries_downloaded = 0
class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
class UrlDownloaderBasePluginExtension(SourcePluginExtension[MultiUrlValidator]):
def _match_entry_to_url_validator(self, entry: Entry) -> UrlValidator:
"""
Handle matching a URL to its original validator. This is for .info.json updates
when older entries have missing variables
"""
input_url_idx = entry.get(v.ytdl_sub_input_url_index, int)
entry_input_url = entry.get(v.ytdl_sub_input_url, str)
if 0 <= input_url_idx < len(self.plugin_options.urls.list):
validator = self.plugin_options.urls.list[input_url_idx]
if self.overrides.apply_formatter(validator.url) == entry_input_url:
return validator
# Match the first validator based on the URL, if one exists
for validator in self.plugin_options.urls.list:
if self.overrides.apply_formatter(validator.url) == entry_input_url:
return validator
# Return the first validator if none exist
return self.plugin_options.urls.list[0]
class UrlDownloaderThumbnailPlugin(UrlDownloaderBasePluginExtension):
def __init__(
self,
options: MultiUrlValidator,
@ -54,10 +78,6 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
enhanced_download_archive=enhanced_download_archive,
)
self._thumbnails_downloaded: Set[str] = set()
self._collection_url_mapping: Dict[str, UrlValidator] = {
self.overrides.apply_formatter(collection_url.url): collection_url
for collection_url in options.urls.list
}
def _download_parent_thumbnails(
self,
@ -136,15 +156,14 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
if not self.is_dry_run:
try_convert_download_thumbnail(entry=entry)
if (input_url := entry.get(v.ytdl_sub_input_url, str)) in self._collection_url_mapping:
self._download_url_thumbnails(
collection_url=self._collection_url_mapping[input_url],
entry=entry,
)
self._download_url_thumbnails(
collection_url=self._match_entry_to_url_validator(entry=entry),
entry=entry,
)
return entry
class UrlDownloaderCollectionVariablePlugin(SourcePluginExtension):
class UrlDownloaderCollectionVariablePlugin(UrlDownloaderBasePluginExtension):
def __init__(
self,
options: MultiUrlValidator,
@ -157,25 +176,12 @@ class UrlDownloaderCollectionVariablePlugin(SourcePluginExtension):
enhanced_download_archive=enhanced_download_archive,
)
self._thumbnails_downloaded: Set[str] = set()
self._collection_url_mapping: Dict[str, UrlValidator] = {
self.overrides.apply_formatter(collection_url.url): collection_url
for collection_url in options.urls.list
}
def modify_entry_metadata(self, entry: Entry) -> Optional[Entry]:
"""
Add collection variables to the entry
"""
# COLLECTION_URL is a recent variable that may not exist for old entries when updating.
# Try to use source_webpage_url if it does not exist
entry_collection_url = entry.get(v.ytdl_sub_input_url, str)
# If the collection URL cannot find its mapping, use the last URL
collection_url = (
self._collection_url_mapping.get(entry_collection_url)
or list(self._collection_url_mapping.values())[-1]
)
collection_url = self._match_entry_to_url_validator(entry=entry)
entry.add(collection_url.variables.dict_with_format_strings)
return entry
@ -232,8 +238,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
self._downloaded_entries: Set[str] = set()
self._url_state: Optional[URLDownloadState] = None
@property
def download_ytdl_options(self) -> Dict:
def download_ytdl_options(self, url_idx: Optional[int] = None) -> Dict:
"""
Returns
-------
@ -242,19 +247,26 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
return (
self._download_ytdl_options_builder.clone()
.add(self.ytdl_option_defaults(), before=True)
.add(
self.plugin_options.urls.list[url_idx].ytdl_options.dict
if url_idx is not None
else None,
before=True,
)
.to_dict()
)
@property
def metadata_ytdl_options(self) -> Dict:
def metadata_ytdl_options(self, ytdl_option_overrides: Dict) -> Dict:
"""
Returns
-------
YTDL options dict for fetching metadata
"""
return (
self._metadata_ytdl_options_builder.clone()
.add(self.ytdl_option_defaults(), before=True)
.add(ytdl_option_overrides, before=True)
.to_dict()
)
@ -265,7 +277,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
-------
True if dry-run is enabled. False otherwise.
"""
return self.download_ytdl_options.get("skip_download", False)
return self.download_ytdl_options().get("skip_download", False)
@property
def is_entry_thumbnails_enabled(self) -> bool:
@ -274,7 +286,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
-------
True if entry thumbnails should be downloaded. False otherwise.
"""
return self.download_ytdl_options.get("writethumbnail", False)
return self.download_ytdl_options().get("writethumbnail", False)
###############################################################################################
# DOWNLOAD FUNCTIONS
@ -301,7 +313,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
clear_info_json_files
Whether to delete info.json files after yield
"""
archive_path = self.download_ytdl_options.get("download_archive", "")
archive_path = self.download_ytdl_options().get("download_archive", "")
backup_archive_path = f"{archive_path}.backup"
# If archive path exists, maintain download archive is enable
@ -336,7 +348,9 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
def _extract_entry_info_with_retry(self, entry: Entry) -> Entry:
download_entry_dict = YTDLP.extract_info_with_retry(
ytdl_options_overrides=self.download_ytdl_options,
ytdl_options_overrides=self.download_ytdl_options(
url_idx=entry.get(v.ytdl_sub_input_url_index, int)
),
is_downloaded_fn=None if self.is_dry_run else entry.is_downloaded,
is_thumbnail_downloaded_fn=None
if (self.is_dry_run or not self.is_entry_thumbnails_enabled)
@ -352,27 +366,29 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
self, entries: List[Entry], download_reversed: bool
) -> Iterator[Entry]:
# Iterate a list of entries, and delete the entries after yielding
indices = list(range(len(entries)))
entries_to_iter: List[Optional[Entry]] = entries
indices = list(range(len(entries_to_iter)))
if download_reversed:
indices = reversed(indices)
for idx in indices:
self._url_state.entries_downloaded += 1
if self._is_downloaded(entries[idx]):
if self._is_downloaded(entries_to_iter[idx]):
download_logger.info(
"Already downloaded entry %d/%d: %s",
self._url_state.entries_downloaded,
self._url_state.entries_total,
entries[idx].title,
entries_to_iter[idx].title,
)
del entries[idx]
entries_to_iter[idx] = None
continue
yield entries[idx]
self._mark_downloaded(entries[idx])
yield entries_to_iter[idx]
self._mark_downloaded(entries_to_iter[idx])
del entries[idx]
entries_to_iter[idx] = None
def _iterate_parent_entry(
self, parent: EntryParent, download_reversed: bool
@ -390,7 +406,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
yield entry_child
def _download_url_metadata(
self, url: str, include_sibling_metadata: bool
self, url: str, include_sibling_metadata: bool, ytdl_options_overrides: Dict
) -> Tuple[List[EntryParent], List[Entry]]:
"""
Downloads only info.json files and forms EntryParent trees
@ -398,7 +414,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
with self._separate_download_archives():
entry_dicts = YTDLP.extract_info_via_info_json(
working_directory=self.working_directory,
ytdl_options_overrides=self.metadata_ytdl_options,
ytdl_options_overrides=ytdl_options_overrides,
log_prefix_on_info_json_dl="Downloading metadata for",
url=url,
)
@ -439,34 +455,50 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
):
yield orphan
def _download_metadata(self, url: str, validator: UrlValidator) -> Iterable[Entry]:
metadata_ytdl_options = self.metadata_ytdl_options(
ytdl_option_overrides=validator.ytdl_options.dict
)
download_reversed = ScriptUtils.bool_formatter_output(
self.overrides.apply_formatter(validator.download_reverse)
)
parents, orphan_entries = self._download_url_metadata(
url=url,
include_sibling_metadata=validator.include_sibling_metadata,
ytdl_options_overrides=metadata_ytdl_options,
)
# TODO: Encapsulate this logic into its own class
self._url_state = URLDownloadState(
entries_total=sum(parent.num_children() for parent in parents) + len(orphan_entries)
)
download_logger.info("Beginning downloads for %s", url)
for entry in self._iterate_entries(
parents=parents,
orphans=orphan_entries,
download_reversed=download_reversed,
):
yield entry
def download_metadata(self) -> Iterable[Entry]:
"""The function to perform the download of all media entries"""
# download the bottom-most urls first since they are top-priority
for collection_url in reversed(self.collection.urls.list):
for idx, url_validator in reversed(list(enumerate(self.collection.urls.list))):
# URLs can be empty. If they are, then skip
if not (url := self.overrides.apply_formatter(collection_url.url)):
if not (url := self.overrides.apply_formatter(url_validator.url)):
continue
parents, orphan_entries = self._download_url_metadata(
url=url, include_sibling_metadata=collection_url.include_sibling_metadata
)
# TODO: Encapsulate this logic into its own class
self._url_state = URLDownloadState(
entries_total=sum(parent.num_children() for parent in parents) + len(orphan_entries)
)
download_logger.info(
"Beginning downloads for %s", self.overrides.apply_formatter(collection_url.url)
)
for entry in self._iterate_entries(
parents=parents,
orphans=orphan_entries,
download_reversed=collection_url.download_reverse,
):
for entry in self._download_metadata(url=url, validator=url_validator):
entry.initialize_script(self.overrides).add(
{v.ytdl_sub_input_url: self.overrides.apply_formatter(collection_url.url)}
{
v.ytdl_sub_input_url: url,
v.ytdl_sub_input_url_index: idx,
v.ytdl_sub_input_url_count: len(self.collection.urls.list),
}
)
yield entry
def download(self, entry: Entry) -> Optional[Entry]:

View file

@ -4,10 +4,12 @@ from typing import Optional
from typing import Set
from ytdl_sub.config.plugin.plugin_operation import PluginOperation
from ytdl_sub.config.preset_options import YTDLOptions
from ytdl_sub.config.validators.options import OptionsValidator
from ytdl_sub.script.parser import parse
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesBooleanFormatterValidator
from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator
from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator
from ytdl_sub.validators.validators import BoolValidator
@ -49,6 +51,7 @@ class UrlValidator(StrictDictValidator):
"source_thumbnails",
"playlist_thumbnails",
"download_reverse",
"ytdl_options",
"include_sibling_metadata",
}
@ -77,7 +80,10 @@ class UrlValidator(StrictDictValidator):
key="playlist_thumbnails", validator=UrlThumbnailListValidator, default=[]
)
self._download_reverse = self._validate_key(
key="download_reverse", validator=BoolValidator, default=True
key="download_reverse", validator=OverridesBooleanFormatterValidator, default="True"
)
self._ytdl_options = self._validate_key(
key="ytdl_options", validator=YTDLOptions, default={}
)
self._include_sibling_metadata = self._validate_key(
key="include_sibling_metadata", validator=BoolValidator, default=False
@ -148,12 +154,20 @@ class UrlValidator(StrictDictValidator):
return self._source_thumbnails
@property
def download_reverse(self) -> bool:
def download_reverse(self) -> OverridesBooleanFormatterValidator:
"""
Optional. Whether to download entries in the reverse order of the metadata downloaded.
Defaults to True.
"""
return self._download_reverse.value
return self._download_reverse
@property
def ytdl_options(self) -> YTDLOptions:
"""
Optional. ``ytdl_options`` that only apply to this URL. These take precedence
over the plugin ``ytdl_options``.
"""
return self._ytdl_options
@property
def include_sibling_metadata(self) -> bool:

View file

@ -5,6 +5,7 @@ from ytdl_sub.entries.script.variable_definitions import VariableDefinitions
v: VariableDefinitions = VARIABLES
# TODO: Make this a proper class with docstrings
CUSTOM_FUNCTION_SCRIPTS: Dict[str, str] = {
#############################################################################################
# SIBLING GETTER

View file

@ -750,6 +750,24 @@ class YtdlSubVariableDefinitions(ABC):
"""
return StringVariable(variable_name="ytdl_sub_input_url", definition="{ %string('') }")
@cached_property
def ytdl_sub_input_url_index(self: "VariableDefinitions") -> IntegerVariable:
"""
:description:
The index of the input URL as defined in the subscription, top-most being the 0th index.
"""
# init as -1 so if prior downloaded entries are known when they do not have this value
# in their .info.json
return IntegerVariable(variable_name="ytdl_sub_input_url_index", definition="{ %int(-1) }")
@cached_property
def ytdl_sub_input_url_count(self: "VariableDefinitions") -> IntegerVariable:
"""
:description:
The total number of input URLs as defined in the subscription.
"""
return IntegerVariable(variable_name="ytdl_sub_input_url_count", definition="{ %int(0) }")
@cached_property
def download_index(self: "VariableDefinitions") -> IntegerVariable:
"""
@ -1100,6 +1118,8 @@ class VariableDefinitions(
self.chapters,
self.sponsorblock_chapters,
self.ytdl_sub_input_url,
self.ytdl_sub_input_url_index,
self.ytdl_sub_input_url_count,
}
@cache

View file

@ -9,6 +9,7 @@ from typing import TypeVar
from ytdl_sub.script.types.array import Array
from ytdl_sub.script.types.map import Map
from ytdl_sub.script.types.resolvable import Boolean
from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import String
@ -63,6 +64,13 @@ class Variable(ABC):
"""
@dataclass(frozen=True)
class BooleanVariable(Variable):
@classmethod
def human_readable_type(cls) -> str:
return Boolean.__name__
@dataclass(frozen=True)
class StringVariable(Variable):
@classmethod

View file

@ -1,5 +1,12 @@
from typing import Dict
from typing import Set
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.script.variable_types import BooleanVariable
from ytdl_sub.entries.script.variable_types import MapVariable
from ytdl_sub.entries.script.variable_types import StringVariable
from ytdl_sub.entries.script.variable_types import Variable
from ytdl_sub.script.functions import Functions
from ytdl_sub.script.utils.name_validation import is_valid_name
@ -9,15 +16,15 @@ SUBSCRIPTION_ARRAY = "subscription_array"
class SubscriptionVariables:
@staticmethod
def subscription_name() -> str:
def subscription_name() -> StringVariable:
"""
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 StringVariable(variable_name="subscription_name", definition="{ %string('') }")
@staticmethod
def subscription_value() -> str:
def subscription_value() -> StringVariable:
"""
For subscriptions in the form of
@ -27,10 +34,10 @@ class SubscriptionVariables:
``subscription_value`` gets set to ``https://...``.
"""
return "subscription_value"
return StringVariable(variable_name="subscription_value", definition="{ %string('') }")
@staticmethod
def subscription_indent_i(index: int) -> str:
def subscription_indent_i(index: int) -> StringVariable:
"""
For subscriptions in the form of
@ -43,10 +50,12 @@ class SubscriptionVariables:
``subscription_indent_1`` and ``subscription_indent_2`` get set to
``Indent Value 1`` and ``Indent Value 2``.
"""
return f"subscription_indent_{index + 1}"
return StringVariable(
variable_name=f"subscription_indent_{index + 1}", definition="{ %string('') }"
)
@staticmethod
def subscription_value_i(index: int) -> str:
def subscription_value_i(index: int) -> StringVariable:
"""
For subscriptions in the form of
@ -60,10 +69,12 @@ class SubscriptionVariables:
and ``https://url2.com/...``. Note that ``subscription_value_1`` also gets set to
``subscription_value``.
"""
return f"subscription_value_{index + 1}"
return StringVariable(
variable_name=f"subscription_value_{index + 1}", definition="{ %string('') }"
)
@staticmethod
def subscription_map() -> str:
def subscription_map() -> MapVariable:
"""
For subscriptions in the form of
@ -89,7 +100,17 @@ class SubscriptionVariables:
]
}
"""
return "subscription_map"
return MapVariable(variable_name="subscription_map", definition="{ {} }")
@staticmethod
def subscription_has_download_archive() -> BooleanVariable:
"""
Returns True if the subscription has any entries recorded in a download archive. False
otherwise.
"""
return BooleanVariable(
variable_name="subscription_has_download_archive", definition="{ %bool(True) }"
)
class OverrideHelpers:
@ -124,3 +145,17 @@ class OverrideHelpers:
return is_valid_name(name=name[1:])
return is_valid_name(name=name)
REQUIRED_OVERRIDE_VARIABLES: Set[Variable] = {
SubscriptionVariables.subscription_name(),
SubscriptionVariables.subscription_has_download_archive(),
}
REQUIRED_OVERRIDE_VARIABLE_DEFINITIONS: Dict[str, str] = {
var.variable_name: var.definition for var in REQUIRED_OVERRIDE_VARIABLES
}
REQUIRED_OVERRIDE_VARIABLE_NAMES: Set[str] = {
var.variable_name for var in REQUIRED_OVERRIDE_VARIABLES
}

View file

@ -330,3 +330,418 @@ presets:
url98: "{subscription_value_98}"
url99: "{subscription_value_99}"
url100: "{subscription_value_100}"
# multi-url with bilateral scraping built into it via
# inspection of the URL and conditionally adding another
# ytdl-sub url download with the scraping and download reversed
_multi_url_bilateral_inner:
preset:
- "_url_bilateral_overrides"
download:
- url: "{%bilateral_url(url) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url2) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url3) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url4) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url5) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url6) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url7) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url8) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url9) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url10) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url11) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url12) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url13) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url14) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url15) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url16) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url17) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url18) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url19) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url20) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url21) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url22) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url23) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url24) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url25) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url26) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url27) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url28) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url29) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url30) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url31) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url32) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url33) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url34) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url35) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url36) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url37) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url38) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url39) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url40) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url41) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url42) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url43) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url44) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url45) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url46) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url47) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url48) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url49) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url50) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url51) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url52) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url53) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url54) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url55) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url56) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url57) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url58) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url59) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url60) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url61) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url62) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url63) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url64) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url65) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url66) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url67) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url68) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url69) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url70) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url71) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url72) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url73) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url74) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url75) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url76) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url77) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url78) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url79) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url80) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url81) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url82) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url83) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url84) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url85) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url86) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url87) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url88) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url89) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url90) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url91) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url92) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url93) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url94) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url95) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url96) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url97) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url98) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(url99) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{%bilateral_url(url100) }"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
_multi_url_bilateral:
preset:
- "_multi_url_bilateral_inner"
- "_multi_url"

View file

@ -0,0 +1,21 @@
presets:
# multi-url with bilateral scraping built into it via
# inspection of the URL and conditionally adding another
# ytdl-sub url download with the scraping and download reversed
_url_bilateral_overrides:
overrides:
enable_bilateral_scraping: True
"%is_bilateral_url": >-
{ %contains( $0, "youtube.com/playlist" ) }
"%bilateral_url": >-
{
%if(
%and(
enable_bilateral_scraping,
subscription_has_download_archive,
%is_bilateral_url($0)
),
$0,
""
)
}

View file

@ -29,15 +29,6 @@ presets:
- "_kodi_base"
- "_jellyfin_tv_show"
####################################################################################################
# TV show from one or more sources. Uses {url}'s avatar and banner as poster and fanart
_tv_show_by_date:
preset: "_multi_url"
overrides:
avatar_uncropped_thumbnail_file_name: "{tv_show_poster_file_name}"
banner_uncropped_thumbnail_file_name: "{tv_show_fanart_file_name}"
####################################################################################################
_episode_video_tags:

View file

@ -31,6 +31,15 @@ presets:
- "plex_tv_show_by_date"
- "season_by_year__episode_by_month_day"
####################################################################################################
# TV show from one or more sources. Uses {url}'s avatar and banner as poster and fanart
_tv_show_by_date:
preset: "_multi_url_bilateral"
overrides:
avatar_uncropped_thumbnail_file_name: "{tv_show_poster_file_name}"
banner_uncropped_thumbnail_file_name: "{tv_show_fanart_file_name}"
####################################################################################################
_season_by_year:

View file

@ -60,6 +60,9 @@ presets:
##############
_tv_show_collection:
preset:
- "_tv_show_collection_bilateral"
download:
- url: "{collection_season_1_url}"
variables:
@ -79,6 +82,7 @@ presets:
uid: "avatar_uncropped"
- name: "{tv_show_fanart_file_name}"
uid: "banner_uncropped"
- url: "{collection_season_2_url}"
variables:
collection_season_number: "2"
@ -86,6 +90,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_3_url}"
variables:
collection_season_number: "3"
@ -93,6 +98,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_4_url}"
variables:
collection_season_number: "4"
@ -100,6 +106,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_5_url}"
variables:
collection_season_number: "5"
@ -107,6 +114,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_6_url}"
variables:
collection_season_number: "6"
@ -114,6 +122,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_7_url}"
variables:
collection_season_number: "7"
@ -121,6 +130,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_8_url}"
variables:
collection_season_number: "8"
@ -128,6 +138,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_9_url}"
variables:
collection_season_number: "9"
@ -135,6 +146,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_10_url}"
variables:
collection_season_number: "10"
@ -142,6 +154,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_11_url}"
variables:
collection_season_number: "11"
@ -149,6 +162,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_12_url}"
variables:
collection_season_number: "12"
@ -156,6 +170,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_13_url}"
variables:
collection_season_number: "13"
@ -163,6 +178,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_14_url}"
variables:
collection_season_number: "14"
@ -170,6 +186,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_15_url}"
variables:
collection_season_number: "15"
@ -177,6 +194,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_16_url}"
variables:
collection_season_number: "16"
@ -184,6 +202,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_17_url}"
variables:
collection_season_number: "17"
@ -191,6 +210,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_18_url}"
variables:
collection_season_number: "18"
@ -198,6 +218,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_19_url}"
variables:
collection_season_number: "19"
@ -205,6 +226,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_20_url}"
variables:
collection_season_number: "20"
@ -212,6 +234,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_21_url}"
variables:
collection_season_number: "21"
@ -219,6 +242,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_22_url}"
variables:
collection_season_number: "22"
@ -226,6 +250,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_23_url}"
variables:
collection_season_number: "23"
@ -233,6 +258,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_24_url}"
variables:
collection_season_number: "24"
@ -240,6 +266,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_25_url}"
variables:
collection_season_number: "25"
@ -247,6 +274,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_26_url}"
variables:
collection_season_number: "26"
@ -254,6 +282,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_27_url}"
variables:
collection_season_number: "27"
@ -261,6 +290,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_28_url}"
variables:
collection_season_number: "28"
@ -268,6 +298,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_29_url}"
variables:
collection_season_number: "29"
@ -275,6 +306,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_30_url}"
variables:
collection_season_number: "30"
@ -282,6 +314,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_31_url}"
variables:
collection_season_number: "31"
@ -289,6 +322,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_32_url}"
variables:
collection_season_number: "32"
@ -296,6 +330,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_33_url}"
variables:
collection_season_number: "33"
@ -303,6 +338,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_34_url}"
variables:
collection_season_number: "34"
@ -310,6 +346,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_35_url}"
variables:
collection_season_number: "35"
@ -317,6 +354,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_36_url}"
variables:
collection_season_number: "36"
@ -324,6 +362,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_37_url}"
variables:
collection_season_number: "37"
@ -331,6 +370,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_38_url}"
variables:
collection_season_number: "38"
@ -338,6 +378,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_39_url}"
variables:
collection_season_number: "39"
@ -345,6 +386,7 @@ presets:
playlist_thumbnails:
- name: "{season_poster_file_name}"
uid: "latest_entry"
- url: "{collection_season_40_url}"
variables:
collection_season_number: "40"
@ -648,6 +690,332 @@ presets:
s39_url: ""
s40_url: ""
_tv_show_collection_bilateral:
preset:
- "_url_bilateral_overrides"
download:
- url: "{ %bilateral_url(collection_season_1_url) }"
variables:
collection_season_number: "1"
collection_season_name: "{collection_season_1_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_2_url) }"
variables:
collection_season_number: "2"
collection_season_name: "{collection_season_2_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_3_url) }"
variables:
collection_season_number: "3"
collection_season_name: "{collection_season_3_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_4_url) }"
variables:
collection_season_number: "4"
collection_season_name: "{collection_season_4_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_5_url) }"
variables:
collection_season_number: "5"
collection_season_name: "{collection_season_5_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_6_url) }"
variables:
collection_season_number: "6"
collection_season_name: "{collection_season_6_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_7_url) }"
variables:
collection_season_number: "7"
collection_season_name: "{collection_season_7_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_8_url) }"
variables:
collection_season_number: "8"
collection_season_name: "{collection_season_8_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_9_url) }"
variables:
collection_season_number: "9"
collection_season_name: "{collection_season_9_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_10_url) }"
variables:
collection_season_number: "10"
collection_season_name: "{collection_season_10_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_11_url) }"
variables:
collection_season_number: "11"
collection_season_name: "{collection_season_11_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_12_url) }"
variables:
collection_season_number: "12"
collection_season_name: "{collection_season_12_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_13_url) }"
variables:
collection_season_number: "13"
collection_season_name: "{collection_season_13_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_14_url) }"
variables:
collection_season_number: "14"
collection_season_name: "{collection_season_14_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_15_url) }"
variables:
collection_season_number: "15"
collection_season_name: "{collection_season_15_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_16_url) }"
variables:
collection_season_number: "16"
collection_season_name: "{collection_season_16_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_17_url) }"
variables:
collection_season_number: "17"
collection_season_name: "{collection_season_17_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_18_url) }"
variables:
collection_season_number: "18"
collection_season_name: "{collection_season_18_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_19_url) }"
variables:
collection_season_number: "19"
collection_season_name: "{collection_season_19_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_20_url) }"
variables:
collection_season_number: "20"
collection_season_name: "{collection_season_20_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_21_url) }"
variables:
collection_season_number: "21"
collection_season_name: "{collection_season_21_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_22_url) }"
variables:
collection_season_number: "22"
collection_season_name: "{collection_season_22_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_23_url) }"
variables:
collection_season_number: "23"
collection_season_name: "{collection_season_23_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_24_url) }"
variables:
collection_season_number: "24"
collection_season_name: "{collection_season_24_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_25_url) }"
variables:
collection_season_number: "25"
collection_season_name: "{collection_season_25_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_26_url) }"
variables:
collection_season_number: "26"
collection_season_name: "{collection_season_26_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_27_url) }"
variables:
collection_season_number: "27"
collection_season_name: "{collection_season_27_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_28_url) }"
variables:
collection_season_number: "28"
collection_season_name: "{collection_season_28_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_29_url) }"
variables:
collection_season_number: "29"
collection_season_name: "{collection_season_29_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_30_url) }"
variables:
collection_season_number: "30"
collection_season_name: "{collection_season_30_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_31_url) }"
variables:
collection_season_number: "31"
collection_season_name: "{collection_season_31_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_32_url) }"
variables:
collection_season_number: "32"
collection_season_name: "{collection_season_32_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_33_url) }"
variables:
collection_season_number: "33"
collection_season_name: "{collection_season_33_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_34_url) }"
variables:
collection_season_number: "34"
collection_season_name: "{collection_season_34_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_35_url) }"
variables:
collection_season_number: "35"
collection_season_name: "{collection_season_35_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_36_url) }"
variables:
collection_season_number: "36"
collection_season_name: "{collection_season_36_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_37_url) }"
variables:
collection_season_number: "37"
collection_season_name: "{collection_season_37_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_38_url) }"
variables:
collection_season_number: "38"
collection_season_name: "{collection_season_38_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_39_url) }"
variables:
collection_season_number: "39"
collection_season_name: "{collection_season_39_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
- url: "{ %bilateral_url(collection_season_40_url) }"
variables:
collection_season_number: "40"
collection_season_name: "{collection_season_40_name}"
download_reverse: False
ytdl_options:
playlist_items: "-1:0:-1"
####################################################################################################
# DEPRECATED SEASON PRESETS

View file

@ -9,6 +9,7 @@ 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.downloaders.url.validators import MultiUrlValidator
from ytdl_sub.entries.variables.override_variables import SubscriptionVariables
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
from ytdl_sub.utils.logger import Logger
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
@ -16,6 +17,24 @@ from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadAr
logger = Logger.get("subscription")
def _initialize_download_archive(
output_options: OutputOptions,
overrides: Overrides,
working_directory: str,
output_directory: str,
) -> EnhancedDownloadArchive:
migrated_file_name: Optional[str] = None
if migrated_file_name_option := output_options.migrated_download_archive_name:
migrated_file_name = overrides.apply_formatter(migrated_file_name_option)
return EnhancedDownloadArchive(
file_name=overrides.apply_formatter(output_options.download_archive_name),
working_directory=working_directory,
output_directory=output_directory,
migrated_file_name=migrated_file_name,
).reinitialize(dry_run=True)
class BaseSubscription(ABC):
"""
Subscription classes are the 'controllers' that perform...
@ -48,20 +67,43 @@ class BaseSubscription(ABC):
self._config_options = config_options
self._preset_options = preset_options
migrated_file_name: Optional[str] = None
if migrated_file_name_option := self.output_options.migrated_download_archive_name:
migrated_file_name = self.overrides.apply_formatter(migrated_file_name_option)
# Add overrides pre-archive
self.overrides.add(
{
SubscriptionVariables.subscription_name(): self.name,
}
)
# TODO: Do not include this as part of the subscription
self._enhanced_download_archive = EnhancedDownloadArchive(
file_name=self.overrides.apply_formatter(self.output_options.download_archive_name),
self._enhanced_download_archive: Optional[
EnhancedDownloadArchive
] = _initialize_download_archive(
output_options=self.output_options,
overrides=self.overrides,
working_directory=self.working_directory,
output_directory=self.output_directory,
migrated_file_name=migrated_file_name,
)
# Add post-archive variables
self.overrides.add(
{
SubscriptionVariables.subscription_has_download_archive(): f"""{{
%bool({self.download_archive.num_entries > 0})
}}""",
}
)
self._exception: Optional[Exception] = None
@property
def download_archive(self) -> EnhancedDownloadArchive:
"""
Returns
-------
Initialized download archive
"""
assert self._enhanced_download_archive is not None
return self._enhanced_download_archive
@property
def downloader_options(self) -> MultiUrlValidator:
"""
@ -141,7 +183,7 @@ class BaseSubscription(ABC):
-------
Number of entries added
"""
return self._enhanced_download_archive.num_entries_added
return self.download_archive.num_entries_added
@property
def num_entries_modified(self) -> int:
@ -150,7 +192,7 @@ class BaseSubscription(ABC):
-------
Number of entries modified
"""
return self._enhanced_download_archive.num_entries_modified
return self.download_archive.num_entries_modified
@property
def num_entries_removed(self) -> int:
@ -159,7 +201,7 @@ class BaseSubscription(ABC):
-------
Number of entries removed
"""
return self._enhanced_download_archive.num_entries_removed
return self.download_archive.num_entries_removed
@property
def num_entries(self) -> int:
@ -168,7 +210,7 @@ class BaseSubscription(ABC):
-------
The number of entries
"""
return self._enhanced_download_archive.num_entries
return self.download_archive.num_entries
@property
def transaction_log(self) -> FileHandlerTransactionLog:
@ -177,7 +219,7 @@ class BaseSubscription(ABC):
-------
Transaction log from the subscription
"""
return self._enhanced_download_archive.get_file_handler_transaction_log()
return self.download_archive.get_file_handler_transaction_log()
@property
def exception(self) -> Optional[Exception]:

View file

@ -67,7 +67,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
output_file_name = self.overrides.apply_formatter(
formatter=self.output_options.file_name, entry=entry
)
self._enhanced_download_archive.save_file_to_output_directory(
self.download_archive.save_file_to_output_directory(
file_name=entry.get_download_file_name(),
file_metadata=entry_metadata,
output_file_name=output_file_name,
@ -81,7 +81,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
)
# Copy the thumbnails since they could be used later for other things
self._enhanced_download_archive.save_file_to_output_directory(
self.download_archive.save_file_to_output_directory(
file_name=entry.get_download_thumbnail_name(),
output_file_name=output_thumbnail_name,
entry=entry,
@ -101,7 +101,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
if not dry_run:
entry.write_info_json()
self._enhanced_download_archive.save_file_to_output_directory(
self.download_archive.save_file_to_output_directory(
file_name=entry.get_download_info_json_name(),
output_file_name=output_info_json_name,
entry=entry,
@ -135,7 +135,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
Context manager to initialize the enhanced download archive
"""
if self.maintain_download_archive:
self._enhanced_download_archive.prepare_download_archive()
self.download_archive.prepare_download_archive()
yield
@ -156,19 +156,19 @@ class SubscriptionDownload(BaseSubscription, ABC):
)
if date_range_to_keep or self.output_options.keep_max_files is not None:
self._enhanced_download_archive.remove_stale_files(
self.download_archive.remove_stale_files(
date_range=date_range_to_keep, keep_max_files=keep_max_files
)
self._enhanced_download_archive.save_download_mappings()
FileHandler.delete(self._enhanced_download_archive.working_file_path)
self.download_archive.save_download_mappings()
FileHandler.delete(self.download_archive.working_file_path)
@contextlib.contextmanager
def _remove_empty_directories_in_output_directory(self):
try:
yield
finally:
if not self._enhanced_download_archive.is_dry_run:
if not self.download_archive.is_dry_run:
for root, dir_names, _ in os.walk(Path(self.output_directory), topdown=False):
for dir_name in dir_names:
dir_path = Path(root) / dir_name
@ -194,7 +194,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
plugin_type(
options=plugin_options,
overrides=self.overrides,
enhanced_download_archive=self._enhanced_download_archive,
enhanced_download_archive=self.download_archive,
)
for plugin_type, plugin_options in self.plugins.zipped()
]
@ -233,7 +233,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
# Re-save the download archive after each entry is moved to the output directory
if self.maintain_download_archive:
self._enhanced_download_archive.save_download_mappings()
self.download_archive.save_download_mappings()
def _process_entry(
self, plugins: List[Plugin], dry_run: bool, entry: Entry, entry_metadata: FileMetadata
@ -323,7 +323,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
for plugin in plugins:
plugin.post_process_subscription()
return self._enhanced_download_archive.get_file_handler_transaction_log()
return self.download_archive.get_file_handler_transaction_log()
def download(self, dry_run: bool = False) -> FileHandlerTransactionLog:
"""
@ -336,14 +336,14 @@ class SubscriptionDownload(BaseSubscription, ABC):
directory.
"""
self._exception = None
self._enhanced_download_archive.reinitialize(dry_run=dry_run)
self.download_archive.reinitialize(dry_run=dry_run)
plugins = self._initialize_plugins()
subscription_ytdl_options = SubscriptionYTDLOptions(
preset=self._preset_options,
plugins=plugins,
enhanced_download_archive=self._enhanced_download_archive,
enhanced_download_archive=self.download_archive,
overrides=self.overrides,
working_directory=self.working_directory,
dry_run=dry_run,
@ -351,7 +351,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
downloader = MultiUrlDownloader(
options=self.downloader_options,
enhanced_download_archive=self._enhanced_download_archive,
enhanced_download_archive=self.download_archive,
download_ytdl_options=subscription_ytdl_options.download_builder(),
metadata_ytdl_options=subscription_ytdl_options.metadata_builder(),
overrides=self.overrides,
@ -389,14 +389,14 @@ class SubscriptionDownload(BaseSubscription, ABC):
If true, do not modify any video/audio files or move anything to the output directory.
"""
self._exception = None
self._enhanced_download_archive.reinitialize(dry_run=dry_run)
self.download_archive.reinitialize(dry_run=dry_run)
plugins = self._initialize_plugins()
subscription_ytdl_options = SubscriptionYTDLOptions(
preset=self._preset_options,
plugins=plugins,
enhanced_download_archive=self._enhanced_download_archive,
enhanced_download_archive=self.download_archive,
overrides=self.overrides,
working_directory=self.working_directory,
dry_run=dry_run,
@ -406,7 +406,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
plugins.extend(
MultiUrlDownloader(
options=self.downloader_options,
enhanced_download_archive=self._enhanced_download_archive,
enhanced_download_archive=self.download_archive,
download_ytdl_options=subscription_ytdl_options.download_builder(),
metadata_ytdl_options=subscription_ytdl_options.metadata_builder(),
overrides=self.overrides,
@ -415,7 +415,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
downloader = InfoJsonDownloader(
options=InfoJsonDownloaderOptions(name="no-op", value={}),
enhanced_download_archive=self._enhanced_download_archive,
enhanced_download_archive=self.download_archive,
download_ytdl_options=YTDLOptionsBuilder(),
metadata_ytdl_options=YTDLOptionsBuilder(),
overrides=self.overrides,

View file

@ -32,7 +32,7 @@ class SubscriptionOutput(Validator, ABC):
indent overrides to merge with the preset dict's overrides
"""
return {
SubscriptionVariables.subscription_indent_i(i): self._indent_overrides[i]
SubscriptionVariables.subscription_indent_i(i).variable_name: self._indent_overrides[i]
for i in range(len(self._indent_overrides))
}
@ -143,7 +143,9 @@ class SubscriptionValueValidator(SubscriptionLeafValidator, StringValidator):
presets=presets,
indent_overrides=indent_overrides,
)
self._overrides_to_add[SubscriptionVariables.subscription_value()] = self.value
self._overrides_to_add[
SubscriptionVariables.subscription_value().variable_name
] = self.value
class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator):
@ -169,11 +171,11 @@ class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValid
# Write the first list value into subscription_value as well
if idx == 0:
self._overrides_to_add[
SubscriptionVariables.subscription_value()
SubscriptionVariables.subscription_value().variable_name
] = list_value.value
self._overrides_to_add[
SubscriptionVariables.subscription_value_i(index=idx)
SubscriptionVariables.subscription_value_i(index=idx).variable_name
] = list_value.value
@ -217,9 +219,9 @@ class SubscriptionMapValidator(SubscriptionLeafValidator, LiteralDictValidator):
presets=presets,
indent_overrides=indent_overrides,
)
self._overrides_to_add[SubscriptionVariables.subscription_map()] = ScriptUtils.to_script(
self.dict
)
self._overrides_to_add[
SubscriptionVariables.subscription_map().variable_name
] = ScriptUtils.to_script(self.dict)
class SubscriptionValidator(SubscriptionOutput):

View file

@ -9,13 +9,16 @@ 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_types import Variable
from ytdl_sub.entries.variables.override_variables import REQUIRED_OVERRIDE_VARIABLE_DEFINITIONS
from ytdl_sub.script.script import Script
from ytdl_sub.script.utils.exceptions import RuntimeException
from ytdl_sub.utils.exceptions import StringFormattingException
from ytdl_sub.utils.script import ScriptUtils
BASE_SCRIPT: Script = Script(
dict(ScriptUtils.add_sanitized_variables(VARIABLE_SCRIPTS), **CUSTOM_FUNCTION_SCRIPTS)
ScriptUtils.add_sanitized_variables(VARIABLE_SCRIPTS)
| ScriptUtils.add_sanitized_variables(REQUIRED_OVERRIDE_VARIABLE_DEFINITIONS)
| CUSTOM_FUNCTION_SCRIPTS
)

View file

View file

View file

@ -0,0 +1,22 @@
from unit.script.conftest import single_variable_output
class TestCustomFunctions:
def test_is_playlist_ordered_by_newest_true(self):
assert (
single_variable_output(
"{%is_playlist_ordered_by_newest('https://www.youtube.com/playlist?list=PL5BC0FC26BECA5A35')}"
)
is True
)
def test_is_playlist_ordered_by_newest_false(self):
assert (
single_variable_output(
"{%is_playlist_ordered_by_newest('https://www.youtube.com/playlist?list=PL2KvlCGf4yFftX466OnFS8wvuSosBfUgm')}"
)
is False
)
def test_is_playlist_ordered_by_newest_defaults_false(self):
assert single_variable_output("{%is_playlist_ordered_by_newest('aaaaaa')}") is False

View file

@ -51,6 +51,49 @@ def playlist_preset_dict(output_directory):
}
@pytest.fixture
def tv_show_by_date_bilateral_dict(output_directory):
return {
"preset": [
"Jellyfin TV Show by Date",
],
"format": "worst[ext=mp4]",
"match_filters": {"filters": ["title *= Feb.1"]},
"overrides": {
"url": "https://www.youtube.com/playlist?list=PLd4Q7G88JqoekF0b30NYQcOTnTiIe9Ali",
"tv_show_directory": output_directory,
},
"nfo_tags": {
"tags": {
"subscription_has_download_archive": "{subscription_has_download_archive}",
"download_index": "{download_index}",
}
},
}
@pytest.fixture
def tv_show_collection_bilateral_dict(output_directory):
return {
"preset": [
"Jellyfin TV Show Collection",
],
"format": "worst[ext=mp4]",
"match_filters": {"filters": ["title *= Feb.1"]},
"overrides": {
"s01_url": "https://www.youtube.com/playlist?list=PLd4Q7G88JqoekF0b30NYQcOTnTiIe9Ali",
"s01_name": "bilateral test",
"tv_show_directory": output_directory,
},
"nfo_tags": {
"tags": {
"subscription_has_download_archive": "{subscription_has_download_archive}",
"download_index": "{download_index}",
}
},
}
class TestPlaylist:
"""
Downloads my old minecraft youtube channel, pretends they are music videos. Ensure the above
@ -222,3 +265,71 @@ class TestPlaylist:
assert len(subscriptions) == 1
assert subscriptions[0].transaction_log.is_empty
def test_tv_show_by_date_downloads_bilateral(
self,
tv_show_by_date_bilateral_dict: Dict,
output_directory: str,
default_config: ConfigFile,
):
playlist_subscription = Subscription.from_dict(
config=default_config,
preset_name="bilateral_test",
preset_dict=tv_show_by_date_bilateral_dict,
)
transaction_log = playlist_subscription.download(dry_run=False)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_playlist_bilateral_p1.txt",
)
# Now that one vid is downloaded, attempt to download all and see if bilateral
# logic kicks in
del tv_show_by_date_bilateral_dict["match_filters"]
playlist_subscription = Subscription.from_dict(
config=default_config,
preset_name="bilateral_test",
preset_dict=tv_show_by_date_bilateral_dict,
)
transaction_log = playlist_subscription.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_playlist_bilateral_p2.txt",
)
def test_tv_show_collection_downloads_bilateral(
self,
tv_show_collection_bilateral_dict: Dict,
output_directory: str,
default_config: ConfigFile,
):
playlist_subscription = Subscription.from_dict(
config=default_config,
preset_name="bilateral_test",
preset_dict=tv_show_collection_bilateral_dict,
)
transaction_log = playlist_subscription.download(dry_run=False)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_playlist_bilateral_collection_p1.txt",
)
# Now that one vid is downloaded, attempt to download all and see if bilateral
# logic kicks in
del tv_show_collection_bilateral_dict["match_filters"]
playlist_subscription = Subscription.from_dict(
config=default_config,
preset_name="bilateral_test",
preset_dict=tv_show_collection_bilateral_dict,
)
transaction_log = playlist_subscription.download(dry_run=True)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_playlist_bilateral_collection_p2.txt",
)

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
s2020.e000005 - Mock Entry 20-7.info.json
s2020.e000006 - Mock Entry 20-6.info.json
s2020.e000007 - Mock Entry 20-5.info.json
s2020.e000008 - Mock Entry 20-4.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,15 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
s2020.e000005 - Mock Entry 20-7.info.json
s2020.e000006 - Mock Entry 20-6.info.json
s2020.e000007 - Mock Entry 20-5.info.json
s2020.e000008 - Mock Entry 20-4.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,15 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -208,6 +208,11 @@ Files created:
title: 2021-08-08 - Mock Entry 21-1
year: 2021
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000003 - Mock Entry 20-5.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -208,6 +208,11 @@ Files created:
title: 2021-08-08 - Mock Entry 21-1
year: 2021
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000003 - Mock Entry 20-5.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,14 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-3.info.json
s01.e000002 - Mock Entry 20-2.info.json
s01.e000003 - Mock Entry 20-1.info.json
s01.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,14 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-3.info.json
s01.e000002 - Mock Entry 20-2.info.json
s01.e000003 - Mock Entry 20-1.info.json
s01.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-7.info.json
s01.e000002 - Mock Entry 20-6.info.json
s01.e000003 - Mock Entry 20-5.info.json
s01.e000004 - Mock Entry 20-4.info.json
{output_directory}/Season 02
s02.e000001 - Mock Entry 20-3.info.json
s02.e000002 - Mock Entry 20-2.info.json
s02.e000003 - Mock Entry 20-1.info.json
s02.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-7.info.json
s01.e000002 - Mock Entry 20-6.info.json
s01.e000003 - Mock Entry 20-5.info.json
s01.e000004 - Mock Entry 20-4.info.json
{output_directory}/Season 02
s02.e000001 - Mock Entry 20-3.info.json
s02.e000002 - Mock Entry 20-2.info.json
s02.e000003 - Mock Entry 20-1.info.json
s02.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
s2020.e000005 - Mock Entry 20-7.info.json
s2020.e000006 - Mock Entry 20-6.info.json
s2020.e000007 - Mock Entry 20-5.info.json
s2020.e000008 - Mock Entry 20-4.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,15 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
s2020.e000005 - Mock Entry 20-7.info.json
s2020.e000006 - Mock Entry 20-6.info.json
s2020.e000007 - Mock Entry 20-5.info.json
s2020.e000008 - Mock Entry 20-4.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,15 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry 20-3.info.json
s2020.e000002 - Mock Entry 20-2.info.json
s2020.e000003 - Mock Entry 20-1.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -208,6 +208,11 @@ Files created:
title: 2021-08-08 - Mock Entry 21-1
year: 2021
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000003 - Mock Entry 20-5.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -208,6 +208,11 @@ Files created:
title: 2021-08-08 - Mock Entry 21-1
year: 2021
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000003 - Mock Entry 20-5.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,14 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-3.info.json
s01.e000002 - Mock Entry 20-2.info.json
s01.e000003 - Mock Entry 20-1.info.json
s01.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,14 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-3.info.json
s01.e000002 - Mock Entry 20-2.info.json
s01.e000003 - Mock Entry 20-1.info.json
s01.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-7.info.json
s01.e000002 - Mock Entry 20-6.info.json
s01.e000003 - Mock Entry 20-5.info.json
s01.e000004 - Mock Entry 20-4.info.json
{output_directory}/Season 02
s02.e000001 - Mock Entry 20-3.info.json
s02.e000002 - Mock Entry 20-2.info.json
s02.e000003 - Mock Entry 20-1.info.json
s02.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry 20-7.info.json
s01.e000002 - Mock Entry 20-6.info.json
s01.e000003 - Mock Entry 20-5.info.json
s01.e000004 - Mock Entry 20-4.info.json
{output_directory}/Season 02
s02.e000001 - Mock Entry 20-3.info.json
s02.e000002 - Mock Entry 20-2.info.json
s02.e000003 - Mock Entry 20-1.info.json
s02.e000004 - Mock Entry 21-1.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry -.info.json
s2020.e000002 - Mock Entry -.info.json
s2020.e000003 - Mock Entry -.info.json
s2020.e000005 - Mock Entry -.info.json
s2020.e000006 - Mock Entry -.info.json
s2020.e000007 - Mock Entry -.info.json
s2020.e000008 - Mock Entry -.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,15 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry -.info.json
s2020.e000002 - Mock Entry -.info.json
s2020.e000003 - Mock Entry -.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry -.info.json
s2020.e000002 - Mock Entry -.info.json
s2020.e000003 - Mock Entry -.info.json
s2020.e000005 - Mock Entry -.info.json
s2020.e000006 - Mock Entry -.info.json
s2020.e000007 - Mock Entry -.info.json
s2020.e000008 - Mock Entry -.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,15 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show by Date-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 2020
s2020.e000001 - Mock Entry -.info.json
s2020.e000002 - Mock Entry -.info.json
s2020.e000003 - Mock Entry -.info.json
{output_directory}/Season 2021
s2021.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -110,6 +110,11 @@ Files created:
title: 2021-08-08 - Mock Entry 21-1
year: 2021
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000003 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -110,6 +110,11 @@ Files created:
title: 2021-08-08 - Mock Entry 21-1
year: 2021
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000003 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,14 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry -.info.json
s01.e000002 - Mock Entry -.info.json
s01.e000003 - Mock Entry -.info.json
s01.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,14 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry -.info.json
s01.e000002 - Mock Entry -.info.json
s01.e000003 - Mock Entry -.info.json
s01.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry -.info.json
s01.e000002 - Mock Entry -.info.json
s01.e000003 - Mock Entry -.info.json
s01.e000004 - Mock Entry -.info.json
{output_directory}/Season 02
s02.e000001 - Mock Entry -.info.json
s02.e000002 - Mock Entry -.info.json
s02.e000003 - Mock Entry -.info.json
s02.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -3,6 +3,19 @@ Files created:
{output_directory}
.ytdl-sub-Best Prebuilt TV Show Collection-download-archive.json
Files modified:
----------------------------------------
{output_directory}/Season 01
s01.e000001 - Mock Entry -.info.json
s01.e000002 - Mock Entry -.info.json
s01.e000003 - Mock Entry -.info.json
s01.e000004 - Mock Entry -.info.json
{output_directory}/Season 02
s02.e000001 - Mock Entry -.info.json
s02.e000002 - Mock Entry -.info.json
s02.e000003 - Mock Entry -.info.json
s02.e000004 - Mock Entry -.info.json
Files removed:
----------------------------------------
{output_directory}

View file

@ -0,0 +1,61 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-bilateral_test-download-archive.json
fanart.jpg
poster.jpg
season01-poster.jpg
tvshow.nfo
NFO tags:
tvshow:
genre: ytdl-sub
mpaa: TV-14
namedseason:
attributes:
number: 1
tag: bilateral test
title: bilateral_test
{output_directory}/Season 01
s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg
s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json
s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4
Video Tags:
contentRating: TV-14
date: 2011-02-01
episode_id: 11020101
genre: ytdl-sub
show: bilateral_test
synopsis:
https://www.youtube.com/watch?v=0SVukUyys10
To join the server, you must apply at:
http://www.jesseminecraft.webs.com/
This is just a brief video of the server as of Feb. 1, 2011.
Texture Pack I Use:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
title: 2011-02-01 - Jesse's Minecraft Server [Trailer - Feb.1]
year: 2011
s01.e11020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo
NFO tags:
episodedetails:
aired: 2011-02-01
download_index: 1
episode: 11020101
genre: ytdl-sub
mpaa: TV-14
plot:
https://www.youtube.com/watch?v=0SVukUyys10
To join the server, you must apply at:
http://www.jesseminecraft.webs.com/
This is just a brief video of the server as of Feb. 1, 2011.
Texture Pack I Use:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
season: 1
subscription_has_download_archive: false
title: 2011-02-01 - Jesse's Minecraft Server [Trailer - Feb.1]
year: 2011

View file

@ -0,0 +1,170 @@
Files created:
----------------------------------------
{output_directory}/Season 01
s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg
s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json
s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].mp4
Video Tags:
contentRating: TV-14
date: 2011-02-27
episode_id: 11022701
genre: ytdl-sub
show: bilateral_test
synopsis:
https://www.youtube.com/watch?v=qPybBrXspds
Website Link:
http://jesseminecraft.webs.com/
All you have to do is read the rules, and fill out a quick, little application to join the Server so we know you read them. We do this to keep the griefers/newbs out, it only takes three minutes, it's not that big of a deal.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 750 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 200 empty properties that are ready for anyone to own! Join now!
This is the server state as of Feb. 27, 2011.
----------------------------------------------------------------------------------
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Given to Fly - Pearl Jam
(Off of the 'Yield' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
title: 2011-02-27 - Jesse's Minecraft Server [Trailer - Feb.27]
year: 2011
s01.e11022701 - Jesse's Minecraft Server [Trailer - Feb.27].nfo
NFO tags:
episodedetails:
aired: 2011-02-27
download_index: 2
episode: 11022701
genre: ytdl-sub
mpaa: TV-14
plot:
https://www.youtube.com/watch?v=qPybBrXspds
Website Link:
http://jesseminecraft.webs.com/
All you have to do is read the rules, and fill out a quick, little application to join the Server so we know you read them. We do this to keep the griefers/newbs out, it only takes three minutes, it's not that big of a deal.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 750 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 200 empty properties that are ready for anyone to own! Join now!
This is the server state as of Feb. 27, 2011.
----------------------------------------------------------------------------------
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Given to Fly - Pearl Jam
(Off of the 'Yield' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
season: 1
subscription_has_download_archive: true
title: 2011-02-27 - Jesse's Minecraft Server [Trailer - Feb.27]
year: 2011
s01.e11032101 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg
s01.e11032101 - Jesse's Minecraft Server [Trailer - Mar.21].info.json
s01.e11032101 - Jesse's Minecraft Server [Trailer - Mar.21].mp4
Video Tags:
contentRating: TV-14
date: 2011-03-21
episode_id: 11032101
genre: ytdl-sub
show: bilateral_test
synopsis:
https://www.youtube.com/watch?v=DBjFvs6HafU
Website Link:
http://jesseminecraft.webs.com/
To get on the whitelist, please look at the website linked above (^^^). Due to the overwhelming amount of people trying to join, I've made it so it costs $2 to become a member through paypal. All of it is explained on the website.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 1000 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 300 empty properties that are ready for anyone to own! Join now!
This is the server state as of Mar. 21, 2011.
--------------------------------------------------------------------------------­--
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Indifference - Pearl Jam
(Off of the 'Vs.' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
title: 2011-03-21 - Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011
s01.e11032101 - Jesse's Minecraft Server [Trailer - Mar.21].nfo
NFO tags:
episodedetails:
aired: 2011-03-21
download_index: 3
episode: 11032101
genre: ytdl-sub
mpaa: TV-14
plot:
https://www.youtube.com/watch?v=DBjFvs6HafU
Website Link:
http://jesseminecraft.webs.com/
To get on the whitelist, please look at the website linked above (^^^). Due to the overwhelming amount of people trying to join, I've made it so it costs $2 to become a member through paypal. All of it is explained on the website.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 1000 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 300 empty properties that are ready for anyone to own! Join now!
This is the server state as of Mar. 21, 2011.
--------------------------------------------------------------------------------­--
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Indifference - Pearl Jam
(Off of the 'Vs.' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
season: 1
subscription_has_download_archive: true
title: 2011-03-21 - Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011
Files modified:
----------------------------------------
{output_directory}
.ytdl-sub-bilateral_test-download-archive.json

View file

@ -0,0 +1,56 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-bilateral_test-download-archive.json
fanart.jpg
poster.jpg
tvshow.nfo
NFO tags:
tvshow:
genre: ytdl-sub
mpaa: TV-14
title: bilateral_test
{output_directory}/Season 2011
s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg
s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json
s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4
Video Tags:
contentRating: TV-14
date: 2011-02-01
episode_id: 20101
genre: ytdl-sub
show: bilateral_test
synopsis:
https://www.youtube.com/watch?v=0SVukUyys10
To join the server, you must apply at:
http://www.jesseminecraft.webs.com/
This is just a brief video of the server as of Feb. 1, 2011.
Texture Pack I Use:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
title: 2011-02-01 - Jesse's Minecraft Server [Trailer - Feb.1]
year: 2011
s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo
NFO tags:
episodedetails:
aired: 2011-02-01
download_index: 1
episode: 20101
genre: ytdl-sub
mpaa: TV-14
plot:
https://www.youtube.com/watch?v=0SVukUyys10
To join the server, you must apply at:
http://www.jesseminecraft.webs.com/
This is just a brief video of the server as of Feb. 1, 2011.
Texture Pack I Use:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
season: 2011
subscription_has_download_archive: false
title: 2011-02-01 - Jesse's Minecraft Server [Trailer - Feb.1]
year: 2011

View file

@ -0,0 +1,170 @@
Files created:
----------------------------------------
{output_directory}/Season 2011
s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg
s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json
s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].mp4
Video Tags:
contentRating: TV-14
date: 2011-02-27
episode_id: 22701
genre: ytdl-sub
show: bilateral_test
synopsis:
https://www.youtube.com/watch?v=qPybBrXspds
Website Link:
http://jesseminecraft.webs.com/
All you have to do is read the rules, and fill out a quick, little application to join the Server so we know you read them. We do this to keep the griefers/newbs out, it only takes three minutes, it's not that big of a deal.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 750 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 200 empty properties that are ready for anyone to own! Join now!
This is the server state as of Feb. 27, 2011.
----------------------------------------------------------------------------------
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Given to Fly - Pearl Jam
(Off of the 'Yield' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
title: 2011-02-27 - Jesse's Minecraft Server [Trailer - Feb.27]
year: 2011
s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].nfo
NFO tags:
episodedetails:
aired: 2011-02-27
download_index: 2
episode: 22701
genre: ytdl-sub
mpaa: TV-14
plot:
https://www.youtube.com/watch?v=qPybBrXspds
Website Link:
http://jesseminecraft.webs.com/
All you have to do is read the rules, and fill out a quick, little application to join the Server so we know you read them. We do this to keep the griefers/newbs out, it only takes three minutes, it's not that big of a deal.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 750 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 200 empty properties that are ready for anyone to own! Join now!
This is the server state as of Feb. 27, 2011.
----------------------------------------------------------------------------------
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Given to Fly - Pearl Jam
(Off of the 'Yield' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
season: 2011
subscription_has_download_archive: true
title: 2011-02-27 - Jesse's Minecraft Server [Trailer - Feb.27]
year: 2011
s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg
s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].info.json
s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].mp4
Video Tags:
contentRating: TV-14
date: 2011-03-21
episode_id: 32101
genre: ytdl-sub
show: bilateral_test
synopsis:
https://www.youtube.com/watch?v=DBjFvs6HafU
Website Link:
http://jesseminecraft.webs.com/
To get on the whitelist, please look at the website linked above (^^^). Due to the overwhelming amount of people trying to join, I've made it so it costs $2 to become a member through paypal. All of it is explained on the website.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 1000 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 300 empty properties that are ready for anyone to own! Join now!
This is the server state as of Mar. 21, 2011.
--------------------------------------------------------------------------------­--
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Indifference - Pearl Jam
(Off of the 'Vs.' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
title: 2011-03-21 - Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011
s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].nfo
NFO tags:
episodedetails:
aired: 2011-03-21
download_index: 3
episode: 32101
genre: ytdl-sub
mpaa: TV-14
plot:
https://www.youtube.com/watch?v=DBjFvs6HafU
Website Link:
http://jesseminecraft.webs.com/
To get on the whitelist, please look at the website linked above (^^^). Due to the overwhelming amount of people trying to join, I've made it so it costs $2 to become a member through paypal. All of it is explained on the website.
Jesse's Minecraft Server is a City Roleplay/Survival Server, there is a currency, public transportation, strict rules for immersive gameplay, and has a decent size population. Over 1000 people have logged onto the server. It is very important you read the rules, we don't tolerate idiots.
There are over 300 empty properties that are ready for anyone to own! Join now!
This is the server state as of Mar. 21, 2011.
--------------------------------------------------------------------------------­--
Texture Pack:
http://www.minecraftforum.net/viewtopic.php?f=25&t=29164
Recording Software:
http://www.fraps.com/download.php
Video Editing Software:
http://explore.live.com/windows-live-movie-maker?os=other
Song:
Indifference - Pearl Jam
(Off of the 'Vs.' album)
I claim no ownership of this song, all the credit goes to Pearl Jam and their producers.
season: 2011
subscription_has_download_archive: true
title: 2011-03-21 - Jesse's Minecraft Server [Trailer - Mar.21]
year: 2011
Files modified:
----------------------------------------
{output_directory}
.ytdl-sub-bilateral_test-download-archive.json

View file

@ -74,7 +74,7 @@ class TestConfigFilePartiallyValidatesPresets:
expected_error_message="Validation error in partial_preset.download.1: "
"'partial_preset.download.1' contains the field 'bad_key' which is not allowed. "
"Allowed fields: download_reverse, include_sibling_metadata, playlist_thumbnails, "
"source_thumbnails, url, variables",
"source_thumbnails, url, variables, ytdl_options",
)
@pytest.mark.parametrize(

View file

@ -80,6 +80,7 @@ class TestScript:
}
)
assert script.resolve_once({"url": "{ %bilateral_url('nope') }"})["url"].native == "nope"
script.add({"%bilateral_url_wrap": "{ %bilateral_url($0) }"})
assert (