maimai_net 0.0.2 → 0.0.3

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: 1536775b0efe6486ed28430da4fdeb186acf36aa70a18f3b3e67ad14b8f6db79
4
- data.tar.gz: f6dae985de61be79aa0a249bb3b4bddfd174a2398ae23a9441528d0a4a833afe
3
+ metadata.gz: e6a043bf61fd5bd226b1f0772ee5a31014fdea73a61f66aac3f016b2f7fa3a74
4
+ data.tar.gz: 835c2b17b0fce9fe35dc83517193e135cd1bf99d052ad9618116dc311b2fa592
5
5
  SHA512:
6
- metadata.gz: a7d0544158ff4426a591cd89b12d5a8436c654732c07ea2b013691d15c04277639e1db624ea5c1a059a103c039e402d9a30ad2a0532d65fadb88a4a1a7a01f01
7
- data.tar.gz: d1da766e1d1bf3095b811fd2f35ccd51e31c4cc700b67082043eb4f9528a49e173f7184539d6ef4763ba6b2864a7fcadeafafbdfb7a6efe0f662b46507c7488b
6
+ metadata.gz: c7afa65b670dfb11d8f1827381c8b3264580784871581b6238f3de84040040607e6bd728b0aac6ad50abcb45707e28a398cd45d2c8a7798b9be98b61363b5584
7
+ data.tar.gz: ea17dd808b4c8030426715535325aed6eb5d12d152e7b888726cba95d63e12d9e79d3078e46e6332f43622662ad82a23719d088e329eb18a99521ed90c77bac0
@@ -25,6 +25,12 @@ module MaimaiNet
25
25
  versions: :version,
26
26
  }.freeze
27
27
 
28
+ module ErrorCodes
29
+ LOGIN_ERROR = 100_101
30
+ SESSION_REFRESH = 200_002
31
+ SESSION_INVALID = 200_004
32
+ end
33
+
28
34
  class Base
29
35
  include ModuleExt
30
36
 
@@ -193,10 +199,14 @@ module MaimaiNet
193
199
  fail TypeError, 'expected a valid index ID format'
194
200
  end
195
201
 
202
+ actual_time = Time.at(/^(\d+),(\d+)$/.match(id).captures[1].to_i).localtime(32400).freeze
203
+
196
204
  send_request(
197
205
  'get', '/maimai-mobile/record/playlogDetail', {idx: id},
198
206
  response_page: Page::TrackResult,
199
- )
207
+ ).tap do |track_result|
208
+ track_result.track.time = actual_time
209
+ end
200
210
  end
201
211
 
202
212
  # access recent session gameplay detailed info
@@ -282,7 +292,16 @@ module MaimaiNet
282
292
  # @return [void]
283
293
  # @raise [Error::LoginError]
284
294
  def on_login_error
285
- fail Error::LoginError, 100101
295
+ fail Error::LoginError, ErrorCodes::LOGIN_ERROR
296
+ end
297
+
298
+ # hook upon receiving login expired page,
299
+ # triggering this hook causes client cookies wiped out.
300
+ # @return [void]
301
+ # @raise [Error::SessionExpiredError]
302
+ def on_login_expired_error
303
+ @client.cookies.clear
304
+ fail Error::SessionExpiredError, ErrorCodes::SESSION_INVALID
286
305
  end
287
306
 
288
307
  # hook upon receiving generic error page
@@ -298,12 +317,12 @@ module MaimaiNet
298
317
  error_code = error_note.match(/\d+/).to_s.to_i
299
318
 
300
319
  case error_code
301
- when 100101
302
- fail Error::LoginError, error_code
303
- when 200002
320
+ when ErrorCodes::LOGIN_ERROR
321
+ on_login_error
322
+ when ErrorCodes::SESSION_REFRESH
304
323
  fail Error::SessionRefreshError, error_code
305
- when 200004
306
- fail Error::SessionExpiredError, error_code
324
+ when ErrorCodes::SESSION_INVALID
325
+ on_login_expired_error
307
326
  else
308
327
  fail Error::GeneralError, error_code
309
328
  end
@@ -1002,7 +1021,7 @@ module MaimaiNet
1002
1021
 
1003
1022
  @conn = Faraday.new(url: info[:base_host]) do |builder|
1004
1023
  builder.request :url_encoded
1005
- builder.response :follow_redirects
1024
+ builder.response :follow_redirects, limit: 5
1006
1025
  builder.use :cookie_jar, jar: client.cookies
1007
1026
  end
1008
1027
  end
@@ -1,5 +1,9 @@
1
1
  module MaimaiNet
2
2
  module CoreExt
3
+ # contains every {Constants AutoConstant} classes turned into a function.
4
+ # used for include or prepend in a scope.
5
+ #
6
+ # also used in `refine` {IncludeAutoConstant}.
3
7
  module AutoConstantInclusion
4
8
  MaimaiNet.constants.each do |k|
5
9
  cls = MaimaiNet.const_get(k)
@@ -9,5 +13,19 @@ module MaimaiNet
9
13
  private k
10
14
  end
11
15
  end
16
+
17
+ # adds JSON conversion support through `#to_h` conversion.
18
+ module JSONSupport
19
+ def as_json(options = nil)
20
+ to_h.transform_values do |val|
21
+ val.respond_to?(:as_json) ?
22
+ val.as_json(options) : val
23
+ end
24
+ end
25
+
26
+ def to_json(options = nil)
27
+ as_json.to_json(options)
28
+ end
29
+ end
12
30
  end
13
31
  end
@@ -26,6 +26,8 @@ module MaimaiNet
26
26
  super(*args)
27
27
  end
28
28
 
29
+ include CoreExt::JSONSupport
30
+
29
31
  class << self
30
32
  # creates a strong-typed struct data
31
33
  # @param opts [Hash{Symbol => Module}]
@@ -276,6 +278,8 @@ module MaimaiNet
276
278
  track: Track,
277
279
  breakdown: Generic[Hash, Symbol, Judgment],
278
280
  timing: Offset,
281
+ rating_before: Integer,
282
+ rating_after: Integer,
279
283
  members: Generic[Array, TourMember],
280
284
  rival: Optional[RivalInfo],
281
285
  players: Generic[Array, PlayerInfo],
@@ -64,6 +64,18 @@ module MaimaiNet
64
64
  # and de-group all of the string into array of integers
65
65
  # @return [Array<Integer>]
66
66
  def scan_int(content); content.scan(GROUPED_INTEGER).map(&method(:int)); end
67
+ # parse time string as JST
68
+ # @return [Time]
69
+ def jst(time); ::Time.strptime(time + ' +09:00', '%Y/%m/%d %H:%M %z'); end
70
+ # parse time string as JST from stripped text content
71
+ # @return [Time]
72
+ def jst_from(node); jst(strip(node)); end
73
+ # @return [String] basename part of the path without any prefixes
74
+ def subpath(uri); ::Kernel.Pathname(::Kernel.URI(uri).path)&.sub_ext('')&.sub(/.+_/, '')&.basename.to_s; end
75
+ # (see #subpath)
76
+ def subpath_from(node); node ? subpath(src(node)) : -'' end
77
+ # @return [String] text contained directly under the node
78
+ def text(node); node.children.select(&:text?).map(&:content).inject('', :concat).strip end
67
79
 
68
80
  inspect_permit_variable_exclude :_page
69
81
  inspect_permit_expression do |value| false end
@@ -73,6 +85,43 @@ module MaimaiNet
73
85
  private :new
74
86
  end
75
87
 
88
+ module TrackHelper
89
+ # @return [Constants::Difficulty] difficulty value of given html element
90
+ def get_chart_difficulty_from(node)
91
+ Difficulty(subpath_from(node))
92
+ end
93
+
94
+ # @return [String] normalized difficulty text
95
+ def get_chart_level_text_from(node)
96
+ strip(node).sub(/\?$/, '')
97
+ end
98
+
99
+ # @return [String] chart type of given html element
100
+ # @return ["unknown"] if the chart element is not defined
101
+ def get_chart_type_from(node)
102
+ return -'unknown' if node.nil?
103
+
104
+ subpath_from(node)
105
+ end
106
+
107
+ # @return [String] chart variant of given html element
108
+ # @return [nil] if the chart element is not utage
109
+ # @see HelperBlock#strip
110
+ # @note this is a semantic clarity for strip function.
111
+ def get_chart_variant_from(node)
112
+ node&.at_css('img[src*="music_utage.png"]').nil? ?
113
+ nil : strip(node)
114
+ end
115
+
116
+ # @return [0] for non buddy chart
117
+ # @return [1] for buddy chart
118
+ def get_chart_buddy_flag_from(node)
119
+ node&.at_css('img[src*="music_utage_buddy.png"]').nil? ? 0 : 1
120
+ end
121
+ end
122
+
123
+ HelperBlock.include TrackHelper
124
+
76
125
  # adds capability to inject methods using hidden helper block
77
126
  module HelperSupport
78
127
  # defines the method to be injected with hidden helper block.
@@ -10,12 +10,8 @@ module MaimaiNet
10
10
  )
11
11
  HelperBlock.send(:new, nil).instance_exec do
12
12
  header_block = elm.at_css('.playlog_top_container')
13
- difficulty = Difficulty(::Kernel.Pathname(src(header_block.at_css('img.playlog_diff'))).sub_ext('').sub(/.+_/, '').basename)
14
- utage_variant = header_block.at_css('.playlog_music_kind_icon_utage').yield_self do |elm|
15
- next if elm.nil?
16
-
17
- strip(elm)
18
- end
13
+ difficulty = get_chart_difficulty_from(header_block.at_css('img.playlog_diff'))
14
+ utage_variant = get_chart_variant_from(header_block.at_css('.playlog_music_kind_icon_utage'))
19
15
 
20
16
  dx_container_classes = MaimaiNet::Difficulty::DELUXE.select do |k, v| v.positive? end
21
17
  .keys.map do |k| ".playlog_#{k}_container" end
@@ -25,36 +21,29 @@ module MaimaiNet
25
21
  result_block = info_block.at_css('.basic_block ~ div:nth-of-type(1)')
26
22
 
27
23
  track_order = get_fullint(strip(header_block.at_css('div.sub_title > span:nth-of-type(1)')))
28
- play_time = Time.strptime(
29
- strip(header_block.at_css('div.sub_title > span:nth-of-type(2)')) + ' +09:00',
30
- '%Y/%m/%d %H:%M %z',
31
- )
24
+ play_time = jst_from(header_block.at_css('div.sub_title > span:nth-of-type(2)'))
32
25
  song_name = strip(chart_header_block.children.last)
33
- chart_level = strip(chart_header_block.at_css('div:nth-of-type(1)'))
26
+ chart_level = get_chart_level_text_from(chart_header_block.at_css('div:nth-of-type(1)'))
34
27
  song_jacket = src(result_block.at_css('img.music_img'))
35
- chart_type = result_block.at_css('img.playlog_music_kind_icon').yield_self do |elm|
36
- next if elm.nil?
37
-
38
- ::Kernel.Pathname(src(elm))&.sub_ext('')&.sub(/.+_/, '')&.basename&.to_s
39
- end
28
+ chart_type = get_chart_type_from(result_block.at_css('img.playlog_music_kind_icon'))
40
29
 
41
30
  result_score = strip(result_block.at_css('.playlog_achievement_txt')).to_f
42
31
  result_deluxe_scores = scan_int(strip(result_block.at_css('.playlog_result_innerblock .playlog_score_block div:nth-of-type(1)')))
43
- result_grade = ::Kernel.Pathname(::Kernel.URI(src(result_block.at_css('.playlog_scorerank'))).path).sub_ext('')&.sub(/.+_/, '')&.basename&.to_s.to_sym
32
+ result_grade = subpath_from(result_block.at_css('.playlog_scorerank')).to_sym
44
33
  result_flags = result_block.css('.playlog_result_innerblock > img').map do |elm|
45
- flag = ::Kernel.Pathname(::Kernel.URI(src(elm)).path).sub_ext('')&.basename.to_s
34
+ flag = subpath_from(elm)
46
35
  case flag
47
36
  when *MaimaiNet::AchievementFlag::RESULT.values; AchievementFlag(result_key: flag)
48
37
  when /_dummy$/; nil
49
38
  end
50
39
  end.compact
51
40
  result_position = result_block.at_css('.playlog_result_innerblock img.playlog_matching_icon')&.yield_self do |elm|
52
- /^\d+/.match(::Kernel.Pathname(::Kernel.URI(src(elm)).path).sub_ext('')&.basename.to_s)[0].to_i
41
+ /^\d+/.match(subpath_from(elm))[0].to_i
53
42
  end
54
43
 
55
44
  challenge_info = nil
56
45
  result_block.at_css('div:has(> .playlog_life_block)')&.tap do |elm|
57
- challenge_type = ::Kernel.Pathname(::Kernel.URI(src(elm.at_css('img:nth-of-type(1)'))).path).basename.sub_ext('').sub(/.+_/, '').to_s.to_sym
46
+ challenge_type = subpath_from(elm.at_css('img:nth-of-type(1)')).to_sym
58
47
  challenge_lives = scan_int(strip(elm.at_css('.playlog_life_block')))
59
48
 
60
49
  challenge_info = Model::Result::Challenge.new(
@@ -135,22 +135,23 @@ module MaimaiNet
135
135
  images.map do |elm|
136
136
  elm = elm.at_css('> div')
137
137
 
138
- chart_type = Pathname(src(elm.at_css('> .music_kind_icon'))).sub_ext('').basename
139
- difficulty = Difficulty(Pathname(src(elm.at_css('> img:nth-of-type(2)'))).sub_ext('').sub('diff_', '').basename)
138
+ chart_type = get_chart_type_from(elm.at_css('> .music_kind_icon'))
139
+ difficulty = get_chart_difficulty_from(elm.at_css('> .block_info:nth-of-type(1) ~ img:nth-of-type(1)'))
140
+ chart_flags = [
141
+ get_chart_buddy_flag_from(elm.at_css('.music_kind_icon_utage:has(img[src*="music_utage_buddy.png"])')),
142
+ ].inject(0, :|)
140
143
 
141
144
  Model::PhotoUpload.new(
142
145
  info: Model::Chart::InfoLite.new(
143
- title: strip(elm.at_css('> div:not(.clearfix):nth-of-type(2)')),
146
+ title: strip(elm.at_css('> .clearfix:nth-of-type(1) ~ div:nth-of-type(1)')),
144
147
  type: chart_type.to_s,
148
+ variant: get_chart_variant_from(elm.at_css('.music_kind_icon_utage:has(img[src*="music_utage.png"])')),
149
+ flags: chart_flags,
145
150
  difficulty: difficulty.id,
146
151
  ),
147
- url: URI(src(elm.at_css('> img:nth-of-type(3)'))),
148
- location: strip(elm.at_css('> div:not(.clearfix):nth-of-type(4)')),
149
- time: Time.strptime(
150
- strip(elm.at_css('> div:not(.clearfix):nth-of-type(1)')) + ' +09:00',
151
- '%Y/%m/%d %H:%M %z',
152
- Time.now.localtime(32400),
153
- ),
152
+ url: URI(src(elm.at_css('> .block_info:nth-of-type(1) ~ img:nth-of-type(2)'))),
153
+ location: strip(elm.at_css('> .clearfix:nth-of-type(1) ~ div:nth-of-type(3)')),
154
+ time: jst_from(elm.at_css('> div:not(.clearfix):nth-of-type(1)')),
154
155
  )
155
156
  end
156
157
  end
@@ -167,8 +168,12 @@ module MaimaiNet
167
168
  song_info_elm = @summary_block.at_css('> div:nth-of-type(1)')
168
169
 
169
170
  song_jacket = URI(src(@summary_block.at_css('> img:nth-of-type(1)')))
170
- set_type = Pathname(src(song_info_elm.at_css('> div:nth-of-type(1) > img'))).sub_ext('').sub(/.+_/, '').basename.to_s
171
- song_genre = strip(song_info_elm.at_css('> div:nth-of-type(1)'))
171
+ set_type = get_chart_type_from(song_info_elm.at_css('> div:nth-of-type(1) > img'))
172
+ set_utage_variant = get_chart_variant_from(song_info_elm.at_css('> div:nth-of-type(1) > .music_kind_icon_utage:has(img[src*="music_utage.png"])'))
173
+ set_flag = [
174
+ get_chart_buddy_flag_from(song_info_elm.at_css('> div:nth-of-type(1) > .music_kind_icon_utage:has(img[src*="music_utage_buddy.png"])')),
175
+ ].inject(0, :|)
176
+ song_genre = text(song_info_elm.at_css('> div:nth-of-type(1)'))
172
177
  song_name = strip(song_info_elm.at_css('> div:nth-of-type(2)'))
173
178
  song_artist = strip(song_info_elm.at_css('> div:nth-of-type(3)'))
174
179
 
@@ -185,30 +190,33 @@ module MaimaiNet
185
190
  chart_score_blocks = @summary_block.css('~ div:has(~ img)')
186
191
 
187
192
  info_blocks.each do |info_block|
188
- level_text = strip(info_block.at_css('.music_lv_back'))
193
+ level_text = get_chart_level_text_from(info_block.at_css('.music_lv_back'))
189
194
  form_block = info_block.at_css('form')
190
195
  form_inputs = form_block.css('input[name][type=hidden]').map do |elm|
191
196
  [elm['name'].to_sym, elm['value']]
192
197
  end.to_h
198
+
193
199
  difficulty = Difficulty(deluxe_web_id: form_inputs[:diff].to_i)
194
200
  difficulty_data[difficulty.abbrev] ||= {}
195
201
  difficulty_data[difficulty.abbrev].store(:info, Model::Chart::Info.new(
196
202
  web_id: Model::WebID.parse(form_inputs[:idx]),
197
203
  title: song_name,
198
204
  type: set_type,
205
+ variant: set_utage_variant,
206
+ flags: set_flag,
199
207
  difficulty: difficulty.id,
200
208
  level_text: level_text,
201
209
  ))
202
210
  end
203
211
 
204
212
  chart_score_blocks.each do |chart_score_block|
205
- difficulty = Difficulty(Pathname(src(chart_score_block.at_css('> img:nth-of-type(1)'))).sub_ext('').sub(/.+_/, '').basename)
206
- chart_type = Pathname(src(chart_score_block.at_css('> img:nth-of-type(2)')))&.sub_ext('')&.sub(/.+_/, '')&.basename&.to_s or set_type
213
+ difficulty = get_chart_difficulty_from(chart_score_block.at_css('> img:nth-of-type(1)'))
214
+ chart_type = get_chart_type_from(chart_score_block.at_css('> img:nth-of-type(2)')) || set_type
207
215
  clearfixes = chart_score_block.css('.clearfix')
208
216
 
209
217
  chart_record_block = clearfixes[0].at_css('~ div:nth-of-type(2)')
210
218
  record_grade, record_flag, record_sync_flag = chart_record_block.css('> img').map do |elm|
211
- value = Pathname(URI(src(elm)).path).sub_ext('')&.sub(/.+_/, '')&.basename.to_s
219
+ value = subpath_from(elm)
212
220
  case value
213
221
  when 'back'; nil
214
222
  when *MaimaiNet::AchievementFlag::RECORD.values; MaimaiNet::AchievementFlag.new(record_key: value)
@@ -216,7 +224,7 @@ module MaimaiNet
216
224
  end
217
225
  end
218
226
  last_played_date, total_play_count = chart_record_block.css('table tr td:nth-of-type(2)').zip([
219
- ->(content){Time.strptime(content + ' +09:00', '%Y/%m/%d %H:%M %z')},
227
+ method(:jst),
220
228
  method(:int),
221
229
  ]).map do |elm, block|
222
230
  block.call(strip(elm))
@@ -227,7 +235,7 @@ module MaimaiNet
227
235
  chart_deluxe_scores = scan_int(strip(chart_best_block.at_css('.music_score_block:nth-of-type(2)')))
228
236
  chart_deluxe_grade_elm = chart_best_block.at_css('.music_score_block:nth-of-type(2) img:nth-child(2)')
229
237
  record_deluxe_grade = chart_deluxe_grade_elm ?
230
- Pathname(src(chart_deluxe_grade_elm))&.sub_ext('')&.sub(/.+_/, '')&.basename&.to_s.to_i :
238
+ subpath_from(chart_deluxe_grade_elm).to_i :
231
239
  0
232
240
 
233
241
  difficulty_data[difficulty.abbrev].tap do |d|
@@ -272,7 +280,7 @@ module MaimaiNet
272
280
 
273
281
  helper_method :data do
274
282
  result_breakdown = @breakdown_block.css('table.playlog_notes_detail tr:not(:first-child)').map do |row|
275
- key = Pathname(URI(src(row.at_css('th img'))).path).sub_ext('').basename.to_s.to_sym
283
+ key = subpath_from(row.at_css('th img')).to_sym
276
284
  values = Model::Result::Judgment.new(**Model::Result::Judgment.members.zip(
277
285
  row.css('td').map(&method(:strip)).map(&method(:get_int))
278
286
  ).to_h)
@@ -282,8 +290,8 @@ module MaimaiNet
282
290
  result_offset_breakdown = @breakdown_block.css('.playlog_fl_block > div').map do |elm|
283
291
  get_int(strip(elm))
284
292
  end
285
- result_rating_after = int(strip(@breakdown_block.at_css('.playlog_rating_detail_block > div:nth-of-type(1) .rating_block')))
286
- result_rating_delta = int(strip(@breakdown_block.at_css('.playlog_rating_detail_block > div:nth-of-type(2)')))
293
+ result_rating_after = int(strip(@breakdown_block.at_css('.playlog_rating_detail_block > div:has(.rating_block) .rating_block')))
294
+ result_rating_delta = get_int(@breakdown_block.at_css('.playlog_rating_detail_block > div:has(.rating_block) ~ img[src*="/playlog/rating"] ~ div > span'))
287
295
  result_combos, result_sync_scores = @breakdown_block.css('.playlog_score_block').map do |elm|
288
296
  scan_int(strip(elm)).tap do |ary| ary.fill(0, ary.size...2) end
289
297
  end
@@ -312,7 +320,7 @@ module MaimaiNet
312
320
  end
313
321
 
314
322
  result_players = @multiplayer_block&.css(':has(img[src*="/diff"])').to_a.map do |elm|
315
- difficulty = Difficulty(Pathname(src(elm.at_css('> img:nth-of-type(1)'))).sub_ext('').sub(/.+_/, '').basename)
323
+ difficulty = get_chart_difficulty_from(elm.at_css('> img:nth-of-type(1)'))
316
324
  name = strip(elm.at_css('> div.basic_block:nth-of-type(1)'))
317
325
 
318
326
  Model::Result::PlayerInfo.new(
@@ -332,6 +340,8 @@ module MaimaiNet
332
340
  ),
333
341
  breakdown: result_breakdown,
334
342
  timing: Model::Result::Offset.new(**Model::Result::Offset.members.zip(result_offset_breakdown).to_h),
343
+ rating_before: result_rating_after - result_rating_delta,
344
+ rating_after: result_rating_after,
335
345
  members: result_tour_members,
336
346
  rival: result_otomodachi_rival,
337
347
  players: result_players,
@@ -383,31 +393,21 @@ module MaimaiNet
383
393
  result = track_segmented_blocks.transform_values do |elm_group|
384
394
  elm_group.map do |elm|
385
395
  chart_info = {}
386
- chart_info[:flags] = 0
387
396
 
388
- chart_info[:type] = elm.at_css('.music_kind_icon').yield_self do |_elm|
389
- next if _elm.nil?
390
-
391
- Pathname(URI(src(_elm)).path).sub_ext('').sub(/.+_/, '').basename.to_s
392
- end.yield_self do |type|
393
- type || -'unknown'
394
- end
395
-
396
- nil.tap do
397
- next if elm.at_css('.music_kind_icon_utage').nil?
398
-
399
- chart_info[:variant] = strip(elm.at_css('.music_kind_icon_utage_text:nth-of-type(1)'))
400
- chart_info[:flags] |= elm.at_css('.music_kind_icon_utage:has(img[src*=music_utage_buddy])').nil? ? 0 : 1
401
- end
397
+ chart_info[:type] = get_chart_type_from(elm.at_css('.music_kind_icon'))
398
+ chart_info[:variant] = get_chart_variant_from(elm.at_css('.music_kind_icon_utage:has(img[src*="music_utage.png"])'))
399
+ chart_info[:flags] = [
400
+ get_chart_buddy_flag_from(elm.at_css('.music_kind_icon_utage:has(img[src*="music_utage_buddy.png"])')),
401
+ ].inject(0, :|)
402
402
 
403
403
  chart_info = Model::Chart::Info.new(
404
404
  web_id: Model::WebID.parse(elm.at_css('input[name=idx][type=hidden]')['value']),
405
- title: elm.at_css('.music_name_block').content,
405
+ title: strip(elm.at_css('.music_name_block')),
406
406
  type: chart_info.fetch(:type, -'unknown'),
407
- difficulty: Difficulty(Pathname(URI(src(elm.at_css('form > img:nth-of-type(1)'))).path).sub_ext('').sub(/.+_/, '').basename.to_s).id,
407
+ difficulty: get_chart_difficulty_from(elm.at_css('form > img:nth-of-type(1)')).id,
408
408
  variant: chart_info.fetch(:variant, nil),
409
409
  flags: chart_info[:flags],
410
- level_text: elm.at_css('.music_lv_block').content,
410
+ level_text: get_chart_level_text_from(elm.at_css('.music_lv_block')),
411
411
  )
412
412
 
413
413
  score_info = nil
@@ -421,7 +421,7 @@ module MaimaiNet
421
421
  )
422
422
  else
423
423
  # ratingTargetMusic page
424
- best_grade = Pathname(URI(src(elm.at_css('.music_score_block:nth-of-type(1) > div > img'))).path).sub_ext('').sub(/.+_/, '').basename.to_s.to_sym
424
+ best_grade = subpath_from(elm.at_css('.music_score_block:nth-of-type(1) > div > img')).to_sym
425
425
  score_info = Model::Result::ScoreOnly.new(
426
426
  score: strip(elm.at_css('.music_score_block:nth-of-type(1)')).to_f,
427
427
  grade: best_grade,
@@ -437,7 +437,7 @@ module MaimaiNet
437
437
  if !elm.at_css('.music_score_block').nil? then
438
438
  best_deluxe_score = scan_int(strip(elm.at_css('.music_score_block:nth-of-type(2)')))
439
439
  flairs = elm.css('.music_score_block ~ img:has(~ .clearfix)').map do |img|
440
- Pathname(URI(src(img)).path).sub_ext('').sub(/.+_/, '').basename.to_s.to_sym.yield_self do |value|
440
+ subpath_from(img).to_sym.yield_self do |value|
441
441
  value == :back ? nil : value
442
442
  end
443
443
  end
@@ -1,5 +1,9 @@
1
1
  module MaimaiNet
2
- # includes AutoConstants into invokable class
2
+ # injects {CoreExt::AutoConstantInclusion} into {Kernel} and {BasicObject}.
3
+ # @note A bug noticed from v0.0.1 where refining Kernel only
4
+ # is not reliable due to prepending on Kernel causes
5
+ # all refines on Kernel are invalidated. A band-aid solution
6
+ # for this is also injecting BasicObject with the same refine.
3
7
  module IncludeAutoConstant
4
8
  refine Kernel do
5
9
  include CoreExt::AutoConstantInclusion
@@ -9,7 +13,8 @@ module MaimaiNet
9
13
  end
10
14
  end
11
15
 
12
- # converts any object into a single-element array unless it's an array
16
+ # grants any object an ability to convert itself into a single-element array.
17
+ # unless it's an array already.
13
18
  module ObjectAsArray
14
19
  refine Object do
15
20
  def as_array
@@ -1,3 +1,3 @@
1
1
  module MaimaiNet
2
- VERSION = -'0.0.2'
2
+ VERSION = -'0.0.3'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maimai_net
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rei Hakurei
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-08 00:00:00.000000000 Z
11
+ date: 2025-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake