vk_music 2.2.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +6 -4
- data/Gemfile.lock +66 -60
- data/README.md +87 -81
- data/bin/console +10 -14
- data/lib/vk_music.rb +18 -18
- data/lib/vk_music/audio.rb +68 -115
- data/lib/vk_music/client.rb +357 -497
- data/lib/vk_music/constants.rb +3 -20
- data/lib/vk_music/exceptions.rb +1 -12
- data/lib/vk_music/link_decoder.rb +2 -8
- data/lib/vk_music/playlist.rb +30 -41
- data/lib/vk_music/utility.rb +5 -63
- data/lib/vk_music/utility/log.rb +51 -0
- data/lib/vk_music/version.rb +5 -7
- metadata +3 -2
data/lib/vk_music/client.rb
CHANGED
@@ -1,298 +1,139 @@
|
|
1
|
-
##
|
2
|
-
# @!macro [new] options_hash_param
|
3
|
-
# @param options [Hash] hash with options.
|
4
|
-
#
|
5
|
-
# @!macro [new] playlist_return
|
6
|
-
# @return [Playlist] playlist with audios. Possibly empty.
|
7
|
-
# Possibly contains audios without download URL.
|
8
|
-
|
9
|
-
##
|
10
|
-
# Main module.
|
11
1
|
module VkMusic
|
12
|
-
|
13
2
|
##
|
14
3
|
# Main class with all the interface.
|
15
|
-
# To start working with VK audios firstly create new client with +Client.new+.
|
16
4
|
class Client
|
17
|
-
|
18
5
|
##
|
19
6
|
# @return [Integer] ID of client.
|
20
7
|
attr_reader :id
|
21
|
-
|
22
8
|
##
|
23
9
|
# @return [String] name of client.
|
24
10
|
attr_reader :name
|
25
|
-
|
26
|
-
@
|
11
|
+
##
|
12
|
+
# @return [Mechanize] client used to access web pages.
|
13
|
+
attr_reader :agent
|
27
14
|
|
28
15
|
##
|
29
16
|
# Create new client and login.
|
30
|
-
#
|
31
|
-
# @
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
# @option options [String] :user_agent (Constants::DEFAULT_USER_AGENT)
|
36
|
-
def initialize(options = {})
|
37
|
-
# Arguments check
|
38
|
-
raise ArgumentError, "Options hash must be provided", caller unless options.class == Hash
|
39
|
-
raise ArgumentError, "Username is not provided", caller unless options.has_key?(:username)
|
40
|
-
raise ArgumentError, "Password is not provided", caller unless options.has_key?(:password)
|
41
|
-
|
17
|
+
# @param username [String] usually telephone number or email.
|
18
|
+
# @param password [String]
|
19
|
+
# @param user_agent [String]
|
20
|
+
def initialize(username: "", password: "", user_agent: Constants::DEFAULT_USER_AGENT)
|
21
|
+
raise ArgumentError if username.empty? || password.empty?
|
42
22
|
# Setting up client
|
43
23
|
@agent = Mechanize.new
|
44
|
-
@agent.user_agent =
|
45
|
-
login(
|
24
|
+
@agent.user_agent = user_agent
|
25
|
+
login(username, password)
|
46
26
|
end
|
47
27
|
|
48
28
|
##
|
49
29
|
#@!group Loading audios
|
50
|
-
|
30
|
+
|
51
31
|
##
|
52
|
-
# @!macro [new] find__options
|
53
|
-
# @option options [Symbol] :type (:audio) what to search for (you can find available values for this option above).
|
54
|
-
#
|
55
32
|
# Search for audio or playlist.
|
56
|
-
#
|
57
|
-
# @note some audios and playlists might be removed from search.
|
58
|
-
#
|
59
|
-
# @todo search in group audios.
|
60
|
-
#
|
61
33
|
# Possible values of +type+ option:
|
62
34
|
# * +:audio+ - search for audios. Returns up to 50 audios.
|
63
|
-
# * +:playlist+ - search for playlists. Returns up to 6 playlists *without* audios (Loaded with +up_to: 0+ option).
|
35
|
+
# * +:playlist+ - search for playlists. Returns up to 6 playlists *without* audios (Loaded with +up_to: 0+ option).
|
64
36
|
# You can get all the audios of selected playlist calling {Client#playlist} method with gained info.
|
65
|
-
#
|
66
|
-
# @
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
# @option options [String] :query string to search for.
|
74
|
-
# @macro find__options
|
75
|
-
#
|
76
|
-
# @return [Array<Audio>, Array<Playlist>] array with audios or playlists matching given string.
|
77
|
-
# Possibly empty. Possibly contains audios or playlists without download URL.
|
78
|
-
def find(*args)
|
79
|
-
begin
|
80
|
-
case
|
81
|
-
when (args.size == 1 && String === args[0]) ||
|
82
|
-
(args.size == 2 && String === args[0] && Hash === args[1])
|
83
|
-
options = args[1] || {}
|
84
|
-
query = args[0]
|
85
|
-
when args.size == 1 && Hash === args[0]
|
86
|
-
options = args[0]
|
87
|
-
query = options[:query].to_s
|
88
|
-
else
|
89
|
-
raise
|
90
|
-
end
|
91
|
-
rescue
|
92
|
-
raise ArgumentError, "Bad arguments", caller
|
93
|
-
end
|
94
|
-
|
95
|
-
options[:type] ||= :audio
|
96
|
-
|
37
|
+
# @note some audios and playlists might be removed from search.
|
38
|
+
# @todo search in group audios.
|
39
|
+
# @param query [String] search query.
|
40
|
+
# @param type [Symbol] what to search for.
|
41
|
+
# @return [Array<Audio>, Array<Playlist>] array with audios or playlists
|
42
|
+
# matching given string.
|
43
|
+
def find(query = "", type: :audio)
|
44
|
+
raise ArgumentError if query.empty?
|
97
45
|
uri = URI(Constants::URL::VK[:audios])
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
raise ArgumentError, "Bad :type option", caller
|
46
|
+
case type
|
47
|
+
when :audio
|
48
|
+
uri.query = Utility.hash_to_params({ "act" => "search", "q" => query })
|
49
|
+
audios_from_page(uri)
|
50
|
+
when :playlist
|
51
|
+
uri.query = Utility.hash_to_params({ "q" => query, "tab" => "global" })
|
52
|
+
urls = playlist_urls_from_page(uri)
|
53
|
+
urls.map { |url| playlist(url: url, up_to: 0, use_web: false) }
|
54
|
+
else
|
55
|
+
raise ArgumentError
|
109
56
|
end
|
110
57
|
end
|
111
|
-
|
112
|
-
|
58
|
+
alias_method :search, :find
|
59
|
+
|
113
60
|
##
|
114
|
-
# @!macro [new] pl__options
|
115
|
-
# @option options [Integer] :up_to (MAXIMUM_PLAYLIST_SIZE) maximum amount of audios to load.
|
116
|
-
# If 0, no audios would be loaded (Just information about playlist).
|
117
|
-
# If less than 0, will load whole playlist.
|
118
|
-
# @option options [Boolean] :with_url (true) makes all the audios have download URLs,
|
119
|
-
# but every 100 audios will cost one more request. You can reduce amount of requests using option +up_to+.
|
120
|
-
# Otherwise audio download URL would be accessable only with {Client#from_id}.
|
121
|
-
# Main advantage of disabling URLs is the fact that 2000 audios will be loaded per request,
|
122
|
-
# which is 20 times more effecient.
|
123
|
-
#
|
124
61
|
# Get VK playlist.
|
125
|
-
#
|
126
|
-
# @
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
131
|
-
# @
|
132
|
-
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
# @
|
140
|
-
def playlist(
|
62
|
+
# Specify either +url+ or +(owner_id,playlist_id,access_hash)+.
|
63
|
+
# @note since updating URLs can take a lot of time in this case, you have to
|
64
|
+
# do it manually with {Client#update_urls}.
|
65
|
+
# @param url [String, nil] playlist URL.
|
66
|
+
# @param owner_id [Integer, nil] playlist owner ID.
|
67
|
+
# @param playlist_id [Integer, nil] ID of the playlist.
|
68
|
+
# @param access_hash [String, nil] access hash to playlist. Might not exist.
|
69
|
+
# @param up_to [Integer] maximum amount of audios to load.
|
70
|
+
# If 0, no audios would be loaded (Just information about playlist).
|
71
|
+
# If less than 0, will load whole playlist.
|
72
|
+
# @param use_web [Boolean, nil] if +true+ web version of pages sill be used, if +false+
|
73
|
+
# JSON will be used (latter is faster, but using web allow to get URLs instantly).
|
74
|
+
# If +nil+ mixed algorithm will be used: if provided +up_to+ value is less than 200
|
75
|
+
# web will be used.
|
76
|
+
# @return [Playlist]
|
77
|
+
def playlist(url: nil, owner_id: nil, playlist_id: nil, access_hash: nil, up_to: Constants::MAXIMUM_PLAYLIST_SIZE, use_web: nil)
|
141
78
|
begin
|
142
|
-
|
143
|
-
when (args.size == 1 && String === args[0]) ||
|
144
|
-
(args.size == 2 && String === args[0] && Hash === args[1])
|
145
|
-
options = args[1] || {}
|
146
|
-
owner_id, playlist_id, access_hash = args[0].to_s.match(Constants::Regex::VK_PLAYLIST_URL_POSTFIX).captures
|
147
|
-
when args.size == 1 && Hash === args[0]
|
148
|
-
options = args[0]
|
149
|
-
owner_id, playlist_id, access_hash = options[:owner_id].to_i, options[:playlist_id].to_i, options[:access_hash].to_s
|
150
|
-
else
|
151
|
-
raise
|
152
|
-
end
|
79
|
+
owner_id, playlist_id, access_hash = url.match(Constants::Regex::VK_PLAYLIST_URL_POSTFIX).captures if url
|
153
80
|
rescue
|
154
|
-
raise
|
81
|
+
raise Exceptions::ParseError
|
155
82
|
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
if options[:with_url]
|
161
|
-
playlist__web(owner_id, playlist_id, access_hash, options)
|
83
|
+
raise ArgumentError unless owner_id && playlist_id
|
84
|
+
use_web = up_to > 200 if use_web.nil?
|
85
|
+
if use_web
|
86
|
+
playlist_web(owner_id, playlist_id, access_hash, up_to: up_to)
|
162
87
|
else
|
163
|
-
|
88
|
+
playlist_json(owner_id, playlist_id, access_hash, up_to: up_to)
|
164
89
|
end
|
165
90
|
end
|
166
|
-
|
91
|
+
|
167
92
|
##
|
168
|
-
# @!macro [new] ua__options
|
169
|
-
# @option options [Integer] :up_to (MAXIMUM_PLAYLIST_SIZE) maximum amount of audios to load.
|
170
|
-
# If 0, no audios would be loaded (Just information about playlist).
|
171
|
-
# If less than 0, will load whole playlist.
|
172
|
-
#
|
173
93
|
# Get user or group audios.
|
174
|
-
#
|
175
|
-
# @note
|
176
|
-
#
|
177
|
-
# @
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
# @
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
#
|
187
|
-
# @macro playlist_return
|
188
|
-
def audios(*args)
|
189
|
-
begin
|
190
|
-
case
|
191
|
-
when (args.size == 1 && String === args[0] ) ||
|
192
|
-
(args.size == 2 && String === args[0] && Hash === args[1])
|
193
|
-
owner_id = page_id(args[0].to_s)
|
194
|
-
options = args[1] || {}
|
195
|
-
when args.size == 1 && Hash === args[0]
|
196
|
-
owner_id = args[0][:owner_id].to_i
|
197
|
-
options = args[0]
|
198
|
-
else
|
199
|
-
raise
|
200
|
-
end
|
201
|
-
rescue
|
202
|
-
raise ArgumentError, "Bad arguments", caller
|
203
|
-
end
|
204
|
-
|
205
|
-
options[:up_to] ||= Constants::MAXIMUM_PLAYLIST_SIZE
|
206
|
-
|
207
|
-
playlist__json(owner_id, -1, nil, options)
|
94
|
+
# Specify either +url+ or +owner_id+.
|
95
|
+
# @note since updating URLs can take a lot of time in this case, you have to
|
96
|
+
# do it manually with {Client#update_urls}.
|
97
|
+
# @param url [String, nil]
|
98
|
+
# @param owner_id [Integer, nil] numerical ID of owner.
|
99
|
+
# @param up_to [Integer] maximum amount of audios to load.
|
100
|
+
# If 0, no audios would be loaded (Just information about playlist).
|
101
|
+
# If less than 0, will load whole playlist.
|
102
|
+
# @return [Playlist]
|
103
|
+
def audios(url: nil, owner_id: nil, up_to: Constants::MAXIMUM_PLAYLIST_SIZE)
|
104
|
+
owner_id = page_id(url) if url
|
105
|
+
playlist_json(owner_id, -1, nil, up_to: up_to)
|
208
106
|
end
|
209
107
|
|
210
108
|
##
|
211
|
-
# @!macro [new] wall__up_to_option
|
212
|
-
# @option up_to [Integer] :up_to (50) maximum amount of audios to load from wall.
|
213
|
-
#
|
214
|
-
# @!macro [new] wall__with_url_option
|
215
|
-
# @option options [Boolean] :with_url (true) automatically use {Client#from_id} to get download URLs.
|
216
|
-
#
|
217
109
|
# Get audios on wall of user or group starting with given post.
|
218
|
-
#
|
110
|
+
# Specify either +url+ or +(owner_id,post_id)+.
|
219
111
|
# @note this method is only able to load up to 91 audios from wall.
|
220
|
-
#
|
221
|
-
# @
|
222
|
-
#
|
223
|
-
# @
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
# @macro wall__with_url_option
|
229
|
-
#
|
230
|
-
# @overload wall(options)
|
231
|
-
# Load audios starting from some exact post.
|
232
|
-
# @macro options_hash_param
|
233
|
-
# @option options [Integer] :owner_id numerical ID of wall owner.
|
234
|
-
# @option options [Integer] :post_id numerical ID of post.
|
235
|
-
# @macro wall__up_to_option
|
236
|
-
# @macro wall__with_url_option
|
237
|
-
#
|
238
|
-
# @return [Array<Audio>] array of audios from wall. Possibly empty.
|
239
|
-
def wall(*args)
|
240
|
-
begin
|
241
|
-
case
|
242
|
-
when (args.size == 1 && args[0].class == String) ||
|
243
|
-
(args.size == 2 && args[0].class == String && args[1].class == Hash)
|
244
|
-
url = args[0].to_s
|
245
|
-
owner_id = page_id(url)
|
246
|
-
post_id = last_post_id(owner_id)
|
247
|
-
options = args[1] || {}
|
248
|
-
return [] if post_id.nil?
|
249
|
-
when args.length == 1 && Hash === args[0]
|
250
|
-
options = args[0]
|
251
|
-
owner_id = options[:owner_id].to_i
|
252
|
-
post_id = options[:post_id].to_i
|
253
|
-
else
|
254
|
-
raise
|
255
|
-
end
|
256
|
-
rescue Exceptions::ParseError => error
|
257
|
-
raise Exceptions::ParseError, "Unable to get last post id. Error: #{error.message}", caller
|
258
|
-
rescue
|
259
|
-
raise ArgumentError, "Bad arguments", caller
|
112
|
+
# @param url [String] URL to post.
|
113
|
+
# @param owner_id [Integer] numerical ID of wall owner.
|
114
|
+
# @param post_id [Integer] numerical ID of post.
|
115
|
+
# @return [Array<Audio>] array of audios from wall.
|
116
|
+
def wall(url: nil, owner_id: nil, post_id: nil, up_to: 91, with_url: false)
|
117
|
+
if url
|
118
|
+
owner_id = page_id(url)
|
119
|
+
post_id = last_post_id(owner_id: owner_id)
|
260
120
|
end
|
261
|
-
|
262
|
-
options[:up_to] ||= 50
|
263
|
-
options[:with_url] = true if options[:with_url].nil?
|
264
|
-
|
265
|
-
wall__json(owner_id, post_id, options)
|
121
|
+
wall_json(owner_id, post_id, up_to: up_to, with_url: with_url)
|
266
122
|
end
|
267
123
|
|
268
124
|
##
|
269
125
|
# Get audios attached to post.
|
270
|
-
#
|
271
|
-
# @
|
272
|
-
#
|
273
|
-
# @
|
274
|
-
#
|
275
|
-
#
|
276
|
-
|
277
|
-
# @macro options_hash_param
|
278
|
-
# @option options [Integer] :owner_id numerical ID of wall owner.
|
279
|
-
# @option options [Integer] :post_id numerical ID of post.
|
280
|
-
#
|
281
|
-
# @return [Array<Audio>] array of audios. Possibly without download URL.
|
282
|
-
def post(arg)
|
126
|
+
# Specify either +url+ or +(owner_id,post_id)+.
|
127
|
+
# @param url [String] URL to post.
|
128
|
+
# @param owner_id [Integer] numerical ID of wall owner.
|
129
|
+
# @param post_id [Integer] numerical ID of post.
|
130
|
+
# @return [Array<Audio>] array of audios attached to post. Most of audios will
|
131
|
+
# already have download URLs, but there might be audios which can't be resolved.
|
132
|
+
def post(url: nil, owner_id: nil, post_id: nil)
|
283
133
|
begin
|
284
|
-
|
285
|
-
when String
|
286
|
-
owner_id, post_id = arg.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures
|
287
|
-
when Hash
|
288
|
-
options = arg
|
289
|
-
owner_id = options[:owner_id].to_i
|
290
|
-
post_id = options[:post_id].to_i
|
291
|
-
else
|
292
|
-
raise
|
293
|
-
end
|
134
|
+
owner_id, post_id = url.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures if url
|
294
135
|
rescue
|
295
|
-
raise
|
136
|
+
raise Exceptions::ParseError
|
296
137
|
end
|
297
138
|
|
298
139
|
attached = attached_audios(owner_id: owner_id, post_id: post_id)
|
@@ -303,59 +144,68 @@ module VkMusic
|
|
303
144
|
wall.find { |a| a.artist == a_empty.artist && a.title == a_empty.title } || a_empty
|
304
145
|
end
|
305
146
|
loaded_audios = from_id(no_link)
|
306
|
-
|
147
|
+
|
307
148
|
loaded_audios.map.with_index { |el, i| el || no_link[i] }
|
308
149
|
end
|
309
150
|
|
310
151
|
##
|
311
152
|
# Get audios with download URLs by their IDs and secrets.
|
312
|
-
#
|
313
153
|
# @param args [Array<Audio, Array<(owner_id, audio_id, secret_1, secret_2)>, "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
|
314
|
-
#
|
315
154
|
# @return [Array<Audio, nil>] array of: audio with download URLs or audio
|
316
|
-
#
|
155
|
+
# without URL if wasn't able to get it for audio or +nil+ if
|
317
156
|
# matching element can't be retrieved for array or string.
|
318
|
-
def
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
raise
|
330
|
-
end
|
157
|
+
def get_urls(args)
|
158
|
+
args_formatted = args.map do |el|
|
159
|
+
case el
|
160
|
+
when Array
|
161
|
+
el.join("_")
|
162
|
+
when Audio
|
163
|
+
el.full_id
|
164
|
+
when String
|
165
|
+
el # Do not change
|
166
|
+
else
|
167
|
+
raise ArgumentError
|
331
168
|
end
|
332
|
-
rescue
|
333
|
-
raise ArgumentError, "Bad arguments", caller
|
334
169
|
end
|
335
170
|
args_formatted.compact.uniq # Not dealing with nil or doubled IDs
|
336
|
-
|
171
|
+
|
337
172
|
audios = []
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
173
|
+
begin
|
174
|
+
args_formatted.each_slice(10) do |subarray|
|
175
|
+
json = load_json_audios_by_id(subarray)
|
176
|
+
subresult = audios_from_data(json["data"][0].to_a)
|
177
|
+
audios.concat(subresult)
|
178
|
+
end
|
179
|
+
rescue
|
180
|
+
raise Exceptions::ParseError
|
342
181
|
end
|
343
|
-
|
182
|
+
VkMusic.debug("Loaded audios from ids: #{audios.map(&:pp).join(", ")}")
|
344
183
|
|
345
184
|
args.map do |el|
|
346
185
|
case el
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
186
|
+
when Array
|
187
|
+
audios.find { |audio| audio.owner_id == el[0].to_i && audio.id == el[1].to_i }
|
188
|
+
when Audio
|
189
|
+
next el if el.full_id.nil? # Audio was skipped
|
190
|
+
audios.find { |audio| audio.owner_id == el.owner_id && audio.id == el.id }
|
191
|
+
when String
|
192
|
+
audios.find { |audio| [audio.owner_id, audio.id] == el.split("_").first(2).map(&:to_i) }
|
193
|
+
else
|
194
|
+
nil # This shouldn't happen actually
|
356
195
|
end
|
357
196
|
end
|
358
197
|
end
|
198
|
+
alias_method :from_id, :get_urls
|
199
|
+
##
|
200
|
+
# Update download URLs of audios.
|
201
|
+
# @param audios [Array<Audio>]
|
202
|
+
def update_urls(audios)
|
203
|
+
audios_with_urls = get_urls(audios)
|
204
|
+
audios.each.with_index do |a, i|
|
205
|
+
a_u = audios_with_urls[i]
|
206
|
+
a.update(from: a_u) unless a_u.nil?
|
207
|
+
end
|
208
|
+
end
|
359
209
|
|
360
210
|
##
|
361
211
|
# @!endgroup
|
@@ -364,140 +214,103 @@ module VkMusic
|
|
364
214
|
# @!group Other
|
365
215
|
|
366
216
|
##
|
367
|
-
# Get user or group ID. Sends one request if custom ID provided
|
368
|
-
#
|
217
|
+
# Get user or group ID. Sends one request if custom ID provided.
|
369
218
|
# @param str [String] link, ID with/without prefix or custom ID.
|
370
|
-
#
|
371
219
|
# @return [Integer] page ID.
|
372
220
|
def page_id(str)
|
373
|
-
raise ArgumentError, "Bad arguments", caller unless str.class == String
|
374
|
-
|
375
221
|
case str
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
222
|
+
when Constants::Regex::VK_URL
|
223
|
+
path = str.match(Constants::Regex::VK_URL)[1]
|
224
|
+
page_id(path) # Recursive call
|
225
|
+
when Constants::Regex::VK_ID_STR
|
226
|
+
str.to_i
|
227
|
+
when Constants::Regex::VK_AUDIOS_URL_POSTFIX
|
228
|
+
str.match(/-?\d+/).to_s.to_i # Numbers with sign
|
229
|
+
when Constants::Regex::VK_PREFIXED_ID_STR
|
230
|
+
id = str.match(/\d+/).to_s.to_i # Just numbers. Sign needed
|
231
|
+
id *= -1 unless str.start_with?("id")
|
232
|
+
id
|
233
|
+
when Constants::Regex::VK_CUSTOM_ID
|
234
|
+
url = "#{Constants::URL::VK[:home]}/#{str}"
|
235
|
+
begin
|
236
|
+
page = load_page(url)
|
237
|
+
rescue Exceptions::RequestError
|
238
|
+
raise Exceptions::ParseError
|
239
|
+
end
|
240
|
+
|
241
|
+
raise Exceptions::ParseError unless page.at_css(".PageBlock .owner_panel")
|
242
|
+
|
243
|
+
begin
|
244
|
+
page.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(Constants::Regex::VK_ID).to_i # Numbers with sign
|
245
|
+
rescue
|
246
|
+
raise Exceptions::ParseError
|
247
|
+
end
|
248
|
+
else
|
249
|
+
raise Exceptions::ParseError
|
403
250
|
end
|
404
|
-
id
|
405
251
|
end
|
406
252
|
|
407
253
|
##
|
408
254
|
# Get ID of last post.
|
409
|
-
#
|
255
|
+
# Specify either +url+ or +owner_id+.
|
410
256
|
# @note requesting for "vk.com/id0" will raise ArgumentError.
|
411
257
|
# Use +client.last_post_id(owner_id: client.id)+ to get last post of client.
|
412
|
-
#
|
413
|
-
# @
|
414
|
-
# @param url [String] URL to wall owner.
|
415
|
-
#
|
416
|
-
# @overload last_post_id(owner_id)
|
417
|
-
# @param owner_id [Integer] numerical ID of wall owner.
|
418
|
-
#
|
258
|
+
# @param url [String] URL to wall owner.
|
259
|
+
# @param owner_id [Integer] numerical ID of wall owner.
|
419
260
|
# @return [Integer, nil] ID of last post or +nil+ if there are no posts.
|
420
|
-
def last_post_id(
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
when Integer
|
426
|
-
owner_id = arg
|
427
|
-
path = "#{owner_id < 0 ? "club" : "id"}#{owner_id.abs}"
|
428
|
-
else
|
429
|
-
raise
|
430
|
-
end
|
431
|
-
rescue
|
432
|
-
raise ArgumentError, "Bad arguments", caller
|
261
|
+
def last_post_id(url: nil, owner_id: nil)
|
262
|
+
path = if url
|
263
|
+
url.match(Constants::Regex::VK_URL)[1]
|
264
|
+
else
|
265
|
+
path = "#{owner_id < 0 ? "club" : "id"}#{owner_id.abs}"
|
433
266
|
end
|
434
267
|
raise ArgumentError, "Requesting this method for id0 is forbidden", caller if path == "id0"
|
435
268
|
|
436
269
|
url = "#{Constants::URL::VK[:home]}/#{path}"
|
437
|
-
|
438
|
-
begin
|
439
|
-
page = load__page(url)
|
440
|
-
rescue Exceptions::RequestError => error
|
441
|
-
raise Exceptions::ParseError, "Failed request: #{error.message}", caller
|
442
|
-
end
|
270
|
+
page = load_page(url)
|
443
271
|
|
444
272
|
# Ensure this isn't some random vk page
|
445
|
-
raise Exceptions::ParseError
|
273
|
+
raise Exceptions::ParseError unless page.at_css(".PageBlock .owner_panel")
|
446
274
|
|
447
275
|
begin
|
448
|
-
posts = page.css(".wall_posts > .wall_item .
|
276
|
+
posts = page.css(".wall_posts > .wall_item .anchor")
|
449
277
|
posts_ids = posts.map do |post|
|
450
278
|
post ? post.attribute("name").to_s.match(Constants::Regex::VK_POST_URL_POSTFIX)[2].to_i : 0
|
451
279
|
end
|
452
280
|
# To avoid checking id of pinned post need to take maximum id.
|
453
281
|
return posts_ids.max
|
454
|
-
rescue
|
455
|
-
raise Exceptions::ParseError
|
282
|
+
rescue
|
283
|
+
raise Exceptions::ParseError
|
456
284
|
end
|
457
285
|
end
|
458
286
|
|
459
287
|
##
|
460
288
|
# Get audios attached to specified post.
|
461
|
-
#
|
462
|
-
# @
|
463
|
-
#
|
464
|
-
#
|
465
|
-
# @overload attached_audios(options)
|
466
|
-
# @macro options_hash_param
|
467
|
-
# @option options [Integer] :owner_id numerical ID of wall owner.
|
468
|
-
# @option options [Integer] :post_id numerical ID of post.
|
469
|
-
#
|
289
|
+
# Specify either +url+ or +(owner_id,post_id)+.
|
290
|
+
# @param url [String] URL to post.
|
291
|
+
# @param owner_id [Integer] numerical ID of wall owner.
|
292
|
+
# @param post_id [Integer] numerical ID of post.
|
470
293
|
# @return [Array<Audio>] audios with only artist, title and duration.
|
471
|
-
def attached_audios(
|
294
|
+
def attached_audios(url: nil, owner_id: nil, post_id: nil)
|
472
295
|
begin
|
473
|
-
|
474
|
-
when String
|
475
|
-
owner_id, post_id = arg.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures
|
476
|
-
when Hash
|
477
|
-
options = arg
|
478
|
-
owner_id = options[:owner_id].to_i
|
479
|
-
post_id = options[:post_id].to_i
|
480
|
-
else
|
481
|
-
raise
|
482
|
-
end
|
296
|
+
owner_id, post_id = url.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures if url
|
483
297
|
rescue
|
484
|
-
raise
|
298
|
+
raise Exceptions::ParseError
|
485
299
|
end
|
486
300
|
|
487
301
|
url = "#{Constants::URL::VK[:wall]}#{owner_id}_#{post_id}"
|
488
302
|
begin
|
489
|
-
page =
|
490
|
-
rescue Exceptions::RequestError
|
491
|
-
raise Exceptions::ParseError
|
303
|
+
page = load_page(url)
|
304
|
+
rescue Exceptions::RequestError
|
305
|
+
raise Exceptions::ParseError
|
492
306
|
end
|
493
307
|
|
494
|
-
raise Exceptions::ParseError
|
308
|
+
raise Exceptions::ParseError unless page.css(".service_msg_error").empty?
|
495
309
|
begin
|
496
|
-
|
497
|
-
rescue
|
498
|
-
raise Exceptions::ParseError
|
310
|
+
page.css(".wi_body > .pi_medias .medias_audio").map { |e| Audio.from_node(e, @id) }
|
311
|
+
rescue
|
312
|
+
raise Exceptions::ParseError
|
499
313
|
end
|
500
|
-
result
|
501
314
|
end
|
502
315
|
|
503
316
|
##
|
@@ -505,20 +318,25 @@ module VkMusic
|
|
505
318
|
|
506
319
|
private
|
507
320
|
|
508
|
-
|
509
|
-
|
321
|
+
##
|
322
|
+
# Load page web page.
|
323
|
+
# @param url [String, URI]
|
324
|
+
# @return [Mechanize::Page]
|
325
|
+
def load_page(url)
|
510
326
|
uri = URI(url) if url.class != URI
|
511
|
-
|
327
|
+
VkMusic.debug("Loading #{uri}")
|
512
328
|
begin
|
513
329
|
@agent.get(uri)
|
514
|
-
rescue
|
515
|
-
raise Exceptions::RequestError
|
330
|
+
rescue
|
331
|
+
raise Exceptions::RequestError
|
516
332
|
end
|
517
333
|
end
|
518
|
-
|
519
|
-
# Load JSON
|
520
|
-
|
521
|
-
|
334
|
+
##
|
335
|
+
# Load JSON from web page.
|
336
|
+
# @param url [String, URI]
|
337
|
+
# @return [Hash]
|
338
|
+
def load_json(url)
|
339
|
+
page = load_page(url)
|
522
340
|
begin
|
523
341
|
JSON.parse(page.body.strip)
|
524
342
|
rescue Exception => error
|
@@ -526,90 +344,113 @@ module VkMusic
|
|
526
344
|
end
|
527
345
|
end
|
528
346
|
|
529
|
-
|
347
|
+
##
|
530
348
|
# Load playlist web page.
|
531
|
-
|
349
|
+
# @param owner_id [Integer]
|
350
|
+
# @param playlist_id [Integer]
|
351
|
+
# @param access_hash [String, nil]
|
352
|
+
# @param offset [Integer]
|
353
|
+
# @return [Mechanize::Page]
|
354
|
+
def load_page_playlist(owner_id, playlist_id, access_hash = nil, offset: 0)
|
532
355
|
uri = URI(Constants::URL::VK[:audios])
|
533
356
|
uri.query = Utility.hash_to_params({
|
534
|
-
|
535
|
-
|
536
|
-
|
357
|
+
act: "audio_playlist#{owner_id}_#{playlist_id}",
|
358
|
+
access_hash: access_hash.to_s,
|
359
|
+
offset: offset
|
537
360
|
})
|
538
|
-
|
361
|
+
load_page(uri)
|
539
362
|
end
|
540
|
-
|
541
|
-
# Load JSON playlist
|
542
|
-
|
363
|
+
##
|
364
|
+
# Load JSON playlist section with +load_section+ request.
|
365
|
+
# @param owner_id [Integer]
|
366
|
+
# @param playlist_id [Integer]
|
367
|
+
# @param access_hash [String, nil]
|
368
|
+
# @param offset [Integer]
|
369
|
+
# @return [Hash]
|
370
|
+
def load_json_playlist_section(owner_id, playlist_id, access_hash = nil, offset: 0)
|
543
371
|
uri = URI(Constants::URL::VK[:audios])
|
544
372
|
uri.query = Utility.hash_to_params({
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
373
|
+
act: "load_section",
|
374
|
+
owner_id: owner_id,
|
375
|
+
playlist_id: playlist_id,
|
376
|
+
access_hash: access_hash.to_s,
|
377
|
+
type: "playlist",
|
378
|
+
offset: offset,
|
379
|
+
utf8: true
|
552
380
|
})
|
553
|
-
|
381
|
+
load_json(uri)
|
554
382
|
end
|
555
383
|
|
556
|
-
|
384
|
+
##
|
557
385
|
# Load JSON audios with +reload_audio+ request.
|
558
|
-
|
386
|
+
# @param ids [Array<String>]
|
387
|
+
# @return [Hash]
|
388
|
+
def load_json_audios_by_id(ids)
|
559
389
|
uri = URI(Constants::URL::VK[:audios])
|
560
390
|
uri.query = Utility.hash_to_params({
|
561
|
-
|
562
|
-
|
563
|
-
|
391
|
+
act: "reload_audio",
|
392
|
+
ids: ids,
|
393
|
+
utf8: true
|
564
394
|
})
|
565
|
-
|
395
|
+
load_json(uri)
|
566
396
|
end
|
567
|
-
|
397
|
+
##
|
568
398
|
# Load JSON audios with +load_section+ from wall.
|
569
|
-
|
399
|
+
# @param owner_id [Integer]
|
400
|
+
# @param post_id [Integer]
|
401
|
+
# @return [Hash]
|
402
|
+
def load_json_audios_wall(owner_id, post_id)
|
570
403
|
uri = URI(Constants::URL::VK[:audios])
|
571
404
|
uri.query = Utility.hash_to_params({
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
405
|
+
act: "load_section",
|
406
|
+
owner_id: owner_id,
|
407
|
+
post_id: post_id,
|
408
|
+
type: "wall",
|
409
|
+
wall_type: "own",
|
410
|
+
utf8: true
|
578
411
|
})
|
579
|
-
|
412
|
+
load_json(uri)
|
580
413
|
end
|
581
414
|
|
582
|
-
|
583
|
-
#
|
584
|
-
|
585
|
-
|
415
|
+
##
|
416
|
+
# Load audios from web page.
|
417
|
+
# @param obj [Mechanize::Page, String, URI]
|
418
|
+
# @return [Array<Audio>]
|
419
|
+
def audios_from_page(obj)
|
420
|
+
page = obj.is_a?(Mechanize::Page) ? obj : load_page(obj)
|
586
421
|
begin
|
587
422
|
page.css(".audio_item.ai_has_btn").map { |elem| Audio.from_node(elem, @id) }
|
588
|
-
rescue
|
589
|
-
raise Exceptions::ParseError
|
423
|
+
rescue
|
424
|
+
raise Exceptions::ParseError
|
590
425
|
end
|
591
|
-
end
|
592
|
-
|
426
|
+
end
|
427
|
+
##
|
593
428
|
# Load audios from JSON data.
|
594
|
-
|
429
|
+
# @param data [Hash]
|
430
|
+
# @return [Array<Audio>]
|
431
|
+
def audios_from_data(data)
|
595
432
|
begin
|
596
433
|
data.map { |audio_data| Audio.from_data(audio_data, @id) }
|
597
|
-
rescue
|
598
|
-
raise Exceptions::ParseError
|
434
|
+
rescue
|
435
|
+
raise Exceptions::ParseError
|
599
436
|
end
|
600
437
|
end
|
601
438
|
|
602
|
-
|
439
|
+
##
|
603
440
|
# Load playlist through web page requests.
|
604
|
-
|
441
|
+
# @param owner_id [Integer]
|
442
|
+
# @param playlist_id [Integer]
|
443
|
+
# @param access_hash [String, nil]
|
444
|
+
# @param up_to [Integer] if less than 0, all audios will be loaded.
|
445
|
+
# @return [Playlist]
|
446
|
+
def playlist_web(owner_id, playlist_id, access_hash = nil, up_to: -1)
|
447
|
+
# Load first page and get info
|
448
|
+
first_page = load_page_playlist(owner_id, playlist_id, access_hash, offset: 0)
|
605
449
|
begin
|
606
|
-
# Load first page and get info
|
607
|
-
first_page = load__page__playlist(owner_id, playlist_id, access_hash, offset: 0)
|
608
|
-
|
609
450
|
# Parse out essential data
|
610
451
|
title = first_page.at_css(".audioPlaylist__title").text.strip
|
611
452
|
subtitle = first_page.at_css(".audioPlaylist__subtitle").text.strip
|
612
|
-
|
453
|
+
|
613
454
|
footer_node = first_page.at_css(".audioPlaylist__footer")
|
614
455
|
if footer_node
|
615
456
|
footer_match = footer_node.text.strip.match(/^\d+/)
|
@@ -617,96 +458,117 @@ module VkMusic
|
|
617
458
|
else
|
618
459
|
real_size = 0
|
619
460
|
end
|
620
|
-
rescue
|
621
|
-
raise Exceptions::ParseError
|
461
|
+
rescue
|
462
|
+
raise Exceptions::ParseError
|
622
463
|
end
|
623
|
-
# Now we can be sure we are on correct page
|
624
|
-
|
625
|
-
first_page_audios =
|
626
|
-
|
464
|
+
# Now we can be sure we are on correct page and have essential data.
|
465
|
+
|
466
|
+
first_page_audios = audios_from_page(first_page)
|
467
|
+
|
627
468
|
# Check whether need to make additional requests
|
628
|
-
|
629
|
-
list = first_page_audios
|
630
|
-
while list.length <
|
631
|
-
playlist_page =
|
632
|
-
list.concat(
|
469
|
+
up_to = real_size if (up_to < 0 || up_to > real_size)
|
470
|
+
list = first_page_audios.first(up_to)
|
471
|
+
while list.length < up_to do
|
472
|
+
playlist_page = load_page_playlist(owner_id, playlist_id, access_hash, offset: list.length)
|
473
|
+
list.concat(audios_from_page(playlist_page).first(up_to - list.length))
|
633
474
|
end
|
634
|
-
|
635
|
-
Playlist.new(list,
|
636
|
-
:
|
637
|
-
:
|
638
|
-
:
|
639
|
-
:
|
640
|
-
:
|
641
|
-
:
|
642
|
-
|
475
|
+
|
476
|
+
Playlist.new(list,
|
477
|
+
id: id,
|
478
|
+
owner_id: owner_id,
|
479
|
+
access_hash: access_hash,
|
480
|
+
title: title,
|
481
|
+
subtitle: subtitle,
|
482
|
+
real_size: real_size
|
483
|
+
)
|
643
484
|
end
|
644
485
|
|
486
|
+
##
|
645
487
|
# Load playlist through JSON requests.
|
646
|
-
|
488
|
+
# @param owner_id [Integer]
|
489
|
+
# @param playlist_id [Integer]
|
490
|
+
# @param access_hash [String, nil]
|
491
|
+
# @param up_to [Integer] if less than 0, all audios will be loaded.
|
492
|
+
# @return [Playlist]
|
493
|
+
def playlist_json(owner_id, playlist_id, access_hash, up_to: -1)
|
647
494
|
# Trying to parse out audios
|
495
|
+
first_json = load_json_playlist_section(owner_id, playlist_id, access_hash, offset: 0)
|
648
496
|
begin
|
649
|
-
first_json = load__json__playlist_section(owner_id, playlist_id, access_hash, offset: 0)
|
650
497
|
first_data = first_json["data"][0]
|
651
|
-
first_data_audios =
|
652
|
-
rescue
|
653
|
-
raise Exceptions::ParseError
|
498
|
+
first_data_audios = audios_from_data(first_data["list"])
|
499
|
+
rescue
|
500
|
+
raise Exceptions::ParseError
|
654
501
|
end
|
655
|
-
|
502
|
+
|
656
503
|
real_size = first_data["totalCount"]
|
657
|
-
|
658
|
-
list = first_data_audios
|
659
|
-
while list.length <
|
660
|
-
json =
|
661
|
-
|
504
|
+
up_to = real_size if (up_to < 0 || up_to > real_size)
|
505
|
+
list = first_data_audios.first(up_to)
|
506
|
+
while list.length < up_to do
|
507
|
+
json = load_json_playlist_section(owner_id, playlist_id, access_hash, offset: list.length)
|
508
|
+
audios = begin
|
509
|
+
audios_from_data(json["data"][0]["list"])
|
510
|
+
rescue
|
511
|
+
raise Exceptions::ParseError
|
512
|
+
end
|
513
|
+
list.concat(audios.first(up_to - list.length))
|
514
|
+
end
|
515
|
+
|
516
|
+
begin
|
517
|
+
Playlist.new(list,
|
518
|
+
id: first_data["id"],
|
519
|
+
owner_id: first_data["owner_id"],
|
520
|
+
access_hash: first_data["access_hash"],
|
521
|
+
title: CGI.unescapeHTML(first_data["title"].to_s),
|
522
|
+
subtitle: CGI.unescapeHTML(first_data["subtitle"].to_s),
|
523
|
+
real_size: real_size
|
662
524
|
)
|
663
|
-
|
664
|
-
|
525
|
+
rescue
|
526
|
+
raise Exceptions::ParseError
|
665
527
|
end
|
666
|
-
|
667
|
-
Playlist.new(list, {
|
668
|
-
:id => first_data["id"],
|
669
|
-
:owner_id => first_data["owner_id"],
|
670
|
-
:access_hash => first_data["access_hash"],
|
671
|
-
:title => CGI.unescapeHTML(first_data["title"].to_s),
|
672
|
-
:subtitle => CGI.unescapeHTML(first_data["subtitle"].to_s),
|
673
|
-
:real_size => real_size
|
674
|
-
})
|
675
528
|
end
|
676
529
|
|
677
|
-
|
678
|
-
|
679
|
-
|
530
|
+
##
|
531
|
+
# Found playlist URLs on *global* search page.
|
532
|
+
# @param obj [Mechanize::Page, String, URI]
|
533
|
+
# @return [Array<String>]
|
534
|
+
def playlist_urls_from_page(obj)
|
535
|
+
page = obj.is_a?(Mechanize::Page) ? obj : load_page(obj)
|
680
536
|
begin
|
681
|
-
page.css(".
|
682
|
-
rescue
|
683
|
-
raise Exceptions::ParseError
|
537
|
+
page.css(".AudioShowcase__block_playlists .AudioPlaylistSlider .al_playlist").map { |elem| elem.attribute("href").to_s }
|
538
|
+
rescue
|
539
|
+
raise Exceptions::ParseError
|
684
540
|
end
|
685
541
|
end
|
686
542
|
|
543
|
+
##
|
687
544
|
# Load audios from wall using JSON request.
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
545
|
+
# @param owner_id [Integer]
|
546
|
+
# @param post_id [Intger]
|
547
|
+
# @param up_to [Integer]
|
548
|
+
# @param with_url [Boolean] whether to retrieve URLs with {Client#from_id} method
|
549
|
+
# @return [Array<Audio>]
|
550
|
+
def wall_json(owner_id, post_id, up_to: 91, with_url: false)
|
551
|
+
if up_to < 0 || up_to > 91
|
552
|
+
up_to = 91
|
553
|
+
VkMusic.warn("Current implementation of this method is not able to return more than 91 audios from wall.")
|
692
554
|
end
|
693
555
|
|
556
|
+
json = load_json_audios_wall(owner_id, post_id)
|
694
557
|
begin
|
695
|
-
json = load__json__audios_wall(owner_id, post_id)
|
696
558
|
data = json["data"][0]
|
697
|
-
audios =
|
698
|
-
rescue
|
699
|
-
raise Exceptions::ParseError
|
559
|
+
audios = audios_from_data(data["list"]).first(up_to)
|
560
|
+
rescue
|
561
|
+
raise Exceptions::ParseError
|
700
562
|
end
|
701
|
-
|
563
|
+
with_url ? from_id(audios) : audios
|
702
564
|
end
|
703
565
|
|
704
|
-
|
705
|
-
# Login
|
566
|
+
##
|
567
|
+
# Login to VK.
|
706
568
|
def login(username, password)
|
707
|
-
|
569
|
+
VkMusic.debug("Logging in.")
|
708
570
|
# Loading login page
|
709
|
-
homepage =
|
571
|
+
homepage = load_page(Constants::URL::VK[:login])
|
710
572
|
# Submitting login form
|
711
573
|
login_form = homepage.forms.find { |form| form.action.start_with?(Constants::URL::VK[:login_action]) }
|
712
574
|
login_form[Constants::VK_LOGIN_FORM_NAMES[:username]] = username.to_s
|
@@ -715,18 +577,16 @@ module VkMusic
|
|
715
577
|
|
716
578
|
# Checking whether logged in
|
717
579
|
raise Exceptions::LoginError, "Unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == Constants::URL::VK[:feed]
|
718
|
-
|
580
|
+
|
719
581
|
# Parsing information about this profile
|
720
|
-
profile =
|
582
|
+
profile = load_page(Constants::URL::VK[:profile])
|
721
583
|
@name = profile.title.to_s
|
722
584
|
@id = profile.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(/\d+/).to_i
|
723
585
|
end
|
724
|
-
|
586
|
+
|
725
587
|
# Shortcut
|
726
588
|
def unmask_link(link)
|
727
589
|
VkMusic::LinkDecoder.unmask_link(link, @id)
|
728
590
|
end
|
729
|
-
|
730
591
|
end
|
731
|
-
|
732
592
|
end
|