Files
2026-03-21 17:16:32 +08:00

201 lines
5.7 KiB
Python

"""
Media timestamp utilities for embedding EXIF data and renaming files.
"""
import os
import logging
import shutil
from datetime import datetime
from typing import Optional
from Whatsapp_Chat_Exporter.data_model import TimeZone
logger = logging.getLogger(__name__)
# Optional imports for EXIF support
try:
import piexif
from PIL import Image
HAS_EXIF_SUPPORT = True
except ImportError:
HAS_EXIF_SUPPORT = False
def format_timestamp_for_filename(timestamp: float, timezone_offset: int = 0) -> str:
"""
Format a Unix timestamp for use in filenames.
Args:
timestamp: Unix timestamp (seconds)
timezone_offset: Hours offset from UTC
Returns:
Formatted string: YYYY-MM-DD_HH-MM-SS
"""
dt = datetime.fromtimestamp(timestamp, TimeZone(timezone_offset))
return dt.strftime("%Y-%m-%d_%H-%M-%S")
def format_timestamp_for_exif(timestamp: float, timezone_offset: int = 0) -> str:
"""
Format a Unix timestamp for EXIF DateTime fields.
Args:
timestamp: Unix timestamp (seconds)
timezone_offset: Hours offset from UTC
Returns:
Formatted string: YYYY:MM:DD HH:MM:SS (EXIF format)
"""
dt = datetime.fromtimestamp(timestamp, TimeZone(timezone_offset))
return dt.strftime("%Y:%m:%d %H:%M:%S")
def generate_timestamped_filename(
original_path: str,
timestamp: float,
timezone_offset: int = 0
) -> str:
"""
Generate a new filename with timestamp prefix.
Args:
original_path: Original file path
timestamp: Unix timestamp (seconds)
timezone_offset: Hours offset from UTC
Returns:
New filename with format: YYYY-MM-DD_HH-MM-SS_original-name.ext
"""
directory = os.path.dirname(original_path)
original_name = os.path.basename(original_path)
timestamp_prefix = format_timestamp_for_filename(timestamp, timezone_offset)
new_name = f"{timestamp_prefix}_{original_name}"
return os.path.join(directory, new_name)
def embed_exif_timestamp(
file_path: str,
timestamp: float,
timezone_offset: int = 0
) -> bool:
"""
Embed timestamp in EXIF data for supported image formats.
Args:
file_path: Path to the image file
timestamp: Unix timestamp (seconds)
timezone_offset: Hours offset from UTC
Returns:
True if successful, False otherwise
"""
if not HAS_EXIF_SUPPORT:
logger.warning("EXIF support not available. Install piexif and Pillow.")
return False
# Check file extension
ext = os.path.splitext(file_path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.tiff', '.tif'):
logger.debug(f"EXIF embedding not supported for {ext} files: {file_path}")
return False
try:
exif_datetime = format_timestamp_for_exif(timestamp, timezone_offset)
exif_datetime_bytes = exif_datetime.encode('utf-8')
# Try to load existing EXIF data
try:
exif_dict = piexif.load(file_path)
except Exception:
# No existing EXIF, create empty structure
exif_dict = {"0th": {}, "Exif": {}, "GPS": {}, "1st": {}, "thumbnail": None}
# Set DateTime fields in Exif IFD
exif_dict["Exif"][piexif.ExifIFD.DateTimeOriginal] = exif_datetime_bytes
exif_dict["Exif"][piexif.ExifIFD.DateTimeDigitized] = exif_datetime_bytes
# Set DateTime in 0th IFD (basic TIFF tag)
exif_dict["0th"][piexif.ImageIFD.DateTime] = exif_datetime_bytes
# Dump and insert EXIF data
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, file_path)
return True
except Exception as e:
logger.warning(f"Failed to embed EXIF in {file_path}: {e}")
return False
def _handle_duplicate_filename(file_path: str) -> str:
"""
Generate a unique filename by appending a counter if file exists.
Args:
file_path: Original file path
Returns:
Unique file path with counter appended if necessary
"""
if not os.path.exists(file_path):
return file_path
base, ext = os.path.splitext(file_path)
counter = 1
while os.path.exists(file_path):
file_path = f"{base}_{counter}{ext}"
counter += 1
return file_path
def process_media_with_timestamp(
source_path: str,
dest_path: str,
timestamp: Optional[float],
timezone_offset: int = 0,
embed_exif: bool = False,
rename_media: bool = False
) -> str:
"""
Process a media file with optional timestamp embedding and renaming.
Args:
source_path: Source file path
dest_path: Destination file path (may be modified if renaming)
timestamp: Unix timestamp (seconds), or None if unavailable
timezone_offset: Hours offset from UTC
embed_exif: Whether to embed EXIF timestamp
rename_media: Whether to rename file with timestamp prefix
Returns:
Final destination path (may differ from dest_path if renamed)
"""
# If no timestamp available, just copy
if timestamp is None:
if source_path != dest_path:
logger.warning(f"No timestamp available for {source_path}, skipping timestamp operations")
shutil.copy2(source_path, dest_path)
return dest_path
# Determine final path
final_path = dest_path
if rename_media:
final_path = generate_timestamped_filename(dest_path, timestamp, timezone_offset)
# Handle duplicate filenames
if os.path.exists(final_path) and final_path != source_path:
final_path = _handle_duplicate_filename(final_path)
# Copy file to destination
shutil.copy2(source_path, final_path)
# Embed EXIF if requested
if embed_exif:
embed_exif_timestamp(final_path, timestamp, timezone_offset)
return final_path