From a0b46c098204b159a421e7c132702b91e92a6b1f Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Tue, 5 Jul 2022 22:51:23 -0700 Subject: [PATCH] Add --dry-run summary (#92) * tests updated with new class * fix nfo tags, no tests only manual inspection * txt * dry-run output tested * readability * all tests now have files except rolling recent * better * almost fixed * updated txt files w/indents * files updated, looking good * rolling recent dry-run test * Print in main --- src/ytdl_sub/downloaders/downloader.py | 4 +- .../downloaders/youtube/merge_playlist.py | 24 +- .../downloaders/youtube/split_video.py | 36 ++- src/ytdl_sub/main.py | 42 ++- src/ytdl_sub/plugins/music_tags.py | 2 +- src/ytdl_sub/plugins/nfo_tags.py | 9 +- .../plugins/output_directory_nfo_tags.py | 4 +- src/ytdl_sub/subscriptions/subscription.py | 8 +- src/ytdl_sub/utils/chapters.py | 50 ++- src/ytdl_sub/utils/file_handler.py | 64 +++- .../enhanced_download_archive.py | 13 +- tests/e2e/expected_download.py | 51 +-- tests/e2e/expected_transaction_log.py | 55 ++++ .../test_soundcloud_discography.txt | 123 ++++++++ .../youtube/test_channel_full.txt | 273 ++++++++++++++++ .../test_channel_no_additional_downloads.txt | 9 + .../youtube/test_channel_recent.txt | 52 ++++ .../youtube/test_channel_rolling_recent.txt | 15 + .../youtube/test_merge_playlist.txt | 15 + .../youtube/test_playlist.txt | 30 ++ .../youtube/test_split_video.txt | 62 ++++ .../youtube/test_video.txt | 11 + .../soundcloud/test_soundcloud_discography.py | 65 ++-- .../youtube/test_channel_as_kodi_tv_show.py | 292 ++++++++++-------- tests/e2e/youtube/test_merge_playlist.py | 39 +-- .../test_playlist_as_kodi_music_videos.py | 91 +++--- tests/e2e/youtube/test_split_video.py | 62 ++-- 27 files changed, 1167 insertions(+), 334 deletions(-) create mode 100644 tests/e2e/expected_transaction_log.py create mode 100644 tests/e2e/resources/transaction_log_summaries/soundcloud/test_soundcloud_discography.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_channel_full.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_channel_no_additional_downloads.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_channel_recent.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_channel_rolling_recent.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_merge_playlist.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_playlist.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_split_video.txt create mode 100644 tests/e2e/resources/transaction_log_summaries/youtube/test_video.txt diff --git a/src/ytdl_sub/downloaders/downloader.py b/src/ytdl_sub/downloaders/downloader.py index d44a9d62..a2743121 100644 --- a/src/ytdl_sub/downloaders/downloader.py +++ b/src/ytdl_sub/downloaders/downloader.py @@ -8,6 +8,7 @@ from typing import Dict from typing import Generic from typing import List from typing import Optional +from typing import Tuple from typing import Type from typing import TypeVar @@ -18,6 +19,7 @@ from yt_dlp.utils import RejectedVideoReached from ytdl_sub.config.preset_options import Overrides from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.entry import Entry +from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.logger import Logger from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver @@ -197,7 +199,7 @@ class Downloader(DownloadArchiver, Generic[DownloaderOptionsT, DownloaderEntryT] return self._get_entry_dicts_from_info_json_files() @abc.abstractmethod - def download(self) -> List[DownloaderEntryT]: + def download(self) -> List[DownloaderEntryT] | List[Tuple[DownloaderEntryT, FileMetadata]]: """The function to perform the download of all media entries""" def post_download(self, overrides: Overrides): diff --git a/src/ytdl_sub/downloaders/youtube/merge_playlist.py b/src/ytdl_sub/downloaders/youtube/merge_playlist.py index ff43ef9f..9aec32bd 100644 --- a/src/ytdl_sub/downloaders/youtube/merge_playlist.py +++ b/src/ytdl_sub/downloaders/youtube/merge_playlist.py @@ -1,6 +1,7 @@ from typing import Dict from typing import List from typing import Optional +from typing import Tuple from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader from ytdl_sub.downloaders.youtube.playlist import YoutubePlaylistDownloaderOptions @@ -8,6 +9,7 @@ from ytdl_sub.entries.youtube import YoutubeVideo from ytdl_sub.utils.chapters import Chapters from ytdl_sub.utils.chapters import Timestamp from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata +from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.validators.validators import BoolValidator @@ -100,7 +102,7 @@ class YoutubeMergePlaylistDownloader( }, ) - def _add_chapters(self, merged_video: YoutubeVideo) -> None: + def _get_chapters(self, merged_video: YoutubeVideo, add_chapters: bool) -> FileMetadata: titles: List[str] = [] timestamps: List[Timestamp] = [] @@ -111,14 +113,17 @@ class YoutubeMergePlaylistDownloader( current_timestamp_sec += video_entry["duration"] - # TODO: return chapter metadata here - if not self.is_dry_run: + chapters = Chapters(timestamps=timestamps, titles=titles) + + if not self.is_dry_run and add_chapters: add_ffmpeg_metadata( file_path=merged_video.get_download_file_path(), - chapters=Chapters(timestamps=timestamps, titles=titles), + chapters=chapters, file_duration_sec=merged_video.kwargs("duration"), ) + return chapters.to_file_metadata(title="Timestamps of playlist videos in the merged file:") + def _to_merged_video(self, entry_dict: Dict) -> YoutubeVideo: """ Adds a few entries not included in a playlist entry to make it look like a merged video @@ -138,13 +143,12 @@ class YoutubeMergePlaylistDownloader( ) return YoutubeVideo(entry_dict=entry_dict, working_directory=self.working_directory) - def download(self) -> List[YoutubeVideo]: + def download(self) -> List[Tuple[YoutubeVideo, FileMetadata]]: """Download a single Youtube video, then split it into multiple videos""" merged_video = self._to_merged_video( entry_dict=self.extract_info(url=self.download_options.playlist_url) ) - - if self.download_options.add_chapters: - self._add_chapters(merged_video=merged_video) - - return [merged_video] + merged_video_metadata = self._get_chapters( + merged_video=merged_video, add_chapters=self.download_options.add_chapters + ) + return [(merged_video, merged_video_metadata)] diff --git a/src/ytdl_sub/downloaders/youtube/split_video.py b/src/ytdl_sub/downloaders/youtube/split_video.py index 45bd4511..5a2e545d 100644 --- a/src/ytdl_sub/downloaders/youtube/split_video.py +++ b/src/ytdl_sub/downloaders/youtube/split_video.py @@ -3,6 +3,7 @@ import re from pathlib import Path from typing import Dict from typing import List +from typing import Tuple from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader from ytdl_sub.downloaders.youtube.video import YoutubeVideoDownloaderOptions @@ -19,6 +20,7 @@ from ytdl_sub.utils.ffmpeg import FFMPEG # 01:00:00 title # where capture group 1 and 2 are the timestamp and title, respectively from ytdl_sub.utils.file_handler import FileHandler +from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.validators.validators import StringValidator @@ -32,8 +34,8 @@ def _split_video_uid(source_uid: str, idx: int) -> str: def _split_video_ffmpeg_cmd( input_file: str, output_file: str, timestamps: List[Timestamp], idx: int ) -> List[str]: - timestamp_begin = timestamps[idx].timestamp_str - timestamp_end = timestamps[idx + 1].timestamp_str if idx + 1 < len(timestamps) else "" + timestamp_begin = timestamps[idx].standardized_str + timestamp_end = timestamps[idx + 1].standardized_str if idx + 1 < len(timestamps) else "" cmd = ["-i", input_file, "-ss", timestamp_begin] if timestamp_end: @@ -121,15 +123,15 @@ class YoutubeSplitVideoDownloader( ) def _create_split_video_entry( - self, source_entry_dict: Dict, title: str, idx: int, split_video_count: int - ) -> YoutubePlaylistVideo: + self, source_entry_dict: Dict, title: str, idx: int, chapters: Chapters + ) -> Tuple[YoutubePlaylistVideo, FileMetadata]: """ Runs ffmpeg to create the split video """ entry_dict = copy.deepcopy(source_entry_dict) entry_dict["title"] = title entry_dict["playlist_index"] = idx + 1 - entry_dict["playlist_count"] = split_video_count + entry_dict["playlist_count"] = len(chapters.timestamps) entry_dict["id"] = _split_video_uid(source_uid=entry_dict["id"], idx=idx) # Remove track and artist since its now split @@ -138,11 +140,20 @@ class YoutubeSplitVideoDownloader( if "artist" in entry_dict: del entry_dict["artist"] - return YoutubePlaylistVideo(entry_dict=entry_dict, working_directory=self.working_directory) + timestamp_begin = chapters.timestamps[idx].readable_str + timestamp_end = Timestamp(source_entry_dict["duration"]).readable_str + if idx + 1 < len(chapters.timestamps): + timestamp_end = chapters.timestamps[idx + 1].readable_str - def download(self) -> List[YoutubePlaylistVideo]: + metadata = FileMetadata(metadata=f"{timestamp_begin} - {timestamp_end}") + return ( + YoutubePlaylistVideo(entry_dict=entry_dict, working_directory=self.working_directory), + metadata, + ) + + def download(self) -> List[Tuple[YoutubePlaylistVideo, FileMetadata]]: """Download a single Youtube video, then split it into multiple videos""" - split_videos: List[YoutubePlaylistVideo] = [] + split_videos_and_metadata: List[Tuple[YoutubePlaylistVideo, FileMetadata]] = [] chapters = Chapters.from_file(chapters_file_path=self.download_options.split_timestamps) entry_dict = self.extract_info(url=self.download_options.video_url) @@ -178,13 +189,10 @@ class YoutubeSplitVideoDownloader( ) # Format the split video as a YoutubePlaylistVideo - split_videos.append( + split_videos_and_metadata.append( self._create_split_video_entry( - source_entry_dict=entry_dict, - title=title, - idx=idx, - split_video_count=len(chapters.timestamps), + source_entry_dict=entry_dict, title=title, idx=idx, chapters=chapters ) ) - return split_videos + return split_videos_and_metadata diff --git a/src/ytdl_sub/main.py b/src/ytdl_sub/main.py index dc656cfa..f866df84 100644 --- a/src/ytdl_sub/main.py +++ b/src/ytdl_sub/main.py @@ -1,6 +1,7 @@ import argparse import sys from typing import List +from typing import Tuple from ytdl_sub.cli.download_args_parser import DownloadArgsParser from ytdl_sub.cli.main_args_parser import parser @@ -8,12 +9,15 @@ from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.preset import Preset from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.logger import Logger logger = Logger.get() -def _download_subscriptions_from_yaml_files(config: ConfigFile, args: argparse.Namespace) -> None: +def _download_subscriptions_from_yaml_files( + config: ConfigFile, args: argparse.Namespace +) -> List[Tuple[Subscription, FileHandlerTransactionLog]]: """ Downloads all subscriptions from one or many subscription yaml files. @@ -23,10 +27,16 @@ def _download_subscriptions_from_yaml_files(config: ConfigFile, args: argparse.N Configuration file args Arguments from argparse + + Returns + ------- + List of (subscription, transaction_log) """ preset_paths: List[str] = args.subscription_paths presets: List[Preset] = [] + output: List[Tuple[Subscription, FileHandlerTransactionLog]] = [] + # Load all of the presets first to perform all validation before downloading for preset_path in preset_paths: presets += Preset.from_file_path(config=config, subscription_path=preset_path) @@ -34,12 +44,16 @@ def _download_subscriptions_from_yaml_files(config: ConfigFile, args: argparse.N subscription = Subscription.from_preset(preset=preset, config=config) logger.info("Beginning subscription download for %s", subscription.name) - subscription.download(dry_run=args.dry_run) + transaction_log = subscription.download(dry_run=args.dry_run) + + output.append((subscription, transaction_log)) + + return output def _download_subscription_from_cli( config: ConfigFile, args: argparse.Namespace, extra_args: List[str] -) -> None: +) -> Tuple[Subscription, FileHandlerTransactionLog]: """ Downloads a one-off subscription using the CLI @@ -51,6 +65,10 @@ def _download_subscription_from_cli( Arguments from argparse extra_args Extra arguments from argparse that contain dynamic subscription options + + Returns + ------- + Subscription and its download transaction log """ dl_args_parser = DownloadArgsParser( extra_arguments=extra_args, config_options=config.config_options @@ -69,7 +87,7 @@ def _download_subscription_from_cli( config=config, ) - subscription.download(dry_run=args.dry_run) + return subscription, subscription.download(dry_run=args.dry_run) def _main(): @@ -86,14 +104,22 @@ def _main(): config: ConfigFile = ConfigFile.from_file_path(args.config).initialize() Logger.set_log_level(log_level_name=args.log_level) + transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]] = [] if args.subparser == "sub": - _download_subscriptions_from_yaml_files(config=config, args=args) - logger.info("Subscription download complete!") + transaction_logs = _download_subscriptions_from_yaml_files(config=config, args=args) # One-off download if args.subparser == "dl": - _download_subscription_from_cli(config=config, args=args, extra_args=extra_args) - logger.info("Download complete!") + transaction_logs.append( + _download_subscription_from_cli(config=config, args=args, extra_args=extra_args) + ) + + for subscription, transaction_log in transaction_logs: + logger.info( + "Downloads for %s:\n%s\n", + subscription.name, + transaction_log.to_output_message(subscription.output_directory), + ) # Ran successfully, so we can delete the debug file Logger.cleanup(delete_debug_file=True) diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index 14724c37..c74c85a1 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -56,7 +56,7 @@ class MusicTagsOptions(PluginOptions): class MusicTagsPlugin(Plugin[MusicTagsOptions]): plugin_options_type = MusicTagsOptions - def post_process_entry(self, entry: Entry): + def post_process_entry(self, entry: Entry) -> FileMetadata: """ Tags the entry's audio file using values defined in the metadata options """ diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index f1b9705a..38d8d15b 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -80,7 +80,7 @@ class NfoTagsOptions(PluginOptions): class NfoTagsPlugin(Plugin[NfoTagsOptions]): plugin_options_type = NfoTagsOptions - def post_process_entry(self, entry: Entry): + def post_process_entry(self, entry: Entry) -> None: """ Creates an entry's NFO file using values defined in the metadata options @@ -115,7 +115,6 @@ class NfoTagsPlugin(Plugin[NfoTagsOptions]): with open(nfo_file_path, "wb") as nfo_file: nfo_file.write(xml) - # Archive the nfo's file name - self.save_file(file_name=nfo_file_name, entry=entry) - - return FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") + # Save the nfo file and log its metadata + nfo_metadata = FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") + self.save_file(file_name=nfo_file_name, file_metadata=nfo_metadata, entry=entry) diff --git a/src/ytdl_sub/plugins/output_directory_nfo_tags.py b/src/ytdl_sub/plugins/output_directory_nfo_tags.py index e0aa7294..1d2a5dba 100644 --- a/src/ytdl_sub/plugins/output_directory_nfo_tags.py +++ b/src/ytdl_sub/plugins/output_directory_nfo_tags.py @@ -106,5 +106,5 @@ class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]): with open(nfo_file_path, "wb") as nfo_file: nfo_file.write(xml) - self.save_file(file_name=nfo_file_name) - return FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") + nfo_metadata = FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") + self.save_file(file_name=nfo_file_name, file_metadata=nfo_metadata) diff --git a/src/ytdl_sub/subscriptions/subscription.py b/src/ytdl_sub/subscriptions/subscription.py index 5601674c..6123a164 100644 --- a/src/ytdl_sub/subscriptions/subscription.py +++ b/src/ytdl_sub/subscriptions/subscription.py @@ -267,10 +267,14 @@ class Subscription: ) for entry in downloader.download(): - # TODO: Add entry metadata from the downloader.download function entry_metadata = FileMetadata() + if isinstance(entry, tuple): + entry, entry_metadata = entry + for plugin in plugins: - entry_metadata.extend(plugin.post_process_entry(entry)) + optional_plugin_entry_metadata = plugin.post_process_entry(entry) + if optional_plugin_entry_metadata: + entry_metadata.extend(optional_plugin_entry_metadata) self._copy_entry_files_to_output_directory( entry=entry, entry_metadata=entry_metadata diff --git a/src/ytdl_sub/utils/chapters.py b/src/ytdl_sub/utils/chapters.py index ccb5164f..8bb6ecbc 100644 --- a/src/ytdl_sub/utils/chapters.py +++ b/src/ytdl_sub/utils/chapters.py @@ -1,8 +1,11 @@ import os import re from typing import List +from typing import Optional +from typing import Tuple from ytdl_sub.utils.exceptions import ValidationException +from ytdl_sub.utils.file_handler import FileMetadata class Timestamp: @@ -48,12 +51,7 @@ class Timestamp: return self._timestamp_sec @property - def timestamp_str(self) -> str: - """ - Returns - ------- - The timestamp in 'HH:MM:SS' format - """ + def _hours_minutes_seconds(self) -> Tuple[int, int, int]: seconds = self.timestamp_sec hours = int(seconds / 3600) @@ -62,6 +60,30 @@ class Timestamp: minutes = int(seconds / 60) seconds -= minutes * 60 + return hours, minutes, seconds + + @property + def readable_str(self) -> str: + """ + Returns + ------- + The timestamp in '0:SS' format (min trim). + """ + hours, minutes, seconds = self._hours_minutes_seconds + if hours: + return f"{str(hours)}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}" + if minutes: + return f"{str(minutes)}:{str(seconds).zfill(2)}" + return f"0:{str(seconds).zfill(2)}" + + @property + def standardized_str(self) -> str: + """ + Returns + ------- + The timestamp in 'HH:MM:SS' format + """ + hours, minutes, seconds = self._hours_minutes_seconds return f"{str(hours).zfill(2)}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}" @classmethod @@ -125,6 +147,22 @@ class Chapters: """ return self.timestamps[0].timestamp_sec == 0 + def to_file_metadata(self, title: Optional[str] = None) -> FileMetadata: + """ + Parameters + ---------- + title + Optional title + + Returns + ------- + Chapter metadata in the format of { readable_timestamp_str: title } + """ + return FileMetadata.from_dict( + value_dict={ts.readable_str: title for ts, title in zip(self.timestamps, self.titles)}, + title=title, + ) + @classmethod def from_file(cls, chapters_file_path: str) -> "Chapters": """ diff --git a/src/ytdl_sub/utils/file_handler.py b/src/ytdl_sub/utils/file_handler.py index 2ef61724..0f749b4d 100644 --- a/src/ytdl_sub/utils/file_handler.py +++ b/src/ytdl_sub/utils/file_handler.py @@ -14,8 +14,12 @@ class FileMetadata: Stores pretty-printed information about a file. Each line in the metadata represents a newline """ - def __init__(self, metadata: Optional[List[str]] = None): - self.metadata: List[str] = metadata if metadata else [] + def __init__(self, metadata: Optional[Union[str, List[str]]] = None): + self.metadata = [] + if isinstance(metadata, str): + self.metadata = [metadata] + elif isinstance(metadata, list): + self.metadata = metadata def append(self, line: str) -> "FileMetadata": """ @@ -54,11 +58,19 @@ class FileMetadata: def _recursive_add_dict_lines(rdict: Dict, indent: int): for key, value in sorted(rdict.items()): - if isinstance(value, Dict): - _recursive_add_dict_lines(rdict=value, indent=indent + 2) - _indent = " " * indent - lines.append(f"{_indent}{key}: {value}") + if isinstance(value, Dict): + lines.append(f"{_indent}{key}:") + _recursive_add_dict_lines(rdict=value, indent=indent + 2) + else: + value = str(value) + # If there are newlines in the value, print them indented + if "\n" in value: + lines.append(f"{_indent}{key}:") + for value_line in value.split("\n"): + lines.append(f" {_indent}{value_line.strip()}") + else: + lines.append(f"{_indent}{key}: {value}") _recursive_add_dict_lines(rdict=value_dict, indent=2) return cls(metadata=lines) @@ -103,6 +115,46 @@ class FileHandlerTransactionLog: self.files_removed.add(file_name) return self + def to_output_message(self, output_directory: str) -> str: + """ + Parameters + ---------- + output_directory + Path to the output directory. Included in the output message + + Returns + ------- + The output message to show users what was recorded in the transaction log + """ + lines: List[str] = [] + + def _indent_metadata_line(line: str) -> str: + # Do not indent empty lines + rstrip_line = line.rstrip() + return f" {rstrip_line}" if rstrip_line else "" + + if self.files_created: + created_line = f"Files created in '{output_directory}'" + created_line_dash = "-" * 40 + lines.extend([created_line, created_line_dash]) + for file_path, file_metadata in sorted(self.files_created.items()): + lines.append(file_path) + if file_metadata: + lines.extend([_indent_metadata_line(line) for line in file_metadata.metadata]) + + if self.files_removed: + # Add a blank line to separate created/removed files + if self.files_created: + lines.append("") + + removed_line = f"Files removed from '{output_directory}'" + removed_line_dash = "-" * 40 + lines.extend([removed_line, removed_line_dash]) + for file_path in sorted(self.files_removed): + lines.append(file_path) + + return "\n".join(lines) + class FileHandler: """ diff --git a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py index cc2ca39d..43eb4346 100644 --- a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +++ b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py @@ -618,7 +618,11 @@ class DownloadArchiver: return self.__enhanced_download_archive.is_dry_run def save_file( - self, file_name: str, output_file_name: Optional[str] = None, entry: Optional[Entry] = None + self, + file_name: str, + file_metadata: Optional[FileMetadata] = None, + output_file_name: Optional[str] = None, + entry: Optional[Entry] = None, ) -> None: """ Saves a file in the working directory to the output directory. @@ -627,6 +631,8 @@ class DownloadArchiver: ---------- file_name Name of the file relative to the working directory + file_metadata + Optional. Metadata to record to the transaction log for this file output_file_name Optional. Final name of the file in the output directory (does not include output directory path). If None, use the same working_directory file_name @@ -634,5 +640,8 @@ class DownloadArchiver: Optional. Entry that the file belongs to """ self.__enhanced_download_archive.save_file_to_output_directory( - file_name=file_name, output_file_name=output_file_name, entry=entry + file_name=file_name, + file_metadata=file_metadata, + output_file_name=output_file_name, + entry=entry, ) diff --git a/tests/e2e/expected_download.py b/tests/e2e/expected_download.py index ffc5dde3..d327444c 100644 --- a/tests/e2e/expected_download.py +++ b/tests/e2e/expected_download.py @@ -1,15 +1,22 @@ import hashlib import os.path +from dataclasses import dataclass from pathlib import Path -from typing import Dict from typing import List from typing import Optional from typing import Union -from ytdl_sub.utils.file_handler import FileHandlerTransactionLog +from ytdl_sub.utils.file_handler import FileMetadata -class ExpectedDownload: +@dataclass +class ExpectedDownloadFile: + path: Path + md5: Optional[Union[str, List[str]]] = None + metadata: Optional[FileMetadata] = None + + +class ExpectedDownloads: """ To test ytdl-sub downloads work, we compare each downloaded file's md5 hash to an expected md5 hash defined in this class. @@ -18,12 +25,15 @@ class ExpectedDownload: try all the hashes (used in case the GitHub env produces different deterministic value). """ - def __init__(self, expected_md5_file_hashes: Dict[Path, Optional[Union[str, List[str]]]]): - self.expected_md5_file_hashes = expected_md5_file_hashes + def __init__(self, expected_downloads: List[ExpectedDownloadFile]): + self.expected_downloads = expected_downloads @property def file_count(self) -> int: - return len(self.expected_md5_file_hashes) + return len(self.expected_downloads) + + def contains(self, relative_path: Path) -> bool: + return sum(relative_path == download.path for download in self.expected_downloads) == 1 def assert_files_exist(self, relative_directory: Path): """ @@ -35,38 +45,29 @@ class ExpectedDownload: num_files += 1 relative_path = Path(*path.parts[3:]) - assert ( - relative_path in self.expected_md5_file_hashes + assert self.contains( + relative_path ), f"File {relative_path} was created but not expected" assert num_files == self.file_count, "Mismatch in number of created files" - for relative_path, expected_md5_hash in self.expected_md5_file_hashes.items(): - full_path = Path(relative_directory) / relative_path + for expected_download in self.expected_downloads: + full_path = Path(relative_directory) / expected_download.path assert os.path.isfile( full_path - ), f"Expected {str(relative_path)} to be a file but it is not" + ), f"Expected {str(expected_download.path)} to be a file but it is not" - if expected_md5_hash is None: + if expected_download.md5 is None: continue with open(full_path, "rb") as file: md5_hash = hashlib.md5(file.read()).hexdigest() - if isinstance(expected_md5_hash, str): - expected_md5_hash = [expected_md5_hash] + expected_md5_hash = expected_download.md5 + if isinstance(expected_download.md5, str): + expected_md5_hash = [expected_download.md5] assert md5_hash in expected_md5_hash, ( - f"MD5 hash for {str(relative_path)} does not match: " + f"MD5 hash for {str(expected_download.path)} does not match: " f"{md5_hash} != {expected_md5_hash}" ) - - def assert_dry_run_files_logged(self, transaction_log: FileHandlerTransactionLog): - assert ( - len(transaction_log.files_created) == self.file_count - ), "Mismatch in number of created files" - - for relative_path in self.expected_md5_file_hashes.keys(): - assert ( - str(relative_path) in transaction_log.files_created - ), f"Expected {str(relative_path)} to be a file but it is not" diff --git a/tests/e2e/expected_transaction_log.py b/tests/e2e/expected_transaction_log.py new file mode 100644 index 00000000..12d23187 --- /dev/null +++ b/tests/e2e/expected_transaction_log.py @@ -0,0 +1,55 @@ +from pathlib import Path +from typing import List + +from ytdl_sub.utils.file_handler import FileHandlerTransactionLog + +_TRANSACTION_LOG_SUMMARY_PATH = Path("tests/e2e/resources/transaction_log_summaries") + + +def assert_transaction_log_matches( + output_directory: str, + transaction_log: FileHandlerTransactionLog, + transaction_log_summary_file_name: str, + regenerate_transaction_log: bool = False, +): + """ + Parameters + ---------- + output_directory + Output directory the files are saved to + transaction_log + Transaction log to check + transaction_log_summary_file_name + Name if the transaction log summary to compare. + Lives in tests/e2e/resources/transaction_log_summaries + regenerate_transaction_log + USE WITH CAUTION - MANUALLY INSPECT CHANGED FILES TO ENSURE THEY LOOK GOOD! + Updates the file with the input transaction log. Should only be used to update + tests after an expected change is made. + """ + transaction_log_path = _TRANSACTION_LOG_SUMMARY_PATH / transaction_log_summary_file_name + summary = transaction_log.to_output_message(output_directory=output_directory) + + # Write the expected summary file if regenerate is True + if regenerate_transaction_log: + with open(transaction_log_path, "w", encoding="utf-8") as summary_file: + summary_file.write( + transaction_log.to_output_message(output_directory="{output_directory}") + ) + + # Read the expected summary file + with open(transaction_log_path, "r", encoding="utf-8") as summary_file: + expected_summary = summary_file.read().format(output_directory=output_directory) + + # Split, ensure there are the same number of new lines + summary_lines: List[str] = summary.split("\n") + expected_summary_lines: List[str] = expected_summary.split("\n") + assert len(summary_lines) == len( + expected_summary_lines + ), f"Summary number of lines differ: {len(summary_lines) != len(expected_summary_lines)}" + + # Ensure each line equals + for idx in range(len(summary_lines)): + line = summary_lines[idx] + expected_line = expected_summary_lines[idx] + assert line == expected_line, f"Summary line {idx} differs: '{line}' != '{expected_line}'" diff --git a/tests/e2e/resources/transaction_log_summaries/soundcloud/test_soundcloud_discography.txt b/tests/e2e/resources/transaction_log_summaries/soundcloud/test_soundcloud_discography.txt new file mode 100644 index 00000000..27e66c04 --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/soundcloud/test_soundcloud_discography.txt @@ -0,0 +1,123 @@ +Files created in '{output_directory}' +---------------------------------------- +.ytdl-sub-jb-download-archive.json +j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3 + Music Tags: + album: Baby Santana's Dorian Groove + albumartist: j_b + artist: j_b + genre: Unset + title: Baby Santana's Dorian Groove + track: 1 + year: 2021 +j_b/[2021] Baby Santana's Dorian Groove/folder.jpg +j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3 + Music Tags: + album: Purple Clouds + albumartist: j_b + artist: j_b + genre: Unset + title: Purple Clouds + track: 1 + year: 2021 +j_b/[2021] Purple Clouds/folder.jpg +j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20160426 184214 + track: 1 + year: 2022 +j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20160502 123150 + track: 2 + year: 2022 +j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20160504 143832 + track: 3 + year: 2022 +j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20160601 221234 + track: 4 + year: 2022 +j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20160601 222440 + track: 5 + year: 2022 +j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20170604 190236 + track: 6 + year: 2022 +j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20170612 193646 + track: 7 + year: 2022 +j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: 20170628 215206 + track: 8 + year: 2022 +j_b/[2022] Acoustic Treats/09 - Finding Home.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: Finding Home + track: 9 + year: 2022 +j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: Shallow Water WIP + track: 10 + year: 2022 +j_b/[2022] Acoustic Treats/11 - Untold History.mp3 + Music Tags: + album: Acoustic Treats + albumartist: j_b + artist: j_b + genre: Unset + title: Untold History + track: 11 + year: 2022 +j_b/[2022] Acoustic Treats/folder.jpg \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_full.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_full.txt new file mode 100644 index 00000000..303e8874 --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_full.txt @@ -0,0 +1,273 @@ +Files created in '{output_directory}' +---------------------------------------- +.ytdl-sub-pz-download-archive.json +Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1-thumb.jpg +Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.mp4 +Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.nfo + NFO tags: + episodedetails: + aired: 2010-08-13 + episode: 813 + plot: + PLEASE WATCH IN HD! + + This is the castle in the mod "Falcor" which we are currently working on. + You can find some more information about the mod @ http://www.oblivionfalcormod.webs.com/ + + This is the first time I've really used the editor, and the beginning of our first mod. Hope you enjoy. + season: 2010 + title: Oblivion Mod "Falcor" p.1 + year: 2010 +Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2-thumb.jpg +Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.mp4 +Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.nfo + NFO tags: + episodedetails: + aired: 2010-12-02 + episode: 1202 + plot: + PLEASE WATCH IN HD. + + Download Link: + http://www.tesnexus.com/downloads/file.php?id=36306 + + Please keep in mind that the mod is not complete, so you'll see some blank areas during the video. I haven't made LOD (Level of Distance) yet either, so areas far away will not appear. + season: 2010 + title: Oblivion Mod "Falcor" p.2 + year: 2010 +Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg +Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].mp4 +Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].nfo + NFO tags: + episodedetails: + aired: 2011-02-01 + episode: 201 + plot: + 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 + title: Jesse's Minecraft Server [Trailer - Feb.1] + year: 2011 +Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg +Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].mp4 +Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].nfo + NFO tags: + episodedetails: + aired: 2011-02-27 + episode: 227 + plot: + 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 + title: Jesse's Minecraft Server [Trailer - Feb.27] + year: 2011 +Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg +Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].mp4 +Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].nfo + NFO tags: + episodedetails: + aired: 2011-03-21 + episode: 321 + plot: + 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 + title: Jesse's Minecraft Server [Trailer - Mar.21] + year: 2011 +Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net)-thumb.jpg +Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).mp4 +Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).nfo + NFO tags: + episodedetails: + aired: 2011-05-29 + episode: 529 + plot: + Currently in alpha testing. + http://www.projectzombie.net/ + + Credits: + -Master_Queef (Jesse) + -Cheeseducksg + -Vsalaka + -Edwingsd + -All Members on my Roleplay Server + -All Mercenaries in Project Zombie + + Song: + Johnny Cash - God's Gonna Cut You Down + + Texture Pack: + http://www.minecraftforum.net/topic/26727-johnsmith-texture-pack-v73-32x32-16x-with-customizer/ + season: 2011 + title: Project Zombie |Official Trailer| (IP: mc.projectzombie.beastnode.net) + year: 2011 +Season 2011/s2011.e0630 - Project Zombie _Fin-thumb.jpg +Season 2011/s2011.e0630 - Project Zombie _Fin.mp4 +Season 2011/s2011.e0630 - Project Zombie _Fin.nfo + NFO tags: + episodedetails: + aired: 2011-06-30 + episode: 630 + plot: + Twas a good run while it lasted. If you're still interested in playing PZ, you can join my Roleplay Server, yes, Project Zombie is completely imported onto that one: + http://www.jesseminecraft.webs.com/ + season: 2011 + title: Project Zombie |Fin| + year: 2011 +Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC]-thumb.jpg +Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].mp4 +Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].nfo + NFO tags: + episodedetails: + aired: 2011-11-21 + episode: 1121 + plot: + Mods I have used: + http://skyrimnexus.com/downloads/file.php?id=329 + http://skyrimnexus.com/downloads/file.php?id=607 + http://skyrimnexus.com/downloads/file.php?id=711 + http://skyrimnexus.com/downloads/file.php?id=302 + http://skyrimnexus.com/downloads/file.php?id=141 + http://skyrimnexus.com/downloads/file.php?id=131 + http://skyrimnexus.com/downloads/file.php?id=85 + http://skyrimnexus.com/downloads/file.php?id=191 + season: 2011 + title: Skyrim 'Ultra HD w/Mods' [PC] + year: 2011 +Season 2012/s2012.e0123 - Project Zombie _Map Trailer-thumb.jpg +Season 2012/s2012.e0123 - Project Zombie _Map Trailer.mp4 +Season 2012/s2012.e0123 - Project Zombie _Map Trailer.nfo + NFO tags: + episodedetails: + aired: 2012-01-23 + episode: 123 + plot: + Faction Server IP: 72.9.157.88:25652 + Project Zombie IP: mc.projectzombie.beastnode.net + Project Zombie Map: http://tinyurl.com/projectzombie1 + Website: http://jesseminecraft.webs.com/ + Map Download: http://www.planetminecraft.com/project/project-zombie-511865/ + + A few little mistakes, but awh well, the dynamic shadows look boss. + + And the song is Comfortably Numb by Pink Floyd. If you didn't know that after the first 10 seconds of listening to it then you may need some help. + season: 2012 + title: Project Zombie |Map Trailer| + year: 2012 +Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer-thumb.jpg +Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.mp4 +Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.nfo + NFO tags: + episodedetails: + aired: 2013-07-19 + episode: 719 + plot: + IP: 172.245.24.118 + http://www.projectzombie.net/ + + With almost a year of downtime, Project Zombie rises from its ashes. Rewind back to May 2011 and immerse yourself in the original Project Zombie with expansions. Open July 20th, 2013. + season: 2013 + title: Project Zombie Rewind |Trailer| + year: 2013 +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4 +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo + NFO tags: + episodedetails: + aired: 2018-10-29 + episode: 1029 + plot: + See the full trailer here: https://youtu.be/LN2e6idGluI + Discord: https://discord.gg/BQN7SSe + Website: https://www.projectzombie.net/ + + It has been about six years since the glory days of the original RP server. Hope to see many familiar faces in the revival of our great server. + + Credits: + ALISON - Space Echo + https://www.youtube.com/watch?v=lPldcjlv3_Y + season: 2018 + title: Jesse's Minecraft Server | Teaser Trailer + year: 2018 +Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg +Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4 +Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo + NFO tags: + episodedetails: + aired: 2018-11-02 + episode: 1102 + plot: + IP: mc.jesse.id + Live Map: http://mc.jesse.id:8123 + Discord: https://discord.gg/BQN7SSe + Website: https://www.projectzombie.net/ + + It has been about six years since the glory days of the original RP server. Hope to see many familiar faces in the revival of our great server. + + The MC version is 1.13.x AND/OR 1.12.x + + Credits: + ALISON - Space Echo + https://www.youtube.com/watch?v=lPldcjlv3_Y + season: 2018 + title: Jesse's Minecraft Server | IP mc.jesse.id + year: 2018 +fanart.jpg +poster.jpg +tvshow.nfo + NFO tags: + tvshow: + title: Project / Zombie \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_no_additional_downloads.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_no_additional_downloads.txt new file mode 100644 index 00000000..7275663d --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_no_additional_downloads.txt @@ -0,0 +1,9 @@ +Files created in '{output_directory}' +---------------------------------------- +.ytdl-sub-pz-download-archive.json +fanart.jpg +poster.jpg +tvshow.nfo + NFO tags: + tvshow: + title: Project / Zombie \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_recent.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_recent.txt new file mode 100644 index 00000000..f8a4f79c --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_recent.txt @@ -0,0 +1,52 @@ +Files created in '{output_directory}' +---------------------------------------- +.ytdl-sub-pz-download-archive.json +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4 +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo + NFO tags: + episodedetails: + aired: 2018-10-29 + episode: 1029 + plot: + See the full trailer here: https://youtu.be/LN2e6idGluI + Discord: https://discord.gg/BQN7SSe + Website: https://www.projectzombie.net/ + + It has been about six years since the glory days of the original RP server. Hope to see many familiar faces in the revival of our great server. + + Credits: + ALISON - Space Echo + https://www.youtube.com/watch?v=lPldcjlv3_Y + season: 2018 + title: Jesse's Minecraft Server | Teaser Trailer + year: 2018 +Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg +Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4 +Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo + NFO tags: + episodedetails: + aired: 2018-11-02 + episode: 1102 + plot: + IP: mc.jesse.id + Live Map: http://mc.jesse.id:8123 + Discord: https://discord.gg/BQN7SSe + Website: https://www.projectzombie.net/ + + It has been about six years since the glory days of the original RP server. Hope to see many familiar faces in the revival of our great server. + + The MC version is 1.13.x AND/OR 1.12.x + + Credits: + ALISON - Space Echo + https://www.youtube.com/watch?v=lPldcjlv3_Y + season: 2018 + title: Jesse's Minecraft Server | IP mc.jesse.id + year: 2018 +fanart.jpg +poster.jpg +tvshow.nfo + NFO tags: + tvshow: + title: Project / Zombie \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_rolling_recent.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_rolling_recent.txt new file mode 100644 index 00000000..2e4d09e9 --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_channel_rolling_recent.txt @@ -0,0 +1,15 @@ +Files created in '{output_directory}' +---------------------------------------- +.ytdl-sub-pz-download-archive.json +fanart.jpg +poster.jpg +tvshow.nfo + NFO tags: + tvshow: + title: Project / Zombie + +Files removed from '{output_directory}' +---------------------------------------- +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4 +Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_merge_playlist.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_merge_playlist.txt new file mode 100644 index 00000000..39df4aac --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_merge_playlist.txt @@ -0,0 +1,15 @@ +Files created in '{output_directory}' +---------------------------------------- +JMC - Jesse's Minecraft Server-thumb.jpg +JMC - Jesse's Minecraft Server.mkv + Timestamps of playlist videos in the merged file: + 0:00: Jesse's Minecraft Server [Trailer - Feb.1] + 5:23: Jesse's Minecraft Server [Trailer - Feb.27] + 9:23: Jesse's Minecraft Server [Trailer - Mar.21] +JMC - Jesse's Minecraft Server.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + title: Jesse's Minecraft Server + year: 2011 \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_playlist.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_playlist.txt new file mode 100644 index 00000000..3d31bea7 --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_playlist.txt @@ -0,0 +1,30 @@ +Files created in '{output_directory}' +---------------------------------------- +.ytdl-sub-jmc-download-archive.json +JMC - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg +JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4 +JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + title: Jesse's Minecraft Server [Trailer - Feb.1] + year: 2011 +JMC - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg +JMC - Jesse's Minecraft Server [Trailer - Feb.27].mp4 +JMC - Jesse's Minecraft Server [Trailer - Feb.27].nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + title: Jesse's Minecraft Server [Trailer - Feb.27] + year: 2011 +JMC - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg +JMC - Jesse's Minecraft Server [Trailer - Mar.21].mp4 +JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + title: Jesse's Minecraft Server [Trailer - Mar.21] + year: 2011 \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_split_video.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_split_video.txt new file mode 100644 index 00000000..28ea9910 --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_split_video.txt @@ -0,0 +1,62 @@ +Files created in '{output_directory}' +---------------------------------------- +Project Zombie - 1-6.Intro.mp4 + 0:00 - 0:10 +Project Zombie - 2-6.Part 1.mp4 + 0:10 - 0:20 +Project Zombie - 3-6.Part 2.mp4 + 0:20 - 0:30 +Project Zombie - 4-6.Part 3.mp4 + 0:30 - 0:40 +Project Zombie - 5-6.Part 4.mp4 + 0:40 - 1:01 +Project Zombie - 6-6.Part 5.mp4 + 1:01 - 1:28 +Project Zombie - Intro-thumb.jpg +Project Zombie - Intro.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Project Zombie + title: Intro + year: 2010 +Project Zombie - Part 1-thumb.jpg +Project Zombie - Part 1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Project Zombie + title: Part 1 + year: 2010 +Project Zombie - Part 2-thumb.jpg +Project Zombie - Part 2.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Project Zombie + title: Part 2 + year: 2010 +Project Zombie - Part 3-thumb.jpg +Project Zombie - Part 3.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Project Zombie + title: Part 3 + year: 2010 +Project Zombie - Part 4-thumb.jpg +Project Zombie - Part 4.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Project Zombie + title: Part 4 + year: 2010 +Project Zombie - Part 5-thumb.jpg +Project Zombie - Part 5.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: Project Zombie + title: Part 5 + year: 2010 \ No newline at end of file diff --git a/tests/e2e/resources/transaction_log_summaries/youtube/test_video.txt b/tests/e2e/resources/transaction_log_summaries/youtube/test_video.txt new file mode 100644 index 00000000..0db01fd8 --- /dev/null +++ b/tests/e2e/resources/transaction_log_summaries/youtube/test_video.txt @@ -0,0 +1,11 @@ +Files created in '{output_directory}' +---------------------------------------- +JMC - Oblivion Mod 'Falcor' p.1-thumb.jpg +JMC - Oblivion Mod 'Falcor' p.1.mp4 +JMC - Oblivion Mod 'Falcor' p.1.nfo + NFO tags: + musicvideo: + album: Music Videos + artist: JMC + title: Oblivion Mod "Falcor" p.1 + year: 2010 \ No newline at end of file diff --git a/tests/e2e/soundcloud/test_soundcloud_discography.py b/tests/e2e/soundcloud/test_soundcloud_discography.py index 033e5e4e..eac92645 100644 --- a/tests/e2e/soundcloud/test_soundcloud_discography.py +++ b/tests/e2e/soundcloud/test_soundcloud_discography.py @@ -1,7 +1,9 @@ from pathlib import Path import pytest -from e2e.expected_download import ExpectedDownload +from e2e.expected_download import ExpectedDownloadFile +from e2e.expected_download import ExpectedDownloads +from e2e.expected_transaction_log import assert_transaction_log_matches from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.preset import Preset @@ -56,34 +58,32 @@ def discography_subscription(config, subscription_name, subscription_dict): def expected_discography_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ + return ExpectedDownloads( + expected_downloads=[ # Download mapping - Path(".ytdl-sub-jb-download-archive.json"): "1a99156e9ece62539fb2608416a07200", + ExpectedDownloadFile(path=Path(".ytdl-sub-jb-download-archive.json"), md5="1a99156e9ece62539fb2608416a07200"), # Entry files (singles) - Path("j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3"): "bffbd558e12c6a9e029dc136a88342c4", - Path("j_b/[2021] Baby Santana's Dorian Groove/folder.jpg"): "967892be44b8c47e1be73f055a7c6f08", + ExpectedDownloadFile(path=Path("j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3"), md5="bffbd558e12c6a9e029dc136a88342c4"), + ExpectedDownloadFile(path=Path("j_b/[2021] Baby Santana's Dorian Groove/folder.jpg"), md5="967892be44b8c47e1be73f055a7c6f08"), - Path("j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3"): "038db58aebe2ba875b733932b42a94d6", - Path("j_b/[2021] Purple Clouds/folder.jpg"): "967892be44b8c47e1be73f055a7c6f08", + ExpectedDownloadFile(path=Path("j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3"), md5="038db58aebe2ba875b733932b42a94d6"), + ExpectedDownloadFile(path=Path("j_b/[2021] Purple Clouds/folder.jpg"), md5="967892be44b8c47e1be73f055a7c6f08"), # Entry files (albums) - Path("j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3"): "e145f0a2f6012768280c38655ca58065", - Path("j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3"): "60c8b8817a197a13e4bb90903af612c5", - Path("j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3"): "8265b7e4f79878af877bc6ecd9757efe", - Path("j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3"): "accf46b76891d2954b893d0f91d82816", - Path("j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3"): "e1f584f523336160d5c1104a61de77f3", - Path("j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3"): "f6885b25901177f0357649afe97328cc", - Path("j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3"): "fa057f221cbe4cf2442cd2fdb960743e", - Path("j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3"): "7794ae812c64580e2ac8fc457d5cc85f", - Path("j_b/[2022] Acoustic Treats/09 - Finding Home.mp3"): "adbf02eddb2090c008eb497d13ff84b9", - Path("j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3"): "65bb10c84366c71498161734f953e93d", - Path("j_b/[2022] Acoustic Treats/11 - Untold History.mp3"): "6904b2918e5dc38d9a9f72d967eb74bf", - Path("j_b/[2022] Acoustic Treats/folder.jpg"): "967892be44b8c47e1be73f055a7c6f08", - - - } + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3"), md5="e145f0a2f6012768280c38655ca58065"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3"), md5="60c8b8817a197a13e4bb90903af612c5"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3"), md5="8265b7e4f79878af877bc6ecd9757efe"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3"), md5="accf46b76891d2954b893d0f91d82816"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3"), md5="e1f584f523336160d5c1104a61de77f3"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3"), md5="f6885b25901177f0357649afe97328cc"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3"), md5="fa057f221cbe4cf2442cd2fdb960743e"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3"), md5="7794ae812c64580e2ac8fc457d5cc85f"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/09 - Finding Home.mp3"), md5="adbf02eddb2090c008eb497d13ff84b9"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3"), md5="65bb10c84366c71498161734f953e93d"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/11 - Untold History.mp3"), md5="6904b2918e5dc38d9a9f72d967eb74bf"), + ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/folder.jpg"), md5="967892be44b8c47e1be73f055a7c6f08"), + ] ) # fmt: on @@ -94,14 +94,15 @@ class TestSoundcloudDiscography: expected md5 file hashes. """ + @pytest.mark.parametrize("dry_run", [True, False]) def test_discography_download( - self, discography_subscription, expected_discography_download, output_directory + self, discography_subscription, expected_discography_download, output_directory, dry_run ): - discography_subscription.download() - expected_discography_download.assert_files_exist(relative_directory=output_directory) - - def test_discography_dry_run( - self, discography_subscription, expected_discography_download, output_directory - ): - transaction_log = discography_subscription.download(dry_run=True) - expected_discography_download.assert_dry_run_files_logged(transaction_log=transaction_log) + transaction_log = discography_subscription.download(dry_run=dry_run) + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="soundcloud/test_soundcloud_discography.txt", + ) + if not dry_run: + expected_discography_download.assert_files_exist(relative_directory=output_directory) diff --git a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py index bf3184fa..61d160c3 100644 --- a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py +++ b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py @@ -3,7 +3,9 @@ from pathlib import Path import mergedeep import pytest from conftest import assert_debug_log -from e2e.expected_download import ExpectedDownload +from e2e.expected_download import ExpectedDownloadFile +from e2e.expected_download import ExpectedDownloads +from e2e.expected_transaction_log import assert_transaction_log_matches import ytdl_sub.downloaders.downloader from ytdl_sub.config.config_file import ConfigFile @@ -65,65 +67,65 @@ def full_channel_subscription(config, subscription_name, subscription_dict): def expected_full_channel_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ + return ExpectedDownloads( + expected_downloads=[ # Download mapping - Path(".ytdl-sub-pz-download-archive.json"): "dd3c6236a107a665b884f701b8d14d4d", + ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="dd3c6236a107a665b884f701b8d14d4d"), # Output directory files - Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", - Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", - Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", + ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"), + ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"), + ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"), # Entry files - Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1-thumb.jpg"): "fb95b510681676e81c321171fc23143e", - Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.mp4"): "931a705864c57d21d6fedebed4af6bbc", - Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.nfo"): "67d8d71d048039080acbba3bce4febaa", + ExpectedDownloadFile(path=Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1-thumb.jpg"), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.mp4"), md5="931a705864c57d21d6fedebed4af6bbc"), + ExpectedDownloadFile(path=Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.nfo"), md5="67d8d71d048039080acbba3bce4febaa"), - Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2-thumb.jpg"): "8b32ee9c037fa669e444a0ac181525a1", - Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.mp4"): "d3469b4dca7139cb3dbc38712b6796bf", - Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.nfo"): "d81f49cedbd7edaee987521e89b37904", + ExpectedDownloadFile(path=Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2-thumb.jpg"), md5="8b32ee9c037fa669e444a0ac181525a1"), + ExpectedDownloadFile(path=Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.mp4"), md5="d3469b4dca7139cb3dbc38712b6796bf"), + ExpectedDownloadFile(path=Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.nfo"), md5="d81f49cedbd7edaee987521e89b37904"), - Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"): "b232d253df621aa770b780c1301d364d", - Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].mp4"): "e66287b9832277b6a4d1554e29d9fdcc", - Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].nfo"): "f7c0de89038f8c491bded8a3968720a2", + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"), md5="b232d253df621aa770b780c1301d364d"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].mp4"), md5="e66287b9832277b6a4d1554e29d9fdcc"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].nfo"), md5="f7c0de89038f8c491bded8a3968720a2"), - Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"): "d17c379ea8b362f5b97c6b213b0342cb", - Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].mp4"): "04ab5cb3cc12325d0c96a7cd04a8b91d", - Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].nfo"): "ee1eda78fa0980bc703e602b5012dd1f", + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"), md5="d17c379ea8b362f5b97c6b213b0342cb"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].mp4"), md5="04ab5cb3cc12325d0c96a7cd04a8b91d"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].nfo"), md5="ee1eda78fa0980bc703e602b5012dd1f"), - Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"): "e7830aa8a64b0cde65ba3f7e5fc56530", - Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].mp4"): "025de6099a5c98e6397153c7a62d517d", - Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].nfo"): "61eb6369430da0ab6134d78829a7621b", + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"), md5="e7830aa8a64b0cde65ba3f7e5fc56530"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].mp4"), md5="025de6099a5c98e6397153c7a62d517d"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].nfo"), md5="61eb6369430da0ab6134d78829a7621b"), - Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net)-thumb.jpg"): "c956192a379b3661595c9920972d4819", - Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).mp4"): "3d9c19835b03355d6fd5d00cd59dbe5b", - Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).nfo"): "60f72b99f5c69f9e03a071a12160928f", + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net)-thumb.jpg"), md5="c956192a379b3661595c9920972d4819"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).mp4"), md5="3d9c19835b03355d6fd5d00cd59dbe5b"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).nfo"), md5="60f72b99f5c69f9e03a071a12160928f"), - Path("Season 2011/s2011.e0630 - Project Zombie _Fin-thumb.jpg"): "00ed383591779ffe98291de60f198fe9", - Path("Season 2011/s2011.e0630 - Project Zombie _Fin.mp4"): "4971cb2d4fa29460361031f3fa8e1ea9", - Path("Season 2011/s2011.e0630 - Project Zombie _Fin.nfo"): "a7b5d9e57d20852f5daf360a1373bb7a", + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0630 - Project Zombie _Fin-thumb.jpg"), md5="00ed383591779ffe98291de60f198fe9"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0630 - Project Zombie _Fin.mp4"), md5="4971cb2d4fa29460361031f3fa8e1ea9"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e0630 - Project Zombie _Fin.nfo"), md5="a7b5d9e57d20852f5daf360a1373bb7a"), - Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC]-thumb.jpg"): "1718599d5189c65f7d8cf6acfa5ea851", - Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].mp4"): "55e9b0add08c48c9c66105da0def2426", - Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].nfo"): "fe60e2b6b564f9316b6c7c183e1cf300", + ExpectedDownloadFile(path=Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC]-thumb.jpg"), md5="1718599d5189c65f7d8cf6acfa5ea851"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].mp4"), md5="55e9b0add08c48c9c66105da0def2426"), + ExpectedDownloadFile(path=Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].nfo"), md5="fe60e2b6b564f9316b6c7c183e1cf300"), - Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer-thumb.jpg"): "54ebe9df801b278fdd17b21afa8373a6", - Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.mp4"): "65e4ce53ed5ec4139995469f99477a50", - Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.nfo"): "c8900adcca83c473c79a4afbc7ad2de1", + ExpectedDownloadFile(path=Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer-thumb.jpg"), md5="54ebe9df801b278fdd17b21afa8373a6"), + ExpectedDownloadFile(path=Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.mp4"), md5="65e4ce53ed5ec4139995469f99477a50"), + ExpectedDownloadFile(path=Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.nfo"), md5="c8900adcca83c473c79a4afbc7ad2de1"), - Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer-thumb.jpg"): "e29d49433175de8a761af35c5307791f", - Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.mp4"): "18620a8257a686beda65e54add4d4cd1", - Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.nfo"): "1c993c41d4308a6049333154d0adee16", + ExpectedDownloadFile(path=Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer-thumb.jpg"), md5="e29d49433175de8a761af35c5307791f"), + ExpectedDownloadFile(path=Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.mp4"), md5="18620a8257a686beda65e54add4d4cd1"), + ExpectedDownloadFile(path=Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.nfo"), md5="1c993c41d4308a6049333154d0adee16"), - Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"): "6f8f5e1e031ec2a04b0a4906c04a19ee", - Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"): "82f6ee7253e1dbb83ae7215af08ffacc", - Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo"): "cc7886aae3af6b7b0facd82f95390242", + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"), md5="6f8f5e1e031ec2a04b0a4906c04a19ee"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"), md5="82f6ee7253e1dbb83ae7215af08ffacc"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo"), md5="cc7886aae3af6b7b0facd82f95390242"), - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"): "49cc64b25314155c1b8ab0361ac0c34f", - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"): "e733b4cc385b953b08c8eb0f47e03c1e", - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo"): "2b3ccb3f1ef81ee49fe1afb88f275a09", - } + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"), md5="49cc64b25314155c1b8ab0361ac0c34f"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"), md5="e733b4cc385b953b08c8eb0f47e03c1e"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo"), md5="2b3ccb3f1ef81ee49fe1afb88f275a09"), + ] ) # fmt: on @@ -161,25 +163,25 @@ def recent_channel_subscription(config, subscription_name, recent_channel_subscr def expected_recent_channel_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ + return ExpectedDownloads( + expected_downloads=[ # Download mapping - Path(".ytdl-sub-pz-download-archive.json"): "91534d1c5921d121aa35d7a197ba1940", + ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="91534d1c5921d121aa35d7a197ba1940"), # Output directory files - Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", - Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", - Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", + ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"), + ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"), + ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"), # Recent Entry files - Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"): "6f8f5e1e031ec2a04b0a4906c04a19ee", - Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"): "82f6ee7253e1dbb83ae7215af08ffacc", - Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo"): "cc7886aae3af6b7b0facd82f95390242", + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"), md5="6f8f5e1e031ec2a04b0a4906c04a19ee"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"), md5="82f6ee7253e1dbb83ae7215af08ffacc"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.nfo"), md5="cc7886aae3af6b7b0facd82f95390242"), - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"): "49cc64b25314155c1b8ab0361ac0c34f", - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"): "e733b4cc385b953b08c8eb0f47e03c1e", - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo"): "2b3ccb3f1ef81ee49fe1afb88f275a09", - } + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"), md5="49cc64b25314155c1b8ab0361ac0c34f"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"), md5="e733b4cc385b953b08c8eb0f47e03c1e"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo"), md5="2b3ccb3f1ef81ee49fe1afb88f275a09"), + ] ) # fmt: on @@ -219,16 +221,16 @@ def recent_channel_no_vids_in_range_subscription( def expected_recent_channel_no_vids_in_range_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ + return ExpectedDownloads( + expected_downloads=[ # Download mapping - Path(".ytdl-sub-pz-download-archive.json"): "99914b932bd37a50b983c5e7c90ae93b", + ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="99914b932bd37a50b983c5e7c90ae93b"), # Output directory files - Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", - Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", - Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", - } + ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"), + ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"), + ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"), + ] ) # fmt: on @@ -266,21 +268,21 @@ def rolling_recent_channel_subscription( def expected_rolling_recent_channel_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ + return ExpectedDownloads( + expected_downloads=[ # Download mapping - Path(".ytdl-sub-pz-download-archive.json"): "97ff47a7c5d89a426a653493ad1a3f06", + ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="97ff47a7c5d89a426a653493ad1a3f06"), # Output directory files - Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", - Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", - Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", + ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"), + ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"), + ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"), # Rolling Recent Entry files - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"): "49cc64b25314155c1b8ab0361ac0c34f", - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"): "e733b4cc385b953b08c8eb0f47e03c1e", - Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo"): "2b3ccb3f1ef81ee49fe1afb88f275a09", - } + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"), md5="49cc64b25314155c1b8ab0361ac0c34f"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"), md5="e733b4cc385b953b08c8eb0f47e03c1e"), + ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.nfo"), md5="2b3ccb3f1ef81ee49fe1afb88f275a09"), + ] ) # fmt: on @@ -291,68 +293,75 @@ class TestChannelAsKodiTvShow: expected md5 file hashes. """ + @pytest.mark.parametrize("dry_run", [True, False]) def test_full_channel_download( - self, full_channel_subscription, expected_full_channel_download, output_directory + self, full_channel_subscription, expected_full_channel_download, output_directory, dry_run ): - full_channel_subscription.download() - expected_full_channel_download.assert_files_exist(relative_directory=output_directory) - - def test_full_channel_dry_run( - self, full_channel_subscription, expected_full_channel_download, output_directory - ): - transaction_log = full_channel_subscription.download(dry_run=True) - expected_full_channel_download.assert_dry_run_files_logged(transaction_log=transaction_log) + transaction_log = full_channel_subscription.download(dry_run=dry_run) + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_channel_full.txt", + ) + if not dry_run: + expected_full_channel_download.assert_files_exist(relative_directory=output_directory) + @pytest.mark.parametrize("dry_run", [True, False]) def test_recent_channel_download( - self, recent_channel_subscription, expected_recent_channel_download, output_directory + self, + recent_channel_subscription, + expected_recent_channel_download, + output_directory, + dry_run, ): - recent_channel_subscription.download() - expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) - - # try downloading again, ensure nothing more was downloaded - with assert_debug_log( - logger=ytdl_sub.downloaders.downloader.logger, - expected_message="ExistingVideoReached, stopping additional downloads", - ): - recent_channel_subscription.download() + transaction_log = recent_channel_subscription.download(dry_run=dry_run) + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_channel_recent.txt", + ) + if not dry_run: expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) - def test_recent_channel_dry_run( - self, recent_channel_subscription, expected_recent_channel_download, output_directory - ): - transaction_log = recent_channel_subscription.download(dry_run=True) - expected_recent_channel_download.assert_dry_run_files_logged( - transaction_log=transaction_log - ) + # try downloading again, ensure nothing more was downloaded + with assert_debug_log( + logger=ytdl_sub.downloaders.downloader.logger, + expected_message="ExistingVideoReached, stopping additional downloads", + ): + transaction_log = recent_channel_subscription.download() + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name=( + "youtube/test_channel_no_additional_downloads.txt" + ), + ) + expected_recent_channel_download.assert_files_exist( + relative_directory=output_directory + ) + @pytest.mark.parametrize("dry_run", [True, False]) def test_recent_channel_download__no_vids_in_range( self, recent_channel_no_vids_in_range_subscription, expected_recent_channel_no_vids_in_range_download, output_directory, + dry_run, ): - recent_channel_no_vids_in_range_subscription.download() - expected_recent_channel_no_vids_in_range_download.assert_files_exist( - relative_directory=output_directory - ) - - # Try again, make sure its the same - recent_channel_no_vids_in_range_subscription.download() - expected_recent_channel_no_vids_in_range_download.assert_files_exist( - relative_directory=output_directory - ) - - def test_recent_channel_dry_run__no_vids_in_range( - self, - recent_channel_no_vids_in_range_subscription, - expected_recent_channel_no_vids_in_range_download, - output_directory, - ): - transaction_log = recent_channel_no_vids_in_range_subscription.download(dry_run=True) - expected_recent_channel_no_vids_in_range_download.assert_dry_run_files_logged( - transaction_log=transaction_log - ) + # Run twice, ensure nothing changes between runs + for _ in range(2): + transaction_log = recent_channel_no_vids_in_range_subscription.download(dry_run=dry_run) + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_channel_no_additional_downloads.txt", + ) + if not dry_run: + expected_recent_channel_no_vids_in_range_download.assert_files_exist( + relative_directory=output_directory + ) + @pytest.mark.parametrize("dry_run", [True, False]) def test_rolling_recent_channel_download( self, recent_channel_subscription, @@ -360,14 +369,22 @@ class TestChannelAsKodiTvShow: expected_recent_channel_download, expected_rolling_recent_channel_download, output_directory, + dry_run, ): - # First, download recent vids + # First, download recent vids. Always download since we want to test dry-run + # on the rolling recent portion. with assert_debug_log( logger=ytdl_sub.downloaders.downloader.logger, expected_message="RejectedVideoReached, stopping additional downloads", ): - recent_channel_subscription.download() - expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) + transaction_log = recent_channel_subscription.download() + + expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_channel_recent.txt", + ) # Then, download the rolling recent vids subscription. This should remove one of the # two videos @@ -375,18 +392,33 @@ class TestChannelAsKodiTvShow: logger=ytdl_sub.downloaders.downloader.logger, expected_message="ExistingVideoReached, stopping additional downloads", ): - rolling_recent_channel_subscription.download() + transaction_log = rolling_recent_channel_subscription.download(dry_run=dry_run) + + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_channel_rolling_recent.txt", + regenerate_transaction_log=True, + ) + if not dry_run: expected_rolling_recent_channel_download.assert_files_exist( relative_directory=output_directory ) # Invoke the rolling download again, ensure downloading stopped early from it already # existing - with assert_debug_log( - logger=ytdl_sub.downloaders.downloader.logger, - expected_message="ExistingVideoReached, stopping additional downloads", - ): - rolling_recent_channel_subscription.download() + if not dry_run: + with assert_debug_log( + logger=ytdl_sub.downloaders.downloader.logger, + expected_message="ExistingVideoReached, stopping additional downloads", + ): + transaction_log = rolling_recent_channel_subscription.download() + + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_channel_no_additional_downloads.txt", + ) expected_rolling_recent_channel_download.assert_files_exist( relative_directory=output_directory ) diff --git a/tests/e2e/youtube/test_merge_playlist.py b/tests/e2e/youtube/test_merge_playlist.py index 007f79b7..ce4fd407 100644 --- a/tests/e2e/youtube/test_merge_playlist.py +++ b/tests/e2e/youtube/test_merge_playlist.py @@ -1,7 +1,9 @@ from pathlib import Path import pytest -from e2e.expected_download import ExpectedDownload +from e2e.expected_download import ExpectedDownloadFile +from e2e.expected_download import ExpectedDownloads +from e2e.expected_transaction_log import assert_transaction_log_matches from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.preset import Preset @@ -63,16 +65,15 @@ def playlist_subscription(config, subscription_name, subscription_dict): @pytest.fixture def expected_playlist_download(): - return ExpectedDownload( - expected_md5_file_hashes={ - Path("JMC - Jesse's Minecraft Server-thumb.jpg"): "a3f1910f9c51f6442f845a528e190829", - Path("JMC - Jesse's Minecraft Server.mkv"): [ - "6053c47a8690519b0a33c13fa4b01ac0", - "3ab42b3e6be0a44deb3a9a28e6ebaf16", - ], - Path("JMC - Jesse's Minecraft Server.nfo"): "10df5dcdb65ab18ecf21b3503c77e48b", - } + # fmt: off + return ExpectedDownloads( + expected_downloads=[ + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server-thumb.jpg"), md5="a3f1910f9c51f6442f845a528e190829"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server.mkv"), md5=["6053c47a8690519b0a33c13fa4b01ac0", "3ab42b3e6be0a44deb3a9a28e6ebaf16"]), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server.nfo"), md5="10df5dcdb65ab18ecf21b3503c77e48b"), + ] ) + # fmt: on class TestYoutubeMergePlaylist: @@ -81,14 +82,16 @@ class TestYoutubeMergePlaylist: files exist and have the expected md5 file hashes. """ + @pytest.mark.parametrize("dry_run", [True, False]) def test_merge_playlist_download( - self, playlist_subscription, expected_playlist_download, output_directory + self, playlist_subscription, expected_playlist_download, output_directory, dry_run ): - playlist_subscription.download() - expected_playlist_download.assert_files_exist(relative_directory=output_directory) + transaction_log = playlist_subscription.download() + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_merge_playlist.txt", + ) - def test_merge_playlist_dry_run( - self, playlist_subscription, expected_playlist_download, output_directory - ): - transaction_log = playlist_subscription.download(dry_run=True) - expected_playlist_download.assert_dry_run_files_logged(transaction_log=transaction_log) + if not dry_run: + expected_playlist_download.assert_files_exist(relative_directory=output_directory) diff --git a/tests/e2e/youtube/test_playlist_as_kodi_music_videos.py b/tests/e2e/youtube/test_playlist_as_kodi_music_videos.py index ddfd219d..76111d1b 100644 --- a/tests/e2e/youtube/test_playlist_as_kodi_music_videos.py +++ b/tests/e2e/youtube/test_playlist_as_kodi_music_videos.py @@ -3,7 +3,9 @@ from pathlib import Path import mergedeep import pytest from conftest import assert_debug_log -from e2e.expected_download import ExpectedDownload +from e2e.expected_download import ExpectedDownloadFile +from e2e.expected_download import ExpectedDownloads +from e2e.expected_transaction_log import assert_transaction_log_matches import ytdl_sub.downloaders.downloader from ytdl_sub.config.config_file import ConfigFile @@ -63,24 +65,24 @@ def playlist_subscription(config, subscription_name, subscription_dict): def expected_playlist_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ + return ExpectedDownloads( + expected_downloads=[ # Download mapping - Path(".ytdl-sub-jmc-download-archive.json"): "9f785c29194a6ecfba6a6b4018763ddc", + ExpectedDownloadFile(path=Path(".ytdl-sub-jmc-download-archive.json"), md5="9f785c29194a6ecfba6a6b4018763ddc"), # Entry files - Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"): "b232d253df621aa770b780c1301d364d", - Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4"): "e66287b9832277b6a4d1554e29d9fdcc", - Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo"): "3d272fe58487b6011ad049b6000b046f", + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"), md5="b232d253df621aa770b780c1301d364d"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4"), md5="e66287b9832277b6a4d1554e29d9fdcc"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo"), md5="3d272fe58487b6011ad049b6000b046f"), - Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"): "d17c379ea8b362f5b97c6b213b0342cb", - Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].mp4"): "04ab5cb3cc12325d0c96a7cd04a8b91d", - Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].nfo"): "6f99af10bef67276a507d1d9770c5e92", + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"), md5="d17c379ea8b362f5b97c6b213b0342cb"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].mp4"), md5="04ab5cb3cc12325d0c96a7cd04a8b91d"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].nfo"), md5="6f99af10bef67276a507d1d9770c5e92"), - Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"): "e7830aa8a64b0cde65ba3f7e5fc56530", - Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].mp4"): "025de6099a5c98e6397153c7a62d517d", - Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo"): "beec3c1326654bd8c858cecf4e40977a", - } + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"), md5="e7830aa8a64b0cde65ba3f7e5fc56530"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].mp4"), md5="025de6099a5c98e6397153c7a62d517d"), + ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo"), md5="beec3c1326654bd8c858cecf4e40977a"), + ] ) # fmt: on @@ -118,12 +120,12 @@ def single_video_subscription(config, subscription_name, single_video_subscripti def expected_single_video_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ - Path("JMC - Oblivion Mod 'Falcor' p.1-thumb.jpg"): "fb95b510681676e81c321171fc23143e", - Path("JMC - Oblivion Mod 'Falcor' p.1.mp4"): "931a705864c57d21d6fedebed4af6bbc", - Path("JMC - Oblivion Mod 'Falcor' p.1.nfo"): "89f509a8a3d9003e22a9091abeeae5dc", - } + return ExpectedDownloads( + expected_downloads=[ + ExpectedDownloadFile(path=Path("JMC - Oblivion Mod 'Falcor' p.1-thumb.jpg"), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path("JMC - Oblivion Mod 'Falcor' p.1.mp4"), md5="931a705864c57d21d6fedebed4af6bbc"), + ExpectedDownloadFile(path=Path("JMC - Oblivion Mod 'Falcor' p.1.nfo"), md5="89f509a8a3d9003e22a9091abeeae5dc"), + ] ) # fmt: on @@ -134,36 +136,37 @@ class TestPlaylistAsKodiMusicVideo: files exist and have the expected md5 file hashes. """ + @pytest.mark.parametrize("dry_run", [True, False]) def test_playlist_download( - self, playlist_subscription, expected_playlist_download, output_directory + self, playlist_subscription, expected_playlist_download, output_directory, dry_run ): - playlist_subscription.download() - expected_playlist_download.assert_files_exist(relative_directory=output_directory) + transaction_log = playlist_subscription.download() + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_playlist.txt", + ) - # After the playlist is downloaded, ensure another invocation will hit ExistingVideoReached - with assert_debug_log( - logger=ytdl_sub.downloaders.downloader.logger, - expected_message="ExistingVideoReached, stopping additional downloads", - ): - playlist_subscription.download() + if not dry_run: expected_playlist_download.assert_files_exist(relative_directory=output_directory) - def test_playlist_dry_run( - self, playlist_subscription, expected_playlist_download, output_directory - ): - file_transaction_log = playlist_subscription.download(dry_run=True) - expected_playlist_download.assert_dry_run_files_logged(transaction_log=file_transaction_log) + # Ensure another invocation will hit ExistingVideoReached + with assert_debug_log( + logger=ytdl_sub.downloaders.downloader.logger, + expected_message="ExistingVideoReached, stopping additional downloads", + ): + playlist_subscription.download() + expected_playlist_download.assert_files_exist(relative_directory=output_directory) + @pytest.mark.parametrize("dry_run", [True, False]) def test_single_video_download( - self, single_video_subscription, expected_single_video_download, output_directory + self, single_video_subscription, expected_single_video_download, output_directory, dry_run ): - single_video_subscription.download() - expected_single_video_download.assert_files_exist(relative_directory=output_directory) - - def test_single_video_dry_run( - self, single_video_subscription, expected_single_video_download, output_directory - ): - file_transaction_log = single_video_subscription.download(dry_run=True) - expected_single_video_download.assert_dry_run_files_logged( - transaction_log=file_transaction_log + transaction_log = single_video_subscription.download() + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_video.txt", ) + if not dry_run: + expected_single_video_download.assert_files_exist(relative_directory=output_directory) diff --git a/tests/e2e/youtube/test_split_video.py b/tests/e2e/youtube/test_split_video.py index af4fc33d..cb324ce6 100644 --- a/tests/e2e/youtube/test_split_video.py +++ b/tests/e2e/youtube/test_split_video.py @@ -4,7 +4,9 @@ from tempfile import NamedTemporaryFile import mergedeep import pytest from conftest import assert_debug_log -from e2e.expected_download import ExpectedDownload +from e2e.expected_download import ExpectedDownloadFile +from e2e.expected_download import ExpectedDownloads +from e2e.expected_transaction_log import assert_transaction_log_matches import ytdl_sub.downloaders.downloader from ytdl_sub.config.config_file import ConfigFile @@ -88,27 +90,27 @@ def single_video_subscription(config, subscription_name, subscription_dict): def expected_single_video_download(): # turn off black formatter here for readability # fmt: off - return ExpectedDownload( - expected_md5_file_hashes={ - Path('Project Zombie - 1-6.Intro.mp4'): "eaec6f50f364b13ef1a201e736ec9c05", - Path('Project Zombie - 2-6.Part 1.mp4'): "5850b19acb250cc13db36f80fa1bba5a", - Path('Project Zombie - 3-6.Part 2.mp4'): "445d95eba437db6df284df7e1ab633e8", - Path('Project Zombie - 4-6.Part 3.mp4'): "2b6e7532d515c9e64ed2a33d850cf199", - Path('Project Zombie - 5-6.Part 4.mp4'): "842bf3c4d1fcc4c5ab110635935dac66", - Path('Project Zombie - 6-6.Part 5.mp4'): "238de99f00f829ab72f042b79da9a33a", - Path('Project Zombie - Intro-thumb.jpg'): "fb95b510681676e81c321171fc23143e", - Path('Project Zombie - Intro.nfo'): "ded59ac906f579312cc3cf98a57e7ea3", - Path('Project Zombie - Part 1-thumb.jpg'): "fb95b510681676e81c321171fc23143e", - Path('Project Zombie - Part 1.nfo'): "70ff5cd0092b8bc22dc4db93a824789b", - Path('Project Zombie - Part 2-thumb.jpg'): "fb95b510681676e81c321171fc23143e", - Path('Project Zombie - Part 2.nfo'): "54450c18a2cbb9d6d2ee5d0a1fb3f279", - Path('Project Zombie - Part 3-thumb.jpg'): "fb95b510681676e81c321171fc23143e", - Path('Project Zombie - Part 3.nfo'): "0effb13fc4039363a95969d1048dde57", - Path('Project Zombie - Part 4-thumb.jpg'): "fb95b510681676e81c321171fc23143e", - Path('Project Zombie - Part 4.nfo'): "74bd0d7c12105469838768a0cc323a8c", - Path('Project Zombie - Part 5-thumb.jpg'): "fb95b510681676e81c321171fc23143e", - Path('Project Zombie - Part 5.nfo'): "a8cf2e77721335ea7c18e22734e7996c", - } + return ExpectedDownloads( + expected_downloads=[ + ExpectedDownloadFile(path=Path('Project Zombie - 1-6.Intro.mp4'), md5="eaec6f50f364b13ef1a201e736ec9c05"), + ExpectedDownloadFile(path=Path('Project Zombie - 2-6.Part 1.mp4'), md5="5850b19acb250cc13db36f80fa1bba5a"), + ExpectedDownloadFile(path=Path('Project Zombie - 3-6.Part 2.mp4'), md5="445d95eba437db6df284df7e1ab633e8"), + ExpectedDownloadFile(path=Path('Project Zombie - 4-6.Part 3.mp4'), md5="2b6e7532d515c9e64ed2a33d850cf199"), + ExpectedDownloadFile(path=Path('Project Zombie - 5-6.Part 4.mp4'), md5="842bf3c4d1fcc4c5ab110635935dac66"), + ExpectedDownloadFile(path=Path('Project Zombie - 6-6.Part 5.mp4'), md5="238de99f00f829ab72f042b79da9a33a"), + ExpectedDownloadFile(path=Path('Project Zombie - Intro-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path('Project Zombie - Intro.nfo'), md5="ded59ac906f579312cc3cf98a57e7ea3"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 1-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 1.nfo'), md5="70ff5cd0092b8bc22dc4db93a824789b"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 2-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 2.nfo'), md5="54450c18a2cbb9d6d2ee5d0a1fb3f279"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 3-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 3.nfo'), md5="0effb13fc4039363a95969d1048dde57"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 4-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 4.nfo'), md5="74bd0d7c12105469838768a0cc323a8c"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 5-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"), + ExpectedDownloadFile(path=Path('Project Zombie - Part 5.nfo'), md5="a8cf2e77721335ea7c18e22734e7996c"), + ] ) # fmt: on @@ -119,12 +121,16 @@ class TestPlaylistAsKodiMusicVideo: files exist and have the expected md5 file hashes. """ + @pytest.mark.parametrize("dry_run", [True, False]) def test_split_video_download( - self, single_video_subscription, expected_single_video_download, output_directory + self, single_video_subscription, expected_single_video_download, output_directory, dry_run ): - single_video_subscription.download() - expected_single_video_download.assert_files_exist(relative_directory=output_directory) + transaction_log = single_video_subscription.download() + assert_transaction_log_matches( + output_directory=output_directory, + transaction_log=transaction_log, + transaction_log_summary_file_name="youtube/test_split_video.txt", + ) - def test_split_video_dry_run(self, single_video_subscription, expected_single_video_download): - transaction_log = single_video_subscription.download(dry_run=True) - expected_single_video_download.assert_dry_run_files_logged(transaction_log=transaction_log) + if not dry_run: + expected_single_video_download.assert_files_exist(relative_directory=output_directory)