Adds support for tracking series and generating pages based on them.
[quanlib.git] / store.rb
1
2 require 'csv'
3 require 'fileutils'
4 require 'pg'
5
6 require 'series'
7
8 class Store
9   def initialize
10     @basepath = '/arc/quanlib'  # TODO: FIXME: configure this in a sane way
11     @conn = nil
12
13     #@dburl = 'dbi:Pg:quanlib:localhost'
14     @dbhost = "localhost"
15     @dbport = 5432
16     @dbname = 'quanlib'
17     @dbuser = 'quanlib'
18     @dbpass = 'quanlib'
19   end
20
21   def connect
22     # @conn = PGconn.connect('localhost', 5432, '', '', 'quanlib', 'quanlib', 'quanlib')
23     @conn = PG.connect('localhost', 5432, '', '', 'quanlib', 'quanlib', 'quanlib')
24     return @conn
25   end
26
27   def disconnect
28     @conn.close()
29   end
30
31   def construct_efs_path(efs_id)
32     id_str = sprintf('%010d', efs_id)
33     path = sprintf('%s/%s/%s/%s', id_str[0,2], id_str[2,2], id_str[4,2], id_str[6,2])
34     name = id_str + '.dat'
35     return path, name
36   end
37
38   def create_schema
39     create_authors = 
40 <<EOS
41       CREATE TABLE Authors (
42         id          INTEGER PRIMARY KEY,
43         grouping    VARCHAR(64),
44         reading     VARCHAR(128),
45         sort        VARCHAR(128)
46       );
47 EOS
48
49     create_books = 
50 <<EOS
51       CREATE TABLE Books (
52         id          INTEGER PRIMARY KEY,
53         author      INTEGER REFERENCES Authors(id),
54         cover       INTEGER,
55         description TEXT,
56         path        VARCHAR(256),
57         series      INTEGER REFERENCES Series(id),
58         title       VARCHAR(196),
59         volume      VARCHAR(16)
60       );
61 EOS
62
63     create_efs = 
64 <<EOS
65       CREATE TABLE EFS (
66         id          INTEGER,
67         mimetype    VARCHAR(64)
68       );
69 EOS
70
71     create_series = 
72 <<EOS
73       CREATE TABLE Series (
74         id          INTEGER PRIMARY KEY,
75         age         VARCHAR(32),
76         genre       VARCHAR(32),
77         grouping    VARCHAR(64),
78         code        VARCHAR(16),
79         descr       VARCHAR(128)
80       )
81 EOS
82
83     stmts = [
84       create_authors,
85       create_efs,
86       create_series,
87       create_books,
88       'CREATE SEQUENCE author_id;',
89       'CREATE SEQUENCE book_id;',
90       'CREATE SEQUENCE efs_id;',
91       'CREATE SEQUENCE series_id;'
92     ]
93
94     for stmt in stmts
95       @conn.exec(stmt)
96     end
97
98     populate_series_table()
99   end
100
101   def dropSchema
102     stmts = [
103       'DROP TABLE Books;',
104       'DROP TABLE Authors;',
105       'DROP TABLE EFS;',
106       'DROP TABLE Series;',
107       'DROP SEQUENCE author_id;',
108       'DROP SEQUENCE book_id;',
109       'DROP SEQUENCE efs_id;',
110       'DROP SEQUENCE series_id;'
111     ]
112
113     for stmt in stmts do
114       @conn.exec(stmt)
115     end
116   end
117
118   def find_author(author)
119     sqlSelect = "SELECT id FROM Authors WHERE grouping=$1 AND reading=$2 AND sort=$3;"
120     args = [author.grouping, author.reading_order, author.sort_order]
121     @conn.exec_params(sqlSelect, args) do |rs|
122       if rs.ntuples > 0
123         return rs[0]['id']
124       end
125     end
126     return nil
127   end
128
129   def init_db
130     sql = "SELECT 1 FROM pg_tables WHERE tableowner='quanlib' AND tablename='books'"
131     found = false
132     @conn.exec(sql).each do |row|
133       found = true
134     end
135
136     if ! found
137       create_schema()
138     end
139   end
140
141   def load_author(id)
142     #puts 'DEBUG:  load_author(' + id + ')'
143     sqlSelect = "SELECT grouping, reading, sort FROM Authors WHERE id=$1"
144     args = [id]
145     @conn.exec_params(sqlSelect, args) do |rs|
146       if rs.ntuples != 1
147         raise "Expected 1 row for " + id + " but got " + rs.ntuples + ":  " + sqlSelect
148       end
149       row = rs[0]
150       author = Author.new(row['grouping'], row['reading'], row['sort'])
151       #puts 'DEBUG:  author:  ' + author.inspect()
152       return author
153     end
154     #puts 'DEBUG:  NOT FOUND'
155     return nil
156   end
157
158   def store_author(author)
159     id = find_author(author)
160     if nil == id
161       id = next_id('author_id')
162       sqlInsert = "INSERT INTO Authors(id, grouping, reading, sort) VALUES ($1, $2, $3, $4);"
163       args = [id, author.grouping, author.reading_order, author.sort_order]
164       begin 
165         rs = @conn.exec_params(sqlInsert, args)
166       rescue Exception => e
167         puts sqlInsert + ":  " + args.inspect()
168         puts e.message
169         puts $@
170       ensure
171         rs.clear if rs
172       end
173     end
174     return id
175   end
176
177   def load_book(id)
178     #puts 'DEBUG:  load_book(' + id + ')'
179     sql = "SELECT author, cover, description, path, series, title, volume FROM Books WHERE id=$1;"
180     book = nil
181
182     begin
183       @conn.exec_params(sql, [id]) do |rs|
184         if 1 != rs.ntuples
185           raise 'Expected one row in Books for id ' + id + ', but found ' + rs.length + '.'
186           return nil
187         end
188         row = rs[0]
189
190         book = Book.new(self)
191         book.author = load_author(row['author'])
192         book.cover = load_cover(row['cover'])
193         book.description = row['description']
194         book.path = row['path']
195         book.series_id = row['series']
196         book.title = row['title']
197         book.volume = row['volume']
198       end    
199     rescue Exception => e
200       puts sql + ": " + id
201       puts e.message
202       puts $@
203     end
204
205     #puts 'DEBUG:  loaded book:   ' + book.inspect()
206     return book
207   end
208
209   def store_book(book)
210     sql = "INSERT INTO Books (id, author, cover, description, path, series, title, volume) VALUES ($1, $2, $3, $4, $5, $6, $7, $8);"
211
212     book_id = next_id('book_id')
213
214     author_id = store_author(book.author)
215     (efs_id, mime_type) = store_cover(book)
216
217     args = [book_id, author_id, efs_id, book.description(), book.path(), book.series_id(), book.title(), book.volume()]
218
219     begin
220       rs = @conn.exec_params(sql, args)
221     rescue Exception => e
222       puts sql + ": " + args.inspect()
223       puts e.message 
224       puts $@
225     ensure
226       rs.clear if rs
227     end
228
229     return book_id
230   end
231
232   def load_cover(id)
233     if nil == id
234       return nil
235     end
236
237     mime_type = 'application/octet-stream'
238
239     sql = "SELECT mimeType FROM Efs WHERE id=$1"
240     @conn.exec_params(sql, [id]) do |rs|
241       if rs.ntuples != 1
242         raise "Expected one row but got " + rs.ntuples + ": " + sql + ": " + id
243       end
244       mime_type = rs[0]['mimeType']
245     end
246
247     (efspath, efsname) = construct_efs_path(id)
248
249     fullpath = @basepath + '/efs/' + efspath + '/' + efsname
250
251     return Cover.new(nil, fullpath, mime_type)
252
253     #File.open(fullpath, 'rb') do |is|
254     #  return Cover.new(is, fullpath, mime_type)
255     #end
256     #
257     #return nil
258   end
259
260   def store_cover(book)
261     efs_id = nil
262     cover = book.cover()
263
264     if nil == cover
265       return nil
266     end
267
268     @conn.exec("SELECT nextval('efs_id')") do |rs|
269       efs_id = rs[0]['nextval']
270     end
271
272     if nil == efs_id
273       return nil
274     end
275
276     (efspath, efsname) = construct_efs_path(efs_id)
277
278     efspath = @basepath + '/efs/' + efspath
279
280     FileUtils.mkdir_p(efspath)
281
282     (filepath, mimetype) = cover.write_image(efspath, efsname)
283
284     sql = "INSERT INTO efs VALUES ($1, $2)"
285     begin
286       rs = @conn.exec_params(sql, [efs_id, mimetype])
287     rescue Exception => e
288       puts sql + ": " + efs_id + ", " + mimetype
289       puts e.message
290       puts $@
291     ensure
292       rs.clear if rs
293     end
294     
295     return efs_id, mimetype
296   end
297
298   def next_id(seq_name)
299     id = nil
300     @conn.exec("SELECT nextval('" + seq_name + "');") do |rs|
301       id = rs[0]['nextval']
302     end 
303     return id
304   end
305
306   def get_series(grouping, code)
307     if nil == code
308       return nil
309     end
310
311     sql = "SELECT id FROM Series WHERE grouping=$1 AND code=$2;"
312     args = [grouping, code]
313     @conn.exec_params(sql, args).each do |row|
314       return row['id']
315     end
316
317     # TODO:  Create a new series object here?
318     puts 'WARNING:  series("' + grouping + '", "' + code + '") not found.'
319     return nil
320   end
321
322   def load_series(id)
323     sql = "SELECT age,genre,grouping,code,descr FROM Series WHERE id=$1;"
324     args = [id]
325     @conn.exec_params(sql, args) do |rs|
326       if rs.ntuples > 0
327         row = rs[0]
328         series = Series.new(id)
329         series.age = row['age']
330         series.genre = row['genre']
331         series.grouping = row['grouping']
332         series.code = row['code']
333         series.descr = row['descr']
334         return series
335       end
336     end
337     return nil
338   end
339
340   def populate_series_table
341     puts "Populating the Series table..."
342     CSV.foreach(@basepath + '/csv/series.csv') do |row|
343       id = next_id('series_id')
344       sqlInsert = "INSERT INTO Series (id, age, genre, grouping, code, descr) VALUES ($1, $2, $3, $4, $5, $6);"
345       args = [id] + row
346       begin
347         # DEBUG: puts 'SQL> ' + sqlInsert + ':  ' + args.inspect()
348         rs = @conn.exec_params(sqlInsert, args)
349       rescue Exception => e
350         puts sqlInsert + ":  " + args.inspect()
351         puts e.message
352         puts $@
353       ensure
354         rs.clear if rs
355       end
356     end
357   end
358
359   def query_books_by_author(pattern)
360     sql = 
361 <<EOS
362       SELECT b.id FROM Authors a 
363       INNER JOIN Books b ON b.author=a.id 
364       LEFT OUTER JOIN Series s on s.id=b.series
365       WHERE upper(a.grouping) LIKE $1 
366       ORDER BY a.grouping, b.series, b.volume, b.title
367 EOS
368     book_ids = []
369     @conn.exec_params(sql, [pattern]) do |rs|
370       rs.each do |row|
371         book_ids.push(row['id'])
372       end
373     end
374     return book_ids
375   end
376
377   def query_books_by_series_id(id)
378     sql = 
379 <<EOS
380       SELECT b.id FROM Books b
381       WHERE b.series = $1
382       ORDER BY b.volume,b.title
383 EOS
384     book_ids = []
385     @conn.exec_params(sql, [id]) do |rs|
386       rs.each do |row|
387         book_ids.push(row['id'])
388       end
389     end
390     return book_ids
391   end
392
393   def query_series_by_age(pattern)
394     sql = 
395 <<EOS
396       SELECT s.id FROM Series s
397       WHERE s.age LIKE $1
398       ORDER BY s.descr
399 EOS
400     series_ids = []
401     @conn.exec_params(sql, [pattern]) do |rs|
402       rs.each do |row|
403         series_ids.push(row['id'])
404       end
405     end
406     return series_ids
407   end
408 end
409