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.
- 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 +71 -20
- data/LICENSE.txt +0 -0
- data/README.md +113 -94
- data/Rakefile +15 -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 +181 -647
- 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 +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/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 +59 -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
|
+
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
|
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
|