Last active
January 3, 2026 16:19
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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