Created
January 26, 2021 14:30
-
-
Save cliss/53136b2c69526eeed561a5517b23cefa to your computer and use it in GitHub Desktop.
Merge Files with Chapters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
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!
Based on previous edits by @jojo2357
+ from uuid import uuid4
- fileListFileName = "files.txt"
+ metadataFileName = f"metadata-{str(uuid4())}.txt"
- fileListFileName = "files.txt"
+ fileListFileName = f"files-{str(uuid4())}.txt"
- file '{sys.argv[1]}'
- file '{sys.argv[2]}'"""
+ file '{os.path.abspath(sys.argv[1])}'
+ file '{os.path.abspath(sys.argv[2])}'
+ """
Resolves an issue I had where the file would delete any existing files I had that happened to be named "files.txt" or "metadata.txt"
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I made the following revisions to my copy to help handle whitespace better:
Fixes "Unsafe File Name" issue that I had. That may be what our windows friend was suffering...hard to say without more info