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.
- checksums.yaml +4 -4
- data/.env.example +3 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +56 -0
- data/Gemfile +38 -10
- data/Gemfile.lock +124 -70
- data/LICENSE.txt +0 -0
- data/README.md +121 -94
- data/Rakefile +15 -22
- data/bin/console +18 -24
- data/lib/vk_music.rb +32 -18
- data/lib/vk_music/audio.rb +112 -187
- data/lib/vk_music/client.rb +193 -677
- data/lib/vk_music/playlist.rb +44 -97
- data/lib/vk_music/request.rb +13 -0
- data/lib/vk_music/request/artist.rb +24 -0
- data/lib/vk_music/request/audios_reload.rb +29 -0
- data/lib/vk_music/request/base.rb +75 -0
- data/lib/vk_music/request/login.rb +35 -0
- data/lib/vk_music/request/my_page.rb +21 -0
- data/lib/vk_music/request/playlist.rb +31 -0
- data/lib/vk_music/request/playlist_section.rb +35 -0
- data/lib/vk_music/request/post.rb +22 -0
- data/lib/vk_music/request/profile.rb +24 -0
- data/lib/vk_music/request/search.rb +34 -0
- data/lib/vk_music/request/wall_section.rb +34 -0
- data/lib/vk_music/utility.rb +8 -78
- data/lib/vk_music/utility/artist_loader.rb +17 -0
- data/lib/vk_music/utility/artist_url_parser.rb +22 -0
- data/lib/vk_music/utility/audio_data_parser.rb +37 -0
- data/lib/vk_music/utility/audio_items_parser.rb +18 -0
- data/lib/vk_music/utility/audio_node_parser.rb +59 -0
- data/lib/vk_music/utility/audios_from_ids_loader.rb +21 -0
- data/lib/vk_music/utility/audios_ids_getter.rb +25 -0
- data/lib/vk_music/utility/audios_loader.rb +37 -0
- data/lib/vk_music/utility/data_type_guesser.rb +48 -0
- data/lib/vk_music/utility/duration_parser.rb +17 -0
- data/lib/vk_music/utility/last_profile_post_loader.rb +26 -0
- data/lib/vk_music/utility/link_decoder.rb +107 -0
- data/lib/vk_music/utility/node_text_children_reader.rb +14 -0
- data/lib/vk_music/utility/playlist_loader.rb +30 -0
- data/lib/vk_music/utility/playlist_node_parser.rb +21 -0
- data/lib/vk_music/utility/playlist_section_loader.rb +29 -0
- data/lib/vk_music/utility/playlist_url_parser.rb +32 -0
- data/lib/vk_music/utility/post_loader.rb +23 -0
- data/lib/vk_music/utility/post_url_parser.rb +24 -0
- data/lib/vk_music/utility/profile_id_resolver.rb +58 -0
- data/lib/vk_music/utility/wall_loader.rb +25 -0
- data/lib/vk_music/version.rb +7 -5
- data/lib/vk_music/web_parser.rb +9 -0
- data/lib/vk_music/web_parser/artist.rb +16 -0
- data/lib/vk_music/web_parser/audios_reload.rb +20 -0
- data/lib/vk_music/web_parser/base.rb +27 -0
- data/lib/vk_music/web_parser/login.rb +13 -0
- data/lib/vk_music/web_parser/my_page.rb +19 -0
- data/lib/vk_music/web_parser/playlist.rb +33 -0
- data/lib/vk_music/web_parser/playlist_section.rb +53 -0
- data/lib/vk_music/web_parser/post.rb +15 -0
- data/lib/vk_music/web_parser/profile.rb +33 -0
- data/lib/vk_music/web_parser/search.rb +56 -0
- data/lib/vk_music/web_parser/wall_section.rb +53 -0
- data/vk_music.gemspec +36 -40
- metadata +63 -77
- data/.travis.yml +0 -7
- data/bin/setup +0 -8
- data/lib/vk_music/constants.rb +0 -78
- data/lib/vk_music/exceptions.rb +0 -21
- data/lib/vk_music/link_decoder.rb +0 -102
- data/lib/vk_music/utility/log.rb +0 -51
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Request
|
5
|
+
# Logging in request
|
6
|
+
class Search < Base
|
7
|
+
# Initialize new request
|
8
|
+
# @param query [String]
|
9
|
+
# @param client_id [Integer]
|
10
|
+
def initialize(query, client_id)
|
11
|
+
@client_id = client_id
|
12
|
+
super(
|
13
|
+
"#{VK_ROOT}/audio",
|
14
|
+
{ q: query, _ajax: 1 },
|
15
|
+
'POST',
|
16
|
+
{ 'content-type' => 'application/x-www-form-urlencoded', 'x-requested-with' => 'XMLHttpRequest' }
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def_delegators :@parser, :audios, :audios_all_path, :playlists, :playlists_all_path
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def after_call
|
25
|
+
json = JSON.parse(@response.body.strip)
|
26
|
+
raise 'Captcha requested' if json['key'] == 'captcha_key'
|
27
|
+
|
28
|
+
inner = json['data'][2]
|
29
|
+
html = Nokogiri::HTML.fragment(CGI.unescapeElement(inner))
|
30
|
+
@parser = WebParser::Search.new(html)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Request
|
5
|
+
# Wall in JSON sections request
|
6
|
+
class WallSection < Base
|
7
|
+
# Initialize new request
|
8
|
+
# @param owner_id [Integer]
|
9
|
+
# @param post_id [Integer]
|
10
|
+
# @param offset [Integer]
|
11
|
+
# @param client_id [Integer]
|
12
|
+
def initialize(owner_id, post_id, client_id)
|
13
|
+
@client_id = client_id
|
14
|
+
super(
|
15
|
+
"#{VK_ROOT}/audio",
|
16
|
+
{
|
17
|
+
act: 'load_section', type: 'wall', utf8: true,
|
18
|
+
owner_id: owner_id, post_id: post_id, wall_type: 'own'
|
19
|
+
},
|
20
|
+
'GET',
|
21
|
+
{}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def_delegators :@parser, :audios, :title, :subtitle, :real_size, :more?
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def after_call
|
30
|
+
@parser = WebParser::WallSection.new(@response, client_id: @client_id)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/vk_music/utility.rb
CHANGED
@@ -1,78 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module VkMusic
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
# @param s [Integer] amount of seconds.
|
10
|
-
# @return [String] formatted string.
|
11
|
-
def self.format_seconds(s)
|
12
|
-
s = s.to_i # Require integer
|
13
|
-
"#{(s / 60).to_s.rjust(2, "0")}:#{(s % 60).to_s.rjust(2, "0")}";
|
14
|
-
end
|
15
|
-
|
16
|
-
##
|
17
|
-
# Guess type of request by from string.
|
18
|
-
#
|
19
|
-
# Possible results:
|
20
|
-
# * +:playlist+ - if string match playlist URL.
|
21
|
-
# * +:post+ - if string match post URL.
|
22
|
-
# * +:audios+ - if string match user or group URL.
|
23
|
-
# * +:find+ - in rest of cases.
|
24
|
-
# @param str [String] request from user for some audios.
|
25
|
-
# @return [Symbol]
|
26
|
-
def self.guess_request_type(str)
|
27
|
-
case str
|
28
|
-
when Constants::Regex::VK_PLAYLIST_URL_POSTFIX
|
29
|
-
:playlist
|
30
|
-
when Constants::Regex::VK_WALL_URL_POSTFIX, Constants::Regex::VK_POST_URL_POSTFIX
|
31
|
-
:post
|
32
|
-
when Constants::Regex::VK_BLOCK_URL
|
33
|
-
:block
|
34
|
-
when Constants::Regex::VK_URL
|
35
|
-
:audios
|
36
|
-
else
|
37
|
-
:find
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
##
|
42
|
-
# Turn hash into URL query string.
|
43
|
-
# @param hash [Hash]
|
44
|
-
# @return [String]
|
45
|
-
def self.hash_to_params(hash = {})
|
46
|
-
qs = ""
|
47
|
-
hash.each_key do |key|
|
48
|
-
qs << "&" unless qs.empty?
|
49
|
-
case hash[key]
|
50
|
-
when Array
|
51
|
-
qs << CGI.escape(key.to_s) << "=" << hash[key].map { |value| CGI.escape(value.to_s) }.join(",")
|
52
|
-
else
|
53
|
-
qs << CGI.escape(key.to_s) << "=" << CGI.escape(hash[key].to_s)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
qs
|
57
|
-
end
|
58
|
-
|
59
|
-
##
|
60
|
-
# Get content of text children of provided Node.
|
61
|
-
# @param node [Nokogiri::Xml::Node]
|
62
|
-
# @return [String]
|
63
|
-
def self.plain_text(node)
|
64
|
-
node.children.select(&:text?).map(&:text).join ""
|
65
|
-
end
|
66
|
-
|
67
|
-
##
|
68
|
-
# Turn human readable track length to its size in seconds.
|
69
|
-
# @param str [String] string in format "HH:MM:SS" or something alike (+/d++ Regex selector is used).
|
70
|
-
# @return [Integer] amount of seconds.
|
71
|
-
def self.parse_duration(str)
|
72
|
-
str.scan(/\d+/)
|
73
|
-
.map(&:to_i)
|
74
|
-
.reverse
|
75
|
-
.each_with_index.reduce(0) { |m, arr| m + arr[0] * 60**arr[1] }
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
# Helpers
|
5
|
+
module Utility; end
|
6
|
+
end
|
7
|
+
|
8
|
+
Dir[File.join(__dir__, 'utility', '*.rb')].each { |file| require_relative file }
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Load artist top audios
|
6
|
+
module ArtistLoader
|
7
|
+
# @param agent [Mechanize]
|
8
|
+
# @param client_id [Integer]
|
9
|
+
# @param name [String]
|
10
|
+
# @return [Array<Audio>]
|
11
|
+
def self.call(agent, client_id, name)
|
12
|
+
page = Request::Artist.new(name, client_id).call(agent)
|
13
|
+
page.audios
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Artist URL parser
|
6
|
+
module ArtistUrlParser
|
7
|
+
# Regex for artist URL
|
8
|
+
ARTIST_POSTFIX = %r{.*artist/([\w.\-~]+)}.freeze
|
9
|
+
public_constant :ARTIST_POSTFIX
|
10
|
+
|
11
|
+
# Get artist name based on provided URL
|
12
|
+
|
13
|
+
# @param url [String]
|
14
|
+
# @return [String?]
|
15
|
+
def self.call(url)
|
16
|
+
url.match(ARTIST_POSTFIX)&.captures&.first
|
17
|
+
rescue StandardError
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Parse {Audio} from +Array+ of audio data
|
6
|
+
module AudioDataParser
|
7
|
+
class << self
|
8
|
+
# @param data [Array]
|
9
|
+
# @param client_id [Integer]
|
10
|
+
# @return [Audio]
|
11
|
+
def call(data, client_id)
|
12
|
+
url_encoded = get_url_encoded(data)
|
13
|
+
_add_hash, _edit_hash, _action_hash, _delete_hash, _teplace_hash, url_hash = get_secrets(data)
|
14
|
+
|
15
|
+
Audio.new(id: data[0], owner_id: data[1],
|
16
|
+
secret1: url_hash, secret2: url_hash,
|
17
|
+
artist: CGI.unescapeHTML(data[4]), title: CGI.unescapeHTML(data[3]),
|
18
|
+
duration: data[5],
|
19
|
+
url_encoded: url_encoded, url: nil, client_id: client_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def get_url_encoded(data)
|
25
|
+
url_encoded = data[2].to_s
|
26
|
+
url_encoded = nil if url_encoded.empty?
|
27
|
+
|
28
|
+
url_encoded
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_secrets(data)
|
32
|
+
data[13].to_s.split('/')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Parse {Audio} from +Nokogiri::XML::Node+
|
6
|
+
module AudioItemsParser
|
7
|
+
# @param node [Nokogiri::XML::Node]
|
8
|
+
# @param client_id [Integer]
|
9
|
+
# @return [Array<Audio>]
|
10
|
+
def self.call(node, client_id)
|
11
|
+
node.css('.audio_item.ai_has_btn,.audio_item.audio_item_disabled').map do |elem|
|
12
|
+
data = JSON.parse(elem.attribute('data-audio').value)
|
13
|
+
Utility::AudioDataParser.call(data, client_id)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Parse {Audio} from +Nokogiri::XML::Node+
|
6
|
+
module AudioNodeParser
|
7
|
+
class << self
|
8
|
+
# @param node [Nokogiri::XML::Node]
|
9
|
+
# @param client_id [Integer]
|
10
|
+
# @return [Audio]
|
11
|
+
def call(node, client_id)
|
12
|
+
input = node.at_css('input')
|
13
|
+
if input
|
14
|
+
parse_input(input, node, client_id)
|
15
|
+
else
|
16
|
+
parse_post(node, client_id)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parse_input(input, node, client_id)
|
23
|
+
id_array = get_id_array(node)
|
24
|
+
artist, title, duration = get_main_data(node)
|
25
|
+
|
26
|
+
Audio.new(id: Integer(id_array[1], 10), owner_id: Integer(id_array[0], 10),
|
27
|
+
artist: artist, title: title, duration: duration,
|
28
|
+
url_encoded: get_encoded_url(input), url: nil, client_id: client_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_encoded_url(input)
|
32
|
+
url_encoded = input.attribute('value').to_s
|
33
|
+
url_encoded = nil if url_encoded.empty? || url_encoded == Constants::URL::VK[:audio_unavailable]
|
34
|
+
|
35
|
+
url_encoded
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_id_array(node)
|
39
|
+
node.attribute('data-id').to_s.split('_')
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_main_data(node)
|
43
|
+
[
|
44
|
+
node.at_css('.ai_artist').text.strip,
|
45
|
+
node.at_css('.ai_title').text.strip,
|
46
|
+
Integer(node.at_css('.ai_dur').attribute('data-dur').to_s, 10)
|
47
|
+
]
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_post(node, client_id)
|
51
|
+
artist = node.at_css('.medias_music_author').text.strip
|
52
|
+
title = NodeTextChildrenReader.call(node.at_css('.medias_audio_title'))
|
53
|
+
duration = DurationParser.call(node.at_css('.medias_audio_dur').text)
|
54
|
+
Audio.new(artist: artist, title: title, duration: duration, client_id: client_id)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Load audios from ids
|
6
|
+
module AudiosFromIdsLoader
|
7
|
+
# @param agent [Mechanize]
|
8
|
+
# @param ids [Array<String>]
|
9
|
+
# @return [Array<Audio>]
|
10
|
+
def self.call(agent, ids, client_id)
|
11
|
+
audios = []
|
12
|
+
ids.each_slice(10) do |subarray|
|
13
|
+
page = Request::AudiosReload.new(subarray, client_id)
|
14
|
+
page.call(agent)
|
15
|
+
audios.concat(page.audios)
|
16
|
+
end
|
17
|
+
audios
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Load ids from array of data
|
6
|
+
module AudiosIdsGetter
|
7
|
+
# @param args [Array<Audio, (owner_id, audio_id, secret_1, secret_2),
|
8
|
+
# "#{owner_id}_#{id}_#{secret_1}_#{secret_2}">]
|
9
|
+
# @return [Array<String>] array of uniq full ids
|
10
|
+
def self.call(args)
|
11
|
+
ids = args.map do |item|
|
12
|
+
case item
|
13
|
+
when Audio then item.full_id
|
14
|
+
when Array then item.join('_')
|
15
|
+
when String then item
|
16
|
+
end
|
17
|
+
end
|
18
|
+
ids.compact!
|
19
|
+
ids.uniq!
|
20
|
+
|
21
|
+
ids
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Load user or group audios
|
6
|
+
module AudiosLoader
|
7
|
+
class << self
|
8
|
+
# @param agent [Mechanize]
|
9
|
+
# @param client_id [Integer]
|
10
|
+
# @param owner_id [Integer]
|
11
|
+
# @param up_to [Integer]
|
12
|
+
# @return [Playlist?]
|
13
|
+
def call(agent, client_id, owner_id, up_to)
|
14
|
+
page = Request::PlaylistSection.new(owner_id, -1, '', 0, client_id).call(agent)
|
15
|
+
audios = page.audios
|
16
|
+
return if audios.nil? || audios.empty?
|
17
|
+
|
18
|
+
up_to = page.real_size if up_to > page.real_size
|
19
|
+
|
20
|
+
load_till_up_to(audios, agent, client_id, owner_id, up_to)
|
21
|
+
|
22
|
+
Playlist.new(audios, id: -1, owner_id: owner_id, access_hash: '',
|
23
|
+
title: page.title, subtitle: page.subtitle, real_size: page.real_size)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def load_till_up_to(audios, agent, client_id, owner_id, up_to)
|
29
|
+
return audios.slice!(up_to..) if up_to <= audios.size
|
30
|
+
|
31
|
+
rest = PlaylistSectionLoader.call(agent, client_id, owner_id, -1, '', audios.size, up_to - audios.size)
|
32
|
+
audios.concat(rest)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'playlist_url_parser'
|
4
|
+
require_relative 'post_url_parser'
|
5
|
+
|
6
|
+
module VkMusic
|
7
|
+
module Utility
|
8
|
+
# Guess type of method to use based on string data
|
9
|
+
module DataTypeGuesser
|
10
|
+
# End of playlist URL
|
11
|
+
PLAYLIST_POSTFIX = PlaylistUrlParser::VK_PLAYLIST_URL_POSTFIX
|
12
|
+
public_constant :PLAYLIST_POSTFIX
|
13
|
+
|
14
|
+
# Artist URL postfix
|
15
|
+
ARTIST_POSTFIX = ArtistUrlParser::ARTIST_POSTFIX
|
16
|
+
public_constant :ARTIST_POSTFIX
|
17
|
+
|
18
|
+
# End of post URL
|
19
|
+
POST_POSTFIX = PostUrlParser::POST_POSTFIX
|
20
|
+
public_constant :POST_POSTFIX
|
21
|
+
|
22
|
+
# End of wall URL
|
23
|
+
WALL_POSTFIX = /.*wall(-?\d+)/.freeze
|
24
|
+
public_constant :WALL_POSTFIX
|
25
|
+
|
26
|
+
# End of profile audios URL
|
27
|
+
AUDIOS_POSTFIX = /.*audios(-?\d+)/.freeze
|
28
|
+
public_constant :AUDIOS_POSTFIX
|
29
|
+
|
30
|
+
# Full profile URL regex
|
31
|
+
PROFILE_URL = %r{(?:https?://)?(?:vk\.com/)([^/?&]+)}.freeze
|
32
|
+
public_constant :PROFILE_URL
|
33
|
+
|
34
|
+
# @param data [String]
|
35
|
+
# @return [Symbol]
|
36
|
+
def self.call(data)
|
37
|
+
case data
|
38
|
+
when PLAYLIST_POSTFIX then :playlist
|
39
|
+
when ARTIST_POSTFIX then :artist
|
40
|
+
when POST_POSTFIX then :post
|
41
|
+
when WALL_POSTFIX then :wall
|
42
|
+
when AUDIOS_POSTFIX, PROFILE_URL then :audios
|
43
|
+
else :find
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|