Improve grouping controls and cleanup
This commit is contained in:
+38
-14
@@ -189,6 +189,7 @@ class UserChoices:
|
||||
artist_value: str | None
|
||||
rename_files: bool
|
||||
group_photos: bool
|
||||
group_min_size: int
|
||||
move_to_working_dir: bool
|
||||
process_pto_files: bool
|
||||
apply_filename_timestamps: bool
|
||||
@@ -207,6 +208,23 @@ def ask_yes_no(prompt: str, default: bool) -> bool:
|
||||
print("Please answer y or n.")
|
||||
|
||||
|
||||
def ask_group_min_size() -> int:
|
||||
while True:
|
||||
answer = prompt_input(
|
||||
"Create automatic burst/HDR groups? Enter=yes, n=no, or minimum group size [2]: "
|
||||
).strip().lower()
|
||||
if not answer or answer in {"y", "yes"}:
|
||||
return 2
|
||||
if answer in {"n", "no"}:
|
||||
return 0
|
||||
if answer.isdigit():
|
||||
value = int(answer)
|
||||
if value <= 0:
|
||||
return 0
|
||||
return max(value, 2)
|
||||
print("Please answer y, n, or an integer minimum group size.")
|
||||
|
||||
|
||||
def parse_timezone_offset(value: str) -> timezone:
|
||||
match = TZ_RE.match(value.strip())
|
||||
if not match:
|
||||
@@ -739,9 +757,10 @@ def prompt_choices(records: list[MediaRecord], working_dir: Path, has_pto_files:
|
||||
rename_files = ask_yes_no("Rename files to timestamps?", default=True)
|
||||
explicit_tz, authoritative_tz, write_missing_tz, video_tz = prompt_timezone(records, rename_files)
|
||||
artist_action, artist_value = prompt_artist_action()
|
||||
group_photos = False
|
||||
group_min_size = 0
|
||||
if sum(1 for record in records if record.is_image and record.adjusted_time) >= 2:
|
||||
group_photos = ask_yes_no("Create automatic burst/HDR groups?", default=True)
|
||||
group_min_size = ask_group_min_size()
|
||||
group_photos = group_min_size >= 2
|
||||
|
||||
process_pto_files = False
|
||||
if has_pto_files:
|
||||
@@ -772,6 +791,7 @@ def prompt_choices(records: list[MediaRecord], working_dir: Path, has_pto_files:
|
||||
artist_value=artist_value,
|
||||
rename_files=rename_files,
|
||||
group_photos=group_photos,
|
||||
group_min_size=group_min_size,
|
||||
move_to_working_dir=move_to_working_dir,
|
||||
process_pto_files=process_pto_files,
|
||||
apply_filename_timestamps=apply_filename_timestamps,
|
||||
@@ -806,7 +826,7 @@ def group_sort_key(record: MediaRecord) -> tuple:
|
||||
)
|
||||
|
||||
|
||||
def detect_photo_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
||||
def detect_photo_groups(records: list[MediaRecord], min_size: int = 2) -> list[list[MediaRecord]]:
|
||||
groups: list[list[MediaRecord]] = []
|
||||
by_camera: dict[str, list[MediaRecord]] = defaultdict(list)
|
||||
for record in records:
|
||||
@@ -814,12 +834,12 @@ def detect_photo_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
||||
by_camera[record.camera_key].append(record)
|
||||
|
||||
for camera_records in by_camera.values():
|
||||
groups.extend(detect_groups_for_camera(sorted(camera_records, key=photo_sort_key)))
|
||||
groups.extend(detect_groups_for_camera(sorted(camera_records, key=photo_sort_key), min_size))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def detect_groups_for_camera(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
||||
def detect_groups_for_camera(records: list[MediaRecord], min_size: int) -> list[list[MediaRecord]]:
|
||||
by_second: dict[datetime, list[MediaRecord]] = defaultdict(list)
|
||||
for record in records:
|
||||
assert record.adjusted_time is not None
|
||||
@@ -843,13 +863,13 @@ def detect_groups_for_camera(records: list[MediaRecord]) -> list[list[MediaRecor
|
||||
continue
|
||||
span_records = [record for second in span for record in by_second[second]]
|
||||
if all(record.sequence is not None for record in span_records) and len(span_records) >= 2:
|
||||
groups.extend(sequence_groups(span_records))
|
||||
groups.extend(sequence_groups(span_records, min_size))
|
||||
else:
|
||||
groups.extend(timestamp_groups(span, by_second))
|
||||
return [group for group in groups if len(group) >= 2]
|
||||
groups.extend(timestamp_groups(span, by_second, min_size))
|
||||
return [group for group in groups if len(group) >= min_size]
|
||||
|
||||
|
||||
def sequence_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
||||
def sequence_groups(records: list[MediaRecord], min_size: int) -> list[list[MediaRecord]]:
|
||||
ordered = sorted(records, key=photo_sort_key)
|
||||
groups: list[list[MediaRecord]] = []
|
||||
current: list[MediaRecord] = [ordered[0]]
|
||||
@@ -858,16 +878,16 @@ def sequence_groups(records: list[MediaRecord]) -> list[list[MediaRecord]]:
|
||||
if record.sequence is not None and previous.sequence is not None and record.sequence > previous.sequence:
|
||||
current.append(record)
|
||||
else:
|
||||
if len(current) >= 2:
|
||||
if len(current) >= min_size:
|
||||
groups.append(current)
|
||||
current = [record]
|
||||
if len(current) >= 2:
|
||||
if len(current) >= min_size:
|
||||
groups.append(current)
|
||||
return groups
|
||||
|
||||
|
||||
def timestamp_groups(
|
||||
span: list[datetime], by_second: dict[datetime, list[MediaRecord]]
|
||||
span: list[datetime], by_second: dict[datetime, list[MediaRecord]], min_size: int
|
||||
) -> list[list[MediaRecord]]:
|
||||
multi_indices = [index for index, second in enumerate(span) if len(by_second[second]) >= 2]
|
||||
if not multi_indices:
|
||||
@@ -893,7 +913,7 @@ def timestamp_groups(
|
||||
for second in seconds:
|
||||
used_seconds.add(second)
|
||||
records = [record for second in seconds for record in sorted(by_second[second], key=photo_sort_key)]
|
||||
if len(records) >= 2:
|
||||
if len(records) >= min_size:
|
||||
groups.append(records)
|
||||
return groups
|
||||
|
||||
@@ -1448,6 +1468,10 @@ def print_preview(records: list[MediaRecord], choices: UserChoices, pto_plans: l
|
||||
print("\nPreview")
|
||||
print(f"Working directory: {choices.working_dir}")
|
||||
print(f"Move ungrouped files into working directory: {'yes' if choices.move_to_working_dir else 'no'}")
|
||||
print(
|
||||
"Automatic burst/HDR grouping: "
|
||||
+ (f"minimum {choices.group_min_size}" if choices.group_photos else "no")
|
||||
)
|
||||
print(f"Panorama project files (.pto): {'group/update' if choices.process_pto_files else 'ignore'}")
|
||||
print(f"Time shift: {format_timedelta(choices.time_offset) if choices.time_offset else 'none'}")
|
||||
print(
|
||||
@@ -1571,7 +1595,7 @@ def prepare_plan(
|
||||
|
||||
group_candidates: list[list[MediaRecord]] = []
|
||||
if choices.group_photos:
|
||||
group_candidates.extend(detect_photo_groups(records))
|
||||
group_candidates.extend(detect_photo_groups(records, choices.group_min_size))
|
||||
if choices.process_pto_files:
|
||||
group_candidates.extend(reference.records for reference in pto_references)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user