vk_music 3.1.5 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +3 -0
  3. data/.github/workflows/ruby.yml +35 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +56 -0
  7. data/Gemfile +38 -10
  8. data/Gemfile.lock +71 -20
  9. data/LICENSE.txt +0 -0
  10. data/README.md +113 -94
  11. data/Rakefile +15 -22
  12. data/bin/console +18 -24
  13. data/lib/vk_music.rb +32 -18
  14. data/lib/vk_music/audio.rb +111 -187
  15. data/lib/vk_music/client.rb +181 -647
  16. data/lib/vk_music/playlist.rb +44 -97
  17. data/lib/vk_music/request.rb +13 -0
  18. data/lib/vk_music/request/audios.rb +29 -0
  19. data/lib/vk_music/request/base.rb +75 -0
  20. data/lib/vk_music/request/login.rb +35 -0
  21. data/lib/vk_music/request/my_page.rb +21 -0
  22. data/lib/vk_music/request/playlist.rb +31 -0
  23. data/lib/vk_music/request/playlist_section.rb +35 -0
  24. data/lib/vk_music/request/post.rb +22 -0
  25. data/lib/vk_music/request/profile.rb +24 -0
  26. data/lib/vk_music/request/search.rb +34 -0
  27. data/lib/vk_music/request/wall_section.rb +34 -0
  28. data/lib/vk_music/utility.rb +8 -78
  29. data/lib/vk_music/utility/audio_data_parser.rb +37 -0
  30. data/lib/vk_music/utility/audio_items_parser.rb +18 -0
  31. data/lib/vk_music/utility/audio_node_parser.rb +59 -0
  32. data/lib/vk_music/utility/audios_from_ids_loader.rb +21 -0
  33. data/lib/vk_music/utility/audios_ids_getter.rb +25 -0
  34. data/lib/vk_music/utility/audios_loader.rb +37 -0
  35. data/lib/vk_music/utility/data_type_guesser.rb +43 -0
  36. data/lib/vk_music/utility/duration_parser.rb +17 -0
  37. data/lib/vk_music/utility/last_profile_post_loader.rb +26 -0
  38. data/lib/vk_music/utility/link_decoder.rb +107 -0
  39. data/lib/vk_music/utility/node_text_children_reader.rb +14 -0
  40. data/lib/vk_music/utility/playlist_loader.rb +30 -0
  41. data/lib/vk_music/utility/playlist_node_parser.rb +21 -0
  42. data/lib/vk_music/utility/playlist_section_loader.rb +29 -0
  43. data/lib/vk_music/utility/playlist_url_parser.rb +32 -0
  44. data/lib/vk_music/utility/post_loader.rb +23 -0
  45. data/lib/vk_music/utility/post_url_parser.rb +24 -0
  46. data/lib/vk_music/utility/profile_id_resolver.rb +58 -0
  47. data/lib/vk_music/utility/wall_loader.rb +25 -0
  48. data/lib/vk_music/version.rb +7 -5
  49. data/lib/vk_music/web_parser.rb +9 -0
  50. data/lib/vk_music/web_parser/audios.rb +20 -0
  51. data/lib/vk_music/web_parser/base.rb +27 -0
  52. data/lib/vk_music/web_parser/login.rb +13 -0
  53. data/lib/vk_music/web_parser/my_page.rb +19 -0
  54. data/lib/vk_music/web_parser/playlist.rb +33 -0
  55. data/lib/vk_music/web_parser/playlist_section.rb +53 -0
  56. data/lib/vk_music/web_parser/post.rb +15 -0
  57. data/lib/vk_music/web_parser/profile.rb +33 -0
  58. data/lib/vk_music/web_parser/search.rb +56 -0
  59. data/lib/vk_music/web_parser/wall_section.rb +53 -0
  60. data/vk_music.gemspec +36 -40
  61. metadata +59 -77
  62. data/.travis.yml +0 -7
  63. data/bin/setup +0 -8
  64. data/lib/vk_music/constants.rb +0 -78
  65. data/lib/vk_music/exceptions.rb +0 -21
  66. data/lib/vk_music/link_decoder.rb +0 -102
  67. data/lib/vk_music/utility/log.rb +0 -51
@@ -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
+ module 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
+ module 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
+ module 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
+ module 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
+ module 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
+ module 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
+ module 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,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Get user or group id from url
6
+ module ProfileIdResolver
7
+ # vk.com url regex
8
+ VK_PATH = %r{(?:https?://)?(?:vk\.com/)?([^/?&]+)}.freeze
9
+ public_constant :VK_PATH
10
+
11
+ # audios list page
12
+ AUDIOS_PATH = /audios(-?\d+)/.freeze
13
+ public_constant :AUDIOS_PATH
14
+
15
+ # vk.com user path regex
16
+ USER_PATH = /id(\d+)/.freeze
17
+ public_constant :USER_PATH
18
+
19
+ # vk.com user club regex
20
+ CLUB_PATH = /(?:club|group|public|event)(\d+)/.freeze
21
+ public_constant :CLUB_PATH
22
+
23
+ class << self
24
+ # @param agent [Mechanize]
25
+ # @param url [String] URL to profile page
26
+ # @return [Integer?] ID of profile or +nil+ if not a profile page
27
+ def call(agent, url)
28
+ path = url.match(VK_PATH)&.captures&.first
29
+ return unless path
30
+
31
+ direct_match = direct_match(path)
32
+ return direct_match if direct_match
33
+
34
+ request = VkMusic::Request::Profile.new(profile_custom_path: path)
35
+ request.call(agent)
36
+ request.id
37
+ rescue Mechanize::ResponseCodeError
38
+ nil
39
+ end
40
+
41
+ private
42
+
43
+ def direct_match(path)
44
+ audios_match = path.match(AUDIOS_PATH)
45
+ return Integer(audios_match.captures.first, 10) if audios_match
46
+
47
+ user_match = path.match(USER_PATH)
48
+ return Integer(user_match.captures.first, 10) if user_match
49
+
50
+ club_match = path.match(CLUB_PATH)
51
+ return -1 * Integer(club_match.captures.first, 10) if club_match
52
+
53
+ nil
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Load wall audios
6
+ module 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,7 @@
1
- module VkMusic
2
- ##
3
- # Library version.
4
- VERSION = "3.1.5"
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ # Library version.
5
+ VERSION = '4.0.2'
6
+ public_constant :VERSION
7
+ 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