Skip to content

Instantly share code, notes, and snippets.

@yanli0303
Last active December 20, 2025 17:40
Show Gist options
  • Select an option

  • Save yanli0303/a38d08cd334fa4cedd930aa2aa032ec3 to your computer and use it in GitHub Desktop.

Select an option

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
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)
@yanli0303
Copy link
Author

yanli0303 commented Aug 29, 2025

uv run https://gist.github.com/yanli0303/a38d08cd334fa4cedd930aa2aa032ec3/
Usage: python3 rename_photos.py <directory>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment