-
-
Save cliss/53136b2c69526eeed561a5517b23cefa to your computer and use it in GitHub Desktop.
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) |
@eugenesvk Huh, thanks for pointing that out.
This most recent activity does not instill confidence, however:
Changed 2 years ago by dextro
There were some minor quirks with file names and dropped audio streams but otherwise this worked a treat, thanks for sharing and saving me a lot of time and frustration!
By the way if you want to make sure it copies all audio streams, change line 128 like so:
["ffmpeg", "-f", "concat", "-i", fileListFileName,"-i", metadataFileName, "-map_metadata", "1", "-map", "0", "-c", "copy", sys.argv[3]],
Basically adding -map 0
just before the copy command, more info here if you're interested how it works: https://trac.ffmpeg.org/wiki/Concatenate#samecodec
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
@xcomprs Sorry, but I don't have any installation of Windows available to me, so I'm not sure what's going on here. :(
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.
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
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]],
Works wonderfully on Arch Linux, thank you!
Just FYI ffmpeg does indeed seem to miss this concat feature as acknowledged in their bug tracker