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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81f3bc458444b3f15a63612fa193c5fbeb0fba3f2ca73a8b3936453fcf269d58
4
- data.tar.gz: ba175f34390fd95271cacc283bec75370b543b1a0edebad81e67a4188ca94790
3
+ metadata.gz: 57d1f41767aaf6489fa9c4ed47e82f122da84f3bbf3d7f69821f86d7d15d4d04
4
+ data.tar.gz: 8c453e409b21603e87a701acba2725f56b570078d89a9e73d6a643568c6580df
5
5
  SHA512:
6
- metadata.gz: 9652bd73252dee11c51d1fd322813660c447e1e9c0b6a863be9e4f277309927ff8eb4d2910a991c055c8b11db833986129e5c74988653a4b50441f2e546a5804
7
- data.tar.gz: 49593099d2603a922a9db02f8147e3b69d1674af663174e4d8bb6f8e7839b7b388036072f402d053699b2aee43b5b0bb6dd1789f248f10905f5aa30615b4f1f1
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
@@ -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"^\$*\w+\W"
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"\w+\.(\w+)\(\w,(\d+)\)",
21
- %r"\w+\[(\"\w+\")\]\(\w,(\d+)\)"
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 == 'b' ? initial_n : 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('calculate_n', 'curr_func')
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.split('')
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('parse_function', 'js_func_patterns')
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"\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
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'(?<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
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('get_initial_function_name', 'multiple')
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('get_transform_object', pattern)
124
+ raise RegexMatchError.new("get_transform_object", pattern)
125
125
  end
126
-
127
- transform_match[1].gsub("\n", " ").split(", ")
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 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('')
177
+ h = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".chars
178
178
  f = 96
179
- self_arr = e.split('')
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 'Length mismatch' unless start_len == end_len
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\%\w.length\];\w\[\w\]=\w}", method(:swap)],
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\%\w.length\];\w\[\w\%\w.length\]=\w}", method(:swap)]
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('map_functions', 'multiple')
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'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]\)',
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('[]', '').split(',')
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('get_throttling_function_name', 'multiple')
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"#{name}=function\(\w\)"
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 == 'null'
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?('function')
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"case\s\d+", method(:throttling_cipher_function)],
324
- [%r"\w\.splice\(0,1,\w\.splice\(\w,1,\w\[0\]\)\[0\]\)", method(:throttling_nested_splice)],
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
- # transform_plan_raw = Parser.find_object_from_startpoint(raw_code, match.end(0) - 1)
355
- transform_plan_raw = js
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
 
@@ -33,10 +33,10 @@ module RubyTube
33
33
  end
34
34
 
35
35
  def streaming_data
36
- return vid_info['streamingData'] if vid_info && vid_info.key?('streamingData')
36
+ return vid_info["streamingData"] if vid_info && vid_info.key?("streamingData")
37
37
 
38
38
  bypass_age_gate
39
- vid_info['streamingData']
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 'UNPLAYABLE'
72
+ when "UNPLAYABLE"
73
73
  case reason
74
- when 'Join this channel to get access to members-only content like this video, and other exclusive perks.'
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 'This live stream recording is not available.'
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 'LOGIN_REQUIRED'
82
- if reason == 'This is a private video. Please sign in to verify that you may see it.'
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 'ERROR'
86
- if reason == 'Video unavailable'
85
+ when "ERROR"
86
+ if reason == "Video unavailable"
87
87
  raise VideoUnavailable.new(video_id)
88
88
  end
89
- when 'LIVE_STREAM'
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('videoDetails', {}).fetch('thumbnail', {}).fetch('thumbnails', [])
103
+ thumbs = vid_info.fetch("videoDetails", {}).fetch("thumbnail", {}).fetch("thumbnails", [])
104
104
 
105
- return thumbs[-1]['url'] if thumbs.size > 0
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: 'ANDROID_EMBED')
120
+ it = InnerTube.new(client: "ANDROID_EMBED")
121
121
  resp = it.player(video_id)
122
122
 
123
- status = resp['playabilityStatus']['status']
124
- if status == 'UNPLAYABLE'
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['videoDetails']['title']
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['videoDetails']['lengthSeconds'].to_i
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['videoDetails']['viewCount'].to_i
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['videoDetails']['author']
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['videoDetails']['keywords']
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['videoDetails']['channelId']
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
@@ -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['playabilityStatus'] || {}
8
-
9
- if status_obj.has_key?('liveStreamability')
10
- return ['LIVE_STREAM', 'Video is a live stream.']
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?('status')
14
- if status_obj.has_key?('reason')
15
- return [status_obj['status'], [status_obj['reason']]]
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?('messages')
19
- return [status_obj['status'], status_obj['messages']]
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
- return Utils.regex_search(/(?:v=|\/)([0-9A-Za-z_-]{11}).*/, url, 1)
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)['assets']['js']
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+\/\w+)\;\scodecs=\"([a-zA-Z\-0-9.,\s]*)\"}
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('get_ytplayer_js', 'js_url_patterns')
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
- begin
73
- return Parser.parse_for_object(html, pattern)
74
- rescue HTMLParseError => e
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
- begin
85
- return Parser.parse_for_object(html, pattern)
86
- rescue HTMLParseError => e
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('get_ytplayer_config', 'config_patterns, setconfig_patterns')
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['url']
95
+ url = stream["url"]
100
96
  rescue NoMethodError
101
- live_stream = vid_info.fetch('playabilityStatus', {})['liveStreamability']
97
+ live_stream = vid_info.fetch("playabilityStatus", {})["liveStreamability"]
102
98
  if live_stream
103
- raise LiveStreamError.new('UNKNOWN')
99
+ raise LiveStreamError.new("UNKNOWN")
104
100
  end
105
101
  end
106
102
 
107
- if url.include?("signature") ||
108
- (!stream.key?("s") && (url.include?("&sig=") || url.include?("&lsig=")))
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['s'])
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['sig'] = signature
122
- unless query_params.key?('ratebypass')
123
- initial_n = query_params['n'].split('')
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['n'] = new_n
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?('url')
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['formats'] if stream_data.has_key?('formats')
140
- formats += stream_data['adaptiveFormats'] if stream_data.has_key?('adaptiveFormats')
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?('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']
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['is_otf'] = data['type'] == 'FORMAT_STREAM_TYPE_OTF'
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
- begin
167
- return Parser.parse_for_object(watch_html, pattern)
168
- rescue HTMLParseError
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('initial_player_response', 'initial_player_response_pattern')
166
+
167
+ raise RegexMatchError.new("initial_player_response", "initial_player_response_pattern")
174
168
  end
175
169
  end
176
170
  end
@@ -1,55 +1,55 @@
1
1
  module RubyTube
2
2
  class InnerTube
3
3
  DEFALUT_CLIENTS = {
4
- 'WEB' => {
4
+ "WEB" => {
5
5
  context: {
6
6
  client: {
7
- clientName: 'WEB',
8
- clientVersion: '2.20200720.00.02'
7
+ clientName: "WEB",
8
+ clientVersion: "2.20200720.00.02"
9
9
  }
10
10
  },
11
- header: { 'User-Agent': 'Mozilla/5.0' },
12
- api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
11
+ header: {"User-Agent": "Mozilla/5.0"},
12
+ api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
13
13
  },
14
- 'ANDROID_MUSIC' => {
14
+ "ANDROID_MUSIC" => {
15
15
  context: {
16
16
  client: {
17
- clientName: 'ANDROID_MUSIC',
18
- clientVersion: '5.16.51',
19
- androidSdkVersion: 30,
20
- },
17
+ clientName: "ANDROID_MUSIC",
18
+ clientVersion: "5.16.51",
19
+ androidSdkVersion: 30
20
+ }
21
21
  },
22
- header: { 'User-Agent': 'com.google.android.apps.youtube.music/'},
23
- api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
22
+ header: {"User-Agent": "com.google.android.apps.youtube.music/"},
23
+ api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
24
24
  },
25
- 'ANDROID_EMBED' => {
25
+ "ANDROID_EMBED" => {
26
26
  context: {
27
- client: {
28
- clientName: 'ANDROID_EMBEDDED_PLAYER',
29
- clientVersion: '17.31.35',
30
- clientScreen: 'EMBED',
31
- androidSdkVersion: 30,
32
- }
27
+ client: {
28
+ clientName: "ANDROID_EMBEDDED_PLAYER",
29
+ clientVersion: "17.31.35",
30
+ clientScreen: "EMBED",
31
+ androidSdkVersion: 30
32
+ }
33
33
  },
34
- header: { 'User-Agent': 'com.google.android.youtube/' },
35
- api_key: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
36
- },
34
+ header: {"User-Agent": "com.google.android.youtube/"},
35
+ api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
36
+ }
37
37
  }
38
38
 
39
- BASE_URL = 'https://www.youtube.com/youtubei/v1'
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: 'ANDROID_MUSIC', use_oauth: false, allow_cache: false)
43
+ def initialize(client: "WEB", use_oauth: false, allow_cache: false)
44
44
  self.context = DEFALUT_CLIENTS[client][:context]
45
- self.header = DEFALUT_CLIENTS[client][: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
- return unless allow_cache
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
- 'Content-Type': 'application/json',
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['Authorization'] = "Bearer #{access_token}"
77
+ headers["Authorization"] = "Bearer #{access_token}"
78
78
  else
79
79
  fetch_bearer_token
80
- headers['Authorization'] = "Bearer #{access_token}"
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 = { 'videoId' => video_id }
100
+ query = {"videoId" => video_id}
101
101
 
102
102
  send(endpoint, query, {context: context})
103
103
  end
@@ -11,31 +11,31 @@ module RubyTube
11
11
  end
12
12
  start_index = result.end(0)
13
13
 
14
- return parse_for_object_from_startpoint(html, start_index)
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 ['{', '['].include?(html[0])
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 [' ', '\n'].include?(curr_char)
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 ['"', '/'].include?(curr_context)
49
- if curr_char == '\\'
48
+ if ['"', "/"].include?(curr_context)
49
+ if curr_char == "\\"
50
50
  i += 2
51
51
  next
52
52
  end
53
- else
54
- if context_closers.keys.include?(curr_char)
55
- unless curr_char == '/' && !['(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';'].include?(last_char)
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
- full_obj = html[0...i]
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 ['{', '['].include?(html[0])
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
- '/' => '/' # JavaScript regex
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 [' ', '\n'].include?(curr_char)
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 ['"', '/'].include?(curr_context)
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
- else
108
+ elsif context_closers.keys.include?(curr_char)
111
109
  # Non-string contexts are when we need to look for context openers.
112
- if context_closers.keys.include?(curr_char)
113
- # Slash starts a regular expression depending on context
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
- full_obj = html[0..(i - 1)]
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?('function')
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
@@ -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 = { 'Content-Type': 'text/html' }
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
- response = connection.send(method) do |req|
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
@@ -16,16 +16,16 @@ module RubyTube
16
16
  def initialize(stream, monostate)
17
17
  self.monostate = monostate
18
18
 
19
- self.url = stream['url']
20
- self.itag = stream['itag'].to_i
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['mimeType'])
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['is_otf']
26
- self.bitrate = stream['bitrate']
25
+ self.is_otf = stream["is_otf"]
26
+ self.bitrate = stream["bitrate"]
27
27
 
28
- self.file_size = stream.fetch('contentLength', 0).to_i
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, 'wb') do |f|
39
- begin
40
- Request.stream(url) do |chunk|
41
- bytes_remaining -= chunk.bytesize
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 == 'audio'
51
+ type == "audio"
54
52
  end
55
53
 
56
54
  def is_video?
57
- type == 'video'
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 = default_filename unless 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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyTube
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/rubytube.rb CHANGED
@@ -1,66 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faraday'
4
- require 'faraday/follow_redirects'
3
+ require "faraday"
4
+ require "faraday/follow_redirects"
5
5
 
6
- require_relative 'rubytube/version'
6
+ require_relative "rubytube/version"
7
7
 
8
- require_relative 'rubytube/cipher'
9
- require_relative 'rubytube/client'
10
- require_relative 'rubytube/extractor'
11
- require_relative 'rubytube/innertube'
12
- require_relative 'rubytube/monostate'
13
- require_relative 'rubytube/parser'
14
- require_relative 'rubytube/request'
15
- require_relative 'rubytube/stream_format'
16
- require_relative 'rubytube/stream_query'
17
- require_relative 'rubytube/stream'
18
- require_relative 'rubytube/utils'
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.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-09-30 00:00:00.000000000 Z
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