Try to preserve time

This commit is contained in:
e1ven 2025-11-13 15:09:52 -04:00
parent 08180c0412
commit 728413affd
4 changed files with 57 additions and 1 deletions

View file

@ -112,6 +112,7 @@ 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):
@ -146,6 +147,9 @@ 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:
@ -240,6 +244,15 @@ 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"}

View file

@ -22,6 +22,7 @@ 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:
@ -32,6 +33,7 @@ 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)
@ -80,6 +82,7 @@ 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,
) )
) )

View file

@ -2,7 +2,9 @@ import hashlib
import json import json
import os import os
import shutil import shutil
import time
from collections import defaultdict from collections import defaultdict
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import Dict from typing import Dict
@ -392,7 +394,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.copyfile(src=src_file_path, dst=atomic_dst) shutil.copy2(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
@ -430,6 +432,25 @@ class FileHandler:
if os.path.isfile(file_path): if os.path.isfile(file_path):
os.remove(file_path) os.remove(file_path)
@classmethod
def set_mtime(cls, file_path: Union[str, Path], mtime: float):
"""
Set the modification time of a file
Parameters
----------
file_path
Path to the file to modify
mtime
Modification time as a Unix timestamp
"""
try:
# Set both access time and modification time
os.utime(file_path, (mtime, mtime))
except OSError:
# If file operation fails, silently continue
pass
def move_file_to_output_directory( def move_file_to_output_directory(
self, self,
file_name: str, file_name: str,

View file

@ -1,6 +1,7 @@
import copy import copy
import json import json
import os.path import os.path
import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@ -397,6 +398,7 @@ 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(
@ -404,6 +406,7 @@ 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
@ -674,6 +677,22 @@ class EnhancedDownloadArchive:
copy_file=copy_file, copy_file=copy_file,
) )
# 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:
upload_date = entry.get(v.upload_date, str)
if upload_date:
try:
# Convert YYYYMMDD to timestamp
upload_datetime = datetime.strptime(upload_date, "%Y%m%d")
upload_timestamp = time.mktime(upload_datetime.timetuple())
# Set mtime on the output file
output_file_path = Path(self._file_handler.output_directory) / output_file_name
FileHandler.set_mtime(output_file_path, upload_timestamp)
except (ValueError, OSError):
# If date parsing or file operation fails, silently continue
pass
# Determine if it's the entry file by seeing if the file_name to move matches the entry # Determine if it's the entry file by seeing if the file_name to move matches the entry
# download file name # download file name
is_entry_file = entry and entry.get_download_file_name() == file_name is_entry_file = entry and entry.get_download_file_name() == file_name