Skip to content

Instantly share code, notes, and snippets.

@cliss
Created January 26, 2021 14:30
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save cliss/53136b2c69526eeed561a5517b23cefa to your computer and use it in GitHub Desktop.
Save cliss/53136b2c69526eeed561a5517b23cefa to your computer and use it in GitHub Desktop.
Merge Files with Chapters
import datetime
import json
import os
import subprocess
import sys
#############
### USAGE ###
#############
if len(sys.argv) < 4:
print("Usage:")
print("{} [input file] [input file] [output file]".format(sys.argv[0]))
print("")
print("Both files are assumed to have their chapters")
print("entered correctly and completely.")
sys.exit(0)
########################
### Get Chapter List ###
########################
def getChapterList(videoFile):
# Get chapter list as JSON
result = subprocess.run(
["ffprobe", "-print_format", "json", "-show_chapters", videoFile],
capture_output=True,
text=True
)
# Load the JSON
fileJson = json.loads(result.stdout)['chapters']
# Map to Python object:
# {
# id: 1
# start: 123.456
# end: 789.012
# }
chapters = list(map(
lambda c: {
'index': c['id'],
'start': float(c['start_time']),
'end': float(c['end_time']),
'title': c['tags']['title']},
fileJson))
return list(chapters)
########################
### Video 1 Duration ###
########################
# Get the duration of the first video
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", sys.argv[1]],
capture_output=True,
text=True
)
# Get the result and trim off the trailing newline.
file1duration = float(result.stdout.rstrip())
print("{} duration is {} seconds.".format(sys.argv[1], file1duration))
############################
### Video 2 Chapter List ###
############################
def chapterMigrator(chapter):
startTime = chapter['start']
endTime = chapter['end']
offsetStartTime = file1duration + startTime
offsetEndTime = file1duration + endTime
return {'index': chapter['index'], 'start': offsetStartTime, 'end': offsetEndTime, 'title': chapter['title']}
video2rawchapters = getChapterList(sys.argv[2])
# Migrate these chapters to be offset from the end of the first file
video2chapters = list(map(chapterMigrator, video2rawchapters))
print("{} has {} chapters.".format(sys.argv[2], len(video2chapters)))
###########################
### Get file 1 metadata ###
###########################
result = subprocess.run(
["ffmpeg", "-i", sys.argv[1], "-f", "ffmetadata", "-"],
capture_output=True,
text=True
)
metadata = result.stdout
##################################
### Append file 2 chapter list ###
##################################
metadataFileName = "metadata.txt"
# Note the timestamps are in milliseconds, and should be integers.
for c in video2chapters:
metadata += f"""
[CHAPTER]
TIMEBASE=1/1000
START={int(c['start'] * 1000)}
END={int(c['end'] * 1000)}
title={c['title']}"""
with open(metadataFileName, "w") as metadataFile:
metadataFile.write(metadata)
############################
### Join two video files ###
############################
fileListFileName = "files.txt"
fileList = f"""
file {sys.argv[1]}
file {sys.argv[2]}"""
with open(fileListFileName, "w") as fileListFile:
fileListFile.write(fileList)
if os.path.exists(sys.argv[3]):
os.remove(sys.argv[3])
print("Joining {} and {} into {}...".format(sys.argv[1], sys.argv[2], sys.argv[3]))
result = subprocess.run(
["ffmpeg", "-f", "concat", "-i", fileListFileName, "-i", metadataFileName, "-map_metadata", "1", "-c", "copy", sys.argv[3]],
capture_output=False
)
print("...file {} created.".format(sys.argv[3]))
# Clean up.
os.remove(metadataFileName)
os.remove(fileListFileName)
@k-barber
Copy link

k-barber commented May 28, 2022

@xcomprs

Hey Guys! I am getting Error below:

Traceback (most recent call last): File "./mergechapters.py", line 55, in text=True File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.2544.0_x64__qbz5n2kfra8p0\lib\subprocess.py", line 488, in run with Popen(*popenargs, **kwargs) as process: File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.2544.0_x64__qbz5n2kfra8p0\lib\subprocess.py", line 800, in init restore_signals, start_new_session) File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.7_3.7.2544.0_x64__qbz5n2kfra8p0\lib\subprocess.py", line 1207, in _execute_child startupinfo) FileNotFoundError: [WinError 2] The system cannot find the file specified

I ran into the same issue, it turns out I didn't have ffprobe installed. It normally comes with ffmpeg, but if you installed ffmpeg manually, you might not have it.

You can check by opening up command prompt and running

ffprobe -version

If you get an error saying ffprobe isn't recognized, you need to install it.

It should be installed if you run

choco install ffmpeg

in command prompt with Administrator privileges.

@jojo2357
Copy link

jojo2357 commented Feb 1, 2023

I made the following revisions to my copy to help handle whitespace better:

-    ["ffmpeg", "-f", "concat", "-i", fileListFileName, "-i", metadataFileName, "-map_metadata", "1", "-c", "copy", sys.argv[3]],
+    ["ffmpeg", "-f", "concat", "-safe", "0", "-i", fileListFileName, "-i", metadataFileName, "-map_metadata", "1", "-c", "copy", sys.argv[3]],

fileList = f"""
- file {sys.argv[1]}
- file {sys.argv[2]}"""
+ file '{sys.argv[1]}'
+ file '{sys.argv[2]}'"""

Fixes "Unsafe File Name" issue that I had. That may be what our windows friend was suffering...hard to say without more info

@mrtumnus
Copy link

On my version of ffmpeg (4.1.11-0), I get an error unless I specify -f ffmetadata prior to -i metadata.txt. Combining this with other suggestions above yields:

    ["ffmpeg", "-f", "concat", "-safe", "0", "-i", fileListFileName, "-f", "ffmetadata", "-i", metadataFileName, "-map_metadata", "1", "-map", "0", "-c", "copy", sys.argv[3]],

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