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
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 = []
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)
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 + '"')
@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
@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)
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")
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
#---------------
# 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"
+require 'page'
require 'store'
require 'walkdir'
outputDir = 'output'
-books = []
+book_ids = []
imageCount = 0
def handleArg(arg)
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()
+
--- /dev/null
+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
+
+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'
create_authors =
<<EOS
CREATE TABLE Authors (
- id SERIAL PRIMARY KEY,
+ id INTEGER PRIMARY KEY,
grouping VARCHAR(64),
reading VARCHAR(128),
sort VARCHAR(128)
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)
);
);
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
'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
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
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|
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
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
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)
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)
(efspath, efsname) = construct_efs_path(efs_id)
- efspath = @basepath + '/' + efspath
+ efspath = @basepath + '/efs/' + efspath
FileUtils.mkdir_p(efspath)
begin
rs = @conn.exec_params(sql, [efs_id, mimetype])
rescue Exception => e
+ puts sql + ": " + efs_id + ", " + mimetype
puts e.message
puts $@
ensure
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