maimai_net 0.0.1 → 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 +4 -4
- data/lib/maimai_net/client.rb +33 -10
- data/lib/maimai_net/core_ext.rb +20 -1
- data/lib/maimai_net/model.rb +76 -63
- data/lib/maimai_net/page-html_helper.rb +49 -0
- data/lib/maimai_net/page-track_result_helper.rb +14 -14
- data/lib/maimai_net/page.rb +72 -33
- data/lib/maimai_net/refines.rb +11 -3
- data/lib/maimai_net/version.rb +1 -1
- data/lib/maimai_net.rb +2 -0
- metadata +3 -11
- data/.github/workflows/gem.yml +0 -53
- data/.gitignore +0 -165
- data/.rspec +0 -5
- data/Gemfile +0 -4
- data/Rakefile +0 -6
- data/bin/console +0 -16
- data/bin/setup +0 -6
- data/maimai-net.gemspec +0 -44
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6a043bf61fd5bd226b1f0772ee5a31014fdea73a61f66aac3f016b2f7fa3a74
|
|
4
|
+
data.tar.gz: 835c2b17b0fce9fe35dc83517193e135cd1bf99d052ad9618116dc311b2fa592
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c7afa65b670dfb11d8f1827381c8b3264580784871581b6238f3de84040040607e6bd728b0aac6ad50abcb45707e28a398cd45d2c8a7798b9be98b61363b5584
|
|
7
|
+
data.tar.gz: ea17dd808b4c8030426715535325aed6eb5d12d152e7b888726cba95d63e12d9e79d3078e46e6332f43622662ad82a23719d088e329eb18a99521ed90c77bac0
|
data/lib/maimai_net/client.rb
CHANGED
|
@@ -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
|
|
|
@@ -61,12 +67,16 @@ module MaimaiNet
|
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
class Connection
|
|
70
|
+
extend Forwardable
|
|
71
|
+
|
|
64
72
|
# @param client [Base] client data
|
|
65
73
|
def initialize(client)
|
|
66
74
|
@client = client
|
|
67
75
|
@conn = nil
|
|
68
76
|
end
|
|
69
77
|
|
|
78
|
+
def_delegator :@client, :cookies
|
|
79
|
+
|
|
70
80
|
# automatically private hook methods
|
|
71
81
|
# @return [void]
|
|
72
82
|
def self.method_added(meth)
|
|
@@ -189,10 +199,14 @@ module MaimaiNet
|
|
|
189
199
|
fail TypeError, 'expected a valid index ID format'
|
|
190
200
|
end
|
|
191
201
|
|
|
202
|
+
actual_time = Time.at(/^(\d+),(\d+)$/.match(id).captures[1].to_i).localtime(32400).freeze
|
|
203
|
+
|
|
192
204
|
send_request(
|
|
193
205
|
'get', '/maimai-mobile/record/playlogDetail', {idx: id},
|
|
194
206
|
response_page: Page::TrackResult,
|
|
195
|
-
)
|
|
207
|
+
).tap do |track_result|
|
|
208
|
+
track_result.track.time = actual_time
|
|
209
|
+
end
|
|
196
210
|
end
|
|
197
211
|
|
|
198
212
|
# access recent session gameplay detailed info
|
|
@@ -278,7 +292,16 @@ module MaimaiNet
|
|
|
278
292
|
# @return [void]
|
|
279
293
|
# @raise [Error::LoginError]
|
|
280
294
|
def on_login_error
|
|
281
|
-
fail Error::LoginError,
|
|
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
|
|
282
305
|
end
|
|
283
306
|
|
|
284
307
|
# hook upon receiving generic error page
|
|
@@ -294,12 +317,12 @@ module MaimaiNet
|
|
|
294
317
|
error_code = error_note.match(/\d+/).to_s.to_i
|
|
295
318
|
|
|
296
319
|
case error_code
|
|
297
|
-
when
|
|
298
|
-
|
|
299
|
-
when
|
|
320
|
+
when ErrorCodes::LOGIN_ERROR
|
|
321
|
+
on_login_error
|
|
322
|
+
when ErrorCodes::SESSION_REFRESH
|
|
300
323
|
fail Error::SessionRefreshError, error_code
|
|
301
|
-
when
|
|
302
|
-
|
|
324
|
+
when ErrorCodes::SESSION_INVALID
|
|
325
|
+
on_login_expired_error
|
|
303
326
|
else
|
|
304
327
|
fail Error::GeneralError, error_code
|
|
305
328
|
end
|
|
@@ -314,7 +337,7 @@ module MaimaiNet
|
|
|
314
337
|
# @param opts [Hash{Symbol => Object}]
|
|
315
338
|
# @option response_page [Class<Page::Base>] response parser class
|
|
316
339
|
# @option response [#call] response method, takes one argument, response body raw.
|
|
317
|
-
# @return [Model::Base
|
|
340
|
+
# @return [Model::Base] returns page data based from provided response_page field
|
|
318
341
|
# @return [void]
|
|
319
342
|
def send_request(method, url, data, **opts)
|
|
320
343
|
fail NotImplementedError, 'abstract method called' if Connection == method(__method__).owner
|
|
@@ -358,7 +381,7 @@ module MaimaiNet
|
|
|
358
381
|
# @!api private
|
|
359
382
|
# @param url [URI] response url
|
|
360
383
|
# @param body [String] response body
|
|
361
|
-
# @return [Model::Base
|
|
384
|
+
# @return [Model::Base, Array<Model::Base>] response page handled result
|
|
362
385
|
# @return [nil] no response page defined to handle the response
|
|
363
386
|
def process_response(url:, body:, request_options:)
|
|
364
387
|
info = @client.class.region_info
|
|
@@ -998,7 +1021,7 @@ module MaimaiNet
|
|
|
998
1021
|
|
|
999
1022
|
@conn = Faraday.new(url: info[:base_host]) do |builder|
|
|
1000
1023
|
builder.request :url_encoded
|
|
1001
|
-
builder.response :follow_redirects
|
|
1024
|
+
builder.response :follow_redirects, limit: 5
|
|
1002
1025
|
builder.use :cookie_jar, jar: client.cookies
|
|
1003
1026
|
end
|
|
1004
1027
|
end
|
data/lib/maimai_net/core_ext.rb
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
module MaimaiNet
|
|
2
2
|
module CoreExt
|
|
3
|
-
|
|
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}.
|
|
7
|
+
module AutoConstantInclusion
|
|
4
8
|
MaimaiNet.constants.each do |k|
|
|
5
9
|
cls = MaimaiNet.const_get(k)
|
|
6
10
|
next unless Class === cls && cls < MaimaiNet::Constant
|
|
7
11
|
|
|
8
12
|
define_method k do |key| cls.new(key) end
|
|
13
|
+
private k
|
|
14
|
+
end
|
|
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)
|
|
9
28
|
end
|
|
10
29
|
end
|
|
11
30
|
end
|
data/lib/maimai_net/model.rb
CHANGED
|
@@ -3,31 +3,32 @@ module MaimaiNet
|
|
|
3
3
|
module Model
|
|
4
4
|
require 'maimai_net/model-typing'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
fail TypeError, "#{key} type mismatch, given #{class_str}, expected #{props[key][:class]}" unless props[key][:class] === value
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
args = kwargs.values_at(*keys)
|
|
27
|
-
super(*args)
|
|
6
|
+
class Base < ::Struct
|
|
7
|
+
using GenericComparison
|
|
8
|
+
# @param kwargs [Hash] options are strong-typed based on class definition
|
|
9
|
+
def initialize(**kwargs)
|
|
10
|
+
props = self.class.instance_variable_get(:@_properties)
|
|
11
|
+
keys = props.keys
|
|
12
|
+
optional_keys = props.select do |k, pr|
|
|
13
|
+
Either === pr[:class] &&
|
|
14
|
+
pr[:class].variants.include?(NilClass)
|
|
15
|
+
end.keys
|
|
16
|
+
|
|
17
|
+
missing_keys = keys - (kwargs.keys | optional_keys)
|
|
18
|
+
fail KeyError, "#{missing_keys.join(', ')} is not defined for #{self.class}" unless missing_keys.empty?
|
|
19
|
+
kwargs.each do |key, value|
|
|
20
|
+
fail KeyError, "#{key} is not defined as struct member" unless keys.include?(key)
|
|
21
|
+
class_str = value.respond_to?(:map_class) ? value.map_class : value.class
|
|
22
|
+
fail TypeError, "#{key} type mismatch, given #{class_str}, expected #{props[key][:class]}" unless props[key][:class] === value
|
|
28
23
|
end
|
|
24
|
+
|
|
25
|
+
args = kwargs.values_at(*keys)
|
|
26
|
+
super(*args)
|
|
29
27
|
end
|
|
30
|
-
|
|
28
|
+
|
|
29
|
+
include CoreExt::JSONSupport
|
|
30
|
+
|
|
31
|
+
class << self
|
|
31
32
|
# creates a strong-typed struct data
|
|
32
33
|
# @param opts [Hash{Symbol => Module}]
|
|
33
34
|
# list of struct members along with respective type definition
|
|
@@ -52,7 +53,7 @@ module MaimaiNet
|
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
SongCount = Base
|
|
56
|
+
SongCount = Base.new(achieved: Integer, total: Integer) do
|
|
56
57
|
def to_s
|
|
57
58
|
"#{achieved}/#{total}"
|
|
58
59
|
end
|
|
@@ -60,7 +61,7 @@ module MaimaiNet
|
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
module PlayerCommon
|
|
63
|
-
Info = Base
|
|
64
|
+
Info = Base.new(
|
|
64
65
|
name: String,
|
|
65
66
|
title: String,
|
|
66
67
|
grade: String,
|
|
@@ -68,16 +69,16 @@ module MaimaiNet
|
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
module PlayerData
|
|
71
|
-
Decoration = Base
|
|
72
|
+
Decoration = Base.new(
|
|
72
73
|
icon: URI::Generic,
|
|
73
74
|
)
|
|
74
|
-
ExtendedInfo = Base
|
|
75
|
+
ExtendedInfo = Base.new(
|
|
75
76
|
rating: Integer,
|
|
76
77
|
class_grade: String,
|
|
77
78
|
partner_star_total: Integer,
|
|
78
79
|
)
|
|
79
80
|
|
|
80
|
-
DifficultyStatistic = Base
|
|
81
|
+
DifficultyStatistic = Base.new(
|
|
81
82
|
clears: SongCount,
|
|
82
83
|
ranks: Generic[Hash, Symbol, SongCount],
|
|
83
84
|
dx_ranks: Generic[Hash, Integer, SongCount],
|
|
@@ -85,22 +86,22 @@ module MaimaiNet
|
|
|
85
86
|
sync_flags: Generic[Hash, Symbol, SongCount],
|
|
86
87
|
)
|
|
87
88
|
|
|
88
|
-
InfoPlate = Base
|
|
89
|
+
InfoPlate = Base.new(
|
|
89
90
|
info: PlayerCommon::Info,
|
|
90
91
|
decoration: Decoration,
|
|
91
92
|
extended: ExtendedInfo,
|
|
92
93
|
)
|
|
93
|
-
Lite = Base
|
|
94
|
+
Lite = Base.new(
|
|
94
95
|
name: String,
|
|
95
96
|
rating: Integer,
|
|
96
97
|
)
|
|
97
|
-
Data = Base
|
|
98
|
+
Data = Base.new(
|
|
98
99
|
plate: InfoPlate,
|
|
99
100
|
statistics: Generic[Hash, Symbol, DifficultyStatistic],
|
|
100
101
|
)
|
|
101
102
|
end
|
|
102
103
|
|
|
103
|
-
WebID = Base
|
|
104
|
+
WebID = Base.new(
|
|
104
105
|
item_hash: String,
|
|
105
106
|
item_key: String,
|
|
106
107
|
) do
|
|
@@ -131,9 +132,11 @@ module MaimaiNet
|
|
|
131
132
|
title: String,
|
|
132
133
|
type: String,
|
|
133
134
|
difficulty: Integer,
|
|
135
|
+
variant: Optional[String],
|
|
136
|
+
flags: Optional[Integer],
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
InfoLite = Base
|
|
139
|
+
InfoLite = Base.new(**info_base) do
|
|
137
140
|
def to_info(level_text: '?')
|
|
138
141
|
Info.new(
|
|
139
142
|
web_id: WebID::DUMMY,
|
|
@@ -145,7 +148,7 @@ module MaimaiNet
|
|
|
145
148
|
end
|
|
146
149
|
end
|
|
147
150
|
|
|
148
|
-
Info = Base
|
|
151
|
+
Info = Base.new(
|
|
149
152
|
web_id: WebID,
|
|
150
153
|
**info_base,
|
|
151
154
|
level_text: String,
|
|
@@ -155,7 +158,7 @@ module MaimaiNet
|
|
|
155
158
|
end
|
|
156
159
|
end
|
|
157
160
|
|
|
158
|
-
Song = Base
|
|
161
|
+
Song = Base.new(
|
|
159
162
|
title: String,
|
|
160
163
|
artist: String,
|
|
161
164
|
genre: String,
|
|
@@ -163,18 +166,18 @@ module MaimaiNet
|
|
|
163
166
|
)
|
|
164
167
|
end
|
|
165
168
|
|
|
166
|
-
SongEntry = Base
|
|
169
|
+
SongEntry = Base.new(
|
|
167
170
|
web_id: WebID,
|
|
168
171
|
title: String,
|
|
169
172
|
genre: String,
|
|
170
173
|
)
|
|
171
174
|
|
|
172
|
-
SongFavoriteInfo = Base
|
|
175
|
+
SongFavoriteInfo = Base.new(
|
|
173
176
|
song: SongEntry,
|
|
174
177
|
flag: Boolean,
|
|
175
178
|
)
|
|
176
179
|
|
|
177
|
-
PhotoUpload = Base
|
|
180
|
+
PhotoUpload = Base.new(
|
|
178
181
|
info: Chart::InfoLite,
|
|
179
182
|
url: URI::Generic,
|
|
180
183
|
location: String,
|
|
@@ -182,7 +185,7 @@ module MaimaiNet
|
|
|
182
185
|
)
|
|
183
186
|
|
|
184
187
|
module Result
|
|
185
|
-
Progress = Base
|
|
188
|
+
Progress = Base.new(
|
|
186
189
|
value: Integer,
|
|
187
190
|
max: Integer,
|
|
188
191
|
) do
|
|
@@ -191,18 +194,23 @@ module MaimaiNet
|
|
|
191
194
|
alias inspect to_s
|
|
192
195
|
end
|
|
193
196
|
|
|
194
|
-
RivalInfo = Base
|
|
197
|
+
RivalInfo = Base.new(
|
|
195
198
|
player: PlayerData::Lite,
|
|
196
199
|
score: Float,
|
|
197
200
|
)
|
|
198
201
|
|
|
199
|
-
|
|
202
|
+
PlayerInfo = Base.new(
|
|
203
|
+
player_name: String,
|
|
204
|
+
difficulty: Integer,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
TourMember = Base.new(
|
|
200
208
|
icon: URI::Generic,
|
|
201
209
|
grade: Integer,
|
|
202
210
|
level: Integer,
|
|
203
211
|
)
|
|
204
212
|
|
|
205
|
-
Judgment = Base
|
|
213
|
+
Judgment = Base.new(
|
|
206
214
|
just: Integer,
|
|
207
215
|
perfect: Integer,
|
|
208
216
|
great: Integer,
|
|
@@ -210,33 +218,35 @@ module MaimaiNet
|
|
|
210
218
|
miss: Integer,
|
|
211
219
|
)
|
|
212
220
|
|
|
213
|
-
Offset = Base
|
|
221
|
+
Offset = Base.new(
|
|
214
222
|
early: Integer,
|
|
215
223
|
late: Integer,
|
|
216
224
|
)
|
|
217
225
|
|
|
218
|
-
Challenge = Base
|
|
226
|
+
Challenge = Base.new(
|
|
219
227
|
type: Symbol,
|
|
220
228
|
lives: Progress,
|
|
221
229
|
)
|
|
222
230
|
|
|
223
|
-
ScoreLite = Base
|
|
231
|
+
ScoreLite = Base.new(
|
|
224
232
|
score: Float,
|
|
225
233
|
deluxe_score: Progress,
|
|
226
234
|
grade: Symbol,
|
|
227
235
|
flags: Generic[Array, Symbol],
|
|
236
|
+
position: Optional[Integer],
|
|
228
237
|
)
|
|
229
238
|
|
|
230
|
-
Score = Base
|
|
239
|
+
Score = Base.new(
|
|
231
240
|
score: Float,
|
|
232
241
|
deluxe_score: Progress,
|
|
233
242
|
combo: Progress,
|
|
234
243
|
sync_score: Progress,
|
|
235
244
|
grade: Symbol,
|
|
236
245
|
flags: Generic[Array, Symbol],
|
|
246
|
+
position: Optional[Integer],
|
|
237
247
|
)
|
|
238
248
|
|
|
239
|
-
ReferenceWebID = Base
|
|
249
|
+
ReferenceWebID = Base.new(
|
|
240
250
|
order: Integer,
|
|
241
251
|
time: Time,
|
|
242
252
|
) do
|
|
@@ -251,7 +261,7 @@ module MaimaiNet
|
|
|
251
261
|
alias to_s to_str
|
|
252
262
|
end
|
|
253
263
|
|
|
254
|
-
Track = Base
|
|
264
|
+
Track = Base.new(
|
|
255
265
|
info: Chart::Info,
|
|
256
266
|
score: Either[Score, ScoreLite],
|
|
257
267
|
order: Integer,
|
|
@@ -259,32 +269,35 @@ module MaimaiNet
|
|
|
259
269
|
challenge: Optional[Challenge],
|
|
260
270
|
)
|
|
261
271
|
|
|
262
|
-
TrackReference = Base
|
|
272
|
+
TrackReference = Base.new(
|
|
263
273
|
track: Track,
|
|
264
274
|
ref_web_id: ReferenceWebID,
|
|
265
275
|
)
|
|
266
276
|
|
|
267
|
-
Data = Base
|
|
277
|
+
Data = Base.new(
|
|
268
278
|
track: Track,
|
|
269
279
|
breakdown: Generic[Hash, Symbol, Judgment],
|
|
270
280
|
timing: Offset,
|
|
281
|
+
rating_before: Integer,
|
|
282
|
+
rating_after: Integer,
|
|
271
283
|
members: Generic[Array, TourMember],
|
|
272
284
|
rival: Optional[RivalInfo],
|
|
285
|
+
players: Generic[Array, PlayerInfo],
|
|
273
286
|
)
|
|
274
287
|
end
|
|
275
288
|
|
|
276
289
|
module Record
|
|
277
|
-
History = Base
|
|
290
|
+
History = Base.new(
|
|
278
291
|
play_count: Integer,
|
|
279
292
|
last_played: Time,
|
|
280
293
|
)
|
|
281
294
|
|
|
282
|
-
ScoreOnly = Base
|
|
295
|
+
ScoreOnly = Base.new(
|
|
283
296
|
score: Float,
|
|
284
297
|
grade: Symbol,
|
|
285
298
|
)
|
|
286
299
|
|
|
287
|
-
Score = Base
|
|
300
|
+
Score = Base.new(
|
|
288
301
|
web_id: WebID,
|
|
289
302
|
score: Float,
|
|
290
303
|
deluxe_score: Result::Progress,
|
|
@@ -293,50 +306,50 @@ module MaimaiNet
|
|
|
293
306
|
flags: Generic[Array, Symbol],
|
|
294
307
|
)
|
|
295
308
|
|
|
296
|
-
ChartRecord = Base
|
|
309
|
+
ChartRecord = Base.new(
|
|
297
310
|
info: Chart::Info,
|
|
298
311
|
record: Optional[Score],
|
|
299
312
|
history: Optional[History],
|
|
300
313
|
)
|
|
301
314
|
|
|
302
|
-
InfoCategory = Base
|
|
315
|
+
InfoCategory = Base.new(
|
|
303
316
|
info: Chart::Info,
|
|
304
317
|
score: Optional[Result::ScoreLite],
|
|
305
318
|
)
|
|
306
319
|
|
|
307
|
-
InfoBest = Base
|
|
320
|
+
InfoBest = Base.new(
|
|
308
321
|
info: Chart::Info,
|
|
309
322
|
play_count: Integer,
|
|
310
323
|
)
|
|
311
324
|
|
|
312
|
-
InfoRating = Base
|
|
325
|
+
InfoRating = Base.new(
|
|
313
326
|
info: Chart::Info,
|
|
314
327
|
score: ScoreOnly,
|
|
315
328
|
)
|
|
316
329
|
|
|
317
|
-
Data = Base
|
|
330
|
+
Data = Base.new(
|
|
318
331
|
info: Chart::Song,
|
|
319
332
|
charts: Generic[Hash, Symbol, ChartRecord],
|
|
320
333
|
)
|
|
321
334
|
end
|
|
322
335
|
|
|
323
336
|
module FinaleArchive
|
|
324
|
-
Decoration = Base
|
|
337
|
+
Decoration = Base.new(
|
|
325
338
|
icon: URI::Generic,
|
|
326
339
|
player_frame: URI::Generic,
|
|
327
340
|
nameplate: URI::Generic,
|
|
328
341
|
)
|
|
329
|
-
Currency = Base
|
|
342
|
+
Currency = Base.new(
|
|
330
343
|
amount: Integer, piece: Integer, parts: Integer,
|
|
331
344
|
)
|
|
332
|
-
ExtendedInfo = Base
|
|
345
|
+
ExtendedInfo = Base.new(
|
|
333
346
|
rating: Float, rating_highest: Float,
|
|
334
347
|
region_count: Integer,
|
|
335
348
|
currency: Currency,
|
|
336
349
|
partner_level_total: Integer,
|
|
337
350
|
)
|
|
338
351
|
|
|
339
|
-
DifficultyStatistic = Base
|
|
352
|
+
DifficultyStatistic = Base.new(
|
|
340
353
|
total_score: Integer,
|
|
341
354
|
clears: SongCount,
|
|
342
355
|
ranks: Generic[Hash, Symbol, SongCount],
|
|
@@ -345,7 +358,7 @@ module MaimaiNet
|
|
|
345
358
|
multi_flags: Generic[Hash, Symbol, SongCount],
|
|
346
359
|
)
|
|
347
360
|
|
|
348
|
-
Data = Base
|
|
361
|
+
Data = Base.new(
|
|
349
362
|
info: PlayerCommon::Info,
|
|
350
363
|
decoration: Decoration,
|
|
351
364
|
extended: ExtendedInfo,
|
|
@@ -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,7 +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 =
|
|
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'))
|
|
14
15
|
|
|
15
16
|
dx_container_classes = MaimaiNet::Difficulty::DELUXE.select do |k, v| v.positive? end
|
|
16
17
|
.keys.map do |k| ".playlog_#{k}_container" end
|
|
@@ -20,32 +21,29 @@ module MaimaiNet
|
|
|
20
21
|
result_block = info_block.at_css('.basic_block ~ div:nth-of-type(1)')
|
|
21
22
|
|
|
22
23
|
track_order = get_fullint(strip(header_block.at_css('div.sub_title > span:nth-of-type(1)')))
|
|
23
|
-
play_time =
|
|
24
|
-
strip(header_block.at_css('div.sub_title > span:nth-of-type(2)')) + ' +09:00',
|
|
25
|
-
'%Y/%m/%d %H:%M %z',
|
|
26
|
-
)
|
|
24
|
+
play_time = jst_from(header_block.at_css('div.sub_title > span:nth-of-type(2)'))
|
|
27
25
|
song_name = strip(chart_header_block.children.last)
|
|
28
|
-
chart_level =
|
|
26
|
+
chart_level = get_chart_level_text_from(chart_header_block.at_css('div:nth-of-type(1)'))
|
|
29
27
|
song_jacket = src(result_block.at_css('img.music_img'))
|
|
30
|
-
chart_type
|
|
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
|
|
28
|
+
chart_type = get_chart_type_from(result_block.at_css('img.playlog_music_kind_icon'))
|
|
34
29
|
|
|
35
30
|
result_score = strip(result_block.at_css('.playlog_achievement_txt')).to_f
|
|
36
31
|
result_deluxe_scores = scan_int(strip(result_block.at_css('.playlog_result_innerblock .playlog_score_block div:nth-of-type(1)')))
|
|
37
|
-
result_grade =
|
|
32
|
+
result_grade = subpath_from(result_block.at_css('.playlog_scorerank')).to_sym
|
|
38
33
|
result_flags = result_block.css('.playlog_result_innerblock > img').map do |elm|
|
|
39
|
-
flag =
|
|
34
|
+
flag = subpath_from(elm)
|
|
40
35
|
case flag
|
|
41
|
-
when *MaimaiNet::AchievementFlag::RESULT.values;
|
|
36
|
+
when *MaimaiNet::AchievementFlag::RESULT.values; AchievementFlag(result_key: flag)
|
|
42
37
|
when /_dummy$/; nil
|
|
43
38
|
end
|
|
44
39
|
end.compact
|
|
40
|
+
result_position = result_block.at_css('.playlog_result_innerblock img.playlog_matching_icon')&.yield_self do |elm|
|
|
41
|
+
/^\d+/.match(subpath_from(elm))[0].to_i
|
|
42
|
+
end
|
|
45
43
|
|
|
46
44
|
challenge_info = nil
|
|
47
45
|
result_block.at_css('div:has(> .playlog_life_block)')&.tap do |elm|
|
|
48
|
-
challenge_type =
|
|
46
|
+
challenge_type = subpath_from(elm.at_css('img:nth-of-type(1)')).to_sym
|
|
49
47
|
challenge_lives = scan_int(strip(elm.at_css('.playlog_life_block')))
|
|
50
48
|
|
|
51
49
|
challenge_info = Model::Result::Challenge.new(
|
|
@@ -63,6 +61,7 @@ module MaimaiNet
|
|
|
63
61
|
end.to_h,
|
|
64
62
|
grade: result_grade,
|
|
65
63
|
flags: result_flags.map(&:to_sym),
|
|
64
|
+
position: result_position,
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
score_cls = score_data.key?(:combo) && score_data.key?(:sync_score) ?
|
|
@@ -76,6 +75,7 @@ module MaimaiNet
|
|
|
76
75
|
title: song_name,
|
|
77
76
|
type: (chart_type or -'unknown'),
|
|
78
77
|
difficulty: difficulty.id,
|
|
78
|
+
variant: utage_variant,
|
|
79
79
|
level_text: chart_level,
|
|
80
80
|
),
|
|
81
81
|
score: score_info,
|
data/lib/maimai_net/page.rb
CHANGED
|
@@ -135,22 +135,23 @@ module MaimaiNet
|
|
|
135
135
|
images.map do |elm|
|
|
136
136
|
elm = elm.at_css('> div')
|
|
137
137
|
|
|
138
|
-
chart_type =
|
|
139
|
-
difficulty =
|
|
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('>
|
|
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(
|
|
148
|
-
location: strip(elm.at_css('>
|
|
149
|
-
time:
|
|
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 =
|
|
171
|
-
|
|
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 =
|
|
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 =
|
|
206
|
-
chart_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 =
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
subpath_from(chart_deluxe_grade_elm).to_i :
|
|
231
239
|
0
|
|
232
240
|
|
|
233
241
|
difficulty_data[difficulty.abbrev].tap do |d|
|
|
@@ -265,13 +273,14 @@ module MaimaiNet
|
|
|
265
273
|
|
|
266
274
|
start_anchor = @root.at_css('img.title')
|
|
267
275
|
@otomodachi_block = start_anchor.at_css('~ div#vsUser > :first-child')
|
|
276
|
+
@multiplayer_block = start_anchor.at_css('~ div#matching')
|
|
268
277
|
@score_block = start_anchor.at_css('~ div:not(#vsUser):nth-of-type(1)')
|
|
269
278
|
@breakdown_block = start_anchor.at_css('~ div:not(#vsUser):nth-of-type(2)')
|
|
270
279
|
end
|
|
271
280
|
|
|
272
281
|
helper_method :data do
|
|
273
282
|
result_breakdown = @breakdown_block.css('table.playlog_notes_detail tr:not(:first-child)').map do |row|
|
|
274
|
-
key =
|
|
283
|
+
key = subpath_from(row.at_css('th img')).to_sym
|
|
275
284
|
values = Model::Result::Judgment.new(**Model::Result::Judgment.members.zip(
|
|
276
285
|
row.css('td').map(&method(:strip)).map(&method(:get_int))
|
|
277
286
|
).to_h)
|
|
@@ -281,8 +290,8 @@ module MaimaiNet
|
|
|
281
290
|
result_offset_breakdown = @breakdown_block.css('.playlog_fl_block > div').map do |elm|
|
|
282
291
|
get_int(strip(elm))
|
|
283
292
|
end
|
|
284
|
-
result_rating_after = int(strip(@breakdown_block.at_css('.playlog_rating_detail_block > div:
|
|
285
|
-
result_rating_delta =
|
|
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'))
|
|
286
295
|
result_combos, result_sync_scores = @breakdown_block.css('.playlog_score_block').map do |elm|
|
|
287
296
|
scan_int(strip(elm)).tap do |ary| ary.fill(0, ary.size...2) end
|
|
288
297
|
end
|
|
@@ -310,6 +319,16 @@ module MaimaiNet
|
|
|
310
319
|
)
|
|
311
320
|
end
|
|
312
321
|
|
|
322
|
+
result_players = @multiplayer_block&.css(':has(img[src*="/diff"])').to_a.map do |elm|
|
|
323
|
+
difficulty = get_chart_difficulty_from(elm.at_css('> img:nth-of-type(1)'))
|
|
324
|
+
name = strip(elm.at_css('> div.basic_block:nth-of-type(1)'))
|
|
325
|
+
|
|
326
|
+
Model::Result::PlayerInfo.new(
|
|
327
|
+
player_name: name,
|
|
328
|
+
difficulty: difficulty.id,
|
|
329
|
+
)
|
|
330
|
+
end
|
|
331
|
+
|
|
313
332
|
chart_web_id = Model::WebID.parse(@root.at_css('form[action$="/record/musicDetail/"] input[name=idx]')['value'])
|
|
314
333
|
|
|
315
334
|
Model::Result::Data.new(
|
|
@@ -321,8 +340,11 @@ module MaimaiNet
|
|
|
321
340
|
),
|
|
322
341
|
breakdown: result_breakdown,
|
|
323
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,
|
|
324
345
|
members: result_tour_members,
|
|
325
346
|
rival: result_otomodachi_rival,
|
|
347
|
+
players: result_players,
|
|
326
348
|
)
|
|
327
349
|
end
|
|
328
350
|
end
|
|
@@ -350,25 +372,42 @@ module MaimaiNet
|
|
|
350
372
|
if track_group_blocks.empty? then
|
|
351
373
|
track_segmented_blocks[current_group] = @root.css('div:has(> form[action$="/musicDetail/"] input[name=idx])')
|
|
352
374
|
else
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
375
|
+
%w(.see_through_block .scroll_point).each do |anchor_class|
|
|
376
|
+
anchor_elm = @root.at_css(anchor_class)
|
|
377
|
+
next if anchor_elm.nil?
|
|
378
|
+
|
|
379
|
+
anchor_elm.css('~ div').each do |elm|
|
|
380
|
+
if elm.classes.include? 'screw_block' then
|
|
381
|
+
current_group = elm.content
|
|
382
|
+
next
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
track_segmented_blocks[current_group] ||= Nokogiri::XML::NodeSet.new(@document)
|
|
386
|
+
track_segmented_blocks[current_group] << elm
|
|
357
387
|
end
|
|
358
388
|
|
|
359
|
-
|
|
360
|
-
track_segmented_blocks[current_group] << elm
|
|
389
|
+
break
|
|
361
390
|
end
|
|
362
391
|
end
|
|
363
392
|
|
|
364
393
|
result = track_segmented_blocks.transform_values do |elm_group|
|
|
365
394
|
elm_group.map do |elm|
|
|
395
|
+
chart_info = {}
|
|
396
|
+
|
|
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
|
+
|
|
366
403
|
chart_info = Model::Chart::Info.new(
|
|
367
404
|
web_id: Model::WebID.parse(elm.at_css('input[name=idx][type=hidden]')['value']),
|
|
368
|
-
title: elm.at_css('.music_name_block')
|
|
369
|
-
type:
|
|
370
|
-
difficulty:
|
|
371
|
-
|
|
405
|
+
title: strip(elm.at_css('.music_name_block')),
|
|
406
|
+
type: chart_info.fetch(:type, -'unknown'),
|
|
407
|
+
difficulty: get_chart_difficulty_from(elm.at_css('form > img:nth-of-type(1)')).id,
|
|
408
|
+
variant: chart_info.fetch(:variant, nil),
|
|
409
|
+
flags: chart_info[:flags],
|
|
410
|
+
level_text: get_chart_level_text_from(elm.at_css('.music_lv_block')),
|
|
372
411
|
)
|
|
373
412
|
|
|
374
413
|
score_info = nil
|
|
@@ -382,7 +421,7 @@ module MaimaiNet
|
|
|
382
421
|
)
|
|
383
422
|
else
|
|
384
423
|
# ratingTargetMusic page
|
|
385
|
-
best_grade =
|
|
424
|
+
best_grade = subpath_from(elm.at_css('.music_score_block:nth-of-type(1) > div > img')).to_sym
|
|
386
425
|
score_info = Model::Result::ScoreOnly.new(
|
|
387
426
|
score: strip(elm.at_css('.music_score_block:nth-of-type(1)')).to_f,
|
|
388
427
|
grade: best_grade,
|
|
@@ -398,7 +437,7 @@ module MaimaiNet
|
|
|
398
437
|
if !elm.at_css('.music_score_block').nil? then
|
|
399
438
|
best_deluxe_score = scan_int(strip(elm.at_css('.music_score_block:nth-of-type(2)')))
|
|
400
439
|
flairs = elm.css('.music_score_block ~ img:has(~ .clearfix)').map do |img|
|
|
401
|
-
|
|
440
|
+
subpath_from(img).to_sym.yield_self do |value|
|
|
402
441
|
value == :back ? nil : value
|
|
403
442
|
end
|
|
404
443
|
end
|
data/lib/maimai_net/refines.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
module MaimaiNet
|
|
2
|
-
#
|
|
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
|
-
include CoreExt::
|
|
9
|
+
include CoreExt::AutoConstantInclusion
|
|
10
|
+
end
|
|
11
|
+
refine BasicObject do
|
|
12
|
+
include CoreExt::AutoConstantInclusion
|
|
6
13
|
end
|
|
7
14
|
end
|
|
8
15
|
|
|
9
|
-
#
|
|
16
|
+
# grants any object an ability to convert itself into a single-element array.
|
|
17
|
+
# unless it's an array already.
|
|
10
18
|
module ObjectAsArray
|
|
11
19
|
refine Object do
|
|
12
20
|
def as_array
|
data/lib/maimai_net/version.rb
CHANGED
data/lib/maimai_net.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2025-10-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -115,15 +115,8 @@ executables: []
|
|
|
115
115
|
extensions: []
|
|
116
116
|
extra_rdoc_files: []
|
|
117
117
|
files:
|
|
118
|
-
- ".github/workflows/gem.yml"
|
|
119
|
-
- ".gitignore"
|
|
120
|
-
- ".rspec"
|
|
121
|
-
- Gemfile
|
|
122
118
|
- LICENSE
|
|
123
119
|
- README.md
|
|
124
|
-
- Rakefile
|
|
125
|
-
- bin/console
|
|
126
|
-
- bin/setup
|
|
127
120
|
- lib/maimai_net.rb
|
|
128
121
|
- lib/maimai_net/client.rb
|
|
129
122
|
- lib/maimai_net/constants.rb
|
|
@@ -143,7 +136,6 @@ files:
|
|
|
143
136
|
- lib/maimai_net/user_option.rb
|
|
144
137
|
- lib/maimai_net/util.rb
|
|
145
138
|
- lib/maimai_net/version.rb
|
|
146
|
-
- maimai-net.gemspec
|
|
147
139
|
homepage: https://github.com/ReiFan49/rb.maimai-net
|
|
148
140
|
licenses:
|
|
149
141
|
- BSD-3-Clause-Clear
|
|
@@ -165,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
165
157
|
- !ruby/object:Gem::Version
|
|
166
158
|
version: '0'
|
|
167
159
|
requirements: []
|
|
168
|
-
rubygems_version: 3.
|
|
160
|
+
rubygems_version: 3.2.33
|
|
169
161
|
signing_key:
|
|
170
162
|
specification_version: 4
|
|
171
163
|
summary: Parses maimai-net into readable data.
|
data/.github/workflows/gem.yml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
name: Ruby Gem
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- v*
|
|
7
|
-
|
|
8
|
-
concurrency:
|
|
9
|
-
group: ${{ github.ref_name }}
|
|
10
|
-
cancel-in-progress: true
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
build:
|
|
14
|
-
name: Build + Publish
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
permissions:
|
|
17
|
-
contents: read
|
|
18
|
-
packages: write
|
|
19
|
-
id-token: write
|
|
20
|
-
env:
|
|
21
|
-
RUBY_VERSION: '2.7'
|
|
22
|
-
steps:
|
|
23
|
-
- uses: actions/checkout@v4
|
|
24
|
-
- name: Set up Ruby ${{ env.RUBY_VERSION }}
|
|
25
|
-
uses: ruby/setup-ruby@v1
|
|
26
|
-
with:
|
|
27
|
-
bundler-cache: true
|
|
28
|
-
ruby-version: ${{ env.RUBY_VERSION }}
|
|
29
|
-
# Publish
|
|
30
|
-
- name: Publish to GPR
|
|
31
|
-
run: |
|
|
32
|
-
mkdir -p $HOME/.gem
|
|
33
|
-
touch $HOME/.gem/credentials
|
|
34
|
-
chmod 0600 $HOME/.gem/credentials
|
|
35
|
-
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
36
|
-
gem build *.gemspec
|
|
37
|
-
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
|
38
|
-
env:
|
|
39
|
-
GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
|
|
40
|
-
OWNER: ${{ github.repository_owner }}
|
|
41
|
-
- name: Publish to RubyGems
|
|
42
|
-
if: ${{ false }}
|
|
43
|
-
run: |
|
|
44
|
-
mkdir -p $HOME/.gem
|
|
45
|
-
touch $HOME/.gem/credentials
|
|
46
|
-
chmod 0600 $HOME/.gem/credentials
|
|
47
|
-
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
48
|
-
gem build *.gemspec
|
|
49
|
-
gem push *.gem
|
|
50
|
-
env:
|
|
51
|
-
GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
|
|
52
|
-
- name: Publish to RubyGems (Trusted)
|
|
53
|
-
uses: rubygems/release-gem@v1
|
data/.gitignore
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
# Created by https://www.toptal.com/developers/gitignore/api/ruby,rubymine+all,linux
|
|
2
|
-
# Edit at https://www.toptal.com/developers/gitignore?templates=ruby,rubymine+all,linux
|
|
3
|
-
|
|
4
|
-
### Linux ###
|
|
5
|
-
*~
|
|
6
|
-
.*.swp
|
|
7
|
-
|
|
8
|
-
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
9
|
-
.fuse_hidden*
|
|
10
|
-
|
|
11
|
-
# KDE directory preferences
|
|
12
|
-
.directory
|
|
13
|
-
|
|
14
|
-
# Linux trash folder which might appear on any partition or disk
|
|
15
|
-
.Trash-*
|
|
16
|
-
|
|
17
|
-
# .nfs files are created when an open file is removed but is still being accessed
|
|
18
|
-
.nfs*
|
|
19
|
-
|
|
20
|
-
### Ruby ###
|
|
21
|
-
*.gem
|
|
22
|
-
*.rbc
|
|
23
|
-
/.config
|
|
24
|
-
/coverage/
|
|
25
|
-
/InstalledFiles
|
|
26
|
-
/pkg/
|
|
27
|
-
/spec/reports/
|
|
28
|
-
/spec/examples.txt
|
|
29
|
-
/test/tmp/
|
|
30
|
-
/test/version_tmp/
|
|
31
|
-
/tmp/
|
|
32
|
-
|
|
33
|
-
# Used by dotenv library to load environment variables.
|
|
34
|
-
# .env
|
|
35
|
-
|
|
36
|
-
# Ignore Byebug command history file.
|
|
37
|
-
.byebug_history
|
|
38
|
-
|
|
39
|
-
## Specific to RubyMotion:
|
|
40
|
-
.dat*
|
|
41
|
-
.repl_history
|
|
42
|
-
build/
|
|
43
|
-
*.bridgesupport
|
|
44
|
-
build-iPhoneOS/
|
|
45
|
-
build-iPhoneSimulator/
|
|
46
|
-
|
|
47
|
-
## Specific to RubyMotion (use of CocoaPods):
|
|
48
|
-
#
|
|
49
|
-
# We recommend against adding the Pods directory to your .gitignore. However
|
|
50
|
-
# you should judge for yourself, the pros and cons are mentioned at:
|
|
51
|
-
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
52
|
-
# vendor/Pods/
|
|
53
|
-
|
|
54
|
-
## Documentation cache and generated files:
|
|
55
|
-
/.yardoc/
|
|
56
|
-
/_yardoc/
|
|
57
|
-
/doc/
|
|
58
|
-
/rdoc/
|
|
59
|
-
|
|
60
|
-
## Environment normalization:
|
|
61
|
-
/.bundle/
|
|
62
|
-
/vendor/bundle
|
|
63
|
-
/lib/bundler/man/
|
|
64
|
-
|
|
65
|
-
# for a library or gem, you might want to ignore these files since the code is
|
|
66
|
-
# intended to run in multiple environments; otherwise, check them in:
|
|
67
|
-
Gemfile.lock
|
|
68
|
-
.ruby-version
|
|
69
|
-
.ruby-gemset
|
|
70
|
-
|
|
71
|
-
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
72
|
-
.rvmrc
|
|
73
|
-
|
|
74
|
-
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
|
75
|
-
# .rubocop-https?--*
|
|
76
|
-
|
|
77
|
-
### RubyMine+all ###
|
|
78
|
-
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
|
79
|
-
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
80
|
-
|
|
81
|
-
# User-specific stuff
|
|
82
|
-
.idea/**/workspace.xml
|
|
83
|
-
.idea/**/tasks.xml
|
|
84
|
-
.idea/**/usage.statistics.xml
|
|
85
|
-
.idea/**/dictionaries
|
|
86
|
-
.idea/**/shelf
|
|
87
|
-
|
|
88
|
-
# AWS User-specific
|
|
89
|
-
.idea/**/aws.xml
|
|
90
|
-
|
|
91
|
-
# Generated files
|
|
92
|
-
.idea/**/contentModel.xml
|
|
93
|
-
|
|
94
|
-
# Sensitive or high-churn files
|
|
95
|
-
.idea/**/dataSources/
|
|
96
|
-
.idea/**/dataSources.ids
|
|
97
|
-
.idea/**/dataSources.local.xml
|
|
98
|
-
.idea/**/sqlDataSources.xml
|
|
99
|
-
.idea/**/dynamic.xml
|
|
100
|
-
.idea/**/uiDesigner.xml
|
|
101
|
-
.idea/**/dbnavigator.xml
|
|
102
|
-
|
|
103
|
-
# Gradle
|
|
104
|
-
.idea/**/gradle.xml
|
|
105
|
-
.idea/**/libraries
|
|
106
|
-
|
|
107
|
-
# Gradle and Maven with auto-import
|
|
108
|
-
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
109
|
-
# since they will be recreated, and may cause churn. Uncomment if using
|
|
110
|
-
# auto-import.
|
|
111
|
-
# .idea/artifacts
|
|
112
|
-
# .idea/compiler.xml
|
|
113
|
-
# .idea/jarRepositories.xml
|
|
114
|
-
# .idea/modules.xml
|
|
115
|
-
# .idea/*.iml
|
|
116
|
-
# .idea/modules
|
|
117
|
-
# *.iml
|
|
118
|
-
# *.ipr
|
|
119
|
-
|
|
120
|
-
# CMake
|
|
121
|
-
cmake-build-*/
|
|
122
|
-
|
|
123
|
-
# Mongo Explorer plugin
|
|
124
|
-
.idea/**/mongoSettings.xml
|
|
125
|
-
|
|
126
|
-
# File-based project format
|
|
127
|
-
*.iws
|
|
128
|
-
|
|
129
|
-
# IntelliJ
|
|
130
|
-
out/
|
|
131
|
-
|
|
132
|
-
# mpeltonen/sbt-idea plugin
|
|
133
|
-
.idea_modules/
|
|
134
|
-
|
|
135
|
-
# JIRA plugin
|
|
136
|
-
atlassian-ide-plugin.xml
|
|
137
|
-
|
|
138
|
-
# Cursive Clojure plugin
|
|
139
|
-
.idea/replstate.xml
|
|
140
|
-
|
|
141
|
-
# SonarLint plugin
|
|
142
|
-
.idea/sonarlint/
|
|
143
|
-
|
|
144
|
-
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
145
|
-
com_crashlytics_export_strings.xml
|
|
146
|
-
crashlytics.properties
|
|
147
|
-
crashlytics-build.properties
|
|
148
|
-
fabric.properties
|
|
149
|
-
|
|
150
|
-
# Editor-based Rest Client
|
|
151
|
-
.idea/httpRequests
|
|
152
|
-
|
|
153
|
-
# Android studio 3.1+ serialized cache file
|
|
154
|
-
.idea/caches/build_file_checksums.ser
|
|
155
|
-
|
|
156
|
-
### RubyMine+all Patch ###
|
|
157
|
-
# Ignore everything but code style settings and run configurations
|
|
158
|
-
# that are supposed to be shared within teams.
|
|
159
|
-
|
|
160
|
-
.idea/*
|
|
161
|
-
|
|
162
|
-
!.idea/codeStyles
|
|
163
|
-
!.idea/runConfigurations
|
|
164
|
-
|
|
165
|
-
# End of https://www.toptal.com/developers/gitignore/api/ruby,rubymine+all,linux
|
data/.rspec
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
data/bin/console
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
begin; require 'byebug'; rescue LoadError; end
|
|
5
|
-
require "maimai_net"
|
|
6
|
-
|
|
7
|
-
console = nil
|
|
8
|
-
begin; require 'pry'
|
|
9
|
-
rescue LoadError; require 'irb'; console = :irb
|
|
10
|
-
else; console = :pry
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
case console
|
|
14
|
-
when :pry; Pry.start
|
|
15
|
-
else; IRB.start(__FILE__)
|
|
16
|
-
end
|
data/bin/setup
DELETED
data/maimai-net.gemspec
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
require_relative 'lib/maimai_net/version'
|
|
2
|
-
|
|
3
|
-
Gem::Specification.new do |spec|
|
|
4
|
-
spec.name = "maimai_net"
|
|
5
|
-
spec.version = MaimaiNet::VERSION
|
|
6
|
-
spec.authors = [
|
|
7
|
-
%(Rei Hakurei),
|
|
8
|
-
]
|
|
9
|
-
spec.email = %w(contact@bloom-juery.net)
|
|
10
|
-
|
|
11
|
-
spec.summary = %q(Parses maimai-net into readable data.)
|
|
12
|
-
spec.homepage = 'https://github.com/ReiFan49/rb.maimai-net'
|
|
13
|
-
spec.license = 'BSD-3-Clause-Clear'
|
|
14
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
|
|
15
|
-
|
|
16
|
-
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
|
17
|
-
|
|
18
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
|
20
|
-
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
|
21
|
-
|
|
22
|
-
# Specify which files should be added to the gem when it is released.
|
|
23
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
24
|
-
begin
|
|
25
|
-
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
26
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
27
|
-
end
|
|
28
|
-
rescue Errno::ENOENT
|
|
29
|
-
spec.files = Dir.glob('lib/**/*.rb', base: File.expand_path('..', __FILE__))
|
|
30
|
-
end
|
|
31
|
-
spec.bindir = "exe"
|
|
32
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
33
|
-
spec.require_paths = ["lib"]
|
|
34
|
-
|
|
35
|
-
spec.add_development_dependency 'rake'
|
|
36
|
-
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
37
|
-
spec.add_development_dependency 'byebug'
|
|
38
|
-
|
|
39
|
-
spec.add_runtime_dependency 'nokogiri', '~> 1.13'
|
|
40
|
-
spec.add_runtime_dependency 'faraday', '~> 2.0'
|
|
41
|
-
spec.add_runtime_dependency 'faraday-follow_redirects'
|
|
42
|
-
|
|
43
|
-
spec.add_runtime_dependency 'http-cookie', '~> 1.0.0'
|
|
44
|
-
end
|