Parse .epub files to extract metadata and cover image.
[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 inspect
35     data = []
36     if nil != @author
37       data.push('author="' + @author.to_s + '"')
38     end
39     if nil != @series
40       data.push('series="' + @series + '"')
41     end
42     if nil != @volume
43       data.push('volume="' + @volume + '"')
44     end
45     if nil != @title
46       data.push('title="' + @title + '"')
47     end
48     if nil != @cover
49       data.push(@cover.inspect())
50     end
51     if nil != @path
52       data.push('path="' + @path + '"')
53     end
54     return '(Book:' + data.join(',') + ')'
55   end
56
57   def to_s
58     return inspect()
59   end
60
61   protected
62   def isUpper?(c)
63     return /[[:upper:]]/.match(c)
64   end
65
66   protected
67   def massageAuthor(input)
68     if nil == input
69       return nil
70     end
71
72     result = ""
73     input.each_char do |c|
74       if isUpper?(c) and (result.length > 0)
75         result += " "
76       end
77       result += c
78     end
79     
80     return result
81   end
82
83   # Returns (series, volumeNo, titleText)
84   protected
85   def processTitle(input)
86     if nil == input
87       return nil
88     end
89
90     arr = input.split('_')
91
92     series = nil
93     vol = nil
94
95     first = arr[0]
96     matchData = (arr[0]).match(/^([A-Z]+)([0-9]+)$/)
97     if nil != matchData
98       capt = matchData.captures
99       series = capt[0]
100       vol = capt[1]
101       arr.shift
102     end
103
104     pos = arr[-1].rindex('.')
105     if nil != pos
106       arr[-1] = arr[-1].slice(0, pos)
107     end
108
109     title = arr.join(' ')
110
111     return series, vol, title
112   end
113
114   protected
115   def parseFileName!(fileName)
116     parts = fileName.split('/')
117     (@series, @volume, @title) = processTitle(parts[-1])
118     if parts.length > 1
119       @author = massageAuthor(parts[-2])
120     end
121
122     if fileName.downcase.end_with?(".epub")
123       scanEpub!(fileName)
124     end
125   end
126
127   protected 
128   def scanEpub!(fileName)
129     Zip::File.open(fileName) do |zipfile|
130       contXml = zipfile.read('META-INF/container.xml')
131       contDoc = Nokogiri::XML(contXml)
132       opfPath = contDoc.css("container rootfiles rootfile")[0]['full-path']
133
134       scanOpf!(zipfile, opfPath)
135     end
136   end
137
138   protected
139   def scanOpf!(zipfile, opfPath)
140     coverId = nil
141
142     opfXml = zipfile.read(opfPath)
143     opfDoc = Nokogiri::XML(opfXml)
144
145     #-------
146     # Author
147
148     creator = opfDoc.css('dc|creator', 'dc' => 'http://purl.org/dc/elements/1.1/')
149     if nil != creator
150       roleNode = creator.attr('role')
151       if nil != roleNode
152         role = roleNode.value
153         if 'aut' == role
154           name = creator.children[0].content
155           parts = name.split(' ')
156           if parts.length > 1
157             surname = parts[-1]
158             givenNames = parts[0..-2].join(' ')
159             @author = Author.new(surname, givenNames)
160           else
161             @author = Author.new(name, '')
162           end
163         end
164       end
165     end
166
167     #---------------------------------------
168     # Other metadata:  series, volume, cover
169
170     metas = opfDoc.css('package metadata meta')
171     for m in metas
172       name = m['name']
173       content = m['content']
174
175       if 'calibre:series' == name
176         @series = content
177       elsif 'calibre:series-index' == name
178         @volume = content
179       elsif 'cover' == name
180         coverId = content
181       end
182     end
183
184     #---------------
185     # Load the cover
186
187     coverFile = nil
188     if nil != coverId
189       items = opfDoc.css('package manifest item')
190       for i in items
191         href = i['href']
192         id = i['id']
193         mimeType = i['media-type']
194
195         if coverId == id
196           entry = zipfile.find_entry(href)
197           entry.get_input_stream() do |is|
198             @cover = Cover.new(is, href, mimeType)
199           end
200         end
201       end
202     end
203   end
204 end
205