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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Turn human readable track length to its size in seconds
6
+ module DurationParser
7
+ # @param str [String] string in format "HH:MM:SS" or something alike (+/d++ Regex selector is used)
8
+ # @return [Integer] amount of seconds
9
+ def self.call(str)
10
+ str.scan(/\d+/)
11
+ .map(&:to_i)
12
+ .reverse
13
+ .each_with_index.reduce(0) { |acc, (count, position)| acc + count * 60**position }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'profile_id_resolver'
4
+
5
+ module VkMusic
6
+ module Utility
7
+ # Get user or group id from url
8
+ module LastProfilePostLoader
9
+ # vk.com url regex
10
+ VK_PATH = ProfileIdResolver::VK_PATH
11
+ private_constant :VK_PATH
12
+
13
+ # @param agent [Mechanize]
14
+ # @param url [String] URL to profile page
15
+ # @return [Array(owner_id?, post_id?)]
16
+ def self.call(agent, url: nil, owner_id: nil)
17
+ path = url&.match(VK_PATH)&.captures&.first
18
+ request = VkMusic::Request::Profile.new(profile_id: owner_id, profile_custom_path: path)
19
+ request.call(agent)
20
+ [request.id, request.last_post_id]
21
+ rescue Mechanize::ResponseCodeError
22
+ [nil, nil]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ module Utility
5
+ # Link decoding utilities
6
+ module LinkDecoder
7
+ # JS code which creates function to unmask audio URL.
8
+ JS_CODE = <<~HEREDOC
9
+ function vk_unmask_link(link, vk_id) {
10
+
11
+ // Utility functions to unmask
12
+
13
+ var audioUnmaskSource = function (encrypted) {
14
+ if (encrypted.indexOf('audio_api_unavailable') != -1) {
15
+ var parts = encrypted.split('?extra=')[1].split('#');
16
+
17
+ var handled_anchor = '' === parts[1] ? '' : handler(parts[1]);
18
+
19
+ var handled_part = handler(parts[0]);
20
+
21
+ if (typeof handled_anchor != 'string' || !handled_part) {
22
+ // if (typeof handled_anchor != 'string') console.warn('Handled_anchor type: ' + typeof handled_anchor);
23
+ // if (!handled_part) console.warn('Handled_part: ' + handled_part);
24
+ return encrypted;
25
+ }
26
+
27
+ handled_anchor = handled_anchor ? handled_anchor.split(String.fromCharCode(9)) : [];
28
+
29
+ for (var func_key, splited_anchor, l = handled_anchor.length; l--;) {
30
+ splited_anchor = handled_anchor[l].split(String.fromCharCode(11));
31
+ func_key = splited_anchor.splice(0, 1, handled_part)[0];
32
+ if (!utility_object[func_key]) {
33
+ // console.warn('Was unable to find key: ' + func_key);
34
+ return encrypted;
35
+ }
36
+ handled_part = utility_object[func_key].apply(null, splited_anchor)
37
+ }
38
+
39
+ if (handled_part && 'http' === handled_part.substr(0, 4)) return handled_part;
40
+ // else console.warn('Failed unmasking: ' + handled_part);
41
+ } else {
42
+ // console.warn('Bad link: ' + encrypted);
43
+ }
44
+ return encrypted;
45
+ }
46
+
47
+ var handler = function (part) {
48
+ if (!part || part.length % 4 == 1) return !1;
49
+ for (var t, i, o = 0, s = 0, a = ''; i = part.charAt(s++);) {
50
+ i = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/='.indexOf(i)
51
+ ~i && (t = o % 4 ? 64 * t + i : i, o++ % 4) && (a += String.fromCharCode(255 & t >> (-2 * o & 6)));
52
+ }
53
+ return a;
54
+ }
55
+
56
+ var utility_object = {
57
+ i: function(e, t) {
58
+ return utility_object.s(e, t ^ vk_id);
59
+ },
60
+ s: function(e, t) {
61
+ var n = e.length;
62
+ if (n) {
63
+ var i = r_func(e, t),
64
+ o = 0;
65
+ for (e = e.split(''); ++o < n;)
66
+ e[o] = e.splice(i[n - 1 - o], 1, e[o])[0];
67
+ e = e.join('')
68
+ }
69
+ return e;
70
+ }
71
+ };
72
+
73
+ var r_func = function (e, t) {
74
+ var n = e.length,
75
+ i = [];
76
+ if (n) {
77
+ var o = n;
78
+ for (t = Math.abs(t); o--;)
79
+ t = (n * (o + 1) ^ t + o) % n,
80
+ i[o] = t;
81
+ }
82
+ return i;
83
+ }
84
+
85
+ return audioUnmaskSource(link);
86
+ }
87
+ HEREDOC
88
+ private_constant :JS_CODE
89
+
90
+ # JS context with unmasking link
91
+ @@js_context = ExecJS.compile(JS_CODE)
92
+
93
+ # Unmask audio download URL
94
+ # @param link [String] encoded link to audio. Usually looks like
95
+ # "https://m.vk.com/mp3/audio_api_unavailable.mp3?extra=...".
96
+ # @param client_id [Integer] ID of user which got this link. ID is required for decoding.
97
+ # @return [String?] audio download URL, which can be used only from current IP.
98
+ def self.call(link, client_id)
99
+ VkMusic.log.debug('LinkDecoder') { "Unmasking link `#{link}` with client id #{client_id}" }
100
+ @@js_context.call('vk_unmask_link', link, client_id)
101
+ rescue StandardError => e
102
+ VkMusic.log.warn('LinkDecoder') { "Failed to decode link `#{link}`: #{e.full_message}" }
103
+ nil
104
+ end
105
+ end
106
+ end
107
+ end
@@ -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.7"
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module VkMusic
4
+ # Library version.
5
+ VERSION = '4.1.0'
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 }