rubytube 0.2.0 → 0.3.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 +4 -4
- data/.rspec +3 -3
- data/.standard.yml +3 -3
- data/CHANGELOG.md +5 -5
- data/CODE_OF_CONDUCT.md +84 -84
- data/Gemfile +12 -12
- data/LICENSE.txt +21 -21
- data/README.md +19 -19
- data/Rakefile +10 -10
- data/lib/rubytube/cipher.rb +371 -370
- data/lib/rubytube/client.rb +173 -173
- data/lib/rubytube/extractor.rb +177 -177
- data/lib/rubytube/innertube.rb +105 -105
- data/lib/rubytube/monostate.rb +5 -5
- data/lib/rubytube/parser.rb +164 -164
- data/lib/rubytube/request.rb +75 -75
- data/lib/rubytube/stream.rb +97 -89
- data/lib/rubytube/stream_format.rb +152 -152
- data/lib/rubytube/stream_query.rb +36 -38
- data/lib/rubytube/utils.rb +24 -24
- data/lib/rubytube/version.rb +5 -5
- data/lib/rubytube.rb +68 -68
- data/sig/rubytube.rbs +4 -4
- metadata +8 -7
data/lib/rubytube/client.rb
CHANGED
@@ -1,173 +1,173 @@
|
|
1
|
-
module RubyTube
|
2
|
-
class Client
|
3
|
-
attr_accessor :video_id, :watch_url, :embed_url, :stream_monostate
|
4
|
-
|
5
|
-
def initialize(url)
|
6
|
-
self.video_id = Extractor.video_id(url)
|
7
|
-
|
8
|
-
self.watch_url = "https://youtube.com/watch?v=#{video_id}"
|
9
|
-
self.embed_url = "https://www.youtube.com/embed/#{video_id}"
|
10
|
-
|
11
|
-
self.stream_monostate = Monostate.new
|
12
|
-
end
|
13
|
-
|
14
|
-
def watch_html
|
15
|
-
return @watch_html if @watch_html
|
16
|
-
|
17
|
-
@watch_html = Request.get(watch_url)
|
18
|
-
@watch_html
|
19
|
-
end
|
20
|
-
|
21
|
-
def js
|
22
|
-
return @js if @js
|
23
|
-
|
24
|
-
@js = Request.get(js_url)
|
25
|
-
@js
|
26
|
-
end
|
27
|
-
|
28
|
-
def js_url
|
29
|
-
return @js_url if @js_url
|
30
|
-
|
31
|
-
@js_url = Extractor.js_url(watch_html)
|
32
|
-
@js_url
|
33
|
-
end
|
34
|
-
|
35
|
-
def streaming_data
|
36
|
-
return vid_info['streamingData'] if vid_info && vid_info.key?('streamingData')
|
37
|
-
|
38
|
-
bypass_age_gate
|
39
|
-
vid_info['streamingData']
|
40
|
-
end
|
41
|
-
|
42
|
-
def fmt_streams
|
43
|
-
check_availability
|
44
|
-
return @fmt_streams if @fmt_streams
|
45
|
-
|
46
|
-
@fmt_streams = []
|
47
|
-
stream_manifest = Extractor.apply_descrambler(streaming_data)
|
48
|
-
|
49
|
-
begin
|
50
|
-
Extractor.apply_signature(stream_manifest, vid_info, js)
|
51
|
-
rescue ExtractError
|
52
|
-
js = nil
|
53
|
-
js_url = nil
|
54
|
-
Extractor.apply_signature(stream_manifest, vid_info, js)
|
55
|
-
end
|
56
|
-
|
57
|
-
for stream in stream_manifest
|
58
|
-
@fmt_streams << Stream.new(stream, stream_monostate)
|
59
|
-
end
|
60
|
-
|
61
|
-
stream_monostate.title = title
|
62
|
-
stream_monostate.duration = length
|
63
|
-
|
64
|
-
@fmt_streams
|
65
|
-
end
|
66
|
-
|
67
|
-
def check_availability
|
68
|
-
status, messages = Extractor.playability_status(watch_html)
|
69
|
-
|
70
|
-
messages.each do |reason|
|
71
|
-
case status
|
72
|
-
when 'UNPLAYABLE'
|
73
|
-
case reason
|
74
|
-
when 'Join this channel to get access to members-only content like this video, and other exclusive perks.'
|
75
|
-
raise MembersOnly.new(video_id)
|
76
|
-
when 'This live stream recording is not available.'
|
77
|
-
raise RecordingUnavailable.new(video_id)
|
78
|
-
else
|
79
|
-
raise VideoUnavailable.new(video_id)
|
80
|
-
end
|
81
|
-
when 'LOGIN_REQUIRED'
|
82
|
-
if reason == 'This is a private video. Please sign in to verify that you may see it.'
|
83
|
-
raise VideoPrivate.new(video_id)
|
84
|
-
end
|
85
|
-
when 'ERROR'
|
86
|
-
if reason == 'Video unavailable'
|
87
|
-
raise VideoUnavailable.new(video_id)
|
88
|
-
end
|
89
|
-
when 'LIVE_STREAM'
|
90
|
-
raise LiveStreamError.new(video_id)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def streams
|
96
|
-
return @streams if @streams
|
97
|
-
|
98
|
-
check_availability
|
99
|
-
@streams = StreamQuery.new(fmt_streams)
|
100
|
-
end
|
101
|
-
|
102
|
-
def thumbnail_url
|
103
|
-
thumbs = vid_info.fetch('videoDetails', {}).fetch('thumbnail', {}).fetch('thumbnails', [])
|
104
|
-
|
105
|
-
return thumbs[-1]['url'] if thumbs.size > 0
|
106
|
-
|
107
|
-
"https://img.youtube.com/vi/#{ideo_id}/maxresdefault.jpg"
|
108
|
-
end
|
109
|
-
|
110
|
-
def vid_info
|
111
|
-
return @vid_info if @vid_info
|
112
|
-
|
113
|
-
it = InnerTube.new
|
114
|
-
@vid_info = it.player(video_id)
|
115
|
-
|
116
|
-
@vid_info
|
117
|
-
end
|
118
|
-
|
119
|
-
def bypass_age_gate
|
120
|
-
it = InnerTube.new(client: 'ANDROID_EMBED')
|
121
|
-
resp = it.player(video_id)
|
122
|
-
|
123
|
-
status = resp['playabilityStatus']['status']
|
124
|
-
if status == 'UNPLAYABLE'
|
125
|
-
raise VideoUnavailable.new(video_id)
|
126
|
-
end
|
127
|
-
|
128
|
-
@vid_info = resp
|
129
|
-
end
|
130
|
-
|
131
|
-
def title
|
132
|
-
return @title if @title
|
133
|
-
|
134
|
-
@title = vid_info['videoDetails']['title']
|
135
|
-
@title
|
136
|
-
end
|
137
|
-
|
138
|
-
def length
|
139
|
-
return @length if @length
|
140
|
-
|
141
|
-
@length = vid_info['videoDetails']['lengthSeconds'].to_i
|
142
|
-
@length
|
143
|
-
end
|
144
|
-
|
145
|
-
def views
|
146
|
-
return @views if @views
|
147
|
-
|
148
|
-
@views = vid_info['videoDetails']['viewCount'].to_i
|
149
|
-
@views
|
150
|
-
end
|
151
|
-
|
152
|
-
def author
|
153
|
-
return @author if @author
|
154
|
-
|
155
|
-
@author = vid_info['videoDetails']['author']
|
156
|
-
@author
|
157
|
-
end
|
158
|
-
|
159
|
-
def keywords
|
160
|
-
return @keywords if @keywords
|
161
|
-
|
162
|
-
@keywords = vid_info['videoDetails']['keywords']
|
163
|
-
@keywords
|
164
|
-
end
|
165
|
-
|
166
|
-
def channel_id
|
167
|
-
return @channel_id if @channel_id
|
168
|
-
|
169
|
-
@channel_id = vid_info['videoDetails']['channelId']
|
170
|
-
@channel_id
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
1
|
+
module RubyTube
|
2
|
+
class Client
|
3
|
+
attr_accessor :video_id, :watch_url, :embed_url, :stream_monostate
|
4
|
+
|
5
|
+
def initialize(url)
|
6
|
+
self.video_id = Extractor.video_id(url)
|
7
|
+
|
8
|
+
self.watch_url = "https://youtube.com/watch?v=#{video_id}"
|
9
|
+
self.embed_url = "https://www.youtube.com/embed/#{video_id}"
|
10
|
+
|
11
|
+
self.stream_monostate = Monostate.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def watch_html
|
15
|
+
return @watch_html if @watch_html
|
16
|
+
|
17
|
+
@watch_html = Request.get(watch_url)
|
18
|
+
@watch_html
|
19
|
+
end
|
20
|
+
|
21
|
+
def js
|
22
|
+
return @js if @js
|
23
|
+
|
24
|
+
@js = Request.get(js_url)
|
25
|
+
@js
|
26
|
+
end
|
27
|
+
|
28
|
+
def js_url
|
29
|
+
return @js_url if @js_url
|
30
|
+
|
31
|
+
@js_url = Extractor.js_url(watch_html)
|
32
|
+
@js_url
|
33
|
+
end
|
34
|
+
|
35
|
+
def streaming_data
|
36
|
+
return vid_info['streamingData'] if vid_info && vid_info.key?('streamingData')
|
37
|
+
|
38
|
+
bypass_age_gate
|
39
|
+
vid_info['streamingData']
|
40
|
+
end
|
41
|
+
|
42
|
+
def fmt_streams
|
43
|
+
check_availability
|
44
|
+
return @fmt_streams if @fmt_streams
|
45
|
+
|
46
|
+
@fmt_streams = []
|
47
|
+
stream_manifest = Extractor.apply_descrambler(streaming_data)
|
48
|
+
|
49
|
+
begin
|
50
|
+
Extractor.apply_signature(stream_manifest, vid_info, js)
|
51
|
+
rescue ExtractError
|
52
|
+
js = nil
|
53
|
+
js_url = nil
|
54
|
+
Extractor.apply_signature(stream_manifest, vid_info, js)
|
55
|
+
end
|
56
|
+
|
57
|
+
for stream in stream_manifest
|
58
|
+
@fmt_streams << Stream.new(stream, stream_monostate)
|
59
|
+
end
|
60
|
+
|
61
|
+
stream_monostate.title = title
|
62
|
+
stream_monostate.duration = length
|
63
|
+
|
64
|
+
@fmt_streams
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_availability
|
68
|
+
status, messages = Extractor.playability_status(watch_html)
|
69
|
+
|
70
|
+
messages.each do |reason|
|
71
|
+
case status
|
72
|
+
when 'UNPLAYABLE'
|
73
|
+
case reason
|
74
|
+
when 'Join this channel to get access to members-only content like this video, and other exclusive perks.'
|
75
|
+
raise MembersOnly.new(video_id)
|
76
|
+
when 'This live stream recording is not available.'
|
77
|
+
raise RecordingUnavailable.new(video_id)
|
78
|
+
else
|
79
|
+
raise VideoUnavailable.new(video_id)
|
80
|
+
end
|
81
|
+
when 'LOGIN_REQUIRED'
|
82
|
+
if reason == 'This is a private video. Please sign in to verify that you may see it.'
|
83
|
+
raise VideoPrivate.new(video_id)
|
84
|
+
end
|
85
|
+
when 'ERROR'
|
86
|
+
if reason == 'Video unavailable'
|
87
|
+
raise VideoUnavailable.new(video_id)
|
88
|
+
end
|
89
|
+
when 'LIVE_STREAM'
|
90
|
+
raise LiveStreamError.new(video_id)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def streams
|
96
|
+
return @streams if @streams
|
97
|
+
|
98
|
+
check_availability
|
99
|
+
@streams = StreamQuery.new(fmt_streams)
|
100
|
+
end
|
101
|
+
|
102
|
+
def thumbnail_url
|
103
|
+
thumbs = vid_info.fetch('videoDetails', {}).fetch('thumbnail', {}).fetch('thumbnails', [])
|
104
|
+
|
105
|
+
return thumbs[-1]['url'] if thumbs.size > 0
|
106
|
+
|
107
|
+
"https://img.youtube.com/vi/#{ideo_id}/maxresdefault.jpg"
|
108
|
+
end
|
109
|
+
|
110
|
+
def vid_info
|
111
|
+
return @vid_info if @vid_info
|
112
|
+
|
113
|
+
it = InnerTube.new
|
114
|
+
@vid_info = it.player(video_id)
|
115
|
+
|
116
|
+
@vid_info
|
117
|
+
end
|
118
|
+
|
119
|
+
def bypass_age_gate
|
120
|
+
it = InnerTube.new(client: 'ANDROID_EMBED')
|
121
|
+
resp = it.player(video_id)
|
122
|
+
|
123
|
+
status = resp['playabilityStatus']['status']
|
124
|
+
if status == 'UNPLAYABLE'
|
125
|
+
raise VideoUnavailable.new(video_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
@vid_info = resp
|
129
|
+
end
|
130
|
+
|
131
|
+
def title
|
132
|
+
return @title if @title
|
133
|
+
|
134
|
+
@title = vid_info['videoDetails']['title']
|
135
|
+
@title
|
136
|
+
end
|
137
|
+
|
138
|
+
def length
|
139
|
+
return @length if @length
|
140
|
+
|
141
|
+
@length = vid_info['videoDetails']['lengthSeconds'].to_i
|
142
|
+
@length
|
143
|
+
end
|
144
|
+
|
145
|
+
def views
|
146
|
+
return @views if @views
|
147
|
+
|
148
|
+
@views = vid_info['videoDetails']['viewCount'].to_i
|
149
|
+
@views
|
150
|
+
end
|
151
|
+
|
152
|
+
def author
|
153
|
+
return @author if @author
|
154
|
+
|
155
|
+
@author = vid_info['videoDetails']['author']
|
156
|
+
@author
|
157
|
+
end
|
158
|
+
|
159
|
+
def keywords
|
160
|
+
return @keywords if @keywords
|
161
|
+
|
162
|
+
@keywords = vid_info['videoDetails']['keywords']
|
163
|
+
@keywords
|
164
|
+
end
|
165
|
+
|
166
|
+
def channel_id
|
167
|
+
return @channel_id if @channel_id
|
168
|
+
|
169
|
+
@channel_id = vid_info['videoDetails']['channelId']
|
170
|
+
@channel_id
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|