Last active
December 20, 2025 17:40
-
-
Save yanli0303/a38d08cd334fa4cedd930aa2aa032ec3 to your computer and use it in GitHub Desktop.
Rename pixel photo/video files to YYYY-MM-DD_HH-mm-SS_sss.jpg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import sys | |
| from datetime import datetime, timedelta, timezone | |
| def is_eastern_dst(dt: datetime) -> bool: | |
| """ | |
| Check if a datetime is during Daylight Saving Time in US Eastern timezone. | |
| DST starts on the second Sunday in March at 2:00 AM. | |
| DST ends on the first Sunday in November at 2:00 AM. | |
| """ | |
| year = dt.year | |
| # Find the second Sunday in March | |
| march_first = datetime(year, 3, 1, tzinfo=timezone.utc) | |
| days_until_sunday = (6 - march_first.weekday()) % 7 | |
| if days_until_sunday == 0: | |
| days_until_sunday = 7 | |
| first_sunday_march = march_first + timedelta(days=days_until_sunday) | |
| second_sunday_march = first_sunday_march + timedelta(days=7) | |
| dst_start = second_sunday_march.replace(hour=7, minute=0, second=0, microsecond=0) # 2 AM EST = 7 AM UTC | |
| # Find the first Sunday in November | |
| november_first = datetime(year, 11, 1, tzinfo=timezone.utc) | |
| days_until_sunday = (6 - november_first.weekday()) % 7 | |
| if days_until_sunday == 0: | |
| days_until_sunday = 7 | |
| first_sunday_november = november_first + timedelta(days=days_until_sunday) | |
| dst_end = first_sunday_november.replace(hour=6, minute=0, second=0, microsecond=0) # 2 AM EDT = 6 AM UTC | |
| # Convert input to UTC for comparison | |
| dt_utc = dt.astimezone(timezone.utc) if dt.tzinfo else dt.replace(tzinfo=timezone.utc) | |
| return dst_start <= dt_utc < dst_end | |
| def get_eastern_tz(dt: datetime) -> timezone: | |
| """Get the appropriate Eastern timezone (EST or EDT) for a given datetime.""" | |
| if is_eastern_dst(dt): | |
| return timezone(timedelta(hours=-4), name="EDT") # Eastern Daylight Time | |
| else: | |
| return timezone(timedelta(hours=-5), name="EST") # Eastern Standard Time | |
| def eastern(dt: int | float | datetime | None = None) -> datetime | None: | |
| """Convert a timestamp or datetime to US Eastern timezone (DST-aware).""" | |
| if isinstance(dt, datetime): | |
| tz = get_eastern_tz(dt) | |
| return dt.astimezone(tz) | |
| if isinstance(dt, (int, float)): | |
| year_2000_ms = 946684800000 # January 1, 2000, in milliseconds | |
| year_2100_ms = 4102444800000 # January 1, 2100, in milliseconds | |
| year_2000_s = 946684800 # January 1, 2000, in seconds | |
| year_2100_s = 4102444800 # January 1, 2100, in seconds | |
| if year_2000_ms < dt < year_2100_ms: | |
| seconds = dt / 1000.0 | |
| elif year_2000_s < dt < year_2100_s: | |
| seconds = dt | |
| else: | |
| return None | |
| utc_dt = datetime.fromtimestamp(seconds, tz=timezone.utc) | |
| tz = get_eastern_tz(utc_dt) | |
| return utc_dt.astimezone(tz) | |
| now_utc = datetime.now(tz=timezone.utc) | |
| tz = get_eastern_tz(now_utc) | |
| return now_utc.astimezone(tz) | |
| def stringify_dt(dt: datetime) -> str: | |
| """Format a datetime into 'YYYY-MM-DD_HH-MM-SS_SSS' format.""" | |
| milliseconds = dt.microsecond // 1000 | |
| formatted = dt.strftime("%Y-%m-%d_%H-%M-%S") | |
| return f"{formatted}_{milliseconds:03d}" | |
| def parse_dt(filename: str) -> datetime | None: | |
| digits = [char for char in filename if char.isdigit()] | |
| year = int("".join(digits[:4])) | |
| month = int("".join(digits[4:6])) | |
| day = int("".join(digits[6:8])) | |
| hour = int("".join(digits[8:10])) | |
| minute = int("".join(digits[10:12])) | |
| second = int("".join(digits[12:14])) | |
| millisecond = int("".join(digits[14:17])) | |
| try: | |
| dt = datetime(year, month, day, hour, minute, second, millisecond * 1000) | |
| return dt | |
| except ValueError: | |
| return None | |
| def rename_file(filename: str) -> str | None: | |
| name, extension = os.path.splitext(filename) | |
| digits = [char for char in name if char.isdigit()] | |
| if not digits: | |
| return None | |
| # Try parsing as unix timestamp first | |
| all_digits_int = int("".join(digits)) | |
| dt = eastern(all_digits_int) | |
| if dt is not None: | |
| return stringify_dt(dt) + extension | |
| # Try parsing as YYYYMMDD[HHMMSS[SSS]] | |
| if len(digits) < 8: # Need at least YYYYMMDD | |
| return None | |
| # Check year range | |
| year = int("".join(digits[:4])) | |
| if not (1984 <= year <= 2099): | |
| return None | |
| # Validate month | |
| month = int("".join(digits[4:6])) | |
| if not (1 <= month <= 12): | |
| return None | |
| # Validate day | |
| day = int("".join(digits[6:8])) | |
| if not (1 <= day <= 31): | |
| return None | |
| # Parse optional time components (default to 0) | |
| hour = 0 | |
| minute = 0 | |
| second = 0 | |
| millisecond = 0 | |
| if len(digits) >= 10: | |
| hour = int("".join(digits[8:10])) | |
| if not (0 <= hour <= 23): | |
| return None | |
| if len(digits) >= 12: | |
| minute = int("".join(digits[10:12])) | |
| if not (0 <= minute <= 59): | |
| return None | |
| if len(digits) >= 14: | |
| second = int("".join(digits[12:14])) | |
| if not (0 <= second <= 59): | |
| return None | |
| if len(digits) >= 17: | |
| millisecond = int("".join(digits[14:17])) | |
| if not (0 <= millisecond <= 999): | |
| return None | |
| # Create datetime - this validates day for the given month/year | |
| try: | |
| dt = datetime(year, month, day, hour, minute, second, millisecond * 1000) | |
| except ValueError: | |
| return None | |
| return stringify_dt(dt) + extension | |
| def add_one_millisecond(timestamp_filename: str) -> str: | |
| name, extension = os.path.splitext(timestamp_filename) | |
| dt = parse_dt(name) | |
| if dt is None: | |
| return timestamp_filename # Unable to parse datetime | |
| dt += timedelta(milliseconds=1) | |
| return stringify_dt(dt) + extension | |
| def rename_files_in_dir(dirname: str) -> None: | |
| for old_filename in os.listdir(dirname): | |
| old_path = os.path.join(dirname, old_filename) | |
| new_filename = rename_file(old_filename) | |
| if new_filename is None: | |
| continue | |
| new_path = os.path.join(dirname, new_filename) | |
| if new_path == old_path: | |
| continue | |
| while os.path.exists(new_path): | |
| new_filename = add_one_millisecond(new_filename) | |
| new_path = os.path.join(dirname, new_filename) | |
| os.rename(old_path, new_path) | |
| print(f"{old_filename} --> {new_filename}") | |
| if __name__ == "__main__": | |
| if len(sys.argv) < 2: | |
| print("Usage: python3 rename_photos.py <directory>") | |
| sys.exit(1) | |
| directory = sys.argv[1] | |
| rename_files_in_dir(directory) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.