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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ccf9ed379a89eda14ad649e19b7b5626045d6d348d4bdb0855155ab83cae752
4
- data.tar.gz: de7cd8866f59df7bea26f5cffa29900361d70e3559704b26d93928b657172d66
3
+ metadata.gz: 0c6e2d05ccf44bc32534cc66a85c2495f799e247876495df962bd312b3cef644
4
+ data.tar.gz: acc2fab3d0f196536f195cecea4f5bd0c997a6e14566a9a8a574b444db98bf4e
5
5
  SHA512:
6
- metadata.gz: 1d4ebee0fb3e0cd2a38fc077de818e167c6f154a3c065ce524359f1c2dd9e3813dfbeb53ebcedf68e91d401f0be90647a851a97a527ae6d985364105a41f363f
7
- data.tar.gz: 41122b9abf3f900c5ea56b063368f1e902eefc7afa939f3ea2febd18fd5978394ac18f6ade0abd7446aa064d95ddc73859f1c15f3a9144a34c07a5ad7c280149
6
+ metadata.gz: 9f31a0049dc14a0ef0603f503733593a5bd8ffc2a26cae4ce4c6bb4ace98d175f86745ead97e2293d9157a2bf62b323c2f61254c01940b37505715c35454fdbc
7
+ data.tar.gz: 20fa21e9ee919f544664c20c149089ce594967bafa9809e7ab28ed68f53ab68d0147f5451fac66d83fe4453d1e3c388614fc543958f024d911ff24439cf94bb5
@@ -2,10 +2,38 @@ require "cgi"
2
2
 
3
3
  module VkMusic
4
4
 
5
+ # VK audio.
5
6
  class Audio
6
7
 
7
- attr_reader :id, :owner_id, :secret_1, :secret_2, :artist, :title, :duration, :url, :url_encoded
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
 
@@ -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
- attr_reader :id, :name
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
- def get_audios(obj, up_to = nil)
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(obj.to_s)
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
@@ -1,41 +1,46 @@
1
1
  module VkMusic
2
2
 
3
- # Web
4
- # DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1636.0 Safari/537.36"
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
- VK_URL = {
7
- :scheme => "https",
8
- :host => "m.vk.com",
9
- :home => "https://m.vk.com",
10
- :profile => "https://m.vk.com/id0",
11
- :feed => "https://m.vk.com/feed",
12
- :audios => "https://m.vk.com/audio",
13
- :login => "https://m.vk.com/login",
14
- :login_action => "https://login.vk.com",
15
- :wall => "https://m.vk.com/wall"
16
- }
17
-
18
- VK_LOGIN_FORM_NAMES = {
19
- :username => "email",
20
- :password => "pass",
21
- }
22
-
23
-
24
- VK_ID_REGEX = /^-?\d+$/
25
- VK_AUDIOS_REGEX = /^audios-?\d+$/
26
- 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
27
- VK_CUSTOM_ID_REGEX = /^\w+$/
28
- VK_URL_REGEX = /(?:https?:\/\/)?(?:m\.|www\.)?vk\.com\/([\w\-]+)/
29
-
30
- VK_HREF_ID_CONTAINING_REGEX = /(?:audios|photo|write|owner_id=|friends\?id=)-?\d+/
31
-
32
- # Playlist
33
- PLAYLIST_URL_REGEX = /.*audio_playlist(-?\d+)_(\d+)(?:(?:(?:&access_hash=)|\/|%2F)([\da-z]+))?/
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
@@ -1,30 +1,35 @@
1
1
  module VkMusic
2
2
 
3
- # General class for all the errors
4
- class VkMusicError < RuntimeError; end
3
+ # Exceptions.
4
+ module Exceptions
5
+ # General class for all the errors.
6
+ class VkMusicError < RuntimeError; end
5
7
 
6
- # Failed to login
7
- class LoginError < VkMusicError; end
8
-
9
- # Unable to parse audios from somewhere
10
- class AudiosParseError < VkMusicError; end
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
- # Unable to load or parse audios section from json
16
- class AudiosSectionParseError < AudiosParseError; end
14
+ # Unable to find playlist or got permission error.
15
+ class PlaylistParseError < AudiosParseError; end
17
16
 
18
- # Unable to load or parse all of audios by ids
19
- class ReloadAudiosParseError < AudiosParseError; end
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
- # Unable to parse audios from wall
25
- class WallParseError < AudiosParseError; end
20
+ # Unable to load or parse all of audios by ids.
21
+ class ReloadAudiosParseError < AudiosParseError; end
26
22
 
27
- # Unable to parse audios from post
28
- class PostParseError < AudiosParseError; end
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
- # Setting up decoding context
6
- js_code = <<~HEREDOC
7
- function vk_unmask_link(link, vk_id) {
5
+ # Module containing link decoding utilities.
6
+ module LinkDecoder
8
7
 
9
- // Utility functions to unmask
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
- var audioUnmaskSource = function (encrypted) {
12
- if (encrypted.indexOf('audio_api_unavailable') != -1) {
13
- var parts = encrypted.split('?extra=')[1].split('#');
12
+ // Utility functions to unmask
14
13
 
15
- var handled_anchor = '' === parts[1] ? '' : handler(parts[1]);
14
+ var audioUnmaskSource = function (encrypted) {
15
+ if (encrypted.indexOf('audio_api_unavailable') != -1) {
16
+ var parts = encrypted.split('?extra=')[1].split('#');
16
17
 
17
- var handled_part = handler(parts[0]);
18
+ var handled_anchor = '' === parts[1] ? '' : handler(parts[1]);
18
19
 
19
- if (typeof handled_anchor != 'string' || !handled_part) {
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
- for (var func_key, splited_anchor, l = handled_anchor.length; l--;) {
28
- splited_anchor = handled_anchor[l].split(String.fromCharCode(11));
29
- func_key = splited_anchor.splice(0, 1, handled_part)[0];
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
- if (handled_part && 'http' === handled_part.substr(0, 4)) return handled_part;
38
- // else console.warn('Failed unmasking: ' + handled_part);
39
- } else {
40
- // console.warn('Bad link: ' + encrypted);
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
- var handler = function (part) {
46
- if (!part || part.length % 4 == 1) return !1;
47
- for (var t, i, o = 0, s = 0, a = ''; i = part.charAt(s++);) {
48
- i = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/='.indexOf(i)
49
- ~i && (t = o % 4 ? 64 * t + i : i, o++ % 4) && (a += String.fromCharCode(255 & t >> (-2 * o & 6)));
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
- var utility_object = {
55
- i: function(e, t) {
56
- return utility_object.s(e, t ^ vk_id);
57
- },
58
- s: function(e, t) {
59
- var n = e.length;
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 i = r_func(e, t),
62
- o = 0;
63
- for (e = e.split(''); ++o < n;)
64
- e[o] = e.splice(i[n - 1 - o], 1, e[o])[0];
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 e;
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
- return i;
85
+
86
+ return audioUnmaskSource(link);
81
87
  }
88
+ HEREDOC
82
89
 
83
- return audioUnmaskSource(link);
84
- }
85
- HEREDOC
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
@@ -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
- attr_reader :id, :owner_id, :access_hash, :title, :subtitle
7
-
8
- def length
9
- @list.length
10
- end
11
- alias size length
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
- def [](index)
30
- @list[index]
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
@@ -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.0"
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.0
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-07-29 00:00:00.000000000 Z
11
+ date: 2019-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize