diff --git a/movmaker.py b/movmaker.py index ee65fc8..e3f06b9 100755 --- a/movmaker.py +++ b/movmaker.py @@ -424,16 +424,20 @@ def infer_place_from_gps(media: list[MediaItem]) -> str | None: return None -def media_date(path: Path) -> datetime: +def media_embedded_date(path: Path) -> datetime | None: ext = path.suffix.lower() - dt = None if ext in IMAGE_EXTS: try: dt = exif_image_date(path) + if dt is not None: + return dt except Exception: - dt = None - if dt is None: - dt = ffprobe_creation_date(path) + pass + return ffprobe_creation_date(path) + + +def media_date(path: Path) -> datetime: + dt = media_embedded_date(path) if dt is None: dt = datetime.fromtimestamp(path.stat().st_mtime) return dt @@ -470,6 +474,26 @@ def display_date(dt: datetime) -> str: return f"{dt.strftime('%B')} {dt.year}" +def prepare_clip_stamps(media: list[MediaItem], data: MovieData) -> list[str]: + geocode_cache: dict[tuple[float, float], str | None] = {} + stamps: list[str] = [] + for item in media: + embedded_date = media_embedded_date(item.path) + date_text = display_date(embedded_date) if embedded_date else data.date + place_text = data.place + gps = media_gps(item.path) + if gps: + key = (round(gps[0], 4), round(gps[1], 4)) + if key not in geocode_cache: + geocode_cache[key] = reverse_geocode_city_country(*gps) + place_text = geocode_cache[key] or place_text + stamp = " | ".join(x for x in [data.title, date_text, place_text] if x) + stamps.append(stamp) + if stamp: + console(f"Clip stamp: {item.path.name}: {ascii_safe(stamp)}") + return stamps + + def timestamp() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -515,6 +539,7 @@ def scan_input(input_dir: Path, image_duration: float) -> tuple[list[MediaItem], if not media: raise SystemExit(f"No images or videos found in {input_dir}") + media.sort(key=lambda item: (media_date(item.path), natural_key(item.path))) return media, audio, parse_data_file(data_file) @@ -747,6 +772,7 @@ def build_video( data: MovieData, creation_date: datetime, captions: list[str], + clip_stamps: list[str], fade: float, audio_fade: float, audio_fade_in: float, @@ -899,16 +925,30 @@ def build_video( caption_label = next_label stamp_size = max(16, height // 58) - stamp_lines = [x for x in [data.title, data.date, data.place] if x] - stamp_text = drawtext_escape(" | ".join(stamp_lines)) - stamp_left_label = "stamp_left" - if stamp_text: + stamp_left_label = caption_label + for idx, stamp_text in enumerate(clip_stamps): + if not stamp_text: + continue + if n == 1: + start = intro_duration + end = total_duration + elif idx == 0: + start = intro_duration + end = intro_duration + transition_offsets[0] + elif idx == n - 1: + start = intro_duration + transition_offsets[idx - 1] + fade + end = total_duration + else: + start = intro_duration + transition_offsets[idx - 1] + fade + end = intro_duration + transition_offsets[idx] + if end <= start: + continue + next_label = f"stamp_left_{idx}" filters.append( - f"[{caption_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(' | '.join(stamp_lines))}:x=24:y=h-text_h-18:" - f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{stamp_left_label}]" + f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(stamp_text)}:x=24:y=h-text_h-18:" + f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black:enable='between(t,{start:.3f},{end:.3f})'[{next_label}]" ) - else: - filters.append(f"[{caption_label}]copy[{stamp_left_label}]") + stamp_left_label = next_label filters.append( f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg('Bubulescu.Org')}:x=w-text_w-24:y=h-text_h-18:" f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]" @@ -1049,6 +1089,7 @@ def main(argv: list[str] | None = None) -> int: # Use probed normalized duration for accuracy. durations = [ffprobe_duration(out) for out in clips] captions = [data.captions.get(item.path.name, data.captions.get(str(item.path), "")) for item in media] + clip_stamps = prepare_clip_stamps(media, data) audio = concat_or_mix_audio(audio_files, tmp / "music.m4a") if len(audio_files) > 1: console(f"Audio: {audio if audio else 'none'}") @@ -1056,7 +1097,7 @@ def main(argv: list[str] | None = None) -> int: artist, title = audio_metadata(audio) if artist or title: console("Audio metadata: " + " - ".join(x for x in [artist, title] if x)) - build_video(clips, durations, audio, output, data, first_media_date, captions, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp) + build_video(clips, durations, audio, output, data, first_media_date, captions, clip_stamps, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp) console(f"Done: {output}") return 0