15 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
exiftoolfromPATH - 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
- timezone handling for photos and local-time video filenames
- artist/author set, clear, or leave unchanged
- timestamp-based renaming to
YYYYMMDD_HHMMSS.ext - video filenames use the beginning timestamp by subtracting rounded duration from the video metadata timestamp
- Sony-style video sidecars like
C0011.MP4plusC0011M01.XMLare 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
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.
If dragged folders contain files in subdirectories and grouping is enabled, ungrouped files are moved into the working directory too. This makes the run organize everything into a deterministic flat reset state, with grouped files in their group folders and the remaining files directly in the working directory. If grouping is disabled, the script asks whether to flatten subdirectory files into the working directory.
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.
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:
-ashows duplicate tag names instead of hiding later duplicates.-uincludes unknown tags ExifTool can identify well enough to print.-eeextracts embedded metadata where ExifTool supports it, which is useful for some video/container formats.-G0:1shows both the broad metadata container and the more exact location, such asEXIF:IFD0orQuickTime:Track1.-suses 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:
- Probe the source files with
ffprobeand ExifTool, including frame timing, embedded timezone, location, and HDR metadata. - Create editable
.avsscripts invideo_workspace/and generated source/validation files invideo_workspace/.source/. - Edit the visible
.avsfiles. Existing editable files are kept unless you confirm an overwrite. - Press Enter for fast blank-frame validation,
rfor real-frame validation, orqto stop after workspace creation. - 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=1names outputs asYYYYMMDD_HHMMSS.mp4;0keeps original-style workspace names.MBT_VIDEO_TIMEZONE=+03:00selects 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=mp4ormkvselects the output container. MP4 is the default.MBT_VIDEO_CODEC=x264,x265, orcopyselects FFmpeglibx264,libx265, or direct source-video remuxing; x265 is the default.copyis only available for unchanged validated video timelines.MBT_VIDEO_CRF=16sets CRF. Interactive defaults are 16 for x264 and 21 for x265.MBT_VIDEO_PRESET=slowsets the encoder preset.MBT_VIDEO_PROFILE=minimumandMBT_VIDEO_LEVEL=minimumcalculate 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, ornoneselects audio handling. AAC, Opus, and FLAC are valid in either supported container, subject to player compatibility.MBT_VIDEO_AUDIO_BITRATE=192ksets AAC bitrate.MBT_VIDEO_AUDIO_PITCH=preserveorshiftselects whether speed-changed AAC audio keeps pitch or shifts pitch with playback speed. The default ispreserve.MBT_VIDEO_FINAL_ACTION=leave,replace, orbackupselects the final disposition.MBT_VIDEO_DELETE_WORKSPACE=1deletesvideo_workspaceafter 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)andMBT_DropEvery(...)helper behavior- MKV output with Opus audio
- MP4 video/audio stream-copy remuxing
Requirements
- Python 3.10 or newer
- ExifTool available as
exiftoolonPATH - FFmpeg available as
ffmpegandffprobeonPATHforvideo_encode.py - MKVToolNix available as
mkvmergeonPATHfor VFR or frame-deletion output (Windows also checks its standard installation path) - the x264 command-line encoder available as
x264onPATHfor timestamp-aware x264 output hdr10plus_toolavailable onPATHonly when re-encoding HDR10+ video while retaining HDR- FFMS2 available to Avisynth as
FFVideoSourcebefore generated.avsscripts can load real videos, either through plugin autoload or aLoadPluginpath - 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.