Skip to content

Instantly share code, notes, and snippets.

@cliss
Created January 26, 2021 14:30
Show Gist options
  • 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)
@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]],

@MightyPork
Copy link

Works wonderfully on Arch Linux, thank you!

@nchietala
Copy link

nchietala commented Nov 28, 2024

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