diff --git a/movmaker.py b/movmaker.py index 8d5c863..5ae8975 100755 --- a/movmaker.py +++ b/movmaker.py @@ -55,6 +55,15 @@ LOG_PATH: Path | None = None def tool_path(local_path: Path, fallback: str) -> str: + """Return the preferred executable path for ffmpeg/ffprobe style tools. + + Args: + local_path: Path to a bundled copy shipped with movmaker. + fallback: Name of the system executable to call when the bundled copy is missing. + + Returns: + The fully-qualified path to execute. + """ return str(local_path) if local_path.exists() else fallback @@ -76,12 +85,14 @@ class MediaItem: def log_message(message: str) -> None: + """Append a diagnostic line to the current render log if it is enabled.""" if LOG_PATH: with LOG_PATH.open("a", encoding="utf-8") as log: log.write(message.rstrip() + "\n") def ffmpeg_progress_cmd(cmd: list[str], progress: bool) -> list[str]: + """Inject ffmpeg-friendly progress flags when progress monitoring is requested.""" if not cmd or Path(cmd[0]).name != "ffmpeg": return cmd out = [cmd[0], "-hide_banner", "-nostats"] @@ -104,6 +115,12 @@ def format_bytes(value: int) -> str: def run(cmd: list[str], progress_duration: float | None = None) -> None: + """Execute a subprocess while streaming progress and teeing output to the log. + + Args: + cmd: argv array ready for subprocess.run. + progress_duration: Expected output duration in seconds used to estimate progress; omit for instant commands. + """ progress = progress_duration is not None and progress_duration > 0 and Path(cmd[0]).name == "ffmpeg" run_cmd = ffmpeg_progress_cmd(cmd, progress) log_message("+ " + shlex.join(run_cmd)) @@ -483,6 +500,7 @@ def media_date(path: Path) -> datetime: def ascii_safe(value: str) -> str: + """Return a human-readable ASCII approximation of Scandinavian/Slavic letters.""" replacements = { # Danish letters "æ": "ae", "Æ": "Ae", @@ -505,6 +523,7 @@ def ascii_safe(value: str) -> str: def slugify(value: str) -> str: + """Convert an arbitrary string into a filesystem-safe slug.""" value = ascii_safe(value) value = value.strip().lower() value = re.sub(r"[^a-z0-9]+", "_", value) @@ -525,6 +544,7 @@ def display_date(dt: datetime) -> str: def prepare_clip_stamps(media: list[MediaItem], data: MovieData) -> list[str]: + """Build per-clip stamp strings containing title/date/place metadata.""" geocode_cache: dict[tuple[float, float], str | None] = {} stamps: list[str] = [] for item in media: @@ -566,6 +586,7 @@ def natural_key(path: Path) -> list[object]: def scan_input(input_dir: Path, image_duration: float) -> tuple[list[MediaItem], list[Path], MovieData]: + """Detect media/audio/data files inside the input directory and build metadata.""" if not input_dir.is_dir(): raise SystemExit(f"Input directory does not exist: {input_dir}") @@ -678,6 +699,15 @@ def drawtext_escape(text: str) -> str: def render_intro_logo_frame(logo: Path, out: Path, width: int, height: int, alpha: float) -> None: + """Build a single black frame with the intro logo centered and scaled. + + Args: + logo: Source PNG to overlay. + out: Destination PNG path for the composed frame. + width: Target video width. + height: Target video height. + alpha: Desired opacity between 0 and 1. + """ if width <= 0 or height <= 0: raise ValueError("intro logo frame must have positive width/height") alpha = min(1.0, max(0.0, alpha)) @@ -703,6 +733,7 @@ def render_intro_logo_frame(logo: Path, out: Path, width: int, height: int, alph def normalize_clip(item: MediaItem, out: Path, width: int, height: int, fps: int, preset: str, crf: int) -> None: + """Transcode one media item into a padded, FPS-normalized H.264 clip.""" vf = ( f"scale={width}:{height}:force_original_aspect_ratio=decrease," f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2:black," @@ -773,6 +804,7 @@ def load_jamendo_client_id() -> str | None: def download_jamendo_music(genre: str, out_dir: Path) -> Path | None: + """Download a random Jamendo track matching the requested genre.""" client_id = load_jamendo_client_id() if not client_id: console("No audio files found and no Jamendo client ID configured; continuing without music.") @@ -843,6 +875,7 @@ def download_jamendo_music(genre: str, out_dir: Path) -> Path | None: def concat_or_mix_audio(audio_files: list[Path], out: Path) -> Path | None: + """Concatenate or mix multiple audio tracks into one temp file.""" if not audio_files: return None if len(audio_files) == 1: @@ -885,6 +918,7 @@ def build_video( intro_visible_lead: float = 0.0, skip_video_fade_out: bool = False, ) -> None: + """Assemble normalized clips, overlays, and audio into the final MP4.""" output.parent.mkdir(parents=True, exist_ok=True) text_file_count = 0 @@ -1144,6 +1178,7 @@ def build_video( def main(argv: list[str] | None = None) -> int: + """CLI entry point for movmaker.""" parser = argparse.ArgumentParser(description="Make a video from a flat folder of images/videos/audio.") parser.add_argument("input", type=Path, help="flat input directory") parser.add_argument("--out-dir", type=Path, default=Path("."), help="directory for auto-named output videos")