vk_music 1.1.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -7
- data/Rakefile +5 -0
- data/lib/vk_music.rb +3 -3
- data/lib/vk_music/audio.rb +143 -79
- data/lib/vk_music/client.rb +543 -250
- data/lib/vk_music/constants.rb +78 -33
- data/lib/vk_music/exceptions.rb +14 -17
- data/lib/vk_music/link_decoder.rb +9 -5
- data/lib/vk_music/playlist.rb +47 -36
- data/lib/vk_music/utility.rb +57 -19
- data/test/test_attached_audios_amount.rb +82 -0
- data/test/test_audios.rb +92 -0
- data/test/test_find.rb +43 -0
- data/test/test_from_id.rb +61 -0
- data/test/test_last_post_id.rb +46 -0
- data/test/test_login.rb +1 -1
- data/test/test_page_id.rb +113 -0
- data/test/test_playlist.rb +39 -10
- data/test/test_post.rb +21 -19
- data/test/test_wall.rb +56 -0
- data/vk_music.gemspec +4 -2
- metadata +31 -11
- data/test/test_audios_by_id.rb +0 -49
- data/test/test_get_id.rb +0 -113
- data/test/test_search.rb +0 -30
- data/test/test_user_or_group_audios.rb +0 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e42dd22de66cd8bb8d2d0b70686102d4d50078677736b036b7c383470ff31f2a
|
4
|
+
data.tar.gz: c6d3eb20221b599d48a1e4464b837ef294d926b4bc9fbeb81b348e9ae5c63f90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 515fbb7b67a8a53b0e2bc8ed0ef8f34a4cdda560ad94a4880ff8e9b36fb7d3c023386b6c2fca3d0df59eb19487d5a708d21684ddb3bf25bcbda4b2141506dd63
|
7
|
+
data.tar.gz: 40eac37ca4af3db6eb0d88a7e93277f923565c013207706e0d29e7b14a18354628d9bdb79b8eb71de753571a29a4ed93bbeb0e3adad697a4cdc6cfed3c9598a0
|
data/README.md
CHANGED
@@ -29,6 +29,8 @@ gem install vk_music-*.gem
|
|
29
29
|
|
30
30
|
## Usage
|
31
31
|
|
32
|
+
You can take a look on documentation [here](https://www.rubydoc.info/gems/vk_music/).
|
33
|
+
|
32
34
|
### Logging in
|
33
35
|
Firstly, it is required to create new *VkMusic::Client* and provide login credentials:
|
34
36
|
```ruby
|
@@ -38,7 +40,7 @@ client = VkMusic::Client.new(username: "+71234567890", password: "password")
|
|
38
40
|
### Searching for audios
|
39
41
|
You can search audios by name with following method:
|
40
42
|
```ruby
|
41
|
-
audios = client.
|
43
|
+
audios = client.find("Acid Spit - Mega Drive")
|
42
44
|
puts audios[0] # Basic information about audio
|
43
45
|
puts audios[0].url # URL to access audio. Notice that it is only accessible from your IP
|
44
46
|
```
|
@@ -46,28 +48,28 @@ puts audios[0].url # URL to access audio. Notice that it is only accessible from
|
|
46
48
|
### Parsing playlists
|
47
49
|
You can load all the audios from playlist using following method:
|
48
50
|
```ruby
|
49
|
-
playlist = client.
|
51
|
+
playlist = client.playlist("https://vk.com/audio?z=audio_playlist-37661843_1/0e420c32c8b69e6637")
|
50
52
|
```
|
51
53
|
It is only possible to load up to 100 audios from playlist per request, so you can reduce amount of requests by setting up how many audios from playlist you actually need.
|
52
54
|
For example, following method will perform only one HTML request:
|
53
55
|
```ruby
|
54
|
-
playlist = client.
|
56
|
+
playlist = client.playlist("https://vk.com/audio?z=audio_playlist121570739_7", up_to: 100)
|
55
57
|
urls = playlist.map(&:url) # URLs for every audio
|
56
58
|
```
|
57
59
|
|
58
60
|
### User or group audios
|
59
61
|
You can load first 100 audios from user or group page. Those audios will be returned as playlist. To do it simply pass user or group id:
|
60
62
|
```ruby
|
61
|
-
user_playlist = client.
|
62
|
-
group_playlist = client.
|
63
|
+
user_playlist = client.audios(owner_id: 8024985)
|
64
|
+
group_playlist = client.audios(owner_id: -4790861) # Group and public id starts with '-'
|
63
65
|
```
|
64
66
|
You can set how many audios you actually need as well:
|
65
67
|
```ruby
|
66
|
-
user_playlist = client.
|
68
|
+
user_playlist = client.audios(owner_id: 8024985, up_to: 10)
|
67
69
|
```
|
68
70
|
|
69
71
|
### Audios from post
|
70
72
|
You can load up to 10 audios attached to some post. Those audios will be returned as array:
|
71
73
|
```ruby
|
72
|
-
audios = client.
|
74
|
+
audios = client.post("https://vk.com/wall-4790861_5453")
|
73
75
|
```
|
data/Rakefile
CHANGED
data/lib/vk_music.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require_relative "vk_music/utility.rb"
|
2
1
|
require_relative "vk_music/constants.rb"
|
3
2
|
require_relative "vk_music/exceptions.rb"
|
4
|
-
require_relative "vk_music/
|
3
|
+
require_relative "vk_music/utility.rb"
|
5
4
|
require_relative "vk_music/link_decoder.rb"
|
5
|
+
require_relative "vk_music/audio.rb"
|
6
6
|
require_relative "vk_music/playlist.rb"
|
7
|
-
require_relative "vk_music/client.rb"
|
7
|
+
require_relative "vk_music/client.rb"
|
data/lib/vk_music/audio.rb
CHANGED
@@ -1,138 +1,202 @@
|
|
1
1
|
require "cgi"
|
2
2
|
|
3
|
+
##
|
4
|
+
# @!macro [new] options_hash_param
|
5
|
+
# @param options [Hash] hash with options.
|
6
|
+
|
3
7
|
module VkMusic
|
4
8
|
|
5
|
-
|
9
|
+
##
|
10
|
+
# Class representing VK audio.
|
6
11
|
class Audio
|
7
12
|
|
8
|
-
|
13
|
+
##
|
14
|
+
# @return [Integer, nil] ID of audio.
|
9
15
|
attr_reader :id
|
10
|
-
|
16
|
+
|
17
|
+
##
|
18
|
+
# @return [Integer, nil] ID of audio owner.
|
11
19
|
attr_reader :owner_id
|
12
|
-
|
20
|
+
|
21
|
+
##
|
22
|
+
# @return [String, nil] part of secret hash which used when using +act=reload_audio+.
|
13
23
|
attr_reader :secret_1, :secret_2
|
14
|
-
|
24
|
+
|
25
|
+
##
|
26
|
+
# @return [String] name of artist.
|
15
27
|
attr_reader :artist
|
16
|
-
|
28
|
+
|
29
|
+
##
|
30
|
+
# @return [String] title of song.
|
17
31
|
attr_reader :title
|
18
|
-
|
32
|
+
|
33
|
+
##
|
34
|
+
# @return [Integer] duration of track in seconds.
|
19
35
|
attr_reader :duration
|
20
|
-
# Download URL.
|
21
|
-
attr_reader :url
|
22
|
-
# Encoded URL.
|
23
|
-
attr_reader :url_encoded
|
24
36
|
|
25
|
-
|
37
|
+
##
|
38
|
+
# Access decoded download URL.
|
26
39
|
#
|
27
|
-
# If
|
28
|
-
# If
|
40
|
+
# If link was already decoded, returns cached value. Else decodes existing link.
|
41
|
+
# If no link can be provided, returns +nil+.
|
29
42
|
#
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# * +:client_id+
|
37
|
-
def update_url(options)
|
38
|
-
raise ArgumentError, "options hash must be provided", caller unless options.class == Hash
|
39
|
-
if !options[:url].to_s.empty?
|
40
|
-
@url_encoded = ""
|
41
|
-
@url = options[:url].to_s
|
42
|
-
elsif !options[:url].to_s.empty? && options[:client_id]
|
43
|
-
@url_encoded = options[:url_encoded].to_s
|
44
|
-
@url = VkMusic::LinkDecoder.unmask_link(options[:url_encoded], options[:client_id])
|
43
|
+
# @return [String, nil] decoded download URL or +nil+ if not available.
|
44
|
+
def url
|
45
|
+
if url_cached?
|
46
|
+
@url
|
47
|
+
elsif @url_encoded && @client_id
|
48
|
+
@url = VkMusic::LinkDecoder.unmask_link(@url_encoded, @client_id)
|
45
49
|
else
|
46
|
-
|
50
|
+
@url # nil
|
47
51
|
end
|
48
52
|
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# @return [String, nil] encoded download URL.
|
56
|
+
attr_reader :url_encoded
|
57
|
+
|
58
|
+
##
|
59
|
+
# @return [Boolean] whether decoded URL is already cached.
|
60
|
+
def url_cached?
|
61
|
+
!!(@url)
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# @return [Boolean] whether able to get download URL without web requests.
|
66
|
+
def url_available?
|
67
|
+
!!(url_cached? || (@url_encoded && @client_id))
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# @return [Boolean] whether it's possible to get download URL with {Client#from_id}.
|
72
|
+
def url_accessable?
|
73
|
+
!!(@id && @owner_id && @secret_1 && @secret_2)
|
74
|
+
end
|
49
75
|
|
50
|
-
|
76
|
+
##
|
77
|
+
# @return [String] information about audio.
|
51
78
|
def to_s
|
52
79
|
"#{@artist} - #{@title} [#{Utility.format_seconds(@duration)}]"
|
53
80
|
end
|
54
81
|
|
55
|
-
|
82
|
+
##
|
83
|
+
# @return [String] extended information about audio.
|
56
84
|
def pp
|
57
|
-
"#{to_s} (
|
85
|
+
"#{to_s} (Able to get decoded URL: #{url_available? ? "yes" : "no"}, able to get URL from VK: #{url_accessable? ? "yes" : "no"})"
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Update audio URLs.
|
90
|
+
#
|
91
|
+
# @overload update_url(decoded_url)
|
92
|
+
# Simply save download URL.
|
93
|
+
# @param decoded_url [String]
|
94
|
+
#
|
95
|
+
# @overload update_url(audio)
|
96
|
+
# Copy URLs from this audio (no checks applied).
|
97
|
+
# @param audio [Audio]
|
98
|
+
#
|
99
|
+
# @overload update_url(options)
|
100
|
+
# @macro options_hash_param
|
101
|
+
# @option options [String, nil] :url decoded download URL.
|
102
|
+
# @option options [String, nil] :url_encoded decoded download URL.
|
103
|
+
# @option options [String, nil] :client_id decoded download URL.
|
104
|
+
#
|
105
|
+
# @return [self]
|
106
|
+
def update_url(arg)
|
107
|
+
case arg
|
108
|
+
when String
|
109
|
+
@url = arg
|
110
|
+
when Audio
|
111
|
+
# Only save URL if it's already decoded and cached
|
112
|
+
@url = arg.url if arg.url_cached?
|
113
|
+
@url_encoded = arg.url_encoded
|
114
|
+
@client_id = arg.client_id
|
115
|
+
when Hash
|
116
|
+
@url_encoded = Utility.unless_nil_to String, options[:url_encoded]
|
117
|
+
@url = Utility.unless_nil_to String, options[:url]
|
118
|
+
@client_id = Utility.unless_nil_to Integer, options[:client_id]
|
119
|
+
else
|
120
|
+
raise ArgumentError, "Bad arguments", caller
|
121
|
+
end
|
122
|
+
self
|
58
123
|
end
|
59
124
|
|
125
|
+
##
|
60
126
|
# Initialize new audio.
|
61
127
|
#
|
62
|
-
#
|
63
|
-
# * [+options+] (+Hash+)
|
128
|
+
# @macro options_hash_param
|
64
129
|
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
def initialize(options)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
raise ArgumentError, "duration is not provided", caller unless options.has_key?(:duration)
|
81
|
-
|
82
|
-
# Setting up attributes
|
83
|
-
@id = options[:id].to_s
|
84
|
-
@owner_id = options[:owner_id].to_s
|
85
|
-
@secret_1 = options[:secret_1].to_s
|
86
|
-
@secret_2 = options[:secret_2].to_s
|
130
|
+
# @option options [Integer, nil] :id
|
131
|
+
# @option options [Integer, nil] :owner_id
|
132
|
+
# @option options [String, nil] :secret_1
|
133
|
+
# @option options [String, nil] :secret_2
|
134
|
+
# @option options [String] :artist
|
135
|
+
# @option options [String] :title
|
136
|
+
# @option options [Integer] :duration
|
137
|
+
# @option options [String, nil] :url_encoded
|
138
|
+
# @option options [String, nil] :url
|
139
|
+
# @option options [Integer, nil] :client_id
|
140
|
+
def initialize(options = {})
|
141
|
+
@id = Utility.unless_nil_to Integer, options[:id]
|
142
|
+
@owner_id = Utility.unless_nil_to Integer, options[:owner_id]
|
143
|
+
@secret_1 = Utility.unless_nil_to String, options[:secret_1]
|
144
|
+
@secret_2 = Utility.unless_nil_to String, options[:secret_2]
|
87
145
|
@artist = options[:artist].to_s
|
88
146
|
@title = options[:title].to_s
|
89
147
|
@duration = options[:duration].to_i
|
90
|
-
@url_encoded = options[:url_encoded]
|
91
|
-
@url = options[:url]
|
148
|
+
@url_encoded = Utility.unless_nil_to String, options[:url_encoded]
|
149
|
+
@url = Utility.unless_nil_to String, options[:url]
|
150
|
+
@client_id = Utility.unless_nil_to Integer, options[:client_id]
|
92
151
|
end
|
93
152
|
|
153
|
+
##
|
94
154
|
# Initialize new audio from Nokogiri HTML node.
|
95
155
|
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
156
|
+
# @param node [Nokogiri::XML::Node] node, which match following CSS selector: +.audio_item.ai_has_btn+
|
157
|
+
# @param client_id [Integer]
|
158
|
+
#
|
159
|
+
# @return [Audio]
|
99
160
|
def self.from_node(node, client_id)
|
100
161
|
url_encoded = node.at_css("input").attribute("value").to_s
|
101
|
-
url_encoded = nil if url_encoded ==
|
162
|
+
url_encoded = nil if url_encoded == Constants::URL::VK[:audio_unavailable]
|
102
163
|
id_array = node.attribute("data-id").to_s.split("_")
|
103
164
|
|
104
165
|
new({
|
105
|
-
:id => id_array[1],
|
106
|
-
:owner_id => id_array[0],
|
166
|
+
:id => id_array[1].to_i,
|
167
|
+
:owner_id => id_array[0].to_i,
|
107
168
|
:artist => node.at_css(".ai_artist").text.strip,
|
108
169
|
:title => node.at_css(".ai_title").text.strip,
|
109
170
|
:duration => node.at_css(".ai_dur").attribute("data-dur").to_s.to_i,
|
110
|
-
:url_encoded => url_encoded,
|
111
|
-
:url =>
|
171
|
+
:url_encoded => url_encoded.to_s,
|
172
|
+
:url => nil,
|
173
|
+
:client_id => client_id
|
112
174
|
})
|
113
175
|
end
|
114
176
|
|
115
|
-
|
177
|
+
##
|
178
|
+
# Initialize new audio from VK data array.
|
179
|
+
#
|
180
|
+
# @param data [Array]
|
181
|
+
# @param client_id [Integer]
|
116
182
|
#
|
117
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
def self.from_data_array(data, client_id)
|
121
|
-
url_encoded = data[2]
|
122
|
-
url_encoded = nil if url_encoded == ""
|
183
|
+
# @return [Audio]
|
184
|
+
def self.from_data(data, client_id)
|
185
|
+
url_encoded = data[2].to_s
|
123
186
|
|
124
|
-
secrets = data[13].split("/")
|
187
|
+
secrets = data[13].to_s.split("/")
|
125
188
|
|
126
189
|
new({
|
127
|
-
:id => data[0],
|
128
|
-
:owner_id => data[1],
|
190
|
+
:id => data[0].to_i,
|
191
|
+
:owner_id => data[1].to_i,
|
129
192
|
:secret_1 => secrets[3],
|
130
193
|
:secret_2 => secrets[5],
|
131
194
|
:artist => CGI.unescapeHTML(data[4]),
|
132
195
|
:title => CGI.unescapeHTML(data[3]),
|
133
|
-
:duration => data[5],
|
196
|
+
:duration => data[5].to_i,
|
134
197
|
:url_encoded => url_encoded,
|
135
|
-
:url =>
|
198
|
+
:url => nil,
|
199
|
+
:client_id => client_id
|
136
200
|
})
|
137
201
|
end
|
138
202
|
|
data/lib/vk_music/client.rb
CHANGED
@@ -1,334 +1,525 @@
|
|
1
1
|
require "mechanize"
|
2
2
|
require "json"
|
3
3
|
|
4
|
+
##
|
5
|
+
# @!macro [new] options_hash_param
|
6
|
+
# @param options [Hash] hash with options.
|
7
|
+
#
|
8
|
+
# @!macro [new] playlist_return
|
9
|
+
# @return [Playlist] playlist with audios. Possibly empty.
|
10
|
+
# Possibly contains audios without download URL.
|
11
|
+
|
12
|
+
##
|
13
|
+
# Main module.
|
4
14
|
module VkMusic
|
5
15
|
|
16
|
+
##
|
6
17
|
# Main class with all the interface.
|
18
|
+
# To start working with VK audios firstly create new client with +Client.new+.
|
7
19
|
class Client
|
8
20
|
|
9
|
-
|
21
|
+
##
|
22
|
+
# @return [Integer] ID of client.
|
10
23
|
attr_reader :id
|
11
|
-
|
24
|
+
|
25
|
+
##
|
26
|
+
# @return [String] name of client.
|
12
27
|
attr_reader :name
|
13
28
|
|
14
|
-
# Mechanize agent
|
15
|
-
|
16
|
-
|
17
|
-
|
29
|
+
@agent = nil # Mechanize agent
|
30
|
+
|
31
|
+
##
|
32
|
+
# Create new client and login.
|
33
|
+
#
|
34
|
+
# @macro options_hash_param
|
35
|
+
#
|
36
|
+
# @option options [String] :username usually telephone number or email.
|
37
|
+
# @option options [String] :password
|
38
|
+
def initialize(options = {})
|
18
39
|
# Arguments check
|
19
|
-
raise ArgumentError, "
|
20
|
-
raise ArgumentError, "
|
21
|
-
raise ArgumentError, "
|
40
|
+
raise ArgumentError, "Options hash must be provided", caller unless options.class == Hash
|
41
|
+
raise ArgumentError, "Username is not provided", caller unless options.has_key?(:username)
|
42
|
+
raise ArgumentError, "Password is not provided", caller unless options.has_key?(:password)
|
22
43
|
|
23
44
|
# Setting up client
|
24
45
|
@agent = Mechanize.new
|
25
46
|
login(options[:username], options[:password])
|
26
47
|
end
|
48
|
+
|
49
|
+
##
|
50
|
+
#@!group Loading audios
|
27
51
|
|
28
|
-
|
52
|
+
##
|
53
|
+
# Search for audio.
|
54
|
+
#
|
55
|
+
# @note some audios might be removed from search.
|
29
56
|
#
|
30
|
-
#
|
31
|
-
# * [+query+] (+String+) - string to search for.
|
57
|
+
# @todo search in group audios.
|
32
58
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
36
|
-
|
59
|
+
# @overload find(query)
|
60
|
+
# @param query [String] string to search for.
|
61
|
+
#
|
62
|
+
# @overload find(options)
|
63
|
+
# @macro options_hash_param
|
64
|
+
# @option options [String] :query string to search for.
|
65
|
+
#
|
66
|
+
# @return [Array<Audio>] array with audios matching given string. Possibly empty.
|
67
|
+
# Possibly contains audios without download URL.
|
68
|
+
def find(arg)
|
69
|
+
begin
|
70
|
+
case arg
|
71
|
+
when String
|
72
|
+
query = arg
|
73
|
+
when Hash
|
74
|
+
query = arg[:query].to_s
|
75
|
+
else
|
76
|
+
raise
|
77
|
+
end
|
78
|
+
rescue
|
79
|
+
raise ArgumentError, "Bad arguments", caller
|
80
|
+
end
|
81
|
+
|
82
|
+
uri = URI(Constants::URL::VK[:audios])
|
37
83
|
uri.query = Utility.hash_to_params({ "act" => "search", "q" => query.to_s })
|
38
|
-
|
84
|
+
|
85
|
+
audios__from_page(uri)
|
39
86
|
end
|
87
|
+
alias search find
|
40
88
|
|
41
|
-
|
89
|
+
##
|
90
|
+
# @!macro [new] pl__options
|
91
|
+
# @option options [Integer] :up_to (MAXIMUM_PLAYLIST_SIZE) maximum amount of audios to load.
|
92
|
+
# If 0, no audios would be loaded (Just information about playlist).
|
93
|
+
# If less than 0, will load whole playlist.
|
94
|
+
# @option options [Boolean] :with_url (true) makes all the audios have download URLs,
|
95
|
+
# but every 100 audios will cost one more request. You can reduce amount of requests using option +up_to+.
|
96
|
+
# Otherwise audio download URL would be accessable only with {Client#from_id}.
|
97
|
+
# Main advantage of disabling URLs is the fact that 2000 audios will be loaded per request,
|
98
|
+
# which is 20 times more effecient.
|
99
|
+
#
|
100
|
+
# Get VK playlist.
|
42
101
|
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
102
|
+
# @overload playlist(url, options)
|
103
|
+
# @param url [String] URL to playlist.
|
104
|
+
# @macro options_hash_param
|
105
|
+
# @macro pl__options
|
46
106
|
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
|
50
|
-
|
107
|
+
# @overload playlist(options)
|
108
|
+
# Use options +owner_id+, +playlist_id+ and +access_hash+ instead of URL.
|
109
|
+
# @macro options_hash_param
|
110
|
+
# @option options [Integer] :owner_id playlist owner ID.
|
111
|
+
# @option options [Integer] :playlist_id ID of the playlist.
|
112
|
+
# @option options [String] :access_hash access hash to playlist. Might not exist.
|
113
|
+
# @macro pl__options
|
114
|
+
#
|
115
|
+
# @macro playlist_return
|
116
|
+
def playlist(*args)
|
51
117
|
begin
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
if footer_node
|
63
|
-
footer_match = footer_node.text.strip.match(/^\d+/)
|
64
|
-
playlist_size = footer_match ? footer_match[0].to_i : 0
|
65
|
-
else
|
66
|
-
playlist_size = 0
|
118
|
+
case
|
119
|
+
when (args.size == 1 && String === args[0]) ||
|
120
|
+
(args.size == 2 && String === args[0] && Hash === args[1])
|
121
|
+
options = args[1] || {}
|
122
|
+
owner_id, playlist_id, access_hash = args[0].to_s.match(Constants::Regex::VK_PLAYLIST_URL_POSTFIX).captures
|
123
|
+
when args.size == 1 && Hash === args[0]
|
124
|
+
options = args[0]
|
125
|
+
owner_id, playlist_id, access_hash = options[:owner_id].to_i, options[:playlist_id].to_i, options[:access_hash].to_s
|
126
|
+
else
|
127
|
+
raise
|
67
128
|
end
|
68
|
-
rescue
|
69
|
-
raise
|
129
|
+
rescue
|
130
|
+
raise ArgumentError, "Bad arguments", caller
|
70
131
|
end
|
71
|
-
# Now we can be sure we are on correct page
|
72
|
-
|
73
|
-
first_page_audios = load_audios_from_page(first_page)
|
74
132
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
playlist_page = load_playlist_page(owner_id: owner_id, id: id, access_hash: access_hash, offset: list.length)
|
83
|
-
list.concat(load_audios_from_page(playlist_page)[0, up_to - list.length])
|
84
|
-
break if list.length == up_to
|
85
|
-
end
|
133
|
+
options[:up_to] ||= Constants::MAXIMUM_PLAYLIST_SIZE
|
134
|
+
options[:with_url] = true if options[:with_url].nil?
|
135
|
+
|
136
|
+
if options[:with_url]
|
137
|
+
playlist__web(owner_id, playlist_id, access_hash, options)
|
138
|
+
else
|
139
|
+
playlist__json(owner_id, playlist_id, access_hash, options)
|
86
140
|
end
|
87
|
-
|
88
|
-
Playlist.new(list, {
|
89
|
-
:id => id,
|
90
|
-
:owner_id => owner_id,
|
91
|
-
:access_hash => access_hash,
|
92
|
-
:title => title,
|
93
|
-
:subtitle => subtitle,
|
94
|
-
})
|
95
141
|
end
|
96
142
|
|
143
|
+
##
|
144
|
+
# @!macro [new] ua__options
|
145
|
+
# @option options [Integer] :up_to (MAXIMUM_PLAYLIST_SIZE) maximum amount of audios to load.
|
146
|
+
# If 0, no audios would be loaded (Just information about playlist).
|
147
|
+
# If less than 0, will load whole playlist.
|
148
|
+
#
|
97
149
|
# Get user or group audios.
|
98
150
|
#
|
99
|
-
#
|
100
|
-
# * [+url+] (+String+) - URL to user or group.
|
101
|
-
# * [+up_to+] (+Integer+) - maximum amount of Audio to load.
|
151
|
+
# @note currently this method is only able to load 100 audios with download URL.
|
102
152
|
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
# Trying to parse out audios
|
153
|
+
# @overload audios(url, options)
|
154
|
+
# @param url [String] URL to user/group page or audios.
|
155
|
+
# @macro options_hash_param
|
156
|
+
# @macro ua__options
|
157
|
+
#
|
158
|
+
# @overload audios(options)
|
159
|
+
# @macro options_hash_param
|
160
|
+
# @option options [Integer] :owner_id numerical ID of owner.
|
161
|
+
# @macro ua__options
|
162
|
+
#
|
163
|
+
# @macro playlist_return
|
164
|
+
def audios(*args)
|
117
165
|
begin
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
166
|
+
case
|
167
|
+
when (args.size == 1 && String === args[0] ) ||
|
168
|
+
(args.size == 2 && String === args[0] && Hash === args[1])
|
169
|
+
owner_id = page_id(args[0].to_s)
|
170
|
+
options = args[1] || {}
|
171
|
+
when args.size == 1 && Hash === args[0]
|
172
|
+
owner_id = args[0][:owner_id].to_i
|
173
|
+
options = args[0]
|
174
|
+
else
|
175
|
+
raise
|
176
|
+
end
|
177
|
+
rescue
|
178
|
+
raise ArgumentError, "Bad arguments", caller
|
123
179
|
end
|
124
|
-
|
125
|
-
#total_count = first_data["totalCount"] # NOTICE: not used due to restrictions described above
|
126
|
-
total_count = first_data_audios.length # Using this instead
|
127
180
|
|
128
|
-
|
181
|
+
options[:up_to] ||= Constants::MAXIMUM_PLAYLIST_SIZE
|
129
182
|
|
130
|
-
|
131
|
-
list = first_data_audios.first(up_to)
|
132
|
-
|
133
|
-
# It turns out user audios are just playlist with id -1
|
134
|
-
Playlist.new(list, {
|
135
|
-
:id => first_data["id"],
|
136
|
-
:owner_id => first_data["owner_id"],
|
137
|
-
:access_hash => first_data["access_hash"],
|
138
|
-
:title => CGI.unescapeHTML(first_data["title"].to_s),
|
139
|
-
:subtitle => CGI.unescapeHTML(first_data["subtitle"].to_s),
|
140
|
-
})
|
183
|
+
playlist__json(owner_id, -1, nil, options)
|
141
184
|
end
|
142
185
|
|
143
|
-
|
186
|
+
##
|
187
|
+
# @!macro [new] wall__up_to_option
|
188
|
+
# @option up_to [Integer] :up_to (10) maximum amount of audios to load from wall.
|
189
|
+
#
|
190
|
+
# @!macro [new] wall__with_url_option
|
191
|
+
# @option options [Boolean] :with_url (true) automatically use {Client#from_id} to get download URLs.
|
144
192
|
#
|
145
|
-
#
|
146
|
-
# * [+arr+] (+Array+) - Array of objects, which can have different types: Audio or Array[owner_id, id, secret_1, secret_2].
|
193
|
+
# Get audios on wall of user or group starting with given post.
|
147
194
|
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
195
|
+
# @note this method is only able to load up to 91 audios from wall.
|
196
|
+
#
|
197
|
+
# @overload wall(url, options)
|
198
|
+
# Load last audios from wall.
|
199
|
+
# @param url [String] URL to user/group page.
|
200
|
+
# @macro options_hash_param
|
201
|
+
# @macro wall__up_to_option
|
202
|
+
# @macro wall__with_url_option
|
203
|
+
#
|
204
|
+
# @overload wall(options)
|
205
|
+
# Load audios starting from some exact post.
|
206
|
+
# @macro options_hash_param
|
207
|
+
# @option options [Integer] :owner_id numerical ID of wall owner.
|
208
|
+
# @option options [Integer] :post_id numerical ID of post.
|
209
|
+
# @macro wall__up_to_option
|
210
|
+
# @macro wall__with_url_option
|
211
|
+
#
|
212
|
+
# @return [Array<Audio>] array of audios from wall. Possibly empty.
|
213
|
+
def wall(*args)
|
214
|
+
begin
|
215
|
+
case
|
216
|
+
when (args.size == 1 && args[0].class == String) ||
|
217
|
+
(args.size == 2 && args[0].class == String && args[1].class == Hash)
|
218
|
+
url = args[0].to_s
|
219
|
+
owner_id = page_id(url)
|
220
|
+
post_id = last_post_id(owner_id)
|
221
|
+
options = args[1] || {}
|
222
|
+
return [] if post_id.nil?
|
223
|
+
when args.length == 1 && Hash === args[0]
|
224
|
+
options = args[0]
|
225
|
+
owner_id = options[:owner_id].to_i
|
226
|
+
post_id = options[:post_id].to_i
|
162
227
|
else
|
163
|
-
|
228
|
+
raise
|
164
229
|
end
|
230
|
+
rescue Exceptions::ParseError => error
|
231
|
+
raise Exceptions::ParseError, "Unable to get last post id. Error: #{error.message}", caller
|
232
|
+
rescue
|
233
|
+
raise ArgumentError, "Bad arguments", caller
|
165
234
|
end
|
166
|
-
json = load_audios_json_by_id(arr)
|
167
|
-
result = load_audios_from_data(json["data"][0].to_a)
|
168
|
-
raise Exceptions::ReloadAudiosParseError, "Result size don't match: excepected #{arr.size}, got #{result.size}", caller if result.size != arr.size
|
169
235
|
|
170
|
-
|
236
|
+
options[:up_to] ||= 10
|
237
|
+
options[:with_url] = true if options[:with_url].nil?
|
238
|
+
|
239
|
+
wall__json(owner_id, post_id, options)
|
171
240
|
end
|
172
241
|
|
173
|
-
|
242
|
+
##
|
243
|
+
# Get audios attached to post.
|
174
244
|
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
# * [+post_id+] (+Integer+)
|
178
|
-
# * [+up_to+] (+Integer+) - maximum amount of Audio to load.
|
245
|
+
# @overload post(url)
|
246
|
+
# @param url [String] URL to post.
|
179
247
|
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
|
248
|
+
# @overload post(options)
|
249
|
+
# @macro options_hash_param
|
250
|
+
# @option options [Integer] :owner_id numerical ID of wall owner.
|
251
|
+
# @option options [Integer] :post_id numerical ID of post.
|
252
|
+
#
|
253
|
+
# @return [Array<Audio>] audios with download URLs.
|
254
|
+
def post(arg)
|
183
255
|
begin
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
256
|
+
case arg
|
257
|
+
when String
|
258
|
+
owner_id, post_id = arg.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures
|
259
|
+
when Hash
|
260
|
+
options = arg
|
261
|
+
owner_id = options[:owner_id].to_i
|
262
|
+
post_id = options[:post_id].to_i
|
263
|
+
else
|
264
|
+
raise
|
265
|
+
end
|
266
|
+
rescue
|
267
|
+
raise ArgumentError, "Bad arguments", caller
|
189
268
|
end
|
190
269
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
list = get_audios_by_id(*no_url_audios)
|
195
|
-
|
196
|
-
Playlist.new(list, {
|
197
|
-
:id => data["id"],
|
198
|
-
:owner_id => data["owner_id"],
|
199
|
-
:access_hash => data["access_hash"],
|
200
|
-
:title => CGI.unescapeHTML(data["title"].to_s),
|
201
|
-
:subtitle => CGI.unescapeHTML(data["subtitle"].to_s),
|
202
|
-
})
|
270
|
+
amount = attached_audios_amount(owner_id: owner_id, post_id: post_id)
|
271
|
+
wall(owner_id: owner_id, post_id: post_id, up_to: amount)
|
203
272
|
end
|
204
|
-
|
205
|
-
|
273
|
+
|
274
|
+
##
|
275
|
+
# Get audios with download URLs by their IDs and secrets.
|
206
276
|
#
|
207
|
-
#
|
208
|
-
# * [+url+] (+String+)
|
277
|
+
# @note warning: audios must not match.
|
209
278
|
#
|
210
|
-
#
|
211
|
-
#
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
279
|
+
# @todo workaround for not unique audios in request
|
280
|
+
#
|
281
|
+
# @param args [Array<Audio, Array<(owner_id, audio_id, secret_1, secret_2)>, "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
|
282
|
+
#
|
283
|
+
# @return [Array<Audio>] array of audios with download URLs.
|
284
|
+
def from_id(args)
|
285
|
+
begin
|
286
|
+
args.map! do |el|
|
287
|
+
case el
|
288
|
+
when Array
|
289
|
+
el.join("_")
|
290
|
+
when Audio
|
291
|
+
"#{el.owner_id}_#{el.id}_#{el.secret_1}_#{el.secret_2}"
|
292
|
+
when String
|
293
|
+
el # Do not change
|
294
|
+
else
|
295
|
+
raise
|
296
|
+
end
|
297
|
+
end
|
298
|
+
rescue
|
299
|
+
raise ArgumentError, "Bad arguments", caller
|
300
|
+
end
|
301
|
+
|
302
|
+
result = []
|
303
|
+
args.each_slice(10) do |subarray|
|
304
|
+
json = load__json__audios_by_id(subarray)
|
305
|
+
subresult = audios__from_data(json["data"][0].to_a)
|
306
|
+
raise Exceptions::ParseError, "Result size don't match: excepected #{subarray.size}, got #{subresult.size}", caller if subresult.size != subarray.size
|
307
|
+
result.concat(subresult)
|
308
|
+
end
|
309
|
+
result
|
217
310
|
end
|
218
311
|
|
219
|
-
|
312
|
+
##
|
313
|
+
# @!endgroup
|
314
|
+
|
315
|
+
##
|
316
|
+
# @!group Other
|
317
|
+
|
318
|
+
##
|
319
|
+
# Get user or group ID. Sends one request if custom ID provided
|
220
320
|
#
|
221
|
-
#
|
222
|
-
# * [+str+] (+String+) - link, id with prefix or custom id.
|
321
|
+
# @param str [String] link, ID with/without prefix or custom ID.
|
223
322
|
#
|
224
|
-
#
|
225
|
-
|
226
|
-
|
323
|
+
# @return [Integer] page ID.
|
324
|
+
def page_id(str)
|
325
|
+
raise ArgumentError, "Bad arguments", caller unless str.class == String
|
326
|
+
|
227
327
|
case str
|
228
|
-
when Constants::
|
229
|
-
path = str.match(Constants::
|
230
|
-
|
231
|
-
when Constants::
|
232
|
-
str
|
233
|
-
when Constants::
|
234
|
-
str.match(/-?\d+/).to_s # Numbers with
|
235
|
-
when Constants::
|
236
|
-
id = str.match(/\d+/).to_s # Just numbers. Sign needed
|
237
|
-
id
|
238
|
-
|
239
|
-
|
328
|
+
when Constants::Regex::VK_URL
|
329
|
+
path = str.match(Constants::Regex::VK_URL)[1]
|
330
|
+
id = page_id(path) # Recursive call
|
331
|
+
when Constants::Regex::VK_ID_STR
|
332
|
+
id = str.to_i
|
333
|
+
when Constants::Regex::VK_AUDIOS_URL_POSTFIX
|
334
|
+
id = str.match(/-?\d+/).to_s.to_i # Numbers with sign
|
335
|
+
when Constants::Regex::VK_PREFIXED_ID_STR
|
336
|
+
id = str.match(/\d+/).to_s.to_i # Just numbers. Sign needed
|
337
|
+
id *= -1 unless str.start_with?("id")
|
338
|
+
when Constants::Regex::VK_CUSTOM_ID
|
339
|
+
url = "#{Constants::URL::VK[:home]}/#{str}"
|
240
340
|
begin
|
241
|
-
page =
|
242
|
-
rescue
|
243
|
-
raise Exceptions::
|
244
|
-
end
|
341
|
+
page = load__page(url)
|
342
|
+
rescue Exceptions::RequestError => error
|
343
|
+
raise Exceptions::ParseError, "Failed request: #{error.message}", caller
|
344
|
+
end
|
245
345
|
|
246
|
-
unless page.at_css(".PageBlock .owner_panel")
|
247
|
-
# Ensure this isn't some random vk page
|
248
|
-
raise Exceptions::IdParseError, "page #{str} doesn't seem to be a group or user page"
|
249
|
-
end
|
346
|
+
raise Exceptions::ParseError, "Page #{str} doesn't seem to be a group or user page", caller unless page.at_css(".PageBlock .owner_panel")
|
250
347
|
|
251
|
-
|
252
|
-
|
348
|
+
begin
|
349
|
+
id = page.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(Constants::Regex::VK_ID).to_i # Numbers with sign
|
350
|
+
rescue Exception => error
|
351
|
+
raise Exceptions::ParseError, "Unable to get user or group ID. Custom ID: #{str}. Error: #{error.message}", caller
|
352
|
+
end
|
253
353
|
else
|
254
|
-
raise Exceptions::
|
354
|
+
raise Exceptions::ParseError, "Unable to convert \"#{str}\" into ID", caller
|
255
355
|
end
|
356
|
+
id
|
256
357
|
end
|
257
358
|
|
359
|
+
##
|
360
|
+
# Get ID of last post.
|
361
|
+
#
|
362
|
+
# @note requesting for "vk.com/id0" will raise ArgumentError.
|
363
|
+
# Use +client.last_post_id(owner_id: client.id)+ to get last post of client.
|
364
|
+
#
|
365
|
+
# @overload last_post_id(url)
|
366
|
+
# @param url [String] URL to wall owner.
|
367
|
+
#
|
368
|
+
# @overload last_post_id(owner_id)
|
369
|
+
# @param owner_id [Integer] numerical ID of wall owner.
|
370
|
+
#
|
371
|
+
# @return [Integer, nil] ID of last post or +nil+ if there are no posts.
|
372
|
+
def last_post_id(arg)
|
373
|
+
begin
|
374
|
+
case arg
|
375
|
+
when String
|
376
|
+
path = arg.match(Constants::Regex::VK_URL)[1]
|
377
|
+
when Integer
|
378
|
+
owner_id = arg
|
379
|
+
path = "#{owner_id < 0 ? "club" : "id"}#{owner_id.abs}"
|
380
|
+
else
|
381
|
+
raise
|
382
|
+
end
|
383
|
+
rescue
|
384
|
+
raise ArgumentError, "Bad arguments", caller
|
385
|
+
end
|
386
|
+
raise ArgumentError, "Requesting this method for id0 is forbidden", caller if path == "id0"
|
387
|
+
|
388
|
+
url = "#{Constants::URL::VK[:home]}/#{path}"
|
389
|
+
|
390
|
+
begin
|
391
|
+
page = load__page(url)
|
392
|
+
rescue Exceptions::RequestError => error
|
393
|
+
raise Exceptions::ParseError, "Failed request: #{error.message}", caller
|
394
|
+
end
|
395
|
+
|
396
|
+
# Ensure this isn't some random vk page
|
397
|
+
raise Exceptions::ParseError, "Page at #{url} doesn't seem to be a group or user page", caller unless page.at_css(".PageBlock .owner_panel")
|
398
|
+
|
399
|
+
begin
|
400
|
+
posts = page.css(".wall_posts > .wall_item .post__anchor")
|
401
|
+
posts_ids = posts.map do |post|
|
402
|
+
post ? post.attribute("name").to_s.match(Constants::Regex::VK_POST_URL_POSTFIX)[2].to_i : 0
|
403
|
+
end
|
404
|
+
# To avoid checking id of pinned post need to take maximum id.
|
405
|
+
return posts_ids.max
|
406
|
+
rescue Exception => error
|
407
|
+
raise Exceptions::ParseError, "Unable to get last post on #{url}. Error: #{error.message}", caller
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
##
|
258
412
|
# Get amount of audios attached to specified post.
|
259
413
|
#
|
260
|
-
#
|
261
|
-
#
|
262
|
-
# * [+post_id+] (+Integer+)
|
414
|
+
# @overload attached_audios_amount(url)
|
415
|
+
# @param url [String] URL to post.
|
263
416
|
#
|
264
|
-
#
|
265
|
-
#
|
266
|
-
|
417
|
+
# @overload attached_audios_amount(options)
|
418
|
+
# @macro options_hash_param
|
419
|
+
# @option options [Integer] :owner_id numerical ID of wall owner.
|
420
|
+
# @option options [Integer] :post_id numerical ID of post.
|
421
|
+
#
|
422
|
+
# @return [Integer] amount of audios.
|
423
|
+
def attached_audios_amount(arg)
|
424
|
+
begin
|
425
|
+
case arg
|
426
|
+
when String
|
427
|
+
owner_id, post_id = arg.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures
|
428
|
+
when Hash
|
429
|
+
options = arg
|
430
|
+
owner_id = options[:owner_id].to_i
|
431
|
+
post_id = options[:post_id].to_i
|
432
|
+
else
|
433
|
+
raise
|
434
|
+
end
|
435
|
+
rescue
|
436
|
+
raise ArgumentError, "Bad arguments", caller
|
437
|
+
end
|
438
|
+
|
439
|
+
url = "#{Constants::URL::VK[:wall]}#{owner_id}_#{post_id}"
|
440
|
+
begin
|
441
|
+
page = load__page(url)
|
442
|
+
rescue Exceptions::RequestError => error
|
443
|
+
raise Exceptions::ParseError, "Failed request: #{error.message}", caller
|
444
|
+
end
|
445
|
+
|
446
|
+
raise Exceptions::ParseError, "Post not found: #{owner_id}_#{post_id}", caller unless page.css(".service_msg_error").empty?
|
267
447
|
begin
|
268
|
-
page = load_page("#{Constants::VK_URL[:wall]}#{owner_id}_#{post_id}")
|
269
448
|
result = page.css(".wi_body > .pi_medias .medias_audio").size
|
270
449
|
rescue Exception => error
|
271
|
-
raise Exceptions::
|
450
|
+
raise Exceptions::ParseError, "Unable to get amount of audios in post #{owner_id}_#{post_id}. Error: #{error.message}", caller
|
272
451
|
end
|
273
|
-
raise Exceptions::PostParseError, "Post not found: #{owner_id}_#{post_id}", caller if result == 0 && !page.css(".service_msg_error").empty?
|
274
452
|
result
|
275
453
|
end
|
276
454
|
|
455
|
+
##
|
456
|
+
# @!endgroup
|
277
457
|
|
278
458
|
private
|
279
|
-
|
280
|
-
#
|
281
|
-
def
|
282
|
-
uri = URI(url) if url.class != URI
|
283
|
-
|
459
|
+
|
460
|
+
# Load page by URL. And return Mechanize::Page.
|
461
|
+
def load__page(url)
|
462
|
+
uri = URI(url) if url.class != URI
|
463
|
+
Utility.debug("Loading #{uri}")
|
464
|
+
begin
|
465
|
+
@agent.get(uri)
|
466
|
+
rescue Exception => error
|
467
|
+
raise Exceptions::RequestError, error.message, caller
|
468
|
+
end
|
284
469
|
end
|
285
|
-
|
286
|
-
|
287
|
-
|
470
|
+
|
471
|
+
# Load JSON by URL. And return JSON object.
|
472
|
+
def load__json(url)
|
473
|
+
page = load__page(url)
|
474
|
+
begin
|
475
|
+
JSON.parse(page.body.strip)
|
476
|
+
rescue Exception => error
|
477
|
+
raise Exceptions::ParseError, error.message, caller
|
478
|
+
end
|
288
479
|
end
|
289
|
-
|
290
|
-
|
291
|
-
|
480
|
+
|
481
|
+
|
482
|
+
# Load playlist web page.
|
483
|
+
def load__page__playlist(owner_id, playlist_id, access_hash, options)
|
484
|
+
uri = URI(Constants::URL::VK[:audios])
|
292
485
|
uri.query = Utility.hash_to_params({
|
293
|
-
"act" => "audio_playlist#{
|
294
|
-
"access_hash" =>
|
486
|
+
"act" => "audio_playlist#{owner_id.to_i}_#{playlist_id.to_i}",
|
487
|
+
"access_hash" => access_hash.to_s,
|
295
488
|
"offset" => options[:offset].to_i
|
296
489
|
})
|
297
|
-
|
490
|
+
load__page(uri)
|
298
491
|
end
|
299
|
-
|
300
|
-
|
492
|
+
|
493
|
+
# Load JSON playlist part with +load_section+ request.
|
494
|
+
def load__json__playlist_section(owner_id, playlist_id, access_hash, options)
|
495
|
+
uri = URI(Constants::URL::VK[:audios])
|
301
496
|
uri.query = Utility.hash_to_params({
|
302
497
|
"act" => "load_section",
|
303
|
-
"owner_id" => owner_id,
|
304
|
-
"playlist_id" => playlist_id,
|
498
|
+
"owner_id" => owner_id.to_i,
|
499
|
+
"playlist_id" => playlist_id.to_i,
|
500
|
+
"access_hash" => access_hash.to_s,
|
305
501
|
"type" => "playlist",
|
306
|
-
"offset" => offset,
|
502
|
+
"offset" => options[:offset].to_i,
|
307
503
|
"utf8" => true
|
308
504
|
})
|
309
|
-
|
310
|
-
load_json(uri)
|
311
|
-
rescue Exception => error
|
312
|
-
raise Exceptions::AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
313
|
-
end
|
505
|
+
load__json(uri)
|
314
506
|
end
|
315
507
|
|
316
|
-
|
317
|
-
|
508
|
+
|
509
|
+
# Load JSON audios with +reload_audio+ request.
|
510
|
+
def load__json__audios_by_id(ids)
|
511
|
+
uri = URI(Constants::URL::VK[:audios])
|
318
512
|
uri.query = Utility.hash_to_params({
|
319
513
|
"act" => "reload_audio",
|
320
|
-
"ids" => ids,
|
514
|
+
"ids" => ids.to_a,
|
321
515
|
"utf8" => true
|
322
516
|
})
|
323
|
-
|
324
|
-
load_json(uri)
|
325
|
-
rescue Exception => error
|
326
|
-
raise Exceptions::AudiosSectionParseError, "unable to load or parse audios section: #{error.message}", caller
|
327
|
-
end
|
517
|
+
load__json(uri)
|
328
518
|
end
|
329
519
|
|
330
|
-
|
331
|
-
|
520
|
+
# Load JSON audios with +load_section+ from wall.
|
521
|
+
def load__json__audios_wall(owner_id, post_id)
|
522
|
+
uri = URI(Constants::URL::VK[:audios])
|
332
523
|
uri.query = Utility.hash_to_params({
|
333
524
|
"act" => "load_section",
|
334
525
|
"owner_id" => owner_id,
|
@@ -337,43 +528,145 @@ module VkMusic
|
|
337
528
|
"wall_type" => "own",
|
338
529
|
"utf8" => true
|
339
530
|
})
|
531
|
+
load__json(uri)
|
532
|
+
end
|
533
|
+
|
534
|
+
|
535
|
+
# Loading audios from web page.
|
536
|
+
def audios__from_page(obj)
|
537
|
+
page = obj.class == Mechanize::Page ? obj : load__page(obj)
|
340
538
|
begin
|
341
|
-
|
539
|
+
page.css(".audio_item.ai_has_btn").map { |elem| Audio.from_node(elem, @id) }
|
342
540
|
rescue Exception => error
|
343
|
-
raise Exceptions::
|
541
|
+
raise Exceptions::ParseError, error.message, caller
|
344
542
|
end
|
345
|
-
end
|
346
|
-
|
347
|
-
|
348
|
-
# Loading audios
|
349
|
-
def load_audios_from_page(obj)
|
350
|
-
page = obj.class == Mechanize::Page ? obj : load_page(obj)
|
351
|
-
page.css(".audio_item.ai_has_btn").map { |elem| Audio.from_node(elem, @id) }
|
352
543
|
end
|
353
|
-
|
354
|
-
|
544
|
+
|
545
|
+
# Load audios from JSON data.
|
546
|
+
def audios__from_data(data)
|
547
|
+
begin
|
548
|
+
data.map { |audio_data| Audio.from_data(audio_data, @id) }
|
549
|
+
rescue Exception => error
|
550
|
+
raise Exceptions::ParseError, error.message, caller
|
551
|
+
end
|
355
552
|
end
|
356
|
-
|
357
|
-
|
553
|
+
|
554
|
+
|
555
|
+
# Load playlist through web page requests.
|
556
|
+
def playlist__web(owner_id, playlist_id, access_hash, options)
|
557
|
+
begin
|
558
|
+
# Load first page and get info
|
559
|
+
first_page = load__page__playlist(owner_id, playlist_id, access_hash, offset: 0)
|
560
|
+
|
561
|
+
# Parse out essential data
|
562
|
+
title = first_page.at_css(".audioPlaylist__title").text.strip
|
563
|
+
subtitle = first_page.at_css(".audioPlaylist__subtitle").text.strip
|
564
|
+
|
565
|
+
footer_node = first_page.at_css(".audioPlaylist__footer")
|
566
|
+
if footer_node
|
567
|
+
footer_match = footer_node.text.strip.match(/^\d+/)
|
568
|
+
playlist_size = footer_match ? footer_match[0].to_i : 0
|
569
|
+
else
|
570
|
+
playlist_size = 0
|
571
|
+
end
|
572
|
+
rescue Exception => error
|
573
|
+
raise Exceptions::ParseError, error.message, caller
|
574
|
+
end
|
575
|
+
# Now we can be sure we are on correct page
|
576
|
+
|
577
|
+
first_page_audios = audios__from_page(first_page)
|
578
|
+
|
579
|
+
# Check whether need to make additional requests
|
580
|
+
options[:up_to] = playlist_size if (options[:up_to] < 0 || options[:up_to] > playlist_size)
|
581
|
+
list = first_page_audios[0, options[:up_to]]
|
582
|
+
while list.length < options[:up_to] do
|
583
|
+
playlist_page = load__page__playlist(owner_id, playlist_id, access_hash, offset: list.length)
|
584
|
+
list.concat(audios__from_page(playlist_page)[0, options[:up_to] - list.length])
|
585
|
+
end
|
586
|
+
|
587
|
+
Playlist.new(list, {
|
588
|
+
:id => id,
|
589
|
+
:owner_id => owner_id,
|
590
|
+
:access_hash => access_hash,
|
591
|
+
:title => title,
|
592
|
+
:subtitle => subtitle,
|
593
|
+
})
|
594
|
+
end
|
595
|
+
|
596
|
+
# Load playlist through JSON requests.
|
597
|
+
def playlist__json(owner_id, playlist_id, access_hash, options)
|
598
|
+
if options[:up_to] < 0 || options[:up_to] > 100
|
599
|
+
Utility.warn("Current implementation of this method is not able to return more than 100 first audios with URL.")
|
600
|
+
end
|
601
|
+
|
602
|
+
# Trying to parse out audios
|
603
|
+
begin
|
604
|
+
first_json = load__json__playlist_section(owner_id, playlist_id, access_hash, offset: 0)
|
605
|
+
first_data = first_json["data"][0]
|
606
|
+
first_data_audios = audios__from_data(first_data["list"])
|
607
|
+
rescue Exception => error
|
608
|
+
raise Exceptions::ParseError, error.message, caller
|
609
|
+
end
|
610
|
+
|
611
|
+
total_count = first_data["totalCount"]
|
612
|
+
options[:up_to] = total_count if (options[:up_to] < 0 || options[:up_to] > total_count)
|
613
|
+
list = first_data_audios[0, options[:up_to]]
|
614
|
+
while list.length < options[:up_to] do
|
615
|
+
json = load__json__playlist_section(owner_id, playlist_id, access_hash,
|
616
|
+
offset: list.length
|
617
|
+
)
|
618
|
+
audios = audios__from_data(json["data"][0]["list"])
|
619
|
+
list.concat(audios[0, options[:up_to] - list.length])
|
620
|
+
end
|
621
|
+
|
622
|
+
Playlist.new(list, {
|
623
|
+
:id => first_data["id"],
|
624
|
+
:owner_id => first_data["owner_id"],
|
625
|
+
:access_hash => first_data["access_hash"],
|
626
|
+
:title => CGI.unescapeHTML(first_data["title"].to_s),
|
627
|
+
:subtitle => CGI.unescapeHTML(first_data["subtitle"].to_s),
|
628
|
+
})
|
629
|
+
end
|
630
|
+
|
631
|
+
# Load audios from wall using JSON request.
|
632
|
+
def wall__json(owner_id, post_id, options)
|
633
|
+
if options[:up_to] < 0 || options[:up_to] > 91
|
634
|
+
options[:up_to] = 91
|
635
|
+
Utility.warn("Current implementation of this method is not able to return more than 91 audios from wall.")
|
636
|
+
end
|
637
|
+
|
638
|
+
begin
|
639
|
+
json = load__json__audios_wall(owner_id, post_id)
|
640
|
+
data = json["data"][0]
|
641
|
+
audios = audios__from_data(data["list"])[0, options[:up_to]]
|
642
|
+
rescue Exception => error
|
643
|
+
raise Exceptions::ParseError, error.message, caller
|
644
|
+
end
|
645
|
+
options[:with_url] ? from_id(audios) : audios
|
646
|
+
end
|
647
|
+
|
648
|
+
|
358
649
|
# Login
|
359
650
|
def login(username, password)
|
651
|
+
Utility.debug("Logging in.")
|
360
652
|
# Loading login page
|
361
|
-
homepage =
|
653
|
+
homepage = load__page(Constants::URL::VK[:home])
|
362
654
|
# Submitting login form
|
363
|
-
login_form = homepage.forms.find { |form| form.action.start_with?(Constants::
|
655
|
+
login_form = homepage.forms.find { |form| form.action.start_with?(Constants::URL::VK[:login_action]) }
|
364
656
|
login_form[Constants::VK_LOGIN_FORM_NAMES[:username]] = username.to_s
|
365
657
|
login_form[Constants::VK_LOGIN_FORM_NAMES[:password]] = password.to_s
|
366
658
|
after_login = @agent.submit(login_form)
|
367
659
|
|
368
660
|
# Checking whether logged in
|
369
|
-
raise Exceptions::LoginError, "
|
661
|
+
raise Exceptions::LoginError, "Unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == Constants::URL::VK[:feed]
|
370
662
|
|
371
663
|
# Parsing information about this profile
|
372
|
-
profile =
|
373
|
-
@name = profile.title
|
374
|
-
@id = profile.link_with(href: Constants::
|
664
|
+
profile = load__page(Constants::URL::VK[:profile])
|
665
|
+
@name = profile.title.to_s
|
666
|
+
@id = profile.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(/\d+/).to_i
|
375
667
|
end
|
376
668
|
|
669
|
+
# Shortcut
|
377
670
|
def unmask_link(link)
|
378
671
|
VkMusic::LinkDecoder.unmask_link(link, @id)
|
379
672
|
end
|