manga-tools 0.1.0 → 0.1.1

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.
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
  - - ">="