rubytube 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/lib/rubytube/cipher.rb +68 -69
- data/lib/rubytube/client.rb +21 -21
- data/lib/rubytube/error.rb +49 -0
- data/lib/rubytube/extractor.rb +51 -57
- data/lib/rubytube/innertube.rb +31 -31
- data/lib/rubytube/parser.rb +35 -40
- data/lib/rubytube/request.rb +4 -6
- data/lib/rubytube/stream.rb +17 -19
- data/lib/rubytube/stream_format.rb +2 -2
- data/lib/rubytube/stream_query.rb +6 -2
- data/lib/rubytube/version.rb +1 -1
- data/lib/rubytube.rb +15 -57
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 57d1f41767aaf6489fa9c4ed47e82f122da84f3bbf3d7f69821f86d7d15d4d04
|
4
|
+
data.tar.gz: 8c453e409b21603e87a701acba2725f56b570078d89a9e73d6a643568c6580df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a23289c4db4a278a0c0916cb53ad910e2bb1ecab5969b7d01253a4e881b149df8e84572754c9ff21b41b89504dac0c2bfa051b309cc66fe72278880bf4cdd51e
|
7
|
+
data.tar.gz: 172741601630ae665a819870ecefd9a0bc908afb393c318f65eb8b28e7e8cf26d85cc20d1aba8144237abec6c9b32977b6d4a33686b6ec1b5942fd00820bac4d
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# RubyTube
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/rubytube.svg)](https://badge.fury.io/rb/rubytube)
|
4
|
+
|
3
5
|
RubyTube is a Ruby implementation of the popular Python library, pytube. This library facilitates the downloading and streaming of YouTube videos, offering the robust functionality of pytube in a Ruby-friendly format.
|
4
6
|
|
5
7
|
## Installation
|
data/lib/rubytube/cipher.rb
CHANGED
@@ -5,20 +5,20 @@ module RubyTube
|
|
5
5
|
def initialize(js)
|
6
6
|
self.transform_plan = get_transform_plan(js)
|
7
7
|
|
8
|
-
var_regex = %r
|
8
|
+
var_regex = %r{^\$*\w+\W}
|
9
9
|
var_match = @transform_plan[0].match(var_regex)
|
10
|
-
|
10
|
+
|
11
11
|
if var_match.nil?
|
12
12
|
raise "RegexMatchError, caller: __init__, pattern: #{var_regex.source}"
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
var = var_match[0][0..-2]
|
16
|
-
|
16
|
+
|
17
17
|
self.transform_map = get_transform_map(js, var)
|
18
18
|
|
19
19
|
self.js_func_patterns = [
|
20
|
-
%r
|
21
|
-
%r
|
20
|
+
%r{\w+\.(\w+)\(\w,(\d+)\)},
|
21
|
+
%r{\w+\[("\w+")\]\(\w,(\d+)\)}
|
22
22
|
]
|
23
23
|
|
24
24
|
self.throttling_array = get_throttling_function_array(js)
|
@@ -27,14 +27,14 @@ module RubyTube
|
|
27
27
|
|
28
28
|
def calculate_n(initial_n)
|
29
29
|
throttling_array.map! do |item|
|
30
|
-
item ==
|
30
|
+
(item == "b") ? initial_n : item
|
31
31
|
end
|
32
32
|
|
33
33
|
throttling_plan.each do |step|
|
34
34
|
curr_func = throttling_array[step[0].to_i]
|
35
35
|
|
36
36
|
unless curr_func.respond_to?(:call)
|
37
|
-
raise ExtractError.new(
|
37
|
+
raise ExtractError.new("calculate_n", "curr_func")
|
38
38
|
end
|
39
39
|
|
40
40
|
first_arg = throttling_array[step[1].to_i]
|
@@ -52,14 +52,14 @@ module RubyTube
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def get_signature(ciphered_signature)
|
55
|
-
signature = ciphered_signature.
|
55
|
+
signature = ciphered_signature.chars
|
56
56
|
|
57
57
|
transform_plan.each do |js_func|
|
58
58
|
name, argument = parse_function(js_func)
|
59
59
|
signature = transform_map[name].call(signature, argument)
|
60
60
|
end
|
61
61
|
|
62
|
-
signature.join(
|
62
|
+
signature.join("")
|
63
63
|
end
|
64
64
|
|
65
65
|
private
|
@@ -76,24 +76,24 @@ module RubyTube
|
|
76
76
|
return [fn_name, fn_arg]
|
77
77
|
end
|
78
78
|
|
79
|
-
raise RegexMatchError.new(
|
79
|
+
raise RegexMatchError.new("parse_function", "js_func_patterns")
|
80
80
|
end
|
81
81
|
end
|
82
82
|
|
83
83
|
def get_initial_function_name(js)
|
84
84
|
function_patterns = [
|
85
|
-
%r
|
86
|
-
%r
|
85
|
+
%r{\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
86
|
+
%r{\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
87
87
|
%r'(?:\b|[^a-zA-Z0-9$])(?<sig>[a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', # noqa: E501
|
88
88
|
%r'(?<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)', # noqa: E501
|
89
|
-
%r
|
90
|
-
%r
|
91
|
-
%r
|
92
|
-
%r
|
93
|
-
%r
|
94
|
-
%r
|
95
|
-
%r
|
96
|
-
%r
|
89
|
+
%r{(?<quote>["\'])signature\k<quote>\s*,\s*(?<sig>[a-zA-Z0-9$]+)\(},
|
90
|
+
%r{\.sig\|\|(?<sig>[a-zA-Z0-9$]+)\(},
|
91
|
+
%r{yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
92
|
+
%r{\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
93
|
+
%r{\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
94
|
+
%r{\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
95
|
+
%r{\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?<sig>[a-zA-Z0-9$]+)\(}, # noqa: E501
|
96
|
+
%r{\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?<sig>[a-zA-Z0-9$]+)\(} # noqa: E501
|
97
97
|
]
|
98
98
|
|
99
99
|
function_patterns.each do |pattern|
|
@@ -104,14 +104,14 @@ module RubyTube
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
-
raise RegexMatchError.new(
|
107
|
+
raise RegexMatchError.new("get_initial_function_name", "multiple")
|
108
108
|
end
|
109
109
|
|
110
110
|
def get_transform_plan(js)
|
111
111
|
name = Regexp.escape(get_initial_function_name(js))
|
112
|
-
pattern = "#{name}=function\\(\\w\\)\\{[a-z
|
112
|
+
pattern = "#{name}=function\\(\\w\\)\\{[a-z=\\.(\"\\)]*;(.*);(?:.+)\\}"
|
113
113
|
|
114
|
-
Utils.regex_search(pattern, js, 1).split(
|
114
|
+
Utils.regex_search(pattern, js, 1).split(";")
|
115
115
|
end
|
116
116
|
|
117
117
|
def get_transform_object(js, var)
|
@@ -119,20 +119,20 @@ module RubyTube
|
|
119
119
|
pattern = "var #{escaped_var}={(.*?)};"
|
120
120
|
regex = Regexp.new(pattern, Regexp::MULTILINE)
|
121
121
|
transform_match = regex.match(js)
|
122
|
-
|
122
|
+
|
123
123
|
if transform_match.nil?
|
124
|
-
raise RegexMatchError.new(
|
124
|
+
raise RegexMatchError.new("get_transform_object", pattern)
|
125
125
|
end
|
126
|
-
|
127
|
-
transform_match[1].
|
126
|
+
|
127
|
+
transform_match[1].tr("\n", " ").split(", ")
|
128
128
|
end
|
129
129
|
|
130
130
|
def get_transform_map(js, var)
|
131
131
|
transform_obejct = get_transform_object(js, var)
|
132
132
|
mapper = {}
|
133
|
-
|
133
|
+
|
134
134
|
transform_obejct.each do |obj|
|
135
|
-
name, function = obj.split(
|
135
|
+
name, function = obj.split(":")
|
136
136
|
fn = map_functions(function)
|
137
137
|
mapper[name] = fn
|
138
138
|
end
|
@@ -144,12 +144,12 @@ module RubyTube
|
|
144
144
|
# Ruby equivalent of JavaScript's Array.reverse()
|
145
145
|
arr.reverse!
|
146
146
|
end
|
147
|
-
|
147
|
+
|
148
148
|
def splice(arr, index)
|
149
149
|
# Ruby equivalent of JavaScript's Array.splice(0, index)
|
150
150
|
arr.shift(index.to_i)
|
151
151
|
end
|
152
|
-
|
152
|
+
|
153
153
|
def swap(arr, index)
|
154
154
|
# Ruby equivalent of the JavaScript swapping function
|
155
155
|
temp = arr[0]
|
@@ -165,7 +165,7 @@ module RubyTube
|
|
165
165
|
def throttling_mod_func(d, e)
|
166
166
|
(e % d.length + d.length) % d.length
|
167
167
|
end
|
168
|
-
|
168
|
+
|
169
169
|
def throttling_unshift(d, e)
|
170
170
|
e = throttling_mod_func(d, e)
|
171
171
|
new_arr = d[-e..-1] + d[0...-e]
|
@@ -174,12 +174,12 @@ module RubyTube
|
|
174
174
|
end
|
175
175
|
|
176
176
|
def throttling_cipher_function(d, e)
|
177
|
-
h =
|
177
|
+
h = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".chars
|
178
178
|
f = 96
|
179
|
-
self_arr = e.
|
180
|
-
|
179
|
+
self_arr = e.chars
|
180
|
+
|
181
181
|
copied_array = d.clone
|
182
|
-
|
182
|
+
|
183
183
|
copied_array.each_with_index do |l, m|
|
184
184
|
bracket_val = (h.index(l) - h.index(self_arr[m]) + m - 32 + f) % h.length
|
185
185
|
self_arr << h[bracket_val]
|
@@ -204,26 +204,26 @@ module RubyTube
|
|
204
204
|
d.clear.concat(new_arr)
|
205
205
|
|
206
206
|
end_len = d.length
|
207
|
-
raise
|
207
|
+
raise "Length mismatch" unless start_len == end_len
|
208
208
|
end
|
209
209
|
|
210
|
-
def js_splice(arr, start, delete_count=nil, *items)
|
210
|
+
def js_splice(arr, start, delete_count = nil, *items)
|
211
211
|
if start.is_a? Integer
|
212
212
|
start = arr.length if start > arr.length
|
213
213
|
start += arr.length if start < 0
|
214
214
|
else
|
215
215
|
start = 0
|
216
216
|
end
|
217
|
-
|
217
|
+
|
218
218
|
delete_count = arr.length - start if delete_count.nil? || delete_count >= arr.length - start
|
219
219
|
deleted_elements = arr[start, delete_count]
|
220
|
-
|
220
|
+
|
221
221
|
new_arr = arr[0...start] + items + arr[(start + delete_count)..-1]
|
222
|
-
|
222
|
+
|
223
223
|
arr.clear.concat(new_arr)
|
224
|
-
|
224
|
+
|
225
225
|
deleted_elements
|
226
|
-
end
|
226
|
+
end
|
227
227
|
|
228
228
|
def map_functions(function)
|
229
229
|
mapper = [
|
@@ -232,22 +232,22 @@ module RubyTube
|
|
232
232
|
# function(a,b){a.splice(0,b)}
|
233
233
|
[%r"{\w\.splice\(0,\w\)}", method(:splice)],
|
234
234
|
# function(a,b){var c=a[0];a[0]=a[b%a.length];a[b]=c}
|
235
|
-
[%r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w
|
235
|
+
[%r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w%\w.length\];\w\[\w\]=\w}", method(:swap)],
|
236
236
|
# function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}
|
237
|
-
[%r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w
|
237
|
+
[%r"{var\s\w=\w\[0\];\w\[0\]=\w\[\w%\w.length\];\w\[\w%\w.length\]=\w}", method(:swap)]
|
238
238
|
]
|
239
239
|
|
240
240
|
mapper.each do |pattern, fn|
|
241
241
|
return fn if Regexp.new(pattern).match?(function)
|
242
242
|
end
|
243
|
-
|
244
|
-
raise RegexMatchError.new(
|
243
|
+
|
244
|
+
raise RegexMatchError.new("map_functions", "multiple")
|
245
245
|
end
|
246
246
|
|
247
247
|
def get_throttling_function_name(js)
|
248
248
|
function_patterns = [
|
249
|
-
%r
|
250
|
-
%r
|
249
|
+
%r{a\.[a-zA-Z]\s*&&\s*\([a-z]\s*=\s*a\.get\("n"\)\)\s*&&.*?\|\|\s*([a-z]+)},
|
250
|
+
%r{\([a-z]\s*=\s*([a-zA-Z0-9$]+)(\[\d+\])\([a-z]\)}
|
251
251
|
]
|
252
252
|
|
253
253
|
function_patterns.each do |pattern|
|
@@ -261,23 +261,23 @@ module RubyTube
|
|
261
261
|
|
262
262
|
idx = function_match[2]
|
263
263
|
if idx
|
264
|
-
idx = idx.tr(
|
264
|
+
idx = idx.tr("[]", "")
|
265
265
|
array_match = js.match(/var #{Regexp.escape(function_match[1])}\s*=\s*(\[.+?\])/)
|
266
266
|
if array_match
|
267
|
-
array = array_match[1].tr(
|
267
|
+
array = array_match[1].tr("[]", "").split(",")
|
268
268
|
array = array.map(&:strip)
|
269
269
|
return array[idx.to_i]
|
270
270
|
end
|
271
271
|
end
|
272
272
|
end
|
273
273
|
|
274
|
-
raise RegexMatchError.new(
|
274
|
+
raise RegexMatchError.new("get_throttling_function_name", "multiple")
|
275
275
|
end
|
276
276
|
|
277
277
|
def get_throttling_function_code(js)
|
278
278
|
name = Regexp.escape(get_throttling_function_name(js))
|
279
279
|
|
280
|
-
pattern_start = %r
|
280
|
+
pattern_start = %r{#{name}=function\(\w\)}
|
281
281
|
regex = Regexp.new(pattern_start)
|
282
282
|
match = js.match(regex)
|
283
283
|
|
@@ -289,12 +289,12 @@ module RubyTube
|
|
289
289
|
|
290
290
|
def get_throttling_function_array(js)
|
291
291
|
raw_code = get_throttling_function_code(js)
|
292
|
-
|
292
|
+
|
293
293
|
array_regex = /,c=\[/
|
294
294
|
match = raw_code.match(array_regex)
|
295
295
|
array_raw = Parser.find_object_from_startpoint(raw_code, match.end(0) - 1)
|
296
296
|
str_array = Parser.throttling_array_split(array_raw)
|
297
|
-
|
297
|
+
|
298
298
|
converted_array = []
|
299
299
|
str_array.each do |el|
|
300
300
|
begin
|
@@ -303,28 +303,28 @@ module RubyTube
|
|
303
303
|
rescue ArgumentError
|
304
304
|
# Not an integer value.
|
305
305
|
end
|
306
|
-
|
307
|
-
if el ==
|
306
|
+
|
307
|
+
if el == "null"
|
308
308
|
converted_array << nil
|
309
309
|
next
|
310
310
|
end
|
311
|
-
|
311
|
+
|
312
312
|
if el.start_with?('"') && el.end_with?('"')
|
313
313
|
converted_array << el[1..-2]
|
314
314
|
next
|
315
315
|
end
|
316
|
-
|
317
|
-
if el.start_with?(
|
316
|
+
|
317
|
+
if el.start_with?("function")
|
318
318
|
mapper = [
|
319
319
|
[%r"{for\(\w=\(\w%\w\.length\+\w\.length\)%\w\.length;\w--;\)\w\.unshift\(\w.pop\(\)\)}", method(:throttling_unshift)],
|
320
320
|
[%r"{\w\.reverse\(\)}", method(:reverse)],
|
321
321
|
[%r"{\w\.push\(\w\)}", method(:push)],
|
322
322
|
[%r";var\s\w=\w\[0\];\w\[0\]=\w\[\w\];\w\[\w\]=\w}", method(:swap)],
|
323
|
-
[%r
|
324
|
-
[%r
|
323
|
+
[%r{case\s\d+}, method(:throttling_cipher_function)],
|
324
|
+
[%r{\w\.splice\(0,1,\w\.splice\(\w,1,\w\[0\]\)\[0\]\)}, method(:throttling_nested_splice)],
|
325
325
|
[%r";\w\.splice\(\w,1\)}", method(:js_splice)],
|
326
326
|
[%r"\w\.splice\(-\w\)\.reverse\(\)\.forEach\(function\(\w\){\w\.unshift\(\w\)}\)", method(:throttling_prepend)],
|
327
|
-
[%r"for\(var \w=\w\.length;\w;\)\w\.push\(\w\.splice\(--\w,1\)\[0\]\)}", method(:reverse)]
|
327
|
+
[%r"for\(var \w=\w\.length;\w;\)\w\.push\(\w\.splice\(--\w,1\)\[0\]\)}", method(:reverse)]
|
328
328
|
]
|
329
329
|
|
330
330
|
found = false
|
@@ -336,10 +336,10 @@ module RubyTube
|
|
336
336
|
end
|
337
337
|
next if found
|
338
338
|
end
|
339
|
-
|
339
|
+
|
340
340
|
converted_array << el
|
341
341
|
end
|
342
|
-
|
342
|
+
|
343
343
|
converted_array.map! { |el| el.nil? ? converted_array : el }
|
344
344
|
converted_array
|
345
345
|
end
|
@@ -351,9 +351,8 @@ module RubyTube
|
|
351
351
|
plan_regex = Regexp.new(transform_start)
|
352
352
|
match = raw_code.match(plan_regex)
|
353
353
|
|
354
|
-
|
355
|
-
|
356
|
-
step_regex = %r"c\[(\d+)\]\(c\[(\d+)\](,c(\[(\d+)\]))?\)"
|
354
|
+
transform_plan_raw = Parser.find_object_from_startpoint(raw_code, match.end(0) - 1)
|
355
|
+
step_regex = %r{c\[(\d+)\]\(c\[(\d+)\](,c(\[(\d+)\]))?\)}
|
357
356
|
matches = transform_plan_raw.scan(step_regex)
|
358
357
|
transform_steps = []
|
359
358
|
|
data/lib/rubytube/client.rb
CHANGED
@@ -33,10 +33,10 @@ module RubyTube
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def streaming_data
|
36
|
-
return vid_info[
|
36
|
+
return vid_info["streamingData"] if vid_info && vid_info.key?("streamingData")
|
37
37
|
|
38
38
|
bypass_age_gate
|
39
|
-
vid_info[
|
39
|
+
vid_info["streamingData"]
|
40
40
|
end
|
41
41
|
|
42
42
|
def fmt_streams
|
@@ -69,24 +69,24 @@ module RubyTube
|
|
69
69
|
|
70
70
|
messages.each do |reason|
|
71
71
|
case status
|
72
|
-
when
|
72
|
+
when "UNPLAYABLE"
|
73
73
|
case reason
|
74
|
-
when
|
74
|
+
when "Join this channel to get access to members-only content like this video, and other exclusive perks."
|
75
75
|
raise MembersOnly.new(video_id)
|
76
|
-
when
|
76
|
+
when "This live stream recording is not available."
|
77
77
|
raise RecordingUnavailable.new(video_id)
|
78
78
|
else
|
79
79
|
raise VideoUnavailable.new(video_id)
|
80
80
|
end
|
81
|
-
when
|
82
|
-
if reason ==
|
81
|
+
when "LOGIN_REQUIRED"
|
82
|
+
if reason == "This is a private video. Please sign in to verify that you may see it."
|
83
83
|
raise VideoPrivate.new(video_id)
|
84
84
|
end
|
85
|
-
when
|
86
|
-
if reason ==
|
85
|
+
when "ERROR"
|
86
|
+
if reason == "Video unavailable"
|
87
87
|
raise VideoUnavailable.new(video_id)
|
88
88
|
end
|
89
|
-
when
|
89
|
+
when "LIVE_STREAM"
|
90
90
|
raise LiveStreamError.new(video_id)
|
91
91
|
end
|
92
92
|
end
|
@@ -100,9 +100,9 @@ module RubyTube
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def thumbnail_url
|
103
|
-
thumbs = vid_info.fetch(
|
103
|
+
thumbs = vid_info.fetch("videoDetails", {}).fetch("thumbnail", {}).fetch("thumbnails", [])
|
104
104
|
|
105
|
-
return thumbs[-1][
|
105
|
+
return thumbs[-1]["url"] if thumbs.size > 0
|
106
106
|
|
107
107
|
"https://img.youtube.com/vi/#{ideo_id}/maxresdefault.jpg"
|
108
108
|
end
|
@@ -117,11 +117,11 @@ module RubyTube
|
|
117
117
|
end
|
118
118
|
|
119
119
|
def bypass_age_gate
|
120
|
-
it = InnerTube.new(client:
|
120
|
+
it = InnerTube.new(client: "ANDROID_EMBED")
|
121
121
|
resp = it.player(video_id)
|
122
122
|
|
123
|
-
status = resp[
|
124
|
-
if status ==
|
123
|
+
status = resp["playabilityStatus"]["status"]
|
124
|
+
if status == "UNPLAYABLE"
|
125
125
|
raise VideoUnavailable.new(video_id)
|
126
126
|
end
|
127
127
|
|
@@ -131,42 +131,42 @@ module RubyTube
|
|
131
131
|
def title
|
132
132
|
return @title if @title
|
133
133
|
|
134
|
-
@title = vid_info[
|
134
|
+
@title = vid_info["videoDetails"]["title"]
|
135
135
|
@title
|
136
136
|
end
|
137
137
|
|
138
138
|
def length
|
139
139
|
return @length if @length
|
140
140
|
|
141
|
-
@length = vid_info[
|
141
|
+
@length = vid_info["videoDetails"]["lengthSeconds"].to_i
|
142
142
|
@length
|
143
143
|
end
|
144
144
|
|
145
145
|
def views
|
146
146
|
return @views if @views
|
147
147
|
|
148
|
-
@views = vid_info[
|
148
|
+
@views = vid_info["videoDetails"]["viewCount"].to_i
|
149
149
|
@views
|
150
150
|
end
|
151
151
|
|
152
152
|
def author
|
153
153
|
return @author if @author
|
154
154
|
|
155
|
-
@author = vid_info[
|
155
|
+
@author = vid_info["videoDetails"]["author"]
|
156
156
|
@author
|
157
157
|
end
|
158
158
|
|
159
159
|
def keywords
|
160
160
|
return @keywords if @keywords
|
161
161
|
|
162
|
-
@keywords = vid_info[
|
162
|
+
@keywords = vid_info["videoDetails"]["keywords"]
|
163
163
|
@keywords
|
164
164
|
end
|
165
165
|
|
166
166
|
def channel_id
|
167
167
|
return @channel_id if @channel_id
|
168
168
|
|
169
|
-
@channel_id = vid_info[
|
169
|
+
@channel_id = vid_info["videoDetails"]["channelId"]
|
170
170
|
@channel_id
|
171
171
|
end
|
172
172
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module RubyTube
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class HTMLParseError < StandardError; end
|
5
|
+
|
6
|
+
class ExtractError < StandardError; end
|
7
|
+
|
8
|
+
class MaxRetriesExceeded < StandardError; end
|
9
|
+
|
10
|
+
class VideoUnavailable < StandardError; end
|
11
|
+
|
12
|
+
class InvalidArgumentError < ArgumentError; end
|
13
|
+
|
14
|
+
class RegexMatchError < StandardError
|
15
|
+
def initialize(caller, pattern)
|
16
|
+
super("Regex match error in #{caller} for pattern #{pattern}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class MembersOnly < StandardError
|
21
|
+
def initialize(video_id)
|
22
|
+
super("Members only video: #{video_id}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class RecordingUnavailable < StandardError
|
27
|
+
def initialize(video_id)
|
28
|
+
super("Recording unavailable: #{video_id}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class VideoUnavailable < StandardError
|
33
|
+
def initialize(video_id)
|
34
|
+
super("Video unavailable: #{video_id}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class VideoPrivate < StandardError
|
39
|
+
def initialize(video_id)
|
40
|
+
super("Video is private: #{video_id}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class LiveStreamError < StandardError
|
45
|
+
def initialize(video_id)
|
46
|
+
super("Video is a live stream: #{video_id}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/rubytube/extractor.rb
CHANGED
@@ -4,32 +4,32 @@ module RubyTube
|
|
4
4
|
def playability_status(watch_html)
|
5
5
|
player_response = initial_player_response(watch_html)
|
6
6
|
player_response = JSON.parse(player_response)
|
7
|
-
status_obj = player_response[
|
8
|
-
|
9
|
-
if status_obj.has_key?(
|
10
|
-
return [
|
7
|
+
status_obj = player_response["playabilityStatus"] || {}
|
8
|
+
|
9
|
+
if status_obj.has_key?("liveStreamability")
|
10
|
+
return ["LIVE_STREAM", "Video is a live stream."]
|
11
11
|
end
|
12
|
-
|
13
|
-
if status_obj.has_key?(
|
14
|
-
if status_obj.has_key?(
|
15
|
-
return [status_obj[
|
12
|
+
|
13
|
+
if status_obj.has_key?("status")
|
14
|
+
if status_obj.has_key?("reason")
|
15
|
+
return [status_obj["status"], [status_obj["reason"]]]
|
16
16
|
end
|
17
|
-
|
18
|
-
if status_obj.has_key?(
|
19
|
-
return [status_obj[
|
17
|
+
|
18
|
+
if status_obj.has_key?("messages")
|
19
|
+
return [status_obj["status"], status_obj["messages"]]
|
20
20
|
end
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
[nil, [nil]]
|
24
24
|
end
|
25
25
|
|
26
26
|
def video_id(url)
|
27
|
-
|
27
|
+
Utils.regex_search(/(?:v=|\/)([0-9A-Za-z_-]{11}).*/, url, 1)
|
28
28
|
end
|
29
29
|
|
30
30
|
def js_url(html)
|
31
31
|
begin
|
32
|
-
base_js = get_ytplayer_config(html)[
|
32
|
+
base_js = get_ytplayer_config(html)["assets"]["js"]
|
33
33
|
rescue RegexMatchError, NoMethodError
|
34
34
|
base_js = get_ytplayer_js(html)
|
35
35
|
end
|
@@ -38,18 +38,18 @@ module RubyTube
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def mime_type_codec(mime_type_codec)
|
41
|
-
pattern = %r{(\w
|
41
|
+
pattern = %r{(\w+/\w+);\scodecs="([a-zA-Z\-0-9.,\s]*)"}
|
42
42
|
results = mime_type_codec.match(pattern)
|
43
|
-
|
43
|
+
|
44
44
|
raise RegexMatchError.new("mime_type_codec, pattern=#{pattern}") if results.nil?
|
45
|
-
|
45
|
+
|
46
46
|
mime_type, codecs = results.captures
|
47
47
|
[mime_type, codecs.split(",").map(&:strip)]
|
48
48
|
end
|
49
49
|
|
50
50
|
def get_ytplayer_js(html)
|
51
51
|
js_url_patterns = [
|
52
|
-
%r{(/s/player/[\w\d]+/[\w\d_/.]+/base\.js)}
|
52
|
+
%r{(/s/player/[\w\d]+/[\w\d_/.]+/base\.js)}
|
53
53
|
]
|
54
54
|
|
55
55
|
js_url_patterns.each do |pattern|
|
@@ -59,7 +59,7 @@ module RubyTube
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
-
raise RegexMatchError.new(
|
62
|
+
raise RegexMatchError.new("get_ytplayer_js", "js_url_patterns")
|
63
63
|
end
|
64
64
|
|
65
65
|
def get_ytplayer_config(html)
|
@@ -69,11 +69,9 @@ module RubyTube
|
|
69
69
|
]
|
70
70
|
|
71
71
|
config_patterns.each do |pattern|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
next
|
76
|
-
end
|
72
|
+
return Parser.parse_for_object(html, pattern)
|
73
|
+
rescue HTMLParseError => e
|
74
|
+
next
|
77
75
|
end
|
78
76
|
|
79
77
|
setconfig_patterns = [
|
@@ -81,14 +79,12 @@ module RubyTube
|
|
81
79
|
]
|
82
80
|
|
83
81
|
setconfig_patterns.each do |pattern|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
next
|
88
|
-
end
|
82
|
+
return Parser.parse_for_object(html, pattern)
|
83
|
+
rescue HTMLParseError => e
|
84
|
+
next
|
89
85
|
end
|
90
86
|
|
91
|
-
raise RegexMatchError.new(
|
87
|
+
raise RegexMatchError.new("get_ytplayer_config", "config_patterns, setconfig_patterns")
|
92
88
|
end
|
93
89
|
|
94
90
|
def apply_signature(stream_manifest, vid_info, js)
|
@@ -96,33 +92,33 @@ module RubyTube
|
|
96
92
|
|
97
93
|
stream_manifest.each_with_index do |stream, i|
|
98
94
|
begin
|
99
|
-
url = stream[
|
95
|
+
url = stream["url"]
|
100
96
|
rescue NoMethodError
|
101
|
-
live_stream = vid_info.fetch(
|
97
|
+
live_stream = vid_info.fetch("playabilityStatus", {})["liveStreamability"]
|
102
98
|
if live_stream
|
103
|
-
raise LiveStreamError.new(
|
99
|
+
raise LiveStreamError.new("UNKNOWN")
|
104
100
|
end
|
105
101
|
end
|
106
102
|
|
107
|
-
if url.include?("signature") ||
|
108
|
-
|
103
|
+
if url.include?("signature") ||
|
104
|
+
(!stream.key?("s") && (url.include?("&sig=") || url.include?("&lsig=")))
|
109
105
|
# For certain videos, YouTube will just provide them pre-signed, in
|
110
106
|
# which case there's no real magic to download them and we can skip
|
111
107
|
# the whole signature descrambling entirely.
|
112
108
|
next
|
113
109
|
end
|
114
110
|
|
115
|
-
signature = cipher.get_signature(stream[
|
111
|
+
signature = cipher.get_signature(stream["s"])
|
116
112
|
|
117
113
|
parsed_url = URI.parse(url)
|
118
114
|
|
119
115
|
query_params = CGI.parse(parsed_url.query)
|
120
116
|
query_params.transform_values!(&:first)
|
121
|
-
query_params[
|
122
|
-
unless query_params.key?(
|
123
|
-
initial_n = query_params[
|
117
|
+
query_params["sig"] = signature
|
118
|
+
unless query_params.key?("ratebypass")
|
119
|
+
initial_n = query_params["n"].chars
|
124
120
|
new_n = cipher.calculate_n(initial_n)
|
125
|
-
query_params[
|
121
|
+
query_params["n"] = new_n
|
126
122
|
end
|
127
123
|
|
128
124
|
url = "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.path}?#{URI.encode_www_form(query_params)}"
|
@@ -132,23 +128,23 @@ module RubyTube
|
|
132
128
|
end
|
133
129
|
|
134
130
|
def apply_descrambler(stream_data)
|
135
|
-
return if stream_data.has_key?(
|
131
|
+
return if stream_data.has_key?("url")
|
136
132
|
|
137
133
|
# Merge formats and adaptiveFormats into a single array
|
138
134
|
formats = []
|
139
|
-
formats += stream_data[
|
140
|
-
formats += stream_data[
|
135
|
+
formats += stream_data["formats"] if stream_data.has_key?("formats")
|
136
|
+
formats += stream_data["adaptiveFormats"] if stream_data.has_key?("adaptiveFormats")
|
141
137
|
|
142
138
|
# Extract url and s from signatureCiphers as necessary
|
143
139
|
formats.each do |data|
|
144
|
-
unless data.has_key?(
|
145
|
-
if data.has_key?(
|
146
|
-
cipher_url = URI.decode_www_form(data[
|
147
|
-
data[
|
148
|
-
data[
|
140
|
+
unless data.has_key?("url")
|
141
|
+
if data.has_key?("signatureCipher")
|
142
|
+
cipher_url = URI.decode_www_form(data["signatureCipher"]).to_h
|
143
|
+
data["url"] = cipher_url["url"]
|
144
|
+
data["s"] = cipher_url["s"]
|
149
145
|
end
|
150
146
|
end
|
151
|
-
data[
|
147
|
+
data["is_otf"] = data["type"] == "FORMAT_STREAM_TYPE_OTF"
|
152
148
|
end
|
153
149
|
|
154
150
|
formats
|
@@ -161,16 +157,14 @@ module RubyTube
|
|
161
157
|
"window\\[['\"]ytInitialPlayerResponse['\"]\\]\\s*=\\s*",
|
162
158
|
"ytInitialPlayerResponse\\s*=\\s*"
|
163
159
|
]
|
164
|
-
|
160
|
+
|
165
161
|
patterns.each do |pattern|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
next
|
170
|
-
end
|
162
|
+
return Parser.parse_for_object(watch_html, pattern)
|
163
|
+
rescue HTMLParseError
|
164
|
+
next
|
171
165
|
end
|
172
|
-
|
173
|
-
raise RegexMatchError.new(
|
166
|
+
|
167
|
+
raise RegexMatchError.new("initial_player_response", "initial_player_response_pattern")
|
174
168
|
end
|
175
169
|
end
|
176
170
|
end
|
data/lib/rubytube/innertube.rb
CHANGED
@@ -1,55 +1,55 @@
|
|
1
1
|
module RubyTube
|
2
2
|
class InnerTube
|
3
3
|
DEFALUT_CLIENTS = {
|
4
|
-
|
4
|
+
"WEB" => {
|
5
5
|
context: {
|
6
6
|
client: {
|
7
|
-
clientName:
|
8
|
-
clientVersion:
|
7
|
+
clientName: "WEB",
|
8
|
+
clientVersion: "2.20200720.00.02"
|
9
9
|
}
|
10
10
|
},
|
11
|
-
header: {
|
12
|
-
api_key:
|
11
|
+
header: {"User-Agent": "Mozilla/5.0"},
|
12
|
+
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
13
13
|
},
|
14
|
-
|
14
|
+
"ANDROID_MUSIC" => {
|
15
15
|
context: {
|
16
16
|
client: {
|
17
|
-
clientName:
|
18
|
-
clientVersion:
|
19
|
-
androidSdkVersion: 30
|
20
|
-
}
|
17
|
+
clientName: "ANDROID_MUSIC",
|
18
|
+
clientVersion: "5.16.51",
|
19
|
+
androidSdkVersion: 30
|
20
|
+
}
|
21
21
|
},
|
22
|
-
header: {
|
23
|
-
api_key:
|
22
|
+
header: {"User-Agent": "com.google.android.apps.youtube.music/"},
|
23
|
+
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
24
24
|
},
|
25
|
-
|
25
|
+
"ANDROID_EMBED" => {
|
26
26
|
context: {
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
client: {
|
28
|
+
clientName: "ANDROID_EMBEDDED_PLAYER",
|
29
|
+
clientVersion: "17.31.35",
|
30
|
+
clientScreen: "EMBED",
|
31
|
+
androidSdkVersion: 30
|
32
|
+
}
|
33
33
|
},
|
34
|
-
header: {
|
35
|
-
api_key:
|
36
|
-
}
|
34
|
+
header: {"User-Agent": "com.google.android.youtube/"},
|
35
|
+
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
36
|
+
}
|
37
37
|
}
|
38
38
|
|
39
|
-
BASE_URL =
|
39
|
+
BASE_URL = "https://www.youtube.com/youtubei/v1"
|
40
40
|
|
41
41
|
attr_accessor :context, :header, :api_key, :access_token, :refresh_token, :use_oauth, :allow_cache, :expires
|
42
42
|
|
43
|
-
def initialize(client:
|
43
|
+
def initialize(client: "WEB", use_oauth: false, allow_cache: false)
|
44
44
|
self.context = DEFALUT_CLIENTS[client][:context]
|
45
|
-
self.header
|
45
|
+
self.header = DEFALUT_CLIENTS[client][:header]
|
46
46
|
self.api_key = DEFALUT_CLIENTS[client][:api_key]
|
47
47
|
self.use_oauth = use_oauth
|
48
48
|
self.allow_cache = allow_cache
|
49
49
|
end
|
50
50
|
|
51
51
|
def cache_tokens
|
52
|
-
|
52
|
+
nil unless allow_cache
|
53
53
|
|
54
54
|
# TODO:
|
55
55
|
end
|
@@ -68,16 +68,16 @@ module RubyTube
|
|
68
68
|
end
|
69
69
|
|
70
70
|
headers = {
|
71
|
-
|
71
|
+
"Content-Type": "application/json"
|
72
72
|
}
|
73
73
|
|
74
74
|
if use_oauth
|
75
75
|
if access_token
|
76
76
|
refresh_bearer_token
|
77
|
-
headers[
|
77
|
+
headers["Authorization"] = "Bearer #{access_token}"
|
78
78
|
else
|
79
79
|
fetch_bearer_token
|
80
|
-
headers[
|
80
|
+
headers["Authorization"] = "Bearer #{access_token}"
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
@@ -87,7 +87,7 @@ module RubyTube
|
|
87
87
|
options[:query] = {
|
88
88
|
key: api_key,
|
89
89
|
contentCheckOk: true,
|
90
|
-
racyCheckOk: true
|
90
|
+
racyCheckOk: true
|
91
91
|
}.merge(query)
|
92
92
|
options[:data] = data
|
93
93
|
|
@@ -97,7 +97,7 @@ module RubyTube
|
|
97
97
|
|
98
98
|
def player(video_id)
|
99
99
|
endpoint = "#{BASE_URL}/player"
|
100
|
-
query = {
|
100
|
+
query = {"videoId" => video_id}
|
101
101
|
|
102
102
|
send(endpoint, query, {context: context})
|
103
103
|
end
|
data/lib/rubytube/parser.rb
CHANGED
@@ -11,31 +11,31 @@ module RubyTube
|
|
11
11
|
end
|
12
12
|
start_index = result.end(0)
|
13
13
|
|
14
|
-
|
14
|
+
parse_for_object_from_startpoint(html, start_index)
|
15
15
|
end
|
16
16
|
|
17
17
|
def find_object_from_startpoint(html, start_point)
|
18
18
|
html = html[start_point..-1]
|
19
|
-
unless [
|
19
|
+
unless ["{", "["].include?(html[0])
|
20
20
|
raise HTMLParseError, "Invalid start point. Start of HTML:\n#{html[0..19]}"
|
21
21
|
end
|
22
22
|
|
23
|
-
last_char =
|
23
|
+
last_char = "{"
|
24
24
|
curr_char = nil
|
25
25
|
stack = [html[0]]
|
26
26
|
i = 1
|
27
27
|
|
28
28
|
context_closers = {
|
29
|
-
|
30
|
-
|
29
|
+
"{" => "}",
|
30
|
+
"[" => "]",
|
31
31
|
'"' => '"',
|
32
|
-
|
32
|
+
"/" => "/"
|
33
33
|
}
|
34
34
|
|
35
35
|
while i < html.length
|
36
36
|
break if stack.empty?
|
37
37
|
|
38
|
-
last_char = curr_char unless [
|
38
|
+
last_char = curr_char unless [" ", '\n'].include?(curr_char)
|
39
39
|
curr_char = html[i]
|
40
40
|
curr_context = stack.last
|
41
41
|
|
@@ -45,54 +45,52 @@ module RubyTube
|
|
45
45
|
next
|
46
46
|
end
|
47
47
|
|
48
|
-
if ['"',
|
49
|
-
if curr_char ==
|
48
|
+
if ['"', "/"].include?(curr_context)
|
49
|
+
if curr_char == "\\"
|
50
50
|
i += 2
|
51
51
|
next
|
52
52
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
stack.push(curr_char)
|
57
|
-
end
|
53
|
+
elsif context_closers.keys.include?(curr_char)
|
54
|
+
unless curr_char == "/" && !["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";"].include?(last_char)
|
55
|
+
stack.push(curr_char)
|
58
56
|
end
|
59
57
|
end
|
60
58
|
|
61
59
|
i += 1
|
62
60
|
end
|
63
61
|
|
64
|
-
|
65
|
-
full_obj
|
62
|
+
html[0...i]
|
66
63
|
end
|
67
64
|
|
68
65
|
def parse_for_object_from_startpoint(html, start_point)
|
69
66
|
html = html[start_point..-1]
|
70
67
|
|
71
|
-
unless [
|
68
|
+
unless ["{", "["].include?(html[0])
|
72
69
|
raise HTMLParseError, "Invalid start point. Start of HTML:\n#{html[0..19]}"
|
73
70
|
end
|
74
71
|
|
75
72
|
# First letter MUST be an open brace, so we put that in the stack,
|
76
73
|
# and skip the first character.
|
77
|
-
last_char =
|
74
|
+
last_char = "{"
|
78
75
|
curr_char = nil
|
79
76
|
stack = [html[0]]
|
80
77
|
i = 1
|
81
78
|
|
82
79
|
context_closers = {
|
83
|
-
|
84
|
-
|
80
|
+
"{" => "}",
|
81
|
+
"[" => "]",
|
85
82
|
'"' => '"',
|
86
|
-
'
|
83
|
+
:"'" => "'",
|
84
|
+
"/" => "/" # JavaScript regex
|
87
85
|
}
|
88
86
|
|
89
87
|
while i < html.length
|
90
88
|
break if stack.empty?
|
91
|
-
|
92
|
-
last_char = curr_char unless [
|
89
|
+
|
90
|
+
last_char = curr_char unless [" ", '\n'].include?(curr_char)
|
93
91
|
curr_char = html[i]
|
94
92
|
curr_context = stack.last
|
95
|
-
|
93
|
+
|
96
94
|
# If we've reached a context closer, we can remove an element off the stack
|
97
95
|
if curr_char == context_closers[curr_context]
|
98
96
|
stack.pop
|
@@ -101,42 +99,39 @@ module RubyTube
|
|
101
99
|
end
|
102
100
|
# Strings and regex expressions require special context handling because they can contain
|
103
101
|
# context openers *and* closers
|
104
|
-
if ['"',
|
102
|
+
if ['"', "/"].include?(curr_context)
|
105
103
|
# If there's a backslash in a string or regex expression, we skip a character
|
106
|
-
if curr_char ==
|
104
|
+
if curr_char == "\\"
|
107
105
|
i += 2
|
108
106
|
next
|
109
107
|
end
|
110
|
-
|
108
|
+
elsif context_closers.keys.include?(curr_char)
|
111
109
|
# Non-string contexts are when we need to look for context openers.
|
112
|
-
|
113
|
-
|
114
|
-
unless curr_char == '/' && ['(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';'].include?(last_char)
|
115
|
-
stack << curr_char
|
116
|
-
end
|
110
|
+
unless curr_char == "/" && ["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";"].include?(last_char)
|
111
|
+
stack << curr_char
|
117
112
|
end
|
113
|
+
# Slash starts a regular expression depending on context
|
118
114
|
end
|
119
115
|
|
120
116
|
i += 1
|
121
117
|
end
|
122
118
|
|
123
|
-
|
124
|
-
full_obj
|
119
|
+
html[0..(i - 1)]
|
125
120
|
end
|
126
121
|
|
127
122
|
def throttling_array_split(js_array)
|
128
123
|
results = []
|
129
124
|
curr_substring = js_array[1..-1]
|
130
|
-
|
125
|
+
|
131
126
|
comma_regex = /,/
|
132
127
|
func_regex = /function\([^)]*\)/
|
133
|
-
|
128
|
+
|
134
129
|
until curr_substring.empty?
|
135
|
-
if curr_substring.start_with?(
|
130
|
+
if curr_substring.start_with?("function")
|
136
131
|
match = func_regex.match(curr_substring)
|
137
132
|
match_start = match.begin(0)
|
138
133
|
match_end = match.end(0)
|
139
|
-
|
134
|
+
|
140
135
|
function_text = find_object_from_startpoint(curr_substring, match_end)
|
141
136
|
full_function_def = curr_substring[0, match_end + function_text.length]
|
142
137
|
results << full_function_def
|
@@ -151,13 +146,13 @@ module RubyTube
|
|
151
146
|
match_start = curr_substring.length - 1
|
152
147
|
match_end = match_start + 1
|
153
148
|
end
|
154
|
-
|
149
|
+
|
155
150
|
curr_el = curr_substring[0, match_start]
|
156
151
|
results << curr_el
|
157
152
|
curr_substring = curr_substring[match_end..-1]
|
158
153
|
end
|
159
154
|
end
|
160
|
-
|
155
|
+
|
161
156
|
results
|
162
157
|
end
|
163
158
|
end
|
data/lib/rubytube/request.rb
CHANGED
@@ -24,7 +24,7 @@ module RubyTube
|
|
24
24
|
stop_pos = [downloaded + DEFAULT_RANGE_SIZE, file_size].min - 1
|
25
25
|
range_header = "bytes=#{downloaded}-#{stop_pos}"
|
26
26
|
tries = 0
|
27
|
-
|
27
|
+
|
28
28
|
while true
|
29
29
|
begin
|
30
30
|
if tries >= 1 + max_retries
|
@@ -56,20 +56,18 @@ module RubyTube
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def send(method, url, options = {})
|
59
|
-
headers = {
|
59
|
+
headers = {"Content-Type": "text/html"}
|
60
60
|
options[:headers] && headers.merge!(options[:headers])
|
61
61
|
|
62
62
|
connection = Faraday.new(url: url) do |faraday|
|
63
63
|
faraday.response :follow_redirects
|
64
64
|
faraday.adapter Faraday.default_adapter
|
65
65
|
end
|
66
|
-
|
66
|
+
connection.send(method) do |req|
|
67
67
|
req.headers = headers
|
68
|
-
options[:query] && req.params = options[:query]
|
68
|
+
options[:query] && req.params = options[:query]
|
69
69
|
options[:data] && req.body = JSON.dump(options[:data])
|
70
70
|
end
|
71
|
-
|
72
|
-
response
|
73
71
|
end
|
74
72
|
end
|
75
73
|
end
|
data/lib/rubytube/stream.rb
CHANGED
@@ -16,16 +16,16 @@ module RubyTube
|
|
16
16
|
def initialize(stream, monostate)
|
17
17
|
self.monostate = monostate
|
18
18
|
|
19
|
-
self.url = stream[
|
20
|
-
self.itag = stream[
|
19
|
+
self.url = stream["url"]
|
20
|
+
self.itag = stream["itag"].to_i
|
21
21
|
|
22
|
-
self.mime_type, self.codecs = Extractor.mime_type_codec(stream[
|
23
|
-
self.type, self.subtype = mime_type.split(
|
22
|
+
self.mime_type, self.codecs = Extractor.mime_type_codec(stream["mimeType"])
|
23
|
+
self.type, self.subtype = mime_type.split("/")
|
24
24
|
|
25
|
-
self.is_otf = stream[
|
26
|
-
self.bitrate = stream[
|
25
|
+
self.is_otf = stream["is_otf"]
|
26
|
+
self.bitrate = stream["bitrate"]
|
27
27
|
|
28
|
-
self.file_size = stream.fetch(
|
28
|
+
self.file_size = stream.fetch("contentLength", 0).to_i
|
29
29
|
end
|
30
30
|
|
31
31
|
def download(filename: nil, output_dir: nil)
|
@@ -35,26 +35,24 @@ module RubyTube
|
|
35
35
|
|
36
36
|
bytes_remaining = file_size
|
37
37
|
|
38
|
-
File.open(file_path,
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
f.write(chunk)
|
43
|
-
end
|
44
|
-
rescue HTTPError => e
|
45
|
-
raise e if e.code != 404
|
38
|
+
File.open(file_path, "wb") do |f|
|
39
|
+
Request.stream(url) do |chunk|
|
40
|
+
bytes_remaining -= chunk.bytesize
|
41
|
+
f.write(chunk)
|
46
42
|
end
|
43
|
+
rescue HTTPError => e
|
44
|
+
raise e if e.code != 404
|
47
45
|
end
|
48
46
|
|
49
47
|
file_path
|
50
48
|
end
|
51
49
|
|
52
50
|
def is_audio?
|
53
|
-
type ==
|
51
|
+
type == "audio"
|
54
52
|
end
|
55
53
|
|
56
54
|
def is_video?
|
57
|
-
type ==
|
55
|
+
type == "video"
|
58
56
|
end
|
59
57
|
|
60
58
|
def is_adaptive?
|
@@ -75,8 +73,8 @@ module RubyTube
|
|
75
73
|
|
76
74
|
private
|
77
75
|
|
78
|
-
def get_file_path(filename, output_dir, prefix =
|
79
|
-
filename
|
76
|
+
def get_file_path(filename, output_dir, prefix = "")
|
77
|
+
filename ||= default_filename
|
80
78
|
|
81
79
|
if prefix
|
82
80
|
filename = "#{prefix}#{filename}"
|
@@ -34,7 +34,7 @@ module RubyTube
|
|
34
34
|
132 => ["240p", "48kbps"],
|
35
35
|
151 => ["720p", "24kbps"],
|
36
36
|
300 => ["720p", "128kbps"],
|
37
|
-
301 => ["1080p", "128kbps"]
|
37
|
+
301 => ["1080p", "128kbps"]
|
38
38
|
}
|
39
39
|
|
40
40
|
DASH_VIDEO = {
|
@@ -112,7 +112,7 @@ module RubyTube
|
|
112
112
|
256 => [nil, "192kbps"],
|
113
113
|
258 => [nil, "384kbps"],
|
114
114
|
325 => [nil, nil],
|
115
|
-
328 => [nil, nil]
|
115
|
+
328 => [nil, nil]
|
116
116
|
}
|
117
117
|
|
118
118
|
ITAGS = {
|
@@ -33,6 +33,10 @@ module RubyTube
|
|
33
33
|
streams.find { |s| s.resolution == resolution }
|
34
34
|
end
|
35
35
|
|
36
|
+
def get_highest_resolution
|
37
|
+
order(resolution: :desc).first
|
38
|
+
end
|
39
|
+
|
36
40
|
def order(arg)
|
37
41
|
case arg
|
38
42
|
when Symbol
|
@@ -40,14 +44,14 @@ module RubyTube
|
|
40
44
|
dir = :asc
|
41
45
|
when Hash
|
42
46
|
field = arg.keys.first
|
43
|
-
dir = arg[field] == :desc ? :desc : :asc
|
47
|
+
dir = (arg[field] == :desc) ? :desc : :asc
|
44
48
|
end
|
45
49
|
|
46
50
|
allowed_fields = [:file_size, :itag, :resolution]
|
47
51
|
raise InvalidArgumentError unless allowed_fields.include? field
|
48
52
|
|
49
53
|
r = streams
|
50
|
-
r.sort! {|a, b| a.send(field).to_i <=> b.send(field).to_i }
|
54
|
+
r.sort! { |a, b| a.send(field).to_i <=> b.send(field).to_i }
|
51
55
|
|
52
56
|
r.reverse! if dir == :desc
|
53
57
|
|
data/lib/rubytube/version.rb
CHANGED
data/lib/rubytube.rb
CHANGED
@@ -1,66 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "faraday"
|
4
|
+
require "faraday/follow_redirects"
|
5
5
|
|
6
|
-
require_relative
|
6
|
+
require_relative "rubytube/version"
|
7
7
|
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require_relative
|
12
|
-
require_relative
|
13
|
-
require_relative
|
14
|
-
require_relative
|
15
|
-
require_relative
|
16
|
-
require_relative
|
17
|
-
require_relative
|
18
|
-
require_relative
|
8
|
+
require_relative "rubytube/cipher"
|
9
|
+
require_relative "rubytube/client"
|
10
|
+
require_relative "rubytube/error"
|
11
|
+
require_relative "rubytube/extractor"
|
12
|
+
require_relative "rubytube/innertube"
|
13
|
+
require_relative "rubytube/monostate"
|
14
|
+
require_relative "rubytube/parser"
|
15
|
+
require_relative "rubytube/request"
|
16
|
+
require_relative "rubytube/stream_format"
|
17
|
+
require_relative "rubytube/stream_query"
|
18
|
+
require_relative "rubytube/stream"
|
19
|
+
require_relative "rubytube/utils"
|
19
20
|
|
20
21
|
module RubyTube
|
21
|
-
class Error < StandardError; end
|
22
|
-
class HTMLParseError < StandardError; end
|
23
|
-
class ExtractError < StandardError; end
|
24
|
-
class MaxRetriesExceeded < StandardError; end
|
25
|
-
class VideoUnavailable < StandardError; end
|
26
|
-
class InvalidArgumentError < ArgumentError; end
|
27
|
-
|
28
|
-
class RegexMatchError < StandardError
|
29
|
-
def initialize(caller, pattern)
|
30
|
-
super("Regex match error in #{caller} for pattern #{pattern}")
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class MembersOnly < StandardError
|
35
|
-
def initialize(video_id)
|
36
|
-
super("Members only video: #{video_id}")
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class RecordingUnavailable < StandardError
|
41
|
-
def initialize(video_id)
|
42
|
-
super("Recording unavailable: #{video_id}")
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class VideoUnavailable < StandardError
|
47
|
-
def initialize(video_id)
|
48
|
-
super("Video unavailable: #{video_id}")
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class VideoPrivate < StandardError
|
53
|
-
def initialize(video_id)
|
54
|
-
super("Video is private: #{video_id}")
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
class LiveStreamError < StandardError
|
59
|
-
def initialize(video_id)
|
60
|
-
super("Video is a live stream: #{video_id}")
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
22
|
class << self
|
65
23
|
def new(url)
|
66
24
|
Client.new(url)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubytube
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- nightswinger
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -56,6 +56,7 @@ files:
|
|
56
56
|
- lib/rubytube.rb
|
57
57
|
- lib/rubytube/cipher.rb
|
58
58
|
- lib/rubytube/client.rb
|
59
|
+
- lib/rubytube/error.rb
|
59
60
|
- lib/rubytube/extractor.rb
|
60
61
|
- lib/rubytube/innertube.rb
|
61
62
|
- lib/rubytube/monostate.rb
|