]> jaekl.net Git - quanlib.git/commitdiff
Address the low-hanging-fruit RuboCop advisories
authorChris Jaekl <chris@jaekl.net>
Sat, 22 Jun 2024 22:51:39 +0000 (18:51 -0400)
committerChris Jaekl <chris@jaekl.net>
Sat, 22 Jun 2024 22:51:39 +0000 (18:51 -0400)
This leaves 50 "offenses" still to be examined.  Most of those are complaints about code complexity, and will require some thought before attempting a refactor.

21 files changed:
.rubocop.yml [new file with mode: 0644]
Gemfile
app/author.rb
app/book.rb
app/book_loader.rb
app/classification.rb
app/cover.rb
app/extract.rb
app/main.rb
app/navigator.rb
app/page.rb
app/series.rb
app/store.rb
app/tconn.rb
app/walk_dir.rb
test/book_test.rb
test/classification_test.rb
test/conn_mock.rb
test/store_mock.rb
test/store_test.rb
test/walk_dir_test.rb

diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644 (file)
index 0000000..9bbd52c
--- /dev/null
@@ -0,0 +1,17 @@
+AllCops:
+  TargetRubyVersion: 3.0
+
+Metrics/MethodLength:
+  Max: 25
+
+Style/StringLiterals:
+  EnforcedStyle: double_quotes
+
+Style/TrailingCommaInArrayLiteral:
+  EnforcedStyleForMultiline: comma
+
+Style/TrailingCommaInHashLiteral:
+  EnforcedStyleForMultiline: comma
+
+Style/StringLiteralsInInterpolation:
+  EnforcedStyle: double_quotes
diff --git a/Gemfile b/Gemfile
index 01814797818a10cc8fa8853589c6f2dccfbc11b5..4f41955c688c1278bdf71feec42261a10670d4c4 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -1,9 +1,11 @@
-source 'http://rubygems.org'
+# frozen_string_literal: true
 
-gem 'inifile'
-gem 'mocha'
-gem 'nokogiri'
-gem 'pg'
-gem 'rspec'
-gem 'rubocop'
-gem 'rubyzip'
+source "http://rubygems.org"
+
+gem "inifile"
+gem "mocha"
+gem "nokogiri"
+gem "pg"
+gem "rspec"
+gem "rubocop"
+gem "rubyzip"
index 781969cd94b03ec48f948cca4ef88d307f6eecd0..716f5aa2e05552ec35c6e28cd4bff51c234d4f49 100644 (file)
@@ -1,18 +1,15 @@
 # frozen_string_literal: true
 
+# Encapsulates information about an author
 class Author
-  attr_accessor :grouping
-  attr_accessor :reading_order
-  attr_accessor :sort_order
+  attr_accessor :grouping, :reading_order, :sort_order
 
   def initialize(grouping, reading_order, sort_order)
     @grouping = grouping
     @reading_order = reading_order
     @sort_order = sort_order
 
-    if (nil == sort_order) || ('Unknown' == sort_order)
-      @sort_order = reading_to_sort_order(reading_order)
-    end
+    @sort_order = reading_to_sort_order(reading_order) if sort_order.nil? || sort_order == "Unknown"
   end
 
   def inspect
@@ -34,12 +31,9 @@ class Author
   def reading_to_sort_order(reading_order)
     sort_order = reading_order
 
-    parts = reading_order.split(' ')
-    if parts.length > 1
-      sort_order = parts[-1] + ', ' + parts[0..-2].join(' ')
-    end
+    parts = reading_order.split(" ")
+    sort_order = "#{parts[-1]}, #{parts[0..-2].join(" ")}" if parts.length > 1
 
-    return sort_order
+    sort_order
   end
 end
-
index c4d7070e8b2f009bec86328b5e2a0f9a30855a3c..3449b6dc76204ac6959fd779bd26e0d24a4d01b3 100644 (file)
+# frozen_string_literal: true
 
-require 'nokogiri'
-require 'rubygems'
-require 'zip'
+require "nokogiri"
+require "rubygems"
+require "zip"
 
-require_relative 'author'
-require_relative 'classification'
-require_relative 'cover'
-require_relative 'store'
+require_relative "author"
+require_relative "classification"
+require_relative "cover"
+require_relative "store"
 
+# Encapsulates info about a book in the library
 class Book
-  @@DC_NS_URL = 'http://purl.org/dc/elements/1.1/'
-  @@SERIES_AND_VOLUME_REGEX = /^([A-Z]+)([0-9]+(\.[0-9]+)?)$/
-
-  attr_accessor :arrived
-  attr_accessor :author
-  attr_accessor :classification_id
-  attr_accessor :cover
-  attr_accessor :description
-  attr_accessor :language
-  attr_accessor :path
-  attr_accessor :series_id
-  attr_accessor :title
-  attr_accessor :volume
+  @@dc_ns_url = "http://purl.org/dc/elements/1.1/"
+  @@series_and_volume_regex = /^([A-Z]+)([0-9]+(\.[0-9]+)?)$/
+
+  attr_accessor(
+    :arrived,
+    :author,
+    :classification_id,
+    :cover,
+    :description,
+    :language,
+    :path,
+    :series_id,
+    :title,
+    :volume
+  )
 
   def initialize(store)
     @store = store
   end
 
-  def load_from_file!(fileName)
-    @path = fileName
-    parse_file_name!(fileName)
+  def load_from_file!(file_name)
+    @path = file_name
+    parse_file_name!(file_name)
   end
 
-  def self.can_handle?(fileName)
-    if nil == fileName
-      return false
-    end
+  def self.can_handle?(file_name)
+    return false if file_name.nil?
 
-    #puts "Filename:  " + fileName.to_s
-    lowerName = fileName.downcase()
+    lower_name = file_name.downcase
 
-    if lowerName.end_with?(".epub")
-      return true
-    end
+    return true if lower_name.end_with?(".epub")
 
-    if lowerName.end_with?(".pdf")
-      return true
-    end
+    return true if lower_name.end_with?(".pdf")
 
-    return false
+    false
   end
 
   def self.grouping_for_title(title)
     result = title
 
-    '\'",!#'.split('').each do |c|
-      result = result.gsub(c, '-')
+    "'\",!#".split("").each do |c|
+      result = result.gsub(c, "-")
     end
-    result = result.gsub(/: */, '--')
-    result = result.gsub(' ', '_')
 
-    result
+    result.gsub(/: */, "--").gsub(" ", "_")
   end
 
   def heading
     result = []
 
-    if nil != @title
-      result.push('<b>' + @title + '</b>')
+    if @title.nil?
+      result.push("<i>(Unknown title)</i>")
     else
-      result.push('<i>(Unknown title)</i>')
-    end
-    if nil != @author
-      result.push('<i>by ' + @author.reading_order + '</i>')
+      result.push("<b>#{@title}</b>")
     end
+    result.push("<i>by #{@author.reading_order}</i>") unless @author.nil?
 
-    seriesInfo = []
+    series_info = []
     series = @store.load_series(@series_id)
-    if nil != series and nil != series.descr
-      seriesInfo.push(series.descr.to_s)
-    end
-    if nil != @volume
-      seriesInfo.push(@volume.to_s)
-    end
-    if seriesInfo.length > 0
-      result.push(seriesInfo.join(' '))
-    end
+    series_info.push(series.descr.to_s) if series&.descr
+    series_info.push(@volume.to_s) unless @volume.nil?
+    result.push(series_info.join(" ")) unless series_info.empty?
 
     classification = nil
-    if nil != @classification_id
-      classification = @store.load_classification(@classification_id)
-    end
-    if nil != classification
-      if nil != classification.ddc
-        result.push('Dewey: ' + classification.ddc.to_s)
-      end
-      if nil != classification.lcc
-        result.push('LCC: ' + classification.lcc.to_s)
-      end
+    classification = @store.load_classification(@classification_id) unless @classification_id.nil?
+    unless classification.nil?
+      result.push("Dewey: #{classification.ddc}") unless classification.ddc.nil?
+      result.push("LCC: #{classification.lcc}") unless classification.lcc.nil?
     end
 
-    return result.join('<br/>')
+    result.join("<br/>")
   end
 
   def inspect
@@ -113,35 +94,31 @@ class Book
       path.nil? ? nil : "path=\"#{path.inspect}",
     ].compact.join(" ")
 
-    return "(Book: #{field_info})"
+    "(Book: #{field_info})"
   end
 
   def to_s
-    return inspect()
+    inspect
   end
 
   def title_grouping
     return if path.nil?
 
-    File.basename(@path, '.*')
+    File.basename(@path, ".*")
   end
 
   private
 
-  def isUpper?(c)
-    /[[:upper:]]/.match(c)
+  def upper?(character)
+    /[[:upper:]]/.match(character)
   end
 
   def massage_author(input)
-    if nil == input
-      return nil
-    end
+    return if input.nil?
 
     reading_order = ""
     input.each_char do |c|
-      if isUpper?(c) and (reading_order.length > 0)
-        reading_order += " "
-      end
+      reading_order += " " if upper?(c) && !reading_order.empty?
       reading_order += c
     end
 
@@ -149,50 +126,48 @@ class Book
   end
 
   # Returns (series, volumeNo, titleText)
-  def processTitle(input)
-    if nil == input
-      return nil
-    end
+  def process_title(input)
+    return if input.nil?
 
-    arr = input.split('_')
+    arr = input.split("_")
 
     series = nil
     vol = nil
 
     first = arr[0]
-    matchData = (first).match(@@SERIES_AND_VOLUME_REGEX)
-    if nil != matchData
-      capt = matchData.captures
+    match_data = first.match(@@series_and_volume_regex)
+    unless match_data.nil?
+      capt = match_data.captures
       series = capt[0]
       vol = capt[1]
       arr.shift
     end
 
-    pos = arr[-1].rindex('.')
-    if nil != pos
-      arr[-1] = arr[-1].slice(0, pos)
-    end
+    pos = arr[-1].rindex(".")
+    arr[-1] = arr[-1].slice(0, pos) unless pos.nil?
 
-    title = arr.join(' ')
+    title = arr.join(" ")
 
-    bare_title_grouping = title_grouping
-      .split('_')
-      .reject { |part| part.match(@@SERIES_AND_VOLUME_REGEX) }
-      .join('_')
+    bare_title_grouping =
+      title_grouping
+      .split("_")
+      .reject { |part| part.match(@@series_and_volume_regex) }
+      .join("_")
 
     unless bare_title_grouping == Book.grouping_for_title(title)
-      puts "WARNING:  title_grouping mismatch:  #{bare_title_grouping.inspect} vs. #{Book.grouping_for_title(title).inspect}"
+      discrepancy = "#{bare_title_grouping.inspect} vs. #{Book.grouping_for_title(title).inspect}"
+      puts "WARNING:  title_grouping mismatch:  #{discrepancy}"
     end
 
-    return series, vol, title
+    [series, vol, title]
   end
 
   def parse_file_name!(file_name)
-    category = nil   # e.g., non-fiction, fan-fiction
-    grouping = ''
+    category = nil # e.g., non-fiction, fan-fiction
+    grouping = ""
 
-    parts = file_name.split('/')
-    (series_code, @volume, @title) = processTitle(parts[-1])
+    parts = file_name.split("/")
+    (series_code, @volume, @title) = process_title(parts[-1])
     if parts.length > 1
       grouping = parts[-2]
       reading_order = massage_author(grouping)
@@ -200,72 +175,68 @@ class Book
       @author = Author.new(grouping, reading_order, sort_order)
       @series_id = @store.get_series(grouping, series_code)
     end
-    if parts.length > 2
-      category = parts[-3]
-    end
+    category = parts[-3] if parts.length > 2
 
     lc_file_name = file_name.downcase
     if lc_file_name.end_with?(".epub")
-      scanEpub!(file_name)
+      scan_epub!(file_name)
     elsif lc_file_name.end_with?(".pdf")
       scan_pdf!(file_name)
     end
 
     @arrived = File.ctime(file_name)
 
-    @classification_id = @store.find_classification(@author.grouping, File.basename(file_name, '.*'))
+    @classification_id = @store.find_classification(@author.grouping, File.basename(file_name, ".*"))
 
     # TODO:  Fix horrible hard-coded strings and paths
-    if ('01_nonfic' == category) && (nil == classification_id)
-      open(Store.unclassified_csv, 'a') do |fd|
-        fd.puts('"' + grouping.to_s + '","' + path + '"')
-      end
+    return unless category == "00_nonFic" && classification_id.nil?
+
+    File.open(Store.unclassified_csv, "a") do |fd|
+      fd.puts "#{grouping.inspect},#{path.inspect}"
     end
   end
 
-  def scanEpub!(fileName)
-    #puts 'Scanning "' + fileName.to_s + '"...'
-    begin
-      Zip.warn_invalid_date = false
-      Zip::File.open(fileName) do |zipfile|
-        entry = zipfile.find_entry('META-INF/container.xml')
-        if nil == entry
-          puts 'No META-INF/container.xml, skipping book ' + fileName
-          return
-        end
-        contXml = zipfile.read('META-INF/container.xml')
-        contDoc = Nokogiri::XML(contXml)
-        opfPath = contDoc.css("container rootfiles rootfile")[0]['full-path']
-
-        scanOpf!(zipfile, opfPath)
+  def scan_epub!(file_name)
+    Zip.warn_invalid_date = false
+    Zip::File.open(file_name) do |zipfile|
+      entry = zipfile.find_entry("META-INF/container.xml")
+      if entry.nil?
+        puts "No META-INF/container.xml, skipping book #{file_name.inspect}"
+        return nil
       end
-    rescue Zip::Error => exc
-      puts 'ERROR processing file "' + fileName + '":'
-      puts exc.message
-      puts exc.backtrace
+      cont_xml = zipfile.read("META-INF/container.xml")
+      cont_doc = Nokogiri::XML(cont_xml)
+      opf_path = cont_doc.css("container rootfiles rootfile")[0]["full-path"]
+
+      scan_opf!(zipfile, opf_path)
+    rescue Zip::Error => e
+      puts "ERROR processing file #{file_name.inspect}:"
+      puts e.message
+      puts e.backtrace
     end
   end
 
   def scan_pdf!(file_name)
     pdf_path = File.expand_path(file_name).to_s
-    if ! pdf_path.end_with?('.pdf')
-      puts 'Unexpected internal error:  path "' + file_name.to_s + '" does not end with ".pdf".'
+    unless pdf_path.end_with?(".pdf")
+      puts "Unexpected internal error:  path #{file_name.inspect} does not end with \".pdf\"."
       return
     end
 
-    jpeg_path = pdf_path[0..-5] + '.jpeg'
-    if File.file?(jpeg_path)
-      File.open(jpeg_path, 'r') do |is|
-        @cover = Cover.new(is, jpeg_path, 'image/jpeg')
-      end
+    jpeg_path = "#{pdf_path[0..-5]}.jpeg"
+
+    return unless File.file?(jpeg_path)
+
+    File.open(jpeg_path, "r") do |is|
+      @cover = Cover.new(is, jpeg_path, "image/jpeg")
     end
   end
 
-  def scanOpf!(zipfile, opfPath)
-    coverId = nil
+  def scan_opf!(zipfile, opf_path)
+    cover_id = nil
 
-    opfXml = zipfile.read(opfPath)
-    opfDoc = Nokogiri::XML(opfXml)
+    opf_xml = zipfile.read(opf_path)
+    opf_doc = Nokogiri::XML(opf_xml)
 
     #-------
     # Author
@@ -274,122 +245,111 @@ class Book
     reading_order = @author.reading_order
     sort_order = @author.sort_order
 
-    creators = opfDoc.css('dc|creator', 'dc' => @@DC_NS_URL)
-    if (creators.length > 0)
+    creators = opf_doc.css("dc|creator", "dc" => @@dc_ns_url)
+    unless creators.empty?
       creator = creators[0]
-      if nil != creator
-        role = creator['opf:role']
-        if 'aut' == role
-          reading_order = creator.content
-
-          file_as = creator['opf:file-as']
-          if nil != file_as
-            sort_order = file_as
-          end
-        end
 
-        @author = Author.new(grouping, reading_order, sort_order)
+      return if creator.nil?
+
+      role = creator["opf:role"]
+      if role == "aut"
+        reading_order = creator.content
+
+        file_as = creator["opf:file-as"]
+        sort_order = file_as unless file_as.nil?
       end
+
+      @author = Author.new(grouping, reading_order, sort_order)
     end
 
     #---------------------------------------
     # Title
 
-    titles = opfDoc.css('dc|title', 'dc' => @@DC_NS_URL)
-    if titles.length > 0
+    titles = opf_doc.css("dc|title", "dc" => @@dc_ns_url)
+    unless titles.empty?
       title = titles[0]
-      if nil != title
-        @title = title.content
-      end
+      @title = title.content unless title.nil?
     end
 
     #---------------------------------------
     # Description
 
-    descrNodes = opfDoc.css('dc|description', 'dc' => @@DC_NS_URL)
-    if (descrNodes.length > 0)
-      descrNode = descrNodes[0]
-      if nil != descrNode
-        @description = descrNode.content
-      end
+    descr_nodes = opf_doc.css("dc|description", "dc" => @@dc_ns_url)
+    unless descr_nodes.empty?
+      descr_node = descr_nodes[0]
+      @description = descr_node.content unless descr_node.nil?
     end
 
     #---------------------------------------
     # Language
 
-    langNodes = opfDoc.css('dc|language', 'dc' => @@DC_NS_URL)
-    if (langNodes.length > 0)
-      langNode = langNodes[0]
-      if langNode
-        @language = langNode.content
-      end
+    lang_nodes = opf_doc.css("dc|language", "dc" => @@dc_ns_url)
+    unless lang_nodes.empty?
+      lang_node = lang_nodes[0]
+      @language = lang_node.content if lang_node
     end
 
     #---------------------------------------
     # Other metadata:  series, volume, cover
 
-    metas = opfDoc.css('package metadata meta')
-    for m in metas
-      name = m['name']
-      content = m['content']
+    metas = opf_doc.css("package metadata meta")
+    metas.each do |m|
+      name = m["name"]
+      content = m["content"]
 
-      if 'calibre:series' == name
+      case name
+      when "calibre:series"
         # TODO:  Dynamically create a new series?
         # @series_id = content
-      elsif 'calibre:series-index' == name
+      when "calibre:series-index"
         @volume = content
-      elsif 'cover' == name
-        coverId = content
-        #puts 'File ' + @path + ' coverId ' + coverId
+      when "cover"
+        cover_id = content
       end
     end
 
     #---------------
     # Load the cover
 
-    @cover = load_cover(zipfile, opfPath, opfDoc, coverId)
+    @cover = load_cover(zipfile, opf_path, opf_doc, cover_id)
   end
 
-  def load_cover(zipfile, opfPath, opfDoc, coverId)
-    if nil == coverId
-      coverId = "cover-image"
-    end
+  def load_cover(zipfile, opf_path, opf_doc, cover_id)
+    cover_id = "cover-image" if cover_id.nil?
 
-    items = opfDoc.css('package manifest item')
-    for i in items
-      href = i['href']
-      id = i['id']
-      mimeType = i['media-type']
-
-      if coverId == id
-        entry = zipfile.find_entry(href)
-
-        if nil == entry
-          # Although the epub standard requires the path to be relative
-          # to the base of the epub (zip), some books encountered in the
-          # wild have been found to use a bath relative to the location
-          # of the opf file.
-          parts = opfPath.split('/')
-          opfBasePath = parts[0..-2].join('/')
-          coverPath = opfBasePath + '/' + href
-          entry = zipfile.find_entry(coverPath)
-        end
+    items = opf_doc.css("package manifest item")
+    items.each do |i|
+      href = i["href"]
+      id = i["id"]
+      mime_type = i["media-type"]
 
-        unless entry
-          # Another case found in the wild:  cover image is at the root, but path is '../cover.jpeg'
-          if href.start_with? '../'
-            coverPath = href[3..-1]
-            entry = zipfile.find_entry(coverPath)
-          end
-        end
+      next unless cover_id == id
 
-        if nil == entry
-          puts 'WARNING!  Cover image "' + href + '" not found in file "' + @path + '".'
-          return nil
-        else
-          entry.get_input_stream() do |is|
-            return Cover.new(is, href, mimeType)
-          end
+      entry = zipfile.find_entry(href)
+
+      if entry.nil?
+        # Although the epub standard requires the path to be relative
+        # to the base of the epub (zip), some books encountered in the
+        # wild have been found to use a bath relative to the location
+        # of the opf file.
+        parts = opf_path.split("/")
+        opf_base_path = parts[0..-2].join("/")
+        cover_path = "#{opf_base_path}/#{href}"
+        entry = zipfile.find_entry(cover_path)
+      end
+
+      if !entry && href.start_with?("../")
+        # Another case found in the wild:  cover image is at the root, but path is '../cover.jpeg'
+        cover_path = href[3..]
+        entry = zipfile.find_entry(cover_path)
+      end
+
+      if entry.nil?
+        puts "WARNING!  Cover image #{href.inspect} not found in file #{@path.inspect}."
+        return nil
+      else
+        entry.get_input_stream do |is|
+          return Cover.new(is, href, mime_type)
         end
       end
     end
@@ -397,4 +357,3 @@ class Book
     nil
   end
 end
-
index 07eb853b8807c3c3ac01e2508562d50610ef3200..ca963ff7badaae45941ad1babd317b00b5ab2e2a 100644 (file)
@@ -1,10 +1,11 @@
 # frozen_string_literal: true
 
-require_relative 'book'
-require_relative 'store'
+require_relative "book"
+require_relative "store"
 
-class BookLoader 
-  DONE_MARKER = '<END>'
+# Worker thread that pulls filenames from a queue and loads them as new books
+class BookLoader
+  DONE_MARKER = "<END>"
 
   def initialize(config_file, queue)
     @config_file = config_file
@@ -13,10 +14,11 @@ class BookLoader
 
   def run
     @store = Store.new(@config_file)
-    @store.connect()
+    @store.connect
 
     file = @queue.pop
-    until file == DONE_MARKER do
+
+    until file == DONE_MARKER
       book = Book.new(@store)
       book.load_from_file!(file)
       @store.store_book(book)
@@ -24,6 +26,6 @@ class BookLoader
       file = @queue.pop
     end
 
-    @store.disconnect()
+    @store.disconnect
   end
 end
index 5eb6f7f24a9e3198a6c62aa64f09ed9885b0f263..c9055a13dbd1252e007d738311fc2f6b8026f5ed 100644 (file)
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+# Encapsulates information about a book's classification, under the
+# Dewey Decimal and/or Library of Congress systems.
 class Classification
   attr_accessor :id, :ddc, :lcc, :author_grouping, :author, :title_grouping, :title
 
@@ -23,7 +25,7 @@ class Classification
       title.nil? ? nil : "title=#{title.inspect}",
     ].compact.join(", ")
 
-    return "(Classification: #{field_info})"
+    "(Classification: #{field_info})"
   end
 
   def to_s
@@ -35,12 +37,10 @@ class Classification
   def reading_to_sort_order(reading_order)
     sort_order = reading_order
 
-    parts = reading_order.split(' ')
-    if parts.length > 1
-      sort_order = parts[-1] + ', ' + parts[0..-2].join(' ')
-    end
+    parts = reading_order.split(" ")
 
-    return sort_order
+    sort_order = [parts[-1], parts[0..-2].join(" ")].join(", ") if parts.length > 1
+
+    sort_order
   end
 end
-
index e3dbad43246b774b231ad7ad607c68406103b4ec..ba3c5707922a2884d61655608c2fde0c00a53b45 100644 (file)
@@ -1,55 +1,46 @@
 # frozen_string_literal: true
 
+# Encapsulates information about a book cover
 class Cover
   attr_reader :path
 
-  def initialize(inputStream, path, mimeType)
-    if nil != inputStream
-      @data = inputStream.read
-    else
-      @data = nil
-    end
+  def initialize(input_stream, path, mime_type)
+    @data =
+      if input_stream.nil?
+        nil
+      else
+        input_stream.read
+      end
+
     @path = path
-    @mimeType = mimeType
+    @mime_type = mime_type
   end
 
   def inspect
     field_info = [
-      @data.nil? ? nil : "size=#{@data.length.to_s}",
+      @data.nil? ? nil : "size=#{@data.length}",
       @path.nil? ? nil : "path=#{@path.inspect}",
-      @mimeType.nil? ? nil : "mime_type=#{@mimeType.inspect}",
+      @mime_type.nil? ? nil : "mime_type=#{@mime_type.inspect}",
     ].compact.join(" ")
 
     "(Cover: #{field_info})"
   end
 
   def read_image(filename)
-    open(filename, 'rb') do |fd|
-      @data = fd.read()
+    File.open(filename, "rb") do |fd|
+      @data = fd.read
     end
   end
 
   def to_s
-    return inspect
+    inspect
   end
 
-  def write_image(outputDir, filename)
-    open(outputDir + '/' + filename, 'wb') do |fd|
+  def write_image(output_dir, filename)
+    File.open("#{output_dir}/#{filename}", "wb") do |fd|
       fd.write(@data)
     end
 
-    return filename, @mimeType
-  end
-
-  private
-
-  def getExt
-    pos = @path.rindex('.')
-    if nil == pos
-      return '.img'
-    end
-
-    @path.slice(pos, @path.length)
+    [filename, @mime_type]
   end
 end
-
index c695941963654944c58e79057cb807892ab71e2f..13854ea5116f77129c9d85b90fe4e9daf3630df8 100644 (file)
@@ -1,50 +1,44 @@
-require 'find'
-require 'pathname'
+# frozen_string_literal: true
+
+require "find"
+require "pathname"
 
 def exec(cmdline)
   puts "$ #{cmdline}"
   result = system(cmdline)
-  unless result
-    puts "FAILED:  #{cmdline}"
-  end
+  puts "FAILED:  #{cmdline}" unless result
   result
 end
 
 def extract_epub(source_file, source_path, dest_path)
-  relative_path = source_file[source_path.length .. source_file.length]
+  relative_path = source_file[source_path.length..source_file.length]
   dest_file = "#{dest_path}/#{relative_path}"
-  dest_file = dest_file[0 .. (dest_file.length - 6)] + ".txt"
+  dest_file = "#{dest_file[0..(dest_file.length - 6)]}.txt"
 
   required_path = Pathname(dest_file).dirname
-  unless File.directory? required_path
-    unless exec("mkdir -p #{required_path}")
-      return false
-    end
-  end
+
+  return false unless File.directory?(required_path) || exec("mkdir -p #{required_path}")
 
   if File.exist? dest_file
     source_time = File.mtime source_file
     dest_time = File.mtime dest_file
     comp = dest_time <=> source_time
-    if comp > 0
-      return true # Nothing to do, extraction is already up-to-date
-    end
+    return true if comp.positive? # Nothing to do, extraction is already up-to-date
   end
-    
+
   exec("ebook-convert #{source_file} #{dest_file}")
 end
 
-def scan_dir(source_path, dest_path) 
+def scan_dir(source_path, dest_path)
   Find.find(source_path) do |f|
-    if f.match(/.epub\Z/)
-      unless (f.match(/_bis.epub\Z/) || f.match(/_ter.epub\Z/) || f.match(/_quater.epub\Z/))
-        extract_epub(f, source_path, dest_path)
-      end
-    end
+    next unless f.match(/.epub\Z/)
+    next if f.match(/_bis.epub\Z/) || f.match(/_ter.epub\Z/) || f.match(/_quater.epub\Z/)
+
+    extract_epub(f, source_path, dest_path)
   end
 end
 
 dest_path = ARGV[0]
-for arg in ARGV[1 .. ARGV.length]
+ARGV[1..ARGV.length].each do |arg|
   scan_dir(arg, dest_path)
 end
index 1eee8b485c51ad1c3faac831b8f674fe806eebd4..abc4601a74066ec85d0d28a9e49203131504b35e 100644 (file)
@@ -1,58 +1,55 @@
 # frozen_string_literal: true
 
-require_relative 'navigator'
-require_relative 'page'
-require_relative 'store'
-require_relative 'walk_dir'
+require_relative "navigator"
+require_relative "page"
+require_relative "store"
+require_relative "walk_dir"
 
-@outputDir = 'output'
+@output_dir = "output"
 
-@config_file = 'quanlib.ini'
+@config_file = "quanlib.ini"
 @skip_class = false
 
-def handleArg(arg)
+def handle_arg(arg)
   if arg.start_with?("--config=")
-    @config_file = arg[9..-1]
-    puts 'Using config file "' + @config_file + '".'
-  elsif "--purge" == arg
-    puts 'Purging database...'
-    @store.dropSchema()
-    if File.exists?(@store.unclassified_csv)
-      File.delete(@store.unclassified_csv)
-    end
-  elsif "--skip-class" == arg
-    puts 'Skipping load of classification table.'
+    @config_file = arg[9..]
+    puts "Using config file #{@config_file.inspect}."
+  elsif arg == "--purge"
+    puts "Purging database..."
+    @store.drop_schema
+    File.delete(@store.unclassified_csv) if File.exist?(@store.unclassified_csv)
+  elsif arg == "--skip-class"
+    puts "Skipping load of classification table."
     @skip_class = true
   elsif arg.start_with?("--")
-    abort('ERROR:  Unrecognized option "' + arg + '".')
+    abort("ERROR:  Unrecognized option #{arg.inspect}.")
   end
 end
 
 @store = Store.new(@config_file)
-@store.connect()
+@store.connect
 
-for arg in ARGV
-  handleArg(arg)
+ARGV.each do |arg|
+  handle_arg(arg)
 end
 
 @store.init_db(@skip_class)
 
-for arg in ARGV
-  if ! arg.start_with?("--")
-    puts 'Scanning directory "' + arg + '"...'
-    w = WalkDir.new(@config_file, arg)
-    w.books
-  end
+ARGV.each do |arg|
+  next if arg.start_with?("--")
+
+  puts "Scanning directory #{arg.inspect}..."
+  w = WalkDir.new(@config_file, arg)
+  w.books
 end
 
 @store.cross_reference_lists
 
-puts 'Creating output...'
+puts "Creating output..."
 
 navigator = Navigator.new(@store)
-navigator.write_atoz_pages()
-navigator.write_series_listing()
-navigator.write_dewey()
-
-@store.disconnect()
+navigator.write_atoz_pages
+navigator.write_series_listing
+navigator.write_dewey
 
+@store.disconnect
index 881b1fa8237231d524197eb8f0cf188d58330268..df0f4e156aab44255a6741858083cad55e3f6bd0 100644 (file)
@@ -1,6 +1,9 @@
-require_relative 'page'
-require_relative 'store'
+# frozen_string_literal: true
 
+require_relative "page"
+require_relative "store"
+
+# Generates navigation pages for the static HTML output of the library index
 class Navigator
   def initialize(store)
     @store = store
@@ -9,55 +12,52 @@ class Navigator
   def write_atoz_pages
     atoz_counts = {}
 
-    ('A'..'Z').each do |letter| 
+    ("A".."Z").each do |letter|
       atoz_counts[letter] = write_authors_starting_with(letter)
     end
 
-    content = '<p><table><tr><th>Author</th><th>Books</th></tr>'
-    ('A'..'Z').each do |letter|
-      content += '  <tr><td><a href="../atoz/output_' + letter + '.html">Starting with ' + letter + '</a></td><td>' + atoz_counts[letter].to_s + '</td></tr>'
+    content = "<p><table><tr><th>Author</th><th>Books</th></tr>"
+    ("A".."Z").each do |letter|
+      link = "<a href=\"../atoz/output_#{letter}.html\">Starting with #{letter}</a>"
+      content += "  <tr><td>#{link}</td><td>#{atoz_counts[letter]}</td></tr>"
     end
-    content += '</table></p>'
+    content += "</table></p>"
     page = Page.new(@store)
-    page.output_dir = 'atoz'
+    page.output_dir = "atoz"
     page.special = content
-    page.up = ['../output/index.html', 'Up']
+    page.up = ["../output/index.html", "Up"]
 
-    page.write_html( [] )
+    page.write_html([])
   end
 
   def write_authors_starting_with(letter)
-    book_ids = @store.query_books_by_author(letter + '%')
-    puts 'Authors starting with "' + letter + '":  ' + book_ids.length.to_s() + ' books.'
+    book_ids = @store.query_books_by_author("#{letter}%")
+    puts "Authors starting with #{letter.inspect}:  #{book_ids.length} books."
 
     page = Page.new(@store)
-    if 'A' != letter
-      page.back = ['../atoz/output_' + (letter.ord - 1).chr + '.html', 'Prev']
-    end
-    if 'Z' != letter
-      page.forward = ['../atoz/output_' + (letter.ord + 1).chr + '.html', 'Next']
-    end
-    page.output_dir = 'atoz'
-    page.index_file = 'output_' + letter + '.html'
-    page.title = "Authors starting with '" + letter + "'"
-    page.up = ['../atoz/index.html', 'Up']
+    page.back = ["../atoz/output_#{(letter.ord - 1).chr}.html", "Prev"] if letter != "A"
+    page.forward = ["../atoz/output_#{(letter.ord + 1).chr}.html", "Next"] if letter != "Z"
+    page.output_dir = "atoz"
+    page.index_file = "output_#{letter}.html"
+    page.title = "Authors starting with #{letter.inspect}"
+    page.up = ["../atoz/index.html", "Up"]
 
     page.write_html(book_ids)
-    return book_ids.length
+    book_ids.length
   end
 
   def write_dewey
-    book_ids = @store.query_books_by_ddc()
-    puts 'Non-fiction books arranged by Dewey Decimal Classification:  ' + book_ids.length.to_s() + ' books.'
+    book_ids = @store.query_books_by_ddc
+    puts "Non-fiction books arranged by Dewey Decimal Classification:  #{book_ids.length} books."
 
     page = Page.new(@store)
-    page.output_dir = 'ddc'
-    page.index_file = 'index.html'
+    page.output_dir = "ddc"
+    page.index_file = "index.html"
     page.title = "Non-fiction books arranged by Dewey Decimal call number"
-    page.up = ['../output/index.html', 'Up']
-    
+    page.up = ["../output/index.html", "Up"]
+
     page.write_html(book_ids)
-    return book_ids.length
+    book_ids.length
   end
 
   def write_series_for_age(age)
@@ -68,45 +68,33 @@ class Navigator
     series_ids.each do |id|
       series = @store.load_series(id)
       book_ids = @store.query_books_by_series_id(id)
-      if nil != book_ids and book_ids.length > 0
-        series_infos.push( [series, book_ids] )
-      end
+      series_infos.push([series, book_ids]) unless book_ids&.empty?
     end
 
-    for idx in 0 .. (series_infos.length - 1) do 
-      #puts series.descr + ': ' + book_ids.length.to_s + ' books.'
-
+    0..(series_infos.length - 1).each do |idx|
       back = nil
       fwd = nil
 
-      if idx > 0
-        back = series_infos[idx-1]
-      end
-      if (idx + 1) < series_infos.length
-        fwd = series_infos[idx+1]
-      end
+      back = series_infos[idx - 1] if idx.positive?
+      fwd = series_infos[idx + 1] if (idx + 1) < series_infos.length
 
       cur = series_infos[idx]
       series = cur[0]
       book_ids = cur[1]
 
       page = Page.new(@store)
-      if nil != back
-        page.back = [back[0].key + '.html', 'Back']
-      end
-      if nil != fwd
-        page.forward = [fwd[0].key + '.html', 'Forward']
-      end
-      page.output_dir = 'series/series_' + age
-      page.index_file = series.key + '.html'
-      page.title = 'Series &ldquo;' + series.descr + '&rdquo; (' + book_ids.length.to_s + ' books)'
-      page.up = ['index.html', 'Up']
-  
+      page.back = ["#{back[0].key}.html", "Back"] unless back.nil?
+      page.forward = ["#{fwd[0].key}.html", "Forward"] unless fwd.nil?
+      page.output_dir = "series/series_#{age}"
+      page.index_file = "#{series.key}.html"
+      page.title = "Series &ldquo;#{series.descr}&rdquo; (#{book_ids.length} books)"
+      page.up = ["index.html", "Up"]
+
       page.write_html(book_ids)
     end
 
-    content =  '<h1>&ldquo;' + age + '&rdquo; Series</h1>'
-    content += '<p><table><tr><th>Author</th><th>Series</th><th>Genre</th><th>Books</th></tr>'
+    content =  "<h1>&ldquo;#{age}&rdquo; Series</h1>"
+    content += "<p><table><tr><th>Author</th><th>Series</th><th>Genre</th><th>Books</th></tr>"
     series_infos.each do |cur|
       series = cur[0]
       book_ids = cur[1]
@@ -114,44 +102,44 @@ class Navigator
       author = series.grouping
       letter = author[0]
 
-      content += '  <tr>'
-      content += '<td><a href="../../atoz/output_' + letter + '.html">' + author + '</a></td>'
-      content += '<td><a href="' + series.key + '.html">' + series.descr + '</a></td>'
-      content += '<td>' + series.genre + '</td>'
-      content += '<td>' + book_ids.length.to_s + '</td>'
-      content += '</tr>'
+      content += "  <tr>"
+      content += "<td><a href=\"../../atoz/output_#{letter}.html\">#{author}</a></td>"
+      content += "<td><a href=\"#{series.key}.html\">#{series.descr}</a></td>"
+      content += "<td>#{series.genre}</td>"
+      content += "<td>#{book_ids.length}</td>"
+      content += "</tr>"
     end
-    content += '</table></p>'
+    content += "</table></p>"
     page = Page.new(@store)
-    page.output_dir = 'series/series_' + age
+    page.output_dir = "series/series_#{age}"
     page.special = content
-    page.up = ['../index.html', 'Up']
-    page.write_html( [] )
+    page.up = ["../index.html", "Up"]
+    page.write_html([])
 
-    return series_infos.length
+    series_infos.length
   end
 
   def write_series_listing
-    ages = ['beginner', 'junior', 'ya', 'adult']
+    ages = %w[beginner junior ya adult]
     series_counts = {}
 
     ages.each do |age|
-      puts 'Series for "' + age + '" readers...'
+      puts "Series for #{age.inspect} readers..."
 
       series_counts[age] = write_series_for_age(age)
     end
 
-    content = '<h1>Browse Books By Series</h1>'
-    content += '<p>'
-    content += '<table><tr><th>Age</th><th>Number of Series</th></tr>'
+    content = "<h1>Browse Books By Series</h1>"
+    content += "<p>"
+    content += "<table><tr><th>Age</th><th>Number of Series</th></tr>"
     ages.each do |age|
-      content += '<tr><td><a href="series_' + age + '/index.html">' + age + '</a></td><td>' + series_counts[age].to_s + '</td></tr>'
+      content += "<tr><td><a href=\"series_#{age}/index.html\">#{age}</a></td><td>#{series_counts[age]}</td></tr>"
     end
-    content += '</table></p>'
+    content += "</table></p>"
     page = Page.new(@store)
-    page.output_dir = 'series'
+    page.output_dir = "series"
     page.special = content
-    page.up = ['../output/index.html', 'Up']
-    page.write_html( [] )
+    page.up = ["../output/index.html", "Up"]
+    page.write_html([])
   end
 end
index 638f9ad1b7c064698c0078e9a34524c8a0ed3d58..2d78d0543b0c86c9af40eb22538d2bff6bae5957 100644 (file)
-require 'fileutils'
+# frozen_string_literal: true
 
-require_relative 'store'
+require "fileutils"
 
+require_relative "store"
+
+# Handles producing a page of output for the Navigator
 class Page
+  attr_accessor :back, :forward, :index_file, :output_dir, :special, :title, :up
+
   def initialize(store)
     @back = nil
     @forward = nil
-    @index_file = 'index.html'
-    @output_dir = 'output'
+    @index_file = "index.html"
+    @output_dir = "output"
     @special = nil
     @store = store
-    @title = 'Books'
+    @title = "Books"
     @up = nil
   end
 
-  def back=(value)
-    @back = value
-  end
-
-  def forward=(value)
-    @forward = value
-  end
-
-  def index_file=(value)
-    @index_file = value
-  end
+  def write_books(file_descriptor, book_ids)
+    book_ids.each do |id|
+      book = @store.load_book(id)
+      image =
+        if book.cover.nil?
+          "(No cover image)"
+        else
+          "<img class=\"cover-thumb\" src=#{book.cover.path.inspect}/>"
+        end
+
+      file_descriptor.puts "    <div><table>"
+      file_descriptor.puts "      <tr><td><a href=#{book.path.inspect}>#{image}</a></td>"
+
+      heading = book.heading
+      description = book.description
+      if description.nil?
+        file_descriptor.puts "          <td>#{heading}</td></tr>"
+      else
+        pop_inner = "<span class=\"pop-inner\"><p>#{heading}</p><p>#{description}</p></span>"
+        file_descriptor.puts "          <td><span class=\"popup\">#{heading}#{pop_inner}</span></td></tr>"
+      end
 
-  def navig_link(data)
-    if (nil == data)
-      return ''
+      file_descriptor.puts "    </table></div>"
     end
-    return '<a href="' + data[0] + '">' + data[1] + '</a>'
-  end
-
-  def output_dir=(value)
-    @output_dir = value
-  end
-
-  def special=(value)
-    @special = value
   end
 
-  def title=(value)
-    @title = value
-  end
+  private
 
-  def up=(value)
-    @up = value
+  def navig_back_up_forward
+    [navig_link(@back), navig_link(@up), navig_link(@forward)].join(" ")
   end
 
-  def write_books(fd, book_ids)
-    for id in book_ids
-      book = @store.load_book(id)
-      image = nil
-      if nil != book.cover
-        #@imageCount += 1
-        #(path, mimeType) = book.cover.write_image(@output_dir, 'image' + @imageCount.to_s)
-        #image = '<img class="cover-thumb" src="' + path + '"/>'
-        path = book.cover.path
-        image = '<img class="cover-thumb" src="' + path + '"/>'
-      else
-        image = '(No cover image)'
-      end
-
-      fd.puts '    <div><table>'
-      fd.puts '      <tr><td><a href="' + book.path + '">' + image + '</a></td>'
+  def navig_link(data)
+    return "" if data.nil?
 
-      heading = book.heading()
-      description = book.description()
-      if nil != description
-        fd.puts '          <td><span class="popup">' + heading + '<span class="pop-inner"><p>' + heading + '</p><p>' + description + '</p></span></span></td></tr>'
-      else
-        fd.puts '          <td>' + heading + '</td></tr>'
-      end
-    
-      fd.puts '    </table></div>'
-    end
+    "<a href=#{data[0].inspect}>#{data[1]}</a>"
   end
 
-  def write_footer(fd)
-    fd.puts '    <p class="navigator">' + navig_link(@back) + ' ' + navig_link(@up) + ' ' + navig_link(@forward) + '</p>'
+  def write_footer(file_descriptor)
+    file_descriptor.puts "    <p class=\"navigator\">#{navig_back_up_forward}</p>"
   end
 
-  def write_header(fd)
-    fd.puts '    <h1 class="header">' + @title + '</h1>'
+  def write_header(file_descriptor)
+    file_descriptor.puts "    <h1 class=\"header\">#{@title}</h1>"
 
-    fd.puts '    <p class="navigator">' + navig_link(@back) + ' ' + navig_link(@up) + ' ' + navig_link(@forward) + '</p>'
+    file_descriptor.puts "    <p class=\"navigator\">#{navig_back_up_forward}</p>"
   end
 
   def write_html(book_ids)
-    @imageCount = 0
+    FileUtils.mkdir_p(@output_dir) unless Dir.exist?(@output_dir)
 
-    if ! Dir.exist?(@output_dir)
-      FileUtils.mkdir_p(@output_dir)
-    end
+    File.open("#{@output_dir}/#{@index_file}", "w") do |file_descriptor|
+      file_descriptor.puts "<html>"
+      file_descriptor.puts "  <head>"
+      file_descriptor.puts "    <meta charset=\"utf-8\"/>"
+      file_descriptor.puts "    <title>#{@title}</title>"
 
-    open(@output_dir + '/' + @index_file, 'w') do |fd|
-      fd.puts '<html>'
-      fd.puts '  <head>'
-      fd.puts '    <meta charset="utf-8"/>'
-      fd.puts '    <title>' + @title + '</title>'
+      write_style_sheet(file_descriptor)
 
-      write_style_sheet(fd)
+      file_descriptor.puts "  </head>"
+      file_descriptor.puts "  <body>"
 
-      fd.puts '  </head>'
-      fd.puts '  <body>'
-      
-      write_header(fd)
+      write_header(file_descriptor)
 
-      write_special(fd)
-      write_books(fd, book_ids)
-  
-      write_footer(fd)
+      write_special(file_descriptor)
+      write_books(file_descriptor, book_ids)
 
-      fd.puts "  </body>"
-      fd.puts "</html>"
+      write_footer(file_descriptor)
+
+      file_descriptor.puts "  </body>"
+      file_descriptor.puts "</html>"
     end
   end
 
-  def write_special(fd)
-    if (nil != @special)
-      fd.puts(@special)
-    end
+  def write_special(file_descriptor)
+    file_descriptor.puts(@special) unless @special.nil?
   end
 
-  def write_style_sheet(fd)
-      style = 
-<<EOS
-    <style>
-      div { 
-        display: inline-block;
-        width: 400px;
-        margin: 10px;
-        border 3px solid #73ad21;
-      }
-      h1.header { 
-        background: #4040a0;
-        color: #ffffff;
-        text-align: center;
-      }
-      img.cover-thumb { max-height: 200px; max-width: 200px; }
-      p.navigator { }
-      span.popup { }
-      span.popup:hover { text-decoration: none; background: #cfffff; z-index: 6; }
-      span.popup span.pop-inner { 
-        border-color:black; 
-        border-style:solid; 
-        border-width:1px;
-        display: none; 
-        margin: 4px 0 0 0px; 
-        padding: 3px 3px 3px 3px;
-        position: absolute; 
-      }
-      span.popup:hover span.pop-inner { 
-        background: #ffffaf; 
-        display: block; 
-        margin: 20px 0 0 0px; 
-        z-index:6;
-      }
-    </style>
-EOS
-      fd.puts style
+  def write_style_sheet(file_descriptor)
+    style = <<~END_STYLE
+      <style>
+        div {
+          display: inline-block;
+          width: 400px;
+          margin: 10px;
+          border 3px solid #73ad21;
+        }
+        h1.header {
+          background: #4040a0;
+          color: #ffffff;
+          text-align: center;
+        }
+        img.cover-thumb { max-height: 200px; max-width: 200px; }
+        p.navigator { }
+        span.popup { }
+        span.popup:hover { text-decoration: none; background: #cfffff; z-index: 6; }
+        span.popup span.pop-inner {
+          border-color:black;
+          border-style:solid;
+          border-width:1px;
+          display: none;
+          margin: 4px 0 0 0px;
+          padding: 3px 3px 3px 3px;
+          position: absolute;
+        }
+        span.popup:hover span.pop-inner {
+          background: #ffffaf;
+          display: block;
+          margin: 20px 0 0 0px;
+          z-index:6;
+        }
+      </style>
+    END_STYLE
+
+    file_descriptor.puts style
   end
 end
-
index 86617048c0dce4c60c2d56103a29c7c1e54f8b5c..418c70e98f2cad0ff5b49806d9b8ef4794197f49 100644 (file)
@@ -1,17 +1,15 @@
+# frozen_string_literal: true
 
+# Encapsulates data about a series of books
 class Series
   attr_reader :id
 
-  attr_accessor :age
-  attr_accessor :code
-  attr_accessor :descr
-  attr_accessor :genre
-  attr_accessor :grouping
+  attr_accessor :age, :code, :descr, :genre, :grouping
 
   def initialize(id)
     @age = nil
     @genre = nil
-    @grouping = nil 
+    @grouping = nil
     @code = nil
     @descr = nil
     @id = id
@@ -30,9 +28,7 @@ class Series
   end
 
   def key
-    if nil != grouping and nil != code
-      return grouping.to_s + '_' + code.to_s
-    end
+    return "#{grouping}_#{code}" if !grouping.nil? && !code.nil?
 
     id.to_s
   end
@@ -41,4 +37,3 @@ class Series
     inspect
   end
 end
-
index 2ee597ae5536caecad2c5782c0c2f4665abca04e..e87ca09e015751bce2fdd4d9a6c246fac0a93d7d 100644 (file)
@@ -1,52 +1,59 @@
 # frozen_string_literal: true
 
-require 'csv'
-require 'fileutils'
-require 'inifile'
-require 'pg'
+require "csv"
+require "English"
+require "fileutils"
+require "inifile"
+require "pg"
 
-require_relative 'series'
-require_relative 'tconn'
+require_relative "series"
+require_relative "tconn"
 
+# Handles reads/writes of data stored in the DB
 class Store
   def unclassified_csv
-    @basePath + '/csv/unclassified.csv'
+    "#{@base_path}/csv/unclassified.csv"
   end
 
   def initialize(config_file)
     @conn = nil
 
     config = IniFile.load(config_file)
-    if nil == config
-      puts 'FATAL:  Failed to load config file "' + config_file + '".  Aborting initialization.'
+    if config.nil?
+      puts "FATAL:  Failed to load config file #{config_file.inspect}.  Aborting initialization."
       return
     end
 
-    section = config['database']
-    @dbhost = section['host']
+    section = config["database"]
+    @dbhost = section["host"]
     @dbport = 5432
-    @dbname = section['name']
-    @dbuser = section['user']
-    @dbpass = section['pass']
+    @dbname = section["name"]
+    @dbuser = section["user"]
+    @dbpass = section["pass"]
 
-    section = config['filesystem']
-    @basePath = section['basePath']
+    section = config["filesystem"]
+    @base_path = section["basePath"]
   end
 
   def connect
-    @conn = TimedConn.new(PG.connect(@dbhost, @dbport, '', '', @dbname, @dbuser, @dbpass))
-    return @conn
+    @conn = TimedConn.new(PG.connect(@dbhost, @dbport, "", "", @dbname, @dbuser, @dbpass))
+    @conn
   end
 
   def disconnect
-    @conn.close()
+    @conn.close
   end
 
   def construct_efs_path(efs_id)
-    id_str = sprintf('%010d', efs_id)
-    path = sprintf('%s/%s/%s/%s', id_str[0,2], id_str[2,2], id_str[4,2], id_str[6,2])
-    name = id_str + '.dat'
-    return path, name
+    id_str = format("%010d", efs_id)
+
+    # rubocop:disable Style/FormatStringToken
+    path = format("%s/%s/%s/%s", id_str[0, 2], id_str[2, 2], id_str[4, 2], id_str[6, 2])
+    # rubocop:enable Style/FormatStringToken
+
+    name = "#{id_str}.dat"
+
+    [path, name]
   end
 
   def cross_reference_lists
@@ -56,18 +63,16 @@ class Store
   end
 
   def create_schema(skip_class)
-    create_authors =
-<<EOS
+    create_authors = <<~SQL
       CREATE TABLE Authors (
         id          INTEGER PRIMARY KEY,
         grouping    VARCHAR(64),
         reading     VARCHAR(512),
         sort        VARCHAR(512)
       );
-EOS
+    SQL
 
-    create_books =
-<<EOS
+    create_books = <<~SQL
       CREATE TABLE Books (
         id             INTEGER PRIMARY KEY,
         arrived        TIMESTAMP,
@@ -81,10 +86,9 @@ EOS
         title          VARCHAR(256),
         volume         VARCHAR(16)
       );
-EOS
+    SQL
 
-    create_classification =
-<<EOS
+    create_classification = <<~SQL
       CREATE TABLE Classifications (
         id              INTEGER PRIMARY KEY,
         ddc             VARCHAR(32),
@@ -94,36 +98,32 @@ EOS
         title_grouping  VARCHAR(256),
         title           VARCHAR(256)
       );
-EOS
+    SQL
 
-    create_efs =
-<<EOS
+    create_efs = <<~SQL
       CREATE TABLE EFS (
         id          INTEGER PRIMARY KEY,
         mimetype    VARCHAR(64)
       );
-EOS
+    SQL
 
-    create_fast =
-<<EOS
+    create_fast = <<~SQL
       CREATE TABLE FAST (
         id          VARCHAR(32) PRIMARY KEY,
         descr       VARCHAR(128)
       );
-EOS
+    SQL
 
     # Associative entity, linking FAST and Classifications tables
     # in a 0..n to 0..m relationship
-    create_fast_classifications =
-<<EOS
+    create_fast_classifications = <<~SQL
       CREATE TABLE FAST_Classifications (
         fast           VARCHAR(32) REFERENCES FAST(id),
         classification INTEGER REFERENCES Classifications(id)
       );
-EOS
+    SQL
 
-    create_lists =
-<<EOS
+    create_lists = <<~SQL
       CREATE TABLE Lists (
         id             INTEGER PRIMARY KEY,
         age            VARCHAR(32),
@@ -133,20 +133,18 @@ EOS
         author         INTEGER REFERENCES Authors(id),
         title          VARCHAR(256)
       );
-EOS
+    SQL
 
     # Associative entity, linking Lists and Books tables
     # in a 0..n to 0..m relationship
-    create_lists_books =
-<<EOS
+    create_lists_books = <<~SQL
       CREATE TABLE Lists_Books (
         list           INTEGER REFERENCES Lists(id),
         book           INTEGER REFERENCES Books(id)
       );
-EOS
+    SQL
 
-    create_series =
-<<EOS
+    create_series = <<~SQL
       CREATE TABLE Series (
         id          INTEGER PRIMARY KEY,
         age         VARCHAR(32),
@@ -155,7 +153,7 @@ EOS
         code        VARCHAR(16),
         descr       VARCHAR(128)
       )
-EOS
+    SQL
 
     stmts = [
       create_authors,
@@ -167,15 +165,15 @@ EOS
       create_fast_classifications,
       create_lists,
       create_lists_books,
-      'CREATE SEQUENCE author_id;',
-      'CREATE SEQUENCE book_id;',
-      'CREATE SEQUENCE classification_id;',
-      'CREATE SEQUENCE efs_id;',
-      'CREATE SEQUENCE list_id;',
-      'CREATE SEQUENCE series_id;'
+      "CREATE SEQUENCE author_id;",
+      "CREATE SEQUENCE book_id;",
+      "CREATE SEQUENCE classification_id;",
+      "CREATE SEQUENCE efs_id;",
+      "CREATE SEQUENCE list_id;",
+      "CREATE SEQUENCE series_id;",
     ]
 
-    for stmt in stmts
+    stmts.each do |stmt|
       @conn.exec(stmt)
     end
 
@@ -187,43 +185,41 @@ EOS
     populate_series_table
   end
 
-  def dropSchema
+  def drop_schema
     stmts = [
-      'DROP TABLE Lists_Books;',
-      'DROP TABLE Lists;',
-      'DROP TABLE Books;',
-      'DROP TABLE FAST_Classifications;',
-      'DROP TABLE Authors;',
-      'DROP TABLE Classifications;',
-      'DROP TABLE EFS;',
-      'DROP TABLE FAST;',
-      'DROP TABLE Series;',
-      'DROP SEQUENCE author_id;',
-      'DROP SEQUENCE book_id;',
-      'DROP SEQUENCE classification_id;',
-      'DROP SEQUENCE efs_id;',
-      'DROP SEQUENCE list_id;',
-      'DROP SEQUENCE series_id;'
+      "DROP TABLE Lists_Books;",
+      "DROP TABLE Lists;",
+      "DROP TABLE Books;",
+      "DROP TABLE FAST_Classifications;",
+      "DROP TABLE Authors;",
+      "DROP TABLE Classifications;",
+      "DROP TABLE EFS;",
+      "DROP TABLE FAST;",
+      "DROP TABLE Series;",
+      "DROP SEQUENCE author_id;",
+      "DROP SEQUENCE book_id;",
+      "DROP SEQUENCE classification_id;",
+      "DROP SEQUENCE efs_id;",
+      "DROP SEQUENCE list_id;",
+      "DROP SEQUENCE series_id;",
     ]
 
-    for stmt in stmts do
-      begin
-        @conn.exec(stmt)
-      rescue Exception => exc
-        puts 'WARNING:  "' + stmt + '" failed:  ' + exc.to_s
-      end
+    stmts.each do |stmt|
+      @conn.exec(stmt)
+    rescue PG::Error => e
+      puts "WARNING:  #{stmt.inspect} failed: #{e}"
     end
   end
 
   def find_all_authors(author_name)
     result = []
 
-    sqlSelect = "SELECT id FROM Authors WHERE grouping=$1;"
+    sql_select = "SELECT id FROM Authors WHERE grouping=$1;"
     args = [author_name]
 
-    @conn.exec_params(sqlSelect, args) do |rs|
+    @conn.exec_params(sql_select, args) do |rs|
       rs.each do |row|
-        result << row['id']
+        result << row["id"]
       end
     end
 
@@ -231,191 +227,194 @@ EOS
   end
 
   def find_author(author)
-    sqlSelect = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
+    sql_select = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
     args = [author.grouping, author.reading_order, author.sort_order]
 
-    @conn.exec_params(sqlSelect, args) do |rs|
-      if rs.ntuples > 0
-        return rs[0]['id']
-      end
+    @conn.exec_params(sql_select, args) do |rs|
+      return rs[0]["id"] if rs.ntuples.positive?
     end
 
-    return nil
+    nil
   end
 
   def init_db(skip_class)
     sql = "SELECT 1 FROM pg_tables WHERE tableowner='quanlib' AND tablename='books'"
     found = false
-    @conn.exec(sql).each do |row|
+    @conn.exec(sql).each do |_row|
       found = true
     end
 
-    if ! found
-      create_schema(skip_class)
-    end
+    create_schema(skip_class) unless found
   end
 
   def load_author(id)
-    sqlSelect = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
+    sql_select = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
     args = [id]
-    @conn.exec_params(sqlSelect, args) do |rs|
-      if rs.ntuples != 1
-        raise "Expected 1 row for " + id + " but got " + rs.ntuples + ":  " + sqlSelect
-      end
+    @conn.exec_params(sql_select, args) do |rs|
+      raise "Expected 1 row for #{id} but got #{rs.ntuples}: #{sql_select}" if rs.ntuples != 1
+
       row = rs[0]
-      author = Author.new(row['grouping'], row['reading'], row['sort'])
+      author = Author.new(row["grouping"], row["reading"], row["sort"])
       return author
     end
-    return nil
+    nil
   end
 
   def store_author(author)
     id = find_author(author)
-    if nil == id
-      id = next_id('author_id')
-      sqlInsert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
+    if id.nil?
+      id = next_id("author_id")
+      sql_insert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
       args = [id, author.grouping, author.reading_order, author.sort_order]
       begin
-        rs = @conn.exec_params(sqlInsert, args)
-      rescue Exception => e
-        puts sqlInsert + ":  " + args.inspect()
+        rs = @conn.exec_params(sql_insert, args)
+      rescue PG::Error => e
+        puts "#{sql_insert}: #{args.inspect}"
         puts e.message
-        puts $@
+        puts $ERROR_POSITION
       ensure
-        rs.clear if rs
+        rs&.clear
       end
     end
-    return id
+    id
   end
 
   def load_book(id)
-    sql = "SELECT author, classification, cover, description, language, path, series, title, volume FROM Books WHERE id=$1;"
+    sql = <<~SQL
+      SELECT author, classification, cover, description, language, path, series, title, volume
+      FROM Books WHERE id=$1;
+    SQL
     book = nil
 
     begin
       @conn.exec_params(sql, [id]) do |rs|
-        if 1 != rs.ntuples
-          raise 'Expected one row in Books for id ' + id + ', but found ' + rs.length + '.'
-          return nil
-        end
+        raise "Expected one row in Books for id #{id}, but found #{rs.length}." if rs.ntuples != 1
+
         row = rs[0]
 
         book = Book.new(self)
-        book.author = load_author(row['author'])
-        book.classification_id = row['classification']
-        book.cover = load_cover(row['cover'])
-        book.description = row['description']
-        book.language = row['language']
-        book.path = row['path']
-        book.series_id = row['series']
-        book.title = row['title']
-        book.volume = row['volume']
+        book.author = load_author(row["author"])
+        book.classification_id = row["classification"]
+        book.cover = load_cover(row["cover"])
+        book.description = row["description"]
+        book.language = row["language"]
+        book.path = row["path"]
+        book.series_id = row["series"]
+        book.title = row["title"]
+        book.volume = row["volume"]
       end
-    rescue Exception => e
-      puts sql + ": " + id
+    rescue PG::Error => e
+      puts "#{sql}: #{id}"
       puts e.message
-      puts $@
+      puts $ERROR_POSITION
     end
 
-    return book
+    book
   end
 
   def store_book(book)
-    sql = "INSERT INTO Books (id, arrived, author, classification, cover, description, language, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);"
+    sql = <<~SQL
+      INSERT INTO
+        Books (id, arrived, author, classification, cover, description, language, path, series, title, volume)
+      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
+    SQL
 
-    book_id = next_id('book_id')
+    book_id = next_id("book_id")
 
     author_id = store_author(book.author)
     efs_id = store_cover(book)&.first
 
-    args = [book_id, book.arrived, author_id, book.classification_id, efs_id, book.description, book.language, book.path, book.series_id, book.title, book.volume]
+    args = [
+      book_id,
+      book.arrived,
+      author_id,
+      book.classification_id,
+      efs_id,
+      book.description,
+      book.language,
+      book.path,
+      book.series_id,
+      book.title,
+      book.volume,
+    ]
 
     begin
       rs = @conn.exec_params(sql, args)
-    rescue Exception => e
-      puts sql + ": " + args.inspect()
+    rescue PG::Error => e
+      puts "#{sql}: #{args.inspect}"
       puts e.message
-      puts $@
+      puts $ERROR_POSITION
     ensure
-      rs.clear if rs
+      rs&.clear
     end
 
-    return book_id
+    book_id
   end
 
   def find_classification(author_grouping, title_grouping)
     sql = "SELECT id FROM Classifications WHERE author_grouping = $1 AND title_grouping = $2;"
     @conn.exec_params(sql, [author_grouping, title_grouping]) do |rs|
-      if rs.ntuples > 0
-        return rs[0]['id']
-      end
+      return rs[0]["id"] if rs.ntuples.positive?
     end
-    return nil
+    nil
   end
 
   def load_classification(id)
     sql  = "SELECT ddc, lcc, author_grouping, author_sort, title_grouping, title "
     sql += " FROM Classifications WHERE id=$1"
     @conn.exec_params(sql, [id]) do |rs|
-      if rs.ntuples > 0
-        row = rs[0]
-        ddc = row['ddc']
-        lcc = row['lcc']
-        author_grouping = row['author_grouping']
-        author = row['author_sort']
-        title_grouping = row['title_grouping']
-        title = row['title']
-
-        result = Classification.new(ddc, lcc, author_grouping, author, title_grouping, title)
-        result.id = id
-        return result
-      end
+      next unless rs.ntuples.positive?
+
+      row = rs[0]
+      ddc = row["ddc"]
+      lcc = row["lcc"]
+      author_grouping = row["author_grouping"]
+      author = row["author_sort"]
+      title_grouping = row["title_grouping"]
+      title = row["title"]
+
+      result = Classification.new(ddc, lcc, author_grouping, author, title_grouping, title)
+      result.id = id
+      return result
     end
 
-    return nil
+    nil
   end
 
   def load_cover(id)
-    if nil == id
-      return nil
-    end
+    return if id.nil?
 
-    mime_type = 'application/octet-stream'
+    mime_type = "application/octet-stream"
 
     sql = "SELECT mimeType FROM Efs WHERE id=$1"
     @conn.exec_params(sql, [id]) do |rs|
-      if rs.ntuples != 1
-        raise "Expected one row but got " + rs.ntuples + ": " + sql + ": " + id
-      end
-      mime_type = rs[0]['mimeType']
+      raise "Expected one row but got #{rs.ntuples}: #{sql}: #{id}" if rs.ntuples != 1
+
+      mime_type = rs[0]["mimeType"]
     end
 
     (efspath, efsname) = construct_efs_path(id)
 
-    fullpath = @basePath + '/efs/' + efspath + '/' + efsname
+    fullpath = "#{@base_path}/efs/#{efspath}/#{efsname}"
 
-    return Cover.new(nil, fullpath, mime_type)
+    Cover.new(nil, fullpath, mime_type)
   end
 
   def store_cover(book)
     efs_id = nil
-    cover = book.cover()
+    cover = book.cover
 
-    if nil == cover
-      return nil
-    end
+    return if cover.nil?
 
     @conn.exec("SELECT nextval('efs_id')") do |rs|
-      efs_id = rs[0]['nextval']
+      efs_id = rs[0]["nextval"]
     end
 
-    if nil == efs_id
-      return nil
-    end
+    return if efs_id.nil?
 
     (efspath, efsname) = construct_efs_path(efs_id)
 
-    efspath = @basePath + '/efs/' + efspath
+    efspath = "#{@base_path}/efs/#{efspath}"
 
     FileUtils.mkdir_p(efspath)
 
@@ -424,85 +423,80 @@ EOS
     sql = "INSERT INTO efs VALUES ($1, $2)"
     begin
       rs = @conn.exec_params(sql, [efs_id, mimetype])
-    rescue Exception => e
-      puts sql + ": " + efs_id + ", " + mimetype
+    rescue PG::Error => e
+      puts "#{sql}: #{efs_id}, #{mimetype}"
       puts e.message
-      puts $@
+      puts $ERROR_POSITION
     ensure
-      rs.clear if rs
+      rs&.clear
     end
 
-    return efs_id, mimetype
+    [efs_id, mimetype]
   end
 
   def exec_id_query(sql, args)
     ids = []
     @conn.exec_params(sql, args) do |rs|
       rs.each do |row|
-        ids.push(row['id'])
+        ids.push(row["id"])
       end
     end
-    return ids
+    ids
   end
 
   def exec_update(sql, args)
-    begin
-      rs = @conn.exec_params(sql, args)
-    rescue Exception => e
-      puts sql + ": " + args.inspect()
-      puts e.message
-      puts $@
-    ensure
-      rs.clear if rs
-    end
+    rs = @conn.exec_params(sql, args)
+  rescue PG::Error => e
+    puts "#{sql}: #{args.inspect}"
+    puts e.message
+    puts $ERROR_POSITION
+  ensure
+    rs&.clear
   end
 
   def next_id(seq_name)
     id = nil
-    @conn.exec("SELECT nextval('" + seq_name + "');") do |rs|
-      id = rs[0]['nextval']
+    @conn.exec("SELECT nextval('#{seq_name}');") do |rs|
+      id = rs[0]["nextval"]
     end
-    return id
+    id
   end
 
   def get_series(grouping, code)
-    if nil == code
-      return nil
-    end
+    return if code.nil?
 
     sql = "SELECT id FROM Series WHERE grouping=$1 AND code=$2;"
     args = [grouping, code]
-    @conn.exec_params(sql, args).each do |row|
-      return row['id']
-    end
+    result = @conn.exec_params(sql, args)
+    return result.first["id"] if result.first
 
     # TODO:  Create a new series object here?
-    puts 'WARNING:  series("' + grouping + '", "' + code + '") not found.'
-    return nil
+    puts "WARNING:  series(#{grouping.inspect}, #{code.inspect}) not found."
+    nil
   end
 
   def load_series(id)
     sql = "SELECT age,genre,grouping,code,descr FROM Series WHERE id=$1;"
     args = [id]
     @conn.exec_params(sql, args) do |rs|
-      if rs.ntuples > 0
-        row = rs[0]
-        series = Series.new(id)
-        series.age = row['age']
-        series.genre = row['genre']
-        series.grouping = row['grouping']
-        series.code = row['code']
-        series.descr = row['descr']
-        return series
-      end
+      next unless rs.ntuples.positive?
+
+      row = rs[0]
+      series = Series.new(id)
+      series.age = row["age"]
+      series.genre = row["genre"]
+      series.grouping = row["grouping"]
+      series.code = row["code"]
+      series.descr = row["descr"]
+      return series
     end
-    return nil
+    nil
   end
 
   def populate_classifications_table
     puts "Populating the Classifications table..."
     first = true
-    CSV.foreach(@basePath + '/csv/class.csv') do |row|
+    CSV.foreach("#{@base_path}/csv/class.csv") do |row|
       if first
         # skip the header row
         first = false
@@ -510,7 +504,7 @@ EOS
 
         # First, add a row to the Classifications table
 
-        id = next_id('classification_id')
+        id = next_id("classification_id")
         ddc = row[0]
         lcc = row[1]
         author_grouping = row[2]
@@ -518,22 +512,23 @@ EOS
         title_grouping = row[4]
         title = row[5]
 
-        sqlInsert = "INSERT INTO Classifications (id, ddc, lcc, author_grouping, author_sort, title_grouping, title) VALUES ($1, $2, $3, $4, $5, $6, $7);"
+        sql_insert = <<~SQL
+          INSERT INTO Classifications (id, ddc, lcc, author_grouping, author_sort, title_grouping, title)
+          VALUES ($1, $2, $3, $4, $5, $6, $7);
+        SQL
         args = [id, ddc, lcc, author_grouping, author_sort, title_grouping, title]
-        exec_update(sqlInsert, args)
+        exec_update(sql_insert, args)
 
         # Second, link up with the appropriate FAST table entries
 
         fast = []
         input = row[6]
-        if input.length > 0
-          fast = input.split(';')
-        end
+        fast = input.split(";") unless input.empty?
 
         fast.each do |fast_id|
-          sqlInsert = "INSERT INTO FAST_Classifications (fast, classification) VALUES ($1, $2);"
+          sql_insert = "INSERT INTO FAST_Classifications (fast, classification) VALUES ($1, $2);"
           args = [fast_id, id]
-          exec_update(sqlInsert, args)
+          exec_update(sql_insert, args)
         end
       end
     end
@@ -542,14 +537,14 @@ EOS
   def populate_fast_table
     puts "Populating the FAST table..."
     first = true
-    CSV.foreach(@basePath + '/csv/fast.csv') do |row|
+    CSV.foreach("#{@base_path}/csv/fast.csv") do |row|
       if first
         first = false  # skip the header row
       else
         id = row[0]
         descr = row[1]
-        sqlInsert = "INSERT INTO FAST (id, descr) VALUES ($1, $2);"
-        exec_update(sqlInsert, [id, descr])
+        sql_insert = "INSERT INTO FAST (id, descr) VALUES ($1, $2);"
+        exec_update(sql_insert, [id, descr])
       end
     end
   end
@@ -557,98 +552,93 @@ EOS
   def populate_lists_table
     puts "Populating the Lists table..."
 
-    CSV.foreach(@basePath + "/csv/lists.csv", headers: true) do |row|
-      author_ids = find_all_authors(row['author'])
+    CSV.foreach("#{@base_path}/csv/lists.csv", headers: true) do |row|
+      author_ids = find_all_authors(row["author"])
       if author_ids.empty?
-        specification = [row['age'], row['category'], row['code'], row['year'], row['author'], row['title']]
-          .map { |x| x.inspect }
-          .join(', ')
+        specification =
+          [row["age"], row["category"], row["code"], row["year"], row["author"], row["title"]]
+          .map(&:inspect)
+          .join(", ")
 
         puts "WARNING: For list entry (#{specification}), no such author was found."
 
         next
       end
 
-      sqlInsert = %Q(
+      sql_insert = <<~SQL
         INSERT INTO Lists (id, age, category, code, year, author, title)
         VALUES ($1, $2, $3, $4, $5, $6, $7);
-      )
+      SQL
       author_ids.each do |author_id|
-        list_id = next_id('list_id')
-        args = [list_id, row['age'], row['category'], row['code'], row['year'], author_id, row['title']]
-        exec_update(sqlInsert, args)
+        list_id = next_id("list_id")
+        args = [list_id, row["age"], row["category"], row["code"], row["year"], author_id, row["title"]]
+        exec_update(sql_insert, args)
 
-        update_lists_books_table(list_id, author_id, row['title'])
+        update_lists_books_table(list_id, author_id, row["title"])
       end
     end
   end
 
   # Scan for books that match this Lists entry, and add any matches to the Lists_Books associative table
   def update_lists_books_table(list_id, author_id, title)
-    title_pattern = Book.grouping_for_title(title).gsub('_', '%')
-    sqlSelect = "SELECT id FROM Books WHERE author = $1 AND title LIKE $2;"
+    title_pattern = Book.grouping_for_title(title).gsub("_", "%")
+    sql_select = "SELECT id FROM Books WHERE author = $1 AND title LIKE $2;"
     args = [author_id, title_pattern]
 
-    @conn.exec_params(sqlSelect, args) do |rs|
+    @conn.exec_params(sql_select, args) do |rs|
       rs.each do |row|
-        sqlInsert = "INSERT INTO Lists_Books (list, book) VALUES ($1, $2)"
-        args = [list_id, row['id']]
-        exec_update(sqlInsert, args)
+        sql_insert = "INSERT INTO Lists_Books (list, book) VALUES ($1, $2)"
+        args = [list_id, row["id"]]
+        exec_update(sql_insert, args)
       end
     end
   end
 
   def populate_series_table
     puts "Populating the Series table..."
-    CSV.foreach(@basePath + '/csv/series.csv') do |row|
-      id = next_id('series_id')
-      sqlInsert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
+    CSV.foreach("#{@base_path}/csv/series.csv") do |row|
+      id = next_id("series_id")
+      sql_insert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
       args = [id] + row
-      exec_update(sqlInsert, args)
+      exec_update(sql_insert, args)
     end
   end
 
   def query_books_by_author(pattern)
-    sql =
-<<EOS
+    sql = <<~SQL
       SELECT b.id FROM Authors a
       INNER JOIN Books b ON b.author=a.id
       LEFT OUTER JOIN Series s on s.id=b.series
       WHERE upper(a.grouping) LIKE $1
       ORDER BY a.grouping, b.series, b.volume, b.title
-EOS
-    return exec_id_query(sql, [pattern])
+    SQL
+    exec_id_query(sql, [pattern])
   end
 
   def query_books_by_ddc
-    sql =
-<<EOS
+    sql = <<~SQL
       SELECT b.id FROM Classifications c
       INNER JOIN Books b ON b.classification=c.id
       ORDER BY c.ddc
-EOS
-    return exec_id_query(sql, [])
+    SQL
+    exec_id_query(sql, [])
   end
 
   def query_books_by_series_id(id)
-    sql =
-<<EOS
-      SELECT b.id FROM Books b
+    sql = <<~SQL
       WHERE b.series = $1
       ORDER BY b.volume,b.title
-EOS
-    return exec_id_query(sql, [id])
+    SQL
+    exec_id_query(sql, [id])
   end
 
   def query_series_by_age(pattern)
-    sql =
-<<EOS
+    sql = <<~SQL
       SELECT s.id
       FROM Series s
       WHERE s.age LIKE $1
       ORDER BY s.grouping,s.descr
-EOS
-    return exec_id_query(sql, [pattern])
+    SQL
+    exec_id_query(sql, [pattern])
   end
 end
-
index 43fa0f5c5cda97e7d83e42e401ba5ebd78935ed5..eb1faf01b84be5b3ffa3192a20c1fd5d88364f57 100644 (file)
@@ -1,12 +1,13 @@
+# frozen_string_literal: true
+
+require "pg"
+
 # tconn.rb
 #
-# Timed Connection:  
+# Timed Connection:
 # Wrapper around a PG Connection that provides a report on where time was spent executing SQL
 #
-
-require 'pg'
-
-class TimedConn 
+class TimedConn
   def initialize(wrapped_conn)
     @conn = wrapped_conn
     @stmts = {}
@@ -14,58 +15,52 @@ class TimedConn
   end
 
   def close
-    @conn.close()
-    puts "Connection closing.  Total SQL time:  " + @total_time.to_s + " secs"
-    @stmts.each do |sql, info| 
+    @conn.close
+    puts "Connection closing.  Total SQL time: #{@total_time} secs"
+    @stmts.each do |sql, info|
       elapsed = info[2]
       calls = info[1]
-      puts elapsed.to_s + " secs: " + calls.to_s + " times:  " + sql
+      puts "#{elapsed} secs: #{calls} times: #{sql}"
     end
   end
 
   def exec(*args, &block)
     before = Time.now
-    #puts args.inspect
     result = @conn.exec(*args)
-    #puts result.inspect
     after = Time.now
     elapsed = (after - before)
     remember(args[0], elapsed)
     @total_time += elapsed
-    if block_given?
-      yield(result)
-    else
-      return result
-    end
+
+    return result unless block_given?
+
+    yield(result)
   end
 
   def exec_params(*args, &block)
     before = Time.now
-    #puts args.inspect
     result = @conn.exec_params(*args)
-    #puts result.inspect
     after = Time.now
     elapsed = (after - before)
     remember(args[0], elapsed)
     @total_time += elapsed
-    if block_given?
-      yield(result)
-    else
-      return result
-    end
+
+    return result unless block_given?
+
+    yield(result)
   end
 
   def remember(sql, elapsed)
-    if @stmts.has_key?(sql)
-      stmt = @stmts[sql]
-    else
-      stmt = [sql, 0, 0]
-    end
+    stmt =
+      if @stmts.key?(sql)
+        @stmts[sql]
+      else
+        [sql, 0, 0]
+      end
+
+    stmt[1] += 1 # Number of times this statement has been invoked
+    stmt[2] += elapsed # total elapsed time spent on this statement
 
-    stmt[1] += 1  # Number of times this statement has been invoked
-    stmt[2] += elapsed   # total elapsed time spent on this statement
-    
     @stmts[sql] = stmt
   end
 end
-
index a2c088f050ba0a48bab0dbd42751f1151f6ecf58..4b2c37e77dedf1ce8db5e26d2d15706b112a0727 100644 (file)
@@ -1,27 +1,28 @@
+# frozen_string_literal: true
+
+require_relative "book"
+require_relative "book_loader"
+require_relative "store"
+
 # Walk the directory (and subdirectories), identifying books.
 #
 # Expected format:
 #   .../AuthorName/Title_of_the_Awesome_Book.ext
 #
-# Author is given as FirstLast.  For example, 
-# Robert Anson Heinlein is RobertHeinlein, and 
+# Author is given as FirstLast.  For example,
+# Robert Anson Heinlein is RobertHeinlein, and
 # JKRowling is JoanneRowling.
 #
 # Book titles have spaces replaced with underscores,
 # and punctuation [,!?'] replaced with hyphens.
 #
-# If the book forms part of a series, then an all-capitals 
-# series designator, followed by a numeric volume number, 
+# If the book forms part of a series, then an all-capitals
+# series designator, followed by a numeric volume number,
 # followed by an underscore, is prefixed to the name.
-# For example, Hardy Boys' volume 1, The Tower Treasure, 
+# For example, Hardy Boys' volume 1, The Tower Treasure,
 # is rendered as .../FranklinDixon/HB001_The_Tower_Treasure.epub
 # and Mrs. Pollifax volume 6, On the China Station, is
 # .../DorothyGilman/P06_On_the_China_Station.epub.
-
-require_relative 'book'
-require_relative 'book_loader'
-require_relative 'store'
-
 class WalkDir
   def initialize(config_file, root)
     @queue = Queue.new
@@ -40,10 +41,9 @@ class WalkDir
       end
     end
 
-    result = []
     @files = remove_duplicates(@files)
-    for file in @files.sort()
-      if Book.can_handle?(file) && (!is_duplicate?(file))
+    @files.sort.each do |file|
+      if Book.can_handle?(file) && !duplicate?(file)
         # Queue this book to be loaded and added to the DB by a BookLoader thread
         @queue << file
       end
@@ -51,68 +51,71 @@ class WalkDir
 
     @threads.count.times { @queue << BookLoader::DONE_MARKER }
 
-    @threads.each { |t| t.join }
+    @threads.each(&:join)
   end
 
-  # Duplicate versions of a text are named 
+  # Duplicate versions of a text are named
   #   xxx_suffix.ext
   # Where suffix is one of bis, ter, quater, quinquies
   # for the 2nd, 3rd, 4th or 5th variant respectively.
-  def is_duplicate?(file)
+  def duplicate?(file)
     s = file.to_s
-    suffix = ['_bis.', '_ter.', '_quater.', '_quinquies.']
+    suffix = ["_bis.", "_ter.", "_quater.", "_quinquies."]
     suffix.each do |pat|
-      if s.include?(pat)
-        return true
-      end
+      return true if s.include?(pat)
     end
-    
-    return false
+
+    false
   end
 
   def remove_duplicates(files)
     unique = {}
-    for file in files
-      if Book.can_handle?(file)
-        key = File.dirname(file) + '/' + File.basename(file, '.*')
-        if unique.has_key?(key)
-          new_ext = File.extname(file)
-          old_ext = File.extname(unique[key])
-          if ('.pdf' == old_ext) && ('.epub' == new_ext)
-            # Prefer EPUB over PDF
-            puts 'REPLACED ' + unique[key].to_s + ' with ' + file.to_s
-            unique[key] = file
-          else
-            puts 'DROPPED ' + file.to_s + " because it's superceded by " + unique[key].to_s
-          end
-        else
+    files.each do |file|
+      next unless Book.can_handle?(file)
+
+      key = "#{File.dirname(file)}/#{File.basename(file, ".*")}"
+      if unique.key?(key)
+        new_ext = File.extname(file)
+        old_ext = File.extname(unique[key])
+        if old_ext == ".pdf" && new_ext == ".epub"
+          # Prefer EPUB over PDF
+          puts "REPLACED #{unique[key]} with #{file}"
           unique[key] = file
+        else
+          puts "DROPPED #{file} because it's superceded by #{unique[key]}"
         end
+      else
+        unique[key] = file
       end
     end
 
-    return unique.values
+    unique.values
   end
 
   def walk(path)
     result = []
     children = Dir.entries(path)
-    for child in children
-      fullName = (path.chomp("/")) + "/" + child
-      if (File.directory?(fullName)) and (child != ".") and (child != "..") and (!File.symlink?(fullName))
-        sub = walk(fullName)
-        if (sub != nil) and (sub.length > 0)
-          result.concat(sub)
-        end
-      elsif (! File.directory?(fullName))
-        result.push(fullName)
+    children.each do |child|
+      full_name = "#{path.chomp("/")}/#{child}"
+      if should_descend_into_directory?(full_name)
+        sub = walk(full_name)
+        result.concat(sub) if !sub.nil? && !sub.empty?
+      elsif !File.directory?(full_name)
+        result.push(full_name)
       end
     end
-    return result
+    result
   end
 
+  private
+
   def num_threads
     # TOOD:  make this (auto?) configurable
     12
   end
+
+  def should_descend_into_directory?(path)
+    child = File.basename(path)
+    File.directory?(path) && child != "." && child != ".." && !File.symlink?(path)
+  end
 end
index bb476c797df46250eb79d65f67e4e54efc94a686..034ae04f6f70dc8731868be4adca96ae029e6f59 100644 (file)
@@ -7,13 +7,13 @@ require "store_mock"
 
 class BookTest < Minitest::Test
   def test_that_it_can_handle_epub_and_pdf_files
-    %w(epub pdf).each do |extension|
+    %w[epub pdf].each do |extension|
       assert_equal true, Book.can_handle?("sample.#{extension}")
     end
   end
 
   def test_that_it_cannot_handle_mobi_html_txt_doc_zip_rtf_nor_rar
-    %w(doc html mobi rar rtf txt zip).each do |extension|
+    %w[doc html mobi rar rtf txt zip].each do |extension|
       assert_equal false, Book.can_handle?("sample.#{extension}")
     end
   end
@@ -33,7 +33,7 @@ class BookTest < Minitest::Test
 
   def test_load_from_file
     store = StoreMock.new
-    store.expects(:get_series).returns(mock_series_LW)
+    store.expects(:get_series).returns(mock_series_lw)
     store.connect
     book = Book.new(store)
 
@@ -45,16 +45,19 @@ class BookTest < Minitest::Test
     assert_equal "Louisa May Alcott", author.reading_order
     assert_equal "Alcott, Louisa May", author.sort_order
 
-    assert_equal "This story follows the lives of the four March sisters&emdash;Meg, Jo, Beth, and Amy&emdash;and details their coming of age.", book.description
+    expected_descr = "This story follows the lives of the four March sisters&emdash;Meg, Jo, Beth, and Amy&emdash;" \
+      "and details their coming of age."
+
+    assert_equal expected_descr, book.description
     assert_equal "en", book.language
     assert_equal "Little Women: Or, Meg, Jo, Beth and Amy", book.title
-    assert_equal mock_series_LW.to_s, book.series_id.to_s
+    assert_equal mock_series_lw.to_s, book.series_id.to_s
     assert_equal 1, book.volume.to_i
   end
 
   def test_heading
     store = StoreMock.new
-    store.expects(:get_series).returns(mock_series_LW)
+    store.expects(:get_series).returns(mock_series_lw)
     store.connect
     book = Book.new(store)
 
@@ -68,7 +71,7 @@ class BookTest < Minitest::Test
 
   private
 
-  def mock_series_LW
+  def mock_series_lw
     id = 1
     series = Series.new(id)
     series.age = "ya"
index fd116434ba56f9a584459f1b1c64d7405d2c69a1..570393cb3721c0ed280e371cde1bf14dc59b579e 100644 (file)
@@ -11,10 +11,12 @@ class ClassificationTest < Minitest::Test
     author_sort = "Franklin, Benjamin"
     title_grouping = "Autobiography_of_Benjamin_Franklin"
     title = "Autobiography of Benjamin Franklin"
-    
+
     classification = Classification.new(ddc, lcc, author_grouping, author_sort, title_grouping, title)
 
-    expected = '(Classification: Dewey="973.3/092 B", LCC="E302.6.F7", author_grouping="BenjaminFranklin", author="Franklin, Benjamin", title_grouping="Autobiography_of_Benjamin_Franklin", title="Autobiography of Benjamin Franklin")'
+    expected = '(Classification: Dewey="973.3/092 B", LCC="E302.6.F7", author_grouping="BenjaminFranklin", ' \
+      'author="Franklin, Benjamin", title_grouping="Autobiography_of_Benjamin_Franklin", title="Autobiography ' \
+      'of Benjamin Franklin")'
     actual = classification.inspect
 
     assert_equal expected, actual
index c0904044b38c0bbbb9bd5e04504a3189bc03f244..ff3b4b3ab7f0073c109d95371bf749f6c5f12cf7 100644 (file)
@@ -12,13 +12,13 @@ class ConnMock
     @mock_connected = false
   end
 
-  def exec(*args, &block)
+  def exec(*args, &_block)
     @stmts << args
-    return nil
+    nil
   end
 
-  def exec_params(*args, &block)
+  def exec_params(*args, &_block)
     @stmts << args
-    return nil
+    nil
   end
 end
index 897e79a763dd0bb18eba735ce5d44db5030fdc9f..5d16cfff936b0252b421449db5c54aaf20de9352 100644 (file)
@@ -4,16 +4,18 @@ require "conn_mock"
 require "store"
 
 class StoreMock < Store
+  # rubocop:disable Lint/MissingSuper
   def initialize
     @dbhost = "host"
     @dbport = 5432
     @dbname = "dbname"
     @dbuser = "dbuser"
     @dbpass = "dbpass"
-    @basePath = "base_path"
+    @base_path = "base_path"
 
     @conn = nil
   end
+  # rubocop:enable Lint/MissingSuper
 
   def connect
     raise "Mock error:  .connect called when already connected" if @conn
index 46676b8f01a9be38f4b970233aa9256d2ba19f6f..9fd049440ba09ee29e96ece3b266168a7e24f54d 100644 (file)
@@ -6,16 +6,18 @@ require "store"
 
 class StoreTest < Minitest::Test
   def test_construct_efs_path
+    # rubocop:disable Layout/ExtraSpacing,Layout/SpaceInsideArrayLiteralBrackets
     data = [
-      [      1234, '00/00/00/12', '0000001234.dat'],
-      [         1, '00/00/00/00', '0000000001.dat'],
-      [1234567890, '12/34/56/78', '1234567890.dat'],
-      [   7778123, '00/07/77/81', '0007778123.dat'],
-      [      0x1b, '00/00/00/00', '0000000027.dat']
+      [         1234, "00/00/00/12", "0000001234.dat"],
+      [            1, "00/00/00/00", "0000000001.dat"],
+      [1_234_567_890, "12/34/56/78", "1234567890.dat"],
+      [    7_778_123, "00/07/77/81", "0007778123.dat"],
+      [         0x1b, "00/00/00/00", "0000000027.dat"],
     ]
+    # rubocop:enable Layout/ExtraSpacing,Layout/SpaceInsideArrayLiteralBrackets
 
-    IniFile.stubs(:load).returns({"database" => {}, "filesystem" => {}})
-    store = Store.new('quanlib.ini')
+    IniFile.stubs(:load).returns({ "database" => {}, "filesystem" => {} })
+    store = Store.new("quanlib.ini")
 
     data.each do |input, expected_path, expected_name|
       (actual_path, actual_name) = store.construct_efs_path(input)
index e47ef47ccc4b1fe9acf1472aff0fcf5ce2d9289b..fc068d15a86d5ca8cd0adc7e15604de982ce1d46 100644 (file)
@@ -5,34 +5,34 @@ require "test_helper"
 require "walk_dir"
 
 class WalkDirTest < Minitest::Test
-  def test_is_duplicate
-    yes = %w(
+  def test_duplicate
+    yes = %w[
       Little_Women_bis.pdf
       The_Autobiography_of_Benjamin_Franklin_ter.epub
       Romeo_and_Juliet_quater.pdf
       As_You_Like_It_quinquies.epub
-    )
+    ]
 
-    no = %w(
+    no = %w[
       Little_Women.pdf
       The_Autobiography_of_Benjamin_Franklin.epub
       Romeo_and_Juliet.pdf
       As_You_Like_It.epub
-    )
+    ]
 
     walk_dir = create_walk_dir
 
     yes.each do |filename|
-      assert_equal true, walk_dir.is_duplicate?(filename)
+      assert_equal true, walk_dir.duplicate?(filename)
     end
 
     no.each do |filename|
-      assert_equal false, walk_dir.is_duplicate?(filename)
+      assert_equal false, walk_dir.duplicate?(filename)
     end
   end
 
   def test_remove_duplicates
-    input = %w(
+    input = %w[
       00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
       00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.jpeg
       00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.pdf
@@ -41,14 +41,14 @@ class WalkDirTest < Minitest::Test
       FranklinDixon/HB001_The_Tower_Treasure.jpeg
       FranklinDixon/HB001_The_Tower_Treasure.pdf
       FranklinDixon/HB002_The_House_on_the_Cliff.epub
-    )
+    ]
 
-    expected = %w(
+    expected = %w[
       00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
       LouisaAlcott/Little_Women.epub
       FranklinDixon/HB001_The_Tower_Treasure.epub
       FranklinDixon/HB002_The_House_on_the_Cliff.epub
-    )
+    ]
 
     walk_dir = create_walk_dir
     actual = walk_dir.remove_duplicates(input)
@@ -57,60 +57,70 @@ class WalkDirTest < Minitest::Test
   end
 
   def test_walk
-    root_entries = %w(
+    root_entries = set_sample_dir_expectations
+
+    walk_dir = create_walk_dir(".", root_entries)
+    files = walk_dir.instance_variable_get(:@files)
+
+    expected = %w[
+      ./00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
+      ./LouisaAlcott/LW01_Little_Women.epub
+      ./FranklinDixon/HB001_The_Tower_Treasure.epub
+      ./FranklinDixon/HB002_The_House_on_the_Cliff.epub
+    ]
+
+    assert_equal expected, files
+  end
+
+  private
+
+  def create_walk_dir(root_path = "/does/not/exist", root_entries = [])
+    config_file = "quanlib.ini"
+    Dir.expects(:entries).with(root_path).returns(root_entries)
+
+    WalkDir.new(config_file, root_path)
+  end
+
+  def set_sample_dir_expectations
+    root_entries = %w[
       00_nonFiction
       LouisaAlcott
       FranklinDixon
-    )
+    ]
 
-    Dir.expects(:entries).with("./00_nonFiction").returns(%w(BenjaminFranklin))
-    Dir.expects(:entries).with("./00_nonFiction/BenjaminFranklin").returns(%w(
-      The_Autobiography_of_Benjamin_Franklin.epub
-    ))
+    Dir.expects(:entries).with("./00_nonFiction").returns(%w[BenjaminFranklin])
+    Dir.expects(:entries).with("./00_nonFiction/BenjaminFranklin").returns(
+      %w[
+        The_Autobiography_of_Benjamin_Franklin.epub
+      ]
+    )
     Dir.expects(:entries).with("./LouisaAlcott").returns(["LW01_Little_Women.epub"])
-    Dir.expects(:entries).with("./FranklinDixon").returns(%w(
-      HB001_The_Tower_Treasure.epub
-      HB002_The_House_on_the_Cliff.epub
-    ))
+    Dir.expects(:entries).with("./FranklinDixon").returns(
+      %w[
+        HB001_The_Tower_Treasure.epub
+        HB002_The_House_on_the_Cliff.epub
+      ]
+    )
 
-    %w(
+    %w[
       ./00_nonFiction
       ./00_nonFiction/BenjaminFranklin
       ./LouisaAlcott
       ./FranklinDixon
-    ).each do |path|
+    ].each do |path|
       File.expects(:directory?).with(path).returns(true).at_least_once
       File.expects(:symlink?).with(path).returns(false).at_least_once
     end
 
-    %w(
+    %w[
       ./00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
       ./LouisaAlcott/LW01_Little_Women.epub
       ./FranklinDixon/HB001_The_Tower_Treasure.epub
       ./FranklinDixon/HB002_The_House_on_the_Cliff.epub
-    ).each do |path|
+    ].each do |path|
       File.expects(:directory?).with(path).returns(false).at_least_once
     end
 
-    walk_dir = create_walk_dir(".", root_entries)
-    files = walk_dir.instance_variable_get(:@files)
-
-    expected = %w(
-      ./00_nonFiction/BenjaminFranklin/The_Autobiography_of_Benjamin_Franklin.epub
-      ./LouisaAlcott/LW01_Little_Women.epub
-      ./FranklinDixon/HB001_The_Tower_Treasure.epub
-      ./FranklinDixon/HB002_The_House_on_the_Cliff.epub
-    )
-
-    assert_equal expected, files
-  end
-
-  private
-
-  def create_walk_dir(root_path = "/does/not/exist", root_entries = [])
-    config_file = "quanlib.ini"
-    Dir.expects(:entries).with(root_path).returns(root_entries)
-
-    WalkDir.new(config_file, root_path)
+    root_entries
   end
 end