manga-tools 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8c2d70d4d5c63f13939ffc01ca319d1c6f711b14ea77a9861ce8c361a914ad1
4
- data.tar.gz: a6f869a956a9e5c4bb705c4bdcdd73636ee17bbe41c8635a95bd7841ca95338d
3
+ metadata.gz: 00e79a7d0c8e37ab036decdaba2d9cd556d97256a523a59d6af0c1c63c07d2b5
4
+ data.tar.gz: 46a97ec12c433b9ce51ee222d82df047edc3b8f311304149dd8d3e9a946b27d3
5
5
  SHA512:
6
- metadata.gz: 256cfe3263281cb6ac3272405f028ab12d8d963da6dd41993e59f6bbb5ac2fb439384c496fe231aa70f8a8bb044b01d4023b9a45bf5eb03c6901d2e6c3a51b8d
7
- data.tar.gz: e5dadf87fcf6e8aaea67355802b1b3b932ebf8f51fb270a3a1ab74e6d3cf8d2c454801530107bb50f9ee32d0e4a89caacf1c97ecc472eca889d1194b3d4b7a4c
6
+ metadata.gz: 3635bfe7dd5cd5c4093fb8281d8137e34c4a3c551c4e377e7332f6712a933190f0515d1159e56bfb78bc6ea917226b6561f1d053afdc5d1b805e4a5e417e9454
7
+ data.tar.gz: 6170c78319d8e7a74ed93860dde010b87b23c9281385bb65bfeec4af5eb2c9c51f02ceb771797126c9365f9b3295d8d5b4db5b812e16dc3af3e022f33cba45bd
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+ *.gem
@@ -35,4 +35,4 @@ Metrics/PerceivedComplexity:
35
35
  Max: 7
36
36
 
37
37
  Style/ClassAndModuleChildren:
38
- EnforcedStyle: compact
38
+ EnforcedStyle: nested
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- manga-tools (0.1.0)
4
+ manga-tools (0.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'manga/tools/cache'
4
- require 'manga/tools/cli'
5
- require 'manga/tools/http'
6
- require 'manga/tools/version'
3
+ require_relative 'tools/cache'
4
+ require_relative 'tools/cli'
5
+ require_relative 'tools/http'
6
+ require_relative 'tools/version'
7
7
 
8
- module Manga::Tools
9
- class Error < StandardError; end
8
+ module Manga
9
+ module Tools
10
+ class Error < StandardError; end
11
+ end
10
12
  end
@@ -3,109 +3,111 @@
3
3
  require 'digest/md5'
4
4
  require 'fileutils'
5
5
 
6
- module Manga::Tools
7
- # Cache class
8
- class Cache
9
- class << self
10
- # Initialize cache
11
- def init
12
- FileUtils.mkdir_p(cache_current_year_dir)
6
+ module Manga
7
+ module Tools
8
+ # Cache class
9
+ class Cache
10
+ class << self
11
+ # Initialize cache
12
+ def init
13
+ FileUtils.mkdir_p(cache_current_year_dir)
14
+ end
15
+
16
+ def root_dir
17
+ @root_dir ||= "#{Dir.home}/.manga-tools"
18
+ end
19
+
20
+ def cache_root_dir
21
+ @cache_root_dir ||= "#{root_dir}/cache"
22
+ end
23
+
24
+ def cache_current_year_dir
25
+ @cache_current_year_dir ||= "#{cache_root_dir}/#{Time.now.year}"
26
+ end
27
+
28
+ # @return [Integer] default expires in seconds (1day)
29
+ def default_expires_in
30
+ @default_expires_in ||= 1 * 24 * 60 * 60
31
+ end
32
+
33
+ # @param key [String] A key to generate a key for the cache
34
+ # @return [String] string of cached data
35
+ def fetch(key:)
36
+ raise ArgumentError, 'mast pass a block' unless block_given?
37
+
38
+ cache = new(key)
39
+ # cache.clear
40
+ return cache.load_data if cache.available?
41
+
42
+ result = yield(cache)
43
+ cache.save_meta_data
44
+ cache.save_data(result)
45
+ result
46
+ end
13
47
  end
14
48
 
15
- def root_dir
16
- @root_dir ||= "#{Dir.home}/.manga-tools"
49
+ # @return [Integer|nil] A cache deadline in seconds.
50
+ attr_accessor :expires_in
51
+ # @return [String] A key to generate a key for the cache.
52
+ attr_reader :key
53
+ # @return [String] A key for the cache.
54
+ attr_reader :cache_key
55
+ # @return [String] A path to the cache metadata file.
56
+ attr_reader :meta_file_path
57
+ # @return [String] A path to the cached data file.
58
+ attr_reader :data_file_path
59
+
60
+ # @param key [String] A key to generate a key for the cache.
61
+ def initialize(key)
62
+ raise ArgumentError, 'mast pass the key param' unless key
63
+
64
+ @expires_in = self.class.default_expires_in
65
+ @key = key
66
+ @cache_key = Digest::MD5.hexdigest(key)
67
+ @meta_file_path = "#{self.class.cache_current_year_dir}/#{@cache_key}"
68
+ @data_file_path = "#{@meta_file_path}.data"
17
69
  end
18
70
 
19
- def cache_root_dir
20
- @cache_root_dir ||= "#{root_dir}/cache"
21
- end
71
+ # @return [Boolean] True if a cache is available, otherwise return false.
72
+ def available?
73
+ t = File.mtime(meta_file_path)
74
+ saved_expires_in = File.read(meta_file_path).strip.to_i
22
75
 
23
- def cache_current_year_dir
24
- @cache_current_year_dir ||= "#{cache_root_dir}/#{Time.now.year}"
76
+ Time.now <= (t + saved_expires_in)
77
+ rescue StandardError
78
+ false
25
79
  end
26
80
 
27
- # @return [Integer] default expires in seconds (1day)
28
- def default_expires_in
29
- @default_expires_in ||= 1 * 24 * 60 * 60
81
+ # @return [String] Reads and returns the cache data.
82
+ def load_data
83
+ File.read(data_file_path)
30
84
  end
31
85
 
32
- # @param key [String] A key to generate a key for the cache
33
- # @return [String] string of cached data
34
- def fetch(key:)
35
- raise ArgumentError, 'mast pass a block' unless block_given?
86
+ # Save the metadata for the cache.
87
+ def save_meta_data
88
+ @expires_in = self.class.default_expires_in unless expires_in
36
89
 
37
- cache = new(key)
38
- # cache.clear
39
- return cache.load_data if cache.available?
90
+ File.open(meta_file_path, 'w') do |f|
91
+ f.write(expires_in.to_s)
92
+ end
40
93
 
41
- result = yield(cache)
42
- cache.save_meta_data
43
- cache.save_data(result)
44
- result
94
+ self
45
95
  end
46
- end
47
-
48
- # @return [Integer|nil] A cache deadline in seconds.
49
- attr_accessor :expires_in
50
- # @return [String] A key to generate a key for the cache.
51
- attr_reader :key
52
- # @return [String] A key for the cache.
53
- attr_reader :cache_key
54
- # @return [String] A path to the cache metadata file.
55
- attr_reader :meta_file_path
56
- # @return [String] A path to the cached data file.
57
- attr_reader :data_file_path
58
-
59
- # @param key [String] A key to generate a key for the cache.
60
- def initialize(key)
61
- raise ArgumentError, 'mast pass the key param' unless key
62
-
63
- @expires_in = self.class.default_expires_in
64
- @key = key
65
- @cache_key = Digest::MD5.hexdigest(key)
66
- @meta_file_path = "#{self.class.cache_current_year_dir}/#{@cache_key}"
67
- @data_file_path = "#{@meta_file_path}.data"
68
- end
69
-
70
- # @return [Boolean] True if a cache is available, otherwise return false.
71
- def available?
72
- t = File.mtime(meta_file_path)
73
- saved_expires_in = File.read(meta_file_path).strip.to_i
74
-
75
- Time.now <= (t + saved_expires_in)
76
- rescue StandardError
77
- false
78
- end
79
96
 
80
- # @return [String] Reads and returns the cache data.
81
- def load_data
82
- File.read(data_file_path)
83
- end
84
-
85
- # Save the metadata for the cache.
86
- def save_meta_data
87
- @expires_in = self.class.default_expires_in unless expires_in
97
+ # @param str [String] Save the specified cache data
98
+ def save_data(str)
99
+ File.open(data_file_path, 'w') do |f|
100
+ f.write(str.force_encoding('utf-8'))
101
+ end
88
102
 
89
- File.open(meta_file_path, 'w') do |f|
90
- f.write(expires_in.to_s)
103
+ self
91
104
  end
92
105
 
93
- self
94
- end
95
-
96
- # @param str [String] Save the specified cache data
97
- def save_data(str)
98
- File.open(data_file_path, 'w') do |f|
99
- f.write(str.force_encoding('utf-8'))
106
+ # for debug
107
+ def clear
108
+ File.delete(meta_file_path) if File.exist?(meta_file_path)
109
+ File.delete(data_file_path) if File.exist?(data_file_path)
100
110
  end
101
-
102
- self
103
- end
104
-
105
- # for debug
106
- def clear
107
- File.delete(meta_file_path) if File.exist?(meta_file_path)
108
- File.delete(data_file_path) if File.exist?(data_file_path)
109
111
  end
110
112
  end
111
113
  end
@@ -5,156 +5,158 @@ require 'nokogiri'
5
5
  require 'stringio'
6
6
  require 'thor'
7
7
 
8
- module Manga::Tools
9
- # CLI class
10
- class CLI < Thor
11
- def self.exit_on_failure?
12
- true
13
- end
14
-
15
- # desc "pub {name}", "publication date of {name}"
16
- # def pub(name)
17
- # doc = Nokogiri::HTML(
18
- # URI.open('https://tsutaya.tsite.jp/feature/book/release/comic/index', 'User-Agent' => UserAgent)
19
- # )
20
- #
21
- # File.open('.index.txt', 'w') {|f| f.write doc }
22
- #
23
- # results = {}
24
- #
25
- # current_date = ''
26
- # state = :title
27
- # current_data = {}
28
- # doc.css('h3, div.comic_list div.c_cols-1of3 span').each do |element|
29
- # case element.name
30
- # when 'h3'
31
- # results[element.content] = []
32
- # current_date = element.content
33
- # when 'span'
34
- # current_data[state] = element.content
35
- # case state
36
- # when :title
37
- # state = :author
38
- # when :author
39
- # state = :label
40
- # when :label
41
- # state = :title
42
- # results[current_date] << current_data
43
- # current_data = {}
44
- # end
45
- # end
46
- # end
47
- #
48
- # puts JSON.pretty_generate(results)
49
- # end
50
-
51
- desc 'search WORD', 'Search for a given WORD'
52
- def search(word)
53
- Manga::Tools::Cache.init
54
-
55
- t = Time.now
56
- url = "https://calendar.gameiroiro.com/manga.php?year=#{t.year}&month=#{t.month}"
57
- data = Manga::Tools::Cache.fetch(key: url) do |cache|
58
- res = Manga::Tools::Http.get(url)
59
- cache.expires_in = Manga::Tools::Http.seconds_to_cache(res)
60
- results = generate_internal_data(t, res.body)
61
- results.to_json
8
+ module Manga
9
+ module Tools
10
+ # CLI class
11
+ class CLI < Thor
12
+ def self.exit_on_failure?
13
+ true
62
14
  end
63
15
 
64
- hash = JSON.parse(data)
16
+ # desc "pub {name}", "publication date of {name}"
17
+ # def pub(name)
18
+ # doc = Nokogiri::HTML(
19
+ # URI.open('https://tsutaya.tsite.jp/feature/book/release/comic/index', 'User-Agent' => UserAgent)
20
+ # )
21
+ #
22
+ # File.open('.index.txt', 'w') {|f| f.write doc }
23
+ #
24
+ # results = {}
25
+ #
26
+ # current_date = ''
27
+ # state = :title
28
+ # current_data = {}
29
+ # doc.css('h3, div.comic_list div.c_cols-1of3 span').each do |element|
30
+ # case element.name
31
+ # when 'h3'
32
+ # results[element.content] = []
33
+ # current_date = element.content
34
+ # when 'span'
35
+ # current_data[state] = element.content
36
+ # case state
37
+ # when :title
38
+ # state = :author
39
+ # when :author
40
+ # state = :label
41
+ # when :label
42
+ # state = :title
43
+ # results[current_date] << current_data
44
+ # current_data = {}
45
+ # end
46
+ # end
47
+ # end
48
+ #
49
+ # puts JSON.pretty_generate(results)
50
+ # end
51
+
52
+ desc 'search WORD', 'Search for a given WORD'
53
+ def search(word)
54
+ Manga::Tools::Cache.init
55
+
56
+ t = Time.now
57
+ url = "https://calendar.gameiroiro.com/manga.php?year=#{t.year}&month=#{t.month}"
58
+ data = Manga::Tools::Cache.fetch(key: url) do |cache|
59
+ res = Manga::Tools::Http.get(url)
60
+ cache.expires_in = Manga::Tools::Http.seconds_to_cache(res)
61
+ results = generate_internal_data(t, res.body)
62
+ results.to_json
63
+ end
65
64
 
66
- puts "Searching '#{word}' ..."
67
- hash.each do |date, items|
68
- next if items.empty?
65
+ hash = JSON.parse(data)
69
66
 
70
- items.each do |item|
71
- puts "Found: #{date}, #{item['title']}" if item['title'].index(word)
67
+ puts "Searching '#{word}' ..."
68
+ hash.each do |date, items|
69
+ next if items.empty?
70
+
71
+ items.each do |item|
72
+ puts "Found: #{date}, #{item['title']}" if item['title'].index(word)
73
+ end
72
74
  end
75
+ puts 'Finished.'
73
76
  end
74
- puts 'Finished.'
75
- end
76
77
 
77
- private
78
-
79
- # @param time [Time] a target time
80
- # @param data [String] a string of HTTP response body.
81
- def generate_internal_data(time, data)
82
- doc = Nokogiri::HTML(StringIO.open(data))
83
-
84
- results = {}
85
- current_date = nil
86
- state = :genre
87
- data = {}
88
- doc.css(targets.join(', ')).each do |element|
89
- value = rm_spaces(element.content)
90
- case element.name
91
- when 'td'
92
- current_date = format('%<ym>s/%<day>02d', ym: time.strftime('%Y/%m'), day: value.to_i)
93
- results[current_date] = []
94
- when 'p', 'a'
95
- case state
96
- when :genre
97
- guard_genre(element)
98
- data[state] = value
99
- state = :title
100
- when :title
101
- guard_title(element)
102
- data[state] = value
103
- data[:link] = element['href']
104
- state = :company
105
- when :company
106
- guard_company(element)
107
- data[state] = value
108
- state = :authors
109
- when :authors
110
- guard_authors(element)
111
- data[state] = value
78
+ private
79
+
80
+ # @param time [Time] a target time
81
+ # @param data [String] a string of HTTP response body.
82
+ def generate_internal_data(time, data)
83
+ doc = Nokogiri::HTML(StringIO.open(data))
84
+
85
+ results = {}
86
+ current_date = nil
87
+ state = :genre
88
+ data = {}
89
+ doc.css(targets.join(', ')).each do |element|
90
+ value = rm_spaces(element.content)
91
+ case element.name
92
+ when 'td'
93
+ current_date = format('%<ym>s/%<day>02d', ym: time.strftime('%Y/%m'), day: value.to_i)
94
+ results[current_date] = []
95
+ when 'p', 'a'
96
+ case state
97
+ when :genre
98
+ guard_genre(element)
99
+ data[state] = value
100
+ state = :title
101
+ when :title
102
+ guard_title(element)
103
+ data[state] = value
104
+ data[:link] = element['href']
105
+ state = :company
106
+ when :company
107
+ guard_company(element)
108
+ data[state] = value
109
+ state = :authors
110
+ when :authors
111
+ guard_authors(element)
112
+ data[state] = value
113
+ state = :genre
114
+ results[current_date] << data
115
+ data = {}
116
+ end
117
+ end
118
+ rescue StandardError
119
+ # when authors, authors may not be present.
120
+ if state == :authors
121
+ data[state] = ''
112
122
  state = :genre
113
123
  results[current_date] << data
114
124
  data = {}
115
125
  end
126
+ retry
116
127
  end
117
- rescue StandardError
118
- # when authors, authors may not be present.
119
- if state == :authors
120
- data[state] = ''
121
- state = :genre
122
- results[current_date] << data
123
- data = {}
124
- end
125
- retry
128
+
129
+ results
126
130
  end
127
131
 
128
- results
129
- end
132
+ def targets
133
+ @targets ||= [
134
+ 'td.day-td',
135
+ 'div.product-description-right p.p-genre',
136
+ 'div.product-description-right a',
137
+ 'div.product-description-right p.p-company'
138
+ ]
139
+ end
130
140
 
131
- def targets
132
- @targets ||= [
133
- 'td.day-td',
134
- 'div.product-description-right p.p-genre',
135
- 'div.product-description-right a',
136
- 'div.product-description-right p.p-company'
137
- ]
138
- end
141
+ # Remove HTML spaces (&nbsp;) and white spaces.
142
+ # @param str [String] a string
143
+ def rm_spaces(str)
144
+ str.gsub("\u00A0", '').strip
145
+ end
139
146
 
140
- # Remove HTML spaces (&nbsp;) and white spaces.
141
- # @param str [String] a string
142
- def rm_spaces(str)
143
- str.gsub("\u00A0", '').strip
144
- end
147
+ def guard_genre(element)
148
+ raise 'invalid state' unless element.name == 'p' && element['class'] == 'p-genre'
149
+ end
145
150
 
146
- def guard_genre(element)
147
- raise 'invalid state' unless element.name == 'p' && element['class'] == 'p-genre'
148
- end
151
+ def guard_title(element)
152
+ raise 'invalid state' unless element.name == 'a'
153
+ end
149
154
 
150
- def guard_title(element)
151
- raise 'invalid state' unless element.name == 'a'
152
- end
155
+ def guard_company(element)
156
+ raise 'invalid state' unless element.name == 'p' && element['class'] == 'p-company'
157
+ end
153
158
 
154
- def guard_company(element)
155
- raise 'invalid state' unless element.name == 'p' && element['class'] == 'p-company'
159
+ alias guard_authors guard_company
156
160
  end
157
-
158
- alias guard_authors guard_company
159
161
  end
160
162
  end
@@ -3,41 +3,43 @@
3
3
  require 'faraday'
4
4
  require 'uri'
5
5
 
6
- module Manga::Tools
7
- # HTTP client class
8
- module Http
9
- # rubocop:disable Layout/LineLength
10
- UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'
11
- # rubocop:enable Layout/LineLength
6
+ module Manga
7
+ module Tools
8
+ # HTTP client class
9
+ module Http
10
+ # rubocop:disable Layout/LineLength
11
+ UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'
12
+ # rubocop:enable Layout/LineLength
12
13
 
13
- # @param url [String] an url string, eg: `https://example.com`
14
- # @return [Faraday::Connection] a connection object
15
- def self.connection(url)
16
- @connection ||= Faraday.new(
17
- url: url,
18
- headers: { 'User-Agent' => UA }
19
- ) do |f|
20
- f.response :logger
14
+ # @param url [String] an url string, eg: `https://example.com`
15
+ # @return [Faraday::Connection] a connection object
16
+ def self.connection(url)
17
+ @connection ||= Faraday.new(
18
+ url: url,
19
+ headers: { 'User-Agent' => UA }
20
+ ) do |f|
21
+ f.response :logger
22
+ end
21
23
  end
22
- end
23
24
 
24
- # @param url [String] an url string, eg: `https://example.com/path/to/object`
25
- # @return [Faraday::Response] a response object
26
- def self.get(url)
27
- u = URI.parse(url)
28
- connection("#{u.scheme}://#{u.host}").get(u.request_uri)
29
- end
25
+ # @param url [String] an url string, eg: `https://example.com/path/to/object`
26
+ # @return [Faraday::Response] a response object
27
+ def self.get(url)
28
+ u = URI.parse(url)
29
+ connection("#{u.scheme}://#{u.host}").get(u.request_uri)
30
+ end
30
31
 
31
- # @param response [Faraday::Response] a response object
32
- # @return [nil|Integer] number of second to cache
33
- #
34
- # supported http header
35
- # - cache-control: max-age=[sec]
36
- def self.seconds_to_cache(response)
37
- result = response['cache-control']&.split(/,/)&.map(&:strip)&.find { |item| item =~ /max-age=(.+)/ }
38
- return nil unless result
32
+ # @param response [Faraday::Response] a response object
33
+ # @return [nil|Integer] number of second to cache
34
+ #
35
+ # supported http header
36
+ # - cache-control: max-age=[sec]
37
+ def self.seconds_to_cache(response)
38
+ result = response['cache-control']&.split(/,/)&.map(&:strip)&.find { |item| item =~ /max-age=(.+)/ }
39
+ return nil unless result
39
40
 
40
- Regexp.last_match(1).to_i
41
+ Regexp.last_match(1).to_i
42
+ end
41
43
  end
42
44
  end
43
45
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # @private
4
3
  module Manga
5
- end
6
-
7
- module Manga::Tools
8
- VERSION = '0.1.0'
4
+ module Tools
5
+ VERSION = '0.1.1'
6
+ end
9
7
  end
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.description = 'Tools to support the automation of daily manga-related activities.'
13
13
  spec.homepage = 'https://github.com/yagihiro/manga-tools'
14
14
  spec.license = 'MIT'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
16
16
 
17
17
  # spec.metadata['allowed_push_host'] = 'TODO: Set to 'http://mygemserver.com''
18
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: manga-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroki Yagita
@@ -52,7 +52,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - ">="
54
54
  - !ruby/object:Gem::Version
55
- version: 2.3.0
55
+ version: 2.6.0
56
56
  required_rubygems_version: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="