Add numbering mode to nn
This commit is contained in:
86
bin/nn
86
bin/nn
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user