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.
- checksums.yaml +7 -0
- data/.github/workflows/gem.yml +53 -0
- data/.gitignore +165 -0
- data/.rspec +5 -0
- data/Gemfile +4 -0
- data/LICENSE +32 -0
- data/README.md +47 -0
- data/Rakefile +6 -0
- data/bin/console +16 -0
- data/bin/setup +6 -0
- data/lib/maimai_net/client.rb +1104 -0
- data/lib/maimai_net/constants.rb +485 -0
- data/lib/maimai_net/core_ext.rb +12 -0
- data/lib/maimai_net/error.rb +55 -0
- data/lib/maimai_net/faraday_ext/cookie_jar.rb +38 -0
- data/lib/maimai_net/model-typing.rb +202 -0
- data/lib/maimai_net/model.rb +359 -0
- data/lib/maimai_net/module_ext.rb +437 -0
- data/lib/maimai_net/page-debug.rb +9 -0
- data/lib/maimai_net/page-html_helper.rb +131 -0
- data/lib/maimai_net/page-player_data_helper.rb +33 -0
- data/lib/maimai_net/page-track_result_helper.rb +90 -0
- data/lib/maimai_net/page.rb +606 -0
- data/lib/maimai_net/refines.rb +28 -0
- data/lib/maimai_net/region.rb +108 -0
- data/lib/maimai_net/user_option.rb +147 -0
- data/lib/maimai_net/util.rb +9 -0
- data/lib/maimai_net/version.rb +3 -0
- data/lib/maimai_net.rb +14 -0
- data/maimai-net.gemspec +44 -0
- metadata +172 -0
|
@@ -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,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
|