vk_music 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/vk_music/audio.rb +59 -4
- data/lib/vk_music/client.rb +104 -40
- data/lib/vk_music/constants.rb +41 -36
- data/lib/vk_music/exceptions.rb +26 -21
- data/lib/vk_music/link_decoder.rb +81 -68
- data/lib/vk_music/playlist.rb +45 -10
- data/lib/vk_music/utility.rb +29 -3
- data/vk_music.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c6e2d05ccf44bc32534cc66a85c2495f799e247876495df962bd312b3cef644
|
4
|
+
data.tar.gz: acc2fab3d0f196536f195cecea4f5bd0c997a6e14566a9a8a574b444db98bf4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f31a0049dc14a0ef0603f503733593a5bd8ffc2a26cae4ce4c6bb4ace98d175f86745ead97e2293d9157a2bf62b323c2f61254c01940b37505715c35454fdbc
|
7
|
+
data.tar.gz: 20fa21e9ee919f544664c20c149089ce594967bafa9809e7ab28ed68f53ab68d0147f5451fac66d83fe4453d1e3c388614fc543958f024d911ff24439cf94bb5
|
data/lib/vk_music/audio.rb
CHANGED
@@ -2,10 +2,38 @@ require "cgi"
|
|
2
2
|
|
3
3
|
module VkMusic
|
4
4
|
|
5
|
+
# VK audio.
|
5
6
|
class Audio
|
6
7
|
|
7
|
-
|
8
|
+
# Id of audio.
|
9
|
+
attr_reader :id
|
10
|
+
# Id of audio owner.
|
11
|
+
attr_reader :owner_id
|
12
|
+
# Parts of secret hash which used when using +act=reload_audio+
|
13
|
+
attr_reader :secret_1, :secret_2
|
14
|
+
# Artist.
|
15
|
+
attr_reader :artist
|
16
|
+
# Title.
|
17
|
+
attr_reader :title
|
18
|
+
# Duration.
|
19
|
+
attr_reader :duration
|
20
|
+
# Download URL.
|
21
|
+
attr_reader :url
|
22
|
+
# Encoded URL.
|
23
|
+
attr_reader :url_encoded
|
8
24
|
|
25
|
+
# Update audio URLs.
|
26
|
+
#
|
27
|
+
# If +:url+ is provided - just save it.
|
28
|
+
# If +:url_encoded+ and +:client_id+ provided - unmask link first.
|
29
|
+
#
|
30
|
+
# ===== Parameters:
|
31
|
+
# * [+options+] (+Hash+)
|
32
|
+
#
|
33
|
+
# ===== Options:
|
34
|
+
# * +:url+
|
35
|
+
# * +:url_encoded+
|
36
|
+
# * +:client_id+
|
9
37
|
def update_url(options)
|
10
38
|
raise ArgumentError, "options hash must be provided", caller unless options.class == Hash
|
11
39
|
if !options[:url].to_s.empty?
|
@@ -13,20 +41,37 @@ module VkMusic
|
|
13
41
|
@url = options[:url].to_s
|
14
42
|
elsif !options[:url].to_s.empty? && options[:client_id]
|
15
43
|
@url_encoded = options[:url_encoded].to_s
|
16
|
-
@url = VkMusic.unmask_link(options[:url_encoded], options[:client_id])
|
44
|
+
@url = VkMusic::LinkDecoder.unmask_link(options[:url_encoded], options[:client_id])
|
17
45
|
else
|
18
46
|
raise ArgumentError, "You should either provide :url or :url_encoded and :client_id", caller
|
19
47
|
end
|
20
48
|
end
|
21
49
|
|
50
|
+
# Returns string with information about audio.
|
22
51
|
def to_s
|
23
52
|
"#{@artist} - #{@title} [#{Utility.format_seconds(@duration)}]"
|
24
53
|
end
|
25
54
|
|
55
|
+
# Returns extended information about audio.
|
26
56
|
def pp
|
27
57
|
"#{to_s} (Got decoded URL: #{@url ? "yes" : "no"}, able to get URL from VK: #{@id && @owner_id && @secret_1 && @secret_2 ? "yes" : "no"})"
|
28
58
|
end
|
29
59
|
|
60
|
+
# Initialize new audio.
|
61
|
+
#
|
62
|
+
# ===== Parameters:
|
63
|
+
# * [+options+] (+Hash+)
|
64
|
+
#
|
65
|
+
# ===== Options:
|
66
|
+
# * +:id+
|
67
|
+
# * +:owner_id+
|
68
|
+
# * +:secret_1+
|
69
|
+
# * +:secret_2+
|
70
|
+
# * +:artist+
|
71
|
+
# * +:title+
|
72
|
+
# * +:duration+
|
73
|
+
# * +:url_encoded+
|
74
|
+
# * +:url+
|
30
75
|
def initialize(options)
|
31
76
|
# Arguments check
|
32
77
|
raise ArgumentError, "options hash must be provided", caller unless options.class == Hash
|
@@ -46,6 +91,11 @@ module VkMusic
|
|
46
91
|
@url = options[:url].to_s
|
47
92
|
end
|
48
93
|
|
94
|
+
# Initialize new audio from Nokogiri HTML node.
|
95
|
+
#
|
96
|
+
# ===== Parameters:
|
97
|
+
# * [+node+] (+Nokogiri::XML::Node+)
|
98
|
+
# * [+client_id+] (+Integer+)
|
49
99
|
def self.from_node(node, client_id)
|
50
100
|
url_encoded = node.at_css("input").attribute("value").to_s
|
51
101
|
url_encoded = nil if url_encoded == "https://m.vk.com/mp3/audio_api_unavailable.mp3"
|
@@ -58,10 +108,15 @@ module VkMusic
|
|
58
108
|
:title => node.at_css(".ai_title").text.strip,
|
59
109
|
:duration => node.at_css(".ai_dur").attribute("data-dur").to_s.to_i,
|
60
110
|
:url_encoded => url_encoded,
|
61
|
-
:url => url_encoded ? VkMusic.unmask_link(url_encoded, client_id) : nil,
|
111
|
+
:url => url_encoded ? VkMusic::LinkDecoder.unmask_link(url_encoded, client_id) : nil,
|
62
112
|
})
|
63
113
|
end
|
64
114
|
|
115
|
+
# Initialize new audio from data array.
|
116
|
+
#
|
117
|
+
# ===== Parameters:
|
118
|
+
# * [+data+] (+Array+)
|
119
|
+
# * [+client_id+] (+Integer+)
|
65
120
|
def self.from_data_array(data, client_id)
|
66
121
|
url_encoded = data[2]
|
67
122
|
url_encoded = nil if url_encoded == ""
|
@@ -77,7 +132,7 @@ module VkMusic
|
|
77
132
|
:title => CGI.unescapeHTML(data[3]),
|
78
133
|
:duration => data[5],
|
79
134
|
:url_encoded => url_encoded,
|
80
|
-
:url => url_encoded ? VkMusic.unmask_link(url_encoded, client_id) : nil,
|
135
|
+
:url => url_encoded ? VkMusic::LinkDecoder.unmask_link(url_encoded, client_id) : nil,
|
81
136
|
})
|
82
137
|
end
|
83
138
|
|
data/lib/vk_music/client.rb
CHANGED
@@ -3,9 +3,13 @@ require "json"
|
|
3
3
|
|
4
4
|
module VkMusic
|
5
5
|
|
6
|
+
# Main class with all the interface.
|
6
7
|
class Client
|
7
8
|
|
8
|
-
|
9
|
+
# ID of user
|
10
|
+
attr_reader :id
|
11
|
+
# Name of user
|
12
|
+
attr_reader :name
|
9
13
|
|
10
14
|
# Mechanize agent
|
11
15
|
@agent = nil
|
@@ -21,16 +25,31 @@ module VkMusic
|
|
21
25
|
login(options[:username], options[:password])
|
22
26
|
end
|
23
27
|
|
28
|
+
# Find Audio.
|
29
|
+
#
|
30
|
+
# ===== Parameters:
|
31
|
+
# * [+query+] (+String+) - string to search for.
|
32
|
+
#
|
33
|
+
# ===== Returns:
|
34
|
+
# * (+Array+) - array of Audio.
|
24
35
|
def find_audio(query)
|
25
|
-
uri = URI(VK_URL[:audios])
|
36
|
+
uri = URI(Constants::VK_URL[:audios])
|
26
37
|
uri.query = Utility.hash_to_params({ "act" => "search", "q" => query.to_s })
|
27
38
|
load_audios_from_page(uri)
|
28
39
|
end
|
29
40
|
|
41
|
+
# Get Playlist.
|
42
|
+
#
|
43
|
+
# ===== Parameters:
|
44
|
+
# * [+url+] (+String+) - url to playlist.
|
45
|
+
# * [+up_to+] (+Integer+) - maximum amount of Audio to load.
|
46
|
+
#
|
47
|
+
# ===== Returns:
|
48
|
+
# * (+Playlist+)
|
30
49
|
def get_playlist(url, up_to = nil)
|
31
50
|
# NOTICE: it is possible to use same type of requests as in get_audios method
|
32
51
|
begin
|
33
|
-
url, owner_id, id, access_hash = url.to_s.match(PLAYLIST_URL_REGEX).to_a
|
52
|
+
url, owner_id, id, access_hash = url.to_s.match(Constants::PLAYLIST_URL_REGEX).to_a
|
34
53
|
|
35
54
|
# Load first page and get info
|
36
55
|
first_page = load_playlist_page(owner_id: owner_id, id: id, access_hash: access_hash, offset: 0)
|
@@ -47,7 +66,7 @@ module VkMusic
|
|
47
66
|
playlist_size = 0
|
48
67
|
end
|
49
68
|
rescue Exception => error
|
50
|
-
raise PlaylistParseError, "unable to parse playlist page. Error: #{error.message}", caller
|
69
|
+
raise Exceptions::PlaylistParseError, "unable to parse playlist page. Error: #{error.message}", caller
|
51
70
|
end
|
52
71
|
# Now we can be sure we are on correct page
|
53
72
|
|
@@ -75,7 +94,15 @@ module VkMusic
|
|
75
94
|
})
|
76
95
|
end
|
77
96
|
|
78
|
-
|
97
|
+
# Get user or group audios.
|
98
|
+
#
|
99
|
+
# ===== Parameters:
|
100
|
+
# * [+url+] (+String+) - URL to user or group.
|
101
|
+
# * [+up_to+] (+Integer+) - maximum amount of Audio to load.
|
102
|
+
#
|
103
|
+
# ===== Returns:
|
104
|
+
# * (+Playlist+)
|
105
|
+
def get_audios(url, up_to = nil)
|
79
106
|
if up_to && up_to > 100
|
80
107
|
Utility.warn("Current implementation of method VkMusic::Client#get_audios is only able to load first 100 audios from user page.")
|
81
108
|
end
|
@@ -84,7 +111,7 @@ module VkMusic
|
|
84
111
|
# NOTICE: it is possible to load up to 2000 audios **without url** if offset is negative
|
85
112
|
|
86
113
|
# Firstly, we need to get numeric id
|
87
|
-
id = get_id(
|
114
|
+
id = get_id(url.to_s)
|
88
115
|
|
89
116
|
# Trying to parse out audios
|
90
117
|
begin
|
@@ -92,7 +119,7 @@ module VkMusic
|
|
92
119
|
first_data = first_json["data"][0]
|
93
120
|
first_data_audios = load_audios_from_data(first_data["list"])
|
94
121
|
rescue Exception => error
|
95
|
-
raise AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
122
|
+
raise Exceptions::AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
96
123
|
end
|
97
124
|
|
98
125
|
#total_count = first_data["totalCount"] # NOTICE: not used due to restrictions described above
|
@@ -113,6 +140,13 @@ module VkMusic
|
|
113
140
|
})
|
114
141
|
end
|
115
142
|
|
143
|
+
# Get audios by their ids and secrets.
|
144
|
+
#
|
145
|
+
# ===== Parameters:
|
146
|
+
# * [+arr+] (+Array+) - Array of objects, which can have different types: Audio or Array[owner_id, id, secret_1, secret_2].
|
147
|
+
#
|
148
|
+
# ===== Returns:
|
149
|
+
# * (+Array+) - array of audios with decoded URLs.
|
116
150
|
def get_audios_by_id(*arr)
|
117
151
|
if arr.size > 10
|
118
152
|
Utility.warn("Current implementation of method VkMusic::Client#get_audios_by_id is only able to handle first 10 audios.")
|
@@ -131,18 +165,27 @@ module VkMusic
|
|
131
165
|
end
|
132
166
|
json = load_audios_json_by_id(arr)
|
133
167
|
result = load_audios_from_data(json["data"][0].to_a)
|
134
|
-
raise ReloadAudiosParseError, "Result size don't match: excepected #{arr.size}, got #{result.size}", caller if result.size != arr.size
|
168
|
+
raise Exceptions::ReloadAudiosParseError, "Result size don't match: excepected #{arr.size}, got #{result.size}", caller if result.size != arr.size
|
135
169
|
|
136
170
|
result
|
137
171
|
end
|
138
172
|
|
173
|
+
# Get audios on wall of user or group starting with given post.
|
174
|
+
#
|
175
|
+
# ===== Parameters:
|
176
|
+
# * [+owner_id+] (+Integer+)
|
177
|
+
# * [+post_id+] (+Integer+)
|
178
|
+
# * [+up_to+] (+Integer+) - maximum amount of Audio to load.
|
179
|
+
#
|
180
|
+
# ===== Returns:
|
181
|
+
# * (+Array+) - array of audios with URLs.
|
139
182
|
def get_audios_from_wall(owner_id, post_id, up_to = nil)
|
140
183
|
begin
|
141
184
|
json = load_audios_json_from_wall(owner_id, post_id)
|
142
185
|
data = json["data"][0]
|
143
186
|
no_url_audios = load_audios_from_data(data["list"])
|
144
187
|
rescue Exception => error
|
145
|
-
raise WallParseError, "Failed to parse wall from #{@owner_id}_#{post_id}. Error: #{error.message}", caller
|
188
|
+
raise Exceptions::WallParseError, "Failed to parse wall from #{@owner_id}_#{post_id}. Error: #{error.message}", caller
|
146
189
|
end
|
147
190
|
|
148
191
|
up_to = no_url_audios.size if (up_to.nil? || up_to < 0 || up_to > no_url_audios.size)
|
@@ -159,54 +202,75 @@ module VkMusic
|
|
159
202
|
})
|
160
203
|
end
|
161
204
|
|
205
|
+
# Get audios attached to post.
|
206
|
+
#
|
207
|
+
# ===== Parameters:
|
208
|
+
# * [+url+] (+String+)
|
209
|
+
#
|
210
|
+
# ===== Returns:
|
211
|
+
# * (+Array+) - array of audios with URLs.
|
162
212
|
def get_audios_from_post(url)
|
163
|
-
url, owner_id, post_id = url.match(POST_URL_REGEX).to_a
|
213
|
+
url, owner_id, post_id = url.match(Constants::POST_URL_REGEX).to_a
|
164
214
|
|
165
215
|
amount = get_amount_of_audios_in_post(owner_id, post_id)
|
166
216
|
get_audios_from_wall(owner_id, post_id, amount).to_a
|
167
217
|
end
|
168
218
|
|
169
|
-
|
219
|
+
# Get user or group id.
|
220
|
+
#
|
221
|
+
# ===== Parameters:
|
222
|
+
# * [+str+] (+String+) - link, id with prefix or custom id.
|
223
|
+
#
|
224
|
+
# ===== Returns:
|
225
|
+
# * (+Integer+)
|
170
226
|
def get_id(str)
|
171
227
|
case str
|
172
|
-
when VK_URL_REGEX
|
173
|
-
path = str.match(VK_URL_REGEX)[1]
|
228
|
+
when Constants::VK_URL_REGEX
|
229
|
+
path = str.match(Constants::VK_URL_REGEX)[1]
|
174
230
|
get_id(path) # Recursive call
|
175
|
-
when VK_ID_REGEX
|
231
|
+
when Constants::VK_ID_REGEX
|
176
232
|
str
|
177
|
-
when VK_AUDIOS_REGEX
|
233
|
+
when Constants::VK_AUDIOS_REGEX
|
178
234
|
str.match(/-?\d+/).to_s # Numbers with sigh
|
179
|
-
when VK_PREFIXED_ID_REGEX
|
235
|
+
when Constants::VK_PREFIXED_ID_REGEX
|
180
236
|
id = str.match(/\d+/).to_s # Just numbers. Sign needed
|
181
237
|
id = "-#{id}" unless str.start_with?("id")
|
182
238
|
id
|
183
|
-
when VK_CUSTOM_ID_REGEX
|
239
|
+
when Constants::VK_CUSTOM_ID_REGEX
|
184
240
|
begin
|
185
|
-
page = load_page("#{VK_URL[:home]}/#{str}")
|
241
|
+
page = load_page("#{Constants::VK_URL[:home]}/#{str}")
|
186
242
|
rescue Exception => error
|
187
|
-
raise IdParseError, "unable to load page by id \"#{str}\". Error: #{error.message}"
|
243
|
+
raise Exceptions::IdParseError, "unable to load page by id \"#{str}\". Error: #{error.message}"
|
188
244
|
end
|
189
245
|
|
190
246
|
unless page.at_css(".PageBlock .owner_panel")
|
191
247
|
# Ensure this isn't some random vk page
|
192
|
-
raise IdParseError, "page #{str} doesn't seem to be a group or user page"
|
248
|
+
raise Exceptions::IdParseError, "page #{str} doesn't seem to be a group or user page"
|
193
249
|
end
|
194
250
|
|
195
|
-
id = page.link_with(href: VK_HREF_ID_CONTAINING_REGEX).href.slice(/-?\d+/) # Numbers with sign
|
251
|
+
id = page.link_with(href: Constants::VK_HREF_ID_CONTAINING_REGEX).href.slice(/-?\d+/) # Numbers with sign
|
196
252
|
id
|
197
253
|
else
|
198
|
-
raise IdParseError, "unable to convert \"#{str}\" into id"
|
254
|
+
raise Exceptions::IdParseError, "unable to convert \"#{str}\" into id"
|
199
255
|
end
|
200
256
|
end
|
201
257
|
|
258
|
+
# Get amount of audios attached to specified post.
|
259
|
+
#
|
260
|
+
# ===== Parameters:
|
261
|
+
# * [+owner_id+] (+Integer+)
|
262
|
+
# * [+post_id+] (+Integer+)
|
263
|
+
#
|
264
|
+
# ===== Returns:
|
265
|
+
# * (+Integer+)
|
202
266
|
def get_amount_of_audios_in_post(owner_id, post_id)
|
203
267
|
begin
|
204
|
-
page = load_page("#{VK_URL[:wall]}#{owner_id}_#{post_id}")
|
268
|
+
page = load_page("#{Constants::VK_URL[:wall]}#{owner_id}_#{post_id}")
|
205
269
|
result = page.css(".wi_body > .pi_medias .medias_audio").size
|
206
270
|
rescue Exception => error
|
207
|
-
raise PostParseError, "Unable to get amount of audios in post #{owner_id}_#{post_id}. Error: #{error.message}", caller
|
271
|
+
raise Exceptions::PostParseError, "Unable to get amount of audios in post #{owner_id}_#{post_id}. Error: #{error.message}", caller
|
208
272
|
end
|
209
|
-
raise PostParseError, "Post not found: #{owner_id}_#{post_id}", caller if result == 0 && !page.css(".service_msg_error").empty?
|
273
|
+
raise Exceptions::PostParseError, "Post not found: #{owner_id}_#{post_id}", caller if result == 0 && !page.css(".service_msg_error").empty?
|
210
274
|
result
|
211
275
|
end
|
212
276
|
|
@@ -224,7 +288,7 @@ module VkMusic
|
|
224
288
|
end
|
225
289
|
|
226
290
|
def load_playlist_page(options)
|
227
|
-
uri = URI(VK_URL[:audios])
|
291
|
+
uri = URI(Constants::VK_URL[:audios])
|
228
292
|
uri.query = Utility.hash_to_params({
|
229
293
|
"act" => "audio_playlist#{options[:owner_id]}_#{options[:id]}",
|
230
294
|
"access_hash" => options[:access_hash].to_s,
|
@@ -233,7 +297,7 @@ module VkMusic
|
|
233
297
|
load_page(uri)
|
234
298
|
end
|
235
299
|
def load_playlist_json_section(owner_id, playlist_id, offset = 0)
|
236
|
-
uri = URI(VK_URL[:audios])
|
300
|
+
uri = URI(Constants::VK_URL[:audios])
|
237
301
|
uri.query = Utility.hash_to_params({
|
238
302
|
"act" => "load_section",
|
239
303
|
"owner_id" => owner_id,
|
@@ -245,12 +309,12 @@ module VkMusic
|
|
245
309
|
begin
|
246
310
|
load_json(uri)
|
247
311
|
rescue Exception => error
|
248
|
-
raise AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
312
|
+
raise Exceptions::AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
249
313
|
end
|
250
314
|
end
|
251
315
|
|
252
316
|
def load_audios_json_by_id(ids)
|
253
|
-
uri = URI(VK_URL[:audios])
|
317
|
+
uri = URI(Constants::VK_URL[:audios])
|
254
318
|
uri.query = Utility.hash_to_params({
|
255
319
|
"act" => "reload_audio",
|
256
320
|
"ids" => ids,
|
@@ -259,12 +323,12 @@ module VkMusic
|
|
259
323
|
begin
|
260
324
|
load_json(uri)
|
261
325
|
rescue Exception => error
|
262
|
-
raise AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
326
|
+
raise Exceptions::AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
263
327
|
end
|
264
328
|
end
|
265
329
|
|
266
330
|
def load_audios_json_from_wall(owner_id, post_id)
|
267
|
-
uri = URI(VK_URL[:audios])
|
331
|
+
uri = URI(Constants::VK_URL[:audios])
|
268
332
|
uri.query = Utility.hash_to_params({
|
269
333
|
"act" => "load_section",
|
270
334
|
"owner_id" => owner_id,
|
@@ -276,7 +340,7 @@ module VkMusic
|
|
276
340
|
begin
|
277
341
|
load_json(uri)
|
278
342
|
rescue Exception => error
|
279
|
-
raise AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
343
|
+
raise Exceptions::AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
280
344
|
end
|
281
345
|
end
|
282
346
|
|
@@ -294,24 +358,24 @@ module VkMusic
|
|
294
358
|
# Login
|
295
359
|
def login(username, password)
|
296
360
|
# Loading login page
|
297
|
-
homepage = load_page(VK_URL[:home])
|
361
|
+
homepage = load_page(Constants::VK_URL[:home])
|
298
362
|
# Submitting login form
|
299
|
-
login_form = homepage.forms.find { |form| form.action.start_with?(VK_URL[:login_action]) }
|
300
|
-
login_form[VK_LOGIN_FORM_NAMES[:username]] = username.to_s
|
301
|
-
login_form[VK_LOGIN_FORM_NAMES[:password]] = password.to_s
|
363
|
+
login_form = homepage.forms.find { |form| form.action.start_with?(Constants::VK_URL[:login_action]) }
|
364
|
+
login_form[Constants::VK_LOGIN_FORM_NAMES[:username]] = username.to_s
|
365
|
+
login_form[Constants::VK_LOGIN_FORM_NAMES[:password]] = password.to_s
|
302
366
|
after_login = @agent.submit(login_form)
|
303
367
|
|
304
368
|
# Checking whether logged in
|
305
|
-
raise LoginError, "unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == VK_URL[:feed]
|
369
|
+
raise Exceptions::LoginError, "unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == Constants::VK_URL[:feed]
|
306
370
|
|
307
371
|
# Parsing information about this profile
|
308
|
-
profile = load_page(VK_URL[:profile])
|
372
|
+
profile = load_page(Constants::VK_URL[:profile])
|
309
373
|
@name = profile.title
|
310
|
-
@id = profile.link_with(href: VK_HREF_ID_CONTAINING_REGEX).href.slice(/\d+/)
|
374
|
+
@id = profile.link_with(href: Constants::VK_HREF_ID_CONTAINING_REGEX).href.slice(/\d+/)
|
311
375
|
end
|
312
376
|
|
313
377
|
def unmask_link(link)
|
314
|
-
VkMusic.unmask_link(link, @id)
|
378
|
+
VkMusic::LinkDecoder.unmask_link(link, @id)
|
315
379
|
end
|
316
380
|
|
317
381
|
end
|
data/lib/vk_music/constants.rb
CHANGED
@@ -1,41 +1,46 @@
|
|
1
1
|
module VkMusic
|
2
2
|
|
3
|
-
#
|
4
|
-
|
3
|
+
# Constants
|
4
|
+
module Constants
|
5
|
+
# Web
|
6
|
+
# DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1636.0 Safari/537.36"
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
8
|
+
VK_URL = {
|
9
|
+
:scheme => "https",
|
10
|
+
:host => "m.vk.com",
|
11
|
+
:home => "https://m.vk.com",
|
12
|
+
:profile => "https://m.vk.com/id0",
|
13
|
+
:feed => "https://m.vk.com/feed",
|
14
|
+
:audios => "https://m.vk.com/audio",
|
15
|
+
:login => "https://m.vk.com/login",
|
16
|
+
:login_action => "https://login.vk.com",
|
17
|
+
:wall => "https://m.vk.com/wall"
|
18
|
+
}
|
19
|
+
|
20
|
+
VK_LOGIN_FORM_NAMES = {
|
21
|
+
:username => "email",
|
22
|
+
:password => "pass",
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
VK_ID_REGEX = /^-?\d+$/
|
27
|
+
VK_AUDIOS_REGEX = /^audios-?\d+$/
|
28
|
+
VK_PREFIXED_ID_REGEX = /^(?:id|club|group|public|event)\d+$/ # TODO: Rework. This one is REALLY dirty. Not quite sure every page can return correct id with this regex
|
29
|
+
VK_CUSTOM_ID_REGEX = /^\w+$/
|
30
|
+
VK_URL_REGEX = /(?:https?:\/\/)?(?:m\.|www\.)?vk\.com\/([\w\-]+)/
|
31
|
+
|
32
|
+
VK_HREF_ID_CONTAINING_REGEX = /(?:audios|photo|write|owner_id=|friends\?id=)-?\d+/
|
33
|
+
|
34
|
+
# Playlist
|
35
|
+
PLAYLIST_URL_REGEX = /.*audio_playlist(-?\d+)_(\d+)(?:(?:(?:&access_hash=)|\/|%2F)([\da-z]+))?/
|
36
|
+
|
37
|
+
# Post
|
38
|
+
POST_URL_REGEX = /.*wall(-?\d+)_(\d+)/
|
39
|
+
|
40
|
+
|
41
|
+
# QUESTION: Should I move ALL the constants (string, regex etc) here? It would make code more flexible, but seems like overkill
|
42
|
+
end
|
43
|
+
|
44
|
+
include Constants
|
34
45
|
|
35
|
-
# Post
|
36
|
-
POST_URL_REGEX = /.*wall(-?\d+)_(\d+)/
|
37
|
-
|
38
|
-
|
39
|
-
# QUESTION: Should I move ALL the constants (string, regex etc) here? It would make code more flexible, but seems like overkill
|
40
|
-
|
41
46
|
end
|
data/lib/vk_music/exceptions.rb
CHANGED
@@ -1,30 +1,35 @@
|
|
1
1
|
module VkMusic
|
2
2
|
|
3
|
-
#
|
4
|
-
|
3
|
+
# Exceptions.
|
4
|
+
module Exceptions
|
5
|
+
# General class for all the errors.
|
6
|
+
class VkMusicError < RuntimeError; end
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# Unable to find playlist or got permission error
|
13
|
-
class PlaylistParseError < AudiosParseError; end
|
8
|
+
# Failed to login.
|
9
|
+
class LoginError < VkMusicError; end
|
10
|
+
|
11
|
+
# Unable to parse audios from somewhere.
|
12
|
+
class AudiosParseError < VkMusicError; end
|
14
13
|
|
15
|
-
|
16
|
-
|
14
|
+
# Unable to find playlist or got permission error.
|
15
|
+
class PlaylistParseError < AudiosParseError; end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# Unable to convert string to id
|
22
|
-
class IdParseError < AudiosParseError; end
|
17
|
+
# Unable to load or parse audios section from json.
|
18
|
+
class AudiosSectionParseError < AudiosParseError; end
|
23
19
|
|
24
|
-
|
25
|
-
|
20
|
+
# Unable to load or parse all of audios by ids.
|
21
|
+
class ReloadAudiosParseError < AudiosParseError; end
|
26
22
|
|
27
|
-
|
28
|
-
|
23
|
+
# Unable to convert string to id.
|
24
|
+
class IdParseError < AudiosParseError; end
|
25
|
+
|
26
|
+
# Unable to parse audios from wall.
|
27
|
+
class WallParseError < AudiosParseError; end
|
28
|
+
|
29
|
+
# Unable to parse audios from post.
|
30
|
+
class PostParseError < AudiosParseError; end
|
31
|
+
end
|
32
|
+
|
33
|
+
include Exceptions
|
29
34
|
|
30
35
|
end
|
@@ -2,92 +2,105 @@ require "execjs"
|
|
2
2
|
|
3
3
|
module VkMusic
|
4
4
|
|
5
|
-
#
|
6
|
-
|
7
|
-
function vk_unmask_link(link, vk_id) {
|
5
|
+
# Module containing link decoding utilities.
|
6
|
+
module LinkDecoder
|
8
7
|
|
9
|
-
|
8
|
+
# JS code which creates function to unmask audio URL.
|
9
|
+
js_code = <<~HEREDOC
|
10
|
+
function vk_unmask_link(link, vk_id) {
|
10
11
|
|
11
|
-
|
12
|
-
if (encrypted.indexOf('audio_api_unavailable') != -1) {
|
13
|
-
var parts = encrypted.split('?extra=')[1].split('#');
|
12
|
+
// Utility functions to unmask
|
14
13
|
|
15
|
-
|
14
|
+
var audioUnmaskSource = function (encrypted) {
|
15
|
+
if (encrypted.indexOf('audio_api_unavailable') != -1) {
|
16
|
+
var parts = encrypted.split('?extra=')[1].split('#');
|
16
17
|
|
17
|
-
|
18
|
+
var handled_anchor = '' === parts[1] ? '' : handler(parts[1]);
|
18
19
|
|
19
|
-
|
20
|
-
// if (typeof handled_anchor != 'string') console.warn('Handled_anchor type: ' + typeof handled_anchor);
|
21
|
-
// if (!handled_part) console.warn('Handled_part: ' + handled_part);
|
22
|
-
return encrypted;
|
23
|
-
}
|
24
|
-
|
25
|
-
handled_anchor = handled_anchor ? handled_anchor.split(String.fromCharCode(9)) : [];
|
20
|
+
var handled_part = handler(parts[0]);
|
26
21
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
if (!utility_object[func_key]) {
|
31
|
-
// console.warn('Was unable to find key: ' + func_key);
|
22
|
+
if (typeof handled_anchor != 'string' || !handled_part) {
|
23
|
+
// if (typeof handled_anchor != 'string') console.warn('Handled_anchor type: ' + typeof handled_anchor);
|
24
|
+
// if (!handled_part) console.warn('Handled_part: ' + handled_part);
|
32
25
|
return encrypted;
|
33
26
|
}
|
34
|
-
handled_part = utility_object[func_key].apply(null, splited_anchor)
|
35
|
-
}
|
36
27
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
28
|
+
handled_anchor = handled_anchor ? handled_anchor.split(String.fromCharCode(9)) : [];
|
29
|
+
|
30
|
+
for (var func_key, splited_anchor, l = handled_anchor.length; l--;) {
|
31
|
+
splited_anchor = handled_anchor[l].split(String.fromCharCode(11));
|
32
|
+
func_key = splited_anchor.splice(0, 1, handled_part)[0];
|
33
|
+
if (!utility_object[func_key]) {
|
34
|
+
// console.warn('Was unable to find key: ' + func_key);
|
35
|
+
return encrypted;
|
36
|
+
}
|
37
|
+
handled_part = utility_object[func_key].apply(null, splited_anchor)
|
38
|
+
}
|
39
|
+
|
40
|
+
if (handled_part && 'http' === handled_part.substr(0, 4)) return handled_part;
|
41
|
+
// else console.warn('Failed unmasking: ' + handled_part);
|
42
|
+
} else {
|
43
|
+
// console.warn('Bad link: ' + encrypted);
|
44
|
+
}
|
45
|
+
return encrypted;
|
41
46
|
}
|
42
|
-
return encrypted;
|
43
|
-
}
|
44
47
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
48
|
+
var handler = function (part) {
|
49
|
+
if (!part || part.length % 4 == 1) return !1;
|
50
|
+
for (var t, i, o = 0, s = 0, a = ''; i = part.charAt(s++);) {
|
51
|
+
i = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/='.indexOf(i)
|
52
|
+
~i && (t = o % 4 ? 64 * t + i : i, o++ % 4) && (a += String.fromCharCode(255 & t >> (-2 * o & 6)));
|
53
|
+
}
|
54
|
+
return a;
|
50
55
|
}
|
51
|
-
return a;
|
52
|
-
}
|
53
56
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
57
|
+
var utility_object = {
|
58
|
+
i: function(e, t) {
|
59
|
+
return utility_object.s(e, t ^ vk_id);
|
60
|
+
},
|
61
|
+
s: function(e, t) {
|
62
|
+
var n = e.length;
|
63
|
+
if (n) {
|
64
|
+
var i = r_func(e, t),
|
65
|
+
o = 0;
|
66
|
+
for (e = e.split(''); ++o < n;)
|
67
|
+
e[o] = e.splice(i[n - 1 - o], 1, e[o])[0];
|
68
|
+
e = e.join('')
|
69
|
+
}
|
70
|
+
return e;
|
71
|
+
}
|
72
|
+
};
|
73
|
+
|
74
|
+
var r_func = function (e, t) {
|
75
|
+
var n = e.length,
|
76
|
+
i = [];
|
60
77
|
if (n) {
|
61
|
-
var
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
e = e.join('')
|
78
|
+
var o = n;
|
79
|
+
for (t = Math.abs(t); o--;)
|
80
|
+
t = (n * (o + 1) ^ t + o) % n,
|
81
|
+
i[o] = t;
|
66
82
|
}
|
67
|
-
return
|
68
|
-
}
|
69
|
-
};
|
70
|
-
|
71
|
-
var r_func = function (e, t) {
|
72
|
-
var n = e.length,
|
73
|
-
i = [];
|
74
|
-
if (n) {
|
75
|
-
var o = n;
|
76
|
-
for (t = Math.abs(t); o--;)
|
77
|
-
t = (n * (o + 1) ^ t + o) % n,
|
78
|
-
i[o] = t;
|
83
|
+
return i;
|
79
84
|
}
|
80
|
-
|
85
|
+
|
86
|
+
return audioUnmaskSource(link);
|
81
87
|
}
|
88
|
+
HEREDOC
|
82
89
|
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
@@js_context = ExecJS.compile(js_code)
|
91
|
+
|
92
|
+
# Unmask audio download URL.
|
93
|
+
#
|
94
|
+
# ===== Parameters:
|
95
|
+
# * [+link+] (+String+) - encoded link to audio. Usually looks like "https://m.vk.com/mp3/audio_api_unavailable.mp3?extra=...".
|
96
|
+
# * [+client_id+] (+Integer+) - ID of user which got this link. ID is required for decoding.
|
97
|
+
#
|
98
|
+
# ===== Returns:
|
99
|
+
# * (+String+) - audio download URL, which can be used from current IP.
|
100
|
+
def self.unmask_link(link, client_id)
|
101
|
+
@@js_context.call("vk_unmask_link", link.to_s, client_id.to_i)
|
102
|
+
end
|
86
103
|
|
87
|
-
@@js_context = ExecJS.compile(js_code)
|
88
|
-
|
89
|
-
def self.unmask_link(link, client_id)
|
90
|
-
@@js_context.call("vk_unmask_link", link.to_s, client_id.to_i)
|
91
104
|
end
|
92
105
|
|
93
|
-
end
|
106
|
+
end
|
data/lib/vk_music/playlist.rb
CHANGED
@@ -1,39 +1,74 @@
|
|
1
1
|
module VkMusic
|
2
2
|
|
3
|
+
# VK playlist. Extended with Enumerable.
|
3
4
|
class Playlist
|
4
5
|
include Enumerable
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# Playlist id.
|
8
|
+
attr_reader :id
|
9
|
+
# Owner of playlist.
|
10
|
+
attr_reader :owner_id
|
11
|
+
# Access hash which should be part of link for some playlists.
|
12
|
+
attr_reader :access_hash
|
13
|
+
# Playlist title.
|
14
|
+
attr_reader :title
|
15
|
+
# Playlist subtitle. May be empty.
|
16
|
+
attr_reader :subtitle
|
12
17
|
|
18
|
+
# Return string describing playlist in Russian.
|
13
19
|
def to_s
|
14
20
|
(@subtitle.empty? ? "" : "#{@subtitle} - ") + "#{@title} (#{self.length} аудиозаписей)"
|
15
21
|
end
|
16
22
|
|
23
|
+
# Same to +to_s+, but also outputs list of audios.
|
17
24
|
def pp
|
18
25
|
"#{to_s}:\n#{@list.map(&:to_s).join("\n")}"
|
19
26
|
end
|
20
27
|
|
28
|
+
# Returns audios array.
|
21
29
|
def to_a
|
22
30
|
@list.dup
|
23
31
|
end
|
24
32
|
|
33
|
+
# :nodoc:
|
25
34
|
def each(&block)
|
26
35
|
@list.each(&block)
|
27
36
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
|
38
|
+
# :stopdoc:
|
39
|
+
def length
|
40
|
+
@list.length
|
31
41
|
end
|
32
|
-
|
42
|
+
alias size length
|
43
|
+
|
33
44
|
def empty?
|
34
45
|
@list.empty?
|
35
46
|
end
|
47
|
+
# :startdoc:
|
48
|
+
|
49
|
+
# Access to audios from playlist.
|
50
|
+
#
|
51
|
+
# ===== Parameters:
|
52
|
+
# * [+index+] (+Integer+) - index of audio (starting from 0).
|
53
|
+
#
|
54
|
+
# ===== Returns:
|
55
|
+
# * (+Audio+, +nil+) - audio or +nil+ if out of range.
|
56
|
+
def [](index)
|
57
|
+
@list[index]
|
58
|
+
end
|
36
59
|
|
60
|
+
# Initialize new playlist.
|
61
|
+
#
|
62
|
+
# ===== Parameters:
|
63
|
+
# * [+list+] (+Array+) - list of audios in album.
|
64
|
+
# * [+options+] (+Hash+)
|
65
|
+
#
|
66
|
+
# ===== Options:
|
67
|
+
# * [+:id+]
|
68
|
+
# * [+:owner_id+]
|
69
|
+
# * [+:access_hash+]
|
70
|
+
# * [+:title+]
|
71
|
+
# * [+:subtitle+]
|
37
72
|
def initialize(list, options = {})
|
38
73
|
# Arguments check
|
39
74
|
raise ArgumentError, "array of audios must be provided", caller unless list.class == Array
|
data/lib/vk_music/utility.rb
CHANGED
@@ -2,27 +2,52 @@ require "cgi"
|
|
2
2
|
|
3
3
|
module VkMusic
|
4
4
|
|
5
|
+
# Utility methods.
|
5
6
|
module Utility
|
6
7
|
|
8
|
+
# Turn amount of seconds into string.
|
9
|
+
#
|
10
|
+
# ===== Parameters:
|
11
|
+
# * [+s+] (+Integer+) - amount of seconds.
|
12
|
+
#
|
13
|
+
# ===== Returns:
|
14
|
+
# * (+String+) - formatted string.
|
7
15
|
def self.format_seconds(s)
|
8
16
|
s = s.to_i # Require integer
|
9
17
|
"#{(s / 60).to_s.rjust(2, "0")}:#{(s % 60).to_s.rjust(2, "0")}";
|
10
18
|
end
|
11
19
|
|
20
|
+
# Guess type of request by from string.
|
21
|
+
#
|
22
|
+
# ===== Parameters:
|
23
|
+
# * [+str+] (+String+) - request from user for some audios.
|
24
|
+
#
|
25
|
+
# ===== Returns:
|
26
|
+
# * +:playlist+ - if string match playlist URL.
|
27
|
+
# * +:post+ - if string match post URL.
|
28
|
+
# * +:audios+ - if string match user or group URL.
|
29
|
+
# * +:find+ - in rest of cases.
|
12
30
|
def self.guess_request_type(str)
|
13
31
|
# Guess what type of request is this. Returns Symbol: :find, :playlist, :audios
|
14
32
|
case str
|
15
|
-
when PLAYLIST_URL_REGEX
|
33
|
+
when Constants::PLAYLIST_URL_REGEX
|
16
34
|
:playlist
|
17
|
-
when POST_URL_REGEX
|
35
|
+
when Constants::POST_URL_REGEX
|
18
36
|
:post
|
19
|
-
when VK_URL_REGEX
|
37
|
+
when Constants::VK_URL_REGEX
|
20
38
|
:audios
|
21
39
|
else
|
22
40
|
:find
|
23
41
|
end
|
24
42
|
end
|
25
43
|
|
44
|
+
# Turn hash into URL query string.
|
45
|
+
#
|
46
|
+
# ===== Parameters:
|
47
|
+
# * [+hash+] (+Hash+)
|
48
|
+
#
|
49
|
+
# ===== Returns:
|
50
|
+
# * (+String+)
|
26
51
|
def self.hash_to_params(hash = {})
|
27
52
|
qs = ""
|
28
53
|
hash.each_key do |key|
|
@@ -37,6 +62,7 @@ module VkMusic
|
|
37
62
|
qs
|
38
63
|
end
|
39
64
|
|
65
|
+
# Send warning.
|
40
66
|
def self.warn(*args)
|
41
67
|
if defined?(Warning.warn)
|
42
68
|
Warning.warn args.join("\n")
|
data/vk_music.gemspec
CHANGED
@@ -2,7 +2,7 @@ Gem::Specification.new do |s|
|
|
2
2
|
s.name = "vk_music"
|
3
3
|
s.summary = "Provides interface to work with VK music via HTTP requests"
|
4
4
|
s.description = "Library to work with audios on popular Russian social network vk.com. VK disabled their public API for audios, so it is now necessary to use parsers instead."
|
5
|
-
s.version = "1.1.
|
5
|
+
s.version = "1.1.1"
|
6
6
|
s.author = "Kuznetsov Vladislav"
|
7
7
|
s.email = "fizvlad@mail.ru"
|
8
8
|
s.homepage = "https://github.com/fizvlad/vk-music-rb"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vk_music
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kuznetsov Vladislav
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mechanize
|