vk_music 3.1.4 → 4.0.1

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