Skip to content

Instantly share code, notes, and snippets.

@palaniraja
Last active May 7, 2023 17:49
Show Gist options
  • Save palaniraja/d14ba9ac49019526e0774b28e2d71b16 to your computer and use it in GitHub Desktop.
Save palaniraja/d14ba9ac49019526e0774b28e2d71b16 to your computer and use it in GitHub Desktop.
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
#!/bin/bash
## 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`
current=`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
metafile="metadata.txt"
echo -n "Extracting meta data"
ffmpeg -i "$bname.mp4" -f ffmetadata $metafile -v quiet
echo "..........[ DONE ]"
## chapter marks
#TODO: (‘=’, ‘;’, ‘#’, ‘\’) to be escaped
ts=0
echo -n "Identifying chapters"
cat files.txt | while read file
do
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
done
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."
@sbma44
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] == '.':
        continue
    m = re_tuple.match(ep)
    if not m:
        continue
    episode_num = int(m.group(1))
    episode_title = m.group(2).strip()
    ms = re_season.search(season_root)
    if not ms:
        continue
    season_num = ms.group(0)

    chapter_list = []
    for chapter in os.listdir(os.path.join(season_root, ep)):
        if chapter[-4:] != '.mp4':
            continue
        m = re_tuple.match(chapter)
        if not m:
            continue
        chapter_num = int(m.group(1))
        chapter_title = m.group(2).replace('.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))
    subprocess.call(args)

    print('extracting metadata')
    subprocess.call('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:
            f.write('[CHAPTER]\n')
            f.write('TIMEBASE=1/1\n')
            f.write('START={}\n'.format(ts))
            
            args = 'ffprobe -v quiet -of csv=p=0 -show_entries format=duration'.split(' ')
            args.append(chapter[2])
            cp = subprocess.check_output(args)            
            ts += float(cp.strip())

            f.write('END={}\n'.format(ts))
            f.write('TITLE={}\n'.format(chapter[1]))

    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(' ')
    subprocess.call(args)

@Ashark
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