maimai_net 0.0.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.
@@ -0,0 +1,437 @@
1
+ require 'fiddle'
2
+
3
+ module MaimaiNet
4
+ # collection of modules with extended functionality
5
+ #
6
+ # @note some of these modules may be moved out into a separate gem in a later date.
7
+ module ModuleExt
8
+ # enables capability of having class_method block definition for a module.
9
+ # @note it's not valid to use this outside this file.
10
+ module HaveClassMethods
11
+ # defines an internal module to be inherited into
12
+ # calling class's eugenclass.
13
+ # @return [void]
14
+ def class_method(&block)
15
+ ref = self
16
+ @_class_module ||= Module.new
17
+ @_class_module.instance_eval <<~EOF
18
+ def to_s
19
+ "#{ref}::ClassMethod"
20
+ end
21
+ alias inspect to_s
22
+ EOF
23
+ @_class_module.class_exec(&block)
24
+ end
25
+
26
+ # defines a hook to automatically extend target class
27
+ # with internal module from #class_method
28
+ # @param cls [Class] calling class
29
+ # @return [void]
30
+ def included(cls)
31
+ super
32
+
33
+ base = self
34
+ cls.instance_exec do
35
+ next unless Class === cls
36
+ next unless base.instance_variable_defined?(:@_class_module)
37
+ next unless Module === base.instance_variable_get(:@_class_module)
38
+ ext_class = base.instance_variable_get(:@_class_module)
39
+ # the use of singleton_class.include and extend are the same.
40
+ extend ext_class
41
+ # singleton_class.send(:include, ext_class)
42
+ end
43
+ end
44
+ end
45
+
46
+ # extends inspect function into customizable inspect output
47
+ module ExtendedInspect
48
+ extend HaveClassMethods
49
+
50
+ # @return [String] simplified human-readable output
51
+ def inspect
52
+ head = '%s:%0#*x' % [
53
+ self.class.name,
54
+ 0.size.succ << 1,
55
+ Fiddle.dlwrap(self),
56
+ ]
57
+
58
+ vars = []
59
+ all_true = !self.class.instance_variable_defined?(:@_inspect_permit)
60
+ if all_true then
61
+ permit_variables = []
62
+ permit_expressions = []
63
+ exclude_variables = []
64
+ else
65
+ all_true |= !self.class.instance_variable_get(:@_inspect_permit)
66
+
67
+ permit_variables = self.class.instance_variable_get(:@_inspect_permit_variables)
68
+ permit_expressions = self.class.instance_variable_get(:@_inspect_permit_expressions)
69
+ exclude_variables = self.class.instance_variable_get(:@_inspect_permit_variable_bans)
70
+
71
+ all_true |= [
72
+ permit_variables,
73
+ exclude_variables,
74
+ permit_expressions,
75
+ ].all?(&:empty?)
76
+ end
77
+
78
+ self.instance_variables.each do |k|
79
+ name = k.to_s[1..-1].to_sym
80
+ value = self.instance_variable_get(k)
81
+
82
+ next if exclude_variables.include? name
83
+
84
+ is_permit = all_true
85
+
86
+ unless is_permit
87
+ is_permit |= permit_variables.include?(name)
88
+ is_permit |= permit_expressions.any? do |expr| !!expr.call(value) end
89
+ end
90
+
91
+ vars << sprintf(
92
+ '@%s=%s',
93
+ name,
94
+ is_permit ? value.inspect : value.class.name,
95
+ )
96
+ end
97
+
98
+ '#<%s>' % [
99
+ [head, *vars].join(' '),
100
+ ]
101
+ end
102
+
103
+ class_method do
104
+ # inherits inspect permit from superclass
105
+ def inherited(cls)
106
+ super
107
+
108
+ %i(
109
+ inspect_permit
110
+ inspect_permit_variables
111
+ inspect_permit_variable_bans
112
+ inspect_permit_expressions
113
+ ).each do |k|
114
+ name = :"@_#{k}"
115
+ source = instance_variable_get(name)
116
+ value = source.dup rescue source
117
+
118
+ cls.instance_variable_set(name, value)
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # @return [void]
125
+ def inspect_permit_reset!
126
+ @_inspect_permit = false
127
+ @_inspect_permit_variables = []
128
+ @_inspect_permit_variable_bans = []
129
+ @_inspect_permit_expressions = []
130
+ end
131
+
132
+ # @param names [Array<String, Symbol>] list of variable name to permit by default
133
+ # @return [void]
134
+ def inspect_permit_variables(*names)
135
+ fail ArgumentError, 'empty list given' if names.empty?
136
+ fail ArgumentError, 'non-String given' if names.any? do |name| !(String === name || Symbol === name) end
137
+
138
+ @_inspect_permit ||= true
139
+ @_inspect_permit_variables.concat names.map(&:to_s).map(&:to_sym)
140
+ nil
141
+ end
142
+
143
+ # @param names [Array<String, Symbol>] list of variable name to exclude from inspection
144
+ # @return [void]
145
+ def inspect_permit_variable_exclude(*names)
146
+ fail ArgumentError, 'empty list given' if names.empty?
147
+ fail ArgumentError, 'non-String given' if names.any? do |name| !(String === name || Symbol === name) end
148
+
149
+ @_inspect_permit ||= true
150
+ @_inspect_permit_variable_bans.concat names.map(&:to_s).map(&:to_sym)
151
+ nil
152
+ end
153
+
154
+ # @param block [#call] a predicate method or block that accepts
155
+ # single argument of instance variable value to permit for.
156
+ # @return [void]
157
+ def inspect_permit_expression(&block)
158
+ fail ArgumentError, 'no block given' unless block_given?
159
+
160
+ @_inspect_permit ||= true
161
+ @_inspect_permit_expressions.push block
162
+ nil
163
+ end
164
+ end
165
+
166
+ # initializes inspect permit variables upon inclusion
167
+ # @note will not apply to modules.
168
+ # @return [void]
169
+ def self.included(cls)
170
+ super
171
+
172
+ cls.instance_exec do
173
+ inspect_permit_reset!
174
+ end if Class === cls
175
+ end
176
+ end
177
+
178
+ module AddInternalMutex
179
+ extend HaveClassMethods
180
+
181
+ class_method do
182
+ private
183
+ def lock(key, meth, &block)
184
+ @_method_mutex ||= {}
185
+ @_method_mutex[key] ||= {}
186
+ can_lock = !@_method_mutex[key].fetch(meth, nil)&.locked?
187
+ return unless can_lock
188
+
189
+ mutex = @_method_mutex[key][meth] = Mutex.new
190
+ mutex.lock
191
+ yield
192
+ ensure
193
+ if can_lock then
194
+ mutex.unlock if mutex.locked?
195
+ @_method_mutex[key].delete(meth) if can_lock
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ # automatically private any initialize-based methods
202
+ module AutoInitialize
203
+ extend HaveClassMethods
204
+
205
+ class_method do
206
+ # automatically private any initialize-based methods
207
+ # @return [void]
208
+ def method_added(meth)
209
+ private meth if /^initialize_/.match? meth
210
+ super
211
+ end
212
+ end
213
+ end
214
+
215
+ # allows registering methods into cacheable results.
216
+ module MethodCache
217
+ extend HaveClassMethods
218
+
219
+ # Hooks method that specified through cache_method with internal cache wrapper.
220
+ # @see #cache_method
221
+ def singleton_method_added(meth)
222
+ singleton_class.class_exec do
223
+ is_locked = false
224
+ mutex = @_method_mutex.to_h.dig(:cache_method, meth)
225
+ is_locked = mutex.locked? if mutex && mutex.locked?
226
+
227
+ if @_cache_methods&.include?(meth) && !is_locked then
228
+ alias_method :"raw_#{meth}", meth
229
+ _cache_method(meth)
230
+ end
231
+ end
232
+
233
+ super
234
+ end
235
+
236
+ class_method do
237
+ # only copies methods to hook definition
238
+ # @return [void]
239
+ def inherited(cls)
240
+ super
241
+
242
+ %i(
243
+ cache_methods
244
+ cache_results
245
+ ).each do |k|
246
+ name = :"@_#{k}"
247
+ source = instance_variable_get(name)
248
+ value = source.dup rescue source
249
+
250
+ cls.instance_variable_set(name, value)
251
+ end
252
+
253
+ cls.instance_variable_get(:@_cache_results)&.tap do |result|
254
+ source = instance_variable_get(:@_cache_results)
255
+ result.clear
256
+ result.default_proc = source.default_proc
257
+ end
258
+ end
259
+
260
+ # Hooks method that specified through cache_method with internal cache wrapper.
261
+ # @see #cache_method
262
+ def method_added(meth)
263
+ mutex = @_method_mutex.to_h.dig(:cache_method, meth)
264
+ is_locked = mutex&.locked?
265
+
266
+ if @_cache_methods&.include?(meth) && !is_locked then
267
+ alias_method :"raw_#{meth}", meth
268
+ _cache_method(meth)
269
+ end
270
+
271
+ super
272
+ end
273
+
274
+ private
275
+
276
+ # This method works in a few ways.
277
+ # If a block is given, this acts like define_method but automatically hooked on-the-fly.
278
+ # Else if the method is previously defined, it will wrap the method into cached method.
279
+ # Otherwise, just add the method to the internal list to hook upon definition.
280
+ #
281
+ # @note does not support methods with any arity yet.
282
+ # @param meth [String, Symbol] method name to cache for
283
+ # @return [String, Symbol] meth parameter returned.
284
+ def cache_method(meth, &block)
285
+ if block_given? then
286
+ define_method :"raw_#{meth}", &block
287
+ _cache_method(meth)
288
+ else
289
+ @_cache_methods ||= []
290
+ @_cache_methods << meth
291
+
292
+ if instance_methods.include? meth then
293
+ if private_instance_methods.include? :"raw_#{meth}" then
294
+ warn "%s: ignoring private method aliasing for '#{meth}'." % [caller_locations(1, 1).first] if $VERBOSE
295
+ else
296
+ alias_method :"raw_#{meth}", meth
297
+ end
298
+ _cache_method(meth)
299
+ else
300
+ # fail NotImplementedError, "cannot lazy-hook '#{meth}' method for singleton class, please define using 'cache_method #{meth.inspect} do ... end' block instead." if singleton_class?
301
+ end
302
+ end
303
+
304
+ meth
305
+ end
306
+
307
+ # @!api private
308
+ # decorator to redefine method to support cached result.
309
+ # @param meth [String, Symbol] method name to redefine
310
+ # @return [void]
311
+ # @see #cache_method
312
+ def _cache_method(meth)
313
+ first, *stack = caller_locations(0)
314
+ stack_fit = ->(count, *labels){
315
+ last = stack[count.pred]
316
+ first.absolute_path == last.absolute_path &&
317
+ labels.map(&:to_s).include?(last.label)
318
+ }
319
+
320
+ fail 'cannot call this method from outside' unless
321
+ stack_fit.call(1, :cache_method, :method_added) ||
322
+ stack_fit.call(3, :singleton_method_added)
323
+
324
+ private :"raw_#{meth}"
325
+ cache = @_cache_results
326
+ invoke = ->(meth, *args, **kwargs, &block) {
327
+ kwargs.empty? ?
328
+ meth.call(*args, &block) :
329
+ meth.call(*args, **kwargs, &block)
330
+ }
331
+ map_parameters = ->(meth, *args, **kwargs, &block) {
332
+ parameters = meth.parameters.map(&:first)
333
+ positionals = {front: [], rest: args, back: []}
334
+ pos_rest_at = parameters.index(:rest)
335
+ if pos_rest_at.nil? then
336
+ positionals[:front] = args
337
+ positionals[:rest] = []
338
+ else
339
+ positionals[:front] = args.slice! (...pos_rest_at)
340
+ positionals[:back] = args.slice! ((pos_rest_at + 1)..)
341
+ end
342
+
343
+ keyword_names = meth.parameters.select do |t, k| %i(keyreq key).include? k end
344
+ keywords, options = keyword_names.select do |t, k| %i(keyreq key keyrest).include? k end
345
+ .partition do |t, k| %i(keyreq key).include?(k) end
346
+ .map do |li|
347
+ kwargs.values_at(*li.map(&:last))
348
+ end
349
+
350
+ have_block = parameters.include? :block
351
+ raw_parameters = [
352
+ *positionals[:front],
353
+ positionals[:rest],
354
+ *positionals[:back],
355
+
356
+ keywords, options,
357
+ ]
358
+ raw_parameters << block if have_block
359
+
360
+ raw_parameters
361
+ }
362
+
363
+ use_param_hash = true
364
+
365
+ lock :cache_method, meth do
366
+ define_method meth do |*args, **kwargs, &block|
367
+ m = method(:"raw_#{meth}")
368
+ if use_param_hash and not(args.empty? and kwargs.empty?) then
369
+ param_hash = map_parameters.call(m, *args, **kwargs, &block).hash
370
+ return cache[meth][__id__][param_hash] if cache.key?(meth) && cache[meth].key?(__id__) && cache[meth][__id__].key?(param_hash)
371
+ cache[meth][__id__] = {} unless cache[meth].key? __id__
372
+ cache[meth][__id__][param_hash] = invoke.call(m, *args, **kwargs, &block)
373
+ else
374
+ return cache[meth][__id__] if cache.key?(meth) && cache[meth].key?(__id__)
375
+ cache[meth][__id__] = invoke.call(m, *args, **kwargs, &block)
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
381
+
382
+ # initializes internal data for method caching
383
+ # @return [void]
384
+ def self.included(cls)
385
+ cls.class_exec do
386
+ include AddInternalMutex
387
+ end
388
+
389
+ super
390
+
391
+ cls.class_exec do
392
+ if singleton_class? then
393
+ # define_method :singleton_method_added, method(:method_added).unbind
394
+ singleton_class.undef_method :method_added
395
+ else
396
+ undef_method :singleton_method_added
397
+ end
398
+ end
399
+
400
+ cls.instance_exec do
401
+ proc_key_to_id = ->(h, k){
402
+ case k
403
+ when Integer
404
+ h[k]
405
+ when Float, NilClass, TrueClass, FalseClass
406
+ fail KeyError, "invalid key"
407
+ else
408
+ h[k.object_id]
409
+ end
410
+ }
411
+
412
+ @_cache_methods ||= []
413
+ @_cache_results ||= Hash.new do |h, k|
414
+ next h[k.to_sym] if h.key?(k.to_sym)
415
+ h[k.to_sym] = Hash.new &proc_key_to_id
416
+ end
417
+ end
418
+ end
419
+ end
420
+
421
+ %i(ExtendedInspect AutoInitialize).tap do |keys|
422
+ const_list = keys.map do |k| const_get k end
423
+ keys.each do |k| remove_const k end
424
+
425
+ [
426
+ %i(append_features include),
427
+ %i(prepend_features prepend),
428
+ ].each do |(source_meth, internal_meth)|
429
+ define_singleton_method source_meth do |cls|
430
+ cls.__send__ internal_meth, *const_list
431
+ end
432
+ end
433
+ end
434
+
435
+ remove_const :HaveClassMethods
436
+ end
437
+ end
@@ -0,0 +1,9 @@
1
+ module MaimaiNet
2
+ module Page
3
+ class Debug < Base
4
+ def debug
5
+ byebug
6
+ end
7
+ end if respond_to? :byebug
8
+ end
9
+ end
@@ -0,0 +1,131 @@
1
+ module MaimaiNet
2
+ module Page
3
+ # Interesting on how refinement ON this file affects the use of helper_method block invocation.
4
+ using IncludeAutoConstant
5
+
6
+ # @!api private
7
+ # scope extension to add various html-related method
8
+ class HelperBlock < ::BasicObject
9
+ include ModuleExt
10
+
11
+ # copies page instance variables
12
+ # @param page [Page::Base] page object tp refer
13
+ def initialize(page)
14
+ page.instance_variables.map do |k|
15
+ "#{k} = page.instance_variable_get(#{k.inspect})"
16
+ end.join($/).tap do |expr|
17
+ instance_eval expr, __FILE__, __LINE__ + 1
18
+ end if Page::Base === page
19
+
20
+ @_page = page
21
+ end
22
+
23
+ # proxy method
24
+ private
25
+ def method_missing(meth, *args, **kwargs, &block)
26
+ return super unless Page::Base === @_page
27
+ kwargs.empty? ?
28
+ @_page.__send__(meth, *args, &block) :
29
+ @_page.__send__(meth, *args, **kwargs, &block)
30
+ end
31
+
32
+ # checks whether current or proxied object have respective method.
33
+ # @return [Boolean]
34
+ def respond_to_missing?(meth, priv=false)
35
+ return super unless Page::Base === @_page
36
+ super or @_page.respond_to?(meth, priv)
37
+ end
38
+
39
+ GROUPED_INTEGER = /0|[1-9](?:[0-9]*(?:,[0-9]+)*)/.freeze
40
+ GROUPED_FREE_INTEGER = /\d+(?:,\d+)*/.freeze
41
+
42
+ [[::Kernel, %i(method)]].each do |cls, methods|
43
+ methods.each do |meth|
44
+ define_method meth, cls.instance_method(meth)
45
+ end
46
+ end
47
+
48
+ private
49
+ # @return [String] stripped text content
50
+ def strip(node); node&.content.strip; end
51
+ # @return [String] src attribute of the element
52
+ def src(node); node['src']; end
53
+ # @return [Integer] de-grouped the integer string
54
+ def int(str); str.gsub(',', '').to_i(10); end
55
+ # scan for the first number-string found on given string
56
+ # and de-group the number-string into an actual integer
57
+ # @return [Integer]
58
+ # @see #int
59
+ def get_int(content); int(GROUPED_INTEGER.match(content).to_s); end
60
+ # (see #get_int)
61
+ # @note This version retrieves potentially padded integer as well.
62
+ def get_fullint(content); int(GROUPED_FREE_INTEGER.match(content).to_s); end
63
+ # scan for all number-string found on given string
64
+ # and de-group all of the string into array of integers
65
+ # @return [Array<Integer>]
66
+ def scan_int(content); content.scan(GROUPED_INTEGER).map(&method(:int)); end
67
+
68
+ inspect_permit_variable_exclude :_page
69
+ inspect_permit_expression do |value| false end
70
+ end
71
+
72
+ class << HelperBlock
73
+ private :new
74
+ end
75
+
76
+ # adds capability to inject methods using hidden helper block
77
+ module HelperSupport
78
+ # defines the method to be injected with hidden helper block.
79
+ # such method have an extended set of capability to use methods
80
+ # provided on HelperBlock class.
81
+ # @param meth [Symbol]
82
+ # @return [Symbol]
83
+ def helper_method(meth, &block)
84
+ lock :helper_method, meth do
85
+ define_method meth do
86
+ HelperBlock.__send__(:new, self).instance_exec(&block)
87
+ end
88
+ end
89
+ end
90
+
91
+ # install an auto-hook for data method for any Page::Base class.
92
+ # @param meth [Symbol]
93
+ # @return [void]
94
+ def method_added(meth)
95
+ return super unless meth === :data && self <= Page::Base
96
+
97
+ lock :helper_method, meth do
98
+ fail NotImplementedError, "no solution found for method definition rebinding. please use helper_method #{meth.inspect} do ... end block instead."
99
+
100
+ # rand_name = '_%0*x' % [0.size << 1, rand(1 << (0.size << 3))]
101
+ old_meth = instance_method(meth)
102
+ # obj = allocate
103
+ # old_meth = obj.method(meth)
104
+ # HelperBlock.define_method rand_name, old_meth
105
+ # old_meth = instance_method(rand_name)
106
+ # p old_meth
107
+ # remove_method rand_name
108
+
109
+ define_method meth do
110
+ HelperBlock.__send__(:new, self).instance_exec(&old_meth.bind(self))
111
+ end
112
+ super
113
+ end
114
+ end
115
+
116
+ # automatically install mutex upon invoked for an extension
117
+ # @return [void]
118
+ def self.extended(cls)
119
+ cls.class_exec do
120
+ include ModuleExt::AddInternalMutex
121
+ end
122
+ end
123
+ end
124
+
125
+ private_constant :HelperBlock
126
+
127
+ class Base
128
+ extend HelperSupport
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,33 @@
1
+ module MaimaiNet
2
+ module Page
3
+ module PlayerDataHelper
4
+ def self.process(elm)
5
+ HelperBlock.send(:new, nil).instance_exec do
6
+ counter_elements = elm.css <<-CSS.strip
7
+ div:not(.musiccount_block):not(.clearfix) ~ .musiccount_block:has(~ .clearfix),
8
+ div:not(.musiccount_block):not(.clearfix) ~ .clearfix:has(~ .clearfix)
9
+ CSS
10
+ cascaded_data = []
11
+ data = []
12
+ column_id = 0
13
+ counter_elements.each do |elm|
14
+ if elm.classes.include?('clearfix') then
15
+ column_id = 0
16
+ next
17
+ end
18
+
19
+ cascaded_data << [] if column_id.succ > cascaded_data.size
20
+
21
+ cascaded_data[column_id] << Model::SongCount.new(
22
+ **%i(achieved total).zip(scan_int(strip(elm))).to_h
23
+ )
24
+ column_id += 1
25
+ end
26
+
27
+ cascaded_data.each do |column_data| data.concat column_data end
28
+ data
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,90 @@
1
+ module MaimaiNet
2
+ module Page
3
+ module TrackResultHelper
4
+ using IncludeAutoConstant
5
+ def self.process(
6
+ elm,
7
+ web_id: MaimaiNet::Model::WebID::DUMMY,
8
+ result_combo: nil,
9
+ result_sync_score: nil
10
+ )
11
+ HelperBlock.send(:new, nil).instance_exec do
12
+ header_block = elm.at_css('.playlog_top_container')
13
+ difficulty = ::Kernel.Difficulty(::Kernel.Pathname(src(header_block.at_css('img.playlog_diff'))).sub_ext('').sub(/.+_/, '').basename)
14
+
15
+ dx_container_classes = MaimaiNet::Difficulty::DELUXE.select do |k, v| v.positive? end
16
+ .keys.map do |k| ".playlog_#{k}_container" end
17
+ # info_block = elm.at_css(*dx_container_classes)
18
+ info_block = elm.at_css(".playlog_#{difficulty.key}_container")
19
+ chart_header_block = info_block.at_css('.basic_block')
20
+ result_block = info_block.at_css('.basic_block ~ div:nth-of-type(1)')
21
+
22
+ track_order = get_fullint(strip(header_block.at_css('div.sub_title > span:nth-of-type(1)')))
23
+ play_time = Time.strptime(
24
+ strip(header_block.at_css('div.sub_title > span:nth-of-type(2)')) + ' +09:00',
25
+ '%Y/%m/%d %H:%M %z',
26
+ )
27
+ song_name = strip(chart_header_block.children.last)
28
+ chart_level = strip(chart_header_block.at_css('div:nth-of-type(1)'))
29
+ song_jacket = src(result_block.at_css('img.music_img'))
30
+ chart_type = nil
31
+ result_block.at_css('img.playlog_music_kind_icon')&.tap do |elm|
32
+ chart_type = ::Kernel.Pathname(src(elm))&.sub_ext('')&.sub(/.+_/, '')&.basename&.to_s
33
+ end
34
+
35
+ result_score = strip(result_block.at_css('.playlog_achievement_txt')).to_f
36
+ result_deluxe_scores = scan_int(strip(result_block.at_css('.playlog_result_innerblock .playlog_score_block div:nth-of-type(1)')))
37
+ result_grade = ::Kernel.Pathname(::Kernel.URI(src(result_block.at_css('.playlog_scorerank'))).path).sub_ext('')&.sub(/.+_/, '')&.basename&.to_s.to_sym
38
+ result_flags = result_block.css('.playlog_result_innerblock > img').map do |elm|
39
+ flag = ::Kernel.Pathname(::Kernel.URI(src(elm)).path).sub_ext('')&.basename.to_s
40
+ case flag
41
+ when *MaimaiNet::AchievementFlag::RESULT.values; MaimaiNet::AchievementFlag.new(result_key: flag)
42
+ when /_dummy$/; nil
43
+ end
44
+ end.compact
45
+
46
+ challenge_info = nil
47
+ result_block.at_css('div:has(> .playlog_life_block)')&.tap do |elm|
48
+ challenge_type = ::Kernel.Pathname(::Kernel.URI(src(elm.at_css('img:nth-of-type(1)'))).path).basename.sub_ext('').sub(/.+_/, '').to_s.to_sym
49
+ challenge_lives = scan_int(strip(elm.at_css('.playlog_life_block')))
50
+
51
+ challenge_info = Model::Result::Challenge.new(
52
+ type: challenge_type,
53
+ lives: Model::Result::Progress.new(**%i(value max).zip(challenge_lives).to_h),
54
+ )
55
+ end
56
+
57
+ score_data = {
58
+ score: result_score,
59
+ **%i(deluxe_score combo sync_score).zip([
60
+ result_deluxe_scores, result_combo, result_sync_score,
61
+ ]).reject do |k, li| li.nil? end.map do |k, li|
62
+ [k, Model::Result::Progress.new(**%i(value max).zip(li).to_h)]
63
+ end.to_h,
64
+ grade: result_grade,
65
+ flags: result_flags.map(&:to_sym),
66
+ }
67
+
68
+ score_cls = score_data.key?(:combo) && score_data.key?(:sync_score) ?
69
+ Model::Result::Score : Model::Result::ScoreLite
70
+
71
+ score_info = score_cls.new(**score_data)
72
+
73
+ Model::Result::Track.new(
74
+ info: Model::Chart::Info.new(
75
+ web_id: web_id,
76
+ title: song_name,
77
+ type: (chart_type or -'unknown'),
78
+ difficulty: difficulty.id,
79
+ level_text: chart_level,
80
+ ),
81
+ score: score_info,
82
+ order: track_order,
83
+ time: play_time,
84
+ challenge: challenge_info,
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end