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.
- checksums.yaml +4 -4
- data/.env.example +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +56 -0
- data/Gemfile +38 -10
- data/Gemfile.lock +78 -24
- data/LICENSE.txt +0 -0
- data/README.md +111 -94
- data/Rakefile +12 -22
- data/bin/console +18 -24
- data/lib/vk_music.rb +32 -18
- data/lib/vk_music/audio.rb +111 -187
- data/lib/vk_music/client.rb +187 -615
- data/lib/vk_music/playlist.rb +44 -97
- data/lib/vk_music/request.rb +13 -0
- data/lib/vk_music/request/audios.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/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 +43 -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 +106 -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 +51 -0
- data/lib/vk_music/utility/wall_loader.rb +25 -0
- data/lib/vk_music/version.rb +8 -5
- data/lib/vk_music/web_parser.rb +9 -0
- data/lib/vk_music/web_parser/audios.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 +58 -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,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
|
data/lib/vk_music/version.rb
CHANGED
@@ -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
|