174 lines
5.1 KiB
Ruby
174 lines
5.1 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
module Onebox
|
||
|
module Engine
|
||
|
class YoutubeOnebox
|
||
|
include Engine
|
||
|
include StandardEmbed
|
||
|
|
||
|
matches_regexp(/^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
|
||
|
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='#{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
|
||
|
></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
|
||
|
></iframe>
|
||
|
HTML
|
||
|
else
|
||
|
# for channel pages
|
||
|
html = Onebox::Engine::AllowlistedGenericOnebox.new(@url, @timeout).to_html
|
||
|
return if Onebox::Helpers.blank?(html)
|
||
|
html.gsub!(/['"]\/\//, "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
|
||
|
# http://youtu.be/afyK1HSFfgw
|
||
|
if uri.host["youtu.be"]
|
||
|
id = uri.path[/\/([\w\-]+)/, 1]
|
||
|
return id if id
|
||
|
end
|
||
|
|
||
|
# https://www.youtube.com/embed/vsF0K3Ou1v0
|
||
|
if uri.path["/embed/"]
|
||
|
id = uri.path[/\/embed\/([\w\-]+)/, 1]
|
||
|
return id if id
|
||
|
end
|
||
|
|
||
|
# https://www.youtube.com/watch?v=Z0UISCEe52Y
|
||
|
params['v']
|
||
|
end
|
||
|
end
|
||
|
|
||
|
def list_id
|
||
|
@list_id ||= params['list']
|
||
|
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)
|
||
|
if string =~ /(\d+h)?(\d+m)?(\d+s?)?/
|
||
|
($1.to_i * 3600) + ($2.to_i * 60) + $3.to_i
|
||
|
end
|
||
|
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
|
||
|
{}
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|