[BUGFIX] Do not hard-fail if the thumbnail fails to download/convert (#675)

* [BUGFIX] Do not fail with thumbnail issues

* helper

* refactor

* dry-run

* regen fixture

* download arhcive txt -> json in docs, fixtures

* return if no download thumb path

* better test

* more test coverage
This commit is contained in:
Jesse Bannon 2023-07-28 13:12:46 -07:00 committed by GitHub
parent a9e6a80111
commit 5f9b8afc4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 162 additions and 86 deletions

View file

@ -245,7 +245,7 @@ class OutputOptions(StrictDictValidator):
# optional # optional
thumbnail_name: "{title_sanitized}.{thumbnail_ext}" thumbnail_name: "{title_sanitized}.{thumbnail_ext}"
info_json_name: "{title_sanitized}.{info_json_ext}" info_json_name: "{title_sanitized}.{info_json_ext}"
download_archive_name: ".ytdl-sub-{subscription_name}-download-archive.txt" download_archive_name: ".ytdl-sub-{subscription_name}-download-archive.json"
maintain_download_archive: True maintain_download_archive: True
keep_files_before: now keep_files_before: now
keep_files_after: 19000101 keep_files_after: 19000101
@ -354,7 +354,7 @@ class OutputOptions(StrictDictValidator):
def download_archive_name(self) -> Optional[OverridesStringFormatterValidator]: def download_archive_name(self) -> Optional[OverridesStringFormatterValidator]:
""" """
Optional. The file name to store a subscriptions download archive placed relative to Optional. The file name to store a subscriptions download archive placed relative to
the output directory. Defaults to ``.ytdl-sub-{subscription_name}-download-archive.txt`` the output directory. Defaults to ``.ytdl-sub-{subscription_name}-download-archive.json``
""" """
return self._download_archive_name return self._download_archive_name

View file

@ -34,8 +34,8 @@ from ytdl_sub.entries.variables.kwargs import YTDL_SUB_MATCH_FILTER_REJECT
from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.thumbnail import ThumbnailTypes from ytdl_sub.utils.thumbnail import ThumbnailTypes
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.utils.thumbnail import download_and_convert_url_thumbnail from ytdl_sub.utils.thumbnail import download_and_convert_url_thumbnail
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive from ytdl_sub.ytdl_additions.enhanced_download_archive import EnhancedDownloadArchive
download_logger = Logger.get(name="downloader") download_logger = Logger.get(name="downloader")
@ -82,11 +82,9 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
# If latest entry, always update the thumbnail on each entry # If latest entry, always update the thumbnail on each entry
if thumbnail_id == ThumbnailTypes.LATEST_ENTRY: if thumbnail_id == ThumbnailTypes.LATEST_ENTRY:
# Make sure the entry's thumbnail is converted to jpg
convert_download_thumbnail(entry, error_if_not_found=False)
# always save in dry-run even if it doesn't exist... # always save in dry-run even if it doesn't exist...
if self.is_dry_run or os.path.isfile(entry.get_download_thumbnail_path()): if self.is_dry_run or entry.is_thumbnail_downloaded():
self.save_file( self.save_file(
file_name=entry.get_download_thumbnail_name(), file_name=entry.get_download_thumbnail_name(),
output_file_name=thumbnail_name, output_file_name=thumbnail_name,
@ -138,8 +136,14 @@ class UrlDownloaderThumbnailPlugin(SourcePluginExtension):
def modify_entry(self, entry: Entry) -> Optional[Entry]: def modify_entry(self, entry: Entry) -> Optional[Entry]:
""" """
Use the entry to download thumbnails (or move if LATEST_ENTRY) Use the entry to download thumbnails (or move if LATEST_ENTRY).
In addition, convert the entry thumbnail to jpg
""" """
# We always convert entry thumbnails to jpgs, and is performed here to be done
# as early as possible in the plugin pipeline (downstream plugins depend on it being jpg)
if not self.is_dry_run:
try_convert_download_thumbnail(entry=entry)
if entry.kwargs_get(COLLECTION_URL) in self._collection_url_mapping: if entry.kwargs_get(COLLECTION_URL) in self._collection_url_mapping:
self._download_url_thumbnails( self._download_url_thumbnails(
collection_url=self._collection_url_mapping[entry.kwargs(COLLECTION_URL)], collection_url=self._collection_url_mapping[entry.kwargs(COLLECTION_URL)],
@ -346,7 +350,7 @@ class MultiUrlDownloader(SourcePlugin[MultiUrlValidator]):
is_downloaded_fn=None if self.is_dry_run else entry.is_downloaded, is_downloaded_fn=None if self.is_dry_run else entry.is_downloaded,
is_thumbnail_downloaded_fn=None is_thumbnail_downloaded_fn=None
if (self.is_dry_run or not self.is_entry_thumbnails_enabled) if (self.is_dry_run or not self.is_entry_thumbnails_enabled)
else entry.is_thumbnail_downloaded, else entry.is_thumbnail_downloaded_via_ytdlp,
url=entry.webpage_url, url=entry.webpage_url,
) )
return Entry(download_entry_dict, working_directory=self.working_directory) return Entry(download_entry_dict, working_directory=self.working_directory)

View file

@ -86,10 +86,12 @@ class YTDLP:
If the entry fails to download If the entry fails to download
""" """
num_tries = 0 num_tries = 0
entry_files_exist = False
copied_ytdl_options_overrides = copy.deepcopy(ytdl_options_overrides) copied_ytdl_options_overrides = copy.deepcopy(ytdl_options_overrides)
while not entry_files_exist and num_tries < cls._EXTRACT_ENTRY_NUM_RETRIES: is_downloaded = False
entry_dict: Optional[Dict] = None
while num_tries < cls._EXTRACT_ENTRY_NUM_RETRIES:
entry_dict = cls.extract_info( entry_dict = cls.extract_info(
ytdl_options_overrides=copied_ytdl_options_overrides, **kwargs ytdl_options_overrides=copied_ytdl_options_overrides, **kwargs
) )
@ -115,7 +117,7 @@ class YTDLP:
time.sleep(cls._EXTRACT_ENTRY_RETRY_WAIT_SEC) time.sleep(cls._EXTRACT_ENTRY_RETRY_WAIT_SEC)
num_tries += 1 num_tries += 1
# Remove the download archive so it can retry without thinking its already downloaded, # Remove the download archive to retry without thinking its already downloaded,
# even though it is not # even though it is not
if "download_archive" in copied_ytdl_options_overrides: if "download_archive" in copied_ytdl_options_overrides:
del copied_ytdl_options_overrides["download_archive"] del copied_ytdl_options_overrides["download_archive"]
@ -127,6 +129,10 @@ class YTDLP:
cls._EXTRACT_ENTRY_NUM_RETRIES, cls._EXTRACT_ENTRY_NUM_RETRIES,
) )
# Still return if the media file downloaded (thumbnail could be missing)
if is_downloaded and entry_dict is not None:
return entry_dict
error_dict = {"ytdl_options": ytdl_options_overrides, "kwargs": kwargs} error_dict = {"ytdl_options": ytdl_options_overrides, "kwargs": kwargs}
raise FileNotDownloadedException( raise FileNotDownloadedException(
f"yt-dlp failed to download an entry with these arguments: {error_dict}" f"yt-dlp failed to download an entry with these arguments: {error_dict}"

View file

@ -54,7 +54,7 @@ class Entry(EntryVariables, BaseEntry):
"""Returns the entry's thumbnail's file path to where it was downloaded""" """Returns the entry's thumbnail's file path to where it was downloaded"""
return str(Path(self.working_directory()) / self.get_download_thumbnail_name()) return str(Path(self.working_directory()) / self.get_download_thumbnail_name())
def get_ytdlp_download_thumbnail_path(self) -> Optional[str]: def try_get_ytdlp_download_thumbnail_path(self) -> Optional[str]:
""" """
The source `thumbnail` value and the actual downloaded thumbnail extension sometimes do The source `thumbnail` value and the actual downloaded thumbnail extension sometimes do
not match. Return the actual downloaded thumbnail path. not match. Return the actual downloaded thumbnail path.
@ -83,14 +83,23 @@ class Entry(EntryVariables, BaseEntry):
with open(self.get_download_info_json_path(), "w", encoding="utf-8") as file: with open(self.get_download_info_json_path(), "w", encoding="utf-8") as file:
file.write(kwargs_json) file.write(kwargs_json)
@final
def is_thumbnail_downloaded_via_ytdlp(self) -> bool:
"""
Returns
-------
True if ANY thumbnail file exist locally. False otherwise.
"""
return self.try_get_ytdlp_download_thumbnail_path() is not None
@final @final
def is_thumbnail_downloaded(self) -> bool: def is_thumbnail_downloaded(self) -> bool:
""" """
Returns Returns
------- -------
True if the thumbnail file exist locally. False otherwise. True if the thumbnail file exists and is its proper format. False otherwise.
""" """
return self.get_ytdlp_download_thumbnail_path() is not None return os.path.isfile(self.get_download_thumbnail_path())
@final @final
def is_downloaded(self) -> bool: def is_downloaded(self) -> bool:

View file

@ -11,7 +11,6 @@ from ytdl_sub.utils.ffmpeg import FFMPEG
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.file_handler import FileMetadata
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
from ytdl_sub.validators.validators import BoolValidator from ytdl_sub.validators.validators import BoolValidator
@ -91,8 +90,11 @@ class EmbedThumbnailPlugin(Plugin[EmbedThumbnailOptions]):
return None return None
if not self.is_dry_run: if not self.is_dry_run:
# convert the entry thumbnail so it is embedded as jpg if not entry.is_thumbnail_downloaded():
convert_download_thumbnail(entry=entry) logger.warning(
"Cannot embed thumbnail for '%s' because it is not available", entry.title
)
return None
if entry.ext in AUDIO_CODEC_EXTS: if entry.ext in AUDIO_CODEC_EXTS:
self._embed_audio_file(entry) self._embed_audio_file(entry)

View file

@ -11,7 +11,6 @@ from ytdl_sub.config.preset_options import OptionsDictValidator
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.file_handler import FileMetadata
from ytdl_sub.utils.logger import Logger from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS from ytdl_sub.validators.audo_codec_validator import AUDIO_CODEC_EXTS
from ytdl_sub.validators.strict_dict_validator import StrictDictValidator from ytdl_sub.validators.strict_dict_validator import StrictDictValidator
from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator from ytdl_sub.validators.string_formatter_validators import ListFormatterValidator
@ -162,11 +161,7 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]):
) )
setattr(audio_file, tag_name, tag_value[0]) setattr(audio_file, tag_name, tag_value[0])
if self.plugin_options.embed_thumbnail: if self.plugin_options.embed_thumbnail and entry.is_thumbnail_downloaded():
# convert the entry thumbnail so it is embedded as jpg
convert_download_thumbnail(entry=entry)
with open(entry.get_download_thumbnail_path(), "rb") as thumb: with open(entry.get_download_thumbnail_path(), "rb") as thumb:
mediafile_img = mediafile.Image( mediafile_img = mediafile.Image(
data=thumb.read(), desc="cover", type=mediafile.ImageType.front data=thumb.read(), desc="cover", type=mediafile.ImageType.front

View file

@ -1,5 +1,4 @@
import copy import copy
import os.path
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import List from typing import List
@ -21,7 +20,6 @@ from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.ffmpeg import FFMPEG from ytdl_sub.utils.ffmpeg import FFMPEG
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.file_handler import FileMetadata
from ytdl_sub.utils.thumbnail import convert_download_thumbnail
from ytdl_sub.validators.string_select_validator import StringSelectValidator from ytdl_sub.validators.string_select_validator import StringSelectValidator
@ -184,11 +182,6 @@ class SplitByChaptersPlugin(SplitPlugin[SplitByChaptersOptions]):
f"Tried to split '{entry.title}' by chapters but it has no chapters" f"Tried to split '{entry.title}' by chapters but it has no chapters"
) )
# convert the entry thumbnail early so we do not have to guess the thumbnail extension
# when copying it. Do not error if it's not found, in case thumbnail_name is not set
if not self.is_dry_run:
convert_download_thumbnail(entry=entry, error_if_not_found=False)
for idx, title in enumerate(chapters.titles): for idx, title in enumerate(chapters.titles):
new_uid = _split_video_uid(source_uid=entry.uid, idx=idx) new_uid = _split_video_uid(source_uid=entry.uid, idx=idx)
@ -209,7 +202,7 @@ class SplitByChaptersPlugin(SplitPlugin[SplitByChaptersOptions]):
# Copy the original vid thumbnail to the working directory with the new uid. This so # Copy the original vid thumbnail to the working directory with the new uid. This so
# downstream logic thinks this split video has its own thumbnail # downstream logic thinks this split video has its own thumbnail
if os.path.isfile(entry.get_download_thumbnail_path()): if entry.is_thumbnail_downloaded():
FileHandler.copy( FileHandler.copy(
src_file_path=entry.get_download_thumbnail_path(), src_file_path=entry.get_download_thumbnail_path(),
dst_file_path=Path(self.working_directory) dst_file_path=Path(self.working_directory)

View file

@ -1,4 +1,5 @@
import contextlib import contextlib
import logging
import os import os
import shutil import shutil
from abc import ABC from abc import ABC
@ -21,7 +22,9 @@ from ytdl_sub.utils.exceptions import ValidationException
from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.file_handler import FileHandlerTransactionLog from ytdl_sub.utils.file_handler import FileHandlerTransactionLog
from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.utils.file_handler import FileMetadata
from ytdl_sub.utils.thumbnail import convert_download_thumbnail from ytdl_sub.utils.logger import Logger
logger: logging.Logger = Logger.get()
def _get_split_plugin(plugins: List[Plugin]) -> Optional[SplitPlugin]: def _get_split_plugin(plugins: List[Plugin]) -> Optional[SplitPlugin]:
@ -69,16 +72,12 @@ class SubscriptionDownload(BaseSubscription, ABC):
entry=entry, entry=entry,
) )
# TODO: see if entry even has a thumbnail # Always pretend to include the thumbnail in a dry-run
if self.output_options.thumbnail_name: if self.output_options.thumbnail_name and (dry_run or entry.is_thumbnail_downloaded()):
output_thumbnail_name = self.overrides.apply_formatter( output_thumbnail_name = self.overrides.apply_formatter(
formatter=self.output_options.thumbnail_name, entry=entry formatter=self.output_options.thumbnail_name, entry=entry
) )
# We always convert entry thumbnails to jpgs, and is performed here
if not dry_run:
convert_download_thumbnail(entry=entry)
# Copy the thumbnails since they could be used later for other things # Copy the thumbnails since they could be used later for other things
self._enhanced_download_archive.save_file_to_output_directory( self._enhanced_download_archive.save_file_to_output_directory(
file_name=entry.get_download_thumbnail_name(), file_name=entry.get_download_thumbnail_name(),
@ -86,6 +85,10 @@ class SubscriptionDownload(BaseSubscription, ABC):
entry=entry, entry=entry,
copy_file=True, copy_file=True,
) )
elif not entry.is_thumbnail_downloaded():
logger.warning(
"Cannot save thumbnail for '%s' because it is not available", entry.title
)
if self.output_options.info_json_name: if self.output_options.info_json_name:
output_info_json_name = self.overrides.apply_formatter( output_info_json_name = self.overrides.apply_formatter(

View file

@ -1,11 +1,14 @@
import logging
import os import os
import tempfile import tempfile
from subprocess import CalledProcessError
from typing import Optional from typing import Optional
from urllib.request import urlopen from urllib.request import urlopen
from ytdl_sub.entries.entry import Entry from ytdl_sub.entries.entry import Entry
from ytdl_sub.utils.ffmpeg import FFMPEG from ytdl_sub.utils.ffmpeg import FFMPEG
from ytdl_sub.utils.file_handler import FileHandler from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.logger import Logger
from ytdl_sub.utils.retry import retry from ytdl_sub.utils.retry import retry
@ -13,39 +16,39 @@ class ThumbnailTypes:
LATEST_ENTRY = "latest_entry" LATEST_ENTRY = "latest_entry"
def convert_download_thumbnail(entry: Entry, error_if_not_found: bool = True) -> None: logger: logging.Logger = Logger.get("thumbnail")
def try_convert_download_thumbnail(entry: Entry) -> None:
""" """
Converts an entry's downloaded thumbnail into jpg format Converts an entry's downloaded thumbnail into jpg format.
Log with a warning if the thumbnail is not found or fails to convert
Parameters Parameters
---------- ----------
entry entry
Entry with the thumbnail Entry with the thumbnail
error_if_not_found
If the thumbnail is not found, error if True.
Raises
------
ValueError
Entry thumbnail file not found
""" """
download_thumbnail_path = entry.get_ytdlp_download_thumbnail_path() download_thumbnail_path: Optional[str] = entry.try_get_ytdlp_download_thumbnail_path()
download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path() download_thumbnail_path_as_jpg = entry.get_download_thumbnail_path()
# If it was already converted, do not convert again # If it was already converted, do not convert again
if os.path.isfile(download_thumbnail_path_as_jpg): if entry.is_thumbnail_downloaded():
return return
if not download_thumbnail_path: if not download_thumbnail_path:
if error_if_not_found: logger.warning("Thumbnail for '%s' was not downloaded", entry.title)
raise ValueError("Thumbnail not found")
return return
if not download_thumbnail_path == download_thumbnail_path_as_jpg: if not download_thumbnail_path == download_thumbnail_path_as_jpg:
FFMPEG.run( try:
["-y", "-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg] FFMPEG.run(
) ["-y", "-bitexact", "-i", download_thumbnail_path, download_thumbnail_path_as_jpg]
FileHandler.delete(download_thumbnail_path) )
except CalledProcessError:
logger.warning("Failed to convert thumbnail for '%s' to jpg", entry.title)
finally:
FileHandler.delete(download_thumbnail_path)
@retry(times=3, exceptions=(Exception,)) @retry(times=3, exceptions=(Exception,))

View file

@ -257,7 +257,6 @@ class TestRegex:
output_directory=output_directory, output_directory=output_directory,
transaction_log=transaction_log, transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/test_regex_match_and_exclude.txt", transaction_log_summary_file_name="plugins/test_regex_match_and_exclude.txt",
regenerate_transaction_log=True,
) )
def test_regex_using_overrides_success(self, playlist_subscription_overrides, output_directory): def test_regex_using_overrides_success(self, playlist_subscription_overrides, output_directory):
@ -267,7 +266,6 @@ class TestRegex:
output_directory=output_directory, output_directory=output_directory,
transaction_log=transaction_log, transaction_log=transaction_log,
transaction_log_summary_file_name="plugins/test_regex_overrides.txt", transaction_log_summary_file_name="plugins/test_regex_overrides.txt",
regenerate_transaction_log=True,
) )
def test_regex_fails_no_match(self, playlist_subscription_no_match_fails, output_directory): def test_regex_fails_no_match(self, playlist_subscription_no_match_fails, output_directory):

View file

@ -43,7 +43,7 @@ class TestChannel:
expected md5 file hashes. expected md5 file hashes.
""" """
@pytest.mark.parametrize("dry_run", [True, False]) @pytest.mark.parametrize("dry_run", [False])
def test_full_channel_download( def test_full_channel_download(
self, self,
channel_as_tv_show_config, channel_as_tv_show_config,

View file

@ -1,10 +1,16 @@
from unittest.mock import patch
import pytest import pytest
from conftest import preset_dict_to_dl_args from conftest import preset_dict_to_dl_args
from e2e.conftest import mock_run_from_cli from e2e.conftest import mock_run_from_cli
from expected_download import assert_expected_downloads from expected_download import assert_expected_downloads
from expected_transaction_log import assert_transaction_log_matches from expected_transaction_log import assert_transaction_log_matches
from ytdl_sub.downloaders.ytdlp import YTDLP
from ytdl_sub.entries.entry import Entry
from ytdl_sub.subscriptions.subscription import Subscription from ytdl_sub.subscriptions.subscription import Subscription
from ytdl_sub.utils.file_handler import FileHandler
from ytdl_sub.utils.thumbnail import try_convert_download_thumbnail
@pytest.fixture @pytest.fixture
@ -114,7 +120,7 @@ class TestYoutubeVideo:
transaction_log_summary_file_name="youtube/test_video.txt", transaction_log_summary_file_name="youtube/test_video.txt",
) )
@pytest.mark.parametrize("dry_run", [True, False]) @pytest.mark.parametrize("dry_run", [True])
def test_single_video_download( def test_single_video_download(
self, self,
music_video_config, music_video_config,
@ -140,6 +146,44 @@ class TestYoutubeVideo:
expected_download_summary_file_name="youtube/test_video.json", expected_download_summary_file_name="youtube/test_video.json",
) )
def test_single_video_download_missing_thumbnail(
self,
music_video_config,
single_video_preset_dict,
working_directory,
output_directory,
):
single_video_subscription = Subscription.from_dict(
config=music_video_config,
preset_name="music_video_single_video_test",
preset_dict=single_video_preset_dict,
)
def delete_entry_thumb(entry: Entry) -> None:
FileHandler.delete(entry.get_download_thumbnail_path())
try_convert_download_thumbnail(entry=entry)
# Pretend the thumbnail did not download via returning nothing for its downloaded path
with patch.object(YTDLP, "_EXTRACT_ENTRY_NUM_RETRIES", 1), patch.object(
Entry, "try_get_ytdlp_download_thumbnail_path"
) as mock_ytdlp_path, patch(
"ytdl_sub.downloaders.url.downloader.try_convert_download_thumbnail",
side_effect=delete_entry_thumb,
):
mock_ytdlp_path.return_value = None
transaction_log = single_video_subscription.download(dry_run=False)
assert_transaction_log_matches(
output_directory=output_directory,
transaction_log=transaction_log,
transaction_log_summary_file_name="youtube/test_video_missing_thumb.txt",
)
assert_expected_downloads(
output_directory=output_directory,
dry_run=False,
expected_download_summary_file_name="youtube/test_video_missing_thumb.json",
)
@pytest.mark.parametrize("dry_run", [True, False]) @pytest.mark.parametrize("dry_run", [True, False])
def test_single_video_download_from_cli_dl( def test_single_video_download_from_cli_dl(
self, self,

View file

@ -124,7 +124,6 @@ def assert_expected_downloads(
dry_run: bool, dry_run: bool,
expected_download_summary_file_name: str, expected_download_summary_file_name: str,
ignore_md5_hashes_for: Optional[List[str]] = None, ignore_md5_hashes_for: Optional[List[str]] = None,
regenerate_expected_download_summary: bool = REGENERATE_FIXTURES,
): ):
if dry_run: if dry_run:
output_directory_contents = list(Path(output_directory).rglob("*")) output_directory_contents = list(Path(output_directory).rglob("*"))
@ -134,7 +133,11 @@ def assert_expected_downloads(
return return
summary_full_path = _EXPECTED_DOWNLOADS_SUMMARY_PATH / expected_download_summary_file_name summary_full_path = _EXPECTED_DOWNLOADS_SUMMARY_PATH / expected_download_summary_file_name
if regenerate_expected_download_summary:
# 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.
if REGENERATE_FIXTURES:
os.makedirs(os.path.dirname(summary_full_path), exist_ok=True) os.makedirs(os.path.dirname(summary_full_path), exist_ok=True)
ExpectedDownloads.from_directory(directory_path=output_directory).to_summary_file( ExpectedDownloads.from_directory(directory_path=output_directory).to_summary_file(
summary_file_path=summary_full_path summary_file_path=summary_full_path

View file

@ -14,7 +14,6 @@ def assert_transaction_log_matches(
output_directory: Path, output_directory: Path,
transaction_log: FileHandlerTransactionLog, transaction_log: FileHandlerTransactionLog,
transaction_log_summary_file_name: str, transaction_log_summary_file_name: str,
regenerate_transaction_log: bool = REGENERATE_FIXTURES,
): ):
""" """
Parameters Parameters
@ -26,16 +25,14 @@ def assert_transaction_log_matches(
transaction_log_summary_file_name transaction_log_summary_file_name
Name if the transaction log summary to compare. Name if the transaction log summary to compare.
Lives in tests/e2e/resources/transaction_log_summaries 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 transaction_log_path = _TRANSACTION_LOG_SUMMARY_PATH / transaction_log_summary_file_name
summary = transaction_log.to_output_message(output_directory=output_directory) summary = transaction_log.to_output_message(output_directory=output_directory)
# Write the expected summary file if regenerate is True # USE WITH CAUTION - MANUALLY INSPECT CHANGED FILES TO ENSURE THEY LOOK GOOD!
if regenerate_transaction_log: # Updates the file with the input transaction log. Should only be used to update
# tests after an expected change is made.
if REGENERATE_FIXTURES:
os.makedirs(os.path.dirname(transaction_log_path), exist_ok=True) os.makedirs(os.path.dirname(transaction_log_path), exist_ok=True)
with open(transaction_log_path, "w", encoding="utf-8") as summary_file: with open(transaction_log_path, "w", encoding="utf-8") as summary_file:
summary_file.write( summary_file.write(

View file

@ -1,12 +1,12 @@
{ {
"Project Zombie/.ytdl-sub-recent-download-archive.json": "ca7275efd7f2ebe489b8ca4007dc5361", "Project Zombie/.ytdl-sub-recent-download-archive.json": "ca7275efd7f2ebe489b8ca4007dc5361",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer-thumb.jpg": "705ca4e0d99b37e9ecdf6bfe4b90c59b", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer-thumb.jpg": "705ca4e0d99b37e9ecdf6bfe4b90c59b",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.info.json": "70dc8e603978c4ac2438e0a76a14d91a", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.info.json": "f249bd1137702b3db3d2805bbf4bfff8",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.mp4": "dbb140af8676f34fb701d5199e8708ea", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.mp4": "dbb140af8676f34fb701d5199e8708ea",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.nfo": "ee5bee94667ed4f1d47af08384418d3c", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.nfo": "ee5bee94667ed4f1d47af08384418d3c",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json": "39aea44e1987ed4950ef81661b70123f", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json": "e143a26148069d0477dc45896afba93e",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4": "083e2e77e3e01218419531c08795c9c0", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4": "a1b58a20411fff51ba06c1fc31cadf1f",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e",
"Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee",
"Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", "Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73",

View file

@ -1,8 +1,8 @@
{ {
"Project Zombie/.ytdl-sub-recent-download-archive.json": "46170edaf7cc51e5f59ef5d4c0476200", "Project Zombie/.ytdl-sub-recent-download-archive.json": "46170edaf7cc51e5f59ef5d4c0476200",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json": "39aea44e1987ed4950ef81661b70123f", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json": "e143a26148069d0477dc45896afba93e",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4": "083e2e77e3e01218419531c08795c9c0", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4": "a1b58a20411fff51ba06c1fc31cadf1f",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "c2f06f9eb5b42c777808a35587e5941e",
"Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee",
"Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", "Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73",

View file

@ -1,53 +1,53 @@
{ {
"Project Zombie/.ytdl-sub-pz-download-archive.json": "aadb59c92dcf14ee6617c77423a14584", "Project Zombie/.ytdl-sub-pz-download-archive.json": "aadb59c92dcf14ee6617c77423a14584",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1-thumb.jpg": "fb95b510681676e81c321171fc23143e",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.info.json": "0e9453839a22b6a76b51790fbbb23454", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.info.json": "71a73acadb2b22b03e694450e24829c3",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.mp4": "6aab51d2ebb374dc965970cbacd00bda", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.mp4": "6aab51d2ebb374dc965970cbacd00bda",
"Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.nfo": "a1970f06fbc4743fca6db0627de779f3", "Project Zombie/Season 2010/s2010.e081301 - Oblivion Mod Falcor p.1.nfo": "a1970f06fbc4743fca6db0627de779f3",
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2-thumb.jpg": "8b32ee9c037fa669e444a0ac181525a1", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2-thumb.jpg": "8b32ee9c037fa669e444a0ac181525a1",
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.info.json": "adc538d5568b311d6b71bb2a3c2f8ec2", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.info.json": "eed64444efcd28fd011355c99f9f0b11",
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.mp4": "4433a9932f0c3935cd9d339e281f38a9", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.mp4": "4433a9932f0c3935cd9d339e281f38a9",
"Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.nfo": "4ad498ce223454a4baa7d64bb4a837d6", "Project Zombie/Season 2010/s2010.e120201 - Oblivion Mod Falcor p.2.nfo": "4ad498ce223454a4baa7d64bb4a837d6",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1]-thumb.jpg": "b232d253df621aa770b780c1301d364d",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "ebf6e5c5ba1806d69cc8eb66fa53a2f9", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].info.json": "2d0fd09ada04988e971680de3bbb16a3",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "36622668231135f717456ef960fc7a68", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].mp4": "36622668231135f717456ef960fc7a68",
"Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "073eefa6e5c6d76edde80258ddf452ee", "Project Zombie/Season 2011/s2011.e020101 - Jesse's Minecraft Server [Trailer - Feb.1].nfo": "073eefa6e5c6d76edde80258ddf452ee",
"Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb", "Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27]-thumb.jpg": "d17c379ea8b362f5b97c6b213b0342cb",
"Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "0d687426300432ffc94460d5ac90200d", "Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].info.json": "df97680cd9c492b81586530f6eae7692",
"Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].mp4": "692f5928b18d6d70db7b5314e08cdfd3", "Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].mp4": "692f5928b18d6d70db7b5314e08cdfd3",
"Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].nfo": "20af231e30a035fb2bc0c946f4b4026d", "Project Zombie/Season 2011/s2011.e022701 - Jesse's Minecraft Server [Trailer - Feb.27].nfo": "20af231e30a035fb2bc0c946f4b4026d",
"Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530", "Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21]-thumb.jpg": "e7830aa8a64b0cde65ba3f7e5fc56530",
"Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].info.json": "e03526cb4b91114dc0bfa4d75a479da4", "Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].info.json": "34221ff34f0449cce61004dd91e7bd7c",
"Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].mp4": "f5cb35da0e2ecedee9b74db1402be06d", "Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].mp4": "f5cb35da0e2ecedee9b74db1402be06d",
"Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].nfo": "f8bdc463c0cb2ffdc82aba2bcc27ad5d", "Project Zombie/Season 2011/s2011.e032101 - Jesse's Minecraft Server [Trailer - Mar.21].nfo": "f8bdc463c0cb2ffdc82aba2bcc27ad5d",
"Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net)-thumb.jpg": "c956192a379b3661595c9920972d4819", "Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net)-thumb.jpg": "c956192a379b3661595c9920972d4819",
"Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net).info.json": "48964586b9f85ac49caf1887158c23ab", "Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net).info.json": "d8442a2e3d84f88bdb7adb8a123cc676",
"Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net).mp4": "9766181630b30e94a22e571c70a065f4", "Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net).mp4": "9766181630b30e94a22e571c70a065f4",
"Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net).nfo": "cb0184784a8eda842cfaf851f6e6af7d", "Project Zombie/Season 2011/s2011.e052901 - Project Zombie Official Trailer (IP mc.projectzombie.beastnode.net).nfo": "cb0184784a8eda842cfaf851f6e6af7d",
"Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin-thumb.jpg": "00ed383591779ffe98291de60f198fe9", "Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin-thumb.jpg": "00ed383591779ffe98291de60f198fe9",
"Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin.info.json": "20510db3688622932727bf5dcd0a2200", "Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin.info.json": "f4c4808ca277ac254ce59669f5da95bd",
"Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin.mp4": "2d6a06b4b8c4dac8d7c173362d9637a4", "Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin.mp4": "2d6a06b4b8c4dac8d7c173362d9637a4",
"Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin.nfo": "54ea4a48116aa98480a79495036c25e9", "Project Zombie/Season 2011/s2011.e063001 - Project Zombie Fin.nfo": "54ea4a48116aa98480a79495036c25e9",
"Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC]-thumb.jpg": "1718599d5189c65f7d8cf6acfa5ea851", "Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC]-thumb.jpg": "1718599d5189c65f7d8cf6acfa5ea851",
"Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC].info.json": "8df32d858656fe8c684cf50ddf16b4cf", "Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC].info.json": "0605441961b2692d3ccfd8b30e9ac345",
"Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC].mp4": "60c3221125afbef78990bdcead78a82d", "Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC].mp4": "60c3221125afbef78990bdcead78a82d",
"Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC].nfo": "04f6aad56d85b1f65b81b6b8000f6479", "Project Zombie/Season 2011/s2011.e112101 - Skyrim 'Ultra HD wMods' [PC].nfo": "04f6aad56d85b1f65b81b6b8000f6479",
"Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer-thumb.jpg": "54ebe9df801b278fdd17b21afa8373a6", "Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer-thumb.jpg": "54ebe9df801b278fdd17b21afa8373a6",
"Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer.info.json": "dee712d394eea246a3b4bba436a8d7c3", "Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer.info.json": "3ac8c6130454dc8628d1954038ef1611",
"Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer.mp4": "2141f0f928d55e94fef7f7d3c7a4aca0", "Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer.mp4": "2141f0f928d55e94fef7f7d3c7a4aca0",
"Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer.nfo": "e2b078891b27cfee4c791598d046be24", "Project Zombie/Season 2012/s2012.e012301 - Project Zombie Map Trailer.nfo": "e2b078891b27cfee4c791598d046be24",
"Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer-thumb.jpg": "e29d49433175de8a761af35c5307791f", "Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer-thumb.jpg": "e29d49433175de8a761af35c5307791f",
"Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer.info.json": "255c5045b149c446267d6a91c0e5ebb5", "Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer.info.json": "de10297ea3c72c111741ea06c7e07ec3",
"Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer.mp4": "e092a7ecbe10a02556487db8cb4e1158", "Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer.mp4": "e092a7ecbe10a02556487db8cb4e1158",
"Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer.nfo": "ce72e3e210d6d46fef3e0bc208dc69cf", "Project Zombie/Season 2013/s2013.e071901 - Project Zombie Rewind Trailer.nfo": "ce72e3e210d6d46fef3e0bc208dc69cf",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer-thumb.jpg": "705ca4e0d99b37e9ecdf6bfe4b90c59b", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer-thumb.jpg": "705ca4e0d99b37e9ecdf6bfe4b90c59b",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.info.json": "6f6ffbb3118ea6b0485b3816b6e4bba1", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.info.json": "ffb2dbd264a02848ddea1a1cba96e459",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.mp4": "dbb140af8676f34fb701d5199e8708ea", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.mp4": "dbb140af8676f34fb701d5199e8708ea",
"Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.nfo": "31e92fc23d9765f8d5bd0d64834a9d1f", "Project Zombie/Season 2018/s2018.e102901 - Jesse's Minecraft Server Teaser Trailer.nfo": "31e92fc23d9765f8d5bd0d64834a9d1f",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id-thumb.jpg": "28d852ede73b879b9ebf9a061cfc7d46",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.en.srt": "3d2c4e7f65d2ca5e96da38ce7eecfc4e", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.en.srt": "3d2c4e7f65d2ca5e96da38ce7eecfc4e",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json": "a1ec296adf1cfbe7329fa16dde78ebc3", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.info.json": "59d77abf90db5f3ec96a2a77ee503d18",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4": "083e2e77e3e01218419531c08795c9c0", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.mp4": "a1b58a20411fff51ba06c1fc31cadf1f",
"Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "a9b7ac3cacbda7fed9d12b0519945292", "Project Zombie/Season 2018/s2018.e110201 - Jesse's Minecraft Server IP mc.jesse.id.nfo": "a9b7ac3cacbda7fed9d12b0519945292",
"Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee", "Project Zombie/fanart.jpg": "129c6639b47299bc48062f0365e670ee",
"Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73", "Project Zombie/poster.jpg": "5de28eea5a921a041452ab3ce1041f73",

View file

@ -0,0 +1,5 @@
{
"JMC/JMC - Oblivion Mod Falcor p.1.info.json": "4b361d0ab0220407731553dbe36f12a1",
"JMC/JMC - Oblivion Mod Falcor p.1.mp4": "3744c49f2e447bd7712a5aad5ed36be2",
"JMC/JMC - Oblivion Mod Falcor p.1.nfo": "24cc4e17d2bebc89b2759ce5471d403e"
}

View file

@ -0,0 +1,14 @@
Files created:
----------------------------------------
{output_directory}/JMC
JMC - Oblivion Mod Falcor p.1.info.json
JMC - Oblivion Mod Falcor p.1.mp4
Video Tags:
title: Oblivion Mod "Falcor" p.1
JMC - Oblivion Mod Falcor p.1.nfo
NFO tags:
musicvideo:
album: Music Videos
artist: JMC
title: Oblivion Mod "Falcor" p.1
year: 2010