vk_music 3.1.5 → 4.0.2

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.
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 +71 -20
  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 +181 -647
  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 +107 -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,647 +1,181 @@
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_text = footer_node.text.strip
549
- footer_text.gsub!(/\s/, "") # Removing all whitespace to get rid of delimiters ('1 042 audios')
550
- footer_match = footer_text.match(/^\d+/)
551
- real_size = footer_match ? footer_match[0].to_i : 0
552
- else
553
- real_size = 0
554
- end
555
-
556
- first_audios = audios_from_page(first_page)
557
- rescue
558
- raise Exceptions::ParseError
559
- end
560
- [first_audios, title, subtitle, real_size]
561
- end
562
-
563
- ##
564
- # Load playlist first page in JSON and return essential data.
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_json(owner_id, playlist_id, access_hash)
570
- first_json = load_json_playlist_section(owner_id, playlist_id, access_hash, offset: 0)
571
- begin
572
- first_data = first_json["data"][0]
573
- first_data_audios = audios_from_data(first_data["list"])
574
- rescue
575
- raise Exceptions::ParseError
576
- end
577
-
578
- real_size = first_data["totalCount"]
579
- title = CGI.unescapeHTML(first_data["title"].to_s)
580
- subtitle = CGI.unescapeHTML(first_data["subtitle"].to_s)
581
-
582
- [first_data_audios, title, subtitle, real_size]
583
- end
584
-
585
- ##
586
- # Found playlist URLs on *global* search page.
587
- # @param obj [Mechanize::Page, String, URI]
588
- # @return [Array<String>]
589
- def playlist_urls_from_page(obj)
590
- page = obj.is_a?(Mechanize::Page) ? obj : load_page(obj)
591
- begin
592
- page.css(".AudioBlock_music_playlists .AudioPlaylistSlider .al_playlist").map { |elem| elem.attribute("href").to_s }
593
- rescue
594
- raise Exceptions::ParseError
595
- end
596
- end
597
-
598
- ##
599
- # Load audios from wall using JSON request.
600
- # @param owner_id [Integer]
601
- # @param post_id [Intger]
602
- # @param up_to [Integer]
603
- # @param with_url [Boolean] whether to retrieve URLs with {Client#from_id} method
604
- # @return [Array<Audio>]
605
- def wall_json(owner_id, post_id, up_to: 91, with_url: false)
606
- if up_to < 0 || up_to > 91
607
- up_to = 91
608
- VkMusic.warn("Current implementation of this method is not able to return more than 91 audios from wall.")
609
- end
610
-
611
- json = load_json_audios_wall(owner_id, post_id)
612
- begin
613
- data = json["data"][0]
614
- audios = audios_from_data(data["list"]).first(up_to)
615
- rescue
616
- raise Exceptions::ParseError
617
- end
618
- with_url ? from_id(audios) : audios
619
- end
620
-
621
- ##
622
- # Login to VK.
623
- def login(username, password)
624
- VkMusic.debug("Logging in.")
625
- # Loading login page
626
- homepage = load_page(Constants::URL::VK[:login])
627
- # Submitting login form
628
- login_form = homepage.forms.find { |form| form.action.start_with?(Constants::URL::VK[:login_action]) }
629
- login_form[Constants::VK_LOGIN_FORM_NAMES[:username]] = username.to_s
630
- login_form[Constants::VK_LOGIN_FORM_NAMES[:password]] = password.to_s
631
- after_login = @agent.submit(login_form)
632
-
633
- # Checking whether logged in
634
- raise Exceptions::LoginError, "Unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == Constants::URL::VK[:feed]
635
-
636
- # Parsing information about this profile
637
- profile = load_page(Constants::URL::VK[:profile])
638
- @name = profile.title.to_s
639
- @id = profile.link_with(href: Constants::Regex::VK_HREF_ID_CONTAINING).href.slice(/\d+/).to_i
640
- end
641
-
642
- # Shortcut
643
- def unmask_link(link)
644
- VkMusic::LinkDecoder.unmask_link(link, @id)
645
- end
646
- end
647
- 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
+ # Get audios with download URLs by their IDs and secrets
142
+ # @param args [Array<Audio, (owner_id, audio_id, secret_1, secret_2),
143
+ # "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
144
+ # @return [Array<Audio, nil>] array of: audio with download URLs or audio
145
+ # without URL if wasn't able to get it for audio or +nil+ if
146
+ # matching element can't be retrieved for array or string
147
+ def get_urls(args)
148
+ ids = Utility::AudiosIdsGetter.call(args)
149
+ audios = Utility::AudiosFromIdsLoader.call(agent, ids, id)
150
+
151
+ args.map do |el|
152
+ # NOTE: can not load unaccessable audio, so just returning it
153
+ next el if el.is_a?(Audio) && !el.url_accessable?
154
+
155
+ audios.find { |a| a.id_matches?(el) }
156
+ end
157
+ end
158
+ alias from_id get_urls
159
+
160
+ # Update download URLs of provided audios
161
+ # @param audios [Array<Audio>]
162
+ def update_urls(audios)
163
+ with_url = get_urls(audios)
164
+ audios.each.with_index do |audio, i|
165
+ audio_with_url = with_url[i]
166
+ audio.update(audio_with_url) if audio_with_url
167
+ end
168
+ audios
169
+ end
170
+
171
+ private
172
+
173
+ def load_id_and_name
174
+ VkMusic.log.info("Client#{@id}") { 'Loading user id and name' }
175
+ my_page = Request::MyPage.new
176
+ my_page.call(agent)
177
+ @id = my_page.id
178
+ @name = my_page.name
179
+ end
180
+ end
181
+ end