Skip to content

Instantly share code, notes, and snippets.

Last active May 7, 2023 17:49
What would you like to do?
Bash script to merge all mp4 videos in current directory (recursively 2 levels). It also updates the chapter marks to retain the folder/filename of source dir
## Script to merge all mp4 videos in current directory (recursively 2 levels)
## And update chapter marks to retain the folder/filename
## Script for merging videos
filename=`basename pwd`
bname=`basename "$current"`
find . -maxdepth 2 -iname '*.mp4' | xargs -L 1 echo | awk '{printf "file \x27%s\x27\n", $0}' >> list.txt
find . -maxdepth 2 -iname '*.mp4' | xargs -L 1 echo | awk '{print $0}' >> files.txt
echo -n "Merging the files"
ffmpeg -f concat -safe 0 -i list.txt -c copy "$bname.mp4" -v quiet
echo "..........[ DONE ]"
## extract meta
# ffmpeg -i all.mp4 -f ffmetadata metafile
echo -n "Extracting meta data"
ffmpeg -i "$bname.mp4" -f ffmetadata $metafile -v quiet
echo "..........[ DONE ]"
## chapter marks
#TODO: (‘=’, ‘;’, ‘#’, ‘\’) to be escaped
echo -n "Identifying chapters"
cat files.txt | while read file
ds=`ffprobe -v quiet -of csv=p=0 -show_entries format=duration "$file"`
# echo "$ds"
echo "[CHAPTER]" >> $metafile
echo "TIMEBASE=1/1" >> $metafile
echo "START=$ts" >> $metafile
ts=`echo $ts + $ds | bc`
echo "END=$ts" >> $metafile
echo "TITLE=$file" >> $metafile
echo "..........[ DONE ]"
## update meta with chaptermarks
echo -n "Adding chapter meta "
ffmpeg -i "$bname.mp4" -i $metafile -map_metadata 1 -codec copy "$bname-meta.mp4" -v quiet
echo "..........[ DONE ]"
## cleanup
echo -n "Cleaning up"
rm files.txt list.txt $metafile
echo "..........[ DONE ]"
echo "Job Completed."
Copy link

bdurrow commented Oct 26, 2019

Thank you for writing this. I looked all over for a tool to accomplish this and didn't find anything. It didn't quite meet my need so I made some changes but tried to keep it generally useful here.
Improvements are:

  • Make bash "strict"
  • Use a temporary directory for the generated intermediate files
  • Cleanup even when there is a failure
  • Sort the input files in each directory
  • Remove the dependency on bc (added dependency on head, sed and tee)
  • Strip the leading ./ and trailing .mp4 from the file for the chapter name
  • Escape special characters in chapter titles (untested)
  • Avoid writing the merged file twice (my final mp4 was over 10GB)
  • Reformated all variables to ${style}
  • Enclosed all filenames in quotes

Copy link

sbma44 commented Apr 19, 2020

I redid this in Python to better handle some unusual whitespace in my filenames (the xargs -L1 echo trick really does not play nice with more than 1 space at a time). Note that this hardcodes the temp file paths/will not work if run in parallel.

import sys, os, os.path, re, subprocess

re_tuple = re.compile(r'(\d+)\s+([\w\d\.\s]+)')
re_season = re.compile(r'S\d\d')

season_root = os.path.normpath(sys.argv[1])
for ep in os.listdir(season_root):
    if ep[0] == '.':
    m = re_tuple.match(ep)
    if not m:
    episode_num = int(
    episode_title =
    ms =
    if not ms:
    season_num =

    chapter_list = []
    for chapter in os.listdir(os.path.join(season_root, ep)):
        if chapter[-4:] != '.mp4':
        m = re_tuple.match(chapter)
        if not m:
        chapter_num = int(
        chapter_title ='.mp4','')
        chapter_list.append((chapter_num, chapter_title, os.path.abspath(os.path.join(season_root, ep, chapter))))

    chapter_list.sort(key=lambda x: x[0])

    with open('/tmp/chapters.txt', 'w') as f:
        for chapter in chapter_list:
            f.write('file \'{}\'\n'.format(chapter[2]))

    print('combining chapters: {}'.format(', '.join([x[1] for x in chapter_list])))
    args = 'ffmpeg -f concat -safe 0 -i /tmp/chapters.txt -c copy /tmp/temp-1.mp4 -v quiet'.split(' ')
    print(' '.join(args))

    print('extracting metadata')'ffmpeg -i /tmp/temp-1.mp4 -f ffmetadata /tmp/metadata.txt -v quiet'.split(' '))

    ts = 0
    with open('/tmp/metadata.txt', 'a') as f:
        for chapter in chapter_list:
            args = 'ffprobe -v quiet -of csv=p=0 -show_entries format=duration'.split(' ')
            cp = subprocess.check_output(args)            
            ts += float(cp.strip())


    print('attaching metadata')
    out_path = os.path.join(season_root, '{}E{:02d} - {}.mp4'.format(season_num, episode_num, episode_title))
    args = 'ffmpeg -i /tmp/temp-1.mp4 -i /tmp/metadata.txt -map_metadata 1 -codec copy'.split(' ') + [out_path] + '-v quiet'.split(' ')

Copy link

Ashark commented May 7, 2023

Yeah, it is not good that you store the merged file twice: at line 14 without metadata, and at line 46 with metadata.
It is better to make the metadata first, and then do merge and inject metadata in one time. The @bdurrow fork does this.

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