rubytube 0.4.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,177 +0,0 @@
1
- module RubyTube
2
- class Extractor
3
- class << self
4
- def playability_status(watch_html)
5
- player_response = initial_player_response(watch_html)
6
- player_response = JSON.parse(player_response)
7
- status_obj = player_response['playabilityStatus'] || {}
8
-
9
- if status_obj.has_key?('liveStreamability')
10
- return ['LIVE_STREAM', 'Video is a live stream.']
11
- end
12
-
13
- if status_obj.has_key?('status')
14
- if status_obj.has_key?('reason')
15
- return [status_obj['status'], [status_obj['reason']]]
16
- end
17
-
18
- if status_obj.has_key?('messages')
19
- return [status_obj['status'], status_obj['messages']]
20
- end
21
- end
22
-
23
- [nil, [nil]]
24
- end
25
-
26
- def video_id(url)
27
- return Utils.regex_search(/(?:v=|\/)([0-9A-Za-z_-]{11}).*/, url, 1)
28
- end
29
-
30
- def js_url(html)
31
- begin
32
- base_js = get_ytplayer_config(html)['assets']['js']
33
- rescue RegexMatchError, NoMethodError
34
- base_js = get_ytplayer_js(html)
35
- end
36
-
37
- "https://youtube.com#{base_js}"
38
- end
39
-
40
- def mime_type_codec(mime_type_codec)
41
- pattern = %r{(\w+\/\w+)\;\scodecs=\"([a-zA-Z\-0-9.,\s]*)\"}
42
- results = mime_type_codec.match(pattern)
43
-
44
- raise RegexMatchError.new("mime_type_codec, pattern=#{pattern}") if results.nil?
45
-
46
- mime_type, codecs = results.captures
47
- [mime_type, codecs.split(",").map(&:strip)]
48
- end
49
-
50
- def get_ytplayer_js(html)
51
- js_url_patterns = [
52
- %r{(/s/player/[\w\d]+/[\w\d_/.]+/base\.js)},
53
- ]
54
-
55
- js_url_patterns.each do |pattern|
56
- function_match = html.match(pattern)
57
- if function_match
58
- return function_match[1]
59
- end
60
- end
61
-
62
- raise RegexMatchError.new('get_ytplayer_js', 'js_url_patterns')
63
- end
64
-
65
- def get_ytplayer_config(html)
66
- config_patterns = [
67
- /ytplayer\.config\s*=\s*/,
68
- /ytInitialPlayerResponse\s*=\s*/
69
- ]
70
-
71
- config_patterns.each do |pattern|
72
- begin
73
- return Parser.parse_for_object(html, pattern)
74
- rescue HTMLParseError => e
75
- next
76
- end
77
- end
78
-
79
- setconfig_patterns = [
80
- /yt\.setConfig\(.*['\"]PLAYER_CONFIG['\"]:\s*/
81
- ]
82
-
83
- setconfig_patterns.each do |pattern|
84
- begin
85
- return Parser.parse_for_object(html, pattern)
86
- rescue HTMLParseError => e
87
- next
88
- end
89
- end
90
-
91
- raise RegexMatchError.new('get_ytplayer_config', 'config_patterns, setconfig_patterns')
92
- end
93
-
94
- def apply_signature(stream_manifest, vid_info, js)
95
- cipher = Cipher.new(js)
96
-
97
- stream_manifest.each_with_index do |stream, i|
98
- begin
99
- url = stream['url']
100
- rescue NoMethodError
101
- live_stream = vid_info.fetch('playabilityStatus', {})['liveStreamability']
102
- if live_stream
103
- raise LiveStreamError.new('UNKNOWN')
104
- end
105
- end
106
-
107
- if url.include?("signature") ||
108
- (!stream.key?("s") && (url.include?("&sig=") || url.include?("&lsig=")))
109
- # For certain videos, YouTube will just provide them pre-signed, in
110
- # which case there's no real magic to download them and we can skip
111
- # the whole signature descrambling entirely.
112
- next
113
- end
114
-
115
- signature = cipher.get_signature(stream['s'])
116
-
117
- parsed_url = URI.parse(url)
118
-
119
- query_params = CGI.parse(parsed_url.query)
120
- query_params.transform_values!(&:first)
121
- query_params['sig'] = signature
122
- unless query_params.key?('ratebypass')
123
- initial_n = query_params['n'].split('')
124
- new_n = cipher.calculate_n(initial_n)
125
- query_params['n'] = new_n
126
- end
127
-
128
- url = "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.path}?#{URI.encode_www_form(query_params)}"
129
-
130
- stream_manifest[i]["url"] = url
131
- end
132
- end
133
-
134
- def apply_descrambler(stream_data)
135
- return if stream_data.has_key?('url')
136
-
137
- # Merge formats and adaptiveFormats into a single array
138
- formats = []
139
- formats += stream_data['formats'] if stream_data.has_key?('formats')
140
- formats += stream_data['adaptiveFormats'] if stream_data.has_key?('adaptiveFormats')
141
-
142
- # Extract url and s from signatureCiphers as necessary
143
- formats.each do |data|
144
- unless data.has_key?('url')
145
- if data.has_key?('signatureCipher')
146
- cipher_url = URI.decode_www_form(data['signatureCipher']).to_h
147
- data['url'] = cipher_url['url']
148
- data['s'] = cipher_url['s']
149
- end
150
- end
151
- data['is_otf'] = data['type'] == 'FORMAT_STREAM_TYPE_OTF'
152
- end
153
-
154
- formats
155
- end
156
-
157
- private
158
-
159
- def initial_player_response(watch_html)
160
- patterns = [
161
- "window\\[['\"]ytInitialPlayerResponse['\"]\\]\\s*=\\s*",
162
- "ytInitialPlayerResponse\\s*=\\s*"
163
- ]
164
-
165
- patterns.each do |pattern|
166
- begin
167
- return Parser.parse_for_object(watch_html, pattern)
168
- rescue HTMLParseError
169
- next
170
- end
171
- end
172
-
173
- raise RegexMatchError.new('initial_player_response', 'initial_player_response_pattern')
174
- end
175
- end
176
- end
177
- end
@@ -1,5 +0,0 @@
1
- module RubyTube
2
- class Monostate
3
- attr_accessor :title, :duration
4
- end
5
- end
@@ -1,164 +0,0 @@
1
- module RubyTube
2
- module Parser
3
- module_function
4
-
5
- def parse_for_object(html, preceding_regex)
6
- regex = Regexp.new(preceding_regex)
7
- result = regex.match(html)
8
-
9
- if result.nil?
10
- raise HTMLParseError, "No matches for regex #{preceding_regex}"
11
- end
12
- start_index = result.end(0)
13
-
14
- return parse_for_object_from_startpoint(html, start_index)
15
- end
16
-
17
- def find_object_from_startpoint(html, start_point)
18
- html = html[start_point..-1]
19
- unless ['{', '['].include?(html[0])
20
- raise HTMLParseError, "Invalid start point. Start of HTML:\n#{html[0..19]}"
21
- end
22
-
23
- last_char = '{'
24
- curr_char = nil
25
- stack = [html[0]]
26
- i = 1
27
-
28
- context_closers = {
29
- '{' => '}',
30
- '[' => ']',
31
- '"' => '"',
32
- '/' => '/',
33
- }
34
-
35
- while i < html.length
36
- break if stack.empty?
37
-
38
- last_char = curr_char unless [' ', '\n'].include?(curr_char)
39
- curr_char = html[i]
40
- curr_context = stack.last
41
-
42
- if curr_char == context_closers[curr_context]
43
- stack.pop
44
- i += 1
45
- next
46
- end
47
-
48
- if ['"', '/'].include?(curr_context)
49
- if curr_char == '\\'
50
- i += 2
51
- next
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
58
- end
59
- end
60
-
61
- i += 1
62
- end
63
-
64
- full_obj = html[0...i]
65
- full_obj
66
- end
67
-
68
- def parse_for_object_from_startpoint(html, start_point)
69
- html = html[start_point..-1]
70
-
71
- unless ['{', '['].include?(html[0])
72
- raise HTMLParseError, "Invalid start point. Start of HTML:\n#{html[0..19]}"
73
- end
74
-
75
- # First letter MUST be an open brace, so we put that in the stack,
76
- # and skip the first character.
77
- last_char = '{'
78
- curr_char = nil
79
- stack = [html[0]]
80
- i = 1
81
-
82
- context_closers = {
83
- '{' => '}',
84
- '[' => ']',
85
- '"' => '"',
86
- '/' => '/' # JavaScript regex
87
- }
88
-
89
- while i < html.length
90
- break if stack.empty?
91
-
92
- last_char = curr_char unless [' ', '\n'].include?(curr_char)
93
- curr_char = html[i]
94
- curr_context = stack.last
95
-
96
- # If we've reached a context closer, we can remove an element off the stack
97
- if curr_char == context_closers[curr_context]
98
- stack.pop
99
- i += 1
100
- next
101
- end
102
- # Strings and regex expressions require special context handling because they can contain
103
- # context openers *and* closers
104
- if ['"', '/'].include?(curr_context)
105
- # If there's a backslash in a string or regex expression, we skip a character
106
- if curr_char == '\\'
107
- i += 2
108
- next
109
- end
110
- else
111
- # 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
117
- end
118
- end
119
-
120
- i += 1
121
- end
122
-
123
- full_obj = html[0..(i - 1)]
124
- full_obj
125
- end
126
-
127
- def throttling_array_split(js_array)
128
- results = []
129
- curr_substring = js_array[1..-1]
130
-
131
- comma_regex = /,/
132
- func_regex = /function\([^)]*\)/
133
-
134
- until curr_substring.empty?
135
- if curr_substring.start_with?('function')
136
- match = func_regex.match(curr_substring)
137
- match_start = match.begin(0)
138
- match_end = match.end(0)
139
-
140
- function_text = find_object_from_startpoint(curr_substring, match_end)
141
- full_function_def = curr_substring[0, match_end + function_text.length]
142
- results << full_function_def
143
- curr_substring = curr_substring[full_function_def.length + 1..-1]
144
- else
145
- match = comma_regex.match(curr_substring)
146
-
147
- begin
148
- match_start = match.begin(0)
149
- match_end = match.end(0)
150
- rescue NoMethodError
151
- match_start = curr_substring.length - 1
152
- match_end = match_start + 1
153
- end
154
-
155
- curr_el = curr_substring[0, match_start]
156
- results << curr_el
157
- curr_substring = curr_substring[match_end..-1]
158
- end
159
- end
160
-
161
- results
162
- end
163
- end
164
- end
@@ -1,75 +0,0 @@
1
- module RubyTube
2
- module Request
3
- module_function
4
-
5
- DEFAULT_RANGE_SIZE = 9437184
6
-
7
- def get(url, options = {})
8
- send(:get, url, options).body
9
- end
10
-
11
- def post(url, options = {})
12
- send(:post, url, options).body
13
- end
14
-
15
- def head(url, options = {})
16
- send(:head, url, options).headers
17
- end
18
-
19
- def stream(url, timeout: 60, max_retries: 0)
20
- file_size = DEFAULT_RANGE_SIZE
21
- downloaded = 0
22
-
23
- while downloaded < file_size
24
- stop_pos = [downloaded + DEFAULT_RANGE_SIZE, file_size].min - 1
25
- range_header = "bytes=#{downloaded}-#{stop_pos}"
26
- tries = 0
27
-
28
- while true
29
- begin
30
- if tries >= 1 + max_retries
31
- raise MaxRetriesExceeded
32
- end
33
- response = send(:get, "#{url}&range=#{downloaded}-#{stop_pos}")
34
- break
35
- rescue Faraday::TimeoutError
36
- rescue Faraday::ClientError => e
37
- raise e
38
- end
39
- tries += 1
40
- end
41
-
42
- if file_size == DEFAULT_RANGE_SIZE
43
- begin
44
- resp = send(:get, "#{url}&range=0-99999999999")
45
- content_range = resp.headers["Content-Length"]
46
- file_size = content_range.to_i
47
- rescue KeyError, IndexError, StandardError => e
48
- end
49
- end
50
-
51
- response.body.each_char do |chunk|
52
- downloaded += chunk.length
53
- yield chunk
54
- end
55
- end
56
- end
57
-
58
- def send(method, url, options = {})
59
- headers = { 'Content-Type': 'text/html' }
60
- options[:headers] && headers.merge!(options[:headers])
61
-
62
- connection = Faraday.new(url: url) do |faraday|
63
- faraday.response :follow_redirects
64
- faraday.adapter Faraday.default_adapter
65
- end
66
- response = connection.send(method) do |req|
67
- req.headers = headers
68
- options[:query] && req.params = options[:query]
69
- options[:data] && req.body = JSON.dump(options[:data])
70
- end
71
-
72
- response
73
- end
74
- end
75
- end
@@ -1,152 +0,0 @@
1
- module RubyTube
2
- class StreamFormat
3
- PROGRESSIVE_VIDEO = {
4
- 5 => ["240p", "64kbps"],
5
- 6 => ["270p", "64kbps"],
6
- 13 => ["144p", nil],
7
- 17 => ["144p", "24kbps"],
8
- 18 => ["360p", "96kbps"],
9
- 22 => ["720p", "192kbps"],
10
- 34 => ["360p", "128kbps"],
11
- 35 => ["480p", "128kbps"],
12
- 36 => ["240p", nil],
13
- 37 => ["1080p", "192kbps"],
14
- 38 => ["3072p", "192kbps"],
15
- 43 => ["360p", "128kbps"],
16
- 44 => ["480p", "128kbps"],
17
- 45 => ["720p", "192kbps"],
18
- 46 => ["1080p", "192kbps"],
19
- 59 => ["480p", "128kbps"],
20
- 78 => ["480p", "128kbps"],
21
- 82 => ["360p", "128kbps"],
22
- 83 => ["480p", "128kbps"],
23
- 84 => ["720p", "192kbps"],
24
- 85 => ["1080p", "192kbps"],
25
- 91 => ["144p", "48kbps"],
26
- 92 => ["240p", "48kbps"],
27
- 93 => ["360p", "128kbps"],
28
- 94 => ["480p", "128kbps"],
29
- 95 => ["720p", "256kbps"],
30
- 96 => ["1080p", "256kbps"],
31
- 100 => ["360p", "128kbps"],
32
- 101 => ["480p", "192kbps"],
33
- 102 => ["720p", "192kbps"],
34
- 132 => ["240p", "48kbps"],
35
- 151 => ["720p", "24kbps"],
36
- 300 => ["720p", "128kbps"],
37
- 301 => ["1080p", "128kbps"],
38
- }
39
-
40
- DASH_VIDEO = {
41
- 133 => ["240p", nil],
42
- 134 => ["360p", nil],
43
- 135 => ["480p", nil],
44
- 136 => ["720p", nil],
45
- 137 => ["1080p", nil],
46
- 138 => ["2160p", nil],
47
- 160 => ["144p", nil],
48
- 167 => ["360p", nil],
49
- 168 => ["480p", nil],
50
- 169 => ["720p", nil],
51
- 170 => ["1080p", nil],
52
- 212 => ["480p", nil],
53
- 218 => ["480p", nil],
54
- 219 => ["480p", nil],
55
- 242 => ["240p", nil],
56
- 243 => ["360p", nil],
57
- 244 => ["480p", nil],
58
- 245 => ["480p", nil],
59
- 246 => ["480p", nil],
60
- 247 => ["720p", nil],
61
- 248 => ["1080p", nil],
62
- 264 => ["1440p", nil],
63
- 266 => ["2160p", nil],
64
- 271 => ["1440p", nil],
65
- 272 => ["4320p", nil],
66
- 278 => ["144p", nil],
67
- 298 => ["720p", nil],
68
- 299 => ["1080p", nil],
69
- 302 => ["720p", nil],
70
- 303 => ["1080p", nil],
71
- 308 => ["1440p", nil],
72
- 313 => ["2160p", nil],
73
- 315 => ["2160p", nil],
74
- 330 => ["144p", nil],
75
- 331 => ["240p", nil],
76
- 332 => ["360p", nil],
77
- 333 => ["480p", nil],
78
- 334 => ["720p", nil],
79
- 335 => ["1080p", nil],
80
- 336 => ["1440p", nil],
81
- 337 => ["2160p", nil],
82
- 394 => ["144p", nil],
83
- 395 => ["240p", nil],
84
- 396 => ["360p", nil],
85
- 397 => ["480p", nil],
86
- 398 => ["720p", nil],
87
- 399 => ["1080p", nil],
88
- 400 => ["1440p", nil],
89
- 401 => ["2160p", nil],
90
- 402 => ["4320p", nil],
91
- 571 => ["4320p", nil],
92
- 694 => ["144p", nil],
93
- 695 => ["240p", nil],
94
- 696 => ["360p", nil],
95
- 697 => ["480p", nil],
96
- 698 => ["720p", nil],
97
- 699 => ["1080p", nil],
98
- 700 => ["1440p", nil],
99
- 701 => ["2160p", nil],
100
- 702 => ["4320p", nil]
101
- }
102
-
103
- DASH_AUDIO = {
104
- 139 => [nil, "48kbps"],
105
- 140 => [nil, "128kbps"],
106
- 141 => [nil, "256kbps"],
107
- 171 => [nil, "128kbps"],
108
- 172 => [nil, "256kbps"],
109
- 249 => [nil, "50kbps"],
110
- 250 => [nil, "70kbps"],
111
- 251 => [nil, "160kbps"],
112
- 256 => [nil, "192kbps"],
113
- 258 => [nil, "384kbps"],
114
- 325 => [nil, nil],
115
- 328 => [nil, nil],
116
- }
117
-
118
- ITAGS = {
119
- **PROGRESSIVE_VIDEO,
120
- **DASH_VIDEO,
121
- **DASH_AUDIO
122
- }
123
-
124
- HDR = [330, 331, 332, 333, 334, 335, 336, 337]
125
- FORMAT_3D = [82, 83, 84, 85, 100, 101, 102]
126
- LIVE = [91, 92, 93, 94, 95, 96, 132, 151]
127
-
128
- attr_reader :itag, :resolution, :abr
129
-
130
- def initialize(_itag)
131
- @itag = _itag
132
-
133
- @resolution, @abr = ITAGS.fetch(itag)
134
- end
135
-
136
- def is_live?
137
- !!LIVE[itag]
138
- end
139
-
140
- def is_3d?
141
- !!FORMAT_3D[itag]
142
- end
143
-
144
- def is_hdr?
145
- !!HDR[itag]
146
- end
147
-
148
- def is_dash?
149
- !!DASH_AUDIO[itag] || !!DASH_VIDEO[itag]
150
- end
151
- end
152
- end
@@ -1,24 +0,0 @@
1
- module RubyTube
2
- module Utils
3
- module_function
4
-
5
- def regex_search(pattern, string, group)
6
- match = string.match(pattern)
7
- if match
8
- return match[group]
9
- end
10
- nil
11
- end
12
-
13
- def target_directory(output_path = nil)
14
- if output_path
15
- result = File.join(Dir.pwd, output_path) unless File.absolute_path?(output_path)
16
- else
17
- result = Dir.pwd
18
- end
19
-
20
- FileUtils.mkdir_p(result) unless Dir.exist?(result)
21
- result
22
- end
23
- end
24
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyTube
4
- VERSION = "0.4.0"
5
- end
data/sig/rubytube.rbs DELETED
@@ -1,4 +0,0 @@
1
- module RubyTube
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end