Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ behavior.

sanitize
~~~~~~~~
:spec: ``sanitize(value: AnyArgument) -> String``
:spec: ``sanitize(value: AnyArgument, ...) -> String``

Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use
for file/directory names on any OS.
Expand Down
4 changes: 2 additions & 2 deletions src/ytdl_sub/entries/script/custom_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ def truncate_filepath_if_too_long(filepath: String) -> String:
return String(FilePathTruncater.maybe_truncate_file_path(filepath.value))

@staticmethod
def sanitize(value: AnyArgument) -> String:
def sanitize(*value: AnyArgument) -> String:
"""
Sanitize a string using yt-dlp's ``sanitize_filename`` method to ensure it's safe to use
for file/directory names on any OS.
"""
return String(sanitize_filename(str(value)))
return String("".join(sanitize_filename(str(val)) for val in value))

@staticmethod
def sanitize_plex_episode(string: String) -> String:
Expand Down
10 changes: 9 additions & 1 deletion src/ytdl_sub/entries/script/variable_definitions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from abc import ABC
from functools import cache, cached_property
from typing import Dict, Set
from typing import Dict, Optional, Set

from ytdl_sub.entries.script.custom_functions import CustomFunctions
from ytdl_sub.entries.script.variable_types import (
Expand Down Expand Up @@ -1215,6 +1215,14 @@ def unresolvable_static_variables(self) -> Set[Variable]:
VARIABLES.entry_metadata,
} | self.injected_variables()

def get(self, name: str) -> Optional[Variable]:
"""
Returns the variable attribute if it exists. None otherwise.
"""
if not hasattr(self, name):
return None
return getattr(self, name)


# Singletons to use externally
VARIABLES: VariableDefinitions = VariableDefinitions()
Expand Down
68 changes: 68 additions & 0 deletions src/ytdl_sub/utils/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import re
from typing import Any, Dict, Optional

from ytdl_sub.entries.script.custom_functions import CustomFunctions
from ytdl_sub.entries.script.variable_definitions import VARIABLES
from ytdl_sub.entries.script.variable_types import BooleanVariable, IntegerVariable
from ytdl_sub.script.parser import parse
from ytdl_sub.script.types.array import Array, UnresolvedArray
from ytdl_sub.script.types.function import BuiltInFunction, Function
Expand All @@ -13,6 +16,7 @@
from ytdl_sub.script.utils.name_validation import is_function

# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches


class ScriptUtils:
Expand Down Expand Up @@ -110,6 +114,62 @@ def _get_quote_char(cls, arg: str) -> str:
return '"'
return "'''"

@classmethod
def _maybe_to_optimized_sanitize(cls, arg: Argument) -> Argument:
# If it is %sanitize(%concat(...)), return %sanitize(...)
if (
isinstance(arg, Function)
and arg.name == "sanitize"
and len(arg.args) == 1
and isinstance(arg.args[0], Function)
and arg.args[0].name == "concat"
):
return BuiltInFunction(name="sanitize", args=arg.args[0].args)

return arg

@classmethod
def _maybe_sanitized_script_code(cls, arg: Argument) -> Optional[str]:
if not (isinstance(arg, Function) and arg.name == "sanitize"):
return None

output = ""
for sub_arg in arg.args:
if isinstance(sub_arg, Variable):
# No need to sanitize built-in integer variables
if isinstance(VARIABLES.get(sub_arg.name), (IntegerVariable, BooleanVariable)):
output += f"{{ {sub_arg.name} }}"
else:
output += f"{{ {sub_arg.name}_sanitized }}"
elif isinstance(sub_arg, (Integer, Float, Boolean)):
output += str(sub_arg.native)
elif isinstance(sub_arg, String):
output += CustomFunctions.sanitize(sub_arg).native
elif isinstance(sub_arg, BuiltInFunction) and (
issubclass(sub_arg.function_spec.return_type, (Integer, Float, Boolean))
or sub_arg.name == "pad_zero"
):
# If we know the function's output is sanitized, let's not wrap it
output += cls._to_script_code(sub_arg, top_level=True)
else:
# Purposefully do not set top_level to True so we do not recurse
output += (
f"{{ {cls._to_script_code(BuiltInFunction(name='sanitize', args=[sub_arg]))} }}"
)

return output

@classmethod
def _maybe_concat_script_code(cls, arg: Argument) -> Optional[str]:
if not (isinstance(arg, Function) and arg.name == "concat"):
return None

out = ""
for sub_arg in arg.args:
out += cls._to_script_code(sub_arg, top_level=True)

return out

@classmethod
def _to_script_code(cls, arg: Argument, top_level: bool = False) -> str:
if not top_level and isinstance(arg, (Integer, Boolean, Float)):
Expand All @@ -123,6 +183,14 @@ def _to_script_code(cls, arg: Argument, top_level: bool = False) -> str:

return arg.native if top_level else f"{quote}{arg.native}{quote}"

arg = cls._maybe_to_optimized_sanitize(arg)

if top_level:
if (out := cls._maybe_sanitized_script_code(arg)) is not None:
return out
if (out := cls._maybe_concat_script_code(arg)) is not None:
return out

if isinstance(arg, Integer):
out = f"%int({arg.native})"
elif isinstance(arg, Boolean):
Expand Down
12 changes: 6 additions & 6 deletions tests/resources/expected_json/music/inspect_sub_fill.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,21 @@
"file_name": "{ track_full_path }",
"keep_files_date_eval": "{ upload_date_standardized }",
"maintain_download_archive": true,
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpzald2h7x",
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpv_h5mst_",
"preserve_mtime": false,
"thumbnail_name": "{ album_cover_path }"
},
"overrides": {
"album_cover_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }",
"album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }",
"album_cover_path": "Lester Young/[{ playlist_max_upload_year }] { playlist_title_sanitized }/folder.{ thumbnail_ext }",
"album_dir": "[{ playlist_max_upload_year }] { playlist_title_sanitized }",
"artist_dir": "Lester Young",
"avatar_uncropped_thumbnail_file_name": "",
"banner_uncropped_thumbnail_file_name": "",
"enable_resolution_assert": true,
"enable_throttle_protection": true,
"include_sibling_metadata": true,
"modified_webpage_url": "{ webpage_url }",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpzald2h7x",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpv_h5mst_",
"resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }",
"resolution_assert_height_gte": 361,
"resolution_assert_ignore_titles": "{ [ ] }",
Expand All @@ -110,8 +110,8 @@
"track_album_artist": "Lester Young",
"track_artist": "Lester Young",
"track_date": "{ upload_date_standardized }",
"track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }",
"track_full_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }",
"track_file_name": "{ playlist_index_padded } - { title_sanitized }.{ ext }",
"track_full_path": "Lester Young/[{ playlist_max_upload_year }] { playlist_title_sanitized }/{ playlist_index_padded } - { title_sanitized }.{ ext }",
"track_genre": "Jazz",
"track_genre_default": "Unset",
"track_number": "{ playlist_index }",
Expand Down
12 changes: 6 additions & 6 deletions tests/resources/expected_json/music/inspect_sub_internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@
},
"output_options": {
"download_archive_name": ".ytdl-sub-Lester Young-download-archive.json",
"file_name": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/{ %concat( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ), \" - \", %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ), \".\", ext ) }",
"file_name": "Lester Young/[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }/{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }",
"keep_files_date_eval": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }",
"maintain_download_archive": true,
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpdu1vad67",
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp7z9ceu_d",
"preserve_mtime": false,
"thumbnail_name": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/folder.jpg"
"thumbnail_name": "Lester Young/[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }/folder.jpg"
},
"overrides": {
"album_cover_path": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/folder.jpg",
"album_cover_path": "Lester Young/[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }/folder.jpg",
"album_dir": "[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }",
"artist_dir": "Lester Young",
"avatar_uncropped_thumbnail_file_name": "",
Expand All @@ -93,7 +93,7 @@
"enable_throttle_protection": true,
"include_sibling_metadata": true,
"modified_webpage_url": "{ %map_get( entry_metadata, \"webpage_url\" ) }",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpdu1vad67",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp7z9ceu_d",
"resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }",
"resolution_assert_height_gte": 361,
"resolution_assert_ignore_titles": "{ [ ] }",
Expand All @@ -111,7 +111,7 @@
"track_artist": "Lester Young",
"track_date": "{ %string( %map_get( %to_date_metadata( %map_get_non_empty( entry_metadata, \"upload_date\", %datetime_strftime( %map_get( entry_metadata, \"epoch\" ), \"%Y%m%d\" ) ) ), \"date_standardized\" ) ) }",
"track_file_name": "{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }",
"track_full_path": "Lester Young/{ %concat( \"[\", %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ), \"] \", %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) ) }/{ %concat( %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ), \" - \", %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ), \".\", ext ) }",
"track_full_path": "Lester Young/[{ %int( %map_get( %to_date_metadata( %array_reduce( %if_passthrough( %extract_field_from_siblings( \"upload_date\" ), [ upload_date ] ), %max ) ), \"year\" ) ) }] { %sanitize( %map_get_non_empty( entry_metadata, \"playlist_title\", %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) ) }/{ %pad_zero( %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ), 2 ) } - { %sanitize( %map_get_non_empty( entry_metadata, \"title\", %map_get( entry_metadata, \"id\" ) ) ) }.{ ext }",
"track_genre": "Jazz",
"track_genre_default": "Unset",
"track_number": "{ %map_get_non_empty( entry_metadata, \"playlist_index\", 1 ) }",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"enable_throttle_protection": true,
"include_sibling_metadata": true,
"modified_webpage_url": "{webpage_url}",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpk6coazyn",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpgcphf_8p",
"resolution_assert": "{\n %if(\n %and(\n enable_resolution_assert,\n %ne( height, 0 ),\n %not(resolution_assert_is_ignored)\n ),\n %assert(\n %gte( height, resolution_assert_height_gte ),\n %concat(\n \"Entry \",\n title,\n \" downloaded at a low resolution (\",\n resolution_readable,\n \"), you've probably been throttled. \",\n \"Stopping further downloads, wait a few hours and try again. \",\n \"Disable using the override variable `enable_resolution_assert: False`.\"\n )\n ),\n \"false is no-op\"\n )\n}",
"resolution_assert_height_gte": 361,
"resolution_assert_ignore_titles": "{ [] }",
Expand Down
16 changes: 8 additions & 8 deletions tests/resources/expected_json/music/inspect_sub_resolve.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,24 @@
},
"output_options": {
"download_archive_name": ".ytdl-sub-Lester Young-download-archive.json",
"file_name": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }",
"file_name": "Lester Young/[{ playlist_max_upload_year }] { playlist_title_sanitized }/{ playlist_index_padded } - { title_sanitized }.{ ext }",
"keep_files_date_eval": "{ upload_date_standardized }",
"maintain_download_archive": true,
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpd5oeacb3",
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmplsyhpyfi",
"preserve_mtime": false,
"thumbnail_name": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }"
"thumbnail_name": "Lester Young/[{ playlist_max_upload_year }] { playlist_title_sanitized }/folder.{ thumbnail_ext }"
},
"overrides": {
"album_cover_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/folder.{ thumbnail_ext }",
"album_dir": "[{ playlist_max_upload_year }] { %sanitize( playlist_title ) }",
"album_cover_path": "Lester Young/[{ playlist_max_upload_year }] { playlist_title_sanitized }/folder.{ thumbnail_ext }",
"album_dir": "[{ playlist_max_upload_year }] { playlist_title_sanitized }",
"artist_dir": "Lester Young",
"avatar_uncropped_thumbnail_file_name": "",
"banner_uncropped_thumbnail_file_name": "",
"enable_resolution_assert": true,
"enable_throttle_protection": true,
"include_sibling_metadata": true,
"modified_webpage_url": "{ webpage_url }",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpd5oeacb3",
"music_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmplsyhpyfi",
"resolution_assert": "{ %if( %and( enable_resolution_assert, %ne( height, 0 ), %not( resolution_assert_is_ignored ) ), %assert( %gte( height, resolution_assert_height_gte ), %concat( \"Entry \", title, \" downloaded at a low resolution (\", resolution_readable, \"), you've probably been throttled. \", \"Stopping further downloads, wait a few hours and try again. \", \"Disable using the override variable `enable_resolution_assert: False`.\" ) ), \"false is no-op\" ) }",
"resolution_assert_height_gte": 361,
"resolution_assert_ignore_titles": "{ [ ] }",
Expand All @@ -110,8 +110,8 @@
"track_album_artist": "Lester Young",
"track_artist": "Lester Young",
"track_date": "{ upload_date_standardized }",
"track_file_name": "{ playlist_index_padded } - { %sanitize( title ) }.{ ext }",
"track_full_path": "Lester Young/{ %concat( \"[\", playlist_max_upload_year, \"] \", %sanitize( playlist_title ) ) }/{ %concat( playlist_index_padded, \" - \", %sanitize( title ), \".\", ext ) }",
"track_file_name": "{ playlist_index_padded } - { title_sanitized }.{ ext }",
"track_full_path": "Lester Young/[{ playlist_max_upload_year }] { playlist_title_sanitized }/{ playlist_index_padded } - { title_sanitized }.{ ext }",
"track_genre": "Jazz",
"track_genre_default": "Unset",
"track_number": "{ playlist_index }",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"info_json_name": "{ music_video_file_name }.{ info_json_ext }",
"keep_files_date_eval": "{ upload_date_standardized }",
"maintain_download_archive": true,
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpyukbh6ta",
"output_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp23dslv3t",
"preserve_mtime": false,
"thumbnail_name": "{ music_video_file_name }.jpg"
},
Expand Down Expand Up @@ -66,7 +66,7 @@
"music_video_album_default": "Music Videos",
"music_video_artist": "Rick Astley",
"music_video_date": "{ %elif( %contains_url_field( \"date\" ), %get_url_field( \"date\", upload_date_standardized ), %contains_url_field( \"year\" ), %concat( %get_url_field( \"date\", upload_year ), \"-01-01\" ), upload_date_standardized ) }",
"music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmpyukbh6ta",
"music_video_directory": "/var/folders/rw/hl1xmkmj68zdl2kjx3l0dwzc0000gn/T/tmp23dslv3t",
"music_video_file_name": "Rick Astley/{ %sanitize( %get_url_field( \"title\", title ) ) }",
"music_video_file_name_suffix": "",
"music_video_genre": "Pop",
Expand Down
Loading
Loading