vk_music 1.1.1 → 2.0.0
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 +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
|