[FEATURE] Enhance music video presets using specialized map syntax (#808)

Enhances the music video presets by supporting the following syntax:

`subscriptions.yaml`
```
__preset__:
  overrides:
    music_video_directory: "/music_videos"

# Choose between Jellyfin/Kodi/Plex Music Videos preset:
#   - Plex Music Videos:
#   - Jellyfin Music Videos:
#   - Kodi Music Videos:
#
"Plex Music Videos":

  = Pop:  # Sets genre tag to "Pop"
    "Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
    "Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"

  = Rock:
    # Prefixing with '+' puts the subscription into 'map-mode'.
    # Music video presets in map-mode support grouping videos into different
    # categories, which get set on the album field.
    #
    # URLs can either be strings, or maps that can overload title, year, date
    "+ Guns N' Roses":
      Music Videos:
        - "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ"
      Concerts:
        - title: "Live at The Ritz - New York City"
          year: "1988"
          url: "https://www.youtube.com/watch?v=OldpIhHPsbs"
        - title: "Live at The Hollywood Bowl"
          date: "2023-01-11"
          url: "https://www.youtube.com/watch?v=Z7hutGlvq9I"
```
The 'map-mode' (denoted by `+`) lets you specify a map of music video categories. Under each category, a URL can be specified as-is or with additional metadata (title, year/date, url).

Music videos and concerts, especially older ones, are typically uploaded by random users with inconsistencies in their titles. This syntax aims to make it easy to specify individual URLs with the ability to overwrite their title and year, and group them by category.
This commit is contained in:
Jesse Bannon 2024-06-06 11:50:33 -07:00 committed by GitHub
parent 5866c12104
commit 5e9472986a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1064 additions and 18 deletions

View file

@ -5,6 +5,18 @@ Static Variables
Subscription Variables
----------------------
subscription_array
~~~~~~~~~~~~~~~~~~
For subscriptions in the form of
.. code-block:: yaml
"Subscription Name":
- "https://url1.com/..."
- "https://url2.com/..."
Store all values into an array named ``subscription_array``.
subscription_has_download_archive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Returns True if the subscription has any entries recorded in a download archive. False

View file

@ -14,12 +14,32 @@ __preset__:
overrides:
music_video_directory: "/music_videos"
# Choose between Jellyfin/Kodi/Plex Music Videos
# "Jellyfin Music Videos":
# "Kodi Music Videos":
# Choose between Jellyfin/Kodi/Plex Music Videos preset:
# - Plex Music Videos:
# - Jellyfin Music Videos:
# - Kodi Music Videos:
"Plex Music Videos":
= Pop: # Sets genre tag to "Pop"
"Rick Astley": "https://www.youtube.com/playlist?list=PLlaN88a7y2_plecYoJxvRFTLHVbIVAOoc"
"Michael Jackson": "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
= Blues:
"Eric Clapton": "https://www.youtube.com/playlist?list=PLABGggHhsbEeaRtdzqnxYoEINsJE_4GF4"
= Rock:
# Prefixing with '+' puts the subscription into 'map-mode'.
# Music video presets in map-mode support grouping videos into different
# categories, which get set on the album field.
#
# URLs can either be strings, or maps that can overload title, year, date
"+ Guns N' Roses":
Music Videos:
- "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ"
Concerts:
- title: "Live at The Ritz - New York City"
year: "1988"
url: "https://www.youtube.com/watch?v=OldpIhHPsbs"
- title: "Live at The Hollywood Bowl"
date: "2023-01-11"
url: "https://www.youtube.com/watch?v=Z7hutGlvq9I"

View file

@ -3,6 +3,7 @@ from typing import Set
from ytdl_sub.entries.script.function_scripts import CUSTOM_FUNCTION_SCRIPTS
from ytdl_sub.entries.script.variable_definitions import VARIABLE_SCRIPTS
from ytdl_sub.entries.script.variable_types import ArrayVariable
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
@ -36,6 +37,21 @@ class SubscriptionVariables:
"""
return StringVariable(variable_name="subscription_value", definition="{ %string('') }")
@staticmethod
def subscription_array() -> ArrayVariable:
"""
For subscriptions in the form of
.. code-block:: yaml
"Subscription Name":
- "https://url1.com/..."
- "https://url2.com/..."
Store all values into an array named ``subscription_array``.
"""
return ArrayVariable(variable_name="subscription_array", definition="{ [] }")
@staticmethod
def subscription_indent_i(index: int) -> StringVariable:
"""

View file

@ -129,6 +129,7 @@ presets:
avatar_uncropped_thumbnail_file_name: ""
banner_uncropped_thumbnail_file_name: ""
subscription_array: "{ [] }"
subscription_value: ""
subscription_value_2: ""
subscription_value_3: ""

View file

@ -0,0 +1,210 @@
presets:
# Preset for categorizing URls like so
#
# subscription_name:
# category_1:
# - https://...1
# - https://...2
# category_2:
# - url: https://...1
# metadata_field_1: 2011
# metadata_field_2: 'abc"
# ...
#
# Metadata field of `url` must exist, other ones can be anything.
# Only supports a single level of nesting.
_url_categorized:
preset:
- "_multi_url_bilateral"
overrides:
# Parameters:
# $0: url string or map containing url + metadata fields
# $1: category key
#
# Output:
# Map containing { url: ..., category: ..., metadata_field_1: ... }
"%flat_array__url_to_map_format": >-
{
%elif(
%is_map($0),
%map_extend( $0, { "category": $1 } ),
%is_string($0),
{ "url": $0, "category": $1 },
%throw("If using album categories, each URL must be either a string or map with metadata fields")
)
}
# Parameters:
# $0: category key
# $1: array of URLs in either string or map form
#
# Output:
# Array containing [{ url: ..., category: ... }, ... ]
"%flat_array__category_to_map_format": >-
{
%array_apply_fixed(
%assert_then(
%is_array( $1 ),
$1,
"If using album categories, each category must map to an array"
),
$0,
%flat_array__url_to_map_format
)
}
# Parameters:
# $0: map containing fields { url: <url_value>, category: ... }
#
# Output:
# Nested map containing { <url_value>: { url: <url_value>, category: ... } }
"%flat_array__url_keyed_map_format": >-
{ { %map_get($0, "url"): $0 } }
# Creates an array in the form of [ { url: ..., category: ..., metadata_field_1: ... }, ... ]
category_url_array: >-
{ %array_flatten( %map_apply( subscription_map, %flat_array__category_to_map_format ) ) }
# Creates a map in the form of { <url value>: { category: ..., metadata_field_1: ... }, ... }
category_url_map: >-
{
%array_reduce(
%array_apply( category_url_array, %flat_array__url_keyed_map_format ),
%map_extend
)
}
# Parameters:
# $0: metadata field to fetch for the current url
# $1: default value
#
# Output:
# Metadata field value if it exists. Otherwise, return default value
"%get_url_field": >-
{ %map_get( %map( %map_get( category_url_map, ytdl_sub_input_url, {} ) ), $0, $1 ) }
# Parameters:
# $0: metadata field to fetch for the current url
#
# Output:
# True if field is specified. False otherwise.
"%contains_url_field": >-
{ %not( %is_null( %get_url_field( $0, null ) ) ) }
# Parameters:
# $0: integer to fetch the i'th url using 1-based indexing
#
# Output:
# i'th url in the category map
"%get_url_i": >-
{
%map_get(
%map( %array_at( category_url_array, %int( %sub($0, 1) ), {} ) ),
"url",
%array_at(subscription_array, %int( %sub($0, 1) ), null)
)
}
subscription_map: "{ {} }"
url: "{ %get_url_i(1) }"
url2: "{ %get_url_i(2) }"
url3: "{ %get_url_i(3) }"
url4: "{ %get_url_i(4) }"
url5: "{ %get_url_i(5) }"
url6: "{ %get_url_i(6) }"
url7: "{ %get_url_i(7) }"
url8: "{ %get_url_i(8) }"
url9: "{ %get_url_i(9) }"
url10: "{ %get_url_i(10) }"
url11: "{ %get_url_i(11) }"
url12: "{ %get_url_i(12) }"
url13: "{ %get_url_i(13) }"
url14: "{ %get_url_i(14) }"
url15: "{ %get_url_i(15) }"
url16: "{ %get_url_i(16) }"
url17: "{ %get_url_i(17) }"
url18: "{ %get_url_i(18) }"
url19: "{ %get_url_i(19) }"
url20: "{ %get_url_i(20) }"
url21: "{ %get_url_i(21) }"
url22: "{ %get_url_i(22) }"
url23: "{ %get_url_i(23) }"
url24: "{ %get_url_i(24) }"
url25: "{ %get_url_i(25) }"
url26: "{ %get_url_i(26) }"
url27: "{ %get_url_i(27) }"
url28: "{ %get_url_i(28) }"
url29: "{ %get_url_i(29) }"
url30: "{ %get_url_i(30) }"
url31: "{ %get_url_i(31) }"
url32: "{ %get_url_i(32) }"
url33: "{ %get_url_i(33) }"
url34: "{ %get_url_i(34) }"
url35: "{ %get_url_i(35) }"
url36: "{ %get_url_i(36) }"
url37: "{ %get_url_i(37) }"
url38: "{ %get_url_i(38) }"
url39: "{ %get_url_i(39) }"
url40: "{ %get_url_i(40) }"
url41: "{ %get_url_i(41) }"
url42: "{ %get_url_i(42) }"
url43: "{ %get_url_i(43) }"
url44: "{ %get_url_i(44) }"
url45: "{ %get_url_i(45) }"
url46: "{ %get_url_i(46) }"
url47: "{ %get_url_i(47) }"
url48: "{ %get_url_i(48) }"
url49: "{ %get_url_i(49) }"
url50: "{ %get_url_i(50) }"
url51: "{ %get_url_i(51) }"
url52: "{ %get_url_i(52) }"
url53: "{ %get_url_i(53) }"
url54: "{ %get_url_i(54) }"
url55: "{ %get_url_i(55) }"
url56: "{ %get_url_i(56) }"
url57: "{ %get_url_i(57) }"
url58: "{ %get_url_i(58) }"
url59: "{ %get_url_i(59) }"
url60: "{ %get_url_i(60) }"
url61: "{ %get_url_i(61) }"
url62: "{ %get_url_i(62) }"
url63: "{ %get_url_i(63) }"
url64: "{ %get_url_i(64) }"
url65: "{ %get_url_i(65) }"
url66: "{ %get_url_i(66) }"
url67: "{ %get_url_i(67) }"
url68: "{ %get_url_i(68) }"
url69: "{ %get_url_i(69) }"
url70: "{ %get_url_i(70) }"
url71: "{ %get_url_i(71) }"
url72: "{ %get_url_i(72) }"
url73: "{ %get_url_i(73) }"
url74: "{ %get_url_i(74) }"
url75: "{ %get_url_i(75) }"
url76: "{ %get_url_i(76) }"
url77: "{ %get_url_i(77) }"
url78: "{ %get_url_i(78) }"
url79: "{ %get_url_i(79) }"
url80: "{ %get_url_i(80) }"
url81: "{ %get_url_i(81) }"
url82: "{ %get_url_i(82) }"
url83: "{ %get_url_i(83) }"
url84: "{ %get_url_i(84) }"
url85: "{ %get_url_i(85) }"
url86: "{ %get_url_i(86) }"
url87: "{ %get_url_i(87) }"
url88: "{ %get_url_i(88) }"
url89: "{ %get_url_i(89) }"
url90: "{ %get_url_i(90) }"
url91: "{ %get_url_i(91) }"
url92: "{ %get_url_i(92) }"
url93: "{ %get_url_i(93) }"
url94: "{ %get_url_i(94) }"
url95: "{ %get_url_i(95) }"
url96: "{ %get_url_i(96) }"
url97: "{ %get_url_i(97) }"
url98: "{ %get_url_i(98) }"
url99: "{ %get_url_i(99) }"
url100: "{ %get_url_i(100) }"

View file

@ -10,3 +10,11 @@ class MusicVideoPresets(PrebuiltPresets):
"Jellyfin Music Videos",
"Plex Music Videos",
}
class MusicVideoExtrasPresets(PrebuiltPresets):
preset_names = {
"Kodi Music Videos with Extras",
"Jellyfin Music Videos with Extras",
"Plex Music Videos with Extras",
}

View file

@ -1,7 +1,8 @@
presets:
_music_video_base:
preset:
- "_multi_url"
- "_url_categorized"
output_options:
output_directory: "{music_video_directory}"
@ -25,12 +26,38 @@ presets:
subscription_indent_1: "{music_video_genre_default}"
# Music Video Overrides
music_video_title: "{title}"
music_video_album: "{music_video_album_default}"
music_video_artist: "{subscription_name}"
music_video_year: "{upload_year}"
music_video_date: "{upload_date_standardized}"
music_video_genre: "{subscription_indent_1}"
music_video_album: >-
{ %get_url_field("category", music_video_album_default) }
music_video_title: >-
{ %get_url_field("title", title) }
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_year: >-
{
%int(%elif(
%contains_url_field("date"),
%slice( %get_url_field("date", upload_date_standardized), 0, 4 ),
%contains_url_field("year"),
%get_url_field("date", upload_year),
upload_year
))
}
# Directory Overrides
music_video_file_name: "{music_video_artist_sanitized}/{music_video_title_sanitized}"
music_video_file_name_suffix: ""
music_video_file_name: "{music_video_artist_sanitized}/{music_video_title_sanitized}{music_video_file_name_suffix}"

View file

@ -0,0 +1,58 @@
#
# Kodi Music Videos with Extras:
# + Rick Astley:
# Music Videos:
# - url1
# - url2
# ...
# Concerts:
# - url: asfasdfasdf
# year: 2023
# title: sdfasff
# exclude: asdffsdf
# - url4
# Extras:
# - url5
# - url6
# Behind The Scenes:
# - ...
# Live:
# - ...
# Lyrics:
# - ...
presets:
_music_video_extras_base:
preset:
- "_url_categorized"
- "_music_video_base"
overrides:
music_video_album: >-
{ %get_url_field("category", music_video_album_default) }
music_video_title: >-
{ %get_url_field("title", title) }
"Jellyfin Music Videos with Extras":
preset:
- "_base"
- "_music_video_extras_base"
- "_music_video_nfo"
- "_music_video_tags"
"Kodi Music Videos with Extras":
preset:
- "_kodi_base"
- "_music_video_extras_base"
- "_music_video_nfo"
- "_music_video_tags"
"Plex Music Videos with Extras":
preset:
- "_plex_video_base"
- "_music_video_extras_base"
- "_music_video_tags"
# overrides:
# metadata_verify_plex_suffix: "{url_metadata}"
# music_video_file_name_suffix: "-{music_video_album}"

View file

@ -143,10 +143,21 @@ class SubscriptionValueValidator(SubscriptionLeafValidator, StringValidator):
presets=presets,
indent_overrides=indent_overrides,
)
# subscription_value
self._overrides_to_add[SubscriptionVariables.subscription_value().variable_name] = (
self.value
)
# subscription_value_0
self._overrides_to_add[
SubscriptionVariables.subscription_value_i(index=0).variable_name
] = self.value
# And the array variable
self._overrides_to_add[SubscriptionVariables.subscription_array().variable_name] = (
ScriptUtils.to_script([self.value])
)
class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValidator):
def __init__(
@ -167,6 +178,7 @@ class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValid
indent_overrides=indent_overrides,
)
# Add indexed variables
for idx, list_value in enumerate(self.list):
# Write the first list value into subscription_value as well
if idx == 0:
@ -178,6 +190,11 @@ class SubscriptionListValuesValidator(SubscriptionLeafValidator, StringListValid
SubscriptionVariables.subscription_value_i(index=idx).variable_name
] = list_value.value
# And the array variable
self._overrides_to_add[SubscriptionVariables.subscription_array().variable_name] = (
ScriptUtils.to_script([list_value.value for list_value in self.list])
)
class SubscriptionWithOverridesValidator(SubscriptionLeafValidator, DictFormatterValidator):
def __init__(
@ -220,7 +237,7 @@ class SubscriptionMapValidator(SubscriptionLeafValidator, LiteralDictValidator):
indent_overrides=indent_overrides,
)
self._overrides_to_add[SubscriptionVariables.subscription_map().variable_name] = (
ScriptUtils.to_script(self.dict)
ScriptUtils.to_script(self.dict, sort_keys=False)
)

View file

@ -35,7 +35,7 @@ class ScriptUtils:
return dict(variables, **sanitized_variables)
@classmethod
def to_script(cls, value: Any) -> str:
def to_script(cls, value: Any, sort_keys: bool = True) -> str:
"""
Converts a python value to a script value
"""
@ -50,7 +50,7 @@ class ScriptUtils:
elif isinstance(value, float):
out = f"{{%float({value})}}"
else:
dumped_json = json.dumps(value, ensure_ascii=False, sort_keys=True)
dumped_json = json.dumps(value, ensure_ascii=False, sort_keys=sort_keys)
# Remove triple-single-quotes from JSON to avoid parsing issues
dumped_json = re.sub("'{3,}", "'", dumped_json)

View file

@ -0,0 +1,19 @@
{
".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed",
"subscription_test/Mock Entry 20-1.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8",
"subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1",
"subscription_test/Mock Entry 20-2.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02",
"subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405",
"subscription_test/Mock Entry 20-3.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d",
"subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352",
"subscription_test/Mock Entry 21-1.info.json": "INFO_JSON",
"subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90",
"subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d"
}

View file

@ -0,0 +1,23 @@
{
".ytdl-sub-subscription_test-download-archive.json": "bcb2e8e8139f5bf589e027f841aa9449",
"subscription_test/Custom Title.info.json": "INFO_JSON",
"subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Custom Title.mp4": "d7c2b8e961a8fcb612bd0d69c96bf0f3",
"subscription_test/Custom Title.nfo": "996f3993b2ba8a0614bef294492dde65",
"subscription_test/Mock Entry 20-4.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-4.mp4": "10f239edf916f8fb9eb461f24a821578",
"subscription_test/Mock Entry 20-4.nfo": "1e7cb1a09c15f155623f6cd796e874ff",
"subscription_test/Mock Entry 20-5.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-5.mp4": "046878a073fb269ec61401cdeb834707",
"subscription_test/Mock Entry 20-5.nfo": "f6100f194ae3c3b24211ff515d97ccdf",
"subscription_test/Mock Entry 20-6.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-6.mp4": "a50ead5d08456f0bcb907f1912caefcf",
"subscription_test/Mock Entry 20-6.nfo": "f51ae05104f5b98531535e75d623f941",
"subscription_test/Mock Entry 20-7.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-7.mp4": "5b35f04254c38c33ba056274b6c3af9f",
"subscription_test/Mock Entry 20-7.nfo": "852c02ea3ab196d808854ee94a9313d2"
}

View file

@ -0,0 +1,19 @@
{
".ytdl-sub-subscription_test-download-archive.json": "b98a30417f259daeec888e1a5047b9ed",
"subscription_test/Mock Entry 20-1.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8",
"subscription_test/Mock Entry 20-1.nfo": "68a6a9e51a12e75d4c68e6e644b409d1",
"subscription_test/Mock Entry 20-2.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02",
"subscription_test/Mock Entry 20-2.nfo": "1cbe51945c1a7a9ced0eb1222e9d2405",
"subscription_test/Mock Entry 20-3.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d",
"subscription_test/Mock Entry 20-3.nfo": "ef07f30d3e882de3f7cb3ad675125352",
"subscription_test/Mock Entry 21-1.info.json": "INFO_JSON",
"subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90",
"subscription_test/Mock Entry 21-1.nfo": "dc714fd9f3c3729f74927ad245b50d9d"
}

View file

@ -0,0 +1,23 @@
{
".ytdl-sub-subscription_test-download-archive.json": "bcb2e8e8139f5bf589e027f841aa9449",
"subscription_test/Custom Title.info.json": "INFO_JSON",
"subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Custom Title.mp4": "d7c2b8e961a8fcb612bd0d69c96bf0f3",
"subscription_test/Custom Title.nfo": "996f3993b2ba8a0614bef294492dde65",
"subscription_test/Mock Entry 20-4.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-4.mp4": "10f239edf916f8fb9eb461f24a821578",
"subscription_test/Mock Entry 20-4.nfo": "1e7cb1a09c15f155623f6cd796e874ff",
"subscription_test/Mock Entry 20-5.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-5.mp4": "046878a073fb269ec61401cdeb834707",
"subscription_test/Mock Entry 20-5.nfo": "f6100f194ae3c3b24211ff515d97ccdf",
"subscription_test/Mock Entry 20-6.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-6.mp4": "a50ead5d08456f0bcb907f1912caefcf",
"subscription_test/Mock Entry 20-6.nfo": "f51ae05104f5b98531535e75d623f941",
"subscription_test/Mock Entry 20-7.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-7.mp4": "5b35f04254c38c33ba056274b6c3af9f",
"subscription_test/Mock Entry 20-7.nfo": "852c02ea3ab196d808854ee94a9313d2"
}

View file

@ -0,0 +1,15 @@
{
".ytdl-sub-subscription_test-download-archive.json": "6cb47203ec56d8318a0d428b5b4efa5d",
"subscription_test/Mock Entry 20-1.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-1.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-1.mp4": "34916dc26e723c3464adcc4687898ca8",
"subscription_test/Mock Entry 20-2.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-2.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-2.mp4": "01a910b7a023325aa9d4ad3a013a1e02",
"subscription_test/Mock Entry 20-3.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-3.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-3.mp4": "812b6db9ff08bf1822e99e43d371400d",
"subscription_test/Mock Entry 21-1.info.json": "INFO_JSON",
"subscription_test/Mock Entry 21-1.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 21-1.mp4": "079832b6e49b283d80be402c9c928b90"
}

View file

@ -0,0 +1,18 @@
{
".ytdl-sub-subscription_test-download-archive.json": "69a9960ce69e3813269412fa7cb1bd33",
"subscription_test/Custom Title.info.json": "INFO_JSON",
"subscription_test/Custom Title.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Custom Title.mp4": "d7c2b8e961a8fcb612bd0d69c96bf0f3",
"subscription_test/Mock Entry 20-4.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-4.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-4.mp4": "10f239edf916f8fb9eb461f24a821578",
"subscription_test/Mock Entry 20-5.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-5.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-5.mp4": "046878a073fb269ec61401cdeb834707",
"subscription_test/Mock Entry 20-6.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-6.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-6.mp4": "a50ead5d08456f0bcb907f1912caefcf",
"subscription_test/Mock Entry 20-7.info.json": "INFO_JSON",
"subscription_test/Mock Entry 20-7.jpg": "e80c508c4818454300133fe1dc1a9cd7",
"subscription_test/Mock Entry 20-7.mp4": "5b35f04254c38c33ba056274b6c3af9f"
}

View file

@ -0,0 +1,77 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Mock Entry 20-1.info.json
Mock Entry 20-1.jpg
Mock Entry 20-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-1
year: 2020
Mock Entry 20-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-1
Mock Entry 20-2.info.json
Mock Entry 20-2.jpg
Mock Entry 20-2.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-2
year: 2020
Mock Entry 20-2.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-2
Mock Entry 20-3.info.json
Mock Entry 20-3.jpg
Mock Entry 20-3.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-07
title: Mock Entry 20-3
year: 2020
Mock Entry 20-3.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-07
title: Mock Entry 20-3
Mock Entry 21-1.info.json
Mock Entry 21-1.jpg
Mock Entry 21-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1
year: 2021
Mock Entry 21-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1

View file

@ -0,0 +1,95 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Custom Title.info.json
Custom Title.jpg
Custom Title.mp4
Video Tags:
album: Concerts
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Custom Title
year: 2021
Custom Title.nfo
NFO tags:
musicvideo:
album: Concerts
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Custom Title
Mock Entry 20-4.info.json
Mock Entry 20-4.jpg
Mock Entry 20-4.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-06
title: Mock Entry 20-4
year: 2020
Mock Entry 20-4.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-06
title: Mock Entry 20-4
Mock Entry 20-5.info.json
Mock Entry 20-5.jpg
Mock Entry 20-5.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-5
year: 2020
Mock Entry 20-5.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-5
Mock Entry 20-6.info.json
Mock Entry 20-6.jpg
Mock Entry 20-6.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-6
year: 2020
Mock Entry 20-6.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-6
Mock Entry 20-7.info.json
Mock Entry 20-7.jpg
Mock Entry 20-7.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-06-06
title: Mock Entry 20-7
year: 2020
Mock Entry 20-7.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-06-06
title: Mock Entry 20-7

View file

@ -0,0 +1,77 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Mock Entry 20-1.info.json
Mock Entry 20-1.jpg
Mock Entry 20-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-1
year: 2020
Mock Entry 20-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-1
Mock Entry 20-2.info.json
Mock Entry 20-2.jpg
Mock Entry 20-2.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-2
year: 2020
Mock Entry 20-2.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-2
Mock Entry 20-3.info.json
Mock Entry 20-3.jpg
Mock Entry 20-3.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-07
title: Mock Entry 20-3
year: 2020
Mock Entry 20-3.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-07
title: Mock Entry 20-3
Mock Entry 21-1.info.json
Mock Entry 21-1.jpg
Mock Entry 21-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1
year: 2021
Mock Entry 21-1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1

View file

@ -0,0 +1,95 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Custom Title.info.json
Custom Title.jpg
Custom Title.mp4
Video Tags:
album: Concerts
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Custom Title
year: 2021
Custom Title.nfo
NFO tags:
musicvideo:
album: Concerts
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Custom Title
Mock Entry 20-4.info.json
Mock Entry 20-4.jpg
Mock Entry 20-4.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-06
title: Mock Entry 20-4
year: 2020
Mock Entry 20-4.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-06
title: Mock Entry 20-4
Mock Entry 20-5.info.json
Mock Entry 20-5.jpg
Mock Entry 20-5.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-5
year: 2020
Mock Entry 20-5.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-5
Mock Entry 20-6.info.json
Mock Entry 20-6.jpg
Mock Entry 20-6.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-6
year: 2020
Mock Entry 20-6.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-6
Mock Entry 20-7.info.json
Mock Entry 20-7.jpg
Mock Entry 20-7.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-06-06
title: Mock Entry 20-7
year: 2020
Mock Entry 20-7.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-06-06
title: Mock Entry 20-7

View file

@ -0,0 +1,45 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Mock Entry 20-1.info.json
Mock Entry 20-1.jpg
Mock Entry 20-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-1
year: 2020
Mock Entry 20-2.info.json
Mock Entry 20-2.jpg
Mock Entry 20-2.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-08
title: Mock Entry 20-2
year: 2020
Mock Entry 20-3.info.json
Mock Entry 20-3.jpg
Mock Entry 20-3.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-07
title: Mock Entry 20-3
year: 2020
Mock Entry 21-1.info.json
Mock Entry 21-1.jpg
Mock Entry 21-1.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Mock Entry 21-1
year: 2021

View file

@ -0,0 +1,55 @@
Files created:
----------------------------------------
{output_directory}
.ytdl-sub-subscription_test-download-archive.json
{output_directory}/subscription_test
Custom Title.info.json
Custom Title.jpg
Custom Title.mp4
Video Tags:
album: Concerts
artist: subscription_test
genre: ytdl-sub
premiered: 2021-08-08
title: Custom Title
year: 2021
Mock Entry 20-4.info.json
Mock Entry 20-4.jpg
Mock Entry 20-4.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-08-06
title: Mock Entry 20-4
year: 2020
Mock Entry 20-5.info.json
Mock Entry 20-5.jpg
Mock Entry 20-5.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-5
year: 2020
Mock Entry 20-6.info.json
Mock Entry 20-6.jpg
Mock Entry 20-6.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-07-06
title: Mock Entry 20-6
year: 2020
Mock Entry 20-7.info.json
Mock Entry 20-7.jpg
Mock Entry 20-7.mp4
Video Tags:
album: Music Videos
artist: subscription_test
genre: ytdl-sub
premiered: 2020-06-06
title: Mock Entry 20-7
year: 2020

View file

@ -34,7 +34,7 @@ def test_subscription_logs_write_to_file(
keep_successful_logs: bool,
match: List[str],
):
subscription_names = ["Rick Astley", "Michael Jackson", "Eric Clapton"]
subscription_names = ["Rick Astley", "Michael Jackson", "Eric Clapton", "Guns N' Roses"]
if match:
subscription_names = ["Rick Astley", "Michael Jackson"]
num_runs = 2

View file

@ -1,3 +1,4 @@
import json
import re
from contextlib import contextmanager
from pathlib import Path
@ -470,6 +471,15 @@ def test_advanced_tv_show_subscriptions(
== "https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_"
)
assert overrides.apply_formatter(overrides.dict["subscription_array"]) == json.dumps(
[
"https://www.youtube.com/@gardeningwithciscoe4430",
"https://www.youtube.com/playlist?list=PLi8V8UemxeG6lo5if5H5g5EbsteELcb0_",
"https://www.youtube.com/playlist?list=PLsJlQSR-KjmaQqqJ9jq18cF6XXXAR4kyn",
"https://www.youtube.com/watch?v=2vq-vPubS5I",
]
)
def test_music_subscriptions(default_config: ConfigFile, music_subscriptions_path: Path):
subs = Subscription.from_file_path(
@ -493,16 +503,31 @@ def test_music_video_subscriptions(default_config: ConfigFile, music_video_subsc
config=default_config, subscription_path=music_video_subscription_path
)
assert len(subs) == 3
assert len(subs) == 4
assert subs[1].name == "Michael Jackson"
monk = subs[1].overrides.script
jackson = subs[1].overrides.script
assert monk.get("subscription_name").native == "Michael Jackson"
assert jackson.get("subscription_name").native == "Michael Jackson"
assert (
monk.get("subscription_value").native
jackson.get("subscription_value").native
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
)
assert monk.get("subscription_indent_1").native == "Pop"
assert jackson.get("subscription_indent_1").native == "Pop"
assert (
jackson.get("url").native
== "https://www.youtube.com/playlist?list=OLAK5uy_mnY03zP6abNWH929q2XhGzWD_2uKJ_n8E"
)
assert subs[3].name == "Guns N' Roses"
gnr = subs[3].overrides.script
assert gnr.get("subscription_name").native == "Guns N' Roses"
assert (
gnr.get("url").native
== "https://www.youtube.com/playlist?list=PLOTK54q5K4INNXaHKtmXYr6J7CajWjqeJ"
)
assert gnr.get("subscription_indent_1").native == "Rock"
assert gnr.get("url2").native == "https://www.youtube.com/watch?v=OldpIhHPsbs"
def test_default_docker_config_and_subscriptions():

View file

@ -1,4 +1,5 @@
import copy
from pathlib import Path
from typing import Dict
from typing import List
from typing import Optional
@ -9,6 +10,7 @@ from expected_download import assert_expected_downloads
from expected_transaction_log import assert_transaction_log_matches
from ytdl_sub.prebuilt_presets.music import MusicPresets
from ytdl_sub.prebuilt_presets.music_videos import MusicVideoExtrasPresets
from ytdl_sub.prebuilt_presets.music_videos import MusicVideoPresets
from ytdl_sub.prebuilt_presets.tv_show import TvShowByDateEpisodeFormattingPresets
from ytdl_sub.prebuilt_presets.tv_show import TvShowByDateOldPresets
@ -16,6 +18,7 @@ from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionEpisodeFormattingP
from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionPresets
from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionSeasonPresets
from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.exceptions import RegexNoMatchException
from ytdl_sub.utils.exceptions import ValidationException
@ -530,3 +533,91 @@ class TestPrebuiltMusicVideoPresets:
dry_run=False,
expected_download_summary_file_name=f"{expected_summary_name}.json",
)
@pytest.mark.parametrize("music_video_preset", MusicVideoPresets.preset_names)
@pytest.mark.parametrize("multi_url", [True, False])
class TestPrebuiltMusicVideoPresetsWithCategories:
def _preset_dict(
self,
output_directory: Path,
music_video_preset: str,
multi_url: bool,
) -> Dict:
subscription_dict = {"Music Videos": ["https://your.name.here"]}
if multi_url:
subscription_dict["Concerts"] = [
{"url": "https://your.name.here2", "title": "Custom Title"}
]
preset_dict = {
"preset": [
music_video_preset,
],
"overrides": {
"music_video_directory": output_directory,
"subscription_map": subscription_dict,
},
}
return preset_dict
def test_compilation(
self,
config,
output_directory: Path,
music_video_preset: str,
multi_url: bool,
):
preset_dict = self._preset_dict(
output_directory=output_directory,
music_video_preset=music_video_preset,
multi_url=multi_url,
)
_ = Subscription.from_dict(
config=config, preset_name="preset_test", preset_dict=preset_dict
)
def test_presets_run(
self,
config,
subscription_name,
output_directory,
mock_download_collection_entries,
music_video_preset: str,
multi_url: bool,
):
expected_summary_name = (
f"unit/music_videos/{music_video_preset} Categorized/multi_url_{multi_url}"
)
preset_dict = self._preset_dict(
output_directory=output_directory,
music_video_preset=music_video_preset,
multi_url=multi_url,
)
subscription = Subscription.from_dict(
config=config,
preset_name=subscription_name,
preset_dict=preset_dict,
)
with mock_download_collection_entries(
is_youtube_channel=False, num_urls=2 if multi_url else 1, is_extracted_audio=False
):
transaction_log = subscription.download(dry_run=False)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name=f"{expected_summary_name}.txt",
)
assert_expected_downloads(
output_directory=output_directory,
dry_run=False,
expected_download_summary_file_name=f"{expected_summary_name}.json",
)