plugins/consensual_youtube.py (90 lines of code) (raw):

#!/usr/bin/python -B # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # # consensual_youtube.py -- Pelican plugin that supports postponing loading # youtube resources until the user explicitly opts into playing the video # # Include a YouTube video with a 'youtube' tag and a youtube_id attribute: # # <youtube youtube_id="GU0SV_2tWkU"/> # # You can specify the 'id' to make it easier to apply additional CSS. This id # will also be carried over to the iframe when the div is replaced. # # The preview image will be taken from `img/{youtube_id}.jpg` in your content # folder. If no preview image is found there, it will be fetched from youtube # at site generation time. from os import path from urllib import request from pelican import contents, signals from bs4 import BeautifulSoup CSS_STYLE = ''' .yt-container { background-size: cover; background-position: center; } .yt-notice { padding: 0.5em; color: black; background-color: white; } .yt-notice::after { content: "Clicking to play this video establishes a connection with YouTube"; } ''' JS_SCRIPT = ''' var cachedPromise; function loadScript() { cachedPromise = cachedPromise || new Promise((resolve, reject) => { let script = document.createElement('script'); script.src = 'https://www.youtube.com/iframe_api'; script.addEventListener('load', resolve); script.addEventListener('error', (e) => reject(e)); document.body.appendChild(script); }); return cachedPromise; } function startPlayer(id, youtube_id) { loadScript().then(() => { window.YT.ready(function() { let player = new YT.Player(id, { videoId: youtube_id, playerVars: { 'playsinline': 1 }, events: { 'onReady': (event) => { event.target.playVideo(); } } }); }); }); } function addElement(e) { startPlayer(e.target.id, e.target.attributes['youtube_id'].value); } document .querySelectorAll('.yt-container') .forEach((video) => video.addEventListener('click', addElement)) ''' def generate_youtube(content): if isinstance(content, contents.Static): return soup = BeautifulSoup(content._content, 'html.parser') # pylint: disable=protected-access tags = soup.find_all('youtube') if not tags: return style = soup.new_tag('style') style.append(CSS_STYLE) soup.append(style) script = soup.new_tag('script') script.append(JS_SCRIPT) soup.append(script) for tag in tags: replace_tag(content.settings['PATH'], content.settings['OUTPUT_PATH'], soup, tag) content._content = soup.decode(formatter='html') # pylint: disable=protected-access def replace_tag(input_path, output_path, soup, tag): tag.name = 'div' if not tag.has_attr('youtube_id'): raise ValueError('Attribute "youtube_id" is mandatory for "youtube" tags') yt_id = tag['youtube_id'] # If a preview file is present in the input content directory, # use that and rely on Pelican to copy it to the output. If not, # fetch it from YouTube at site generation time and place it # straight into the output directory: preview = f'/img/yt_preview_{yt_id}.jpg' if not path.isfile(input_path + preview): request.urlretrieve(f'https://img.youtube.com/vi/{yt_id}/0.jpg', output_path + preview) # Default YouTube player size is 360p: player_width = 640 player_height = 360 if not tag.has_attr('id'): tag['id'] = f'yt-container-{yt_id}' tag['class'] = 'yt-container' tag['style'] = f"background-image: url('{preview}'); width: {player_width}px; height: {player_height}px;" warning = soup.new_tag('div') warning['class'] = "yt-notice" tag.append(warning) def register(): signals.content_object_init.connect(generate_youtube)