rubytube 0.2.0 → 0.3.0

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.
@@ -1,177 +1,177 @@
1
- module RubyTube
2
- class Extractor
3
- class << self
4
- def playability_status(watch_html)
5
- player_response = initial_player_response(watch_html)
6
- player_response = JSON.parse(player_response)
7
- status_obj = player_response['playabilityStatus'] || {}
8
-
9
- if status_obj.has_key?('liveStreamability')
10
- return ['LIVE_STREAM', 'Video is a live stream.']
11
- end
12
-
13
- if status_obj.has_key?('status')
14
- if status_obj.has_key?('reason')
15
- return [status_obj['status'], [status_obj['reason']]]
16
- end
17
-
18
- if status_obj.has_key?('messages')
19
- return [status_obj['status'], status_obj['messages']]
20
- end
21
- end
22
-
23
- [nil, [nil]]
24
- end
25
-
26
- def video_id(url)
27
- return Utils.regex_search(/(?:v=|\/)([0-9A-Za-z_-]{11}).*/, url, 1)
28
- end
29
-
30
- def js_url(html)
31
- begin
32
- base_js = get_ytplayer_config(html)['assets']['js']
33
- rescue RegexMatchError, NoMethodError
34
- base_js = get_ytplayer_js(html)
35
- end
36
-
37
- "https://youtube.com#{base_js}"
38
- end
39
-
40
- def mime_type_codec(mime_type_codec)
41
- pattern = %r{(\w+\/\w+)\;\scodecs=\"([a-zA-Z\-0-9.,\s]*)\"}
42
- results = mime_type_codec.match(pattern)
43
-
44
- raise RegexMatchError.new("mime_type_codec, pattern=#{pattern}") if results.nil?
45
-
46
- mime_type, codecs = results.captures
47
- [mime_type, codecs.split(",").map(&:strip)]
48
- end
49
-
50
- def get_ytplayer_js(html)
51
- js_url_patterns = [
52
- %r{(/s/player/[\w\d]+/[\w\d_/.]+/base\.js)},
53
- ]
54
-
55
- js_url_patterns.each do |pattern|
56
- function_match = html.match(pattern)
57
- if function_match
58
- return function_match[1]
59
- end
60
- end
61
-
62
- raise RegexMatchError.new('get_ytplayer_js', 'js_url_patterns')
63
- end
64
-
65
- def get_ytplayer_config(html)
66
- config_patterns = [
67
- /ytplayer\.config\s*=\s*/,
68
- /ytInitialPlayerResponse\s*=\s*/
69
- ]
70
-
71
- config_patterns.each do |pattern|
72
- begin
73
- return Parser.parse_for_object(html, pattern)
74
- rescue HTMLParseError => e
75
- next
76
- end
77
- end
78
-
79
- setconfig_patterns = [
80
- /yt\.setConfig\(.*['\"]PLAYER_CONFIG['\"]:\s*/
81
- ]
82
-
83
- setconfig_patterns.each do |pattern|
84
- begin
85
- return Parser.parse_for_object(html, pattern)
86
- rescue HTMLParseError => e
87
- next
88
- end
89
- end
90
-
91
- raise RegexMatchError.new('get_ytplayer_config', 'config_patterns, setconfig_patterns')
92
- end
93
-
94
- def apply_signature(stream_manifest, vid_info, js)
95
- cipher = Cipher.new(js)
96
-
97
- stream_manifest.each_with_index do |stream, i|
98
- begin
99
- url = stream['url']
100
- rescue NoMethodError
101
- live_stream = vid_info.fetch('playabilityStatus', {})['liveStreamability']
102
- if live_stream
103
- raise LiveStreamError.new('UNKNOWN')
104
- end
105
- end
106
-
107
- if url.include?("signature") ||
108
- (!stream.key?("s") && (url.include?("&sig=") || url.include?("&lsig=")))
109
- # For certain videos, YouTube will just provide them pre-signed, in
110
- # which case there's no real magic to download them and we can skip
111
- # the whole signature descrambling entirely.
112
- next
113
- end
114
-
115
- signature = cipher.get_signature(stream['s'])
116
-
117
- parsed_url = URI.parse(url)
118
-
119
- query_params = CGI.parse(parsed_url.query)
120
- query_params.transform_values!(&:first)
121
- query_params['sig'] = signature
122
- unless query_params.key?('ratebypass')
123
- initial_n = query_params['n'].split('')
124
- new_n = cipher.calculate_n(initial_n)
125
- query_params['n'] = new_n
126
- end
127
-
128
- url = "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.path}?#{URI.encode_www_form(query_params)}"
129
-
130
- stream_manifest[i]["url"] = url
131
- end
132
- end
133
-
134
- def apply_descrambler(stream_data)
135
- return if stream_data.has_key?('url')
136
-
137
- # Merge formats and adaptiveFormats into a single array
138
- formats = []
139
- formats += stream_data['formats'] if stream_data.has_key?('formats')
140
- formats += stream_data['adaptiveFormats'] if stream_data.has_key?('adaptiveFormats')
141
-
142
- # Extract url and s from signatureCiphers as necessary
143
- formats.each do |data|
144
- unless data.has_key?('url')
145
- if data.has_key?('signatureCipher')
146
- cipher_url = URI.decode_www_form(data['signatureCipher']).to_h
147
- data['url'] = cipher_url['url']
148
- data['s'] = cipher_url['s']
149
- end
150
- end
151
- data['is_otf'] = data['type'] == 'FORMAT_STREAM_TYPE_OTF'
152
- end
153
-
154
- formats
155
- end
156
-
157
- private
158
-
159
- def initial_player_response(watch_html)
160
- patterns = [
161
- "window\\[['\"]ytInitialPlayerResponse['\"]\\]\\s*=\\s*",
162
- "ytInitialPlayerResponse\\s*=\\s*"
163
- ]
164
-
165
- patterns.each do |pattern|
166
- begin
167
- return Parser.parse_for_object(watch_html, pattern)
168
- rescue HTMLParseError
169
- next
170
- end
171
- end
172
-
173
- raise RegexMatchError.new('initial_player_response', 'initial_player_response_pattern')
174
- end
175
- end
176
- end
177
- end
1
+ module RubyTube
2
+ class Extractor
3
+ class << self
4
+ def playability_status(watch_html)
5
+ player_response = initial_player_response(watch_html)
6
+ player_response = JSON.parse(player_response)
7
+ status_obj = player_response['playabilityStatus'] || {}
8
+
9
+ if status_obj.has_key?('liveStreamability')
10
+ return ['LIVE_STREAM', 'Video is a live stream.']
11
+ end
12
+
13
+ if status_obj.has_key?('status')
14
+ if status_obj.has_key?('reason')
15
+ return [status_obj['status'], [status_obj['reason']]]
16
+ end
17
+
18
+ if status_obj.has_key?('messages')
19
+ return [status_obj['status'], status_obj['messages']]
20
+ end
21
+ end
22
+
23
+ [nil, [nil]]
24
+ end
25
+
26
+ def video_id(url)
27
+ return Utils.regex_search(/(?:v=|\/)([0-9A-Za-z_-]{11}).*/, url, 1)
28
+ end
29
+
30
+ def js_url(html)
31
+ begin
32
+ base_js = get_ytplayer_config(html)['assets']['js']
33
+ rescue RegexMatchError, NoMethodError
34
+ base_js = get_ytplayer_js(html)
35
+ end
36
+
37
+ "https://youtube.com#{base_js}"
38
+ end
39
+
40
+ def mime_type_codec(mime_type_codec)
41
+ pattern = %r{(\w+\/\w+)\;\scodecs=\"([a-zA-Z\-0-9.,\s]*)\"}
42
+ results = mime_type_codec.match(pattern)
43
+
44
+ raise RegexMatchError.new("mime_type_codec, pattern=#{pattern}") if results.nil?
45
+
46
+ mime_type, codecs = results.captures
47
+ [mime_type, codecs.split(",").map(&:strip)]
48
+ end
49
+
50
+ def get_ytplayer_js(html)
51
+ js_url_patterns = [
52
+ %r{(/s/player/[\w\d]+/[\w\d_/.]+/base\.js)},
53
+ ]
54
+
55
+ js_url_patterns.each do |pattern|
56
+ function_match = html.match(pattern)
57
+ if function_match
58
+ return function_match[1]
59
+ end
60
+ end
61
+
62
+ raise RegexMatchError.new('get_ytplayer_js', 'js_url_patterns')
63
+ end
64
+
65
+ def get_ytplayer_config(html)
66
+ config_patterns = [
67
+ /ytplayer\.config\s*=\s*/,
68
+ /ytInitialPlayerResponse\s*=\s*/
69
+ ]
70
+
71
+ config_patterns.each do |pattern|
72
+ begin
73
+ return Parser.parse_for_object(html, pattern)
74
+ rescue HTMLParseError => e
75
+ next
76
+ end
77
+ end
78
+
79
+ setconfig_patterns = [
80
+ /yt\.setConfig\(.*['\"]PLAYER_CONFIG['\"]:\s*/
81
+ ]
82
+
83
+ setconfig_patterns.each do |pattern|
84
+ begin
85
+ return Parser.parse_for_object(html, pattern)
86
+ rescue HTMLParseError => e
87
+ next
88
+ end
89
+ end
90
+
91
+ raise RegexMatchError.new('get_ytplayer_config', 'config_patterns, setconfig_patterns')
92
+ end
93
+
94
+ def apply_signature(stream_manifest, vid_info, js)
95
+ cipher = Cipher.new(js)
96
+
97
+ stream_manifest.each_with_index do |stream, i|
98
+ begin
99
+ url = stream['url']
100
+ rescue NoMethodError
101
+ live_stream = vid_info.fetch('playabilityStatus', {})['liveStreamability']
102
+ if live_stream
103
+ raise LiveStreamError.new('UNKNOWN')
104
+ end
105
+ end
106
+
107
+ if url.include?("signature") ||
108
+ (!stream.key?("s") && (url.include?("&sig=") || url.include?("&lsig=")))
109
+ # For certain videos, YouTube will just provide them pre-signed, in
110
+ # which case there's no real magic to download them and we can skip
111
+ # the whole signature descrambling entirely.
112
+ next
113
+ end
114
+
115
+ signature = cipher.get_signature(stream['s'])
116
+
117
+ parsed_url = URI.parse(url)
118
+
119
+ query_params = CGI.parse(parsed_url.query)
120
+ query_params.transform_values!(&:first)
121
+ query_params['sig'] = signature
122
+ unless query_params.key?('ratebypass')
123
+ initial_n = query_params['n'].split('')
124
+ new_n = cipher.calculate_n(initial_n)
125
+ query_params['n'] = new_n
126
+ end
127
+
128
+ url = "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.path}?#{URI.encode_www_form(query_params)}"
129
+
130
+ stream_manifest[i]["url"] = url
131
+ end
132
+ end
133
+
134
+ def apply_descrambler(stream_data)
135
+ return if stream_data.has_key?('url')
136
+
137
+ # Merge formats and adaptiveFormats into a single array
138
+ formats = []
139
+ formats += stream_data['formats'] if stream_data.has_key?('formats')
140
+ formats += stream_data['adaptiveFormats'] if stream_data.has_key?('adaptiveFormats')
141
+
142
+ # Extract url and s from signatureCiphers as necessary
143
+ formats.each do |data|
144
+ unless data.has_key?('url')
145
+ if data.has_key?('signatureCipher')
146
+ cipher_url = URI.decode_www_form(data['signatureCipher']).to_h
147
+ data['url'] = cipher_url['url']
148
+ data['s'] = cipher_url['s']
149
+ end
150
+ end
151
+ data['is_otf'] = data['type'] == 'FORMAT_STREAM_TYPE_OTF'
152
+ end
153
+
154
+ formats
155
+ end
156
+
157
+ private
158
+
159
+ def initial_player_response(watch_html)
160
+ patterns = [
161
+ "window\\[['\"]ytInitialPlayerResponse['\"]\\]\\s*=\\s*",
162
+ "ytInitialPlayerResponse\\s*=\\s*"
163
+ ]
164
+
165
+ patterns.each do |pattern|
166
+ begin
167
+ return Parser.parse_for_object(watch_html, pattern)
168
+ rescue HTMLParseError
169
+ next
170
+ end
171
+ end
172
+
173
+ raise RegexMatchError.new('initial_player_response', 'initial_player_response_pattern')
174
+ end
175
+ end
176
+ end
177
+ end
@@ -1,105 +1,105 @@
1
- module RubyTube
2
- class InnerTube
3
- DEFALUT_CLIENTS = {
4
- 'WEB' => {
5
- context: {
6
- client: {
7
- clientName: 'WEB',
8
- clientVersion: '2.20200720.00.02'
9
- }
10
- },
11
- header: { 'User-Agent': 'Mozilla/5.0' },
12
- api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
13
- },
14
- 'ANDROID_MUSIC' => {
15
- context: {
16
- client: {
17
- clientName: 'ANDROID_MUSIC',
18
- clientVersion: '5.16.51',
19
- androidSdkVersion: 30,
20
- },
21
- },
22
- header: { 'User-Agent': 'com.google.android.apps.youtube.music/'},
23
- api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
24
- },
25
- 'ANDROID_EMBED' => {
26
- context: {
27
- client: {
28
- clientName: 'ANDROID_EMBEDDED_PLAYER',
29
- clientVersion: '17.31.35',
30
- clientScreen: 'EMBED',
31
- androidSdkVersion: 30,
32
- }
33
- },
34
- header: { 'User-Agent': 'com.google.android.youtube/' },
35
- api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
36
- },
37
- }
38
-
39
- BASE_URL = 'https://www.youtube.com/youtubei/v1'
40
-
41
- attr_accessor :context, :header, :api_key, :access_token, :refresh_token, :use_oauth, :allow_cache, :expires
42
-
43
- def initialize(client: 'ANDROID_MUSIC', use_oauth: false, allow_cache: false)
44
- self.context = DEFALUT_CLIENTS[client][:context]
45
- self.header = DEFALUT_CLIENTS[client][:header]
46
- self.api_key = DEFALUT_CLIENTS[client][:api_key]
47
- self.use_oauth = use_oauth
48
- self.allow_cache = allow_cache
49
- end
50
-
51
- def cache_tokens
52
- return unless allow_cache
53
-
54
- # TODO:
55
- end
56
-
57
- def refresh_bearer_token(force: false)
58
- # TODO:
59
- end
60
-
61
- def fetch_bearer_token
62
- # TODO:
63
- end
64
-
65
- def send(endpoint, query, data)
66
- if use_oauth
67
- query.delete(:key)
68
- end
69
-
70
- headers = {
71
- 'Content-Type': 'application/json',
72
- }
73
-
74
- if use_oauth
75
- if access_token
76
- refresh_bearer_token
77
- headers['Authorization'] = "Bearer #{access_token}"
78
- else
79
- fetch_bearer_token
80
- headers['Authorization'] = "Bearer #{access_token}"
81
- end
82
- end
83
-
84
- options = {}
85
- options[:headers] = headers.merge(header)
86
-
87
- options[:query] = {
88
- key: api_key,
89
- contentCheckOk: true,
90
- racyCheckOk: true,
91
- }.merge(query)
92
- options[:data] = data
93
-
94
- resp = Request.post(endpoint, options)
95
- JSON.parse(resp)
96
- end
97
-
98
- def player(video_id)
99
- endpoint = "#{BASE_URL}/player"
100
- query = { 'videoId' => video_id }
101
-
102
- send(endpoint, query, {context: context})
103
- end
104
- end
105
- end
1
+ module RubyTube
2
+ class InnerTube
3
+ DEFALUT_CLIENTS = {
4
+ 'WEB' => {
5
+ context: {
6
+ client: {
7
+ clientName: 'WEB',
8
+ clientVersion: '2.20200720.00.02'
9
+ }
10
+ },
11
+ header: { 'User-Agent': 'Mozilla/5.0' },
12
+ api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
13
+ },
14
+ 'ANDROID_MUSIC' => {
15
+ context: {
16
+ client: {
17
+ clientName: 'ANDROID_MUSIC',
18
+ clientVersion: '5.16.51',
19
+ androidSdkVersion: 30,
20
+ },
21
+ },
22
+ header: { 'User-Agent': 'com.google.android.apps.youtube.music/'},
23
+ api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
24
+ },
25
+ 'ANDROID_EMBED' => {
26
+ context: {
27
+ client: {
28
+ clientName: 'ANDROID_EMBEDDED_PLAYER',
29
+ clientVersion: '17.31.35',
30
+ clientScreen: 'EMBED',
31
+ androidSdkVersion: 30,
32
+ }
33
+ },
34
+ header: { 'User-Agent': 'com.google.android.youtube/' },
35
+ api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
36
+ },
37
+ }
38
+
39
+ BASE_URL = 'https://www.youtube.com/youtubei/v1'
40
+
41
+ attr_accessor :context, :header, :api_key, :access_token, :refresh_token, :use_oauth, :allow_cache, :expires
42
+
43
+ def initialize(client: 'ANDROID_MUSIC', use_oauth: false, allow_cache: false)
44
+ self.context = DEFALUT_CLIENTS[client][:context]
45
+ self.header = DEFALUT_CLIENTS[client][:header]
46
+ self.api_key = DEFALUT_CLIENTS[client][:api_key]
47
+ self.use_oauth = use_oauth
48
+ self.allow_cache = allow_cache
49
+ end
50
+
51
+ def cache_tokens
52
+ return unless allow_cache
53
+
54
+ # TODO:
55
+ end
56
+
57
+ def refresh_bearer_token(force: false)
58
+ # TODO:
59
+ end
60
+
61
+ def fetch_bearer_token
62
+ # TODO:
63
+ end
64
+
65
+ def send(endpoint, query, data)
66
+ if use_oauth
67
+ query.delete(:key)
68
+ end
69
+
70
+ headers = {
71
+ 'Content-Type': 'application/json',
72
+ }
73
+
74
+ if use_oauth
75
+ if access_token
76
+ refresh_bearer_token
77
+ headers['Authorization'] = "Bearer #{access_token}"
78
+ else
79
+ fetch_bearer_token
80
+ headers['Authorization'] = "Bearer #{access_token}"
81
+ end
82
+ end
83
+
84
+ options = {}
85
+ options[:headers] = headers.merge(header)
86
+
87
+ options[:query] = {
88
+ key: api_key,
89
+ contentCheckOk: true,
90
+ racyCheckOk: true,
91
+ }.merge(query)
92
+ options[:data] = data
93
+
94
+ resp = Request.post(endpoint, options)
95
+ JSON.parse(resp)
96
+ end
97
+
98
+ def player(video_id)
99
+ endpoint = "#{BASE_URL}/player"
100
+ query = { 'videoId' => video_id }
101
+
102
+ send(endpoint, query, {context: context})
103
+ end
104
+ end
105
+ end
@@ -1,5 +1,5 @@
1
- module RubyTube
2
- class Monostate
3
- attr_accessor :title, :duration
4
- end
5
- end
1
+ module RubyTube
2
+ class Monostate
3
+ attr_accessor :title, :duration
4
+ end
5
+ end