vk_music 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 63b8f9761d95697dd5ee579716fe92afdf11b8a1dfed9e1ec4549190149df0ef
4
+ data.tar.gz: 6c061d1b1c76e1012312a076cd286d56ba330e96746d7eef5d05b62b84301773
5
+ SHA512:
6
+ metadata.gz: 5f5dc98d3e2442de549c547a983cdbedb01a3638a2381d8eaaacb20c9453b711ebf684f284991e000387908310410fd897649c47fed7d6ec1dbb61477d139771
7
+ data.tar.gz: cac49dbd426c6825d615f5566c7eb94ee086df721366b626b6bb13a5faed7fdadb56087793f5ffb2f278f6d940502ff8de0244532544099196e0e7227c7ed1ba
data/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Kuznecov Vladislav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ desc "Build gem file"
2
+ task :build do
3
+ `gem build vk_music.gemfile`
4
+ end
5
+
6
+ # TODO: Test task
data/lib/vk_music.rb ADDED
@@ -0,0 +1,7 @@
1
+ require_relative 'vk_music/utility.rb'
2
+ require_relative 'vk_music/constants.rb'
3
+ require_relative 'vk_music/exceptions.rb'
4
+ require_relative 'vk_music/audio.rb'
5
+ require_relative 'vk_music/link_decoder.rb'
6
+ require_relative 'vk_music/playlist.rb'
7
+ require_relative 'vk_music/client.rb'
@@ -0,0 +1,46 @@
1
+ module VkMusic
2
+
3
+ class Audio
4
+
5
+ attr_reader :id, :owner_id, :artist, :title, :duration, :url, :url_encoded
6
+
7
+ def to_s
8
+ "#{@artist} - #{@title} [#{Utility.format_seconds(@duration)}]"
9
+ end
10
+
11
+ def initialize(options)
12
+ # Arguments check
13
+ raise ArgumentError, "options hash must be provided", caller unless options.class == Hash
14
+ raise ArgumentError, "artist is not provided", caller unless options.has_key?(:artist)
15
+ raise ArgumentError, "title is not provided", caller unless options.has_key?(:title)
16
+ raise ArgumentError, "duration is not provided", caller unless options.has_key?(:duration)
17
+
18
+ # Setting up attributes
19
+ @id = options[:id].to_s
20
+ @owner_id = options[:owner_id].to_s
21
+ @artist = options[:artist].to_s
22
+ @title = options[:title].to_s
23
+ @duration = options[:duration].to_i
24
+ @url_encoded = options[:url_encoded].to_s
25
+ @url = options[:url].to_s
26
+ end
27
+
28
+ def self.from_node(node, client_id)
29
+ url_encoded = node.at_css("input").attribute("value").to_s
30
+ url_encoded = nil if url_encoded == "https://m.vk.com/mp3/audio_api_unavailable.mp3"
31
+ id_array = node.attribute("data-id").to_s.split("_")
32
+
33
+ new({
34
+ :id => id_array[1],
35
+ :owner_id => id_array[0],
36
+ :artist => node.at_css(".ai_artist").text.strip,
37
+ :title => node.at_css(".ai_title").text.strip,
38
+ :duration => node.at_css(".ai_dur").attribute("data-dur").to_s.to_i,
39
+ :url_encoded => url_encoded,
40
+ :url => url_encoded ? VkMusic.unmask_link(url_encoded, client_id) : "",
41
+ })
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,113 @@
1
+ require "mechanize"
2
+
3
+ module VkMusic
4
+
5
+ class Client
6
+
7
+ attr_reader :id, :name
8
+
9
+ # Mechanize agent
10
+ @agent = nil
11
+
12
+ def initialize(options)
13
+ # Arguments check
14
+ raise ArgumentError, "options hash must be provided", caller unless options.class == Hash
15
+ raise ArgumentError, "username is not provided", caller unless options.has_key?(:username)
16
+ raise ArgumentError, "password is not provided", caller unless options.has_key?(:password)
17
+
18
+ # Setting up client
19
+ @agent = Mechanize.new
20
+ login(options[:username], options[:password])
21
+ end
22
+
23
+ def find_audio(query)
24
+ uri = URI(VK_URL[:audios])
25
+ uri.query = URI.encode_www_form("act" => "search", "q" => query.to_s)
26
+ load_audios_from(uri)
27
+ end
28
+
29
+ def get_playlist(url, up_to = nil)
30
+ url, owner_id, id, access_hash = url.match(PLAYLIST_URL_REGEX).to_a
31
+
32
+ # Load first page and get info
33
+ first_page = load_playlist_page(owner_id: owner_id, id: id, access_hash: access_hash, offset: 0)
34
+ title = first_page.at_css(".audioPlaylist__title").text.strip
35
+ subtitle = first_page.at_css(".audioPlaylist__subtitle").text.strip
36
+
37
+ footer_node = first_page.at_css(".audioPlaylist__footer")
38
+ if footer_node
39
+ footer_match = footer_node.text.strip.match(/^\d+/)
40
+ playlist_size = footer_match ? footer_match[0].to_i : 0
41
+ else
42
+ playlist_size = 0
43
+ end
44
+
45
+ first_page_audios = load_audios_from(first_page)
46
+
47
+ # Check whether need to make additional requests
48
+ up_to = playlist_size if (up_to.nil? || up_to < 0 || up_to > playlist_size)
49
+ if first_page_audios.length >= up_to
50
+ list = first_page_audios[0, up_to]
51
+ else
52
+ list = first_page_audios
53
+ loop do
54
+ playlist_page = load_playlist_page(owner_id: owner_id, id: id, access_hash: access_hash, offset: list.length)
55
+ list.concat(load_audios_from(playlist_page)[0, up_to - list.length])
56
+ break if list.length == up_to
57
+ end
58
+ end
59
+
60
+ Playlist.new(list, {
61
+ :id => id,
62
+ :owner_id => owner_id,
63
+ :access_hash => access_hash,
64
+ :title => title,
65
+ :subtitle => subtitle,
66
+ })
67
+ end
68
+
69
+ private
70
+ # Loading pages
71
+ def load_page(url)
72
+ uri = URI(url) if url.class != URI
73
+ @agent.get(uri)
74
+ end
75
+ def load_playlist_page(options)
76
+ uri = URI(VK_URL[:audios])
77
+ uri.query = URI.encode_www_form("act" => "audio_playlist#{options[:owner_id]}_#{options[:id]}", "access_hash" => options[:access_hash].to_s, "offset" => options[:offset].to_i)
78
+ load_page(uri)
79
+ end
80
+
81
+
82
+ # Loading audios
83
+ def load_audios_from(obj)
84
+ page = obj.class == Mechanize::Page ? obj : load_page(obj)
85
+ page.css(".audio_item.ai_has_btn").map { |elem| Audio.from_node(elem, @id) }
86
+ end
87
+
88
+
89
+ def login(username, password)
90
+ # Loading login page
91
+ homepage = load_page(VK_URL[:home])
92
+ # Submitting login form
93
+ login_form = homepage.forms.find { |form| form.action.start_with?(VK_URL[:login_action]) }
94
+ login_form[VK_LOGIN_FORM_NAMES[:username]] = username.to_s
95
+ login_form[VK_LOGIN_FORM_NAMES[:password]] = password.to_s
96
+ after_login = @agent.submit(login_form)
97
+
98
+ # Checking whether logged in
99
+ raise LoginError, "unable to login. Redirected to #{after_login.uri.to_s}", caller unless after_login.uri.to_s == VK_URL[:feed]
100
+
101
+ # Parsing information about this profile
102
+ profile = load_page(VK_URL[:profile])
103
+ @name = profile.title
104
+ @id = profile.link_with(href: /audios/).href.slice(/\d+/)
105
+ end
106
+
107
+ def unmask_link(link)
108
+ VkMusic.unmask_link(link, @id)
109
+ end
110
+
111
+ end
112
+
113
+ end
@@ -0,0 +1,28 @@
1
+ module VkMusic
2
+
3
+ # Web
4
+ # DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1636.0 Safari/537.36"
5
+
6
+ VK_URL = {
7
+ :scheme => "https",
8
+ :host => "m.vk.com",
9
+ :home => "https://m.vk.com",
10
+ :profile => "https://m.vk.com/id0",
11
+ :feed => "https://m.vk.com/feed",
12
+ :audios => "https://m.vk.com/audio",
13
+ :login => "https://m.vk.com/login",
14
+ :login_action => "https://login.vk.com",
15
+ }
16
+
17
+ VK_LOGIN_FORM_NAMES = {
18
+ :username => "email",
19
+ :password => "pass",
20
+ }
21
+
22
+ # Playlist
23
+ PLAYLIST_URL_REGEX = /.*audio_playlist(-?[\d]+)_([\d]+)(?:(?:(?:&access_hash=)|\/|%2F)([\da-z]+))?/
24
+
25
+
26
+ # QUESTION: Should I move ALL the constants (string, regex etc) here? It would make code more flexible, but seems like overkill
27
+
28
+ end
@@ -0,0 +1,7 @@
1
+ module VkMusic
2
+
3
+ class LoginError < RuntimeError
4
+ # Unable to login
5
+ end
6
+
7
+ end
@@ -0,0 +1,94 @@
1
+ require "duktape"
2
+
3
+ module VkMusic
4
+
5
+ # Setting up decoding context
6
+ @@js_context = Duktape::Context.new
7
+ js_code = <<END_OF_STRING
8
+ function vk_unmask_link(link, vk_id) {
9
+
10
+ // Utility functions to unmask
11
+
12
+ var audioUnmaskSource = function (encrypted) {
13
+ if (encrypted.indexOf('audio_api_unavailable') != -1) {
14
+ var parts = encrypted.split('?extra=')[1].split('#');
15
+
16
+ var handled_anchor = '' === parts[1] ? '' : handler(parts[1]);
17
+
18
+ var handled_part = handler(parts[0]);
19
+
20
+ if (typeof handled_anchor != 'string' || !handled_part) {
21
+ // if (typeof handled_anchor != 'string') console.warn('Handled_anchor type: ' + typeof handled_anchor);
22
+ // if (!handled_part) console.warn('Handled_part: ' + handled_part);
23
+ return encrypted;
24
+ }
25
+
26
+ handled_anchor = handled_anchor ? handled_anchor.split(String.fromCharCode(9)) : [];
27
+
28
+ for (var func_key, splited_anchor, l = handled_anchor.length; l--;) {
29
+ splited_anchor = handled_anchor[l].split(String.fromCharCode(11));
30
+ func_key = splited_anchor.splice(0, 1, handled_part)[0];
31
+ if (!utility_object[func_key]) {
32
+ // console.warn('Was unable to find key: ' + func_key);
33
+ return encrypted;
34
+ }
35
+ handled_part = utility_object[func_key].apply(null, splited_anchor)
36
+ }
37
+
38
+ if (handled_part && 'http' === handled_part.substr(0, 4)) return handled_part;
39
+ // else console.warn('Failed unmasking: ' + handled_part);
40
+ } else {
41
+ // console.warn('Bad link: ' + encrypted);
42
+ }
43
+ return encrypted;
44
+ }
45
+
46
+ var handler = function (part) {
47
+ if (!part || part.length % 4 == 1) return !1;
48
+ for (var t, i, o = 0, s = 0, a = ''; i = part.charAt(s++);) {
49
+ i = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/='.indexOf(i)
50
+ ~i && (t = o % 4 ? 64 * t + i : i, o++ % 4) && (a += String.fromCharCode(255 & t >> (-2 * o & 6)));
51
+ }
52
+ return a;
53
+ }
54
+
55
+ var utility_object = {
56
+ i: function(e, t) {
57
+ return utility_object.s(e, t ^ vk_id);
58
+ },
59
+ s: function(e, t) {
60
+ var n = e.length;
61
+ if (n) {
62
+ var i = r_func(e, t),
63
+ o = 0;
64
+ for (e = e.split(''); ++o < n;)
65
+ e[o] = e.splice(i[n - 1 - o], 1, e[o])[0];
66
+ e = e.join('')
67
+ }
68
+ return e;
69
+ }
70
+ };
71
+
72
+ var r_func = function (e, t) {
73
+ var n = e.length,
74
+ i = [];
75
+ if (n) {
76
+ var o = n;
77
+ for (t = Math.abs(t); o--;)
78
+ t = (n * (o + 1) ^ t + o) % n,
79
+ i[o] = t;
80
+ }
81
+ return i;
82
+ }
83
+
84
+ return audioUnmaskSource(link);
85
+ }
86
+ END_OF_STRING
87
+ @@js_context.exec_string js_code
88
+
89
+
90
+ def self.unmask_link(link, client_id)
91
+ @@js_context.call_prop("vk_unmask_link", link.to_s, client_id.to_i)
92
+ end
93
+
94
+ end
@@ -0,0 +1,42 @@
1
+ module VkMusic
2
+
3
+ class Playlist
4
+ include Enumerable
5
+
6
+ attr_reader :id, :owner_id, :access_hash, :title, :subtitle
7
+
8
+ def length
9
+ @list.length
10
+ end
11
+ alias size length
12
+
13
+ def to_s
14
+ "#{@subtitle} - #{@title} (#{self.length} аудиозаписей)"
15
+ end
16
+
17
+ def each(&block)
18
+ @list.each(&block)
19
+ end
20
+
21
+ def [](index)
22
+ @list[index]
23
+ end
24
+
25
+ def initialize(list, options = {})
26
+ # Arguments check
27
+ raise ArgumentError, "array of audios must be provided", caller unless list.class == Array
28
+
29
+ # Saving list
30
+ @list = list.dup
31
+
32
+ # Setting up attributes
33
+ @id = options[:id].to_s
34
+ @owner_id = options[:owner_id].to_s
35
+ @access_hash = options[:access_hash].to_s
36
+ @title = options[:title].to_s
37
+ @subtitle = options[:subtitle].to_s
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,12 @@
1
+ module VkMusic
2
+
3
+ module Utility
4
+
5
+ def self.format_seconds(s)
6
+ s = s.to_i # Require integer
7
+ "#{(s / 60).to_s.rjust(2, "0")}:#{(s % 60).to_s.rjust(2, "0")}";
8
+ end
9
+
10
+ end
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ require "minitest/autorun"
2
+ require_relative "../lib/vk_music.rb"
3
+
4
+ class Example < MiniTest::Test
5
+ def test_bad_data
6
+ assert_raises(VkMusic::LoginError) {
7
+ client = VkMusic::Client.new(username: "login", password: "password")
8
+ }
9
+ end
10
+
11
+ # TODO: any way to test correct login?
12
+ end
data/vk_music.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "vk_music"
3
+ s.summary = "Provides interface to work with VK music via HTTP requests"
4
+ s.version = "0.0.1"
5
+ s.author = "Kuznecov Vladislav"
6
+ s.email = "fizvlad@mail.ru"
7
+ s.homepage = "https://github.com/fizvlad/vk-music-rb"
8
+ s.platform = Gem::Platform::RUBY
9
+ s.required_ruby_version = ">=2.5.1"
10
+ s.files = Dir[ "lib/**/**", "test/**/**", "LICENSE", "Rakefile", "README", "vk_music.gemspec" ]
11
+ s.test_files = Dir[ "test/test*.rb" ]
12
+ s.has_rdoc = false
13
+ s.license = "MIT"
14
+
15
+ s.add_runtime_dependency "mechanize", "~>2.7"
16
+ s.add_runtime_dependency "duktape", "~>2.3"
17
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vk_music
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kuznecov Vladislav
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mechanize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: duktape
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.3'
41
+ description:
42
+ email: fizvlad@mail.ru
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - LICENSE
48
+ - Rakefile
49
+ - lib/vk_music.rb
50
+ - lib/vk_music/audio.rb
51
+ - lib/vk_music/client.rb
52
+ - lib/vk_music/constants.rb
53
+ - lib/vk_music/exceptions.rb
54
+ - lib/vk_music/link_decoder.rb
55
+ - lib/vk_music/playlist.rb
56
+ - lib/vk_music/utility.rb
57
+ - test/test_login.rb
58
+ - vk_music.gemspec
59
+ homepage: https://github.com/fizvlad/vk-music-rb
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 2.5.1
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.7.6
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Provides interface to work with VK music via HTTP requests
83
+ test_files:
84
+ - test/test_login.rb