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
This commit is contained in:
Jesse Bannon 2022-07-05 22:51:23 -07:00 committed by GitHub
parent cb31156b42
commit a0b46c0982
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1167 additions and 334 deletions

View file

@ -8,6 +8,7 @@ from typing import Dict
from typing import Generic from typing import Generic
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple
from typing import Type from typing import Type
from typing import TypeVar 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.config.preset_options import Overrides
from ytdl_sub.entries.base_entry import BaseEntry from ytdl_sub.entries.base_entry import BaseEntry
from ytdl_sub.entries.entry import Entry 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.utils.logger import Logger
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
from ytdl_sub.ytdl_additions.enhanced_download_archive import DownloadArchiver 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() return self._get_entry_dicts_from_info_json_files()
@abc.abstractmethod @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""" """The function to perform the download of all media entries"""
def post_download(self, overrides: Overrides): def post_download(self, overrides: Overrides):

View file

@ -1,6 +1,7 @@
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Tuple
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader
from ytdl_sub.downloaders.youtube.playlist import YoutubePlaylistDownloaderOptions 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 Chapters
from ytdl_sub.utils.chapters import Timestamp from ytdl_sub.utils.chapters import Timestamp
from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata from ytdl_sub.utils.ffmpeg import add_ffmpeg_metadata
from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.validators.validators import BoolValidator 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] = [] titles: List[str] = []
timestamps: List[Timestamp] = [] timestamps: List[Timestamp] = []
@ -111,14 +113,17 @@ class YoutubeMergePlaylistDownloader(
current_timestamp_sec += video_entry["duration"] current_timestamp_sec += video_entry["duration"]
# TODO: return chapter metadata here chapters = Chapters(timestamps=timestamps, titles=titles)
if not self.is_dry_run:
if not self.is_dry_run and add_chapters:
add_ffmpeg_metadata( add_ffmpeg_metadata(
file_path=merged_video.get_download_file_path(), file_path=merged_video.get_download_file_path(),
chapters=Chapters(timestamps=timestamps, titles=titles), chapters=chapters,
file_duration_sec=merged_video.kwargs("duration"), 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: 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 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) 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""" """Download a single Youtube video, then split it into multiple videos"""
merged_video = self._to_merged_video( merged_video = self._to_merged_video(
entry_dict=self.extract_info(url=self.download_options.playlist_url) entry_dict=self.extract_info(url=self.download_options.playlist_url)
) )
merged_video_metadata = self._get_chapters(
if self.download_options.add_chapters: merged_video=merged_video, add_chapters=self.download_options.add_chapters
self._add_chapters(merged_video=merged_video) )
return [(merged_video, merged_video_metadata)]
return [merged_video]

View file

@ -3,6 +3,7 @@ import re
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from typing import List from typing import List
from typing import Tuple
from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader from ytdl_sub.downloaders.youtube.abc import YoutubeDownloader
from ytdl_sub.downloaders.youtube.video import YoutubeVideoDownloaderOptions from ytdl_sub.downloaders.youtube.video import YoutubeVideoDownloaderOptions
@ -19,6 +20,7 @@ from ytdl_sub.utils.ffmpeg import FFMPEG
# 01:00:00 title # 01:00:00 title
# where capture group 1 and 2 are the timestamp and title, respectively # 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 FileHandler
from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.validators.validators import StringValidator 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( def _split_video_ffmpeg_cmd(
input_file: str, output_file: str, timestamps: List[Timestamp], idx: int input_file: str, output_file: str, timestamps: List[Timestamp], idx: int
) -> List[str]: ) -> List[str]:
timestamp_begin = timestamps[idx].timestamp_str timestamp_begin = timestamps[idx].standardized_str
timestamp_end = timestamps[idx + 1].timestamp_str if idx + 1 < len(timestamps) else "" timestamp_end = timestamps[idx + 1].standardized_str if idx + 1 < len(timestamps) else ""
cmd = ["-i", input_file, "-ss", timestamp_begin] cmd = ["-i", input_file, "-ss", timestamp_begin]
if timestamp_end: if timestamp_end:
@ -121,15 +123,15 @@ class YoutubeSplitVideoDownloader(
) )
def _create_split_video_entry( def _create_split_video_entry(
self, source_entry_dict: Dict, title: str, idx: int, split_video_count: int self, source_entry_dict: Dict, title: str, idx: int, chapters: Chapters
) -> YoutubePlaylistVideo: ) -> Tuple[YoutubePlaylistVideo, FileMetadata]:
""" """
Runs ffmpeg to create the split video Runs ffmpeg to create the split video
""" """
entry_dict = copy.deepcopy(source_entry_dict) entry_dict = copy.deepcopy(source_entry_dict)
entry_dict["title"] = title entry_dict["title"] = title
entry_dict["playlist_index"] = idx + 1 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) entry_dict["id"] = _split_video_uid(source_uid=entry_dict["id"], idx=idx)
# Remove track and artist since its now split # Remove track and artist since its now split
@ -138,11 +140,20 @@ class YoutubeSplitVideoDownloader(
if "artist" in entry_dict: if "artist" in entry_dict:
del entry_dict["artist"] 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""" """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) chapters = Chapters.from_file(chapters_file_path=self.download_options.split_timestamps)
entry_dict = self.extract_info(url=self.download_options.video_url) entry_dict = self.extract_info(url=self.download_options.video_url)
@ -178,13 +189,10 @@ class YoutubeSplitVideoDownloader(
) )
# Format the split video as a YoutubePlaylistVideo # Format the split video as a YoutubePlaylistVideo
split_videos.append( split_videos_and_metadata.append(
self._create_split_video_entry( self._create_split_video_entry(
source_entry_dict=entry_dict, source_entry_dict=entry_dict, title=title, idx=idx, chapters=chapters
title=title,
idx=idx,
split_video_count=len(chapters.timestamps),
) )
) )
return split_videos return split_videos_and_metadata

View file

@ -1,6 +1,7 @@
import argparse import argparse
import sys import sys
from typing import List from typing import List
from typing import Tuple
from ytdl_sub.cli.download_args_parser import DownloadArgsParser from ytdl_sub.cli.download_args_parser import DownloadArgsParser
from ytdl_sub.cli.main_args_parser import parser 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.config.preset import Preset
from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
logger = Logger.get() 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. 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 Configuration file
args args
Arguments from argparse Arguments from argparse
Returns
-------
List of (subscription, transaction_log)
""" """
preset_paths: List[str] = args.subscription_paths preset_paths: List[str] = args.subscription_paths
presets: List[Preset] = [] 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: for preset_path in preset_paths:
presets += Preset.from_file_path(config=config, subscription_path=preset_path) 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) subscription = Subscription.from_preset(preset=preset, config=config)
logger.info("Beginning subscription download for %s", subscription.name) 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( def _download_subscription_from_cli(
config: ConfigFile, args: argparse.Namespace, extra_args: List[str] config: ConfigFile, args: argparse.Namespace, extra_args: List[str]
) -> None: ) -> Tuple[Subscription, FileHandlerTransactionLog]:
""" """
Downloads a one-off subscription using the CLI Downloads a one-off subscription using the CLI
@ -51,6 +65,10 @@ def _download_subscription_from_cli(
Arguments from argparse Arguments from argparse
extra_args extra_args
Extra arguments from argparse that contain dynamic subscription options Extra arguments from argparse that contain dynamic subscription options
Returns
-------
Subscription and its download transaction log
""" """
dl_args_parser = DownloadArgsParser( dl_args_parser = DownloadArgsParser(
extra_arguments=extra_args, config_options=config.config_options extra_arguments=extra_args, config_options=config.config_options
@ -69,7 +87,7 @@ def _download_subscription_from_cli(
config=config, config=config,
) )
subscription.download(dry_run=args.dry_run) return subscription, subscription.download(dry_run=args.dry_run)
def _main(): def _main():
@ -86,14 +104,22 @@ def _main():
config: ConfigFile = ConfigFile.from_file_path(args.config).initialize() config: ConfigFile = ConfigFile.from_file_path(args.config).initialize()
Logger.set_log_level(log_level_name=args.log_level) Logger.set_log_level(log_level_name=args.log_level)
transaction_logs: List[Tuple[Subscription, FileHandlerTransactionLog]] = []
if args.subparser == "sub": if args.subparser == "sub":
_download_subscriptions_from_yaml_files(config=config, args=args) transaction_logs = _download_subscriptions_from_yaml_files(config=config, args=args)
logger.info("Subscription download complete!")
# One-off download # One-off download
if args.subparser == "dl": if args.subparser == "dl":
_download_subscription_from_cli(config=config, args=args, extra_args=extra_args) transaction_logs.append(
logger.info("Download complete!") _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 # Ran successfully, so we can delete the debug file
Logger.cleanup(delete_debug_file=True) Logger.cleanup(delete_debug_file=True)

View file

@ -56,7 +56,7 @@ class MusicTagsOptions(PluginOptions):
class MusicTagsPlugin(Plugin[MusicTagsOptions]): class MusicTagsPlugin(Plugin[MusicTagsOptions]):
plugin_options_type = 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 Tags the entry's audio file using values defined in the metadata options
""" """

View file

@ -80,7 +80,7 @@ class NfoTagsOptions(PluginOptions):
class NfoTagsPlugin(Plugin[NfoTagsOptions]): class NfoTagsPlugin(Plugin[NfoTagsOptions]):
plugin_options_type = 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 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: with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml) nfo_file.write(xml)
# Archive the nfo's file name # Save the nfo file and log its metadata
self.save_file(file_name=nfo_file_name, entry=entry) 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)
return FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:")

View file

@ -106,5 +106,5 @@ class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]):
with open(nfo_file_path, "wb") as nfo_file: with open(nfo_file_path, "wb") as nfo_file:
nfo_file.write(xml) nfo_file.write(xml)
self.save_file(file_name=nfo_file_name) nfo_metadata = FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:")
return FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") self.save_file(file_name=nfo_file_name, file_metadata=nfo_metadata)

View file

@ -267,10 +267,14 @@ class Subscription:
) )
for entry in downloader.download(): for entry in downloader.download():
# TODO: Add entry metadata from the downloader.download function
entry_metadata = FileMetadata() entry_metadata = FileMetadata()
if isinstance(entry, tuple):
entry, entry_metadata = entry
for plugin in plugins: 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( self._copy_entry_files_to_output_directory(
entry=entry, entry_metadata=entry_metadata entry=entry, entry_metadata=entry_metadata

View file

@ -1,8 +1,11 @@
import os import os
import re import re
from typing import List from typing import List
from typing import Optional
from typing import Tuple
from ytdl_sub.utils.exceptions import ValidationException from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.file_handler import FileMetadata
class Timestamp: class Timestamp:
@ -48,12 +51,7 @@ class Timestamp:
return self._timestamp_sec return self._timestamp_sec
@property @property
def timestamp_str(self) -> str: def _hours_minutes_seconds(self) -> Tuple[int, int, int]:
"""
Returns
-------
The timestamp in 'HH:MM:SS' format
"""
seconds = self.timestamp_sec seconds = self.timestamp_sec
hours = int(seconds / 3600) hours = int(seconds / 3600)
@ -62,6 +60,30 @@ class Timestamp:
minutes = int(seconds / 60) minutes = int(seconds / 60)
seconds -= minutes * 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)}" return f"{str(hours).zfill(2)}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}"
@classmethod @classmethod
@ -125,6 +147,22 @@ class Chapters:
""" """
return self.timestamps[0].timestamp_sec == 0 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 @classmethod
def from_file(cls, chapters_file_path: str) -> "Chapters": def from_file(cls, chapters_file_path: str) -> "Chapters":
""" """

View file

@ -14,8 +14,12 @@ class FileMetadata:
Stores pretty-printed information about a file. Each line in the metadata represents a newline Stores pretty-printed information about a file. Each line in the metadata represents a newline
""" """
def __init__(self, metadata: Optional[List[str]] = None): def __init__(self, metadata: Optional[Union[str, List[str]]] = None):
self.metadata: List[str] = metadata if metadata else [] self.metadata = []
if isinstance(metadata, str):
self.metadata = [metadata]
elif isinstance(metadata, list):
self.metadata = metadata
def append(self, line: str) -> "FileMetadata": def append(self, line: str) -> "FileMetadata":
""" """
@ -54,11 +58,19 @@ class FileMetadata:
def _recursive_add_dict_lines(rdict: Dict, indent: int): def _recursive_add_dict_lines(rdict: Dict, indent: int):
for key, value in sorted(rdict.items()): for key, value in sorted(rdict.items()):
if isinstance(value, Dict):
_recursive_add_dict_lines(rdict=value, indent=indent + 2)
_indent = " " * indent _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) _recursive_add_dict_lines(rdict=value_dict, indent=2)
return cls(metadata=lines) return cls(metadata=lines)
@ -103,6 +115,46 @@ class FileHandlerTransactionLog:
self.files_removed.add(file_name) self.files_removed.add(file_name)
return self 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: class FileHandler:
""" """

View file

@ -618,7 +618,11 @@ class DownloadArchiver:
return self.__enhanced_download_archive.is_dry_run return self.__enhanced_download_archive.is_dry_run
def save_file( 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: ) -> None:
""" """
Saves a file in the working directory to the output directory. Saves a file in the working directory to the output directory.
@ -627,6 +631,8 @@ class DownloadArchiver:
---------- ----------
file_name file_name
Name of the file relative to the working directory 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 output_file_name
Optional. Final name of the file in the output directory (does not include output 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 directory path). If None, use the same working_directory file_name
@ -634,5 +640,8 @@ class DownloadArchiver:
Optional. Entry that the file belongs to Optional. Entry that the file belongs to
""" """
self.__enhanced_download_archive.save_file_to_output_directory( 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,
) )

View file

@ -1,15 +1,22 @@
import hashlib import hashlib
import os.path import os.path
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Union 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 To test ytdl-sub downloads work, we compare each downloaded file's md5 hash to an
expected md5 hash defined in this class. 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). 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]]]]): def __init__(self, expected_downloads: List[ExpectedDownloadFile]):
self.expected_md5_file_hashes = expected_md5_file_hashes self.expected_downloads = expected_downloads
@property @property
def file_count(self) -> int: 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): def assert_files_exist(self, relative_directory: Path):
""" """
@ -35,38 +45,29 @@ class ExpectedDownload:
num_files += 1 num_files += 1
relative_path = Path(*path.parts[3:]) relative_path = Path(*path.parts[3:])
assert ( assert self.contains(
relative_path in self.expected_md5_file_hashes relative_path
), f"File {relative_path} was created but not expected" ), f"File {relative_path} was created but not expected"
assert num_files == self.file_count, "Mismatch in number of created files" 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(): for expected_download in self.expected_downloads:
full_path = Path(relative_directory) / relative_path full_path = Path(relative_directory) / expected_download.path
assert os.path.isfile( assert os.path.isfile(
full_path 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 continue
with open(full_path, "rb") as file: with open(full_path, "rb") as file:
md5_hash = hashlib.md5(file.read()).hexdigest() md5_hash = hashlib.md5(file.read()).hexdigest()
if isinstance(expected_md5_hash, str): expected_md5_hash = expected_download.md5
expected_md5_hash = [expected_md5_hash] if isinstance(expected_download.md5, str):
expected_md5_hash = [expected_download.md5]
assert md5_hash in expected_md5_hash, ( 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}" 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"

View file

@ -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}'"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,7 +1,9 @@
from pathlib import Path from pathlib import Path
import pytest 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.config_file import ConfigFile
from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset import Preset
@ -56,34 +58,32 @@ def discography_subscription(config, subscription_name, subscription_dict):
def expected_discography_download(): def expected_discography_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
# Download mapping # Download mapping
Path(".ytdl-sub-jb-download-archive.json"): "1a99156e9ece62539fb2608416a07200", ExpectedDownloadFile(path=Path(".ytdl-sub-jb-download-archive.json"), md5="1a99156e9ece62539fb2608416a07200"),
# Entry files (singles) # Entry files (singles)
Path("j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3"): "bffbd558e12c6a9e029dc136a88342c4", ExpectedDownloadFile(path=Path("j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3"), md5="bffbd558e12c6a9e029dc136a88342c4"),
Path("j_b/[2021] Baby Santana's Dorian Groove/folder.jpg"): "967892be44b8c47e1be73f055a7c6f08", 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", ExpectedDownloadFile(path=Path("j_b/[2021] Purple Clouds/01 - Purple Clouds.mp3"), md5="038db58aebe2ba875b733932b42a94d6"),
Path("j_b/[2021] Purple Clouds/folder.jpg"): "967892be44b8c47e1be73f055a7c6f08", ExpectedDownloadFile(path=Path("j_b/[2021] Purple Clouds/folder.jpg"), md5="967892be44b8c47e1be73f055a7c6f08"),
# Entry files (albums) # Entry files (albums)
Path("j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3"): "e145f0a2f6012768280c38655ca58065", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/01 - 20160426 184214.mp3"), md5="e145f0a2f6012768280c38655ca58065"),
Path("j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3"): "60c8b8817a197a13e4bb90903af612c5", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/02 - 20160502 123150.mp3"), md5="60c8b8817a197a13e4bb90903af612c5"),
Path("j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3"): "8265b7e4f79878af877bc6ecd9757efe", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/03 - 20160504 143832.mp3"), md5="8265b7e4f79878af877bc6ecd9757efe"),
Path("j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3"): "accf46b76891d2954b893d0f91d82816", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/04 - 20160601 221234.mp3"), md5="accf46b76891d2954b893d0f91d82816"),
Path("j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3"): "e1f584f523336160d5c1104a61de77f3", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/05 - 20160601 222440.mp3"), md5="e1f584f523336160d5c1104a61de77f3"),
Path("j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3"): "f6885b25901177f0357649afe97328cc", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/06 - 20170604 190236.mp3"), md5="f6885b25901177f0357649afe97328cc"),
Path("j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3"): "fa057f221cbe4cf2442cd2fdb960743e", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/07 - 20170612 193646.mp3"), md5="fa057f221cbe4cf2442cd2fdb960743e"),
Path("j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3"): "7794ae812c64580e2ac8fc457d5cc85f", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/08 - 20170628 215206.mp3"), md5="7794ae812c64580e2ac8fc457d5cc85f"),
Path("j_b/[2022] Acoustic Treats/09 - Finding Home.mp3"): "adbf02eddb2090c008eb497d13ff84b9", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/09 - Finding Home.mp3"), md5="adbf02eddb2090c008eb497d13ff84b9"),
Path("j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3"): "65bb10c84366c71498161734f953e93d", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/10 - Shallow Water WIP.mp3"), md5="65bb10c84366c71498161734f953e93d"),
Path("j_b/[2022] Acoustic Treats/11 - Untold History.mp3"): "6904b2918e5dc38d9a9f72d967eb74bf", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/11 - Untold History.mp3"), md5="6904b2918e5dc38d9a9f72d967eb74bf"),
Path("j_b/[2022] Acoustic Treats/folder.jpg"): "967892be44b8c47e1be73f055a7c6f08", ExpectedDownloadFile(path=Path("j_b/[2022] Acoustic Treats/folder.jpg"), md5="967892be44b8c47e1be73f055a7c6f08"),
]
}
) )
# fmt: on # fmt: on
@ -94,14 +94,15 @@ class TestSoundcloudDiscography:
expected md5 file hashes. expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [True, False])
def test_discography_download( 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() transaction_log = discography_subscription.download(dry_run=dry_run)
expected_discography_download.assert_files_exist(relative_directory=output_directory) assert_transaction_log_matches(
output_directory=output_directory,
def test_discography_dry_run( transaction_log=transaction_log,
self, discography_subscription, expected_discography_download, output_directory transaction_log_summary_file_name="soundcloud/test_soundcloud_discography.txt",
): )
transaction_log = discography_subscription.download(dry_run=True) if not dry_run:
expected_discography_download.assert_dry_run_files_logged(transaction_log=transaction_log) expected_discography_download.assert_files_exist(relative_directory=output_directory)

View file

@ -3,7 +3,9 @@ from pathlib import Path
import mergedeep import mergedeep
import pytest import pytest
from conftest import assert_debug_log 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 import ytdl_sub.downloaders.downloader
from ytdl_sub.config.config_file import ConfigFile 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(): def expected_full_channel_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
# Download mapping # Download mapping
Path(".ytdl-sub-pz-download-archive.json"): "dd3c6236a107a665b884f701b8d14d4d", ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="dd3c6236a107a665b884f701b8d14d4d"),
# Output directory files # Output directory files
Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"),
Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"),
Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"),
# Entry files # Entry files
Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1-thumb.jpg"): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1-thumb.jpg"), md5="fb95b510681676e81c321171fc23143e"),
Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.mp4"): "931a705864c57d21d6fedebed4af6bbc", ExpectedDownloadFile(path=Path("Season 2010/s2010.e0813 - Oblivion Mod 'Falcor' p.1.mp4"), md5="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.nfo"), md5="67d8d71d048039080acbba3bce4febaa"),
Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2-thumb.jpg"): "8b32ee9c037fa669e444a0ac181525a1", ExpectedDownloadFile(path=Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2-thumb.jpg"), md5="8b32ee9c037fa669e444a0ac181525a1"),
Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.mp4"): "d3469b4dca7139cb3dbc38712b6796bf", ExpectedDownloadFile(path=Path("Season 2010/s2010.e1202 - Oblivion Mod 'Falcor' p.2.mp4"), md5="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.nfo"), md5="d81f49cedbd7edaee987521e89b37904"),
Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"): "b232d253df621aa770b780c1301d364d", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"), md5="b232d253df621aa770b780c1301d364d"),
Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].mp4"): "e66287b9832277b6a4d1554e29d9fdcc", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0201 - Jesse's Minecraft Server [Trailer - Feb.1].mp4"), md5="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].nfo"), md5="f7c0de89038f8c491bded8a3968720a2"),
Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"): "d17c379ea8b362f5b97c6b213b0342cb", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"), md5="d17c379ea8b362f5b97c6b213b0342cb"),
Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].mp4"): "04ab5cb3cc12325d0c96a7cd04a8b91d", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0227 - Jesse's Minecraft Server [Trailer - Feb.27].mp4"), md5="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].nfo"), md5="ee1eda78fa0980bc703e602b5012dd1f"),
Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"): "e7830aa8a64b0cde65ba3f7e5fc56530", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"), md5="e7830aa8a64b0cde65ba3f7e5fc56530"),
Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].mp4"): "025de6099a5c98e6397153c7a62d517d", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0321 - Jesse's Minecraft Server [Trailer - Mar.21].mp4"), md5="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].nfo"), md5="61eb6369430da0ab6134d78829a7621b"),
Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net)-thumb.jpg"): "c956192a379b3661595c9920972d4819", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net)-thumb.jpg"), md5="c956192a379b3661595c9920972d4819"),
Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).mp4"): "3d9c19835b03355d6fd5d00cd59dbe5b", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0529 - Project Zombie _Official Trailer_ (IP - mc.projectzombie.beastnode.net).mp4"), md5="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).nfo"), md5="60f72b99f5c69f9e03a071a12160928f"),
Path("Season 2011/s2011.e0630 - Project Zombie _Fin-thumb.jpg"): "00ed383591779ffe98291de60f198fe9", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0630 - Project Zombie _Fin-thumb.jpg"), md5="00ed383591779ffe98291de60f198fe9"),
Path("Season 2011/s2011.e0630 - Project Zombie _Fin.mp4"): "4971cb2d4fa29460361031f3fa8e1ea9", ExpectedDownloadFile(path=Path("Season 2011/s2011.e0630 - Project Zombie _Fin.mp4"), md5="4971cb2d4fa29460361031f3fa8e1ea9"),
Path("Season 2011/s2011.e0630 - Project Zombie _Fin.nfo"): "a7b5d9e57d20852f5daf360a1373bb7a", 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", ExpectedDownloadFile(path=Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC]-thumb.jpg"), md5="1718599d5189c65f7d8cf6acfa5ea851"),
Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].mp4"): "55e9b0add08c48c9c66105da0def2426", ExpectedDownloadFile(path=Path("Season 2011/s2011.e1121 - Skyrim 'Ultra HD w_Mods' [PC].mp4"), md5="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].nfo"), md5="fe60e2b6b564f9316b6c7c183e1cf300"),
Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer-thumb.jpg"): "54ebe9df801b278fdd17b21afa8373a6", ExpectedDownloadFile(path=Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer-thumb.jpg"), md5="54ebe9df801b278fdd17b21afa8373a6"),
Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.mp4"): "65e4ce53ed5ec4139995469f99477a50", ExpectedDownloadFile(path=Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.mp4"), md5="65e4ce53ed5ec4139995469f99477a50"),
Path("Season 2012/s2012.e0123 - Project Zombie _Map Trailer.nfo"): "c8900adcca83c473c79a4afbc7ad2de1", 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", ExpectedDownloadFile(path=Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer-thumb.jpg"), md5="e29d49433175de8a761af35c5307791f"),
Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.mp4"): "18620a8257a686beda65e54add4d4cd1", ExpectedDownloadFile(path=Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.mp4"), md5="18620a8257a686beda65e54add4d4cd1"),
Path("Season 2013/s2013.e0719 - Project Zombie Rewind _Trailer.nfo"): "1c993c41d4308a6049333154d0adee16", 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", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"), md5="6f8f5e1e031ec2a04b0a4906c04a19ee"),
Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"): "82f6ee7253e1dbb83ae7215af08ffacc", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"), md5="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.nfo"), md5="cc7886aae3af6b7b0facd82f95390242"),
Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"): "49cc64b25314155c1b8ab0361ac0c34f", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"), md5="49cc64b25314155c1b8ab0361ac0c34f"),
Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"): "e733b4cc385b953b08c8eb0f47e03c1e", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"), md5="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.nfo"), md5="2b3ccb3f1ef81ee49fe1afb88f275a09"),
} ]
) )
# fmt: on # fmt: on
@ -161,25 +163,25 @@ def recent_channel_subscription(config, subscription_name, recent_channel_subscr
def expected_recent_channel_download(): def expected_recent_channel_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
# Download mapping # Download mapping
Path(".ytdl-sub-pz-download-archive.json"): "91534d1c5921d121aa35d7a197ba1940", ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="91534d1c5921d121aa35d7a197ba1940"),
# Output directory files # Output directory files
Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"),
Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"),
Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"),
# Recent Entry files # Recent Entry files
Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"): "6f8f5e1e031ec2a04b0a4906c04a19ee", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer-thumb.jpg"), md5="6f8f5e1e031ec2a04b0a4906c04a19ee"),
Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"): "82f6ee7253e1dbb83ae7215af08ffacc", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1029 - Jesse's Minecraft Server _ Teaser Trailer.mp4"), md5="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.nfo"), md5="cc7886aae3af6b7b0facd82f95390242"),
Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"): "49cc64b25314155c1b8ab0361ac0c34f", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"), md5="49cc64b25314155c1b8ab0361ac0c34f"),
Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"): "e733b4cc385b953b08c8eb0f47e03c1e", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"), md5="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.nfo"), md5="2b3ccb3f1ef81ee49fe1afb88f275a09"),
} ]
) )
# fmt: on # fmt: on
@ -219,16 +221,16 @@ def recent_channel_no_vids_in_range_subscription(
def expected_recent_channel_no_vids_in_range_download(): def expected_recent_channel_no_vids_in_range_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
# Download mapping # Download mapping
Path(".ytdl-sub-pz-download-archive.json"): "99914b932bd37a50b983c5e7c90ae93b", ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="99914b932bd37a50b983c5e7c90ae93b"),
# Output directory files # Output directory files
Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"),
Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"),
Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"),
} ]
) )
# fmt: on # fmt: on
@ -266,21 +268,21 @@ def rolling_recent_channel_subscription(
def expected_rolling_recent_channel_download(): def expected_rolling_recent_channel_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
# Download mapping # Download mapping
Path(".ytdl-sub-pz-download-archive.json"): "97ff47a7c5d89a426a653493ad1a3f06", ExpectedDownloadFile(path=Path(".ytdl-sub-pz-download-archive.json"), md5="97ff47a7c5d89a426a653493ad1a3f06"),
# Output directory files # Output directory files
Path("fanart.jpg"): "c16b8b88a82cbd47d217ee80f6a8b5f3", ExpectedDownloadFile(path=Path("fanart.jpg"), md5="c16b8b88a82cbd47d217ee80f6a8b5f3"),
Path("poster.jpg"): "e92872ff94c96ad49e9579501c791578", ExpectedDownloadFile(path=Path("poster.jpg"), md5="e92872ff94c96ad49e9579501c791578"),
Path("tvshow.nfo"): "83c7db96081ac5bdf289fcf396bec157", ExpectedDownloadFile(path=Path("tvshow.nfo"), md5="83c7db96081ac5bdf289fcf396bec157"),
# Rolling Recent Entry files # Rolling Recent Entry files
Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"): "49cc64b25314155c1b8ab0361ac0c34f", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id-thumb.jpg"), md5="49cc64b25314155c1b8ab0361ac0c34f"),
Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"): "e733b4cc385b953b08c8eb0f47e03c1e", ExpectedDownloadFile(path=Path("Season 2018/s2018.e1102 - Jesse's Minecraft Server _ IP mc.jesse.id.mp4"), md5="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.nfo"), md5="2b3ccb3f1ef81ee49fe1afb88f275a09"),
} ]
) )
# fmt: on # fmt: on
@ -291,68 +293,75 @@ class TestChannelAsKodiTvShow:
expected md5 file hashes. expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [True, False])
def test_full_channel_download( 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() transaction_log = full_channel_subscription.download(dry_run=dry_run)
expected_full_channel_download.assert_files_exist(relative_directory=output_directory) assert_transaction_log_matches(
output_directory=output_directory,
def test_full_channel_dry_run( transaction_log=transaction_log,
self, full_channel_subscription, expected_full_channel_download, output_directory transaction_log_summary_file_name="youtube/test_channel_full.txt",
): )
transaction_log = full_channel_subscription.download(dry_run=True) if not dry_run:
expected_full_channel_download.assert_dry_run_files_logged(transaction_log=transaction_log) expected_full_channel_download.assert_files_exist(relative_directory=output_directory)
@pytest.mark.parametrize("dry_run", [True, False])
def test_recent_channel_download( 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() transaction_log = recent_channel_subscription.download(dry_run=dry_run)
expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) assert_transaction_log_matches(
output_directory=output_directory,
# try downloading again, ensure nothing more was downloaded transaction_log=transaction_log,
with assert_debug_log( transaction_log_summary_file_name="youtube/test_channel_recent.txt",
logger=ytdl_sub.downloaders.downloader.logger, )
expected_message="ExistingVideoReached, stopping additional downloads", if not dry_run:
):
recent_channel_subscription.download()
expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) expected_recent_channel_download.assert_files_exist(relative_directory=output_directory)
def test_recent_channel_dry_run( # try downloading again, ensure nothing more was downloaded
self, recent_channel_subscription, expected_recent_channel_download, output_directory with assert_debug_log(
): logger=ytdl_sub.downloaders.downloader.logger,
transaction_log = recent_channel_subscription.download(dry_run=True) expected_message="ExistingVideoReached, stopping additional downloads",
expected_recent_channel_download.assert_dry_run_files_logged( ):
transaction_log=transaction_log 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( def test_recent_channel_download__no_vids_in_range(
self, self,
recent_channel_no_vids_in_range_subscription, recent_channel_no_vids_in_range_subscription,
expected_recent_channel_no_vids_in_range_download, expected_recent_channel_no_vids_in_range_download,
output_directory, output_directory,
dry_run,
): ):
recent_channel_no_vids_in_range_subscription.download() # Run twice, ensure nothing changes between runs
expected_recent_channel_no_vids_in_range_download.assert_files_exist( for _ in range(2):
relative_directory=output_directory transaction_log = recent_channel_no_vids_in_range_subscription.download(dry_run=dry_run)
) assert_transaction_log_matches(
output_directory=output_directory,
# Try again, make sure its the same transaction_log=transaction_log,
recent_channel_no_vids_in_range_subscription.download() transaction_log_summary_file_name="youtube/test_channel_no_additional_downloads.txt",
expected_recent_channel_no_vids_in_range_download.assert_files_exist( )
relative_directory=output_directory if not dry_run:
) 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
)
@pytest.mark.parametrize("dry_run", [True, False])
def test_rolling_recent_channel_download( def test_rolling_recent_channel_download(
self, self,
recent_channel_subscription, recent_channel_subscription,
@ -360,14 +369,22 @@ class TestChannelAsKodiTvShow:
expected_recent_channel_download, expected_recent_channel_download,
expected_rolling_recent_channel_download, expected_rolling_recent_channel_download,
output_directory, 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( with assert_debug_log(
logger=ytdl_sub.downloaders.downloader.logger, logger=ytdl_sub.downloaders.downloader.logger,
expected_message="RejectedVideoReached, stopping additional downloads", expected_message="RejectedVideoReached, stopping additional downloads",
): ):
recent_channel_subscription.download() transaction_log = recent_channel_subscription.download()
expected_recent_channel_download.assert_files_exist(relative_directory=output_directory)
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 # Then, download the rolling recent vids subscription. This should remove one of the
# two videos # two videos
@ -375,18 +392,33 @@ class TestChannelAsKodiTvShow:
logger=ytdl_sub.downloaders.downloader.logger, logger=ytdl_sub.downloaders.downloader.logger,
expected_message="ExistingVideoReached, stopping additional downloads", 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( expected_rolling_recent_channel_download.assert_files_exist(
relative_directory=output_directory relative_directory=output_directory
) )
# Invoke the rolling download again, ensure downloading stopped early from it already # Invoke the rolling download again, ensure downloading stopped early from it already
# existing # existing
with assert_debug_log( if not dry_run:
logger=ytdl_sub.downloaders.downloader.logger, with assert_debug_log(
expected_message="ExistingVideoReached, stopping additional downloads", logger=ytdl_sub.downloaders.downloader.logger,
): expected_message="ExistingVideoReached, stopping additional downloads",
rolling_recent_channel_subscription.download() ):
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( expected_rolling_recent_channel_download.assert_files_exist(
relative_directory=output_directory relative_directory=output_directory
) )

View file

@ -1,7 +1,9 @@
from pathlib import Path from pathlib import Path
import pytest 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.config_file import ConfigFile
from ytdl_sub.config.preset import Preset from ytdl_sub.config.preset import Preset
@ -63,16 +65,15 @@ def playlist_subscription(config, subscription_name, subscription_dict):
@pytest.fixture @pytest.fixture
def expected_playlist_download(): def expected_playlist_download():
return ExpectedDownload( # fmt: off
expected_md5_file_hashes={ return ExpectedDownloads(
Path("JMC - Jesse's Minecraft Server-thumb.jpg"): "a3f1910f9c51f6442f845a528e190829", expected_downloads=[
Path("JMC - Jesse's Minecraft Server.mkv"): [ ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server-thumb.jpg"), md5="a3f1910f9c51f6442f845a528e190829"),
"6053c47a8690519b0a33c13fa4b01ac0", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server.mkv"), md5=["6053c47a8690519b0a33c13fa4b01ac0", "3ab42b3e6be0a44deb3a9a28e6ebaf16"]),
"3ab42b3e6be0a44deb3a9a28e6ebaf16", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server.nfo"), md5="10df5dcdb65ab18ecf21b3503c77e48b"),
], ]
Path("JMC - Jesse's Minecraft Server.nfo"): "10df5dcdb65ab18ecf21b3503c77e48b",
}
) )
# fmt: on
class TestYoutubeMergePlaylist: class TestYoutubeMergePlaylist:
@ -81,14 +82,16 @@ class TestYoutubeMergePlaylist:
files exist and have the expected md5 file hashes. files exist and have the expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [True, False])
def test_merge_playlist_download( 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() transaction_log = playlist_subscription.download()
expected_playlist_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_merge_playlist.txt",
)
def test_merge_playlist_dry_run( if not dry_run:
self, playlist_subscription, expected_playlist_download, output_directory expected_playlist_download.assert_files_exist(relative_directory=output_directory)
):
transaction_log = playlist_subscription.download(dry_run=True)
expected_playlist_download.assert_dry_run_files_logged(transaction_log=transaction_log)

View file

@ -3,7 +3,9 @@ from pathlib import Path
import mergedeep import mergedeep
import pytest import pytest
from conftest import assert_debug_log 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 import ytdl_sub.downloaders.downloader
from ytdl_sub.config.config_file import ConfigFile from ytdl_sub.config.config_file import ConfigFile
@ -63,24 +65,24 @@ def playlist_subscription(config, subscription_name, subscription_dict):
def expected_playlist_download(): def expected_playlist_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
# Download mapping # Download mapping
Path(".ytdl-sub-jmc-download-archive.json"): "9f785c29194a6ecfba6a6b4018763ddc", ExpectedDownloadFile(path=Path(".ytdl-sub-jmc-download-archive.json"), md5="9f785c29194a6ecfba6a6b4018763ddc"),
# Entry files # Entry files
Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"): "b232d253df621aa770b780c1301d364d", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg"), md5="b232d253df621aa770b780c1301d364d"),
Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4"): "e66287b9832277b6a4d1554e29d9fdcc", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].mp4"), md5="e66287b9832277b6a4d1554e29d9fdcc"),
Path("JMC - Jesse's Minecraft Server [Trailer - Feb.1].nfo"): "3d272fe58487b6011ad049b6000b046f", 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", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg"), md5="d17c379ea8b362f5b97c6b213b0342cb"),
Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].mp4"): "04ab5cb3cc12325d0c96a7cd04a8b91d", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].mp4"), md5="04ab5cb3cc12325d0c96a7cd04a8b91d"),
Path("JMC - Jesse's Minecraft Server [Trailer - Feb.27].nfo"): "6f99af10bef67276a507d1d9770c5e92", 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", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg"), md5="e7830aa8a64b0cde65ba3f7e5fc56530"),
Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].mp4"): "025de6099a5c98e6397153c7a62d517d", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].mp4"), md5="025de6099a5c98e6397153c7a62d517d"),
Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo"): "beec3c1326654bd8c858cecf4e40977a", ExpectedDownloadFile(path=Path("JMC - Jesse's Minecraft Server [Trailer - Mar.21].nfo"), md5="beec3c1326654bd8c858cecf4e40977a"),
} ]
) )
# fmt: on # fmt: on
@ -118,12 +120,12 @@ def single_video_subscription(config, subscription_name, single_video_subscripti
def expected_single_video_download(): def expected_single_video_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
Path("JMC - Oblivion Mod 'Falcor' p.1-thumb.jpg"): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path("JMC - Oblivion Mod 'Falcor' p.1-thumb.jpg"), md5="fb95b510681676e81c321171fc23143e"),
Path("JMC - Oblivion Mod 'Falcor' p.1.mp4"): "931a705864c57d21d6fedebed4af6bbc", ExpectedDownloadFile(path=Path("JMC - Oblivion Mod 'Falcor' p.1.mp4"), md5="931a705864c57d21d6fedebed4af6bbc"),
Path("JMC - Oblivion Mod 'Falcor' p.1.nfo"): "89f509a8a3d9003e22a9091abeeae5dc", ExpectedDownloadFile(path=Path("JMC - Oblivion Mod 'Falcor' p.1.nfo"), md5="89f509a8a3d9003e22a9091abeeae5dc"),
} ]
) )
# fmt: on # fmt: on
@ -134,36 +136,37 @@ class TestPlaylistAsKodiMusicVideo:
files exist and have the expected md5 file hashes. files exist and have the expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [True, False])
def test_playlist_download( 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() transaction_log = playlist_subscription.download()
expected_playlist_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_playlist.txt",
)
# After the playlist is downloaded, ensure another invocation will hit ExistingVideoReached if not dry_run:
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) expected_playlist_download.assert_files_exist(relative_directory=output_directory)
def test_playlist_dry_run( # Ensure another invocation will hit ExistingVideoReached
self, playlist_subscription, expected_playlist_download, output_directory with assert_debug_log(
): logger=ytdl_sub.downloaders.downloader.logger,
file_transaction_log = playlist_subscription.download(dry_run=True) expected_message="ExistingVideoReached, stopping additional downloads",
expected_playlist_download.assert_dry_run_files_logged(transaction_log=file_transaction_log) ):
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( 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() transaction_log = single_video_subscription.download()
expected_single_video_download.assert_files_exist(relative_directory=output_directory) assert_transaction_log_matches(
output_directory=output_directory,
def test_single_video_dry_run( transaction_log=transaction_log,
self, single_video_subscription, expected_single_video_download, output_directory transaction_log_summary_file_name="youtube/test_video.txt",
):
file_transaction_log = single_video_subscription.download(dry_run=True)
expected_single_video_download.assert_dry_run_files_logged(
transaction_log=file_transaction_log
) )
if not dry_run:
expected_single_video_download.assert_files_exist(relative_directory=output_directory)

View file

@ -4,7 +4,9 @@ from tempfile import NamedTemporaryFile
import mergedeep import mergedeep
import pytest import pytest
from conftest import assert_debug_log 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 import ytdl_sub.downloaders.downloader
from ytdl_sub.config.config_file import ConfigFile 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(): def expected_single_video_download():
# turn off black formatter here for readability # turn off black formatter here for readability
# fmt: off # fmt: off
return ExpectedDownload( return ExpectedDownloads(
expected_md5_file_hashes={ expected_downloads=[
Path('Project Zombie - 1-6.Intro.mp4'): "eaec6f50f364b13ef1a201e736ec9c05", ExpectedDownloadFile(path=Path('Project Zombie - 1-6.Intro.mp4'), md5="eaec6f50f364b13ef1a201e736ec9c05"),
Path('Project Zombie - 2-6.Part 1.mp4'): "5850b19acb250cc13db36f80fa1bba5a", ExpectedDownloadFile(path=Path('Project Zombie - 2-6.Part 1.mp4'), md5="5850b19acb250cc13db36f80fa1bba5a"),
Path('Project Zombie - 3-6.Part 2.mp4'): "445d95eba437db6df284df7e1ab633e8", ExpectedDownloadFile(path=Path('Project Zombie - 3-6.Part 2.mp4'), md5="445d95eba437db6df284df7e1ab633e8"),
Path('Project Zombie - 4-6.Part 3.mp4'): "2b6e7532d515c9e64ed2a33d850cf199", ExpectedDownloadFile(path=Path('Project Zombie - 4-6.Part 3.mp4'), md5="2b6e7532d515c9e64ed2a33d850cf199"),
Path('Project Zombie - 5-6.Part 4.mp4'): "842bf3c4d1fcc4c5ab110635935dac66", ExpectedDownloadFile(path=Path('Project Zombie - 5-6.Part 4.mp4'), md5="842bf3c4d1fcc4c5ab110635935dac66"),
Path('Project Zombie - 6-6.Part 5.mp4'): "238de99f00f829ab72f042b79da9a33a", ExpectedDownloadFile(path=Path('Project Zombie - 6-6.Part 5.mp4'), md5="238de99f00f829ab72f042b79da9a33a"),
Path('Project Zombie - Intro-thumb.jpg'): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path('Project Zombie - Intro-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"),
Path('Project Zombie - Intro.nfo'): "ded59ac906f579312cc3cf98a57e7ea3", ExpectedDownloadFile(path=Path('Project Zombie - Intro.nfo'), md5="ded59ac906f579312cc3cf98a57e7ea3"),
Path('Project Zombie - Part 1-thumb.jpg'): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path('Project Zombie - Part 1-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"),
Path('Project Zombie - Part 1.nfo'): "70ff5cd0092b8bc22dc4db93a824789b", ExpectedDownloadFile(path=Path('Project Zombie - Part 1.nfo'), md5="70ff5cd0092b8bc22dc4db93a824789b"),
Path('Project Zombie - Part 2-thumb.jpg'): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path('Project Zombie - Part 2-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"),
Path('Project Zombie - Part 2.nfo'): "54450c18a2cbb9d6d2ee5d0a1fb3f279", ExpectedDownloadFile(path=Path('Project Zombie - Part 2.nfo'), md5="54450c18a2cbb9d6d2ee5d0a1fb3f279"),
Path('Project Zombie - Part 3-thumb.jpg'): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path('Project Zombie - Part 3-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"),
Path('Project Zombie - Part 3.nfo'): "0effb13fc4039363a95969d1048dde57", ExpectedDownloadFile(path=Path('Project Zombie - Part 3.nfo'), md5="0effb13fc4039363a95969d1048dde57"),
Path('Project Zombie - Part 4-thumb.jpg'): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path('Project Zombie - Part 4-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"),
Path('Project Zombie - Part 4.nfo'): "74bd0d7c12105469838768a0cc323a8c", ExpectedDownloadFile(path=Path('Project Zombie - Part 4.nfo'), md5="74bd0d7c12105469838768a0cc323a8c"),
Path('Project Zombie - Part 5-thumb.jpg'): "fb95b510681676e81c321171fc23143e", ExpectedDownloadFile(path=Path('Project Zombie - Part 5-thumb.jpg'), md5="fb95b510681676e81c321171fc23143e"),
Path('Project Zombie - Part 5.nfo'): "a8cf2e77721335ea7c18e22734e7996c", ExpectedDownloadFile(path=Path('Project Zombie - Part 5.nfo'), md5="a8cf2e77721335ea7c18e22734e7996c"),
} ]
) )
# fmt: on # fmt: on
@ -119,12 +121,16 @@ class TestPlaylistAsKodiMusicVideo:
files exist and have the expected md5 file hashes. files exist and have the expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [True, False])
def test_split_video_download( 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() transaction_log = single_video_subscription.download()
expected_single_video_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_split_video.txt",
)
def test_split_video_dry_run(self, single_video_subscription, expected_single_video_download): if not dry_run:
transaction_log = single_video_subscription.download(dry_run=True) expected_single_video_download.assert_files_exist(relative_directory=output_directory)
expected_single_video_download.assert_dry_run_files_logged(transaction_log=transaction_log)