vk_music 3.1.7 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.env.example +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +56 -0
- data/Gemfile +38 -10
- data/Gemfile.lock +123 -70
- data/LICENSE.txt +0 -0
- data/README.md +111 -94
- data/Rakefile +12 -22
- data/bin/console +18 -24
- data/lib/vk_music.rb +32 -18
- data/lib/vk_music/audio.rb +111 -187
- data/lib/vk_music/client.rb +187 -677
- data/lib/vk_music/playlist.rb +44 -97
- data/lib/vk_music/request.rb +13 -0
- data/lib/vk_music/request/audios.rb +29 -0
- data/lib/vk_music/request/base.rb +75 -0
- data/lib/vk_music/request/login.rb +35 -0
- data/lib/vk_music/request/my_page.rb +21 -0
- data/lib/vk_music/request/playlist.rb +31 -0
- data/lib/vk_music/request/playlist_section.rb +35 -0
- data/lib/vk_music/request/post.rb +22 -0
- data/lib/vk_music/request/profile.rb +24 -0
- data/lib/vk_music/request/search.rb +34 -0
- data/lib/vk_music/request/wall_section.rb +34 -0
- data/lib/vk_music/utility.rb +8 -78
- data/lib/vk_music/utility/audio_data_parser.rb +37 -0
- data/lib/vk_music/utility/audio_items_parser.rb +18 -0
- data/lib/vk_music/utility/audio_node_parser.rb +59 -0
- data/lib/vk_music/utility/audios_from_ids_loader.rb +21 -0
- data/lib/vk_music/utility/audios_ids_getter.rb +25 -0
- data/lib/vk_music/utility/audios_loader.rb +37 -0
- data/lib/vk_music/utility/data_type_guesser.rb +43 -0
- data/lib/vk_music/utility/duration_parser.rb +17 -0
- data/lib/vk_music/utility/last_profile_post_loader.rb +26 -0
- data/lib/vk_music/utility/link_decoder.rb +106 -0
- data/lib/vk_music/utility/node_text_children_reader.rb +14 -0
- data/lib/vk_music/utility/playlist_loader.rb +30 -0
- data/lib/vk_music/utility/playlist_node_parser.rb +21 -0
- data/lib/vk_music/utility/playlist_section_loader.rb +29 -0
- data/lib/vk_music/utility/playlist_url_parser.rb +32 -0
- data/lib/vk_music/utility/post_loader.rb +23 -0
- data/lib/vk_music/utility/post_url_parser.rb +24 -0
- data/lib/vk_music/utility/profile_id_resolver.rb +51 -0
- data/lib/vk_music/utility/wall_loader.rb +25 -0
- data/lib/vk_music/version.rb +8 -5
- data/lib/vk_music/web_parser.rb +9 -0
- data/lib/vk_music/web_parser/audios.rb +20 -0
- data/lib/vk_music/web_parser/base.rb +27 -0
- data/lib/vk_music/web_parser/login.rb +13 -0
- data/lib/vk_music/web_parser/my_page.rb +19 -0
- data/lib/vk_music/web_parser/playlist.rb +33 -0
- data/lib/vk_music/web_parser/playlist_section.rb +53 -0
- data/lib/vk_music/web_parser/post.rb +15 -0
- data/lib/vk_music/web_parser/profile.rb +33 -0
- data/lib/vk_music/web_parser/search.rb +56 -0
- data/lib/vk_music/web_parser/wall_section.rb +53 -0
- data/vk_music.gemspec +36 -40
- metadata +58 -77
- data/.travis.yml +0 -7
- data/bin/setup +0 -8
- data/lib/vk_music/constants.rb +0 -78
- data/lib/vk_music/exceptions.rb +0 -21
- data/lib/vk_music/link_decoder.rb +0 -102
- data/lib/vk_music/utility/log.rb +0 -51
data/Rakefile
CHANGED
@@ -1,25 +1,15 @@
|
|
1
|
-
|
2
|
-
require "rake/testtask"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
print "Path to SSL certificate (leave empty if there is no troubles with SSL): "
|
15
|
-
ssl_cert_path = STDIN.gets.chomp
|
16
|
-
puts
|
17
|
-
ENV["SSL_CERT_FILE"] = ssl_cert_path unless ssl_cert_path.empty?
|
18
|
-
|
19
|
-
Dir[ "test/test_*.rb" ].each do |file|
|
20
|
-
puts "\n\nRunning #{file}:"
|
21
|
-
ruby "-w #{file} '#{username}' '#{password}'"
|
22
|
-
end
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'yard'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
YARD::Rake::YardocTask.new do |t|
|
10
|
+
t.files = ['lib/**/*.rb']
|
11
|
+
t.options = ['--any', '--extra', '--opts']
|
12
|
+
t.stats_options = ['--list-undoc']
|
23
13
|
end
|
24
14
|
|
25
|
-
task :
|
15
|
+
task default: %i[spec yard]
|
data/bin/console
CHANGED
@@ -1,24 +1,18 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
|
4
|
-
require
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
$client = VkMusic::Client.new(username: username, password: password)
|
20
|
-
puts "You can now access client at $client"
|
21
|
-
end
|
22
|
-
puts
|
23
|
-
|
24
|
-
Pry.start
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'vk_music'
|
6
|
+
require 'pry'
|
7
|
+
require 'dotenv'
|
8
|
+
Dotenv.load
|
9
|
+
|
10
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
11
|
+
# with your gem easier. You can also use a different console, if you like.
|
12
|
+
|
13
|
+
if ENV['VK_LOGIN'] && ENV['VK_PASSWORD']
|
14
|
+
client = VkMusic::Client.new(login: ENV['VK_LOGIN'], password: ENV['VK_PASSWORD'])
|
15
|
+
puts "You now can access client##{client.id}"
|
16
|
+
end
|
17
|
+
|
18
|
+
Pry.start(client || Object.new)
|
data/lib/vk_music.rb
CHANGED
@@ -1,18 +1,32 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'execjs'
|
4
|
+
require 'mechanize'
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
7
|
+
require 'forwardable'
|
8
|
+
|
9
|
+
# Main module
|
10
|
+
module VkMusic
|
11
|
+
@@log = Logger.new($stdout)
|
12
|
+
|
13
|
+
# Logger of library classes
|
14
|
+
# @return [Logger]
|
15
|
+
def self.log
|
16
|
+
@@log
|
17
|
+
end
|
18
|
+
|
19
|
+
# Replace logger
|
20
|
+
# @param logger [Logger]
|
21
|
+
def self.log=(logger)
|
22
|
+
@@log = logger
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require_relative 'vk_music/version'
|
27
|
+
require_relative 'vk_music/utility'
|
28
|
+
require_relative 'vk_music/request'
|
29
|
+
require_relative 'vk_music/web_parser'
|
30
|
+
require_relative 'vk_music/client'
|
31
|
+
require_relative 'vk_music/audio'
|
32
|
+
require_relative 'vk_music/playlist'
|
data/lib/vk_music/audio.rb
CHANGED
@@ -1,187 +1,111 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# @return [
|
7
|
-
attr_reader :
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# @return [String
|
13
|
-
attr_reader :
|
14
|
-
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
# @
|
19
|
-
|
20
|
-
|
21
|
-
# @
|
22
|
-
|
23
|
-
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
# @return [
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# @param
|
91
|
-
# @
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
# @param artist [String]
|
113
|
-
# @param title [String]
|
114
|
-
# @param duration [Integer]
|
115
|
-
# @param url_encoded [String, nil]
|
116
|
-
# @param url [String, nil]
|
117
|
-
# @param client_id [Integer, nil]
|
118
|
-
def initialize(id: nil, owner_id: nil, secret_1: nil, secret_2: nil, artist: "", title: "", duration: 0, url_encoded: nil, url: nil, client_id: nil)
|
119
|
-
@id = id
|
120
|
-
@owner_id = owner_id
|
121
|
-
@secret_1 = secret_1
|
122
|
-
@secret_2 = secret_2
|
123
|
-
@secret_1 = @secret_2 if @secret_1.nil? || @secret_1.empty?
|
124
|
-
@artist = artist.strip
|
125
|
-
@title = title.strip
|
126
|
-
@duration = duration
|
127
|
-
@url_encoded = url_encoded
|
128
|
-
@url = url
|
129
|
-
@client_id = client_id
|
130
|
-
end
|
131
|
-
##
|
132
|
-
# Initialize new audio from Nokogiri HTML node.
|
133
|
-
# @param node [Nokogiri::XML::Node] node, which match following CSS selector: +.audio_item.ai_has_btn+
|
134
|
-
# @param client_id [Integer]
|
135
|
-
# @return [Audio]
|
136
|
-
def self.from_node(node, client_id)
|
137
|
-
input = node.at_css("input")
|
138
|
-
if input
|
139
|
-
url_encoded = input.attribute("value").to_s
|
140
|
-
url_encoded = nil if url_encoded == Constants::URL::VK[:audio_unavailable] || url_encoded.empty?
|
141
|
-
id_array = node.attribute("data-id").to_s.split("_")
|
142
|
-
|
143
|
-
new(
|
144
|
-
id: id_array[1].to_i,
|
145
|
-
owner_id: id_array[0].to_i,
|
146
|
-
artist: node.at_css(".ai_artist").text.strip,
|
147
|
-
title: node.at_css(".ai_title").text.strip,
|
148
|
-
duration: node.at_css(".ai_dur").attribute("data-dur").to_s.to_i,
|
149
|
-
url_encoded: url_encoded,
|
150
|
-
url: nil,
|
151
|
-
client_id: client_id
|
152
|
-
)
|
153
|
-
else
|
154
|
-
# Probably audios from some post
|
155
|
-
new(
|
156
|
-
artist: node.at_css(".medias_music_author").text.strip,
|
157
|
-
title: Utility.plain_text(node.at_css(".medias_audio_title")).strip,
|
158
|
-
duration: Utility.parse_duration(node.at_css(".medias_audio_dur").text)
|
159
|
-
)
|
160
|
-
end
|
161
|
-
end
|
162
|
-
##
|
163
|
-
# Initialize new audio from VK data array.
|
164
|
-
# @param data [Array]
|
165
|
-
# @param client_id [Integer]
|
166
|
-
# @return [Audio]
|
167
|
-
def self.from_data(data, client_id)
|
168
|
-
url_encoded = data[2].to_s
|
169
|
-
url_encoded = nil if url_encoded.empty?
|
170
|
-
|
171
|
-
secrets = data[13].to_s.split("/")
|
172
|
-
|
173
|
-
new(
|
174
|
-
id: data[0].to_i,
|
175
|
-
owner_id: data[1].to_i,
|
176
|
-
secret_1: secrets[3],
|
177
|
-
secret_2: secrets[5],
|
178
|
-
artist: CGI.unescapeHTML(data[4]),
|
179
|
-
title: CGI.unescapeHTML(data[3]),
|
180
|
-
duration: data[5].to_i,
|
181
|
-
url_encoded: url_encoded,
|
182
|
-
url: nil,
|
183
|
-
client_id: client_id
|
184
|
-
)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
# Class representing VK audio
|
5
|
+
class Audio
|
6
|
+
# @return [String] name of artist
|
7
|
+
attr_reader :artist
|
8
|
+
# @return [String] title of song
|
9
|
+
attr_reader :title
|
10
|
+
# @return [Integer] duration of track in seconds
|
11
|
+
attr_reader :duration
|
12
|
+
# @return [String?] encoded URL which can be manually decoded if client ID is known
|
13
|
+
attr_reader :url_encoded
|
14
|
+
|
15
|
+
# Initialize new audio
|
16
|
+
# @param id [Integer, nil]
|
17
|
+
# @param owner_id [Integer, nil]
|
18
|
+
# @param secret1 [String, nil]
|
19
|
+
# @param secret2 [String, nil]
|
20
|
+
# @param artist [String]
|
21
|
+
# @param title [String]
|
22
|
+
# @param duration [Integer]
|
23
|
+
# @param url_encoded [String, nil]
|
24
|
+
# @param url [String, nil] decoded URL
|
25
|
+
# @param client_id [Integer, nil]
|
26
|
+
def initialize(id: nil, owner_id: nil, secret1: nil, secret2: nil,
|
27
|
+
artist: '', title: '', duration: 0,
|
28
|
+
url_encoded: nil, url: nil, client_id: nil)
|
29
|
+
@id = id
|
30
|
+
@owner_id = owner_id
|
31
|
+
@secret1 = secret1
|
32
|
+
@secret2 = secret2
|
33
|
+
@artist = artist.to_s.strip
|
34
|
+
@title = title.to_s.strip
|
35
|
+
@duration = duration
|
36
|
+
@url_encoded = url_encoded
|
37
|
+
@url_decoded = url
|
38
|
+
@client_id = client_id
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String?]
|
42
|
+
def url
|
43
|
+
return @url_decoded if @url_decoded
|
44
|
+
|
45
|
+
return unless @url_encoded && @client_id
|
46
|
+
|
47
|
+
Utility::LinkDecoder.call(@url_encoded, @client_id)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Update audio data from another one
|
51
|
+
def update(audio)
|
52
|
+
VkMusic.log.warn('Audio') { "Performing update of #{self} from #{audio}" } unless like?(audio)
|
53
|
+
@id = audio.id
|
54
|
+
@owner_id = audio.owner_id
|
55
|
+
@secret1 = audio.secret1
|
56
|
+
@secret2 = audio.secret2
|
57
|
+
@url_encoded = audio.url_encoded
|
58
|
+
@url_decoded = audio.url_decoded
|
59
|
+
@client_id = audio.client_id
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String?]
|
63
|
+
def full_id
|
64
|
+
return unless @id && @owner_id && @secret1 && @secret2
|
65
|
+
|
66
|
+
"#{@owner_id}_#{@id}_#{@secret1}_#{@secret2}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [Boolean] whether URL saved into url attribute
|
70
|
+
def url_cached?
|
71
|
+
!!@url_decoded
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Boolean] whether able to get download URL without web requests
|
75
|
+
def url_available?
|
76
|
+
url_cached? || !!(@url_encoded && @client_id)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Boolean] whether it's possible to get download URL with {Client#from_id}
|
80
|
+
def url_accessable?
|
81
|
+
!!full_id
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param audio [Audio]
|
85
|
+
# @return [Boolean] whether artist, title and duration are same
|
86
|
+
def like?(audio)
|
87
|
+
artist == audio.artist && title == audio.title && duration == audio.duration
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param [Audio, Array(owner_id, audio_id, secret1, secret2), String]
|
91
|
+
# @return [Boolean] id-based comparison
|
92
|
+
def id_matches?(data)
|
93
|
+
data_id = case data
|
94
|
+
when Array then data.join('_')
|
95
|
+
when Audio then data.full_id
|
96
|
+
when String then data.strip
|
97
|
+
end
|
98
|
+
|
99
|
+
full_id == data_id
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [String] pretty-printed audio name
|
103
|
+
def to_s
|
104
|
+
"#{@artist} - #{@title} [#{@duration}s]"
|
105
|
+
end
|
106
|
+
|
107
|
+
protected
|
108
|
+
|
109
|
+
attr_reader :id, :owner_id, :secret1, :secret2, :url_decoded, :client_id
|
110
|
+
end
|
111
|
+
end
|
data/lib/vk_music/client.rb
CHANGED
@@ -1,677 +1,187 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# @
|
18
|
-
|
19
|
-
# @
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
#
|
42
|
-
#
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# @
|
66
|
-
# @
|
67
|
-
# @param
|
68
|
-
# @param
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
#
|
101
|
-
# @
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
#
|
114
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
#
|
152
|
-
#
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
next el if el.full_id.nil? # Audio was skipped
|
189
|
-
audios.find { |audio| audio.owner_id == el.owner_id && audio.id == el.id }
|
190
|
-
when String
|
191
|
-
audios.find { |audio| [audio.owner_id, audio.id] == el.split("_").first(2).map(&:to_i) }
|
192
|
-
else
|
193
|
-
nil # This shouldn't happen actually
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
alias_method :from_id, :get_urls
|
198
|
-
|
199
|
-
##
|
200
|
-
# Update download URLs of audios.
|
201
|
-
# @param audios [Array<Audio>]
|
202
|
-
def update_urls(audios)
|
203
|
-
audios_with_urls = get_urls(audios)
|
204
|
-
audios.each.with_index do |a, i|
|
205
|
-
a_u = audios_with_urls[i]
|
206
|
-
a.update(from: a_u) unless a_u.nil?
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
##
|
211
|
-
# Retrieve audios from recommendations or alike pages.
|
212
|
-
# Specify either +url+ or +block_id+.
|
213
|
-
# @param url [String] URL.
|
214
|
-
# @param block_id [String] ID of block.
|
215
|
-
# @return [Array<Audio>] array of audios attached to post. Most of audios will
|
216
|
-
# already have download URLs, but there might be audios which can't be resolved.
|
217
|
-
def block(url: nil, block_id: nil)
|
218
|
-
begin
|
219
|
-
block_id = url.match(Constants::Regex::VK_BLOCK_URL).captures.first if url
|
220
|
-
rescue
|
221
|
-
raise Exceptions::ParseError
|
222
|
-
end
|
223
|
-
|
224
|
-
uri = URI(Constants::URL::VK[:audios])
|
225
|
-
uri.query = Utility.hash_to_params({ "act" => "block", "block" => block_id })
|
226
|
-
audios_from_page(uri)
|
227
|
-
end
|
228
|
-
|
229
|
-
##
|
230
|
-
# @!endgroup
|
231
|
-
|
232
|
-
##
|
233
|
-
# @!group Other
|
234
|
-
|
235
|
-
##
|
236
|
-
# Get user or group ID. Sends one request if custom ID provided.
|
237
|
-
# @param str [String] link, ID with/without prefix or custom ID.
|
238
|
-
# @return [Integer] page ID.
|
239
|
-
def page_id(str)
|
240
|
-
case str
|
241
|
-
when Constants::Regex::VK_URL
|
242
|
-
path = str.match(Constants::Regex::VK_URL)[1]
|
243
|
-
page_id(path) # Recursive call
|
244
|
-
when Constants::Regex::VK_ID_STR
|
245
|
-
str.to_i
|
246
|
-
when Constants::Regex::VK_AUDIOS_URL_POSTFIX
|
247
|
-
str.match(/-?\d+/).to_s.to_i # Numbers with sign
|
248
|
-
when Constants::Regex::VK_PREFIXED_ID_STR
|
249
|
-
id = str.match(/\d+/).to_s.to_i # Just numbers. Sign needed
|
250
|
-
id *= -1 unless str.start_with?("id")
|
251
|
-
id
|
252
|
-
when Constants::Regex::VK_CUSTOM_ID
|
253
|
-
url = "#{Constants::URL::VK[:home]}/#{str}"
|
254
|
-
begin
|
255
|
-
page = load_page(url)
|
256
|
-
rescue Exceptions::RequestError
|
257
|
-
raise Exceptions::ParseError
|
258
|
-
end
|
259
|
-
|
260
|
-
raise Exceptions::ParseError unless page.at_css(".PageBlock .owner_panel")
|
261
|
-
|
262
|
-
begin
|
263
|
-
page.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(Constants::Regex::VK_ID).to_i # Numbers with sign
|
264
|
-
rescue
|
265
|
-
raise Exceptions::ParseError
|
266
|
-
end
|
267
|
-
else
|
268
|
-
raise Exceptions::ParseError
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
##
|
273
|
-
# Get ID of last post.
|
274
|
-
# Specify either +url+ or +owner_id+.
|
275
|
-
# @note requesting for "vk.com/id0" will raise ArgumentError.
|
276
|
-
# Use +client.last_post_id(owner_id: client.id)+ to get last post of client.
|
277
|
-
# @param url [String] URL to wall owner.
|
278
|
-
# @param owner_id [Integer] numerical ID of wall owner.
|
279
|
-
# @return [Integer, nil] ID of last post or +nil+ if there are no posts.
|
280
|
-
def last_post_id(url: nil, owner_id: nil)
|
281
|
-
path = if url
|
282
|
-
url.match(Constants::Regex::VK_URL)[1]
|
283
|
-
else
|
284
|
-
path = "#{owner_id < 0 ? "club" : "id"}#{owner_id.abs}"
|
285
|
-
end
|
286
|
-
raise ArgumentError, "Requesting this method for id0 is forbidden", caller if path == "id0"
|
287
|
-
|
288
|
-
url = "#{Constants::URL::VK[:home]}/#{path}"
|
289
|
-
page = load_page(url)
|
290
|
-
|
291
|
-
# Ensure this isn't some random vk page
|
292
|
-
raise Exceptions::ParseError unless page.at_css(".PageBlock .owner_panel")
|
293
|
-
|
294
|
-
begin
|
295
|
-
posts = page.css(".wall_posts > .wall_item .anchor")
|
296
|
-
posts_ids = posts.map do |post|
|
297
|
-
post ? post.attribute("name").to_s.match(Constants::Regex::VK_POST_URL_POSTFIX)[2].to_i : 0
|
298
|
-
end
|
299
|
-
# To avoid checking id of pinned post need to take maximum id.
|
300
|
-
return posts_ids.max
|
301
|
-
rescue
|
302
|
-
raise Exceptions::ParseError
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
##
|
307
|
-
# Get audios attached to specified post.
|
308
|
-
# Specify either +url+ or +(owner_id,post_id)+.
|
309
|
-
# @param url [String] URL to post.
|
310
|
-
# @param owner_id [Integer] numerical ID of wall owner.
|
311
|
-
# @param post_id [Integer] numerical ID of post.
|
312
|
-
# @return [Array<Audio>] audios with only artist, title and duration.
|
313
|
-
def attached_audios(url: nil, owner_id: nil, post_id: nil)
|
314
|
-
begin
|
315
|
-
owner_id, post_id = url.match(Constants::Regex::VK_WALL_URL_POSTFIX).captures if url
|
316
|
-
rescue
|
317
|
-
raise Exceptions::ParseError
|
318
|
-
end
|
319
|
-
|
320
|
-
url = "#{Constants::URL::VK[:wall]}#{owner_id}_#{post_id}"
|
321
|
-
begin
|
322
|
-
page = load_page(url)
|
323
|
-
rescue Exceptions::RequestError
|
324
|
-
raise Exceptions::ParseError
|
325
|
-
end
|
326
|
-
|
327
|
-
raise Exceptions::ParseError unless page.css(".service_msg_error").empty?
|
328
|
-
begin
|
329
|
-
page.css(".wi_body > .pi_medias .medias_audio").map { |e| Audio.from_node(e, @id) }
|
330
|
-
rescue
|
331
|
-
raise Exceptions::ParseError
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
##
|
336
|
-
# @!endgroup
|
337
|
-
|
338
|
-
private
|
339
|
-
|
340
|
-
##
|
341
|
-
# Load page web page.
|
342
|
-
# @param url [String, URI]
|
343
|
-
# @return [Mechanize::Page]
|
344
|
-
def load_page(url)
|
345
|
-
uri = URI(url) if url.class != URI
|
346
|
-
VkMusic.debug("Loading #{uri}")
|
347
|
-
begin
|
348
|
-
@agent.get(uri)
|
349
|
-
rescue
|
350
|
-
raise Exceptions::RequestError
|
351
|
-
end
|
352
|
-
end
|
353
|
-
##
|
354
|
-
# Load JSON from web page.
|
355
|
-
# @param url [String, URI]
|
356
|
-
# @return [Hash]
|
357
|
-
def load_json(url)
|
358
|
-
page = load_page(url)
|
359
|
-
begin
|
360
|
-
JSON.parse(page.body.strip)
|
361
|
-
rescue Exception => error
|
362
|
-
raise Exceptions::ParseError, error.message, caller
|
363
|
-
end
|
364
|
-
end
|
365
|
-
##
|
366
|
-
# Load response to AJAX post request.
|
367
|
-
# @param url [String, URI]
|
368
|
-
# @return [Nokogiri::XML::Document]
|
369
|
-
def load_ajax(url, query = {})
|
370
|
-
uri = URI(url) if url.class != URI
|
371
|
-
query["_ajax"] = 1
|
372
|
-
headers = { "Content-Type" => "application/x-www-form-urlencoded", "x-requested-with" => "XMLHttpRequest" }
|
373
|
-
VkMusic.debug("Loading #{uri} with query #{query}")
|
374
|
-
begin
|
375
|
-
page = @agent.post(uri, query, headers)
|
376
|
-
str = JSON.parse(page.body.strip)["data"][2]
|
377
|
-
Nokogiri::XML("<body>#{CGI.unescapeElement(str)}</body>")
|
378
|
-
rescue
|
379
|
-
raise Exceptions::RequestError
|
380
|
-
end
|
381
|
-
end
|
382
|
-
|
383
|
-
##
|
384
|
-
# Load playlist web page.
|
385
|
-
# @param owner_id [Integer]
|
386
|
-
# @param playlist_id [Integer]
|
387
|
-
# @param access_hash [String, nil]
|
388
|
-
# @param offset [Integer]
|
389
|
-
# @return [Mechanize::Page]
|
390
|
-
def load_page_playlist(owner_id, playlist_id, access_hash = nil, offset: 0)
|
391
|
-
uri = URI(Constants::URL::VK[:audios])
|
392
|
-
uri.query = Utility.hash_to_params({
|
393
|
-
act: "audio_playlist#{owner_id}_#{playlist_id}",
|
394
|
-
access_hash: access_hash.to_s,
|
395
|
-
offset: offset
|
396
|
-
})
|
397
|
-
load_page(uri)
|
398
|
-
end
|
399
|
-
##
|
400
|
-
# Load JSON playlist section with +load_section+ request.
|
401
|
-
# @param owner_id [Integer]
|
402
|
-
# @param playlist_id [Integer]
|
403
|
-
# @param access_hash [String, nil]
|
404
|
-
# @param offset [Integer]
|
405
|
-
# @return [Hash]
|
406
|
-
def load_json_playlist_section(owner_id, playlist_id, access_hash = nil, offset: 0)
|
407
|
-
uri = URI(Constants::URL::VK[:audios])
|
408
|
-
uri.query = Utility.hash_to_params({
|
409
|
-
act: "load_section",
|
410
|
-
owner_id: owner_id,
|
411
|
-
playlist_id: playlist_id,
|
412
|
-
access_hash: access_hash.to_s,
|
413
|
-
type: "playlist",
|
414
|
-
offset: offset,
|
415
|
-
utf8: true
|
416
|
-
})
|
417
|
-
load_json(uri)
|
418
|
-
end
|
419
|
-
|
420
|
-
##
|
421
|
-
# Load JSON audios with +reload_audio+ request.
|
422
|
-
# @param ids [Array<String>]
|
423
|
-
# @return [Hash]
|
424
|
-
def load_json_audios_by_id(ids)
|
425
|
-
uri = URI(Constants::URL::VK[:audios])
|
426
|
-
uri.query = Utility.hash_to_params({
|
427
|
-
act: "reload_audio",
|
428
|
-
ids: ids,
|
429
|
-
utf8: true
|
430
|
-
})
|
431
|
-
load_json(uri)
|
432
|
-
end
|
433
|
-
##
|
434
|
-
# Load JSON audios with +load_section+ from wall.
|
435
|
-
# @param owner_id [Integer]
|
436
|
-
# @param post_id [Integer]
|
437
|
-
# @return [Hash]
|
438
|
-
def load_json_audios_wall(owner_id, post_id)
|
439
|
-
uri = URI(Constants::URL::VK[:audios])
|
440
|
-
uri.query = Utility.hash_to_params({
|
441
|
-
act: "load_section",
|
442
|
-
owner_id: owner_id,
|
443
|
-
post_id: post_id,
|
444
|
-
type: "wall",
|
445
|
-
wall_type: "own",
|
446
|
-
utf8: true
|
447
|
-
})
|
448
|
-
load_json(uri)
|
449
|
-
end
|
450
|
-
|
451
|
-
##
|
452
|
-
# Load audios from web page.
|
453
|
-
# @param obj [Mechanize::Page, String, URI]
|
454
|
-
# @return [Array<Audio>]
|
455
|
-
def audios_from_page(obj)
|
456
|
-
page = obj.is_a?(Mechanize::Page) ? obj : load_page(obj)
|
457
|
-
begin
|
458
|
-
page.css(".audio_item.ai_has_btn").map do |elem|
|
459
|
-
data = JSON.parse(elem.attribute("data-audio"))
|
460
|
-
Audio.from_data(data, @id)
|
461
|
-
end
|
462
|
-
rescue
|
463
|
-
raise Exceptions::ParseError
|
464
|
-
end
|
465
|
-
end
|
466
|
-
##
|
467
|
-
# Load audios from JSON data.
|
468
|
-
# @param data [Hash]
|
469
|
-
# @return [Array<Audio>]
|
470
|
-
def audios_from_data(data)
|
471
|
-
begin
|
472
|
-
data.map { |audio_data| Audio.from_data(audio_data, @id) }
|
473
|
-
rescue
|
474
|
-
raise Exceptions::ParseError
|
475
|
-
end
|
476
|
-
end
|
477
|
-
##
|
478
|
-
# Load audios from AJAX data.
|
479
|
-
# @param page [Nokogiri::XML::Document]
|
480
|
-
# @return [Array<Audio>]
|
481
|
-
def audios_from_ajax(page)
|
482
|
-
begin
|
483
|
-
page.css(".audio_item.ai_has_btn").map do |elem|
|
484
|
-
data = JSON.parse(elem.attribute("data-audio"))
|
485
|
-
Audio.from_data(data, @id)
|
486
|
-
end
|
487
|
-
rescue
|
488
|
-
raise Exceptions::ParseError
|
489
|
-
end
|
490
|
-
end
|
491
|
-
|
492
|
-
##
|
493
|
-
# Load playlist through web page requests.
|
494
|
-
# @param owner_id [Integer]
|
495
|
-
# @param playlist_id [Integer]
|
496
|
-
# @param access_hash [String, nil]
|
497
|
-
# @param up_to [Integer] if less than 0, all audios will be loaded.
|
498
|
-
# @return [Playlist]
|
499
|
-
def playlist_web(owner_id, playlist_id, access_hash = nil, up_to: -1)
|
500
|
-
first_page_audios, title, subtitle, real_size = playlist_first_page_web(owner_id, playlist_id, access_hash || "")
|
501
|
-
|
502
|
-
# Check whether need to make additional requests
|
503
|
-
up_to = real_size if (up_to < 0 || up_to > real_size)
|
504
|
-
list = first_page_audios.first(up_to)
|
505
|
-
while list.length < up_to do
|
506
|
-
playlist_page = load_page_playlist(owner_id, playlist_id, access_hash, offset: list.length)
|
507
|
-
list.concat(audios_from_page(playlist_page).first(up_to - list.length))
|
508
|
-
end
|
509
|
-
|
510
|
-
Playlist.new(list,
|
511
|
-
id: id,
|
512
|
-
owner_id: owner_id,
|
513
|
-
access_hash: access_hash,
|
514
|
-
title: title,
|
515
|
-
subtitle: subtitle,
|
516
|
-
real_size: real_size
|
517
|
-
)
|
518
|
-
end
|
519
|
-
|
520
|
-
##
|
521
|
-
# Load playlist through JSON requests.
|
522
|
-
# @param owner_id [Integer]
|
523
|
-
# @param playlist_id [Integer]
|
524
|
-
# @param access_hash [String, nil]
|
525
|
-
# @param up_to [Integer] if less than 0, all audios will be loaded.
|
526
|
-
# @return [Playlist]
|
527
|
-
def playlist_json(owner_id, playlist_id, access_hash, up_to: -1)
|
528
|
-
if playlist_id == -1
|
529
|
-
first_audios, title, subtitle, real_size = playlist_first_page_json(owner_id, playlist_id, access_hash || "")
|
530
|
-
else
|
531
|
-
first_audios, title, subtitle, real_size = playlist_first_page_web(owner_id, playlist_id, access_hash || "")
|
532
|
-
end
|
533
|
-
# NOTE: We need to load first page from web to be able to unmask links in future
|
534
|
-
|
535
|
-
# Check whether need to make additional requests
|
536
|
-
up_to = real_size if (up_to < 0 || up_to > real_size)
|
537
|
-
list = first_audios.first(up_to)
|
538
|
-
while list.length < up_to do
|
539
|
-
json = load_json_playlist_section(owner_id, playlist_id, access_hash, offset: list.length)
|
540
|
-
audios = begin
|
541
|
-
audios_from_data(json["data"][0]["list"])
|
542
|
-
rescue
|
543
|
-
raise Exceptions::ParseError
|
544
|
-
end
|
545
|
-
list.concat(audios.first(up_to - list.length))
|
546
|
-
end
|
547
|
-
|
548
|
-
begin
|
549
|
-
Playlist.new(list,
|
550
|
-
id: playlist_id,
|
551
|
-
owner_id: owner_id,
|
552
|
-
access_hash: access_hash,
|
553
|
-
title: title,
|
554
|
-
subtitle: subtitle,
|
555
|
-
real_size: real_size
|
556
|
-
)
|
557
|
-
rescue
|
558
|
-
raise Exceptions::ParseError
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
##
|
563
|
-
# Load playlist first page in web and return essential data.
|
564
|
-
# @note not suitable for user audios
|
565
|
-
# @param owner_id [Integer]
|
566
|
-
# @param playlist_id [Integer]
|
567
|
-
# @param access_hash [String, nil]
|
568
|
-
# @return [Array<Array, String, String, Integer>] array with audios from first page, title, subtitle and playlist real size.
|
569
|
-
def playlist_first_page_web(owner_id, playlist_id, access_hash)
|
570
|
-
first_page = load_page_playlist(owner_id, playlist_id, access_hash, offset: 0)
|
571
|
-
begin
|
572
|
-
# Parse out essential data
|
573
|
-
title = first_page.at_css(".audioPlaylist__title").text.strip
|
574
|
-
subtitle = first_page.at_css(".audioPlaylist__subtitle").text.strip
|
575
|
-
|
576
|
-
footer_node = first_page.at_css(".audioPlaylist__footer")
|
577
|
-
if footer_node
|
578
|
-
footer_text = footer_node.text.strip
|
579
|
-
footer_text.gsub!(/\s/, "") # Removing all whitespace to get rid of delimiters ('1 042 audios')
|
580
|
-
footer_match = footer_text.match(/^\d+/)
|
581
|
-
real_size = footer_match ? footer_match[0].to_i : 0
|
582
|
-
else
|
583
|
-
real_size = 0
|
584
|
-
end
|
585
|
-
|
586
|
-
first_audios = audios_from_page(first_page)
|
587
|
-
rescue
|
588
|
-
raise Exceptions::ParseError
|
589
|
-
end
|
590
|
-
[first_audios, title, subtitle, real_size]
|
591
|
-
end
|
592
|
-
|
593
|
-
##
|
594
|
-
# Load playlist first page in JSON and return essential data.
|
595
|
-
# @param owner_id [Integer]
|
596
|
-
# @param playlist_id [Integer]
|
597
|
-
# @param access_hash [String, nil]
|
598
|
-
# @return [Array<Array, String, String, Integer>] array with audios from first page, title, subtitle and playlist real size.
|
599
|
-
def playlist_first_page_json(owner_id, playlist_id, access_hash)
|
600
|
-
first_json = load_json_playlist_section(owner_id, playlist_id, access_hash, offset: 0)
|
601
|
-
begin
|
602
|
-
first_data = first_json["data"][0]
|
603
|
-
first_data_audios = audios_from_data(first_data["list"])
|
604
|
-
rescue
|
605
|
-
raise Exceptions::ParseError
|
606
|
-
end
|
607
|
-
|
608
|
-
real_size = first_data["totalCount"]
|
609
|
-
title = CGI.unescapeHTML(first_data["title"].to_s)
|
610
|
-
subtitle = CGI.unescapeHTML(first_data["subtitle"].to_s)
|
611
|
-
|
612
|
-
[first_data_audios, title, subtitle, real_size]
|
613
|
-
end
|
614
|
-
|
615
|
-
##
|
616
|
-
# Found playlist URLs on *global* search page.
|
617
|
-
# @param obj [Mechanize::Page, String, URI]
|
618
|
-
# @return [Array<String>]
|
619
|
-
def playlist_urls_from_page(obj)
|
620
|
-
page = obj.is_a?(Mechanize::Page) ? obj : load_page(obj)
|
621
|
-
begin
|
622
|
-
page.css(".AudioBlock_music_playlists .AudioPlaylistSlider .al_playlist").map { |elem| elem.attribute("href").to_s }
|
623
|
-
rescue
|
624
|
-
raise Exceptions::ParseError
|
625
|
-
end
|
626
|
-
end
|
627
|
-
|
628
|
-
##
|
629
|
-
# Load audios from wall using JSON request.
|
630
|
-
# @param owner_id [Integer]
|
631
|
-
# @param post_id [Intger]
|
632
|
-
# @param up_to [Integer]
|
633
|
-
# @param with_url [Boolean] whether to retrieve URLs with {Client#from_id} method
|
634
|
-
# @return [Array<Audio>]
|
635
|
-
def wall_json(owner_id, post_id, up_to: 91, with_url: false)
|
636
|
-
if up_to < 0 || up_to > 91
|
637
|
-
up_to = 91
|
638
|
-
VkMusic.warn("Current implementation of this method is not able to return more than 91 audios from wall.")
|
639
|
-
end
|
640
|
-
|
641
|
-
json = load_json_audios_wall(owner_id, post_id)
|
642
|
-
begin
|
643
|
-
data = json["data"][0]
|
644
|
-
audios = audios_from_data(data["list"]).first(up_to)
|
645
|
-
rescue
|
646
|
-
raise Exceptions::ParseError
|
647
|
-
end
|
648
|
-
with_url ? from_id(audios) : audios
|
649
|
-
end
|
650
|
-
|
651
|
-
##
|
652
|
-
# Login to VK.
|
653
|
-
def login(username, password)
|
654
|
-
VkMusic.debug("Logging in.")
|
655
|
-
# Loading login page
|
656
|
-
homepage = load_page(Constants::URL::VK[:login])
|
657
|
-
# Submitting login form
|
658
|
-
login_form = homepage.forms.find { |form| form.action.start_with?(Constants::URL::VK[:login_action]) }
|
659
|
-
login_form[Constants::VK_LOGIN_FORM_NAMES[:username]] = username.to_s
|
660
|
-
login_form[Constants::VK_LOGIN_FORM_NAMES[:password]] = password.to_s
|
661
|
-
after_login = @agent.submit(login_form)
|
662
|
-
|
663
|
-
# Checking whether logged in
|
664
|
-
raise Exceptions::LoginError, "Unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == Constants::URL::VK[:feed]
|
665
|
-
|
666
|
-
# Parsing information about this profile
|
667
|
-
profile = load_page(Constants::URL::VK[:profile])
|
668
|
-
@name = profile.title.to_s
|
669
|
-
@id = profile.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(/\d+/).to_i
|
670
|
-
end
|
671
|
-
|
672
|
-
# Shortcut
|
673
|
-
def unmask_link(link)
|
674
|
-
VkMusic::LinkDecoder.unmask_link(link, @id)
|
675
|
-
end
|
676
|
-
end
|
677
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
# VK client
|
5
|
+
class Client
|
6
|
+
# Default user agent to use
|
7
|
+
DEFAULT_USERAGENT = 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) ' \
|
8
|
+
'AppleWebKit/537.36 (KHTML, like Gecko) ' \
|
9
|
+
'Chrome/86.0.4240.111 Mobile Safari/537.36'
|
10
|
+
public_constant :DEFAULT_USERAGENT
|
11
|
+
# Mximum size of VK playlist
|
12
|
+
MAXIMUM_PLAYLIST_SIZE = 10_000
|
13
|
+
public_constant :MAXIMUM_PLAYLIST_SIZE
|
14
|
+
|
15
|
+
# @return [Integer] ID of client
|
16
|
+
attr_reader :id
|
17
|
+
# @return [String] name of client
|
18
|
+
attr_reader :name
|
19
|
+
# @return [Mechanize] client used to access web pages
|
20
|
+
attr_reader :agent
|
21
|
+
|
22
|
+
# @param login [String, nil]
|
23
|
+
# @param password [String, nil]
|
24
|
+
# @param user_agent [String]
|
25
|
+
# @param agent [Mechanize?] if specified, provided agent will be used
|
26
|
+
def initialize(login: nil, password: nil, user_agent: DEFAULT_USERAGENT, agent: nil)
|
27
|
+
@login = login
|
28
|
+
@password = password
|
29
|
+
@agent = agent
|
30
|
+
if @agent.nil?
|
31
|
+
@agent = Mechanize.new
|
32
|
+
@agent.user_agent = user_agent
|
33
|
+
|
34
|
+
raise('Failed to login!') unless self.login
|
35
|
+
end
|
36
|
+
|
37
|
+
load_id_and_name
|
38
|
+
VkMusic.log.info("Client#{@id}") { "Logged in as User##{@id} (#{@name})" }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Make a login request
|
42
|
+
# @return [Boolean] whether login was successful
|
43
|
+
def login
|
44
|
+
VkMusic.log.info("Client#{@id}") { 'Logging in...' }
|
45
|
+
login = Request::Login.new
|
46
|
+
login.call(agent)
|
47
|
+
login.send_form(@login, @password, agent)
|
48
|
+
return true if login.success?
|
49
|
+
|
50
|
+
VkMusic.log.warn("Client#{@id}") { "Login failed. Redirected to #{login.response.uri}" }
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
54
|
+
# Unmask old VK music link
|
55
|
+
# @param limk [String]
|
56
|
+
def unmask_link(link)
|
57
|
+
VkMusic::LinkDecoder.unmask_link(link, @id)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Search for audio or playlist
|
61
|
+
#
|
62
|
+
# Possible values of +type+ option:
|
63
|
+
# * +:audio+ - search for audios
|
64
|
+
# * +:playlist+ - search for playlists
|
65
|
+
# @note some audios and playlists might be removed from search
|
66
|
+
# @todo search in group audios
|
67
|
+
# @param query [String]
|
68
|
+
# @param type [Symbol]
|
69
|
+
# @return [Array<Audio>, Array<Playlist>]
|
70
|
+
def find(query = '', type: :audio)
|
71
|
+
return [] if query.empty?
|
72
|
+
|
73
|
+
page = Request::Search.new(query, id)
|
74
|
+
page.call(agent)
|
75
|
+
|
76
|
+
case type
|
77
|
+
when :audio, :audios then page.audios
|
78
|
+
when :playlist, :playlists then page.playlists
|
79
|
+
else []
|
80
|
+
end
|
81
|
+
end
|
82
|
+
alias search find
|
83
|
+
|
84
|
+
# Get VK playlist. Specify either +url+ or +(owner_id,playlist_id,access_hash)+
|
85
|
+
# @param url [String, nil]
|
86
|
+
# @param owner_id [Integer, nil]
|
87
|
+
# @param playlist_id [Integer, nil]
|
88
|
+
# @param access_hash [String, nil] access hash for the playlist. Might not exist
|
89
|
+
# @param up_to [Integer] maximum amount of audios to load. If 0, no audios
|
90
|
+
# would be loaded (plain information about playlist)
|
91
|
+
# @return [Playlist?]
|
92
|
+
def playlist(url: nil, owner_id: nil, playlist_id: nil, access_hash: nil,
|
93
|
+
up_to: MAXIMUM_PLAYLIST_SIZE)
|
94
|
+
owner_id, playlist_id, access_hash = Utility::PlaylistUrlParser.call(url) if url
|
95
|
+
return if owner_id.nil? || playlist_id.nil?
|
96
|
+
|
97
|
+
Utility::PlaylistLoader.call(agent, id, owner_id, playlist_id, access_hash, up_to)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Get user or group audios. Specify either +url+ or +owner_id+
|
101
|
+
# @param url [String, nil]
|
102
|
+
# @param owner_id [Integer, nil]
|
103
|
+
# @param up_to [Integer] maximum amount of audios to load. If 0, no audios
|
104
|
+
# would be loaded (plain information about playlist)
|
105
|
+
# @return [Playlist?]
|
106
|
+
def audios(url: nil, owner_id: nil, up_to: MAXIMUM_PLAYLIST_SIZE)
|
107
|
+
owner_id = Utility::ProfileIdResolver.call(agent, url) if url
|
108
|
+
return if owner_id.nil?
|
109
|
+
|
110
|
+
Utility::AudiosLoader.call(agent, id, owner_id, up_to)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get audios on wall of user or group starting. Specify either +url+ or +owner_id+
|
114
|
+
# or +(owner_id,post_id)+
|
115
|
+
# @param url [String] URL to post or profile page
|
116
|
+
# @param owner_id [Integer] numerical ID of wall owner
|
117
|
+
# @param owner_id [Integer] ID of post to start looking from. If not specified, will be
|
118
|
+
# used ID of last post
|
119
|
+
# @return [Playlist?]
|
120
|
+
def wall(url: nil, owner_id: nil, post_id: nil)
|
121
|
+
owner_id, post_id = Utility::PostUrlParser.call(url) if url
|
122
|
+
if post_id.nil?
|
123
|
+
if url
|
124
|
+
owner_id, post_id = Utility::LastProfilePostLoader.call(agent, url: url)
|
125
|
+
elsif owner_id
|
126
|
+
owner_id, post_id = Utility::LastProfilePostLoader.call(agent, owner_id: owner_id)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
return if owner_id.nil? || post_id.nil?
|
130
|
+
|
131
|
+
Utility::WallLoader.call(agent, id, owner_id, post_id)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Get audios attached to post. Specify either +url+ or +(owner_id,post_id)+.
|
135
|
+
# @param url [String]
|
136
|
+
# @param owner_id [Integer]
|
137
|
+
# @param post_id [Integer]
|
138
|
+
# @return [Array<Audio>] array of audios attached to post
|
139
|
+
def post(url: nil, owner_id: nil, post_id: nil)
|
140
|
+
owner_id, post_id = Utility::PostUrlParser.call(url) if url
|
141
|
+
|
142
|
+
return [] if owner_id.nil? || post_id.nil?
|
143
|
+
|
144
|
+
Utility::PostLoader.call(agent, id, owner_id, post_id)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Get audios with download URLs by their IDs and secrets
|
148
|
+
# @param args [Array<Audio, (owner_id, audio_id, secret_1, secret_2),
|
149
|
+
# "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
|
150
|
+
# @return [Array<Audio, nil>] array of: audio with download URLs or audio
|
151
|
+
# without URL if wasn't able to get it for audio or +nil+ if
|
152
|
+
# matching element can't be retrieved for array or string
|
153
|
+
def get_urls(args)
|
154
|
+
ids = Utility::AudiosIdsGetter.call(args)
|
155
|
+
audios = Utility::AudiosFromIdsLoader.call(agent, ids, id)
|
156
|
+
|
157
|
+
args.map do |el|
|
158
|
+
# NOTE: can not load unaccessable audio, so just returning it
|
159
|
+
next el if el.is_a?(Audio) && !el.url_accessable?
|
160
|
+
|
161
|
+
audios.find { |a| a.id_matches?(el) }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
alias from_id get_urls
|
165
|
+
|
166
|
+
# Update download URLs of provided audios
|
167
|
+
# @param audios [Array<Audio>]
|
168
|
+
def update_urls(audios)
|
169
|
+
with_url = get_urls(audios)
|
170
|
+
audios.each.with_index do |audio, i|
|
171
|
+
audio_with_url = with_url[i]
|
172
|
+
audio.update(audio_with_url) if audio_with_url
|
173
|
+
end
|
174
|
+
audios
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def load_id_and_name
|
180
|
+
VkMusic.log.info("Client#{@id}") { 'Loading user id and name' }
|
181
|
+
my_page = Request::MyPage.new
|
182
|
+
my_page.call(agent)
|
183
|
+
@id = my_page.id
|
184
|
+
@name = my_page.name
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|