rubytube 0.5.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,171 +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
- 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
- return Parser.parse_for_object(html, pattern)
73
- rescue HTMLParseError => e
74
- next
75
- end
76
-
77
- setconfig_patterns = [
78
- /yt\.setConfig\(.*['\"]PLAYER_CONFIG['\"]:\s*/
79
- ]
80
-
81
- setconfig_patterns.each do |pattern|
82
- return Parser.parse_for_object(html, pattern)
83
- rescue HTMLParseError => e
84
- next
85
- end
86
-
87
- raise RegexMatchError.new("get_ytplayer_config", "config_patterns, setconfig_patterns")
88
- end
89
-
90
- def apply_signature(stream_manifest, vid_info, js)
91
- cipher = Cipher.new(js)
92
-
93
- stream_manifest.each_with_index do |stream, i|
94
- begin
95
- url = stream["url"]
96
- rescue NoMethodError
97
- live_stream = vid_info.fetch("playabilityStatus", {})["liveStreamability"]
98
- if live_stream
99
- raise LiveStreamError.new("UNKNOWN")
100
- end
101
- end
102
-
103
- if url.include?("signature") ||
104
- (!stream.key?("s") && (url.include?("&sig=") || url.include?("&lsig=")))
105
- # For certain videos, YouTube will just provide them pre-signed, in
106
- # which case there's no real magic to download them and we can skip
107
- # the whole signature descrambling entirely.
108
- next
109
- end
110
-
111
- signature = cipher.get_signature(stream["s"])
112
-
113
- parsed_url = URI.parse(url)
114
-
115
- query_params = CGI.parse(parsed_url.query)
116
- query_params.transform_values!(&:first)
117
- query_params["sig"] = signature
118
- unless query_params.key?("ratebypass")
119
- initial_n = query_params["n"].chars
120
- new_n = cipher.calculate_n(initial_n)
121
- query_params["n"] = new_n
122
- end
123
-
124
- url = "#{parsed_url.scheme}://#{parsed_url.host}#{parsed_url.path}?#{URI.encode_www_form(query_params)}"
125
-
126
- stream_manifest[i]["url"] = url
127
- end
128
- end
129
-
130
- def apply_descrambler(stream_data)
131
- return if stream_data.has_key?("url")
132
-
133
- # Merge formats and adaptiveFormats into a single array
134
- formats = []
135
- formats += stream_data["formats"] if stream_data.has_key?("formats")
136
- formats += stream_data["adaptiveFormats"] if stream_data.has_key?("adaptiveFormats")
137
-
138
- # Extract url and s from signatureCiphers as necessary
139
- formats.each do |data|
140
- unless data.has_key?("url")
141
- if data.has_key?("signatureCipher")
142
- cipher_url = URI.decode_www_form(data["signatureCipher"]).to_h
143
- data["url"] = cipher_url["url"]
144
- data["s"] = cipher_url["s"]
145
- end
146
- end
147
- data["is_otf"] = data["type"] == "FORMAT_STREAM_TYPE_OTF"
148
- end
149
-
150
- formats
151
- end
152
-
153
- private
154
-
155
- def initial_player_response(watch_html)
156
- patterns = [
157
- "window\\[['\"]ytInitialPlayerResponse['\"]\\]\\s*=\\s*",
158
- "ytInitialPlayerResponse\\s*=\\s*"
159
- ]
160
-
161
- patterns.each do |pattern|
162
- return Parser.parse_for_object(watch_html, pattern)
163
- rescue HTMLParseError
164
- next
165
- end
166
-
167
- raise RegexMatchError.new("initial_player_response", "initial_player_response_pattern")
168
- end
169
- end
170
- end
171
- end
@@ -1,5 +0,0 @@
1
- module RubyTube
2
- class Monostate
3
- attr_accessor :title, :duration
4
- end
5
- end
@@ -1,159 +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
- 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
- elsif context_closers.keys.include?(curr_char)
54
- unless curr_char == "/" && !["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";"].include?(last_char)
55
- stack.push(curr_char)
56
- end
57
- end
58
-
59
- i += 1
60
- end
61
-
62
- html[0...i]
63
- end
64
-
65
- def parse_for_object_from_startpoint(html, start_point)
66
- html = html[start_point..-1]
67
-
68
- unless ["{", "["].include?(html[0])
69
- raise HTMLParseError, "Invalid start point. Start of HTML:\n#{html[0..19]}"
70
- end
71
-
72
- # First letter MUST be an open brace, so we put that in the stack,
73
- # and skip the first character.
74
- last_char = "{"
75
- curr_char = nil
76
- stack = [html[0]]
77
- i = 1
78
-
79
- context_closers = {
80
- "{" => "}",
81
- "[" => "]",
82
- '"' => '"',
83
- :"'" => "'",
84
- "/" => "/" # JavaScript regex
85
- }
86
-
87
- while i < html.length
88
- break if stack.empty?
89
-
90
- last_char = curr_char unless [" ", '\n'].include?(curr_char)
91
- curr_char = html[i]
92
- curr_context = stack.last
93
-
94
- # If we've reached a context closer, we can remove an element off the stack
95
- if curr_char == context_closers[curr_context]
96
- stack.pop
97
- i += 1
98
- next
99
- end
100
- # Strings and regex expressions require special context handling because they can contain
101
- # context openers *and* closers
102
- if ['"', "/"].include?(curr_context)
103
- # If there's a backslash in a string or regex expression, we skip a character
104
- if curr_char == "\\"
105
- i += 2
106
- next
107
- end
108
- elsif context_closers.keys.include?(curr_char)
109
- # Non-string contexts are when we need to look for context openers.
110
- unless curr_char == "/" && ["(", ",", "=", ":", "[", "!", "&", "|", "?", "{", "}", ";"].include?(last_char)
111
- stack << curr_char
112
- end
113
- # Slash starts a regular expression depending on context
114
- end
115
-
116
- i += 1
117
- end
118
-
119
- html[0..(i - 1)]
120
- end
121
-
122
- def throttling_array_split(js_array)
123
- results = []
124
- curr_substring = js_array[1..-1]
125
-
126
- comma_regex = /,/
127
- func_regex = /function\([^)]*\)/
128
-
129
- until curr_substring.empty?
130
- if curr_substring.start_with?("function")
131
- match = func_regex.match(curr_substring)
132
- match_start = match.begin(0)
133
- match_end = match.end(0)
134
-
135
- function_text = find_object_from_startpoint(curr_substring, match_end)
136
- full_function_def = curr_substring[0, match_end + function_text.length]
137
- results << full_function_def
138
- curr_substring = curr_substring[full_function_def.length + 1..-1]
139
- else
140
- match = comma_regex.match(curr_substring)
141
-
142
- begin
143
- match_start = match.begin(0)
144
- match_end = match.end(0)
145
- rescue NoMethodError
146
- match_start = curr_substring.length - 1
147
- match_end = match_start + 1
148
- end
149
-
150
- curr_el = curr_substring[0, match_start]
151
- results << curr_el
152
- curr_substring = curr_substring[match_end..-1]
153
- end
154
- end
155
-
156
- results
157
- end
158
- end
159
- end
@@ -1,73 +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
- 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
- end
72
- end
73
- 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.5.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