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.
- checksums.yaml +4 -4
- data/.rspec +3 -3
- data/.standard.yml +3 -3
- data/CHANGELOG.md +5 -5
- data/CODE_OF_CONDUCT.md +84 -84
- data/Gemfile +12 -12
- data/LICENSE.txt +21 -21
- data/README.md +19 -19
- data/Rakefile +10 -10
- data/lib/rubytube/cipher.rb +371 -370
- data/lib/rubytube/client.rb +173 -173
- data/lib/rubytube/extractor.rb +177 -177
- data/lib/rubytube/innertube.rb +105 -105
- data/lib/rubytube/monostate.rb +5 -5
- data/lib/rubytube/parser.rb +164 -164
- data/lib/rubytube/request.rb +75 -75
- data/lib/rubytube/stream.rb +97 -89
- data/lib/rubytube/stream_format.rb +152 -152
- data/lib/rubytube/stream_query.rb +36 -38
- data/lib/rubytube/utils.rb +24 -24
- data/lib/rubytube/version.rb +5 -5
- data/lib/rubytube.rb +68 -68
- data/sig/rubytube.rbs +4 -4
- metadata +8 -7
data/lib/rubytube/extractor.rb
CHANGED
@@ -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
|
data/lib/rubytube/innertube.rb
CHANGED
@@ -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
|
data/lib/rubytube/monostate.rb
CHANGED
@@ -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
|