238 lines
7.0 KiB
Python
238 lines
7.0 KiB
Python
import os
|
|||
|
|
import re
|
||
|
|
import random
|
||
|
|
import datetime
|
||
|
|
import sys
|
||
|
|
from PIL import Image, ExifTags
|
||
|
|
import subprocess
|
||
|
|
import json
|
||
|
|
from serverLibary import safe_rename
|
||
|
|
|
||
|
|
# Folder to scan
|
||
|
|
folder_path = '.'
|
||
|
|
# Supported image and video extensions
|
||
|
|
|
||
|
|
video_extensions = {
|
||
|
|
'.mp4', '.mov', '.avi', '.mkv', '.hevc'
|
||
|
|
}
|
||
|
|
img_extensions = {
|
||
|
|
'.jpg', '.jpeg', '.png', '.heic', '.webp',
|
||
|
|
}
|
||
|
|
|
||
|
|
extensions = img_extensions | video_extensions
|
||
|
|
prefixes = { 'IMG', 'VID'}
|
||
|
|
# Build reusable groups for regex
|
||
|
|
ext_group = r'(jpg|jpeg|png|heic|webp|mp4|mov|avi|mkv|hevc)'
|
||
|
|
prefix_group = r'(?:IMG|VID)'
|
||
|
|
# Correct format to keep
|
||
|
|
correct_format_re = re.compile(
|
||
|
|
rf'^{prefix_group}_(\d{{8}}_\d{{6}}).*\.{ext_group}$',
|
||
|
|
re.IGNORECASE
|
||
|
|
)
|
||
|
|
|
||
|
|
# Alternate timestamp formats and their patterns
|
||
|
|
alt_patterns = [
|
||
|
|
# PREFIX_YYYY_MM_DD_HH_MM_SS.ext
|
||
|
|
(
|
||
|
|
re.compile(
|
||
|
|
rf'^(?:{prefix_group}_)?(\d{{4}})_(\d{{2}})_(\d{{2}})_(\d{{2}})_(\d{{2}})_(\d{{2}}).*\.{ext_group}$',
|
||
|
|
re.IGNORECASE
|
||
|
|
),
|
||
|
|
'%Y_%m_%d_%H_%M_%S'
|
||
|
|
),
|
||
|
|
|
||
|
|
# PREFIX_YYYYMMDD_HHMMSS.ext
|
||
|
|
(
|
||
|
|
re.compile(
|
||
|
|
rf'^(?:{prefix_group}_)?(\d{{8}})_(\d{{6}}).*\.{ext_group}$',
|
||
|
|
re.IGNORECASE
|
||
|
|
),
|
||
|
|
'%Y%m%d_%H%M%S'
|
||
|
|
),
|
||
|
|
|
||
|
|
# PREFIX_YYYY.MM.DD-HHMMSS.ext
|
||
|
|
(
|
||
|
|
re.compile(
|
||
|
|
rf'^(?:{prefix_group}_)?(\d{{4}})\.(\d{{2}})\.(\d{{2}})-(\d{{6}}).*\.{ext_group}$',
|
||
|
|
re.IGNORECASE
|
||
|
|
),
|
||
|
|
'%Y.%m.%d-%H%M%S'
|
||
|
|
),
|
||
|
|
|
||
|
|
# PREFIX_YYYYMMDD.ext
|
||
|
|
(
|
||
|
|
re.compile(
|
||
|
|
rf'^(?:{prefix_group}_)?(\d{{8}})\.{ext_group}$',
|
||
|
|
re.IGNORECASE
|
||
|
|
),
|
||
|
|
'%Y%m%d'
|
||
|
|
),
|
||
|
|
|
||
|
|
# PREFIX_HHMMSS.ext
|
||
|
|
(
|
||
|
|
re.compile(
|
||
|
|
rf'^(?:{prefix_group}_)?(\d{{6}})\.{ext_group}$',
|
||
|
|
re.IGNORECASE
|
||
|
|
),
|
||
|
|
'%H%M%S'
|
||
|
|
),
|
||
|
|
]
|
||
|
|
|
||
|
|
# EXIF tag name for datetime
|
||
|
|
EXIF_DATE_TAGS = ['DateTimeOriginal', 'DateTimeDigitized', 'DateTime']
|
||
|
|
|
||
|
|
|
||
|
|
def local_safe_rename(src_path, new_filename):
|
||
|
|
"""Rename file safely, avoiding overwrites"""
|
||
|
|
directory = os.path.dirname(src_path)
|
||
|
|
base, ext = os.path.splitext(new_filename)
|
||
|
|
counter = 1
|
||
|
|
final_name = new_filename
|
||
|
|
|
||
|
|
while os.path.exists(os.path.join(directory, final_name)):
|
||
|
|
final_name = f"{base}_{counter}{ext}"
|
||
|
|
counter += 1
|
||
|
|
|
||
|
|
os.rename(src_path, os.path.join(directory, final_name))
|
||
|
|
print(f"Renamed: {os.path.basename(src_path)} ➜ {final_name}")
|
||
|
|
|
||
|
|
def get_exif_creation_datetime(filepath):
|
||
|
|
"""Try to extract creation datetime from image EXIF metadata"""
|
||
|
|
try:
|
||
|
|
with Image.open(filepath) as img:
|
||
|
|
exif = img._getexif()
|
||
|
|
if not exif:
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Map tag ids to tag names
|
||
|
|
tag_label = {ExifTags.TAGS.get(k): v for k, v in exif.items() if k in ExifTags.TAGS}
|
||
|
|
|
||
|
|
for tag in EXIF_DATE_TAGS:
|
||
|
|
if tag in tag_label:
|
||
|
|
# EXIF date format: 'YYYY:MM:DD HH:MM:SS'
|
||
|
|
return datetime.datetime.strptime(tag_label[tag], '%Y:%m:%d %H:%M:%S')
|
||
|
|
except Exception as e:
|
||
|
|
pass
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def get_manufacturer(file_path):
|
||
|
|
# Call exiftool and get JSON output
|
||
|
|
result = subprocess.run(
|
||
|
|
["exiftool", "-j", file_path], # -j outputs JSON
|
||
|
|
capture_output=True,
|
||
|
|
text=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Parse JSON output
|
||
|
|
metadata = json.loads(result.stdout)[0]
|
||
|
|
|
||
|
|
# Extract Author tag and remove spaces
|
||
|
|
author = metadata.get("Author", "")
|
||
|
|
if author == "":
|
||
|
|
author = metadata.get("AndroidManufacturer", "")
|
||
|
|
author_no_spaces = author.replace(" ", "")
|
||
|
|
|
||
|
|
return author_no_spaces
|
||
|
|
|
||
|
|
def process_images(folder):
|
||
|
|
for fname in os.listdir(folder):
|
||
|
|
fpath = os.path.join(folder, fname)
|
||
|
|
|
||
|
|
if not os.path.isfile(fpath):
|
||
|
|
continue
|
||
|
|
|
||
|
|
_, ext = os.path.splitext(fname)
|
||
|
|
ext = ext.lower()
|
||
|
|
|
||
|
|
if ext not in extensions:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Step 1: Normalize filename by replacing '-' with '_'
|
||
|
|
normalized_name = fname.replace('-', '_')
|
||
|
|
|
||
|
|
if normalized_name != fname:
|
||
|
|
# Rename the file to the normalized name before continuing
|
||
|
|
new_path = os.path.join(folder, normalized_name)
|
||
|
|
|
||
|
|
safe_rename(fpath, new_path)
|
||
|
|
|
||
|
|
print(f"Normalized: {fname} ➜ {os.path.basename(new_path)}")
|
||
|
|
|
||
|
|
# Update fname and fpath to the new normalized filename
|
||
|
|
fname = os.path.basename(new_path)
|
||
|
|
fpath = new_path
|
||
|
|
|
||
|
|
videoManufacturer = ""
|
||
|
|
file_prefix = "IMG"
|
||
|
|
for extention in video_extensions:
|
||
|
|
if extention in ext:
|
||
|
|
file_prefix = "VID"
|
||
|
|
videoManufacturer = "_" + get_manufacturer(fpath)
|
||
|
|
for extention in img_extensions:
|
||
|
|
if extention in ext:
|
||
|
|
file_prefix = "IMG"
|
||
|
|
|
||
|
|
# Now continue with your existing logic on normalized fname
|
||
|
|
if correct_format_re.match(fname):
|
||
|
|
if videoManufacturer != "" and file_prefix == "VID":
|
||
|
|
if videoManufacturer in fname:
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
basename, _ = os.path.splitext(fname)
|
||
|
|
new_fname = f"{basename}{videoManufacturer}{ext}"
|
||
|
|
safe_rename(fpath, os.path.join(folder, new_fname))
|
||
|
|
continue
|
||
|
|
|
||
|
|
print(f"[OK] {fname} already correctly named.")
|
||
|
|
continue
|
||
|
|
|
||
|
|
matched = False
|
||
|
|
for pattern, fmt in alt_patterns:
|
||
|
|
m = pattern.match(normalized_name)
|
||
|
|
if m:
|
||
|
|
try:
|
||
|
|
parts = m.groups()
|
||
|
|
timestamp_str = '_'.join(parts[:2]) if len(parts) >= 2 else parts[0]
|
||
|
|
dt = datetime.datetime.strptime(timestamp_str, fmt)
|
||
|
|
new_fname = f"{file_prefix}_{dt.strftime('%Y%m%d_%H%M%S')}{videoManufacturer}{ext}"
|
||
|
|
safe_rename(fpath, os.path.join(folder, new_fname))
|
||
|
|
matched = True
|
||
|
|
break
|
||
|
|
except Exception as e:
|
||
|
|
continue
|
||
|
|
|
||
|
|
if matched:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# 3. Try reading creation date from metadata
|
||
|
|
dt = get_exif_creation_datetime(fpath)
|
||
|
|
if dt:
|
||
|
|
new_fname = f"{file_prefix}_{dt.strftime('%Y%m%d_%H%M%S')}{videoManufacturer}{ext}"
|
||
|
|
safe_rename(fpath, new_fname)
|
||
|
|
continue
|
||
|
|
|
||
|
|
# 4. Fallback: keep original name, add _RX_<random> (only if not already added)
|
||
|
|
if '_RX_' in fname:
|
||
|
|
print(f"[SKIP] {fname} already has fallback name.")
|
||
|
|
continue
|
||
|
|
|
||
|
|
basename, _ = os.path.splitext(fname)
|
||
|
|
rand_num = random.randint(100000, 999999)
|
||
|
|
new_fname = f"{basename}_RX_{rand_num}{videoManufacturer}{ext}"
|
||
|
|
safe_rename(fpath, new_fname)
|
||
|
|
|
||
|
|
# ---- Main script entry ----
|
||
|
|
if __name__ == '__main__':
|
||
|
|
if len(sys.argv) < 2:
|
||
|
|
print("Usage: python script.py <image_folder_path>")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
folder_path = sys.argv[1]
|
||
|
|
|
||
|
|
if not os.path.isdir(folder_path):
|
||
|
|
print(f"Error: '{folder_path}' is not a valid directory.")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
print(f"Processing images in: {os.path.abspath(folder_path)}\n")
|
||
|
|
process_images(folder_path)
|