Python Script to remove empty pgs tracks from mkv files

I trying to create a python batch code that uses mkvtoolnix to detect and remove empty pgs tracks from mkv files. i got so i manage to detect and print which tracks to keep, but it’s having problems actually removing the empty track

here is the script:

import os
import json
import subprocess

def run_command(command):
    """Run a shell command and return the output."""
    try:
        print(f"Running command: {' '.join(command)}")  # Debugging
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        print(f"stdout: {result.stdout}")  # Debugging
        print(f"stderr: {result.stderr}")  # Debugging
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f"Error running command: {' '.join(command)}")
        print(f"stderr: {e.stderr}")
        print(f"stdout: {e.stdout}")
        return None

def get_mkv_metadata(mkv_file):
    """Extracts MKV metadata using mkvmerge."""
    output = run_command(["mkvmerge", "-J", mkv_file])
    return json.loads(output) if output else None

def get_empty_pgs_tracks(mkv_file):
    """Finds empty PGS subtitle tracks."""
    metadata = get_mkv_metadata(mkv_file)
    if not metadata:
        return []

    empty_tracks = []
    for track in metadata["tracks"]:
        if track["type"] == "subtitles" and "PGS" in track["properties"].get("codec_id", ""):
            track_id = track["id"]
            if is_pgs_track_empty(mkv_file, track_id):
                empty_tracks.append(track_id)
    
    print(f"Empty PGS subtitle tracks: {empty_tracks}")  # Debugging
    return empty_tracks

def is_pgs_track_empty(mkv_file, track_id):
    """Checks if a PGS track is empty using FFmpeg by extracting frames."""
    temp_dir = "pgs_temp"
    os.makedirs(temp_dir, exist_ok=True)
    output_pattern = os.path.join(temp_dir, f"track_{track_id}_%03d.png")

    run_command(["ffmpeg", "-y", "-loglevel", "error", "-i", mkv_file, "-map", f"0:{track_id}", output_pattern])
    
    images = [f for f in os.listdir(temp_dir) if f.startswith(f"track_{track_id}_")]
    is_empty = len(images) == 0  # True if no images (empty track)
    
    # Cleanup extracted images
    for file in images:
        os.remove(os.path.join(temp_dir, file))
    os.rmdir(temp_dir)

    return is_empty

def get_empty_subtitle_tracks(mkv_file):
    """Finds empty subtitle tracks (SUP format)."""
    metadata = get_mkv_metadata(mkv_file)
    if not metadata:
        return []

    empty_tracks = []
    for track in metadata["tracks"]:
        if track["type"] == "subtitles":
            track_id = track["id"]
            if is_subtitle_track_empty(mkv_file, track_id):
                empty_tracks.append(track_id)

    print(f"Empty subtitle tracks: {empty_tracks}")  # Debugging
    return empty_tracks

def is_subtitle_track_empty(mkv_file, track_id):
    """Extracts and checks if a subtitle track is empty."""
    temp_file = f"subtitle_{track_id}.sup"
    run_command(["mkvextract", "tracks", mkv_file, f"{track_id}:{temp_file}"])

    if os.path.exists(temp_file):
        if os.path.getsize(temp_file) <= 100:  # Very small or empty
            os.remove(temp_file)
            return True
        os.remove(temp_file)

    return False

def remove_empty_tracks(mkv_file):
    """Removes empty subtitle and PGS tracks from an MKV file."""
    empty_pgs_tracks = get_empty_pgs_tracks(mkv_file)
    empty_subtitle_tracks = get_empty_subtitle_tracks(mkv_file)

    metadata = get_mkv_metadata(mkv_file)
    if not metadata:
        print(f"Failed to get metadata for {mkv_file}.")
        return

    # Debugging: Show all detected tracks
    print("\n--- Track List ---")
    for track in metadata["tracks"]:
        print(f"Track {track['id']} | Type: {track['type']} | Codec: {track['properties'].get('codec_id', '')}")

    # Identify valid tracks to keep
    valid_tracks = [
        track["id"] for track in metadata["tracks"] 
        if track["type"] != "subtitles" or track["id"] not in empty_subtitle_tracks + empty_pgs_tracks
    ]

    print(f"\nValid tracks to keep: {valid_tracks}")  # Debugging

    if not empty_pgs_tracks and not empty_subtitle_tracks:
        print(f"No empty subtitle or PGS tracks found in {mkv_file}. Skipping remux.")
        return

    output_file = mkv_file.replace(".mkv", "_cleaned.mkv")
    mkvmerge_cmd = ["mkvmerge", "-o", output_file, mkv_file]

    # Only include valid tracks
    track_order = ",".join([f"0:{track_id}" for track_id in valid_tracks])
    mkvmerge_cmd.extend(["--track-order", track_order])


    print(f"\nFinal mkvmerge command: {' '.join(mkvmerge_cmd)}")  # Debugging
    run_command(mkvmerge_cmd)

def process_directory(directory_path):
    """Processes all MKV files in the given directory."""
    for file_name in os.listdir(directory_path):
        if file_name.lower().endswith(".mkv"):
            mkv_file = os.path.join(directory_path, file_name)
            remove_empty_tracks(mkv_file)

if __name__ == "__main__":
    directory_path = r"C:\Users\travi\Videos\MediaRips"
    process_directory(directory_path)

Welcome!

This is wrong. The --track-order option does not control which tracks are copied and which aren’t. It only sets the order in which the tracks that are copied are created in the output file. In general you only need --track-order if you want to change the order from what it is in the source file.

You’ll need to look into the options --stracks (selecting which subtitle tracks to keep) & --no-subtitles (if no subtitle tracks should be kept).

The rest looks OK to me, but I’ve only glanced over it.

For something i’m getting an error: Error: mkvmerge is not installed or not in the system path.
Exiting: Required tools are missing. it is installed and in the system path

I must be doing something wrong because it’s still not removing the empty tracks

import os
import json
import subprocess

def run_command(command):
“”“Run a shell command and return the output.”“”
try:
print(f"Running command: {’ ‘.join(command)}“) # Debugging
result = subprocess.run(command, capture_output=True, text=True, check=True)
print(f"stdout: {result.stdout}”) # Debugging
print(f"stderr: {result.stderr}") # Debugging
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error running command: {’ '.join(command)}“)
print(f"stderr: {e.stderr}”)
print(f"stdout: {e.stdout}")
return None

def get_mkv_metadata(mkv_file):
“”“Extracts MKV metadata using mkvmerge.”“”
output = run_command([“mkvmerge”, “-J”, mkv_file])
return json.loads(output) if output else None

def get_empty_pgs_tracks(mkv_file):
“”“Finds empty PGS subtitle tracks.”“”
metadata = get_mkv_metadata(mkv_file)
if not metadata:
return

empty_tracks = []
for track in metadata["tracks"]:
    if track["type"] == "subtitles" and "PGS" in track["properties"].get("codec_id", ""):
        track_id = track["id"]
        if is_pgs_track_empty(mkv_file, track_id):
            empty_tracks.append(track_id)

print(f"Empty PGS subtitle tracks: {empty_tracks}")  # Debugging
return empty_tracks

def is_pgs_track_empty(mkv_file, track_id):
“”“Checks if a PGS track is empty using FFmpeg by extracting frames.”“”
temp_dir = “pgs_temp”
os.makedirs(temp_dir, exist_ok=True)
output_pattern = os.path.join(temp_dir, f"track_{track_id}_%03d.png")

run_command(["ffmpeg", "-y", "-loglevel", "error", "-i", mkv_file, "-map", f"0:{track_id}", output_pattern])

images = [f for f in os.listdir(temp_dir) if f.startswith(f"track_{track_id}_")]
is_empty = len(images) == 0  # True if no images (empty track)

# Cleanup extracted images
for file in images:
    os.remove(os.path.join(temp_dir, file))
os.rmdir(temp_dir)

return is_empty

def get_empty_subtitle_tracks(mkv_file):
“”“Finds empty subtitle tracks (SUP format).”“”
metadata = get_mkv_metadata(mkv_file)
if not metadata:
return

empty_tracks = []
for track in metadata["tracks"]:
    if track["type"] == "subtitles":
        track_id = track["id"]
        if is_subtitle_track_empty(mkv_file, track_id):
            empty_tracks.append(track_id)

print(f"Empty subtitle tracks: {empty_tracks}")  # Debugging
return empty_tracks

def is_subtitle_track_empty(mkv_file, track_id):
“”“Extracts and checks if a subtitle track is empty.”“”
temp_file = f"subtitle_{track_id}.sup"
run_command([“mkvextract”, “tracks”, mkv_file, f"{track_id}:{temp_file}"])

if os.path.exists(temp_file):
    if os.path.getsize(temp_file) <= 100:  # Very small or empty
        os.remove(temp_file)
        return True
    os.remove(temp_file)

return False

def remove_empty_tracks(mkv_file, stracks=None, no_subtitles=False):
“”“Removes empty subtitle and PGS tracks from an MKV file.”“”
empty_pgs_tracks = get_empty_pgs_tracks(mkv_file)
empty_subtitle_tracks = get_empty_subtitle_tracks(mkv_file)

metadata = get_mkv_metadata(mkv_file)
if not metadata:
    print(f"Failed to get metadata for {mkv_file}.")
    return

# Debugging: Show all detected tracks
print("\n--- Track List ---")
for track in metadata["tracks"]:
    print(f"Track {track['id']} | Type: {track['type']} | Codec: {track['properties'].get('codec_id', '')}")

# Identify valid tracks to keep
valid_tracks = [
    track["id"] for track in metadata["tracks"] 
    if track["type"] != "subtitles" or track["id"] not in empty_subtitle_tracks + empty_pgs_tracks
]

# Apply --stracks filter if specified
if stracks:
    strack_ids = set(map(int, stracks.split(',')))
    valid_tracks = [track_id for track_id in valid_tracks if track_id in strack_ids]

# Apply --no-subtitles filter if set to True
if no_subtitles:
    valid_tracks = [track_id for track_id in valid_tracks if track_id not in empty_subtitle_tracks]

print(f"\nValid tracks to keep: {valid_tracks}")  # Debugging

if not empty_pgs_tracks and not empty_subtitle_tracks and not stracks and not no_subtitles:
    print(f"No empty subtitle or PGS tracks found in {mkv_file}. Skipping remux.")
    return

output_file = mkv_file.replace(".mkv", "_cleaned.mkv")
mkvmerge_cmd = ["mkvmerge", "-o", output_file, mkv_file]

# Only include valid tracks
track_order = ",".join([f"0:{track_id}" for track_id in valid_tracks])
mkvmerge_cmd.extend(["--track-order", track_order])

print(f"\nFinal mkvmerge command: {' '.join(mkvmerge_cmd)}")  # Debugging
run_command(mkvmerge_cmd)

def process_directory(directory_path, stracks=None, no_subtitles=False):
“”“Processes all MKV files in the given directory.”“”
for file_name in os.listdir(directory_path):
if file_name.lower().endswith(“.mkv”):
mkv_file = os.path.join(directory_path, file_name)
remove_empty_tracks(mkv_file, stracks, no_subtitles)

if name == “main”:
directory_path = r"C:\Users\travi\Videos\MediaRips"

# Example usage: stracks="1,2" to keep subtitle tracks 1 and 2
# Example usage: no_subtitles=True to remove all subtitle tracks
stracks = "1,2"  # Replace with desired track IDs
no_subtitles = False  # Set to True to remove all subtitles
process_directory(directory_path, stracks=stracks, no_subtitles=no_subtitles)

i finally figured out the script by using a program called pymkv

import os
import json
import subprocess
from pymkv import MKVFile

def get_pgs_tracks(mkv_file):
“”“Extracts track information using mkvmerge -J and finds PGS subtitle tracks.”“”
try:
result = subprocess.run([“mkvmerge”, “-J”, mkv_file], capture_output=True, text=True, check=True)
mkv_info = json.loads(result.stdout)

    pgs_tracks = []
    for track in mkv_info.get("tracks", []):
        if track["type"] == "subtitles" and track["codec"] == "HDMV PGS":
            track_id = track["id"]
            pgs_tracks.append(track_id)

    return pgs_tracks
except subprocess.CalledProcessError:
    print(f"Error reading {mkv_file} with mkvmerge.")
    return []

def is_pgs_empty(mkv_file, track_id):
“”“Check if a PGS subtitle track is empty using mkvextract.”“”
try:
temp_file = f"temp_track_{track_id}.sup"
cmd = [“mkvextract”, “tracks”, mkv_file, f"{track_id}:{temp_file}"]
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)

    if os.path.exists(temp_file) and os.path.getsize(temp_file) < 1024:  # Less than 1KB
        os.remove(temp_file)
        return True  # Empty PGS track

    os.remove(temp_file)  # Cleanup
except subprocess.CalledProcessError:
    print(f"Failed to extract track {track_id}, skipping check.")

return False

def remove_chapters(mkv_file):
“”“Removes chapters by replacing them with an empty chapter file.”“”
try:
temp_chapter_file = “empty_chapters.xml”

    # Create an empty chapter file
    with open(temp_chapter_file, "w", encoding="utf-8") as f:
        f.write('<?xml version="1.0"?>\n<Chapters>\n</Chapters>\n')

    # Use mkvpropedit to replace the chapters with an empty one
    subprocess.run(["mkvpropedit", mkv_file, "--chapters", temp_chapter_file], check=True)
    os.remove(temp_chapter_file)  # Cleanup
    print("Removed chapters successfully.")
    return True
except subprocess.CalledProcessError:
    print("Failed to remove chapters.")
    return False

def remove_empty_pgs_and_chapters(file_path):
“”“Remove empty PGS subtitle tracks and chapters from an MKV file.”“”
print(f"Processing {file_path}")
mkv = MKVFile(file_path)
removed_track = False

pgs_tracks = get_pgs_tracks(file_path)

for track_id in pgs_tracks:
    if is_pgs_empty(file_path, track_id):
        mkv.remove_track(track_id)
        removed_track = True
        print(f"Removed empty PGS track {track_id}")

# Remove chapters using mkvpropedit (fixes duplication issue)
chapters_removed = remove_chapters(file_path)

if removed_track:
    output_file = os.path.join(os.path.dirname(file_path), "cleaned_" + os.path.basename(file_path))
    mkv.mux(output_file)
    print(f"Saved the file as {output_file}")
elif chapters_removed:
    print("Chapters removed, but no empty PGS tracks found.")
else:
    print("No empty PGS tracks or chapters found.")

def process_directory(directory_path):
“”“Process all MKV files in the specified directory.”“”
for root, _, files in os.walk(directory_path):
for file in files:
if file.lower().endswith(‘.mkv’):
file_path = os.path.join(root, file)
remove_empty_pgs_and_chapters(file_path)

if name == “main”:
directory_path = “C:/Users/travi/Videos/MediaRips”
process_directory(directory_path)