vk_music 3.1.3 → 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 +78 -24
  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 -615
  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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Read inner of text-childrens of +Nokogiri::XML::Node+ node
6
+ class NodeTextChildrenReader
7
+ # @param node [Nokogiri::XML::Node]
8
+ # @return [String]
9
+ def self.call(node)
10
+ node.children.select(&:text?).map(&:text).join('').strip
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Load playlist audios
6
+ class PlaylistLoader
7
+ # @param agent [Mechanize]
8
+ # @param client_id [Integer]
9
+ # @param owner_id [Integer]
10
+ # @param playlist_id [Integer]
11
+ # @param access_hash [String, nil]
12
+ # @param up_to [Integer]
13
+ # @return [Playlist?]
14
+ def self.call(agent, client_id, owner_id, playlist_id, access_hash, up_to)
15
+ page = Request::Playlist.new(owner_id, playlist_id, access_hash, client_id)
16
+ page.call(agent)
17
+ audios = page.audios
18
+ return if audios.nil? || audios.empty?
19
+
20
+ up_to = page.real_size if up_to > page.real_size
21
+
22
+ rest = PlaylistSectionLoader.call(agent, client_id, owner_id, playlist_id, access_hash,
23
+ audios.size, up_to - audios.size)
24
+ audios.concat(rest)
25
+ Playlist.new(audios, id: playlist_id, owner_id: owner_id, access_hash: access_hash,
26
+ title: page.title, subtitle: page.subtitle, real_size: page.real_size)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Read inner of text-childrens of +Nokogiri::XML::Node+ node
6
+ class PlaylistNodeParser
7
+ # @param node [Nokogiri::XML::Node]
8
+ # @return [Playlist]
9
+ def self.call(node)
10
+ url = node.at_css('.audioPlaylists__itemLink').attribute('href').value
11
+ owner_id, id, access_hash = PlaylistUrlParser.call(url)
12
+
13
+ Playlist.new([],
14
+ id: id, owner_id: owner_id, access_hash: access_hash,
15
+ title: node.at_css('.audioPlaylists__itemTitle').content,
16
+ subtitle: node.at_css('.audioPlaylists__itemSubtitle').content,
17
+ real_size: nil)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Load sections into playlist
6
+ class PlaylistSectionLoader
7
+ # @param agent [Mechanize]
8
+ # @param client_id [Integer]
9
+ # @param owner_id [Integer]
10
+ # @param playlist_id [Integer]
11
+ # @param access_hash [String, nil]
12
+ # @param offset [Integer]
13
+ # @param up_to [Integer]
14
+ # @return [Array<Audio>]
15
+ def self.call(agent, client_id, owner_id, playlist_id, access_hash, offset, up_to)
16
+ audios = []
17
+
18
+ while audios.size < up_to
19
+ section = Request::PlaylistSection.new(owner_id, playlist_id, access_hash, offset + audios.size, client_id)
20
+ section.call(agent)
21
+ audios.concat(section.audios)
22
+ break if section.audios.empty? || !section.more?
23
+ end
24
+
25
+ audios.first(up_to)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Read inner of text-childrens of +Nokogiri::XML::Node+ node
6
+ class PlaylistUrlParser
7
+ # Regular expression to parse playlist link. Oh my, it is so big
8
+ VK_PLAYLIST_URL_POSTFIX = %r{
9
+ .* # Garbage
10
+ (?:audio_playlist|album/|playlist/) # Start of ids
11
+ (-?\d+)_(\d+) # Ids themself
12
+ (?:(?:(?:.*(?=&access_hash=)&access_hash=)|/|%2F|_)([\da-z]+))? # Access hash
13
+ }x.freeze
14
+ public_constant :VK_PLAYLIST_URL_POSTFIX
15
+
16
+ # @param url [String]
17
+ # @return [Array(Integer?, Integer?, String?)] playlist data array:
18
+ # +[owner_id, playlist_id, access_hash]+
19
+ def self.call(url)
20
+ owner_id, playlist_id, access_hash = url.match(VK_PLAYLIST_URL_POSTFIX).captures
21
+
22
+ owner_id = Integer(owner_id, 10)
23
+ playlist_id = Integer(playlist_id, 10)
24
+ access_hash = nil if access_hash&.empty?
25
+
26
+ [owner_id, playlist_id, access_hash]
27
+ rescue StandardError
28
+ [nil, nil, nil]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Load wall audios
6
+ class PostLoader
7
+ # @param agent [Mechanize]
8
+ # @param client_id [Integer]
9
+ # @param owner_id [Integer]
10
+ # @param post_id [Integer]
11
+ # @return [Array<Audio>]
12
+ def self.call(agent, client_id, owner_id, post_id)
13
+ page = Request::Post.new(owner_id, post_id, client_id)
14
+ page.call(agent)
15
+ urlles_audios = page.audios
16
+
17
+ wall_audios = WallLoader.call(agent, client_id, owner_id, post_id).audios
18
+
19
+ urlles_audios.map { |urlles| wall_audios.find { |audio| audio.like?(urlles) } }.compact
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Load wall audios
6
+ class PostUrlParser
7
+ # Regex for post URL
8
+ POST_POSTFIX = /.*wall(-?\d+)_(\d+)/.freeze
9
+ public_constant :POST_POSTFIX
10
+
11
+ # @param url [String]
12
+ # @return [Array(owner_id?, post_id?)]
13
+ def self.call(url)
14
+ matches = url.match(POST_POSTFIX)&.captures
15
+ return [nil, nil] unless matches && matches.size == 2
16
+
17
+ owner_id = Integer(matches[0], 10)
18
+ post_id = Integer(matches[1], 10)
19
+
20
+ [owner_id, post_id]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Get user or group id from url
6
+ class ProfileIdResolver
7
+ # vk.com url regex
8
+ VK_PATH = %r{(?:https?://)?(?:vk\.com/)?([^/?&]+)}.freeze
9
+ public_constant :VK_PATH
10
+
11
+ # vk.com user path regex
12
+ USER_PATH = /id(\d+)/.freeze
13
+ public_constant :USER_PATH
14
+
15
+ # vk.com user club regex
16
+ CLUB_PATH = /(?:club|group|public|event)(\d+)/.freeze
17
+ public_constant :CLUB_PATH
18
+
19
+ class << self
20
+ # @param agent [Mechanize]
21
+ # @param url [String] URL to profile page
22
+ # @return [Integer?] ID of profile or +nil+ if not a profile page
23
+ def call(agent, url)
24
+ path = url.match(VK_PATH)&.captures&.first
25
+ return unless path
26
+
27
+ direct_match = direct_match(path)
28
+ return direct_match if direct_match
29
+
30
+ request = VkMusic::Request::Profile.new(profile_custom_path: path)
31
+ request.call(agent)
32
+ request.id
33
+ rescue Mechanize::ResponseCodeError
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def direct_match(path)
40
+ user_match = path.match(USER_PATH)
41
+ return Integer(user_match.captures.first, 10) if user_match
42
+
43
+ club_match = path.match(CLUB_PATH)
44
+ return -1 * Integer(club_match.captures.first, 10) if club_match
45
+
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Load wall audios
6
+ class WallLoader
7
+ # @param agent [Mechanize]
8
+ # @param client_id [Integer]
9
+ # @param owner_id [Integer]
10
+ # @param post_id [Integer]
11
+ # @param up_to [Integer]
12
+ # @return [Playlist?]
13
+ def self.call(agent, client_id, owner_id, post_id)
14
+ page = Request::WallSection.new(owner_id, post_id, client_id)
15
+ page.call(agent)
16
+ audios = page.audios
17
+ return if audios.nil? || audios.empty?
18
+
19
+ Playlist.new(audios, id: 0, owner_id: owner_id, access_hash: '',
20
+ title: page.title, subtitle: page.subtitle,
21
+ real_size: audios.size)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,8 @@
1
- module VkMusic
2
- ##
3
- # Library version.
4
- VERSION = "3.1.3"
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ # Library version.
5
+ VERSION = '4.0.0'
6
+
7
+ public_constant :VERSION
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ # Parses out any data from page received by {Request} objects
5
+ module WebParser; end
6
+ end
7
+
8
+ require_relative 'web_parser/base'
9
+ Dir[File.join(__dir__, 'web_parser', '*.rb')].each { |file| require_relative file }
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module WebParser
5
+ # Audios reload JSON parser
6
+ class AudiosReload < Base
7
+ # Array with audio data
8
+ def audios_data
9
+ @audios_data ||= json['data'].first || []
10
+ end
11
+
12
+ # @return [Array<Audio>]
13
+ def audios
14
+ audios_data.map do |el|
15
+ Utility::AudioDataParser.call(el, @client_id)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module WebParser
5
+ # Base class for all web parsers
6
+ class Base
7
+ # @param content [String, Nokogiri::XML::Searchable]
8
+ # @param client_id [Integer?]
9
+ def initialize(content, client_id: nil)
10
+ @content = content
11
+ @client_id = client_id
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :content
17
+
18
+ def node
19
+ @node ||= @content.is_a?(String) ? Nokogiri::HTML.fragment(@content) : @content
20
+ end
21
+
22
+ def json
23
+ @json ||= JSON.parse(@content.is_a?(String) ? @content : @content.body)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module WebParser
5
+ # Login page parser
6
+ class Login < Base
7
+ # @return [Mechanize::Form]
8
+ def login_form
9
+ node.forms.find { |f| f.action.start_with?('https://login.vk.com') }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module WebParser
5
+ # Current user page parser
6
+ class MyPage < Base
7
+ # User id
8
+ def id
9
+ Integer(node.content.match(/window.vk = {"id":(\d+)/).captures.first, 10)
10
+ end
11
+
12
+ # User name
13
+ def name
14
+ link = node.at_css('.ip_user_link .op_owner')
15
+ link.attribute('data-name').value
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module WebParser
5
+ # Playlist mobile web page parser
6
+ class Playlist < Base
7
+ # @return [Array<Audio>]
8
+ def audios
9
+ Utility::AudioItemsParser.call(node, @client_id)
10
+ end
11
+
12
+ # @return [String]
13
+ def title
14
+ node.at_css('.audioPlaylist__title').content.strip
15
+ end
16
+
17
+ # @return [String?]
18
+ def subtitle
19
+ result = node.at_css('.audioPlaylist__subtitle').content.strip
20
+ return if result.nil? || result.empty?
21
+
22
+ result
23
+ end
24
+
25
+ # @return [Integer?]
26
+ def real_size
27
+ content = node.at_css('.audioPlaylist__footer').content
28
+ matches = content.gsub(/\s/, '').match(/^(\d+)/)&.captures
29
+ matches ? Integer(matches.first, 10) : nil
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module WebParser
5
+ # PlaylistSection JSON parser
6
+ class PlaylistSection < Base
7
+ # Parsed JSON
8
+ def data
9
+ @data ||= json['data'].first || {}
10
+ end
11
+
12
+ # @return [Array<Audio>]
13
+ def audios
14
+ return unless data&.key?('list')
15
+
16
+ data['list'].map do |el|
17
+ Utility::AudioDataParser.call(el, @client_id)
18
+ end
19
+ end
20
+
21
+ # @return [String]
22
+ def title
23
+ return unless data&.key?('title')
24
+
25
+ data['title'].to_s
26
+ end
27
+
28
+ # @return [String?]
29
+ def subtitle
30
+ return unless data&.key?('rawDescription')
31
+
32
+ re = data['rawDescription']
33
+ return if re.nil? || re.empty?
34
+
35
+ re
36
+ end
37
+
38
+ # @return [Integer?]
39
+ def real_size
40
+ return unless data&.key?('totalCount')
41
+
42
+ data['totalCount']
43
+ end
44
+
45
+ # @return [Boolean]
46
+ def more?
47
+ return unless data&.key?('hasMore')
48
+
49
+ data['hasMore'].to_s == '1'
50
+ end
51
+ end
52
+ end
53
+ end