Files
media-batch-tools/README.md
T

16 KiB

Media Batch Tools

Small Python scripts for organizing photo collections and preparing or encoding edited videos.

Scripts

photo_metadata.py

Interactive metadata cleanup for photo collections:

python3 photo_metadata.py path/to/file/or/directory [...]

Directory inputs are scanned recursively. The script reads metadata, asks for the choices it needs, prints a preview, and only then applies changes in place after confirmation.

Current features:

  • supports .jpg, .jpeg, .heic, .arw, .mp4, .mov, and .mts
  • reads and writes metadata through exiftool from PATH
  • optional direct time shifts, such as +1:30, -02:00:00, or +1h 2m
  • optional reference-clock correction using a source photo timestamp or filename plus the correct time
  • keyboard-driven settings table with timezone gap filling and optional fixed timezone offsets
  • artist/author set, clear, or leave unchanged
  • timestamp-based rename modes: adjust timestamps already in filenames, add one, or replace the stem
  • video filenames use the beginning timestamp by subtracting rounded duration from the video metadata timestamp
  • Sony-style video sidecars like C0011.MP4 plus C0011M01.XML are moved and renamed with the video
  • automatic photo burst/HDR grouping per camera
  • optional panorama project (.pto) grouping and reference updates
  • inferred subsecond metadata for grouped same-second photo bursts when the group has no existing subseconds
  • optional capture-time inference from a safe timestamp filename, then ModifyDate as a fallback

The working directory is determined from the dragged items. A single dragged folder is the working directory. For multiple dragged files/folders, their last common ancestor is the working directory. Automatic burst/HDR and .pto group folders are created inside that working directory.

The Organize/group setting controls all file movement. When enabled, ungrouped files are moved into the working directory and groups are created there; when disabled, files remain in their original directories. Panorama-project grouping is also enabled from that row.

Panorama project files use the .pto extension. When enabled, every media file referenced by a .pto file belongs to the same group. If those files already belong to burst/HDR groups, the groups are merged. .pto files are moved into the group folder with their referenced files, renamed from the first and last referenced media stems, and their file references are rewritten to the new relative paths.

Reference-clock correction is useful when one photo shows a reliable clock. For example, if DSC01234.JPG has camera metadata 2026:06:02 22:10:00, but the clock in the photo shows 22:13:25, choose reference mode, enter DSC01234.JPG as the source, and enter 22:13:25 as the correct time. The script computes a +00:03:25 shift and applies that correction to the selected files. If you omit the date in the correct time, the script chooses the date closest to the source timestamp, within 12 hours.

The settings table requires an ANSI-capable terminal because it uses arrow keys and inline editing. Use Up/Down to select a row, Left/Right to select an option, Space to toggle an option, and Enter to continue. The preview separates common changes, inferred timestamps, warnings, and file operations before asking for confirmation.

grouping.py

Fast manual grouping for selected files:

python3 grouping.py path/to/file/or/directory [...]

It does not ask questions. It recursively collects all files inside the dragged files/folders, sorts those files by name, and moves them into one folder named [first stem]-[last stem].

Manual grouping uses the same working-directory rule as photo_metadata.py:

  • one dragged folder is the working directory
  • multiple dragged files/folders use their last common ancestor as the working directory
  • the group folder is created inside the working directory
  • all collected files are flattened into that group folder

view_metadata.py

Show all readable metadata for the first file provided:

python3 view_metadata.py path/to/file/or/directory [...]

This script only displays one file. If several files are given, it displays the first one. If a directory is given, it searches recursively and displays the first file it finds.

It runs ExifTool as:

exiftool -a -u -ee -G0:1 -s file

The important choices are:

  • -a shows duplicate tag names instead of hiding later duplicates.
  • -u includes unknown tags ExifTool can identify well enough to print.
  • -ee extracts embedded metadata where ExifTool supports it, which is useful for some video/container formats.
  • -G0:1 shows both the broad metadata container and the more exact location, such as EXIF:IFD0 or QuickTime:Track1.
  • -s uses compact tag names, which are easier to copy into scripts than the descriptive labels.

The old -G style shows broad groups like EXIF, File, or QuickTime. That is readable, but it can hide where inside a container a value came from. Family 1 groups, from -G1, show more exact locations like IFD0, ExifIFD, MakerNotes, or Track1, but sometimes lose the broader context. -G0:1 combines both, so it is the most useful default for debugging metadata without changing which normal tags are extracted. The organization option changes labels, not the underlying metadata extraction; -a and -u are the parts that affect whether duplicate or unknown printed tags are included.

copy_metadata.py

Copy meaningful metadata from one file to another:

python3 copy_metadata.py
python3 copy_metadata.py source.jpg destination.jpg

If run without files, it asks for source and destination paths. If exactly two files are dragged onto it, it asks which one is the source. Any other number of dragged files is an error.

The script copies normal metadata with ExifTool -TagsFromFile, while excluding file/system tags, composite tags, image dimensions, pixel dimensions, and resolution fields that should remain specific to the destination file.

video_encode.py

Prepare, validate, encode, and remux Avisynth-edited videos:

python3 video_encode.py path/to/file/or/directory [...]

Source videos are untouched until you explicitly choose a final action after encoding. The script recursively collects .mp4, .mov, .mkv, .mts, .m2ts, and .avi files, builds a collapsed drag tree, and creates a workspace in video_workspace/.

The workflow is:

  1. Probe the source files with ffprobe and ExifTool, including frame timing, embedded timezone, location, and HDR metadata.
  2. Create editable .avs scripts in video_workspace/ and generated source/validation files in video_workspace/.source/.
  3. Edit the visible .avs files. Existing editable files are kept unless you confirm an overwrite.
  4. Press Enter for fast blank-frame validation, r for real-frame validation, or q to stop after workspace creation.
  5. Review encoding settings, encode accepted scripts, then leave outputs in the workspace or move them to the source directories.

Generated source scripts use FFMS2 FFVideoSource, mark every frame with source identity properties, and leave the source clip in last. Custom Avisynth filters must preserve those frame properties. Validation renders every output frame and rejects scripts that lose or corrupt the identity mapping.

The visible scripts document the available helpers, including MBT_Drop, MBT_DropEvery, MBT_Info, MBT_AbsoluteTimeStretch, Resize, RotateCrop, MBT_CorrectMatrix, MBT_ToSDR, Cropf, DeShake, and FixContrast. MBT_Drop omits frames while preserving their timeline duration; normal operations such as Trim and SelectEven remove source time. MBT_AbsoluteTimeStretch() adjusts the absolute timestamps used for metadata and MBT_Info() without changing playback speed.

On Windows, ending a visible .avs file with a commented #32bit or # 32-bit marker selects a bundled/configured 32-bit runner for that script. Linux uses the 64-bit runner.

The bundled runner is used automatically when found. Build it on Linux with:

tools/avisynth_runner/build_linux.sh

On Windows, use the bundled tools/avisynth_runner/mbt_avs_runner.exe with installed AviSynth+. Set MBT_AVS_RUNNER, MBT_AVS_RUNNER64, or MBT_AVS_RUNNER32 to override runner locations. Generated scripts normally rely on FFMS2 autoload; set MBT_FFMS2_PLUGIN to generate LoadPlugin("..."), or MBT_ASK_FFMS2_PLUGIN=1 to ask interactively.

Encoding

  • x265 is the default encoder. x264, x265, and unchanged-video stream copy are available when compatible with the validated output.
  • The tool supports CFR, VFR, trims, playback-speed changes, MBT_Drop, normal frame deletion, and mixed deletion/drop edits.
  • VFR and normal-deletion output preserves frame timing through timestamp-v2 files and MKVToolNix. x264 receives those timestamps through --tcfile-in; x265 receives them during the muxing step.
  • Video filenames can use the edited beginning timestamp in a chosen timezone. Video container metadata is written with the edited ending timestamp. Meaningful source metadata, including location, is copied while physical stream properties remain specific to the output.
  • Existing output names can be overwritten or given -1, -2, and so on. Same-batch collisions always receive a suffix.
  • Profile and level can be unset, calculated as the per-clip minimum, or selected as a floor. x264 is unavailable for output above 10-bit, and output above 12-bit is rejected.
  • MP4 and MKV support AAC, Opus, FLAC, copied audio, or no audio. Stream-copy compatibility is checked before encoding.

Audio is read from the original source by default. An optional [visible script stem]_audio.avs is rendered to temporary WAV and used instead when present. AAC audio is trimmed and tempo-adjusted for the validated output. Audio stream copy is limited to whole-video and simple beginning/end trims, so normal frame deletion and speed changes require audio re-encoding or no audio.

After successful encoding, choose to leave files in video_workspace/, replace the originals, or move originals to video_workspace/originals/ before moving the new files to the source directories.

HDR

HDR clips remain HDR unless the script calls MBT_ToSDR(). HDR-preserving x265 encodes retain validated color properties and static HDR metadata. HDR10+ additionally requires hdr10plus_tool; other dynamic HDR formats currently require video stream copy. MBT_ToSDR() uses HDRTools and offers MPC (default), Hable, Mobius, Reinhard, ACES, and BT2446C. Tone-mapped output should be visually checked before relying on it for archival work.

Non-interactive Encoding

Set MBT_ENCODE_VIDEO=1 to encode non-interactively after validation. The following optional variables configure that run:

  • MBT_VIDEO_TIMESTAMP_NAMES=1 names outputs as YYYYMMDD_HHMMSS.mp4; 0 keeps original-style workspace names.
  • MBT_VIDEO_TIMEZONE=+03:00 selects the timezone used for timestamp filenames. If omitted, one consistent embedded source timezone is preferred; conflicting or missing source offsets fall back to the system timezone. No timezone is needed when timestamp filenames are disabled.
  • MBT_VIDEO_CONTAINER=mp4 or mkv selects the output container. MP4 is the default.
  • MBT_VIDEO_CODEC=x264, x265, or copy selects FFmpeg libx264, libx265, or direct source-video remuxing; x265 is the default. copy is only available for unchanged validated video timelines.
  • MBT_VIDEO_CRF=16 sets CRF. Interactive defaults are 16 for x264 and 21 for x265.
  • MBT_VIDEO_PRESET=slow sets the encoder preset.
  • MBT_VIDEO_PROFILE=minimum and MBT_VIDEO_LEVEL=minimum calculate per-clip constraints. A named profile or level acts as a requested floor. Omit either variable to apply no constraint for it.
  • MBT_VIDEO_THREADS=auto (the default) leaves encoder thread selection automatic. Set it to a positive integer to limit x264 threads or x265 worker pools.
  • MBT_VIDEO_AUDIO=aac, opus, flac, copy, or none selects audio handling. AAC, Opus, and FLAC are valid in either supported container, subject to player compatibility.
  • MBT_VIDEO_AUDIO_BITRATE=192k sets AAC bitrate.
  • MBT_VIDEO_AUDIO_PITCH=preserve or shift selects whether speed-changed AAC audio keeps pitch or shifts pitch with playback speed. The default is preserve.
  • MBT_VIDEO_FINAL_ACTION=leave, replace, or backup selects the final disposition.
  • MBT_VIDEO_DELETE_WORKSPACE=1 deletes video_workspace after moving outputs.

Missing executables are requested by absolute path for the current run when possible. mkvmerge also checks the standard Windows installation path.

Testing

Run the Python tests with:

python3 -m unittest discover -s unit_tests

The pure Python tests always run. The video end-to-end scenario tests skip unless the local test fixtures and toolchain are available: tests/, ExifTool, FFmpeg/FFprobe, FFMS2 for Avisynth, and the bundled mbt_avs_runner.

Current video scenarios copy fixture videos to temporary directories before processing and validate:

  • CFR MP4 encode with AAC audio and metadata copy
  • VFR MP4 trim with copied GPS metadata and adjusted end timestamp
  • normal frame deletion such as SelectEven() with segmented AAC audio
  • mixed normal frame deletion plus MBT_Drop
  • MBT_Drop(start,end) and MBT_DropEvery(...) helper behavior
  • MKV output with Opus audio
  • MP4 video/audio stream-copy remuxing

Requirements

  • Python 3.10 or newer
  • ExifTool available as exiftool on PATH
  • FFmpeg available as ffmpeg and ffprobe on PATH for video_encode.py
  • MKVToolNix available as mkvmerge on PATH for VFR or frame-deletion output (Windows also checks its standard installation path)
  • the x264 command-line encoder available as x264 on PATH for timestamp-aware x264 output
  • hdr10plus_tool available on PATH only when re-encoding HDR10+ video while retaining HDR
  • FFMS2 available to Avisynth as FFVideoSource before generated .avs scripts can load real videos, either through plugin autoload or a LoadPlugin path
  • AviSynth+ development files and a C compiler are needed only if building the bundled runner from source

On Debian/Ubuntu-like Linux systems, ExifTool is commonly installed with:

sudo apt install libimage-exiftool-perl

FFmpeg/FFprobe is commonly installed with:

sudo apt install ffmpeg x264 mkvtoolnix

On Windows, install ExifTool and make sure the exiftool command is available in a normal terminal. For timestamp-driven video output, also install MKVToolNix; the usual C:\Program Files\MKVToolNix\mkvmerge.exe location is detected automatically, otherwise add mkvmerge to PATH. Timestamp-aware x264 output additionally needs the x264 command-line encoder as x264 on PATH.

No virtual environment is required. The scripts use only the Python standard library plus external command-line tools.

Safety

photo_metadata.py edits files in place, but it prints a preview and asks for confirmation before writing metadata or moving files. Test media should be copied to a temporary directory before running destructive checks.

video_encode.py creates files under video_workspace/, including encoded outputs when requested. It only modifies source directories or source videos if you explicitly choose a final action that moves outputs back to the source folders.

tests/, old/, and video_workspace/ are intentionally ignored by git. The tests/ directory is for reusable source fixtures that should not be modified directly.

AI Note

This code was written with AI assistance and should be reviewed and tested before relying on it for important media archives.