From be9714d9e3e35f9f7e51b27e515e9cca32d37128 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Fri, 31 Jan 2025 12:53:03 +0000 Subject: [PATCH] Add bin/epubr --- bin/epubr | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 bin/epubr diff --git a/bin/epubr b/bin/epubr new file mode 100644 index 0000000..01b3626 --- /dev/null +++ b/bin/epubr @@ -0,0 +1,166 @@ +#!/usr/bin/env ruby + +require 'bundler/inline' + +#gemfile do +# source 'https://rubygems.org' +# gem 'zip' +# gem 'nokogiri' +# gem 'tty-prompt' +# gem 'debug' +#end + + +require 'zip' +require 'nokogiri' +require 'tty-prompt' +require 'debug' + + +class Epub + def initialize(file) + @file = file + end + + def read_epub + Zip::File.open(@file) do |zip_file| + opf_name = find_opf_file(zip_file) + opf_entry = zip_file.find_entry(opf_name) + opf_content = opf_entry.get_input_stream.read + opf_doc = Nokogiri::XML(opf_content) + + yield zip_file, opf_doc, opf_name if block_given? + end + end + + def write_epub + read_epub do |zip_file, opf_doc, opf_name| + yield zip_file, opf_doc, opf_name if block_given? + zip_file.get_output_stream(opf_name) do |output_stream| + output_stream.write(opf_doc.to_xml) + end + end + end + + def opf + read_epub do |zip_file, opf_doc, opf_name| + opf_doc + end + end + + def series + read_epub do |zip_file, opf_doc, opf_name| + series_element = opf_doc.at_css('meta[name="calibre:series"]') + series_element["content"] if series_element + end + end + + def series=(new_series_name) + write_epub do |zip_file, opf_doc, opf_name| + series_element = opf_doc.at_css('meta[name="calibre:series"]') + + if series_element && new_series_name.nil? + # Delete the element for the series + series_element.remove + elsif series_element + # Update the content of the element + series_element['content'] = new_series_name + else + # Create a new element for the series if it doesn't exist + opf_namespace_url = "http://www.idpf.org/2007/opf" + metadata_element = opf_doc.at_css("metadata, opf|metadata", 'opf' => opf_namespace_url) + series_element = Nokogiri::XML::Node.new('meta', opf_doc) + series_element['name'] = 'calibre:series' + series_element['content'] = new_series_name + metadata_element.add_child(series_element) + end + end + end + + def series_index + read_epub do |zip_file, opf_doc, opf_name| + series_index = opf_doc.at_css('meta[name="calibre:series_index"]') + series_index['content'] if series_index + end + end + + def series_index=(new_si) + write_epub do |zip_file, opf_doc, opf_name| + series_index = opf_doc.at_css('meta[name="calibre:series_index"]') + + old_si = series_index['content'] if series_index + + if series_index && new_si.nil? + series_index.remove + elsif series_index + series_index['content'] = new_si + else + opf_namespace_url = "http://www.idpf.org/2007/opf" + metadata_element = opf_doc.at_css("metadata, opf|metadata", 'opf' => opf_namespace_url) + + si_element = Nokogiri::XML::Node.new('meta', opf_doc) + si_element['name'] = 'calibre:series_index' + si_element['content'] = new_si + metadata_element.add_child(si_element) + end + end + end + + def find_opf_file(zip_file) + # Locate and read the container.xml file + container_xml_entry = zip_file.find_entry('META-INF/container.xml') + container_xml = container_xml_entry.get_input_stream.read + + # Parse the container.xml file + doc = Nokogiri::XML(container_xml) + + # Find the rootfile element and extract the 'full-path' attribute + doc.at_xpath("xmlns:container/xmlns:rootfiles/xmlns:rootfile")['full-path'] + end +end + +def run(argv) + files = [] + action = nil + attribute = nil + new_series_name = nil + + argv.each_with_index do |arg, index| + if arg == 'get' || arg == 'set' + action = arg.to_sym + attribute = argv[index + 1] + new_series_name = argv[index + 2] + break + else + files << arg if File.exist?(arg) + end + end + + raise ArgumentError.new("Action must be :get or :set") unless [:get, :set].include?(action) + raise ArgumentError.new("Attribute must be 'series' or 'series_index'") unless ['series', 'series_index'].include?(attribute) + + files.sort! + + if attribute == 'series' + result = files.map do |file| + e = Epub.new(file) + {file: }.merge e.series_name(file, action:, new_series_name:) + end + + result.each { |r| puts "#{r[:file]} : #{r[:series_name]}" } if action == :get + result.each { |r| puts "#{r[:file]} : #{r[:old_series_name]} => #{r[:new_series_name]}" } if action == :set + + elsif attribute == 'series_index' + result = files.map do |file| + e = Epub.new(file) + {file: }.merge(e.series_index(file, action: )) + end + + result.each { |r| puts "#{r[:file]} : #{r[:old_series_index]}" } if action == :get + result.each { |r| puts "#{r[:file]} : #{r[:old_series_index]} -> #{r[:new_series_index]}" } if action == :set + end +end + +if __FILE__ == $PROGRAM_NAME + run(ARGV) +end