move preserve_mtime to output_options
This commit is contained in:
parent
c4b79893b4
commit
5599c912d9
9 changed files with 55 additions and 22 deletions
|
|
@ -763,6 +763,15 @@ Defines where to output files and thumbnails after all post-processing has compl
|
||||||
The output directory to store all media files downloaded.
|
The output directory to store all media files downloaded.
|
||||||
|
|
||||||
|
|
||||||
|
``preserve_mtime``
|
||||||
|
|
||||||
|
:expected type: Optional[Boolean]
|
||||||
|
:description:
|
||||||
|
Preserve the video's original upload time as the file modification time.
|
||||||
|
When True, sets the file's mtime to match the video's upload_date from
|
||||||
|
yt-dlp metadata. Defaults to False.
|
||||||
|
|
||||||
|
|
||||||
``thumbnail_name``
|
``thumbnail_name``
|
||||||
|
|
||||||
:expected type: Optional[EntryFormatter]
|
:expected type: Optional[EntryFormatter]
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__pypi_version__ = "2023.10.22.post3"
|
__pypi_version__ = "2025.11.14"
|
||||||
__local_version__ = "2023.10.22+bfba4f0"
|
__local_version__ = "2025.11.14+c4b7989"
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,6 @@ class ConfigOptions(StrictDictValidator):
|
||||||
"ffprobe_path",
|
"ffprobe_path",
|
||||||
"file_name_max_bytes",
|
"file_name_max_bytes",
|
||||||
"experimental",
|
"experimental",
|
||||||
"preserve_mtime",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, name: str, value: Any):
|
def __init__(self, name: str, value: Any):
|
||||||
|
|
@ -147,9 +146,6 @@ class ConfigOptions(StrictDictValidator):
|
||||||
self._file_name_max_bytes = self._validate_key(
|
self._file_name_max_bytes = self._validate_key(
|
||||||
key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
|
key="file_name_max_bytes", validator=IntValidator, default=MAX_FILE_NAME_BYTES
|
||||||
)
|
)
|
||||||
self._preserve_mtime = self._validate_key(
|
|
||||||
key="preserve_mtime", validator=BoolValidator, default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def working_directory(self) -> str:
|
def working_directory(self) -> str:
|
||||||
|
|
@ -244,15 +240,6 @@ class ConfigOptions(StrictDictValidator):
|
||||||
"""
|
"""
|
||||||
return self._ffprobe_path.value
|
return self._ffprobe_path.value
|
||||||
|
|
||||||
@property
|
|
||||||
def preserve_mtime(self) -> bool:
|
|
||||||
"""
|
|
||||||
Preserve the video's original upload time as the file modification time.
|
|
||||||
When True, sets the file's mtime to match the video's upload_date from
|
|
||||||
yt-dlp metadata. (default ``False``)
|
|
||||||
"""
|
|
||||||
return self._preserve_mtime.value
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigValidator(StrictDictValidator):
|
class ConfigValidator(StrictDictValidator):
|
||||||
_optional_keys = {"configuration", "presets"}
|
_optional_keys = {"configuration", "presets"}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ class OutputOptions(OptionsDictValidator):
|
||||||
"keep_max_files",
|
"keep_max_files",
|
||||||
"download_archive_standardized_date",
|
"download_archive_standardized_date",
|
||||||
"keep_files_date_eval",
|
"keep_files_date_eval",
|
||||||
|
"preserve_mtime",
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -170,6 +171,10 @@ class OutputOptions(OptionsDictValidator):
|
||||||
default=f"{{{v.upload_date_standardized.variable_name}}}",
|
default=f"{{{v.upload_date_standardized.variable_name}}}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._preserve_mtime = self._validate_key_if_present(
|
||||||
|
key="preserve_mtime", validator=BoolValidator, default=False
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self._keep_files_before or self._keep_files_after or self._keep_max_files
|
self._keep_files_before or self._keep_files_after or self._keep_max_files
|
||||||
) and not self.maintain_download_archive:
|
) and not self.maintain_download_archive:
|
||||||
|
|
@ -309,6 +314,17 @@ class OutputOptions(OptionsDictValidator):
|
||||||
"""
|
"""
|
||||||
return self._keep_max_files
|
return self._keep_max_files
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preserve_mtime(self) -> bool:
|
||||||
|
"""
|
||||||
|
:expected type: Optional[Boolean]
|
||||||
|
:description:
|
||||||
|
Preserve the video's original upload time as the file modification time.
|
||||||
|
When True, sets the file's mtime to match the video's upload_date from
|
||||||
|
yt-dlp metadata. Defaults to False.
|
||||||
|
"""
|
||||||
|
return self._preserve_mtime.value
|
||||||
|
|
||||||
def added_variables(self, unresolved_variables: Set[str]) -> Dict[PluginOperation, Set[str]]:
|
def added_variables(self, unresolved_variables: Set[str]) -> Dict[PluginOperation, Set[str]]:
|
||||||
return {
|
return {
|
||||||
# PluginOperation.MODIFY_ENTRY_METADATA: {
|
# PluginOperation.MODIFY_ENTRY_METADATA: {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ def _initialize_download_archive(
|
||||||
overrides: Overrides,
|
overrides: Overrides,
|
||||||
working_directory: str,
|
working_directory: str,
|
||||||
output_directory: str,
|
output_directory: str,
|
||||||
config_options: ConfigOptions,
|
|
||||||
) -> EnhancedDownloadArchive:
|
) -> EnhancedDownloadArchive:
|
||||||
migrated_file_name: Optional[str] = None
|
migrated_file_name: Optional[str] = None
|
||||||
if migrated_file_name_option := output_options.migrated_download_archive_name:
|
if migrated_file_name_option := output_options.migrated_download_archive_name:
|
||||||
|
|
@ -33,7 +32,6 @@ def _initialize_download_archive(
|
||||||
working_directory=working_directory,
|
working_directory=working_directory,
|
||||||
output_directory=output_directory,
|
output_directory=output_directory,
|
||||||
migrated_file_name=migrated_file_name,
|
migrated_file_name=migrated_file_name,
|
||||||
preserve_mtime=config_options.preserve_mtime,
|
|
||||||
).reinitialize(dry_run=True)
|
).reinitialize(dry_run=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -82,7 +80,6 @@ class BaseSubscription(ABC):
|
||||||
overrides=self.overrides,
|
overrides=self.overrides,
|
||||||
working_directory=self.working_directory,
|
working_directory=self.working_directory,
|
||||||
output_directory=self.output_directory,
|
output_directory=self.output_directory,
|
||||||
config_options=self._config_options,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
||||||
file_metadata=entry_metadata,
|
file_metadata=entry_metadata,
|
||||||
output_file_name=output_file_name,
|
output_file_name=output_file_name,
|
||||||
entry=entry,
|
entry=entry,
|
||||||
|
preserve_mtime=self.output_options.preserve_mtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Always pretend to include the thumbnail in a dry-run
|
# Always pretend to include the thumbnail in a dry-run
|
||||||
|
|
@ -87,6 +88,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
||||||
output_file_name=output_thumbnail_name,
|
output_file_name=output_thumbnail_name,
|
||||||
entry=entry,
|
entry=entry,
|
||||||
copy_file=True,
|
copy_file=True,
|
||||||
|
preserve_mtime=self.output_options.preserve_mtime,
|
||||||
)
|
)
|
||||||
elif not entry.is_thumbnail_downloaded():
|
elif not entry.is_thumbnail_downloaded():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
@ -106,6 +108,7 @@ class SubscriptionDownload(BaseSubscription, ABC):
|
||||||
file_name=entry.get_download_info_json_name(),
|
file_name=entry.get_download_info_json_name(),
|
||||||
output_file_name=output_info_json_name,
|
output_file_name=output_info_json_name,
|
||||||
entry=entry,
|
entry=entry,
|
||||||
|
preserve_mtime=self.output_options.preserve_mtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _delete_working_directory(self, is_error: bool = False) -> None:
|
def _delete_working_directory(self, is_error: bool = False) -> None:
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,7 @@ class FileHandler:
|
||||||
# Perform the copy by first writing to a temp file, then moving it.
|
# Perform the copy by first writing to a temp file, then moving it.
|
||||||
# This tries to prevent corrupted writes if the processed dies mid-write,
|
# This tries to prevent corrupted writes if the processed dies mid-write,
|
||||||
atomic_dst = f"{dst_file_path}-ytdl-sub-incomplete"
|
atomic_dst = f"{dst_file_path}-ytdl-sub-incomplete"
|
||||||
shutil.copy2(src=src_file_path, dst=atomic_dst)
|
shutil.copyfile(src=src_file_path, dst=atomic_dst)
|
||||||
shutil.move(src=atomic_dst, dst=dst_file_path)
|
shutil.move(src=atomic_dst, dst=dst_file_path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -398,7 +398,6 @@ class EnhancedDownloadArchive:
|
||||||
output_directory: str,
|
output_directory: str,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
migrated_file_name: Optional[str] = None,
|
migrated_file_name: Optional[str] = None,
|
||||||
preserve_mtime: bool = False,
|
|
||||||
):
|
):
|
||||||
self._file_name = file_name
|
self._file_name = file_name
|
||||||
self._file_handler = FileHandler(
|
self._file_handler = FileHandler(
|
||||||
|
|
@ -406,7 +405,6 @@ class EnhancedDownloadArchive:
|
||||||
)
|
)
|
||||||
self._download_mapping = DownloadMappings() # gets reinitialized
|
self._download_mapping = DownloadMappings() # gets reinitialized
|
||||||
self._migrated_file_name = migrated_file_name
|
self._migrated_file_name = migrated_file_name
|
||||||
self._preserve_mtime = preserve_mtime
|
|
||||||
|
|
||||||
self.num_entries_added: int = 0
|
self.num_entries_added: int = 0
|
||||||
self.num_entries_modified: int = 0
|
self.num_entries_modified: int = 0
|
||||||
|
|
@ -645,6 +643,7 @@ class EnhancedDownloadArchive:
|
||||||
output_file_name: Optional[str] = None,
|
output_file_name: Optional[str] = None,
|
||||||
entry: Optional[Entry] = None,
|
entry: Optional[Entry] = None,
|
||||||
copy_file: bool = False,
|
copy_file: bool = False,
|
||||||
|
preserve_mtime: bool = False,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Saves a file from the working directory to the output directory and record it in the
|
Saves a file from the working directory to the output directory and record it in the
|
||||||
|
|
@ -663,6 +662,8 @@ class EnhancedDownloadArchive:
|
||||||
Optional. Entry that this file belongs to
|
Optional. Entry that this file belongs to
|
||||||
copy_file
|
copy_file
|
||||||
Optional. If True, copy the file. Move otherwise
|
Optional. If True, copy the file. Move otherwise
|
||||||
|
preserve_mtime
|
||||||
|
Optional. If True and entry has upload_date, set file mtime to upload date
|
||||||
"""
|
"""
|
||||||
if output_file_name is None:
|
if output_file_name is None:
|
||||||
output_file_name = file_name
|
output_file_name = file_name
|
||||||
|
|
@ -678,7 +679,7 @@ class EnhancedDownloadArchive:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set mtime if preserve_mtime is enabled and we have an entry with upload_date
|
# Set mtime if preserve_mtime is enabled and we have an entry with upload_date
|
||||||
if self._preserve_mtime and entry and not self._file_handler.dry_run:
|
if preserve_mtime and entry and not self._file_handler.dry_run:
|
||||||
upload_date = entry.get(v.upload_date, str)
|
upload_date = entry.get(v.upload_date, str)
|
||||||
if upload_date:
|
if upload_date:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,26 @@ class TestPreset:
|
||||||
== "today-2months"
|
== "today-2months"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_preset_preserve_mtime_option(self, config_file, youtube_video, output_options):
|
||||||
|
# Test preserve_mtime defaults to False
|
||||||
|
preset_default = Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test_default",
|
||||||
|
value={"download": youtube_video, "output_options": output_options},
|
||||||
|
)
|
||||||
|
assert preset_default.output_options.preserve_mtime is False
|
||||||
|
|
||||||
|
# Test preserve_mtime can be set to True
|
||||||
|
preset_enabled = Preset(
|
||||||
|
config=config_file,
|
||||||
|
name="test_enabled",
|
||||||
|
value={
|
||||||
|
"download": youtube_video,
|
||||||
|
"output_options": dict(output_options, **{"preserve_mtime": True}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert preset_enabled.output_options.preserve_mtime is True
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"parent_preset", ["preset_self_loop", "preset_loop_0", "preset_loop_1"]
|
"parent_preset", ["preset_self_loop", "preset_loop_0", "preset_loop_1"]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue