我一直在mkvmerge
使用以下命令来合并视频文件和字幕
mkvmerge -o output.mkv video.mp4 subtitles.srt
它运行正常,但你可能已经猜到,这是一个非常缓慢的操作
- 我必须在每个存储电影的目录中打开终端。
- 然后我必须复制并粘贴命令中的文件名
- 然后我必须重命名输出文件
- 然后删除我使用的原始文件
这是一个漫长的过程。
我想要的理想状态。
- 我将进入包含同名电影文件和字幕文件的文件夹。
- 我选择一个电影文件和一个字幕文件,右键单击并在它们上运行 nautilus 脚本。
- 它们合并了
- 原始文件被删除。
- 将合并后的文件重命名为原始文件的名称,并添加
mkv
扩展名
这不需要递归,我不希望我的硬盘一次创建和删除 100 个文件。电影文件和字幕文件通常具有相同的名称,但有时会将“_en”帖子固定到字幕文件中。如果您有其他方法,我也会同意。
答案1
我决定获得一些 Bash 经验并编写了以下脚本,它具有以下特点:
- 它可以处理放置在当前目录中的所有文件和文件夹。
- 如果有多个字幕或视频文件(示例文件除外),脚本将要求手动交互。
- 在所有其他情况下,脚本将自动将所有文件和文件夹移动到备份目录(运行它的时候一定要小心!),这些邮件将被移至用户的垃圾文件夹,而不是被删除。
- 它可以处理多种类型的视频和字幕扩展。
- 它用
mkvmerge
将视频与字幕文件合并,还用于notify-send
在 GUI 中显示一些消息。它还用于gvfs-trash
将文件移动到用户的垃圾文件夹中。 - 它可以用作Nautilus 脚本
notify-send
或者作为常规 shell 脚本,但当命令存在于脚本主体中时,需要 GIU 环境。 - 输出文件的名称可以基于目录名称(默认)或源视频文件的名称。
- 此外,当仅选择两个文件时(一个视频和一个字幕文件),它们将会合并文件夹的其余内容将保留。输出文件将以源视频文件命名。仅当脚本用作 Nautilus 脚本时,此选项才可用。
剧本:
#!/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
设置:
创建可执行文件并将上述内容粘贴到其中。我们称此文件为
merge-video-sub
:touch merge-video-sub chmod +x merge-video-sub nano merge-video-sub
将此文件复制(或
ln -s
)到文件夹中~/.local/share/nautilus/scripts
,以使其成为当前用户的 Nautilus 脚本。目前我找不到如何使其作为 Nautilus 脚本在整个系统范围内可用的方法。
将文件复制到
~/bin
(如果需要则添加export PATH=$PATH:~/bin
到底部~/.bashrc
)以使其可作为当前用户的 shell 命令使用。将文件复制到
/usr/local/bin
以使其可用作系统范围内的 shell 命令。捷径是
curl
来自的脚本我的 PasteBin直接进入nautilus/scripts
文件夹: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
演示:
其他参考资料:
答案2
我制作了另一个具有 GUI 的脚本。它可以在文件夹和子文件夹中找到所有带有相关视频(搜索相同名称)的字幕文件,并将它们合并在一起。
以下是该脚本的 GitHub 链接:https://github.com/bhaktanishant/Total-Subtitle-Merger
代码如下:
#!/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()
如何将其用作 nautilus 脚本:
将此代码粘贴到文件中并将其命名为merge
。
现在,在您保存文件的目录打开一个终端marge
,然后输入mv merge ~/.local/share/nautilus/scripts/merge
并按回车键。
现在输入cd ~/.local/share/nautilus/scripts/
并按回车键。
现在输入chmod +x merge
并按回车键。
现在转到电影和字幕所在的根文件夹,右键单击任何文件或文件夹,然后选择scripts > merge
完毕。
答案3
这里我为此编写了一个脚本。只需确保电影文件夹中只有两个文件,一个是电影,另一个是字幕(它们不需要有相同的名称)。
创建一个名为merge
Desktop 的文件并粘贴以下代码:
#!/usr/bin/env python
import subprocess, os, sys
def main():
movieName = ""
subtitleName = ""
for name in os.listdir(os.getcwd()):
if (name != sys.argv[0]):
if ".srt" in name:
subtitleName = name
else:
movieName = name
start(movieName, subtitleName)
def start(movieName, subtitleName):
if (subprocess.call(["mkvmerge", "-o", "merging", movieName, subtitleName]) == 0):
subprocess.call(["rm", movieName, subtitleName])
subprocess.call(["mv", "merging", movieName])
else:
for name in os.listdir(os.getcwd()):
if name == "merging":
subprocess.call("rm", "merging")
print "Sorry, Something went wrong."
if __name__ == "__main__":
main()
现在,在您保存的目录marge
(在本例中在桌面上)打开一个终端,然后输入mv merge ~/.local/share/nautilus/scripts/merge
并按回车键。
现在输入cd ~/.local/share/nautilus/scripts/
并按回车键。
现在输入chmod +x merge
并按回车键。
现在转到电影和字幕所在的文件夹,右键单击任何文件,然后选择scripts > merge
完毕。
如果你有任何问题,请告诉我。