diff --git a/examples/soundcloud_discography_config.yaml b/examples/soundcloud_discography_config.yaml index 13304eb4..5b10b3c8 100644 --- a/examples/soundcloud_discography_config.yaml +++ b/examples/soundcloud_discography_config.yaml @@ -43,9 +43,9 @@ presets: # store previously downloaded song IDs to tell YTDL not to re-download # them on a successive invocation. output_options: - output_directory: "/{music_directory}/{artist_sanitized}" - file_name: "{album_directory_name}/{track_number_padded} - {title_sanitized}.{ext}" - thumbnail_name: "{album_directory_name}/folder.jpg" + output_directory: "{music_directory}" + file_name: "{artist_sanitized}/{album_directory_name}/{track_number_padded} - {title_sanitized}.{ext}" + thumbnail_name: "{artist_sanitized}/{album_directory_name}/folder.jpg" maintain_download_archive: True # For each song downloaded, populate the audio file with music tags. diff --git a/src/ytdl_sub/plugins/music_tags.py b/src/ytdl_sub/plugins/music_tags.py index c2b13b91..14724c37 100644 --- a/src/ytdl_sub/plugins/music_tags.py +++ b/src/ytdl_sub/plugins/music_tags.py @@ -1,8 +1,11 @@ +from typing import Dict + import mediafile from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.validators import StringValidator @@ -57,17 +60,28 @@ class MusicTagsPlugin(Plugin[MusicTagsOptions]): """ Tags the entry's audio file using values defined in the metadata options """ - audio_file = mediafile.MediaFile(entry.get_download_file_path()) - for tag, tag_formatter in self.plugin_options.tags.dict.items(): - if tag not in audio_file.fields(): + supported_fields = list(mediafile.MediaFile.sorted_fields()) + tags_to_write: Dict[str, str] = {} + for tag_name, tag_formatter in self.plugin_options.tags.dict.items(): + if tag_name not in supported_fields: + # TODO: Add support for custom fields self._logger.warning( "tag '%s' is not supported for %s files. Supported tags: %s", - tag, + tag_name, entry.ext, - ", ".join(audio_file.sorted_fields()), + ", ".join(sorted(supported_fields)), ) + continue tag_value = self.overrides.apply_formatter(formatter=tag_formatter, entry=entry) - setattr(audio_file, tag, tag_value) + tags_to_write[tag_name] = tag_value - audio_file.save() + # write the actual tags if its not a dry run + if not self.is_dry_run: + audio_file = mediafile.MediaFile(entry.get_download_file_path()) + for tag_name, tag_value in tags_to_write.items(): + setattr(audio_file, tag_name, tag_value) + audio_file.save() + + # report the tags written + return FileMetadata.from_dict(value_dict=tags_to_write, title="Music Tags:") diff --git a/src/ytdl_sub/plugins/nfo_tags.py b/src/ytdl_sub/plugins/nfo_tags.py index a3ad879d..f1b9705a 100644 --- a/src/ytdl_sub/plugins/nfo_tags.py +++ b/src/ytdl_sub/plugins/nfo_tags.py @@ -6,6 +6,7 @@ import dicttoxml from ytdl_sub.entries.entry import Entry from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.validators.string_formatter_validators import DictFormatterValidator from ytdl_sub.validators.string_formatter_validators import StringFormatterValidator @@ -116,3 +117,5 @@ class NfoTagsPlugin(Plugin[NfoTagsOptions]): # Archive the nfo's file name self.save_file(file_name=nfo_file_name, entry=entry) + + return FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") diff --git a/src/ytdl_sub/plugins/output_directory_nfo_tags.py b/src/ytdl_sub/plugins/output_directory_nfo_tags.py index 6028b8d8..e0aa7294 100644 --- a/src/ytdl_sub/plugins/output_directory_nfo_tags.py +++ b/src/ytdl_sub/plugins/output_directory_nfo_tags.py @@ -5,6 +5,7 @@ import dicttoxml from ytdl_sub.plugins.plugin import Plugin from ytdl_sub.plugins.plugin import PluginOptions +from ytdl_sub.utils.file_handler import FileMetadata from ytdl_sub.validators.string_formatter_validators import OverridesDictFormatterValidator from ytdl_sub.validators.string_formatter_validators import OverridesStringFormatterValidator @@ -106,3 +107,4 @@ class OutputDirectoryNfoTagsPlugin(Plugin[OutputDirectoryNfoTagsOptions]): nfo_file.write(xml) self.save_file(file_name=nfo_file_name) + return FileMetadata.from_dict(value_dict={nfo_root: nfo}, title="NFO tags:") diff --git a/src/ytdl_sub/plugins/plugin.py b/src/ytdl_sub/plugins/plugin.py index 869c950a..65e916da 100644 --- a/src/ytdl_sub/plugins/plugin.py +++ b/src/ytdl_sub/plugins/plugin.py @@ -46,6 +46,14 @@ class Plugin(Generic[PluginOptionsT], ABC): def working_directory(self) -> str: return self.__enhanced_download_archive.working_directory + @property + def output_directory(self) -> str: + return self.__enhanced_download_archive.output_directory + + @property + def is_dry_run(self) -> bool: + return self.__enhanced_download_archive.is_dry_run + def save_file(self, file_name: str, entry: Optional[Entry] = None) -> None: """ Saves a file in the working directory to the output directory. diff --git a/src/ytdl_sub/utils/file_handler.py b/src/ytdl_sub/utils/file_handler.py index f43664c1..5eaa9ac5 100644 --- a/src/ytdl_sub/utils/file_handler.py +++ b/src/ytdl_sub/utils/file_handler.py @@ -12,10 +12,31 @@ class FileMetadata: def __init__(self, metadata: Optional[List[str]] = None): self.metadata: List[str] = metadata if metadata else [] - def append(self, other: "FileMetadata") -> "FileMetadata": + def append(self, line: str) -> "FileMetadata": + self.metadata.append(line) + return self + + def extend(self, other: "FileMetadata") -> "FileMetadata": self.metadata.extend(other.metadata) return self + @classmethod + def from_dict(cls, value_dict: Dict[str, str], title: Optional[str] = None) -> "FileMetadata": + lines: List[str] = [] + if title is not None: + lines.append(title) + + def _recursive_add_dict_lines(rdict: Dict, indent: int): + for key, value in sorted(rdict.items()): + if isinstance(value, Dict): + _recursive_add_dict_lines(rdict=value, indent=indent + 2) + + _indent = " " * indent + lines.append(f"{_indent}{key}: {value}") + + _recursive_add_dict_lines(rdict=value_dict, indent=2) + return cls(metadata=lines) + class FileHandlerTransactionLog: """ diff --git a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py index dd57bddc..c637fa40 100644 --- a/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py +++ b/src/ytdl_sub/ytdl_additions/enhanced_download_archive.py @@ -373,6 +373,10 @@ class EnhancedDownloadArchive: self._download_mapping = DownloadMappings() return self + @property + def is_dry_run(self) -> bool: + return self._file_handler.dry_run + @property def archive_file_name(self) -> str: """ diff --git a/tests/e2e/soundcloud/test_soundcloud_discography.py b/tests/e2e/soundcloud/test_soundcloud_discography.py index 2679f772..488c5505 100644 --- a/tests/e2e/soundcloud/test_soundcloud_discography.py +++ b/tests/e2e/soundcloud/test_soundcloud_discography.py @@ -29,7 +29,7 @@ def subscription_dict(output_directory, subscription_name): "preset": "sc_discography", "soundcloud": {"url": "https://soundcloud.com/jessebannon"}, # override the output directory with our fixture-generated dir - "output_options": {"output_directory": output_directory + "/{artist_sanitized}"}, + "output_options": {"output_directory": output_directory}, # download the worst format so it is fast "ytdl_options": { "format": "worst[ext=mp3]", @@ -59,7 +59,7 @@ def expected_discography_download(): return ExpectedDownload( expected_md5_file_hashes={ # Download mapping - Path("j_b/.ytdl-sub-jb-download-archive.json"): "ae55de93b71267b5712c9a3d06c07c26", + Path(".ytdl-sub-jb-download-archive.json"): "1a99156e9ece62539fb2608416a07200", # Entry files (singles) Path("j_b/[2021] Baby Santana's Dorian Groove/01 - Baby Santana's Dorian Groove.mp3"): "bffbd558e12c6a9e029dc136a88342c4", @@ -99,3 +99,9 @@ class TestSoundcloudDiscography: ): discography_subscription.download() expected_discography_download.assert_files_exist(relative_directory=output_directory) + + def test_discography_dry_run( + self, discography_subscription, expected_discography_download, output_directory + ): + transaction_log = discography_subscription.download(dry_run=True) + expected_discography_download.assert_dry_run_files_logged(transaction_log=transaction_log) diff --git a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py index f0afb3f6..56a824a6 100644 --- a/tests/e2e/youtube/test_channel_as_kodi_tv_show.py +++ b/tests/e2e/youtube/test_channel_as_kodi_tv_show.py @@ -297,6 +297,12 @@ class TestChannelAsKodiTvShow: full_channel_subscription.download() expected_full_channel_download.assert_files_exist(relative_directory=output_directory) + def test_full_channel_dry_run( + self, full_channel_subscription, expected_full_channel_download, output_directory + ): + transaction_log = full_channel_subscription.download(dry_run=True) + expected_full_channel_download.assert_dry_run_files_logged(transaction_log=transaction_log) + def test_recent_channel_download( self, recent_channel_subscription, expected_recent_channel_download, output_directory ): @@ -311,6 +317,14 @@ class TestChannelAsKodiTvShow: recent_channel_subscription.download() expected_recent_channel_download.assert_files_exist(relative_directory=output_directory) + def test_recent_channel_dry_run( + self, recent_channel_subscription, expected_recent_channel_download, output_directory + ): + transaction_log = recent_channel_subscription.download(dry_run=True) + expected_recent_channel_download.assert_dry_run_files_logged( + transaction_log=transaction_log + ) + def test_recent_channel_download__no_vids_in_range( self, recent_channel_no_vids_in_range_subscription, @@ -328,6 +342,17 @@ class TestChannelAsKodiTvShow: 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 + ) + def test_rolling_recent_channel_download( self, recent_channel_subscription,