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,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,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: action_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,43 @@
|
|
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
|
+
# End of post URL
|
15
|
+
POST_POSTFIX = PostUrlParser::POST_POSTFIX
|
16
|
+
public_constant :POST_POSTFIX
|
17
|
+
|
18
|
+
# End of wall URL
|
19
|
+
WALL_POSTFIX = /.*wall(-?\d+)/.freeze
|
20
|
+
public_constant :WALL_POSTFIX
|
21
|
+
|
22
|
+
# End of profile audios URL
|
23
|
+
AUDIOS_POSTFIX = /.*audios(-?\d+)/.freeze
|
24
|
+
public_constant :AUDIOS_POSTFIX
|
25
|
+
|
26
|
+
# Full profile URL regex
|
27
|
+
PROFILE_URL = %r{(?:https?://)?(?:vk\.com/)([^/?&]+)}.freeze
|
28
|
+
public_constant :PROFILE_URL
|
29
|
+
|
30
|
+
# @param data [String]
|
31
|
+
# @return [Symbol]
|
32
|
+
def self.call(data)
|
33
|
+
case data
|
34
|
+
when PLAYLIST_POSTFIX then :playlist
|
35
|
+
when POST_POSTFIX then :post
|
36
|
+
when WALL_POSTFIX then :wall
|
37
|
+
when AUDIOS_POSTFIX, PROFILE_URL then :audios
|
38
|
+
else :find
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -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
|