Refactor page generations, and add hard-coded series naming.
authorChris Jaekl <cejaekl@yahoo.com>
Wed, 31 May 2017 14:04:56 +0000 (23:04 +0900)
committerChris Jaekl <cejaekl@yahoo.com>
Wed, 31 May 2017 14:04:56 +0000 (23:04 +0900)
book.rb
cover.rb
main.rb
page.rb [new file with mode: 0644]
store.rb
walkdir.rb

diff --git a/book.rb b/book.rb
index 03a5f259beb0005d4250bd7988edab21729ba41d..f1481f0c7ef5cb6e96b72615a2b50f30ff044b0d 100644 (file)
--- a/book.rb
+++ b/book.rb
@@ -4,16 +4,18 @@ require 'zip'
 
 require 'author'
 require 'cover'
+require 'store'
 
 class Book
   @@DC_NS_URL = 'http://purl.org/dc/elements/1.1/'
 
-  def initialize
+  def initialize(store)
     @author = nil
     @cover = nil
     @description = nil
     @path = nil
-    @series = nil
+    @series_id = nil
+    @store = store
     @title = nil
     @volume = nil
   end
@@ -42,14 +44,26 @@ class Book
     return @author
   end
 
+  def author=(value)
+    @author = value
+  end
+
   def cover
     return @cover
   end
 
+  def cover=(value)
+    @cover = value
+  end
+
   def description
     @description
   end
 
+  def description=(value)
+    @description = value
+  end
+
   def heading
     result = []
 
@@ -63,8 +77,9 @@ class Book
     end
     
     seriesInfo = []
-    if nil != @series
-      seriesInfo.push(@series.to_s)
+    series = @store.load_series(@series_id)
+    if nil != series
+      seriesInfo.push(series.to_s)
     end
     if nil != @volume
       seriesInfo.push(@volume.to_s)
@@ -81,8 +96,8 @@ class Book
     if nil != @author
       data.push('author="' + @author.inspect + '"')
     end
-    if nil != @series
-      data.push('series="' + @series + '"')
+    if nil != @series_id
+      data.push('series_id="' + @series_id.to_s() + '"')
     end
     if nil != @volume
       data.push('volume="' + @volume + '"')
@@ -103,8 +118,16 @@ class Book
     @path
   end
 
-  def series
-    @series
+  def path=(value)
+    @path = value
+  end
+
+  def series_id
+    @series_id
+  end
+  
+  def series_id=(value)
+    @series_id = value
   end
 
   def to_s
@@ -115,10 +138,18 @@ class Book
     @title
   end
 
+  def title=(value)
+    @title = value
+  end
+
   def volume
     @volume
   end
 
+  def volume=(value)
+    @volume = value
+  end
+
   protected
   def isUpper?(c)
     return /[[:upper:]]/.match(c)
@@ -175,12 +206,13 @@ class Book
   protected
   def parseFileName!(fileName)
     parts = fileName.split('/')
-    (@series, @volume, @title) = processTitle(parts[-1])
+    (series_code, @volume, @title) = processTitle(parts[-1])
     if parts.length > 1
       grouping = parts[-2]
       reading_order = massage_author(grouping)
       sort_order = nil
       @author = Author.new(grouping, reading_order, sort_order)
+      @series_id = @store.get_series(grouping, series_code)
     end
 
     if fileName.downcase.end_with?(".epub")
@@ -274,7 +306,8 @@ class Book
       content = m['content']
 
       if 'calibre:series' == name
-        @series = content
+        # TODO:  Dynamically create a new series?
+        # @series_id = content
       elsif 'calibre:series-index' == name
         @volume = content
       elsif 'cover' == name
@@ -286,11 +319,11 @@ class Book
     #---------------
     # Load the cover
 
-    @cover = loadCover(zipfile, opfPath, opfDoc, coverId)
+    @cover = load_cover(zipfile, opfPath, opfDoc, coverId)
   end
 
   protected
-  def loadCover(zipfile, opfPath, opfDoc, coverId)
+  def load_cover(zipfile, opfPath, opfDoc, coverId)
     coverFile = nil
     if nil == coverId
       coverId = "cover-image"
index b00d002dfdac8439857b08833738485510ed7376..5cc80a0f24930027dddd82f8b51ec50348d7f1c0 100644 (file)
--- a/cover.rb
+++ b/cover.rb
@@ -32,8 +32,7 @@ class Cover
     return inspect
   end
 
-  def write_image(outputDir, baseName)
-    filename = baseName + getExt()
+  def write_image(outputDir, filename)
     open(outputDir + '/' + filename, 'wb') do |fd|
       fd.write(@data)
     end
diff --git a/main.rb b/main.rb
index 596e11ff526fec0f09227bda8bcad927ab163180..d193c22360fab18aa2850cbc5a2b6d132c2aa843 100644 (file)
--- a/main.rb
+++ b/main.rb
@@ -1,9 +1,10 @@
+require 'page'
 require 'store'
 require 'walkdir'
 
 outputDir = 'output'
 
-books = []
+book_ids = []
 imageCount = 0
 
 def handleArg(arg)
@@ -28,65 +29,41 @@ for arg in ARGV
   if ! arg.start_with?("--")
     puts 'Scanning directory "' + arg + '"...'
     w = WalkDir.new(@store, arg)
-    books += (w.books)
+    book_ids += (w.books)
   end
 end
 
 puts 'Creating output...'
 
-if ! Dir.exist?(outputDir)
-  Dir.mkdir(outputDir)
-end
-
-open(outputDir + '/index.html', 'w') do |fd|
-  fd.puts '<html>'
-  fd.puts '  <head>'
-  fd.puts '    <meta charset="utf-8"/>'
-  fd.puts '    <title>Books</title>'
-  fd.puts '    <style>'
-  fd.puts 'div { '
-  fd.puts '  display: inline-block;'
-  fd.puts '  width: 400px;'
-  fd.puts '  margin: 10px;'
-  fd.puts '  border 3px solid #73ad21;'
-  fd.puts '}'
-  fd.puts 'span.popup {  }'
-  fd.puts 'span.popup:hover {text-decoration: none; background: #cfffff; z-index: 6; }'
-  fd.puts 'span.popup span {display: none; position: absolute; '
-  fd.puts '  margin: 4px 0 0 0px; padding: 3px 3px 3px 3px;'
-  fd.puts '  border-style:solid; border-color:black; border-width:1px;}'
-  fd.puts 'span.popup:hover span {display: block; margin: 20px 0 0 0px; background: #ffffaf; z-index:6;}'
-  fd.puts '    </style>'
-  fd.puts '  </head>'
-  fd.puts '  <body>'
-  
-  for book in books
-    image = nil
-    if nil != book.cover
-      imageCount += 1
-      (path, mimeType) = book.cover.write_image(outputDir, 'image' + imageCount.to_s)
-      image = '<img height="200px" src="' + path + '"/>'
-    else
-      image = '(No cover image)'
-    end
+counts = {}
 
-    fd.puts '    <div><table>'
-    fd.puts '      <tr><td><a href="' + book.path + '">' + image + '</a></td>'
+('A'..'Z').each do |letter| 
+  book_ids = @store.query_books_by_author(letter + '%')
+  puts 'Authors starting with "' + letter + '":  ' + book_ids.length.to_s() + ' books.'
+  counts[letter] = book_ids.length
 
-    heading = book.heading()
-    description = book.description()
-    if nil != description
-      fd.puts '          <td><span class="popup">' + heading + '<span><p>' + heading + '</p><p>' + description + '</p></span></span></td></tr>'
-    else
-      fd.puts '          <td>' + heading + '</td></tr>'
-    end
-      
-    fd.puts '    </table></div>'
+  page = Page.new(@store)
+  if 'A' != letter
+    page.back = ['../output_' + (letter.ord - 1).chr + '/index.html', 'Prev']
   end
-  
-  fd.puts "    </table>"
-  fd.puts "  </body>"
-  fd.puts "</html>"
+  if 'Z' != letter
+    page.forward = ['../output_' + (letter.ord + 1).chr + '/index.html', 'Next']
+  end
+  page.output_dir = 'output_' + letter
+  page.title = "Authors starting with '" + letter + "'"
+  page.up = ['../output/index.html', 'Index']
+
+  page.write_html(book_ids)
 end
 
+content = '<table><tr><th>Author</th><th>Books</th></tr>'
+('A'..'Z').each do |letter|
+  content += '  <tr><td><a href="../output_' + letter + '/index.html">Starting with ' + letter + '</a></td><td>' + counts[letter].to_s + '</td></tr>'
+end
+page = Page.new(@store)
+page.output_dir = 'output'
+page.special = content
+page.write_html( [] )
+  
 @store.disconnect()
+
diff --git a/page.rb b/page.rb
new file mode 100644 (file)
index 0000000..2e5d735
--- /dev/null
+++ b/page.rb
@@ -0,0 +1,158 @@
+require 'store'
+
+
+class Page
+  def initialize(store)
+    @back = nil
+    @forward = nil
+    @output_dir = 'output'
+    @special = nil
+    @store = store
+    @title = 'Books'
+    @up = nil
+  end
+
+  def back=(value)
+    @back = value
+  end
+
+  def forward=(value)
+    @forward = value
+  end
+
+  def navig_link(data)
+    if (nil == data)
+      return ''
+    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
+
+  def up=(value)
+    @up = value
+  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 + '"/>'
+      else
+        image = '(No cover image)'
+      end
+
+      fd.puts '    <div><table>'
+      fd.puts '      <tr><td><a href="' + book.path + '">' + image + '</a></td>'
+
+      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
+  end
+
+  def write_footer(fd)
+    fd.puts '    <p class="navigator">' + navig_link(@back) + ' ' + navig_link(@up) + ' ' + navig_link(@forward) + '</p>'
+  end
+
+  def write_header(fd)
+    fd.puts '    <h1 class="header">' + @title + '</h1>'
+
+    fd.puts '    <p class="navigator">' + navig_link(@back) + ' ' + navig_link(@up) + ' ' + navig_link(@forward) + '</p>'
+  end
+
+  def write_html(book_ids)
+    @imageCount = 0
+
+    if ! Dir.exist?(@output_dir)
+      Dir.mkdir(@output_dir)
+    end
+
+    open(@output_dir + '/index.html', 'w') do |fd|
+      fd.puts '<html>'
+      fd.puts '  <head>'
+      fd.puts '    <meta charset="utf-8"/>'
+      fd.puts '    <title>' + @title + '</title>'
+
+      write_style_sheet(fd)
+
+      fd.puts '  </head>'
+      fd.puts '  <body>'
+      
+      write_header(fd)
+
+      write_special(fd)
+      write_books(fd, book_ids)
+  
+      write_footer(fd)
+
+      fd.puts "  </body>"
+      fd.puts "</html>"
+    end
+  end
+
+  def write_special(fd)
+    if (nil != @special)
+      fd.puts(@special)
+    end
+  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
+  end
+end
+
index 645224a7c4bfa22bdcc66d08e031ad91d7c0f176..946c43c4f9056a53a1e753b2240babeb90c08e81 100644 (file)
--- a/store.rb
+++ b/store.rb
@@ -1,10 +1,11 @@
 
+require 'csv'
 require 'fileutils'
 require 'pg'
 
 class Store
   def initialize
-    @basepath = '/home/chris/prog/quanlib/efs' # TODO: FIXME: configure this in a sane way
+    @basepath = '/arc/quanlib' # TODO: FIXME: configure this in a sane way
     @conn = nil
 
     #@dburl = 'dbi:Pg:quanlib:localhost'
@@ -36,7 +37,7 @@ class Store
     create_authors = 
 <<EOS
       CREATE TABLE Authors (
-        id          SERIAL PRIMARY KEY,
+        id          INTEGER PRIMARY KEY,
         grouping    VARCHAR(64),
         reading     VARCHAR(128),
         sort        VARCHAR(128)
@@ -46,12 +47,12 @@ EOS
     create_books = 
 <<EOS
       CREATE TABLE Books (
-        id          SERIAL PRIMARY KEY,
+        id          INTEGER PRIMARY KEY,
         author      INTEGER REFERENCES Authors(id),
         cover       INTEGER,
         description TEXT,
         path        VARCHAR(256),
-        series      VARCHAR(128),
+        series      INTEGER REFERENCES Series(id),
         title       VARCHAR(196),
         volume      VARCHAR(16)
       );
@@ -65,16 +66,34 @@ EOS
       );
 EOS
 
+    create_series = 
+<<EOS
+      CREATE TABLE Series (
+        id          INTEGER PRIMARY KEY,
+        age         VARCHAR(32),
+        genre       VARCHAR(32),
+        grouping    VARCHAR(64),
+        code        VARCHAR(16),
+        descr       VARCHAR(128)
+      )
+EOS
+
     stmts = [
       create_authors,
-      create_books,
       create_efs,
-      'CREATE SEQUENCE efs_id;'
+      create_series,
+      create_books,
+      'CREATE SEQUENCE author_id;',
+      'CREATE SEQUENCE book_id;',
+      'CREATE SEQUENCE efs_id;',
+      'CREATE SEQUENCE series_id;'
     ]
 
     for stmt in stmts
       @conn.exec(stmt)
     end
+
+    populate_series_table()
   end
 
   def dropSchema
@@ -82,7 +101,11 @@ EOS
       'DROP TABLE Books;',
       'DROP TABLE Authors;',
       'DROP TABLE EFS;',
-      'DROP SEQUENCE efs_id;'
+      'DROP TABLE Series;',
+      'DROP SEQUENCE author_id;',
+      'DROP SEQUENCE book_id;',
+      'DROP SEQUENCE efs_id;',
+      'DROP SEQUENCE series_id;'
     ]
 
     for stmt in stmts do
@@ -90,6 +113,17 @@ EOS
     end
   end
 
+  def find_author(author)
+    sqlSelect = "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
+    end
+    return nil
+  end
+
   def init_db
     sql = "SELECT 1 FROM pg_tables WHERE tableowner='quanlib' AND tablename='books'"
     found = false
@@ -102,18 +136,8 @@ EOS
     end
   end
 
-  def find_author(author)
-    sqlSelect = "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
-    end
-    return nil
-  end
-
   def load_author(id)
+    #puts 'DEBUG:  load_author(' + id + ')'
     sqlSelect = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
     args = [id]
     @conn.exec_params(sqlSelect, args) do |rs|
@@ -121,15 +145,20 @@ EOS
         raise "Expected 1 row for " + id + " but got " + rs.ntuples + ":  " + sqlSelect
       end
       row = rs[0]
-      return Author.new(row['grouping'], row['reading'], row['sort'])
+      author = Author.new(row['grouping'], row['reading'], row['sort'])
+      #puts 'DEBUG:  author:  ' + author.inspect()
+      return author
     end
+    #puts 'DEBUG:  NOT FOUND'
+    return nil
   end
 
   def store_author(author)
     id = find_author(author)
     if nil == id
-      sqlInsert = "INSERT INTO Authors(grouping, reading, sort) VALUES ($1, $2, $3);"
-      args = [author.grouping, author.reading_order, author.sort_order]
+      id = next_id('author_id')
+      sqlInsert = "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
@@ -140,10 +169,11 @@ EOS
         rs.clear if rs
       end
     end
-    return find_author(author)
+    return id
   end
 
   def load_book(id)
+    #puts 'DEBUG:  load_book(' + id + ')'
     sql = "SELECT author, cover, description, path, series, title, volume FROM Books WHERE id=$1;"
     book = nil
 
@@ -155,30 +185,34 @@ EOS
         end
         row = rs[0]
 
-        book = Book.new()
+        book = Book.new(self)
         book.author = load_author(row['author'])
         book.cover = load_cover(row['cover'])
         book.description = row['description']
         book.path = row['path']
-        book.series = row['series']
+        book.series_id = row['series']
         book.title = row['title']
         book.volume = row['volume']
       end    
     rescue Exception => e
       puts sql + ": " + id
       puts e.message
+      puts $@
     end
 
+    #puts 'DEBUG:  loaded book:   ' + book.inspect()
     return book
   end
 
   def store_book(book)
-    sql = "INSERT INTO Books (author, cover, description, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7);"
+    sql = "INSERT INTO Books (id, author, cover, description, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);"
+
+    book_id = next_id('book_id')
 
     author_id = store_author(book.author)
     (efs_id, mime_type) = store_cover(book)
 
-    args = [author_id, efs_id, book.description(), book.path(), book.series(), book.title(), book.volume()]
+    args = [book_id, author_id, efs_id, book.description(), book.path(), book.series_id(), book.title(), book.volume()]
 
     begin
       rs = @conn.exec_params(sql, args)
@@ -189,14 +223,32 @@ EOS
     ensure
       rs.clear if rs
     end
+
+    return book_id
   end
 
   def load_cover(id)
+    if nil == id
+      return nil
+    end
+
+    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']
+    end
+
     (efspath, efsname) = construct_efs_path(id)
-    efspath = @basepath + '/' + efspath
-    cover = Cover.new()
-    cover.load_image(efspath + '/' + efsname)
-    return cover
+
+    File.open(@basepath + '/efs/' + efspath + '/' + efsname, 'rb') do |is|
+      return Cover.new(is, efsname, mime_type)
+    end
+
+    return nil
   end
 
   def store_cover(book)
@@ -217,7 +269,7 @@ EOS
 
     (efspath, efsname) = construct_efs_path(efs_id)
 
-    efspath = @basepath + '/' + efspath
+    efspath = @basepath + '/efs/' + efspath
 
     FileUtils.mkdir_p(efspath)
 
@@ -227,6 +279,7 @@ EOS
     begin
       rs = @conn.exec_params(sql, [efs_id, mimetype])
     rescue Exception => e
+      puts sql + ": " + efs_id + ", " + mimetype
       puts e.message
       puts $@
     ensure
@@ -235,5 +288,77 @@ EOS
     
     return efs_id, mimetype
   end
+
+  def next_id(seq_name)
+    id = nil
+    @conn.exec("SELECT nextval('" + seq_name + "');") do |rs|
+      id = rs[0]['nextval']
+    end 
+    return id
+  end
+
+  def get_series(grouping, code)
+    if nil == code
+      return nil
+    end
+
+    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
+
+    # TODO:  Create a new series object here?
+    puts 'WARNING:  series("' + grouping + '", "' + code + '") not found.'
+    return nil
+  end
+
+  def load_series(id)
+    sql = "SELECT descr FROM Series WHERE id=$1;"
+    args = [id]
+    @conn.exec_params(sql, args) do |rs|
+      if rs.ntuples > 0
+        return rs[0]['descr']
+      end
+    end
+    return nil
+  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);"
+      args = [id] + row
+      begin
+        # DEBUG: puts 'SQL> ' + sqlInsert + ':  ' + args.inspect()
+        rs = @conn.exec_params(sqlInsert, args)
+      rescue Exception => e
+        puts sqlInsert + ":  " + args.inspect()
+        puts e.message
+        puts $@
+      ensure
+        rs.clear if rs
+      end
+    end
+  end
+
+  def query_books_by_author(pattern)
+    sql = 
+<<EOS
+      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
+    book_ids = []
+    @conn.exec_params(sql, [pattern]) do |rs|
+      rs.each do |row|
+        book_ids.push(row['id'])
+      end
+    end
+    return book_ids
+  end
 end
 
index ce1954ca7a11d27417afecf739af99fdbd98c83e..1dadd2e35b95974f81ad43366698c46b93dac7b9 100644 (file)
@@ -32,10 +32,10 @@ class WalkDir
     result = []
     for file in @files.sort
       if Book.canHandle?(file)
-        book = Book.new()
+        book = Book.new(@store)
         book.loadFromFile(file)
-        @store.store_book(book)
-        result.push(book)
+        id = @store.store_book(book)
+        result.push(id)
       end
     end
     return result