rubytube 0.4.0 → 0.5.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 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