kindai 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Kindai
3
+ class Publisher
4
+ attr_accessor :root_path
5
+
6
+ def self.new_from_path(root_path)
7
+ me = self.new
8
+ me.root_path = root_path
9
+ me
10
+ end
11
+
12
+ def name(n)
13
+ config(:name, n)
14
+ self
15
+ end
16
+
17
+ def resize(width, height)
18
+ config(:resize, {:width => width, :height => height})
19
+ self
20
+ end
21
+
22
+ def trim(geometry = true)
23
+ config(:trim, geometry) unless config(:trim)
24
+ self
25
+ end
26
+
27
+ def zip
28
+ config(:zip, true)
29
+ self
30
+ end
31
+
32
+ def divide
33
+ config(:divide, true)
34
+ self
35
+ end
36
+
37
+ def empty(glob)
38
+ FileUtils.rm_r(Dir.glob(File.join(self.root_path, glob)))
39
+ end
40
+
41
+ def publish
42
+ Kindai::Util.logger.info("publish #{root_path}, #{config(:name)}")
43
+ raise "no name" unless config(:name)
44
+ if seems_finished?
45
+ Kindai::Util.logger.info("already published")
46
+ return
47
+ end
48
+ create_directory
49
+
50
+ path = original_path
51
+
52
+ path = trim!(path) if trim?
53
+ path = divide!(path) if divide?
54
+ path = resize!(path) if resize?
55
+ path = zip!(path) if zip?
56
+ end
57
+
58
+ def publish_auto
59
+ self.clone.trim.resize(1280, 960).trim.zip.name('iphone').publish
60
+ self.clone.trim.resize(600, 800).divide.zip.name('kindle').publish
61
+ end
62
+
63
+ # ------------------------------------
64
+ protected
65
+
66
+ def config(k, v = nil)
67
+ @config ||= {}
68
+ return @config[k] unless v
69
+ @config[k] = v
70
+ @config
71
+ end
72
+
73
+ def trim?
74
+ config(:trim)
75
+ end
76
+
77
+ def resize?
78
+ config(:resize)
79
+ end
80
+
81
+ def zip?
82
+ config(:zip)
83
+ end
84
+
85
+ def divide?
86
+ config(:divide)
87
+ end
88
+
89
+ # ---------- aciton --------------
90
+
91
+ def trim!(source_path)
92
+ return trim_path if files(source_path).length == files(trim_path).length
93
+ info = config(:trim).kind_of?(Hash) ? config(:trim) : Kindai::Util.trim_info_by_files(original_files)
94
+ files(source_path).each{|file|
95
+ dst = File.join(trim_path, File.basename(file))
96
+ Kindai::Util.trim_file_to(file, dst, info)
97
+ GC.start
98
+ }
99
+ return trim_path
100
+ end
101
+
102
+ def resize!(source_path)
103
+ files(source_path).each{|file|
104
+ dst = File.join(output_path, File.basename(file))
105
+ Kindai::Util.resize_file_to(file, dst, config(:resize))
106
+ GC.start
107
+ }
108
+ return output_path
109
+ end
110
+
111
+ def divide!(source_path)
112
+ files(source_path).each{|file|
113
+ Kindai::Util.divide_43(file, output_path)
114
+ GC.start
115
+ }
116
+ return output_path
117
+ end
118
+
119
+ def zip!(source_path)
120
+ Kindai::Util.generate_zip(source_path)
121
+ FileUtils.rm_r(self.output_path)
122
+ return source_path
123
+ end
124
+
125
+ # ---------util------------
126
+
127
+ def create_directory
128
+ Dir.mkdir(trim_path) unless File.directory?(trim_path)
129
+ Dir.mkdir(output_path) unless File.directory?(output_path)
130
+ end
131
+
132
+ def trim_path
133
+ File.join(root_path, 'trim')
134
+ end
135
+
136
+ def original_path
137
+ File.join(root_path, 'original')
138
+ end
139
+
140
+ def output_path
141
+ File.join(root_path, File.basename(root_path) + '_' + config(:name))
142
+ end
143
+
144
+ def original_files
145
+ files(original_path)
146
+ end
147
+
148
+ def files(path)
149
+ Dir.glob(File.join(path, '*jpg'))
150
+ end
151
+
152
+ def seems_finished?
153
+ zip? ? File.exists?(output_path + '.zip') : File.directory?(output_path)
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,52 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Kindai
3
+ class Searcher
4
+ include Enumerable
5
+ attr_accessor :keyword
6
+ def self.search keyword
7
+ Kindai::Util.logger.debug "keyword: #{keyword}"
8
+ me = self.new
9
+ me.keyword = keyword
10
+ me
11
+ end
12
+
13
+ def length
14
+ @length ||= total_of(@keyword)
15
+ end
16
+
17
+ def each
18
+ (0..(1/0.0)).each{ |page|
19
+ Kindai::Util.logger.debug "page #{page}"
20
+ uris = result_for(@keyword, page)
21
+ return if uris.empty?
22
+ uris.each{ |uri|
23
+ yield Kindai::Book.new_from_permalink(uri)
24
+ }
25
+ }
26
+ end
27
+
28
+ protected
29
+ def total_of(keyword)
30
+ page = Nokogiri(Kindai::Util.fetch_uri(uri_for(keyword)))
31
+ total = page.at('.//opensearch:totalResults', {"opensearch"=>"http://a9.com/-/spec/opensearchrss/1.0/"} ).content.to_i
32
+
33
+ Kindai::Util.logger.debug "total: #{total}"
34
+ total
35
+ end
36
+
37
+ def result_for keyword, page = 0
38
+ page = Nokogiri Kindai::Util.fetch_uri(uri_for(keyword, page))
39
+ page.search('item').map{ |item|
40
+ item.at('link').content
41
+ }
42
+ end
43
+
44
+ def uri_for keyword, page = 0
45
+ count = 10
46
+ params = { :any => keyword, :dpid => 'kindai', :idx => page * count + 1, :cnt => count}
47
+ root = URI.parse("http://api.porta.ndl.go.jp/servicedp/opensearch")
48
+ path = '?' + Kindai::Util.expand_params(params)
49
+ root + path
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Kindai
3
+ class Spread
4
+ attr_accessor :book
5
+ attr_accessor :spread_number
6
+
7
+ def self.new_from_book_and_spread_number(book, spread_number)
8
+ raise TypeError, "#{book} is not Kindai::Book" unless book.is_a? Kindai::Book
9
+ me = new
10
+ me.book = book
11
+ me.spread_number = spread_number
12
+ me
13
+ end
14
+
15
+ def uri
16
+ book.base_uri.gsub(/koma=(\d+)/) { "koma=#{spread_number}" }
17
+ end
18
+
19
+ def image_uri
20
+ image = page.at("img#imMain")
21
+ raise "not exists" unless image
22
+ image['src']
23
+ end
24
+
25
+
26
+ def has_local_file?
27
+ end
28
+
29
+ def local_file_path
30
+ end
31
+
32
+ # protected
33
+ # XXX: book use this
34
+ def page
35
+ @page ||= Nokogiri Kindai::Util.fetch_uri self.uri
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Kindai
3
+ class SpreadDownloader
4
+ attr_accessor :spread
5
+ attr_accessor :retry_count
6
+ attr_accessor :book_path
7
+
8
+ def self.new_from_spread(spread)
9
+ raise TypeError, "#{spread} is not Kindai::Spread" unless spread.is_a? Kindai::Spread
10
+ me = self.new
11
+ me.spread = spread
12
+ me.retry_count = 30
13
+ me.book_path = Pathname.new(ENV["HOME"]).to_s
14
+ me
15
+ end
16
+
17
+ def download
18
+ return false if self.has_file?
19
+ self.create_directory
20
+ self.download_spread
21
+ return true
22
+ end
23
+
24
+ def create_directory
25
+ path = File.join self.book_path, "original"
26
+ Dir.mkdir(path) unless File.directory?(path)
27
+ end
28
+
29
+ def spread_path
30
+ path = File.join self.book_path, "original", "%03d.jpg" % self.spread.spread_number
31
+ File.expand_path path
32
+ end
33
+
34
+ def delete
35
+ return File.delete(self.spread_path) && true rescue false
36
+ end
37
+
38
+ def has_file?
39
+ File.size? self.spread_path
40
+ end
41
+
42
+ protected
43
+
44
+ def download_spread
45
+ failed_count = 0
46
+
47
+ begin
48
+ Kindai::Util.logger.info "downloading " + [self.spread.book.author, self.spread.book.title, "spread #{self.spread.spread_number} / #{self.spread.book.total_spread}"].join(' - ')
49
+ Kindai::Util.rich_download(spread.image_uri, self.spread_path)
50
+ rescue Interrupt => err
51
+ Kindai::Util.logger.error "#{err.class}: #{err.message}"
52
+ exit 1
53
+ rescue StandardError, TimeoutError => err
54
+ Kindai::Util.logger.warn "failed (#{failed_count+1}/#{self.retry_count}) #{err.class}: #{err.message}"
55
+ raise err if failed_count == self.retry_count
56
+
57
+ Kindai::Util.logger.info "sleep and retry"
58
+ failed_count += 1
59
+ sleep 10
60
+ retry
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,239 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'open3'
3
+ require 'tempfile'
4
+ require 'digest/sha1'
5
+ require 'RMagick'
6
+ require 'zipruby'
7
+
8
+ module Kindai::Util
9
+ def self.logger
10
+ return @logger if @logger
11
+ @logger ||= Logger.new(STDOUT)
12
+ @logger.level = Logger::INFO
13
+ @logger
14
+ end
15
+
16
+ def self.debug_mode!
17
+ self.logger.level = Logger::DEBUG
18
+ Kindai::Util.logger.info "debug mode enabled"
19
+ end
20
+
21
+ def self.download(uri, file)
22
+ total = nil
23
+ uri = URI.parse(uri) unless uri.kind_of? URI
24
+
25
+ got = fetch_uri(uri)
26
+ open(file, 'w') {|local|
27
+ local.write(got)
28
+ }
29
+ rescue Exception, TimeoutError => error
30
+ if File.exists?(file)
31
+ logger.debug "delete cache"
32
+ File.delete(file)
33
+ end
34
+ raise error
35
+ end
36
+
37
+ def self.rich_download(uri, file)
38
+ total = nil
39
+ uri = URI.parse(uri) unless uri.kind_of? URI
40
+
41
+ got = fetch_uri(uri, true)
42
+ open(file, 'w') {|local|
43
+ local.write(got)
44
+ }
45
+ rescue Exception, TimeoutError => error
46
+ if File.exists?(file)
47
+ logger.debug "delete cache"
48
+ File.delete(file)
49
+ end
50
+ raise error
51
+ end
52
+
53
+ # input: {:a => 'a', :b => 'bbb'}
54
+ # output: 'a=a&b=bbb
55
+ def self.expand_params(params)
56
+ params.each_pair.map{ |k, v| [URI.escape(k.to_s), URI.escape(v.to_s)].join('=')}.join('&')
57
+ end
58
+
59
+ def self.append_suffix(path, suffix)
60
+ path.gsub(/\.(\w+)$/, "-#{suffix}.\\1")
61
+ end
62
+
63
+ def self.execute_and_log(command)
64
+ logger.debug command
65
+ system command or raise "#{commands} failed"
66
+ end
67
+
68
+ def self.generate_zip(directory)
69
+ Kindai::Util.logger.info "zip #{directory}"
70
+ directory = File.expand_path(directory)
71
+ raise "#{directory} is not directory." unless File.directory? directory
72
+
73
+ filename = File.expand_path(File.join(directory, '..', "#{File.basename(directory)}.zip"))
74
+ files = Dir.glob(File.join(directory, '*jpg'))
75
+ begin
76
+ Zip::Archive.open(filename, Zip::CREATE) {|arc|
77
+ files.each{|f| arc.add_file(f) }
78
+ }
79
+ rescue => error
80
+ File.delete(filename) if File.exists?(filename)
81
+ logger.warn "#{error.class}: #{error.message}"
82
+ logger.warn "zipruby died. trying zip command"
83
+ generate_zip_system_command(directory)
84
+ end
85
+ end
86
+
87
+ def self.generate_zip_system_command(directory)
88
+ Kindai::Util.logger.info "zip(system) #{directory}"
89
+ from = Dir.pwd
90
+ Dir.chdir(directory)
91
+ execute_and_log "zip -q -r '../#{File.basename(directory)}.zip' *jpg"
92
+ Dir.chdir(from)
93
+ end
94
+
95
+ def self.fetch_uri(uri, rich = false)
96
+ uri = URI.parse(uri) unless uri.kind_of? URI
97
+ self.logger.debug "fetch_uri #{uri}"
98
+
99
+ return uri.read unless rich
100
+
101
+ total = nil
102
+ from = Time.now
103
+ got = uri.read(
104
+ :content_length_proc => proc{|_total|
105
+ total = _total
106
+ },
107
+ :progress_proc => proc{|now|
108
+ if Time.now - from > 0.2
109
+ from = Time.now
110
+ print "%3d%% #{now}/#{total}\r" % (now/total.to_f*100)
111
+ $stdout.flush
112
+ end
113
+ })
114
+ raise "received size unmatch(#{got.bytesize}, #{total})" if got.bytesize != total
115
+ return got
116
+ end
117
+
118
+ def self.trim_info_by_files(files)
119
+ Kindai::Util.logger.info "get trim info"
120
+ positions = {:x => [], :y => [], :width => [], :height => []}
121
+ files.each{|file|
122
+ pos = trim_info(file)
123
+
124
+ [:x, :y, :width, :height].each{|key|
125
+ positions[key] << pos[key]
126
+ }
127
+
128
+ GC.start
129
+ }
130
+
131
+ good_pos = {}
132
+ [:x, :y, :width, :height].each{|key|
133
+ good_pos[key] = average(positions[key])
134
+ }
135
+ Kindai::Util.logger.info "trim position #{good_pos}"
136
+ good_pos
137
+ end
138
+
139
+ # XXX: GC
140
+ def self.trim_info(img_path, erase_center_line = true)
141
+ debug = false
142
+ img = Magick::ImageList.new(img_path)
143
+
144
+ thumb = img.resize_to_fit(400, 400)
145
+
146
+ thumb.write('a1.jpg') if debug
147
+ # thumb = thumb.normalize
148
+ thumb = thumb.level(Magick::QuantumRange*0.4, Magick::QuantumRange*0.7)
149
+ thumb.write('a2.jpg') if debug
150
+
151
+ d = Magick::Draw.new
152
+ d.fill = 'white'
153
+ cut_x = 0.07
154
+ cut_y = 0.04
155
+ d.rectangle(thumb.columns * 0.4, 0, thumb.columns * 0.6, thumb.rows) if erase_center_line # center line
156
+ d.rectangle(0, 0, thumb.columns * cut_x, thumb.rows) # h
157
+ d.rectangle(0, thumb.rows * (1 - cut_y), thumb.columns, thumb.rows) # j
158
+ d.rectangle(0, 0, thumb.columns, thumb.rows * cut_y) # k
159
+ d.rectangle(thumb.columns * (1 - cut_x), 0, thumb.columns, thumb.rows) # l
160
+ d.draw(thumb)
161
+ thumb.write('a.jpg') if debug
162
+
163
+ # thumb = thumb.threshold(Magick::QuantumRange*0.8)
164
+ # thumb.write('b.jpg') if debug
165
+
166
+ thumb.fuzz = 50
167
+ thumb.trim!
168
+ thumb.write('c.jpg') if debug
169
+
170
+ scale = thumb.base_columns / thumb.page.width.to_f
171
+
172
+ info = {
173
+ :x => thumb.page.x * scale,
174
+ :y => thumb.page.y * scale,
175
+ :width => thumb.columns * scale,
176
+ :height => thumb.rows * scale
177
+ }
178
+
179
+ # erased by cente line?
180
+ if (thumb.page.x / thumb.page.width.to_f - 0.6).abs < 0.05 && erase_center_line
181
+ Kindai::Util.logger.info "retry trim(erased by center line?)"
182
+ new_info = trim_info(img_path, false)
183
+ Kindai::Util.logger.debug "x: #{info[:x]} -> #{new_info[:x]}"
184
+ Kindai::Util.logger.debug "width: #{info[:width]} -> #{new_info[:width]}"
185
+ info[:x] = new_info[:x]
186
+ info[:width] = new_info[:width]
187
+ end
188
+
189
+ img = nil
190
+ thumb = nil
191
+
192
+ return info
193
+ end
194
+
195
+ def self.trim_file_to(src_path, dst_path, info = nil)
196
+ info = trim_info(src_path) unless info
197
+ Kindai::Util.logger.info "trim #{src_path}"
198
+
199
+ img = Magick::ImageList.new(src_path)
200
+ img.crop! info[:x], info[:y], info[:width], info[:height]
201
+ img.write dst_path
202
+
203
+ img = nil
204
+ end
205
+
206
+
207
+ def self.resize_file_to(src_path, dst_path, info)
208
+ Kindai::Util.logger.info "resize #{src_path}"
209
+ img = Magick::ImageList.new(src_path)
210
+ img.resize_to_fit(info[:width], info[:height]).write dst_path
211
+
212
+ img = nil
213
+ end
214
+
215
+ def self.average(array)
216
+ array.inject{|a, b| a + b} / array.length.to_f
217
+ end
218
+
219
+ def self.divide_43(src_path, output_directory)
220
+ raise "#{src_path} not exist" unless File.exists? src_path
221
+ Kindai::Util.logger.info "divide #{src_path}"
222
+
223
+ output_base = File.join(output_directory, File.basename(src_path))
224
+
225
+ img = Magick::ImageList.new(src_path)
226
+
227
+ right = img.crop(img.columns - img.rows * 0.75, 0, img.columns * 0.75, img.rows)
228
+ right.write(append_suffix(output_base, '0'))
229
+ right = nil
230
+
231
+ left = img.crop(0, 0, img.rows * 0.75, img.rows)
232
+ left.write(append_suffix(output_base, '1'))
233
+ left = nil
234
+
235
+ File.delete(src_path) if File.basename(src_path) == output_directory
236
+ end
237
+
238
+
239
+ end
data/lib/kindai.rb ADDED
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'open-uri'
4
+ require 'nokogiri'
5
+ require 'nkf'
6
+ require 'logger'
7
+ require 'open-uri'
8
+ require 'cgi'
9
+ require 'pathname'
10
+ require 'fileutils'
11
+
12
+ module Kindai
13
+ VERSION = File.read(File.join(File.dirname(__FILE__), '../VERSION')).strip
14
+
15
+ require 'kindai/cli'
16
+ require 'kindai/util'
17
+ require 'kindai/book'
18
+ require 'kindai/spread'
19
+ require 'kindai/book_downloader'
20
+ require 'kindai/spread_downloader'
21
+ require 'kindai/searcher'
22
+ require 'kindai/interface'
23
+ require 'kindai/publisher'
24
+ end
data/publish.rb ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ self_file =
5
+ if File.symlink?(__FILE__)
6
+ require 'pathname'
7
+ Pathname.new(__FILE__).realpath
8
+ else
9
+ __FILE__
10
+ end
11
+ $:.unshift(File.dirname(self_file) + "/lib")
12
+
13
+ require 'kindai'
14
+
15
+ warn 'WARNING: This script is deprecated. Use bin/kindai.rb'
16
+
17
+ Kindai::CLI.execute(STDOUT, ['publish'].concat(ARGV))
@@ -0,0 +1,46 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe Kindai::BookDownloader do
5
+ before do
6
+ @book = Kindai::Book.new_from_permalink('http://kindai.ndl.go.jp/info:ndljp/pid/922693')
7
+ @downloader = Kindai::BookDownloader.new_from_book(@book)
8
+ end
9
+
10
+ it 'has book' do
11
+ @downloader.book.should == @book
12
+ end
13
+
14
+ it 'has retry_count' do
15
+ @downloader.retry_count.should == 30
16
+ @downloader.retry_count = 50
17
+ @downloader.retry_count.should == 50
18
+ end
19
+
20
+ it 'has base path' do
21
+ @downloader.base_path = "/path/to/library"
22
+ @downloader.book_path.should == "/path/to/library/正義熱血社 - 正義の叫"
23
+
24
+ @downloader.base_path = "/path/to/library/"
25
+ @downloader.book_path.should == "/path/to/library/正義熱血社 - 正義の叫"
26
+ end
27
+
28
+ it 'can download book' do
29
+ base_path = File.join(ENV['TMPDIR'] || ENV['TMP'] || ENV['TEMP'] || '/tmp', rand.to_s)
30
+ Dir.mkdir(base_path)
31
+ @downloader.base_path = base_path
32
+
33
+ @downloader.has_file?.should be_false
34
+ @downloader.download.should be_true
35
+ @downloader.has_file?.should be_true
36
+ @downloader.download.should be_false
37
+
38
+ @downloader.delete.should be_true
39
+ @downloader.has_file?.should be_false
40
+ @downloader.delete.should be_false
41
+
42
+ Dir.delete(base_path)
43
+ end
44
+
45
+
46
+ end
data/spec/book_spec.rb ADDED
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe Kindai::Book do
5
+ before do
6
+ @book = Kindai::Book.new_from_permalink('http://kindai.ndl.go.jp/info:ndljp/pid/922693')
7
+ end
8
+
9
+ it 'has title' do
10
+ @book.title.should == '正義の叫'
11
+ end
12
+
13
+ it 'has total spread' do
14
+ @book.total_spread.should == 20
15
+ end
16
+
17
+ it 'has author' do
18
+ @book.author.should == '正義熱血社'
19
+ end
20
+
21
+ it 'has spreads' do
22
+ @book.spreads.should have_exactly(@book.total_spread).spreads
23
+ end
24
+
25
+ it 'has base_uri' do
26
+ @book.base_uri.should == "http://kindai.da.ndl.go.jp/scrpt/ndlimageviewer-rgc.aspx?pid=info%3Andljp%2Fpid%2F922693&jp=42016454&vol=10010&koma=1&vs=10000,10000,0,0,0,0,0,0"
27
+ end
28
+
29
+ end
30
+
31
+ describe Kindai::Book, 'with series' do
32
+ before do
33
+ @book = Kindai::Book.new_from_permalink('http://kindai.da.ndl.go.jp/info:ndljp/pid/890078')
34
+ end
35
+
36
+ it 'has title' do
37
+ @book.title.should == '講談日露戦争記[第3冊]第3編'
38
+ end
39
+ end