/
youtube_onebox.rb
186 lines (153 loc) · 5.52 KB
/
youtube_onebox.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# frozen_string_literal: true
module Onebox
module Engine
class YoutubeOnebox
include Engine
include StandardEmbed
matches_regexp(%r{^https?://(?:www\.)?(?:m\.)?(?:youtube\.com|youtu\.be)/.+$})
requires_iframe_origins "https://www.youtube.com"
always_https
WIDTH ||= 480
HEIGHT ||= 360
def parse_embed_response
return unless video_id
return @parse_embed_response if defined?(@parse_embed_response)
embed_url = "https://www.youtube.com/embed/#{video_id}"
@embed_doc ||= Onebox::Helpers.fetch_html_doc(embed_url)
begin
script_tag =
@embed_doc.xpath("//script").find { |tag| tag.to_s.include?("ytcfg.set") }.to_s
match = script_tag.to_s.match(/ytcfg\.set\((?<json>.*)\)/)
yt_json = ::JSON.parse(match[:json])
renderer =
::JSON.parse(yt_json["PLAYER_VARS"]["embedded_player_response"])["embedPreview"][
"thumbnailPreviewRenderer"
]
title = renderer["title"]["runs"].first["text"]
image = "https://img.youtube.com/vi/#{video_id}/hqdefault.jpg"
rescue StandardError
return
end
@parse_embed_response = { image: image, title: title }
end
def placeholder_html
if video_id || list_id
result = parse_embed_response
result ||= get_opengraph.data
"<img src='#{result[:image]}' width='#{WIDTH}' height='#{HEIGHT}' title='#{CGI.escapeHTML(result[:title])}'>"
else
to_html
end
end
def to_html
if video_id
<<-HTML
<iframe
src="https://www.youtube.com/embed/#{video_id}?#{embed_params}"
width="#{WIDTH}"
height="#{HEIGHT}"
frameborder="0"
allowfullscreen
class="youtube-onebox"
></iframe>
HTML
elsif list_id
<<-HTML
<iframe
src="https://www.youtube.com/embed/videoseries?list=#{list_id}&wmode=transparent&rel=0&autohide=1&showinfo=1&enablejsapi=1"
width="#{WIDTH}"
height="#{HEIGHT}"
frameborder="0"
allowfullscreen
class="youtube-onebox"
></iframe>
HTML
else
# for channel pages
html = Onebox::Engine::AllowlistedGenericOnebox.new(@url, @timeout).to_html
return if html.blank?
html.gsub!(%r{['"]//}, "https://")
html
end
end
def video_title
@video_title ||=
begin
result = parse_embed_response || get_opengraph.data
result[:title]
end
end
private
def video_id
@video_id ||=
begin
id = nil
# http://youtu.be/afyK1HSFfgw
id = uri.path[%r{/([\w\-]+)}, 1] if uri.host["youtu.be"]
# https://www.youtube.com/embed/vsF0K3Ou1v0
id ||= uri.path[%r{/embed/([\w\-]+)}, 1] if uri.path["/embed/"]
# https://www.youtube.com/shorts/wi2jAtpBl0Y
id ||= uri.path[%r{/shorts/([\w\-]+)}, 1] if uri.path["/shorts/"]
# https://www.youtube.com/watch?v=Z0UISCEe52Y
id ||= params["v"]
sanitize_yt_id(id)
end
end
def list_id
@list_id ||= sanitize_yt_id(params["list"])
end
def sanitize_yt_id(raw)
raw&.match?(/\A[\w-]+\z/) ? raw : nil
end
def embed_params
p = { "feature" => "oembed", "wmode" => "opaque" }
p["list"] = list_id if list_id
# Parse timestrings, and assign the result as a start= parameter
start =
if params["start"]
params["start"]
elsif params["t"]
params["t"]
elsif uri.fragment && uri.fragment.start_with?("t=")
# referencing uri is safe here because any throws were already caught by video_id returning nil
# remove the t= from the start
uri.fragment[2..-1]
end
p["start"] = parse_timestring(start) if start
p["end"] = parse_timestring params["end"] if params["end"]
# Official workaround for looping videos
# https://developers.google.com/youtube/player_parameters#loop
# use params.include? so that you can just add "&loop"
if params.include?("loop")
p["loop"] = 1
p["playlist"] = video_id
end
# https://developers.google.com/youtube/player_parameters#rel
p["rel"] = 0 if params.include?("rel")
# https://developers.google.com/youtube/player_parameters#enablejsapi
p["enablejsapi"] = params["enablejsapi"] if params.include?("enablejsapi")
URI.encode_www_form(p)
end
def parse_timestring(string)
($1.to_i * 3600) + ($2.to_i * 60) + $3.to_i if string =~ /(\d+h)?(\d+m)?(\d+s?)?/
end
def params
return {} unless uri.query
# This mapping is necessary because CGI.parse returns a hash of keys to arrays.
# And *that* is necessary because querystrings support arrays, so they
# force you to deal with it to avoid security issues that would pop up
# if one day it suddenly gave you an array.
#
# However, we aren't interested. Just take the first one.
@params ||=
begin
p = {}
CGI.parse(uri.query).each { |k, v| p[k] = v.first }
p
end
rescue StandardError
{}
end
end
end
end