vk_music 3.1.7 → 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 +123 -70
- 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 -677
- 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,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
|
+
class 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
|
+
secrets = get_secrets(data)
|
14
|
+
|
15
|
+
Audio.new(id: data[0], owner_id: data[1],
|
16
|
+
secret1: secrets[2], secret2: secrets[5],
|
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
|
+
class 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').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
|
+
class 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
|
+
class 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
|
+
class 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
|
+
class 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
|
+
class 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
|
+
class 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
|
+
class 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,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module VkMusic
|
4
|
+
module Utility
|
5
|
+
# Link decoding utilities
|
6
|
+
class 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
|
+
@@js_context.call('vk_unmask_link', link, client_id)
|
100
|
+
rescue StandardError
|
101
|
+
VkMusic.log.warn('LinkDecoder') { "Failed to decode link: #{link}" }
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|