Add numbering mode to nn

This commit is contained in:
2026-03-16 08:46:28 +01:00
parent 5a73875782
commit 145f21eae7

86
bin/nn
View File

@@ -100,6 +100,35 @@ def plan_parse_rename(filepath):
return new_name, STATUS_RENAME
def get_date_for_file(filepath, parse_mode):
"""Resolve a date string for a file, or None if unavailable."""
if parse_mode:
date_str = None
if shutil.which('exiftool') is not None:
date_str = get_exif_date(filepath)
if not date_str:
date_str = get_file_date(filepath)
return date_str
# Default pattern mode: extract date from filename
stem = filepath.stem
# Check if already normalized (e.g. "2024-06-01 breakfast.jpg")
norm_match = re.match(r'^(\d{4})-(\d{2})-(\d{2}) ', filepath.name)
if norm_match:
return f'{norm_match.group(1)}-{norm_match.group(2)}-{norm_match.group(3)}'
# Try the standard pattern
match = PATTERN.match(stem)
if not match:
match = PATTERN.match(filepath.name)
if match:
year, month, day, _ = match.groups()
return f'{year}-{month}-{day}'
return None
def collect_files(paths):
"""Resolve the list of files to process."""
if not paths:
@@ -144,6 +173,49 @@ def build_plan(files, parse_mode):
return plan
def build_number_plan(files, parse_mode):
"""Build a rename plan using sequential numbering per day."""
entries = []
for file_path in files:
date_str = get_date_for_file(file_path, parse_mode)
entries.append((file_path, date_str))
day_counter = {}
plan = []
seen_targets = {}
for file_path, date_str in entries:
if date_str is None:
plan.append((file_path, None, STATUS_NO_MATCH))
continue
day_counter[date_str] = day_counter.get(date_str, 0) + 1
seq = day_counter[date_str]
ext = file_path.suffix
new_name = f'{date_str} {seq:02d}{ext}'
if new_name == file_path.name:
plan.append((file_path, None, STATUS_ALREADY))
continue
status = STATUS_RENAME
target = file_path.parent / new_name
target_key = str(target).casefold()
if target.exists() and target.resolve() != file_path.resolve():
status = STATUS_CONFLICT
elif target_key in seen_targets:
prev_idx = seen_targets[target_key]
plan[prev_idx] = (plan[prev_idx][0], plan[prev_idx][1], STATUS_CONFLICT)
status = STATUS_CONFLICT
else:
seen_targets[target_key] = len(plan)
plan.append((file_path, new_name, status))
return plan
def print_preview(plan):
"""Print a formatted preview table."""
if not plan:
@@ -194,6 +266,15 @@ def main():
action='store_true',
help='Use EXIF data or file dates instead of pattern matching.',
)
parser.add_argument(
'-n',
'--number',
action='store_true',
help=(
'Discard original filename stems and number files sequentially '
'per day. Combines with --parse to select the date source.'
),
)
parser.add_argument(
'files',
nargs='*',
@@ -207,7 +288,10 @@ def main():
print('No files found.')
sys.exit(0)
plan = build_plan(files, args.parse)
if args.number:
plan = build_number_plan(files, args.parse)
else:
plan = build_plan(files, args.parse)
print_preview(plan)
renames = sum(1 for _, _, status in plan if status == STATUS_RENAME)