Skip to content

Instantly share code, notes, and snippets.

@natyusha
Last active September 7, 2025 14:11
Show Gist options
  • Select an option

  • Save natyusha/2d7877de2c6924bafd6fc65f66a61178 to your computer and use it in GitHub Desktop.

Select an option

Save natyusha/2d7877de2c6924bafd6fc65f66a61178 to your computer and use it in GitHub Desktop.
A utility that automatically compresses timestamps for mkv chapter xml files from 23.976fps sources that have been converted to 24fps (only affects ChapterTimeStart as other timestamps can be removed)
import os, sys
import tkinter as tk
import xml.etree.ElementTree as ET
try:
from tkinterdnd2 import TkinterDnD, DND_FILES
except ImportError:
print('Please install tkinterdnd2: pip install tkinterdnd2')
sys.exit(1)
r"""
Description:
- A utility that automatically compresses timestamps for mkv chapter xml files from 23.976fps sources that have been converted to 24fps
- This only affects ChapterTimeStart (other time stamps are not required and can be safely removed) and will overwrite the xml file(s)
Author:
- natyusha
Requirements:
- programs : python 3.7+
- pip : tkinterdnd2 (pip install tkinterdnd2)
Usage:
- Run the script and drag chapter xml file(s) into the window
"""
def parse_time(time_str):
"""Convert Matroska timestamp (HH:MM:SS.nnnnnnnnn) to milliseconds"""
time_parts = time_str.split(':')
hours = int(time_parts[0])
minutes = int(time_parts[1])
seconds = float(time_parts[2])
total_ms = (hours * 3600 + minutes * 60 + seconds) * 1000
return total_ms
def format_time(ms):
"""Convert milliseconds back to Matroska timestamp format (HH:MM:SS.nnnnnnnnn), rounded to 3 decimal places"""
seconds = ms / 1000
hours = int(seconds // 3600)
seconds %= 3600
minutes = int(seconds // 60)
seconds = seconds % 60
seconds = round(seconds, 3) # round seconds to 3 decimal places
return f'{hours:02d}:{minutes:02d}:{seconds:012.9f}' # format with 9 decimal places, padding with zeros
def convert_chapters(input_file, console):
"""Apply 23.976fps to 24fps conversion to ChapterTimeStart timestamps"""
scale_factor = 23.976 / 24 # Approximately 0.999 (999 / 1000)
try:
# parse the XML file
tree = ET.parse(input_file)
root = tree.getroot()
# process all ChapterTimeStart elements
for chapter in root.findall('.//ChapterTimeStart'):
time_ms = parse_time(chapter.text)
scaled_ms = time_ms * scale_factor
chapter.text = format_time(scaled_ms)
# overwrite the original file
tree.write(input_file, encoding='utf-8', xml_declaration=True)
console.insert(tk.END, f'Success: Timestamps converted in:\n{os.path.basename(input_file)}\n')
console.see(tk.END)
return True
except ET.ParseError:
console.insert(tk.END, f'Error: Failed to parse XML file:\n{input_file}\n')
console.see(tk.END)
except FileNotFoundError:
console.insert(tk.END, f'Error: File not found:\n{input_file}\n')
console.see(tk.END)
except Exception as e:
console.insert(tk.END, f'Error: An error occurred:\n{e}\n')
console.see(tk.END)
return False
def on_drop(event, console):
"""Handle drag-and-drop for multiple files"""
# get the dropped file path
file_paths = event.data
# handle TkinterDnD2 formatting (braced or space-separated paths)
if file_paths.startswith('{') and file_paths.endswith('}'):
file_paths = file_paths[1:-1].split('} {')
else:
file_paths = file_paths.split()
# process each file
for file_path in file_paths:
if os.path.isfile(file_path) and file_path.lower().endswith('.xml'):
convert_chapters(file_path, console)
else:
console.insert(tk.END, f'Error: Not a valid XML chapter file:\n{file_path}\n')
console.see(tk.END)
def create_gui():
"""Create the GUI window for drag-and-drop with a console"""
root = TkinterDnD.Tk() # use TkinterDnD2.Tk for drag-and-drop support
root.title('23.976fps to 24fps Chapters')
root.geometry('500x200')
root.resizable(False, False)
# create a label to indicate drag-and-drop area
label = tk.Label(
root,
text='Drag and Drop a Chapter XML File Here',
font='TkHeadingFont',
justify='center',
borderwidth=2,
relief='groove',
padx=10,
pady=10
)
label.pack(expand=False, fill='both', padx=10, pady=10)
# create a scrollable console for messages
console = tk.Text(
root,
font='TkFixedFont',
wrap='word',
state='normal'
)
console.pack(expand=True, fill='both', padx=10, pady=10)
# add scrollbar to the console
scrollbar = tk.Scrollbar(root, orient='vertical', command=console.yview)
scrollbar.pack(side='right', fill='y')
console['yscrollcommand'] = scrollbar.set
# bind drag-and-drop events to the root window
root.drop_target_register(DND_FILES)
root.dnd_bind('<<Drop>>', lambda event: on_drop(event, console))
root.mainloop()
def main():
create_gui()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment