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.
623 lines
21 KiB
Python
623 lines
21 KiB
Python
import copy
|
|
from pathlib import Path
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import Optional
|
|
from typing import Tuple
|
|
|
|
import pytest
|
|
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
|
|
from ytdl_sub.prebuilt_presets.tv_show import TvShowCollectionEpisodeFormattingPresets
|
|
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
|
|
|
|
|
|
def _tv_show_by_date_combos() -> List[Tuple[None, str, str]]:
|
|
combos: List[Tuple[None, str, str]] = []
|
|
|
|
for media_player_preset in TvShowByDateOldPresets.preset_names:
|
|
for tv_show_structure_preset in TvShowByDateEpisodeFormattingPresets.preset_names:
|
|
combos.append((None, media_player_preset, tv_show_structure_preset))
|
|
|
|
return combos
|
|
|
|
|
|
def _tv_show_by_date_parent_presets(
|
|
all_in_one_preset: Optional[str],
|
|
media_player_preset: str,
|
|
tv_show_structured_preset: str,
|
|
) -> List[str]:
|
|
"""Hack to include the all_in_one presets in this test suite"""
|
|
if all_in_one_preset is not None:
|
|
return [all_in_one_preset]
|
|
return [
|
|
media_player_preset,
|
|
tv_show_structured_preset,
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"all_in_one_preset, media_player_preset, tv_show_structure_preset",
|
|
[
|
|
# Test that the all-in-ones are equivalent to using the two underlying ones
|
|
("Kodi TV Show by Date", "kodi_tv_show_by_date", "season_by_year__episode_by_month_day"),
|
|
(
|
|
"Jellyfin TV Show by Date",
|
|
"jellyfin_tv_show_by_date",
|
|
"season_by_year__episode_by_month_day",
|
|
),
|
|
("Plex TV Show by Date", "plex_tv_show_by_date", "season_by_year__episode_by_month_day"),
|
|
*_tv_show_by_date_combos(),
|
|
],
|
|
)
|
|
class TestPrebuiltTVShowPresets:
|
|
def test_compilation(
|
|
self,
|
|
config,
|
|
all_in_one_preset: Optional[str],
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
):
|
|
parent_presets = _tv_show_by_date_parent_presets(
|
|
all_in_one_preset, media_player_preset, tv_show_structure_preset
|
|
)
|
|
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": parent_presets,
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "test-compile",
|
|
"tv_show_directory": "output_dir",
|
|
},
|
|
},
|
|
)
|
|
|
|
def test_compilation_many_urls(
|
|
self,
|
|
config,
|
|
all_in_one_preset: Optional[str],
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
):
|
|
parent_presets = _tv_show_by_date_parent_presets(
|
|
all_in_one_preset, media_player_preset, tv_show_structure_preset
|
|
)
|
|
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": parent_presets,
|
|
"download": "https://second.url",
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "test-compile",
|
|
"tv_show_directory": "output_dir",
|
|
},
|
|
},
|
|
)
|
|
|
|
def test_compilation_errors_missing_one(
|
|
self,
|
|
config,
|
|
all_in_one_preset: Optional[str],
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
):
|
|
parent_presets = _tv_show_by_date_parent_presets(
|
|
all_in_one_preset, media_player_preset, tv_show_structure_preset
|
|
)
|
|
|
|
for parent_preset in parent_presets:
|
|
parent_presets_missing_one = copy.deepcopy(parent_presets).remove(parent_preset)
|
|
|
|
with pytest.raises(ValidationException):
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": parent_presets_missing_one,
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "test-compile",
|
|
"tv_show_directory": "output_dir",
|
|
},
|
|
},
|
|
)
|
|
|
|
@pytest.mark.parametrize("is_youtube_channel", [True, False])
|
|
@pytest.mark.parametrize("is_many_urls", [True, False])
|
|
def test_non_collection_presets_compile(
|
|
self,
|
|
config,
|
|
subscription_name,
|
|
output_directory,
|
|
mock_download_collection_entries,
|
|
all_in_one_preset: Optional[str],
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
is_youtube_channel: bool,
|
|
is_many_urls: bool,
|
|
):
|
|
|
|
expected_summary_name = "unit/{}/{}/is_yt_{}{}".format(
|
|
media_player_preset,
|
|
tv_show_structure_preset,
|
|
int(is_youtube_channel),
|
|
"_many_urls" if is_many_urls else "",
|
|
)
|
|
parent_presets = _tv_show_by_date_parent_presets(
|
|
all_in_one_preset, media_player_preset, tv_show_structure_preset
|
|
)
|
|
|
|
preset_dict = {
|
|
"preset": parent_presets,
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "Best Prebuilt TV Show by Date",
|
|
"tv_show_directory": output_directory,
|
|
},
|
|
}
|
|
if is_many_urls:
|
|
preset_dict["overrides"]["url2"] = "https://url.number.2.here"
|
|
|
|
subscription = Subscription.from_dict(
|
|
config=config,
|
|
preset_name=subscription_name,
|
|
preset_dict=preset_dict,
|
|
)
|
|
|
|
with mock_download_collection_entries(
|
|
is_youtube_channel=is_youtube_channel, num_urls=2 if is_many_urls else 1
|
|
):
|
|
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",
|
|
)
|
|
|
|
###################################### Perform reformat
|
|
reformatted_tv_show_structure_preset = "season_by_year__episode_by_download_index"
|
|
reformatted_expected_summary_name = "unit/{}/{}/is_yt_{}{}".format(
|
|
media_player_preset,
|
|
reformatted_tv_show_structure_preset,
|
|
int(is_youtube_channel),
|
|
"_many_urls" if is_many_urls else "",
|
|
)
|
|
|
|
reformatted_preset_dict = {
|
|
"preset": parent_presets + [reformatted_tv_show_structure_preset],
|
|
"output_options": {
|
|
"migrated_download_archive_name": ".ytdl-sub-{tv_show_name_sanitized}-download-archive.json"
|
|
},
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "Best Prebuilt TV Show by Date",
|
|
"tv_show_directory": output_directory,
|
|
},
|
|
}
|
|
if is_many_urls:
|
|
reformatted_preset_dict["overrides"]["url2"] = "https://url.number.2.here"
|
|
|
|
reformatted_subscription = Subscription.from_dict(
|
|
config=config, preset_name=subscription_name, preset_dict=reformatted_preset_dict
|
|
)
|
|
|
|
reformatted_transaction_log = reformatted_subscription.update_with_info_json(dry_run=False)
|
|
assert_transaction_log_matches(
|
|
output_directory=output_directory,
|
|
transaction_log=reformatted_transaction_log,
|
|
transaction_log_summary_file_name=(
|
|
f"{expected_summary_name}_reformatted_to_{reformatted_tv_show_structure_preset}.txt"
|
|
),
|
|
)
|
|
assert_expected_downloads(
|
|
output_directory=output_directory,
|
|
dry_run=False,
|
|
expected_download_summary_file_name=f"{reformatted_expected_summary_name}_migrated.json",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("media_player_preset", TvShowCollectionPresets.preset_names)
|
|
@pytest.mark.parametrize(
|
|
"tv_show_structure_preset", TvShowCollectionEpisodeFormattingPresets.preset_names
|
|
)
|
|
class TestPrebuiltTvShowCollectionPresets:
|
|
@pytest.mark.parametrize("season_preset", TvShowCollectionSeasonPresets.preset_names)
|
|
def test_compilation(
|
|
self,
|
|
config,
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
season_preset: str,
|
|
):
|
|
parent_presets: List[str] = [media_player_preset, tv_show_structure_preset, season_preset]
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": parent_presets,
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "test-compile",
|
|
"tv_show_directory": "output_dir",
|
|
f"{season_preset}_url": "https://your.name.here",
|
|
f"{season_preset}_name": "test season name",
|
|
},
|
|
},
|
|
)
|
|
|
|
@pytest.mark.parametrize("season_preset", TvShowCollectionSeasonPresets.preset_names)
|
|
def test_compilation_errors_missing_one(
|
|
self,
|
|
config,
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
season_preset: str,
|
|
):
|
|
parent_presets: List[str] = [media_player_preset, tv_show_structure_preset, season_preset]
|
|
for parent_preset in parent_presets:
|
|
parent_presets_missing_one = copy.deepcopy(parent_presets).remove(parent_preset)
|
|
|
|
with pytest.raises(ValidationException):
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": parent_presets_missing_one,
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"tv_show_name": "test-compile",
|
|
"tv_show_directory": "output_dir",
|
|
f"{season_preset}_url": "https://your.name.here",
|
|
f"{season_preset}_name": "test season name",
|
|
},
|
|
},
|
|
)
|
|
|
|
@pytest.mark.parametrize("season_indices", [[1], [1, 2]])
|
|
@pytest.mark.parametrize("is_youtube_channel", [True, False])
|
|
def test_collection_presets_compile(
|
|
self,
|
|
config,
|
|
subscription_name,
|
|
output_directory,
|
|
mock_download_collection_entries,
|
|
media_player_preset: str,
|
|
tv_show_structure_preset: str,
|
|
season_indices: List[int],
|
|
is_youtube_channel: bool,
|
|
):
|
|
expected_summary_name = "unit/{}/{}/s_{}/is_yt_{}".format(
|
|
media_player_preset,
|
|
tv_show_structure_preset,
|
|
len(season_indices),
|
|
int(is_youtube_channel),
|
|
)
|
|
parent_presets: List[str] = [media_player_preset, tv_show_structure_preset]
|
|
|
|
overrides: Dict[str, str] = {}
|
|
for season_index in season_indices:
|
|
parent_presets.append(f"collection_season_{season_index}")
|
|
|
|
overrides = dict(
|
|
overrides,
|
|
**{
|
|
f"collection_season_{season_index}_name": f"Named Season {season_index}",
|
|
f"collection_season_{season_index}_url": f"https://season.{season_index}.com",
|
|
},
|
|
)
|
|
|
|
subscription = Subscription.from_dict(
|
|
config=config,
|
|
preset_name=subscription_name,
|
|
preset_dict={
|
|
"preset": parent_presets,
|
|
"overrides": dict(
|
|
overrides,
|
|
**{
|
|
"tv_show_name": "Best Prebuilt TV Show Collection",
|
|
"tv_show_directory": output_directory,
|
|
},
|
|
),
|
|
},
|
|
)
|
|
|
|
with mock_download_collection_entries(
|
|
is_youtube_channel=is_youtube_channel, num_urls=len(season_indices)
|
|
):
|
|
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",
|
|
)
|
|
|
|
###################################### Perform reformat
|
|
reformatted_tv_show_structure_preset = (
|
|
"season_by_collection__episode_by_playlist_index_reversed"
|
|
)
|
|
reformatted_expected_summary_name = "unit/{}/{}/s_{}/is_yt_{}".format(
|
|
media_player_preset,
|
|
reformatted_tv_show_structure_preset,
|
|
len(season_indices),
|
|
int(is_youtube_channel),
|
|
)
|
|
|
|
reformatted_subscription = Subscription.from_dict(
|
|
config=config,
|
|
preset_name=subscription_name,
|
|
preset_dict={
|
|
"preset": parent_presets + [reformatted_tv_show_structure_preset],
|
|
"output_options": {
|
|
"migrated_download_archive_name": ".ytdl-sub-{tv_show_name_sanitized}-download-archive.json"
|
|
},
|
|
"overrides": dict(
|
|
overrides,
|
|
**{
|
|
"tv_show_name": "Best Prebuilt TV Show Collection",
|
|
"tv_show_directory": output_directory,
|
|
},
|
|
),
|
|
},
|
|
)
|
|
|
|
reformatted_transaction_log = reformatted_subscription.update_with_info_json(dry_run=False)
|
|
assert_transaction_log_matches(
|
|
output_directory=output_directory,
|
|
transaction_log=reformatted_transaction_log,
|
|
transaction_log_summary_file_name=(
|
|
f"{expected_summary_name}_reformatted_to_{reformatted_tv_show_structure_preset}.txt"
|
|
),
|
|
)
|
|
assert_expected_downloads(
|
|
output_directory=output_directory,
|
|
dry_run=False,
|
|
expected_download_summary_file_name=f"{reformatted_expected_summary_name}_migrated.json",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("music_preset", MusicPresets.preset_names)
|
|
class TestPrebuiltMusicPresets:
|
|
def test_compilation(
|
|
self,
|
|
config,
|
|
music_preset: str,
|
|
):
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": [
|
|
music_preset,
|
|
],
|
|
"overrides": {
|
|
"music_directory": "/music",
|
|
"subscription_value": "https://your.name.here",
|
|
},
|
|
},
|
|
)
|
|
|
|
def test_presets_run(
|
|
self,
|
|
config,
|
|
subscription_name,
|
|
output_directory,
|
|
mock_download_collection_entries,
|
|
music_preset: str,
|
|
):
|
|
expected_summary_name = f"unit/music/{music_preset}"
|
|
|
|
preset_dict = {
|
|
"preset": [
|
|
music_preset,
|
|
],
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"music_directory": output_directory,
|
|
},
|
|
}
|
|
|
|
subscription = Subscription.from_dict(
|
|
config=config,
|
|
preset_name=subscription_name,
|
|
preset_dict=preset_dict,
|
|
)
|
|
|
|
num_urls = 1
|
|
if music_preset == "SoundCloud Discography":
|
|
num_urls = 2 # simulate /albums and /tracks
|
|
|
|
with mock_download_collection_entries(
|
|
is_youtube_channel=False, num_urls=num_urls, is_extracted_audio=True
|
|
):
|
|
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",
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("music_video_preset", MusicVideoPresets.preset_names)
|
|
class TestPrebuiltMusicVideoPresets:
|
|
def test_compilation(
|
|
self,
|
|
config,
|
|
music_video_preset: str,
|
|
):
|
|
_ = Subscription.from_dict(
|
|
config=config,
|
|
preset_name="preset_test",
|
|
preset_dict={
|
|
"preset": [
|
|
music_video_preset,
|
|
],
|
|
"overrides": {
|
|
"music_video_directory": "/music_videos",
|
|
"subscription_value": "https://your.name.here",
|
|
},
|
|
},
|
|
)
|
|
|
|
def test_presets_run(
|
|
self,
|
|
config,
|
|
subscription_name,
|
|
output_directory,
|
|
mock_download_collection_entries,
|
|
music_video_preset: str,
|
|
):
|
|
expected_summary_name = f"unit/music_videos/{music_video_preset}"
|
|
|
|
preset_dict = {
|
|
"preset": [
|
|
music_video_preset,
|
|
],
|
|
"overrides": {
|
|
"url": "https://your.name.here",
|
|
"music_video_directory": output_directory,
|
|
},
|
|
}
|
|
|
|
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=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",
|
|
)
|
|
|
|
|
|
@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",
|
|
)
|