Last active
June 17, 2022 23:41
-
-
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
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
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@valantislevas Yes it should. If not, please report it
Linux Instructions
Installing ffmpeg
There are many way to do it on linux
In my script above, change the path to ffmpeg executable,
change path from
self.FFMPEG_PATH = 'ffmpeg'
to your ffmpeg executable likeself.FFMPEG_PATH = '/usr/local/bin/ffmpeg'