vk_music 3.1.7 → 4.0.0

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