mirror of
https://github.com/KnugiHK/WhatsApp-Chat-Exporter.git
synced 2026-03-29 17:25:26 +00:00
201 lines
5.7 KiB
Python
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
|