Create a basic html output, to validate that we're loading data correctly.
[quanlib.git] / book.rb
1
2 require 'nokogiri'
3 require 'zip'
4
5 require 'author'
6 require 'cover'
7
8 class Book
9   def initialize(fileName)
10     @author = nil
11     @cover = nil
12     @path = fileName
13     @series = nil
14     @title = nil
15     @volume = nil
16
17     parseFileName!(fileName)
18   end
19
20   def self.canHandle?(fileName)
21     if nil == fileName
22       return false
23     end
24
25     lowerName = fileName.downcase()
26
27     if lowerName.end_with?(".epub")
28       return true
29     end
30
31     return false
32   end
33
34   def cover
35     return @cover
36   end
37
38   def describe
39     result = []
40
41     if nil != @title
42       result.push('<b>' + @title + '</b>')
43     else
44       result.push('<i>(Unknown title)</i>')
45     end
46     if nil != @author
47       result.push(@author.to_s())
48     end
49     
50     seriesInfo = []
51     if nil != @series
52       seriesInfo.push(@series.to_s)
53     end
54     if nil != @volume
55       seriesInfo.push(@volume.to_s)
56     end
57     if seriesInfo.length > 0
58       result.push(seriesInfo.join(' '))
59     end
60
61     return result.join('<br/>')
62   end
63
64   def inspect
65     data = []
66     if nil != @author
67       data.push('author="' + @author.to_s + '"')
68     end
69     if nil != @series
70       data.push('series="' + @series + '"')
71     end
72     if nil != @volume
73       data.push('volume="' + @volume + '"')
74     end
75     if nil != @title
76       data.push('title="' + @title + '"')
77     end
78     if nil != @cover
79       data.push(@cover.inspect())
80     end
81     if nil != @path
82       data.push('path="' + @path + '"')
83     end
84     return '(Book:' + data.join(',') + ')'
85   end
86
87   def to_s
88     return inspect()
89   end
90
91   protected
92   def isUpper?(c)
93     return /[[:upper:]]/.match(c)
94   end
95
96   protected
97   def massageAuthor(input)
98     if nil == input
99       return nil
100     end
101
102     result = ""
103     input.each_char do |c|
104       if isUpper?(c) and (result.length > 0)
105         result += " "
106       end
107       result += c
108     end
109     
110     return result
111   end
112
113   # Returns (series, volumeNo, titleText)
114   protected
115   def processTitle(input)
116     if nil == input
117       return nil
118     end
119
120     arr = input.split('_')
121
122     series = nil
123     vol = nil
124
125     first = arr[0]
126     matchData = (arr[0]).match(/^([A-Z]+)([0-9]+)$/)
127     if nil != matchData
128       capt = matchData.captures
129       series = capt[0]
130       vol = capt[1]
131       arr.shift
132     end
133
134     pos = arr[-1].rindex('.')
135     if nil != pos
136       arr[-1] = arr[-1].slice(0, pos)
137     end
138
139     title = arr.join(' ')
140
141     return series, vol, title
142   end
143
144   protected
145   def parseFileName!(fileName)
146     parts = fileName.split('/')
147     (@series, @volume, @title) = processTitle(parts[-1])
148     if parts.length > 1
149       @author = massageAuthor(parts[-2])
150     end
151
152     if fileName.downcase.end_with?(".epub")
153       scanEpub!(fileName)
154     end
155   end
156
157   protected 
158   def scanEpub!(fileName)
159     puts 'Scanning "' + fileName.to_s + '"...'
160     Zip::File.open(fileName) do |zipfile|
161       contXml = zipfile.read('META-INF/container.xml')
162       contDoc = Nokogiri::XML(contXml)
163       opfPath = contDoc.css("container rootfiles rootfile")[0]['full-path']
164
165       scanOpf!(zipfile, opfPath)
166     end
167   end
168
169   protected
170   def scanOpf!(zipfile, opfPath)
171     coverId = nil
172
173     opfXml = zipfile.read(opfPath)
174     opfDoc = Nokogiri::XML(opfXml)
175
176     #-------
177     # Author
178
179     creator = opfDoc.css('dc|creator', 'dc' => 'http://purl.org/dc/elements/1.1/')
180     if nil != creator
181       roleNode = creator.attr('role')
182       if nil != roleNode
183         role = roleNode.value
184         if 'aut' == role
185           name = creator.children[0].content
186           parts = name.split(' ')
187           if parts.length > 1
188             surname = parts[-1]
189             givenNames = parts[0..-2].join(' ')
190             @author = Author.new(surname, givenNames)
191           else
192             @author = Author.new(name, '')
193           end
194         end
195       end
196     end
197
198     #---------------------------------------
199     # Other metadata:  series, volume, cover
200
201     metas = opfDoc.css('package metadata meta')
202     for m in metas
203       name = m['name']
204       content = m['content']
205
206       if 'calibre:series' == name
207         @series = content
208       elsif 'calibre:series-index' == name
209         @volume = content
210       elsif 'cover' == name
211         coverId = content
212       end
213     end
214
215     #---------------
216     # Load the cover
217
218     coverFile = nil
219     if nil != coverId
220       items = opfDoc.css('package manifest item')
221       for i in items
222         href = i['href']
223         id = i['id']
224         mimeType = i['media-type']
225
226         if coverId == id
227           entry = zipfile.find_entry(href)
228           entry.get_input_stream() do |is|
229             @cover = Cover.new(is, href, mimeType)
230           end
231         end
232       end
233     end
234   end
235 end
236