vk_music 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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