vk_music 3.1.7 → 4.1.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/.env.example +3 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +56 -0
- data/Gemfile +38 -10
- data/Gemfile.lock +124 -70
- data/LICENSE.txt +0 -0
- data/README.md +121 -94
- data/Rakefile +15 -22
- data/bin/console +18 -24
- data/lib/vk_music.rb +32 -18
- data/lib/vk_music/audio.rb +112 -187
- data/lib/vk_music/client.rb +193 -677
- data/lib/vk_music/playlist.rb +44 -97
- data/lib/vk_music/request.rb +13 -0
- data/lib/vk_music/request/artist.rb +24 -0
- data/lib/vk_music/request/audios_reload.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/artist_loader.rb +17 -0
- data/lib/vk_music/utility/artist_url_parser.rb +22 -0
- 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 +48 -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 +107 -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 +58 -0
- data/lib/vk_music/utility/wall_loader.rb +25 -0
- data/lib/vk_music/version.rb +7 -5
- data/lib/vk_music/web_parser.rb +9 -0
- data/lib/vk_music/web_parser/artist.rb +16 -0
- data/lib/vk_music/web_parser/audios_reload.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 +63 -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,18 @@
|
|
1
|
-
|
2
|
-
require "rake/testtask"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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 'rubocop/rake_task'
|
6
|
+
require 'yard'
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
|
12
|
+
YARD::Rake::YardocTask.new do |t|
|
13
|
+
t.files = ['lib/**/*.rb']
|
14
|
+
t.options = ['--any', '--extra', '--opts']
|
15
|
+
t.stats_options = ['--list-undoc']
|
23
16
|
end
|
24
17
|
|
25
|
-
task :
|
18
|
+
task default: %i[rubocop 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,112 @@
|
|
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
|
-
end
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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_owner_id, data_id = case data
|
94
|
+
when Audio then [data.owner_id, data.id]
|
95
|
+
when Array then data.first(2).reverse.map(&:to_i)
|
96
|
+
when String then data.split('_').first(2).map(&:to_i)
|
97
|
+
else return false
|
98
|
+
end
|
99
|
+
|
100
|
+
owner_id == data_owner_id && id == data_id
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [String] pretty-printed audio name
|
104
|
+
def to_s
|
105
|
+
"#{@artist} - #{@title} [#{@duration}s]"
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
attr_reader :id, :owner_id, :secret1, :secret2, :url_decoded, :client_id
|
111
|
+
end
|
112
|
+
end
|
data/lib/vk_music/client.rb
CHANGED
@@ -1,677 +1,193 @@
|
|
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
|
-
|
68
|
-
|
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
|
-
# @param
|
97
|
-
# @param
|
98
|
-
#
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
106
|
-
|
107
|
-
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# @
|
111
|
-
# @param
|
112
|
-
#
|
113
|
-
# @
|
114
|
-
|
115
|
-
|
116
|
-
if
|
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
|
-
end
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
+
# Search for audio or playlist
|
55
|
+
#
|
56
|
+
# Possible values of +type+ option:
|
57
|
+
# * +:audio+ - search for audios
|
58
|
+
# * +:playlist+ - search for playlists
|
59
|
+
# @note some audios and playlists might be removed from search
|
60
|
+
# @todo search in group audios
|
61
|
+
# @param query [String]
|
62
|
+
# @param type [Symbol]
|
63
|
+
# @return [Array<Audio>, Array<Playlist>]
|
64
|
+
def find(query = '', type: :audio)
|
65
|
+
return [] if query.empty?
|
66
|
+
|
67
|
+
page = Request::Search.new(query, id)
|
68
|
+
page.call(agent)
|
69
|
+
|
70
|
+
case type
|
71
|
+
when :audio, :audios then page.audios
|
72
|
+
when :playlist, :playlists then page.playlists
|
73
|
+
else []
|
74
|
+
end
|
75
|
+
end
|
76
|
+
alias search find
|
77
|
+
|
78
|
+
# Get VK playlist. Specify either +url+ or +(owner_id,playlist_id,access_hash)+
|
79
|
+
# @param url [String, nil]
|
80
|
+
# @param owner_id [Integer, nil]
|
81
|
+
# @param playlist_id [Integer, nil]
|
82
|
+
# @param access_hash [String, nil] access hash for the playlist. Might not exist
|
83
|
+
# @param up_to [Integer] maximum amount of audios to load. If 0, no audios
|
84
|
+
# would be loaded (plain information about playlist)
|
85
|
+
# @return [Playlist?]
|
86
|
+
def playlist(url: nil, owner_id: nil, playlist_id: nil, access_hash: nil,
|
87
|
+
up_to: MAXIMUM_PLAYLIST_SIZE)
|
88
|
+
owner_id, playlist_id, access_hash = Utility::PlaylistUrlParser.call(url) if url
|
89
|
+
return if owner_id.nil? || playlist_id.nil?
|
90
|
+
|
91
|
+
Utility::PlaylistLoader.call(agent, id, owner_id, playlist_id, access_hash, up_to)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get user or group audios. Specify either +url+ or +owner_id+
|
95
|
+
# @param url [String, nil]
|
96
|
+
# @param owner_id [Integer, nil]
|
97
|
+
# @param up_to [Integer] maximum amount of audios to load. If 0, no audios
|
98
|
+
# would be loaded (plain information about playlist)
|
99
|
+
# @return [Playlist?]
|
100
|
+
def audios(url: nil, owner_id: nil, up_to: MAXIMUM_PLAYLIST_SIZE)
|
101
|
+
owner_id = Utility::ProfileIdResolver.call(agent, url) if url
|
102
|
+
return if owner_id.nil?
|
103
|
+
|
104
|
+
Utility::AudiosLoader.call(agent, id, owner_id, up_to)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get audios on wall of user or group starting. Specify either +url+ or +owner_id+
|
108
|
+
# or +(owner_id,post_id)+
|
109
|
+
# @param url [String] URL to post or profile page
|
110
|
+
# @param owner_id [Integer] numerical ID of wall owner
|
111
|
+
# @param owner_id [Integer] ID of post to start looking from. If not specified, will be
|
112
|
+
# used ID of last post
|
113
|
+
# @return [Playlist?]
|
114
|
+
def wall(url: nil, owner_id: nil, post_id: nil)
|
115
|
+
owner_id, post_id = Utility::PostUrlParser.call(url) if url
|
116
|
+
if post_id.nil?
|
117
|
+
if url
|
118
|
+
owner_id, post_id = Utility::LastProfilePostLoader.call(agent, url: url)
|
119
|
+
elsif owner_id
|
120
|
+
owner_id, post_id = Utility::LastProfilePostLoader.call(agent, owner_id: owner_id)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
return if owner_id.nil? || post_id.nil?
|
124
|
+
|
125
|
+
Utility::WallLoader.call(agent, id, owner_id, post_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Get audios attached to post. Specify either +url+ or +(owner_id,post_id)+
|
129
|
+
# @param url [String]
|
130
|
+
# @param owner_id [Integer]
|
131
|
+
# @param post_id [Integer]
|
132
|
+
# @return [Array<Audio>] array of audios attached to post
|
133
|
+
def post(url: nil, owner_id: nil, post_id: nil)
|
134
|
+
owner_id, post_id = Utility::PostUrlParser.call(url) if url
|
135
|
+
|
136
|
+
return [] if owner_id.nil? || post_id.nil?
|
137
|
+
|
138
|
+
Utility::PostLoader.call(agent, id, owner_id, post_id)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Artist top audios. Specify either +url+ or +name+ of the artist
|
142
|
+
# @param url [String]
|
143
|
+
# @param name [String]
|
144
|
+
# @return [Array<Audio>] array of audios attached to post
|
145
|
+
def artist(url: nil, name: nil)
|
146
|
+
name = Utility::ArtistUrlParser.call(url) if url
|
147
|
+
|
148
|
+
return [] if name.nil? || name.empty?
|
149
|
+
|
150
|
+
Utility::ArtistLoader.call(agent, id, name)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Get audios with download URLs by their IDs and secrets
|
154
|
+
# @param args [Array<Audio, (owner_id, audio_id, secret_1, secret_2),
|
155
|
+
# "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
|
156
|
+
# @return [Array<Audio, nil>] array of: audio with download URLs or audio
|
157
|
+
# without URL if wasn't able to get it for audio or +nil+ if
|
158
|
+
# matching element can't be retrieved for array or string
|
159
|
+
def get_urls(args)
|
160
|
+
ids = Utility::AudiosIdsGetter.call(args)
|
161
|
+
audios = Utility::AudiosFromIdsLoader.call(agent, ids, id)
|
162
|
+
|
163
|
+
args.map do |el|
|
164
|
+
# NOTE: can not load unaccessable audio, so just returning it
|
165
|
+
next el if el.is_a?(Audio) && !el.url_accessable?
|
166
|
+
|
167
|
+
audios.find { |a| a.id_matches?(el) }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
alias from_id get_urls
|
171
|
+
|
172
|
+
# Update download URLs of provided audios
|
173
|
+
# @param audios [Array<Audio>]
|
174
|
+
def update_urls(audios)
|
175
|
+
with_url = get_urls(audios)
|
176
|
+
audios.each.with_index do |audio, i|
|
177
|
+
audio_with_url = with_url[i]
|
178
|
+
audio.update(audio_with_url) if audio_with_url
|
179
|
+
end
|
180
|
+
audios
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def load_id_and_name
|
186
|
+
VkMusic.log.info("Client#{@id}") { 'Loading user id and name' }
|
187
|
+
my_page = Request::MyPage.new
|
188
|
+
my_page.call(agent)
|
189
|
+
@id = my_page.id
|
190
|
+
@name = my_page.name
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|