kindai 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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