Skip to content

Instantly share code, notes, and snippets.

@AlexanderMakarov
Last active January 3, 2026 16:19
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 ffmpeg and ffprobe installed.
#!/usr/bin/env python3
import sys
import json
import subprocess
import shutil
import os
import re
import threading
def check_dependencies():
"""Check if ffmpeg and ffprobe are installed."""
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)
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 run_ffmpeg(input_file, output_file, audio_streams, subtitle_streams):
"""Run ffmpeg with the selected streams and show real-time output."""
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
)
def read_stderr():
for line in iter(process.stderr.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 main():
if len(sys.argv) < 2:
print("Usage: {} <input.mkv> [output.mkv]".format(sys.argv[0]))
sys.exit(1)
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)
check_dependencies()
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}")
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