Skip to content

Instantly share code, notes, and snippets.

@FNGarvin
Created April 19, 2025 03:59
Show Gist options
  • Save FNGarvin/517fadca0e629067eddf62afd30c3ead to your computer and use it in GitHub Desktop.
Save FNGarvin/517fadca0e629067eddf62afd30c3ead to your computer and use it in GitHub Desktop.
A Simple Script to Convert a Video Into a Collection of MP3s, One File Per Chapter
#!/usr/bin/ruby -w
#a simple script to take a video file and convert it
#into a collection of mp3 files, one file per chapter
#requires an ffmpeg install with ffprobe and support for
#mp3 plus whatever formats the input uses
#FNG, 2025
require 'json'
require 'shellwords' # Todo: safely handle chapter titles with spaces and special characters, replace sanitize
class Chapter
attr_reader :id, :title, :start_time, :end_time, :duration
def initialize(id, title, start_time, end_time)
@id = id
@title = title
@start_time = start_time.to_f
@end_time = end_time.to_f
@duration = @end_time - @start_time
end
def to_s
"ID: #{@id}, Title: #{@title}, Start: #{@start_time}, End: #{@end_time}, Duration: #{@duration}"
end
end
def extract_chapters_from_video(video_filename)
begin
ffprobe_command = "ffprobe -v quiet -print_format json -show_chapters \"#{video_filename}\""
json_output = `#{ffprobe_command}`
data = JSON.parse(json_output)
chapters_data = data['chapters'] || []
chapter_objects = []
chapters_data.each do |chapter_info|
id = chapter_info['id']
title = chapter_info.dig('tags', 'title')
start_time = chapter_info['start_time']
end_time = chapter_info['end_time']
chapter_objects << Chapter.new(id, title, start_time, end_time)
end
chapter_objects
rescue Errno::ENOENT
puts "Error: File '#{video_filename}' not found."
return []
rescue JSON::ParserError
puts "Error: Could not parse ffprobe output. Make sure ffmpeg/ffprobe is installed and the video file is valid."
return []
rescue StandardError => e
puts "An unexpected error occurred: #{e.message}"
return []
end
end
def sanitize_filename(title)
title.gsub(/[^a-zA-Z0-9\s_-]/, '')
end
def convert_chapter_to_mp3(video_filename, chapter)
sanitized_title = sanitize_filename(chapter.title)
output_filename = "[#{chapter.id}] #{sanitized_title}.mp3"
ffmpeg_command = "ffmpeg -i \"#{video_filename}\" -ss #{chapter.start_time} -t #{chapter.duration} -vn \"#{output_filename}\""
puts "Converting chapter: #{chapter.title} (Start: #{chapter.start_time}, Duration: #{chapter.duration} seconds) to #{output_filename}"
system(ffmpeg_command)
end
if __FILE__ == $0
if ARGV.empty?
puts "Usage: ruby #{$0} <video_filename>"
else
video_filename = ARGV[0]
chapters = extract_chapters_from_video(video_filename)
chapters.each do |chapter|
convert_chapter_to_mp3(video_filename, chapter)
end
puts "Finished processing all chapters."
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment