vk_music 3.1.7 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) 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 +124 -70
  9. data/LICENSE.txt +0 -0
  10. data/README.md +121 -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 +112 -187
  15. data/lib/vk_music/client.rb +193 -677
  16. data/lib/vk_music/playlist.rb +44 -97
  17. data/lib/vk_music/request.rb +13 -0
  18. data/lib/vk_music/request/artist.rb +24 -0
  19. data/lib/vk_music/request/audios_reload.rb +29 -0
  20. data/lib/vk_music/request/base.rb +75 -0
  21. data/lib/vk_music/request/login.rb +35 -0
  22. data/lib/vk_music/request/my_page.rb +21 -0
  23. data/lib/vk_music/request/playlist.rb +31 -0
  24. data/lib/vk_music/request/playlist_section.rb +35 -0
  25. data/lib/vk_music/request/post.rb +22 -0
  26. data/lib/vk_music/request/profile.rb +24 -0
  27. data/lib/vk_music/request/search.rb +34 -0
  28. data/lib/vk_music/request/wall_section.rb +34 -0
  29. data/lib/vk_music/utility.rb +8 -78
  30. data/lib/vk_music/utility/artist_loader.rb +17 -0
  31. data/lib/vk_music/utility/artist_url_parser.rb +22 -0
  32. data/lib/vk_music/utility/audio_data_parser.rb +37 -0
  33. data/lib/vk_music/utility/audio_items_parser.rb +18 -0
  34. data/lib/vk_music/utility/audio_node_parser.rb +59 -0
  35. data/lib/vk_music/utility/audios_from_ids_loader.rb +21 -0
  36. data/lib/vk_music/utility/audios_ids_getter.rb +25 -0
  37. data/lib/vk_music/utility/audios_loader.rb +37 -0
  38. data/lib/vk_music/utility/data_type_guesser.rb +48 -0
  39. data/lib/vk_music/utility/duration_parser.rb +17 -0
  40. data/lib/vk_music/utility/last_profile_post_loader.rb +26 -0
  41. data/lib/vk_music/utility/link_decoder.rb +107 -0
  42. data/lib/vk_music/utility/node_text_children_reader.rb +14 -0
  43. data/lib/vk_music/utility/playlist_loader.rb +30 -0
  44. data/lib/vk_music/utility/playlist_node_parser.rb +21 -0
  45. data/lib/vk_music/utility/playlist_section_loader.rb +29 -0
  46. data/lib/vk_music/utility/playlist_url_parser.rb +32 -0
  47. data/lib/vk_music/utility/post_loader.rb +23 -0
  48. data/lib/vk_music/utility/post_url_parser.rb +24 -0
  49. data/lib/vk_music/utility/profile_id_resolver.rb +58 -0
  50. data/lib/vk_music/utility/wall_loader.rb +25 -0
  51. data/lib/vk_music/version.rb +7 -5
  52. data/lib/vk_music/web_parser.rb +9 -0
  53. data/lib/vk_music/web_parser/artist.rb +16 -0
  54. data/lib/vk_music/web_parser/audios_reload.rb +20 -0
  55. data/lib/vk_music/web_parser/base.rb +27 -0
  56. data/lib/vk_music/web_parser/login.rb +13 -0
  57. data/lib/vk_music/web_parser/my_page.rb +19 -0
  58. data/lib/vk_music/web_parser/playlist.rb +33 -0
  59. data/lib/vk_music/web_parser/playlist_section.rb +53 -0
  60. data/lib/vk_music/web_parser/post.rb +15 -0
  61. data/lib/vk_music/web_parser/profile.rb +33 -0
  62. data/lib/vk_music/web_parser/search.rb +56 -0
  63. data/lib/vk_music/web_parser/wall_section.rb +53 -0
  64. data/vk_music.gemspec +36 -40
  65. metadata +63 -77
  66. data/.travis.yml +0 -7
  67. data/bin/setup +0 -8
  68. data/lib/vk_music/constants.rb +0 -78
  69. data/lib/vk_music/exceptions.rb +0 -21
  70. data/lib/vk_music/link_decoder.rb +0 -102
  71. 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,112 @@
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_owner_id, data_id = case data
94
+ when Audio then [data.owner_id, data.id]
95
+ when Array then data.first(2).reverse.map(&:to_i)
96
+ when String then data.split('_').first(2).map(&:to_i)
97
+ else return false
98
+ end
99
+
100
+ owner_id == data_owner_id && id == data_id
101
+ end
102
+
103
+ # @return [String] pretty-printed audio name
104
+ def to_s
105
+ "#{@artist} - #{@title} [#{@duration}s]"
106
+ end
107
+
108
+ protected
109
+
110
+ attr_reader :id, :owner_id, :secret1, :secret2, :url_decoded, :client_id
111
+ end
112
+ end
@@ -1,677 +1,193 @@
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
+ # Search for audio or playlist
55
+ #
56
+ # Possible values of +type+ option:
57
+ # * +:audio+ - search for audios
58
+ # * +:playlist+ - search for playlists
59
+ # @note some audios and playlists might be removed from search
60
+ # @todo search in group audios
61
+ # @param query [String]
62
+ # @param type [Symbol]
63
+ # @return [Array<Audio>, Array<Playlist>]
64
+ def find(query = '', type: :audio)
65
+ return [] if query.empty?
66
+
67
+ page = Request::Search.new(query, id)
68
+ page.call(agent)
69
+
70
+ case type
71
+ when :audio, :audios then page.audios
72
+ when :playlist, :playlists then page.playlists
73
+ else []
74
+ end
75
+ end
76
+ alias search find
77
+
78
+ # Get VK playlist. Specify either +url+ or +(owner_id,playlist_id,access_hash)+
79
+ # @param url [String, nil]
80
+ # @param owner_id [Integer, nil]
81
+ # @param playlist_id [Integer, nil]
82
+ # @param access_hash [String, nil] access hash for the playlist. Might not exist
83
+ # @param up_to [Integer] maximum amount of audios to load. If 0, no audios
84
+ # would be loaded (plain information about playlist)
85
+ # @return [Playlist?]
86
+ def playlist(url: nil, owner_id: nil, playlist_id: nil, access_hash: nil,
87
+ up_to: MAXIMUM_PLAYLIST_SIZE)
88
+ owner_id, playlist_id, access_hash = Utility::PlaylistUrlParser.call(url) if url
89
+ return if owner_id.nil? || playlist_id.nil?
90
+
91
+ Utility::PlaylistLoader.call(agent, id, owner_id, playlist_id, access_hash, up_to)
92
+ end
93
+
94
+ # Get user or group audios. Specify either +url+ or +owner_id+
95
+ # @param url [String, nil]
96
+ # @param owner_id [Integer, nil]
97
+ # @param up_to [Integer] maximum amount of audios to load. If 0, no audios
98
+ # would be loaded (plain information about playlist)
99
+ # @return [Playlist?]
100
+ def audios(url: nil, owner_id: nil, up_to: MAXIMUM_PLAYLIST_SIZE)
101
+ owner_id = Utility::ProfileIdResolver.call(agent, url) if url
102
+ return if owner_id.nil?
103
+
104
+ Utility::AudiosLoader.call(agent, id, owner_id, up_to)
105
+ end
106
+
107
+ # Get audios on wall of user or group starting. Specify either +url+ or +owner_id+
108
+ # or +(owner_id,post_id)+
109
+ # @param url [String] URL to post or profile page
110
+ # @param owner_id [Integer] numerical ID of wall owner
111
+ # @param owner_id [Integer] ID of post to start looking from. If not specified, will be
112
+ # used ID of last post
113
+ # @return [Playlist?]
114
+ def wall(url: nil, owner_id: nil, post_id: nil)
115
+ owner_id, post_id = Utility::PostUrlParser.call(url) if url
116
+ if post_id.nil?
117
+ if url
118
+ owner_id, post_id = Utility::LastProfilePostLoader.call(agent, url: url)
119
+ elsif owner_id
120
+ owner_id, post_id = Utility::LastProfilePostLoader.call(agent, owner_id: owner_id)
121
+ end
122
+ end
123
+ return if owner_id.nil? || post_id.nil?
124
+
125
+ Utility::WallLoader.call(agent, id, owner_id, post_id)
126
+ end
127
+
128
+ # Get audios attached to post. Specify either +url+ or +(owner_id,post_id)+
129
+ # @param url [String]
130
+ # @param owner_id [Integer]
131
+ # @param post_id [Integer]
132
+ # @return [Array<Audio>] array of audios attached to post
133
+ def post(url: nil, owner_id: nil, post_id: nil)
134
+ owner_id, post_id = Utility::PostUrlParser.call(url) if url
135
+
136
+ return [] if owner_id.nil? || post_id.nil?
137
+
138
+ Utility::PostLoader.call(agent, id, owner_id, post_id)
139
+ end
140
+
141
+ # Artist top audios. Specify either +url+ or +name+ of the artist
142
+ # @param url [String]
143
+ # @param name [String]
144
+ # @return [Array<Audio>] array of audios attached to post
145
+ def artist(url: nil, name: nil)
146
+ name = Utility::ArtistUrlParser.call(url) if url
147
+
148
+ return [] if name.nil? || name.empty?
149
+
150
+ Utility::ArtistLoader.call(agent, id, name)
151
+ end
152
+
153
+ # Get audios with download URLs by their IDs and secrets
154
+ # @param args [Array<Audio, (owner_id, audio_id, secret_1, secret_2),
155
+ # "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
156
+ # @return [Array<Audio, nil>] array of: audio with download URLs or audio
157
+ # without URL if wasn't able to get it for audio or +nil+ if
158
+ # matching element can't be retrieved for array or string
159
+ def get_urls(args)
160
+ ids = Utility::AudiosIdsGetter.call(args)
161
+ audios = Utility::AudiosFromIdsLoader.call(agent, ids, id)
162
+
163
+ args.map do |el|
164
+ # NOTE: can not load unaccessable audio, so just returning it
165
+ next el if el.is_a?(Audio) && !el.url_accessable?
166
+
167
+ audios.find { |a| a.id_matches?(el) }
168
+ end
169
+ end
170
+ alias from_id get_urls
171
+
172
+ # Update download URLs of provided audios
173
+ # @param audios [Array<Audio>]
174
+ def update_urls(audios)
175
+ with_url = get_urls(audios)
176
+ audios.each.with_index do |audio, i|
177
+ audio_with_url = with_url[i]
178
+ audio.update(audio_with_url) if audio_with_url
179
+ end
180
+ audios
181
+ end
182
+
183
+ private
184
+
185
+ def load_id_and_name
186
+ VkMusic.log.info("Client#{@id}") { 'Loading user id and name' }
187
+ my_page = Request::MyPage.new
188
+ my_page.call(agent)
189
+ @id = my_page.id
190
+ @name = my_page.name
191
+ end
192
+ end
193
+ end