Script to merge Video and subs then delete the existing files (non recursive)
I've decided to gain some experience with Bash and wrote the below script, that has next features:
- It can handle all files and folders, placed into the current directory.
- If there are more than one subtitle or video file (except sample files), the script will ask for manual interaction.
- In all other cases the script will automatically move all files and folders into a backup directory (be careful where you run it!) , that will be moved into the user's trash folder, instead of to be deleted.
- It can handle several types of video and subtitle extensions.
- It uses
mkvmerge
to merge video with subtitle files also usesnotify-send
to show some messages in GUI. Also it usesgvfs-trash
to move files in user's trash folder. - It can be used as Nautilus script or as regular shell script but GIU environment is required while
notify-send
command exists into the script's body. - The name of the output file could be based on the directory name (default) or on the name of the source video file.
- Additionally when only two files are selected (one video and one subtitle file), they will be merged and rest of the folder's content will be retained. The output file will be named after the source video file. This option is available only when the script is used as Nautilus script.
The script:
#!/bin/bash -e
# Check if all tools are available
[ -x /usr/bin/notify-send ] || (echo "Please, install 'notify-send'"; exit 1)
[ -x /usr/bin/mkvmerge ] || (echo "Please, install 'mkvmerge'"; exit 1)
# Allowed video and subtitle file extensions
EXT_VIDEO=("mp4 avi mpg mov mkv wmv")
EXT_SUB=("sub str srt vtt")
# Files, which names contains some of next strings will be removed in auto mode
FILTER=("sample Sample SAMPLE")
# Log file
MERGE_LOG="/tmp/merge-video-sub.log"
echo > "$MERGE_LOG"
#
# Functions
#
function get-video-and-sub-file-names {
# Get the names of the video and subtitle files and move the rest of the files into the Backup directory
for ((i=0; i<${#FILE_LIST[@]}; i++)); do
FILE_NAME="${FILE_LIST[$i]%.*}"
FILE_EXT="${FILE_LIST[$i]##*.}"
if [[ "${EXT_SUB[@]}" == *"$FILE_EXT"* ]]; then
SUB_FULL_FILE_NAME="${FILE_LIST[$i]}"
SUB_FILE_NAME="${FILE_NAME}"
SUB_FILE_EXT="${FILE_EXT}"
elif [[ "${EXT_VIDEO[@]}" == *"$FILE_EXT"* ]]; then
VIDEO_FULL_FILE_NAME="${FILE_LIST[$i]}"
VIDEO_FILE_NAME="${FILE_NAME}"
VIDEO_FILE_EXT="${FILE_EXT}"
else
# We need 'find' to manipulate only with files, because "$BACKUP_DIR" is in the queue
find ./* -maxdepth 0 -type f -name "${FILE_LIST[$i]}" -exec mv "{}" "$BACKUP_DIR" \; -exec echo -e "The file {} was REMOVED.\n" >> "$MERGE_LOG" \;
fi
done
}
function get-the-content-of-the-current-directory {
# Get the content of the current directory
shopt -s nullglob
FILE_LIST=(*)
shopt -u nullglob
}
function mkvmerge-video-and-sub-files {
# Create merged file
mkvmerge -o "$OUTPUT_FILE" "$VIDEO_FULL_FILE_NAME" "$SUB_FULL_FILE_NAME"
sleep 3
}
#
# Scenario 1: If exactly two files are selected in Nautilus! Then check if they are 1 video and 1 subtitle files, if yes - merge and remove them
# Scenario 2: Else run the standard procedure
#
# Get the files, selected in Nautilus as file list. Use next command to check the result: notify-send "MESSAGE" "`echo -e "${#FILE_LIST[@]}"; printf '%s\n' "${FILE_LIST[@]}"`"
IFS_BAK=$IFS
IFS=$'\t\n'
FILE_LIST=($NAUTILUS_SCRIPT_SELECTED_FILE_PATHS)
IFS=$IFS_BAK
if [ "${#FILE_LIST[@]}" -eq "2" ]
then # Scenario 1
# Get the names of the video and subtitle files
get-video-and-sub-file-names
if [[ "${EXT_SUB[@]}" == *" $SUB_FILE_EXT "* ]] && [[ "${EXT_VIDEO[@]}" == *" $VIDEO_FILE_EXT "* ]]
then
notify-send "OK" "`echo -e "The following files will be MERGED and MOVED to trash:"; printf '\t-\ %s\n' "${FILE_LIST[@]##*/}"`"
# Construct the name of the merged file.
OUTPUT_FILE="${VIDEO_FILE_NAME}.sub.mkv"
# Merge the files
mkvmerge-video-and-sub-files
# Move video and subtitle files into user's trash directory and create trash infofile
if [ -f "$OUTPUT_FILE" ]
then
gvfs-trash "$VIDEO_FULL_FILE_NAME"
gvfs-trash "$SUB_FULL_FILE_NAME"
notify-send "OK" "`echo -e "THE NAME OF THE NEW MERGED FILE IS:\n${OUTPUT_FILE##*/}"`"
else
notify-send "ERROR 1" "`echo "Something went wrong!"`"
fi
else
notify-send "ERROR" "`echo -e "\n\t\nTo use this function, please select exactly:\n\t- 1 video file and\n\t- 1 subtitle file!\n\t\nYou are selected these files:"; printf '\t-\ %s\n' "${FILE_LIST[@]##*/}"`"
fi
else # Scenario 2
# Get the current directory name
DIR_NAME="${PWD##*/}"
# Create Backup sub-directory
BACKUP_DIR="${DIR_NAME}.backup"
[ -d "${BACKUP_DIR}" ] || mkdir "$BACKUP_DIR" && echo "The directory $BACKUP_DIR was CREATED.\n" > "$MERGE_LOG"
# Move all sub-directories into the Backup directory
shopt -s dotglob
find ./* -maxdepth 0 -type d ! -name "*$BACKUP_DIR*" -prune -exec mv "{}" "$BACKUP_DIR" \; -exec echo "The directory {} was REMOVED.\n" >> "$MERGE_LOG" \;
shopt -u dotglob
# Move all files and folders, whose names contains a string, that exists in $FILTER[@]
for f in $FILTER; do
shopt -s dotglob
find ./* -maxdepth 0 ! -name "*$BACKUP_DIR*" -type f -name "*$f*" -exec mv "{}" "$BACKUP_DIR" \; -exec echo "The file {} was REMOVED.\n" >> "$MERGE_LOG" \;
shopt -u dotglob
done
# Get the entire content of the current directory
get-the-content-of-the-current-directory
# Get the names of the video and subtitle files and move the rest of the files into the Backup directory
get-video-and-sub-file-names
# Construct the name of the merged file. It could be based on the parent directory or on the video file name Make your choice and comment/uncomment next lines
#OUTPUT_FILE="${VIDEO_FILE_NAME}.sub.mkv"
OUTPUT_FILE="${DIR_NAME}.sub.mkv"
# Get the entire content of the current directory after the filtering
get-the-content-of-the-current-directory
echo -e "$(cat $MERGE_LOG)" && notify-send "OK" "`echo -e "$(cat $MERGE_LOG)"`" && echo > "$MERGE_LOG"
# Check the current structure of the directory
if [ "${#FILE_LIST[@]}" -ne "3" ]; then
echo "The content structure must consists of next 3 items:" > "$MERGE_LOG"
echo "\t- 1 movie file,\n\t- 1 subtitle file and\n\t- 1 backup directory." >> "$MERGE_LOG"
echo "\n\t\nThe current number of contained items is ${#FILE_LIST[@]}." >> "$MERGE_LOG" && echo >> "$MERGE_LOG"
echo "\n\t\nPLEASE RESOLVE THIS MANUALLY!" >> "$MERGE_LOG"
echo -e "$(cat $MERGE_LOG)" && notify-send "ERROR" "`echo -e "$(cat $MERGE_LOG)"`"
else
echo "The directory structure looks good, is contains ${#FILE_LIST[@]} items." > "$MERGE_LOG"
echo " - The source VIDEO file is: ${VIDEO_FULL_FILE_NAME::21}... .${VIDEO_FULL_FILE_NAME##*.}" >> "$MERGE_LOG"
echo " - The source SUB file is: ${SUB_FULL_FILE_NAME::25}... .${SUB_FULL_FILE_NAME##*.}" >> "$MERGE_LOG"
echo "They has been merged and removed!" | tr /a-z/ /A-Z/ >> "$MERGE_LOG"
# Merge the files
mkvmerge-video-and-sub-files
# Move video and subtitle files into the Backup directory
mv "$VIDEO_FULL_FILE_NAME" "$BACKUP_DIR"
mv "$SUB_FULL_FILE_NAME" "$BACKUP_DIR"
# Move the Backup directory to trash and create trash infofile
if [ -f "$OUTPUT_FILE" ]; then
gvfs-trash "$BACKUP_DIR"
echo "\n\t\nThe Backup directory has been MOVED to Trash!\n\t\n" >> "$MERGE_LOG"
echo "The name of the new merged file is:" | tr /a-z/ /A-Z/ >> "$MERGE_LOG"
echo "${OUTPUT_FILE##*/}" >> "$MERGE_LOG"
echo -e "$(cat $MERGE_LOG)" && notify-send "OK" "`echo -e "$(cat $MERGE_LOG)"`"
else
echo "Something went wrong!" && notify-send "ERROR 2" "`echo "Something went wrong!"`"
fi
fi
fi
rm "$MERGE_LOG"
exit 1
Setup:
Create executable file and paste the above content inside. Let's call this file
merge-video-sub
:touch merge-video-sub chmod +x merge-video-sub nano merge-video-sub
Copy (or
ln -s
) this file into the folder~/.local/share/nautilus/scripts
to make it available as Nautilus script for the current user.At the moment I can't find a way how to make it available system wide as Nautilus script.
Copy the file into
~/bin
(and addexport PATH=$PATH:~/bin
to the bottom of~/.bashrc
if is needed) to make it available as shell command for the current user.Copy the file into
/usr/local/bin
to make it available as shell command system wide.The short way is to
curl
the script from my PasteBin directly intonautilus/scripts
folder:curl https://pastebin.com/raw/HrLTibuR | sed -e 's/\r$//' > $HOME/.local/share/nautilus/scripts/merge-video-sub chmod +x $HOME/.local/share/nautilus/scripts/merge-video-sub
Demo:
- The previous demo
Additional references:
- Encode forced subtitles using mkvmerge
- How to merge srt, idx or sub subtitles with MKV in seconds
- ToolSlick: SUB to SRT Converter and SubtitleTools: Convert Sub/Idx to Srt
I made another script which has GUI. It can find all subtitle files with their related videos (searches for the same name) in folder and sub-folder and merge them at ones.
Here is GitHub link for the script: https://github.com/bhaktanishant/Total-Subtitle-Merger
Here is code :
#!/usr/bin/env python
from Tkinter import Tk, Listbox, Button, Scrollbar, Canvas, Frame, Label
from subprocess import call
from threading import Thread
import os, tkMessageBox
from time import sleep
class MergeApp:
def __init__(self, root):
self.root = root
self.title = "Subtitle Merger By - Nishant Bhakta"
self.messageBoxTitle = "Message Box"
self.cancelWarning = "The video which has been started to merge will be merge. Rest will be cancel."
self.movieListBox = Listbox(self.root)
self.scrollBar = Scrollbar(self.root)
self.startButton = Button(self.root, text = "start", state = "disable", command = self.startMerging)
self.cancelButton = Button(self.root, text = "Stop", state = "disable", command = self.stopMerging)
self.finishButton = Button(self.root, text = "Exit", state = "normal", command = self.endApplication)
self.loadingLabel = Label(self.root)
self.processState = Label(self.root)
self.movieMap = {}
self.keyList = []
self.loadingIcons = ["--", "\\", "|", "/"]
self.wantToMerge = True
self.loading = False
self.warningMessageLoaded = False
def start(self):
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
# calculate position x and y coordinates
x = (screen_width/2) - (700/2)
y = (screen_height/2) - (300/2)
self.root.geometry('%dx%d+%d+%d' % (700, 300, x, y))
self.root.title(self.title)
self.movieListBox.config(width = 68, yscrollcommand = self.scrollBar.set)
self.movieListBox.pack(side = "left", fill = "y")
self.scrollBar.config(command = self.movieListBox.yview)
self.scrollBar.pack(fill = "y", side = "left")
self.startButton.pack(fill = "x")
self.cancelButton.pack(fill = "x")
self.finishButton.pack(fill = "x")
self.processState.pack(fill = "x", side = "bottom")
self.loadingLabel.pack(fill = "x", side = "bottom")
Thread(target = self.createMovieMap).start()
self.mainThread = Thread(target = self.startMerge)
self.root.protocol("WM_DELETE_WINDOW", self.ifCloseWindow)
self.root.mainloop()
def createMovieMap(self):
#Looking for subtitle
index = 0
Thread(target = self.startLoading, args = (True, )).start()
self.processState.config(text = "Searching Videos..")
for oneWalk in os.walk(os.getcwd()):
for fileName in oneWalk[2]:
if ".srt" in fileName:
subtitleName = fileName
#Now looking for movie with the name of subtitle
for oneWalk in os.walk(os.getcwd()):
for fileName in oneWalk[2]:
if ".srt" not in fileName:
key = subtitleName.replace(".srt", "")
if key in fileName:
movieName = fileName
if key not in self.movieMap:
self.movieMap[key] = dict([("subtitleUri", oneWalk[0] + "/" + subtitleName)
, ("movieUri", oneWalk[0] + "/" + movieName)
, ("moviePath", oneWalk[0])])
self.movieListBox.insert(index, " Queued - " + key)
self.keyList.append(key)
index += 1
self.startButton.config(state = "normal")
self.processState.config(text = "Search Complete.")
self.loading = False
def startMerge(self):
self.changeButtonState()
for key, value in self.movieMap.iteritems():
if self.wantToMerge:
self.processState.config(text = "Merging Video..")
Thread(target = self.startLoading, args = (True, )).start()
index = self.keyList.index(key)
self.movieListBox.delete(index)
self.movieListBox.insert(index, " Merging - " + key)
self.movieListBox.itemconfig(index, bg = "yellow")
if (call(["mkvmerge", "-o", value['moviePath'] + "/merging", value['movieUri'], value['subtitleUri']]) == 0):
call(["rm", value['movieUri'], value['subtitleUri']])
call(["mv", value['moviePath'] + "/merging", value['moviePath'] + "/"+ key + ".mkv"])
self.movieListBox.delete(index)
self.movieListBox.insert(index, " Successful - " + key)
self.movieListBox.itemconfig(index, bg = "green")
else:
for name in os.listdir(value['moviePath'] + "/"):
if name == "merging":
call(["rm", value['moviePath'] + "/merging"])
self.movieListBox.delete(index)
self.movieListBox.insert(index, " Failed - "+ key)
self.movieListBox.itemconfig(index, bg = "red", foreground = "white")
else:
break
self.loading = False
self.cancelButton.config(state = "disable")
self.finishButton.config(state = "normal")
if self.wantToMerge:
self.processState.config(text = "Merge Complete.")
def startLoading(self, loadOrNot):
self.loading = loadOrNot
while self.loading:
for icon in self.loadingIcons:
self.loadingLabel.config(text = icon)
sleep(.2)
def startMerging(self):
self.mainThread.start()
def changeButtonState(self):
self.startButton.config(state = "disable")
self.cancelButton.config(state = "normal")
self.finishButton.config(state = "disable")
def stopMerging(self):
self.wantToMerge = False
self.startButton.config(state = "disable")
self.cancelButton.config(state = "disable")
self.finishButton.config(state = "normal")
self.processState.config(text = "Merge Canceled.")
if not self.warningMessageLoaded:
tkMessageBox.showwarning(self.messageBoxTitle, self.cancelWarning)
self.warningMessageLoaded = True
def endApplication(self):
self.root.destroy()
def ifCloseWindow(self):
if self.mainThread.is_alive():
self.stopMerging()
self.endApplication()
if __name__ == "__main__":
tk = Tk()
app = MergeApp(tk)
app.start()
How to use it as nautilus script:
paste this code to a file and name it merge
.
Now, open a terminal at the directory where you saved file marge
and put mv merge ~/.local/share/nautilus/scripts/merge
and hit enter.
now put cd ~/.local/share/nautilus/scripts/
and hit enter.
now put chmod +x merge
and hit enter.
now go to the root folder where movies and subtitles are and right click on any file or folder then select scripts > merge
Done.