Skip to content

Instantly share code, notes, and snippets.

@mafar
Last active June 17, 2022 23:41
Show Gist options
  • Save mafar/7c7aec6e098b85b750aa697c9352f09c to your computer and use it in GitHub Desktop.
Save mafar/7c7aec6e098b85b750aa697c9352f09c to your computer and use it in GitHub Desktop.
Remux subtitles into MKV container without any quality loss using ffmpeg or mkvmerge with python
import subprocess
import os
import re
import pycountry
class RemuxingFilestoMKV:
"""
This python script combines videos and their subtitles into mkv container without loosing quality.
1- Subtitles must have same name as filename
a- filename.mp4 , filename.srt
2- Subtitles with more than one language are supported
a- filename.mp4 , filename.eng.srt, filename.de.srt
3- subtitles must be in utf-8 encoding to work properly
4- Feel free to change CONFIGURABLE SETTINGS below
Requirements:
1- install python 3.x (required to run this script)
2- Either Install ffmpeg from https://www.ffmpeg.org/download.html (required to mux)
3- or Install mkvmerge from https://mkvtoolnix.download/ (required to mux)
Notice:
You either need ffmpeg or mkvmerge as tools for muxing. You can install and configure either of both.
If you install both then you must define you preferred application by changing value
of self.FORCE_USE_TOOL = 'mkvmerge' below in CONFIGURABLE SETTINGS
"""
def __init__(self, workingDir=os.getcwd()):
#
#-------------------------------------------------------------------------------
# CONFIGURABLE SETTINGS
#-------------------------------------------------------------------------------
#
#
# path to ffmpeg executable
# self.FFMPEG_PATH = '/usr/local/bin/ffmpeg'
# self.FFMPEG_PATH = 'ffmpeg'
self.FFMPEG_PATH = 'ffmpeg'
#
#
# path to mkvmerge executable
# self.MKVMERGE_PATH = '/usr/local/bin/mkvtoolnix/mkvmerge'
# self.MKVMERGE_PATH = 'mkvmerge'
self.MKVMERGE_PATH = 'mkvmerge'
#
#
# if you have both ffmpeg and mkvmerge then define which tool script should use
# supported values are ffmpeg and mkvmerge
# self.FORCE_USE_TOOL = 'ffmpeg'
# self.FORCE_USE_TOOL = 'mkvmerge'
self.FORCE_USE_TOOL = 'mkvmerge'
#
#
# video types to support
self.SUPPORTED_VIDEO_FORMATS = ('.mkv', '.mp4', '.avi', '.m4v', '.flv', '.wmv', '.mov')
#
#
# Subtitle types to support
self.SUPPORTED_SUBTITLE_FORMATS = ('.srt', '.sub', '.ssa', '.ass', '.idx')
#
#
# Name of output file
# DO not change .mkv at the end to other containers since this script is supposed to be for mkv container
self.DEST_FILE_FORMAT = '{}-CONVERTED.mkv'
#
#
# Define pattern if you want to skip files
self.IGNORE_FILES_PATTERN = '-CONVERTED.mkv$'
#
#
# if no language is found in subtitle name then use this as language
# Expected name are like movieName.eng.srt
# but if no language is found then for example movieName.srt
# then following metadata will be used for given sub file
# self.LANGUAGE_META = 'und'
# self.LANGUAGE_META = 'en'
self.LANGUAGE_META = 'en'
#
#
# for vlc and others, set given subtitles track as default
self.LANGUAGE_DEFAULT = 'en'
#
# which directy to work with
self.WORKING_DIR = workingDir
#
#
#
def processDir(self):
#
which_tool = self.which_tool()
if not(which_tool):
print('Can not find any tool to mux')
else:
print('==============\n Using Tool ' + which_tool['path'] + '\n==============')
#
#
#
sourceList = filter(lambda f: f.endswith(self.SUPPORTED_VIDEO_FORMATS), os.listdir(self.WORKING_DIR))
sourceList = sorted(sourceList)
for source in sourceList:
source_name, file_extension = os.path.splitext(source)
#
pattern = re.compile(self.IGNORE_FILES_PATTERN)
should_ignore = pattern.search(source)
#
if not(should_ignore):
subtitlesList = ''
if (which_tool['name'] == 'ffmpeg'):
subtitlesList = self.get_ass_files(source_name)
else:
subtitlesList = self.get_files(source_name, self.SUPPORTED_SUBTITLE_FORMATS)
if (subtitlesList):
command = ''
if (which_tool['name'] == 'ffmpeg'):
command = self.build_ffmpeg_Command(source, source_name, subtitlesList)
elif (which_tool['name'] == 'mkvmerge'):
command = self.build_mkvmerge_Command(source, source_name, subtitlesList)
if (command):
print('==============\n Running Command ' + ' '.join(map(str, command)) + '\n==============')
subprocess.call(command)
else:
print('==============\n skipping ' + source + '\n==============')
def force_tool_exists(self):
tool = {}
if self.FORCE_USE_TOOL == 'mkvmerge':
tool['name'] = 'mkvmerge'
tool['path'] = self.MKVMERGE_PATH
elif self.FORCE_USE_TOOL == 'ffmpeg':
tool['name'] = 'ffmpeg'
tool['path'] = self.FFMPEG_PATH
if self.is_tool(tool['path']):
return tool
else:
return ''
def which_tool(self):
force_tool = {}
if self.FORCE_USE_TOOL:
force_tool = self.force_tool_exists()
# if forced tool is found return
if force_tool:
return force_tool
elif self.is_tool(self.MKVMERGE_PATH):
return {'name': 'mkvmerge', 'path': self.MKVMERGE_PATH}
elif self.is_tool(self.FFMPEG_PATH):
return {'name': 'ffmpeg', 'path': self.FFMPEG_PATH}
else:
return ''
def is_tool(self, name):
try:
devnull = open(os.devnull)
subprocess.Popen([name], stdout=devnull, stderr=devnull).communicate()
except OSError as e:
if e.errno == os.errno.ENOENT:
return False
return True
def build_mkvmerge_Command(self, source, source_name, subtitlesList):
dest_file_name = self.DEST_FILE_FORMAT.format(source_name)
command = [self.MKVMERGE_PATH, '-o', dest_file_name, source]
for index, subtitle in enumerate(subtitlesList):
# check if subtitle has language before extension
subNameSplitted = subtitle.split('.')
subNameSplitted.pop(-1)
language = subNameSplitted[-1]
if (language == source_name):
language = self.LANGUAGE_META
command += ['--language', '0:' + language]
if (self.LANGUAGE_DEFAULT in language):
command += ['--default-track', '0:' + str(index)]
command += [subtitle]
return command
def build_ffmpeg_Command(self, source, source_name, subtitlesList):
mapsArgument = ['-map', '0:0', '-map', '0:1']
subsArgument = []
metaArgument = []
defaultTrack = []
command = [self.FFMPEG_PATH, '-i', source]
# self.LANGUAGE_DEFAULT
dest_file_name = self.DEST_FILE_FORMAT.format(source_name)
#
copyArguments = ['-c:v', 'copy', '-c:a', 'copy']
outputFileArguments = ['-y', dest_file_name]
#
for index, subtitle in enumerate(subtitlesList):
mapsArgument += ['-map', str(index + 1) + ':0']
subsArgument += ['-i', subtitle]
# check if subtitle has language before extension
subNameSplitted = subtitle.split('.')
subNameSplitted.pop(-1)
language = subNameSplitted[-1]
if (language == source_name):
language = self.LANGUAGE_META
metaArgument += ['-metadata:s:s:' + str(index), 'language=' + language]
if (self.LANGUAGE_DEFAULT in language):
defaultTrack = ['-disposition:s:' + str(index), 'default']
#
command += subsArgument + mapsArgument + metaArgument + copyArguments + defaultTrack + outputFileArguments
return command
def get_files(self, startswith, endswith):
filesList = [f for f in os.listdir(self.WORKING_DIR) if f.endswith(endswith) and f.startswith(startswith)]
return filesList
def get_ass_files(self, fileName):
# convert all found subtitles with matching file_name to .ass format
filesList = self.get_files(fileName, self.SUPPORTED_SUBTITLE_FORMATS)
for file in filesList:
file_name, file_extension = os.path.splitext(file)
if not(file_extension == '.ass'):
command = [self.FFMPEG_PATH, '-i', file, file_name + '.ass', '-y']
subprocess.call(command)
print('==============\n Running Command ' + ' '.join(command) + '\n==============')
newList = self.get_files(fileName, '.ass')
return newList
if __name__ == "__main__":
remux = RemuxingFilestoMKV()
remux.processDir()
@mafar
Copy link
Author

mafar commented Nov 23, 2017

@valantislevas Yes it should. If not, please report it

Linux Instructions

  1. Install python 3.x ( you must have it already)
  2. Install ffmpeg

Installing ffmpeg
There are many way to do it on linux

  1. yon can compile it for example https://gist.github.com/mustafaturan/7053900
  2. or install it https://www.vultr.com/docs/how-to-install-ffmpeg-on-centos

In my script above, change the path to ffmpeg executable,
change path from self.FFMPEG_PATH = 'ffmpeg' to your ffmpeg executable like self.FFMPEG_PATH = '/usr/local/bin/ffmpeg'

@mafar
Copy link
Author

mafar commented Nov 23, 2017

@valantislevas
I updated script.
Now you can use mkvmerge instead of ffmpeg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment