Skip to content

Instantly share code, notes, and snippets.

@AlexanderMakarov
Last active January 31, 2026 18:00
Show Gist options
  • Select an option

  • Save AlexanderMakarov/be78d387d2b9bb62d12c0be3452790dc to your computer and use it in GitHub Desktop.

Select an option

Save AlexanderMakarov/be78d387d2b9bb62d12c0be3452790dc to your computer and use it in GitHub Desktop.
Interactive Python 3 script to remove audio and subtitle tracks from MKV video files without touching video track(s). Requires mkvmerge (mkvtoolnix) or (slower) ffmpeg and ffprobe CLI installed.
#!/usr/bin/env python3
import sys
import json
import subprocess
import shutil
import os
import re
import threading
import time
def check_dependencies():
"""Check if ffmpeg and ffprobe are installed. mkvmerge is optional (faster for MKV)."""
if not shutil.which('ffmpeg'):
print("Error: ffmpeg is not installed")
print("Install it with: sudo apt-get install ffmpeg")
sys.exit(1)
if not shutil.which('ffprobe'):
print("Error: ffprobe is not installed")
print("Install it with: sudo apt-get install ffmpeg")
sys.exit(1)
return shutil.which('mkvmerge') is not None
def get_stream_info(input_file):
"""Get stream information from the file using ffprobe."""
try:
result = subprocess.run(
['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', input_file],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error: Failed to analyze file: {e}")
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Failed to parse stream information: {e}")
sys.exit(1)
def display_tracks(streams, track_type):
"""Display tracks of a specific type (audio or subtitle)."""
tracks = []
for stream in streams:
if stream.get('codec_type') == track_type:
idx = stream.get('index')
codec_name = stream.get('codec_name', 'unknown')
codec_long = stream.get('codec_long_name', '')
language = stream.get('tags', {}).get('language', 'unknown')
title = stream.get('tags', {}).get('title', '')
tracks.append({
'index': idx,
'codec': codec_name,
'codec_long': codec_long,
'language': language,
'title': title
})
return tracks
def get_user_selection(tracks, track_type):
"""Get user selection for which tracks to keep."""
if not tracks:
print(f"\nNo {track_type} tracks found.")
return []
print(f"\n{track_type.capitalize()} tracks:")
for i, track in enumerate(tracks):
info_parts = [f"Stream {track['index']}", track['codec']]
if track['language'] != 'unknown':
info_parts.append(f"lang: {track['language']}")
if track['title']:
info_parts.append(f"title: {track['title']}")
print(f" [{i}] {' | '.join(info_parts)}")
print(f"\nEnter {track_type} track numbers to keep (space-separated, or 'all' for all, or 'none' for none):")
user_input = input("> ").strip().lower()
if user_input == 'all':
return [track['index'] for track in tracks]
elif user_input == 'none' or user_input == '':
return []
try:
indices = [int(x.strip()) for x in user_input.split()]
selected = []
for idx in indices:
if 0 <= idx < len(tracks):
selected.append(tracks[idx]['index'])
else:
print(f"Warning: Track number {idx} is out of range, skipping.")
return selected
except ValueError:
print(f"Error: Invalid input. Please enter space-separated numbers.")
return get_user_selection(tracks, track_type)
def _format_eta(seconds: float) -> str:
"""Format seconds as M:SS or H:MM:SS for ETA display."""
if seconds < 0 or seconds != seconds: # NaN
return "?"
s = int(round(seconds))
if s < 60:
return f"{s}s"
m, s = divmod(s, 60)
if m < 60:
return f"{m}:{s:02d}s"
h, m = divmod(m, 60)
return f"{h}:{m:02d}:{s:02d}s"
def run_mkvmerge(input_file, output_file, audio_streams, subtitle_streams):
"""Run mkvmerge to keep only selected tracks. Much faster than ffmpeg for MKV remux."""
# mkvmerge track IDs match ffprobe stream index for MKV (both 0-based, same order)
# Run without -q so mkvmerge prints progress; we stream it and optionally show ETA
cmd = ['mkvmerge', '-o', output_file]
if audio_streams:
cmd.extend(['--audio-tracks', ','.join(str(i) for i in audio_streams)])
else:
cmd.append('-A') # --no-audio
if subtitle_streams:
cmd.extend(['--subtitle-tracks', ','.join(str(i) for i in subtitle_streams)])
else:
cmd.append('-S') # --no-subtitles
cmd.append(input_file)
print(f"\nRunning mkvmerge (fast MKV remux):")
print(f" {' '.join(cmd)}")
print()
progress_pct_re = re.compile(r'(\d+)\s*%')
start_time = time.monotonic()
last_pct = -1
last_eta_print = 0.0
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
)
out = process.stdout
if out:
for line in iter(out.readline, ''):
line = line.rstrip('\n\r')
if not line:
continue
# Forward mkvmerge's progress line (may use \r, so we print with \r for in-place update)
is_progress = '%' in line
if is_progress:
sys.stdout.write('\r' + line)
sys.stdout.flush()
# Parse percentage and show ETA
m = progress_pct_re.search(line)
if m:
pct = int(m.group(1))
if 0 < pct <= 100 and pct != last_pct:
last_pct = pct
elapsed = time.monotonic() - start_time
now = time.monotonic()
if elapsed > 2 and pct >= 2 and (now - last_eta_print) >= 1.0:
last_eta_print = now
remaining = elapsed * (100 - pct) / pct
eta_str = _format_eta(remaining)
sys.stdout.write(f" ETA {eta_str} ")
sys.stdout.flush()
else:
if last_pct >= 0:
sys.stdout.write('\n')
sys.stdout.flush()
last_pct = -1
print(line)
if last_pct >= 0:
sys.stdout.write('\n')
sys.stdout.flush()
process.wait()
if process.returncode != 0:
print(f"\nError: mkvmerge failed with return code {process.returncode}")
sys.exit(1)
except KeyboardInterrupt:
print("\n\nProcess interrupted by user.")
sys.exit(1)
except Exception as e:
print(f"\nError running mkvmerge: {e}")
sys.exit(1)
def run_ffmpeg(input_file, output_file, audio_streams, subtitle_streams):
"""Run ffmpeg with the selected streams and show real-time output (fallback)."""
cmd = ['ffmpeg', '-i', input_file]
cmd.extend(['-map', '0:v'])
for audio_idx in audio_streams:
cmd.extend(['-map', f'0:{audio_idx}'])
for sub_idx in subtitle_streams:
cmd.extend(['-map', f'0:{sub_idx}'])
cmd.extend(['-c', 'copy', '-y', output_file])
print(f"\nRunning ffmpeg command:")
print(f" {' '.join(cmd)}")
print(f"\nProcessing... (this may take a while)\n")
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1
)
stderr_fd = process.stderr
def read_stderr():
if stderr_fd:
for line in iter(stderr_fd.readline, ''):
if line:
print(line, end='', flush=True)
stderr_thread = threading.Thread(target=read_stderr, daemon=True)
stderr_thread.start()
process.wait()
stderr_thread.join(timeout=1)
if process.returncode != 0:
print(f"\nError: ffmpeg failed with return code {process.returncode}")
sys.exit(1)
except KeyboardInterrupt:
print("\n\nProcess interrupted by user.")
if 'process' in locals() and process.poll() is None:
process.terminate()
sys.exit(1)
except Exception as e:
print(f"\nError running ffmpeg: {e}")
sys.exit(1)
def format_size(size_bytes):
"""Format file size in human-readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} PB"
def print_usage():
"""Print usage and help to stdout."""
prog = os.path.basename(sys.argv[0])
print(f"Usage: {prog} <input.mkv> [output.mkv]")
print()
print(" input.mkv Input MKV file (required)")
print(" output.mkv Output file (optional). If omitted, defaults to <input>_processed.mkv")
print()
print("Options:")
print(" -h, --help Show this help and exit")
print()
print("Interactive track selection for audio and subtitles; video is always kept.")
def main():
if len(sys.argv) < 2:
print_usage()
sys.exit(1)
if sys.argv[1] in ('--help', '-h'):
print_usage()
sys.exit(0)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else input_file.replace('.mkv', '_processed.mkv')
if not os.path.isfile(input_file):
print(f"Error: Input file '{input_file}' not found")
sys.exit(1)
use_mkvmerge = check_dependencies()
if use_mkvmerge:
print("Using mkvmerge for fast MKV remux (video + selected audio/subs only).")
else:
print("mkvmerge not found; using ffmpeg (install mkvtoolnix for faster processing).")
print(f"Analyzing file: {input_file}")
stream_data = get_stream_info(input_file)
streams = stream_data.get('streams', [])
audio_tracks = display_tracks(streams, 'audio')
subtitle_tracks = display_tracks(streams, 'subtitle')
selected_audio = get_user_selection(audio_tracks, 'audio')
selected_subtitles = get_user_selection(subtitle_tracks, 'subtitle')
if not selected_audio and not selected_subtitles:
print("\nWarning: No audio or subtitle tracks selected. Only video will be copied.")
response = input("Continue? (y/n): ").strip().lower()
if response != 'y':
print("Aborted.")
sys.exit(0)
print(f"\nSelected audio tracks: {selected_audio if selected_audio else 'none'}")
print(f"Selected subtitle tracks: {selected_subtitles if selected_subtitles else 'none'}")
print(f"Output file: {output_file}")
if use_mkvmerge:
run_mkvmerge(input_file, output_file, selected_audio, selected_subtitles)
else:
run_ffmpeg(input_file, output_file, selected_audio, selected_subtitles)
if os.path.isfile(output_file):
input_size = os.path.getsize(input_file)
output_size = os.path.getsize(output_file)
print(f"\n{'='*60}")
print("Success! File processed.")
print(f"Original file size: {format_size(input_size)}")
print(f"New file size: {format_size(output_size)}")
print(f"{'='*60}")
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment