Skip to content
Snippets Groups Projects
Verified Commit df123b26 authored by Kevin von Spiczak's avatar Kevin von Spiczak
Browse files

[FEATURE] add youtube shorts support

parent 65ff6f08
No related branches found
No related tags found
2 merge requests!15[TASK] move flexform field down, add max-height for shorts videos, add backend...,!14[TASK] move flexform field down, add max-height for shorts videos, add backend...
......@@ -74,6 +74,7 @@ class YoutubeController extends ActionController {
$thumbnailType = $this->settings['thumbnailType'] ?? 'byAspectRatio';
$aspectRatio = $this->settings['aspectRatio'] ?? '16:9';
$showApiResult = (bool) ($this->settings['showApiResult'] ?? FALSE);
$isShorts = (bool) ($this->settings['isShorts'] ?? FALSE);
$debugOutput = '';
$filterIds = explode(',', $filterIds);
......@@ -133,7 +134,8 @@ class YoutubeController extends ActionController {
$id,
$aspectRatio,
$thumbnailType,
$apiKey
$apiKey,
$isShorts
);
$jsonArray['items'] = $this->mapJsonArrayWithPossibleCustomThumbnails($jsonArray['items']);
......
......@@ -72,6 +72,7 @@ class YoutubeService {
* @param string $aspectRatio 16:9 (default) or 4:3 (ONLY used if byAspectRation is set as thumbnail type)
* @param string $thumbnailType maxres, standard, high, medium, default, byAspectRatio (default)
* @param string $apiKey
* @param bool $isShorts indicates whether the video is a "YouTube Shorts" video
* @return array
*/
public function mapArray(
......@@ -79,7 +80,8 @@ class YoutubeService {
$youtubeId = '',
$aspectRatio = '16:9',
$thumbnailType = 'byAspectRatio',
$apiKey = ''
$apiKey = '',
$isShorts = FALSE
): array {
if (count($jsonArray) <= 0) {
return $jsonArray;
......@@ -143,12 +145,16 @@ class YoutubeService {
}
}
$videoUrl = $isShorts
? 'https://youtube.com/shorts/' . $field['id']
: 'https://www.youtube.com/watch?v=' . $field['id'];
// Don't cache the preview URL. YouTube has a CDN and delivers much faster.
$result[] = [
'title' => $field['snippet']['title'],
'description' => strip_tags($field['snippet']['description']),
'thumbnail' => $previewImage['url'],
'url' => 'https://www.youtube.com/watch?v=' . $field['id'],
'url' => $videoUrl,
'publishedAt' => $field['snippet']['publishedAt'],
];
}
......
......@@ -16,6 +16,16 @@
<renderType>SgYoutubeLicenceCheck</renderType>
</config>
</settings.licenseStatus>
<settings.isShorts>
<exclude>0</exclude>
<label>LLL:EXT:sg_youtube/Resources/Private/Language/locallang.xlf:flexform.isShorts</label>
<description>
LLL:EXT:sg_youtube/Resources/Private/Language/locallang.xlf:flexform.isShorts.description
</description>
<config>
<type>check</type>
</config>
</settings.isShorts>
<settings.id>
<exclude>0</exclude>
<label>LLL:EXT:sg_youtube/Resources/Private/Language/locallang.xlf:flexform.id</label>
......
......@@ -129,6 +129,14 @@
<source><![CDATA[Specify as a list separated by commas (ABCD_12, EFGH_34).]]></source>
<target><![CDATA[Als Liste durch Kommas getrennt angeben (ABCD_12, EFGH_34).]]></target>
</trans-unit>
<trans-unit id="flexform.isShorts" resname="flexform.isShorts" approved="yes">
<source><![CDATA[Video is a "YouTube shorts" video (https://www.youtube.com/intl/en_EN/creators/shorts/)]]></source>
<target><![CDATA[Video ist ein "YouTube Shorts"-Video (https://www.youtube.com/intl/de_ALL/creators/shorts/)]]></target>
</trans-unit>
<trans-unit id="flexform.isShorts.description" resname="flexform.isShorts.description" approved="yes">
<source><![CDATA[Make sure to select a thumbnail type, with enough height and choose the 4:3 aspect ratio.]]></source>
<target><![CDATA[Stellen Sie sicher, dass Sie einen Thumbnail-Typ mit ausreichender Höhe und das Seitenverhältnis 4:3 wählen.)]]></target>
</trans-unit>
<trans-unit id="flexform.id" resname="flexform.id" approved="yes">
<source><![CDATA[ID of Channel (UC), Playlist (PL) or Single Video (youtube.com/watch?v=%ID%)]]></source>
<target><![CDATA[ID eines Channels (UC), einer Playlist (PL) oder eines einzelnen Videos (youtube.com/watch?v=%ID%)]]></target>
......@@ -251,4 +259,4 @@
</trans-unit>
</body>
</file>
</xliff>
\ No newline at end of file
</xliff>
......@@ -101,6 +101,12 @@
<trans-unit id="flexform.id" resname="flexform.id">
<source><![CDATA[ID of Channel (UC), Playlist (PL) or Single Video (youtube.com/watch?v=%ID%)]]></source>
</trans-unit>
<trans-unit id="flexform.isShorts" resname="flexform.isShorts">
<source><![CDATA[Video is a "YouTube shorts" video (https://www.youtube.com/intl/en_EN/creators/shorts/)]]></source>
</trans-unit>
<trans-unit id="flexform.isShorts.description" resname="flexform.isShorts.description">
<source><![CDATA[Make sure to select a thumbnail type, with enough height and choose the 4:3 aspect ratio.]]></source>
</trans-unit>
<trans-unit id="flexform.layout" resname="flexform.layout">
<source><![CDATA[Layout Style]]></source>
</trans-unit>
......@@ -190,4 +196,4 @@
</trans-unit>
</body>
</file>
</xliff>
\ No newline at end of file
</xliff>
......@@ -124,9 +124,7 @@
<f:section name="videoItem">
<f:variable name="urlParameters">{f:if(condition: '{settings.urlParameters}', then: '{settings.urlParameters}', else: '{settings.globalUrlParameters}')}</f:variable>
<f:variable name="feedItemUrl">
<vi:urlWithQueryParameters url="{feedItem.url}" parameters="{urlParameters}" />
</f:variable>
<f:variable name="feedItemUrl"><vi:urlWithQueryParameters url="{feedItem.url}" parameters="{urlParameters}" /></f:variable>
<f:variable name="feedItemId">{contentUid}-{settings.id}-{feedIterator}</f:variable>
<f:variable name="hasText"><f:if condition="{settings.showTitle} || {settings.showDescription}">1</f:if></f:variable>
<f:variable name="hasDescription"><f:if condition="{settings.showDescription} && {feedItem.description}">1</f:if></f:variable>
......@@ -136,7 +134,7 @@
<div class="sg-video__item {itemClasses}">
<f:if condition="{feedItem.thumbnail}">
<a class="sg-video-item overflow-hidden text-light sg-video__link position-relative {f:if(condition: '!{hasText} || {isRowsLayout}', then: 'rounded')} {f:if(condition: '{isRowsLayout}', then: 'col-12 col-sm-3 shadow', else: 'card-img-top')}" href="{feedItemUrl}" data-disable-lightbox="{settings.disableLightbox}" target="_blank" data-disable-lightbox-mobile="{settings.disableLightboxMobile}" data-additional-url-parameters="{urlParameters}" data-video-type="youtube">
<a class="sg-video-item overflow-hidden text-light sg-video__link position-relative {f:if(condition: '!{hasText} || {isRowsLayout}', then: 'rounded')} {f:if(condition: '{isRowsLayout}', then: 'col-12 col-sm-3 shadow', else: 'card-img-top')}" href="{feedItemUrl}" data-disable-lightbox="{settings.disableLightbox}" target="_blank" data-disable-lightbox-mobile="{settings.disableLightboxMobile}" data-additional-url-parameters="{urlParameters}" data-video-type="youtube" data-is-shorts="{settings.isShorts}">
<div class="sg-video__svg position-absolute top-50p start-50p translate-middle z-1">
<span class="sg-video__svg-inner d-flex shadow text-bg-black bg-opacity-50 rounded-circle justify-content-center p-2">
<vi:renderSvg color="currentColor" name="solid-play" width="24" height="24"></vi:renderSvg>
......@@ -146,10 +144,10 @@
<div class="overflow-hidden">
<f:if condition="{feedItem.thumbnailImageObject}">
<f:then>
<vi:picture class="sg-video__image object-fit-cover h-100 w-100" width="{thumbnailWidth}" height="{thumbnailHeight}" image="{feedItem.thumbnailImageObject}" alt="{feedItem.title}" />
<vi:picture class="sg-video__image object-fit-cover h-100 w-100{f:if(condition: '{settings.isShorts}', then: ' sg-video__image--shorts')}" width="{thumbnailWidth}" height="{thumbnailHeight}" image="{feedItem.thumbnailImageObject}" alt="{feedItem.title}" />
</f:then>
<f:else>
<vi:picture class="sg-video__image object-fit-cover h-100 w-100" width="{thumbnailWidth}" height="{thumbnailHeight}" image="{feedItem.thumbnail}" alt="{feedItem.title}" treatIdAsReference="TRUE" />
<vi:picture class="sg-video__image object-fit-cover h-100 w-100{f:if(condition: '{settings.isShorts}', then: ' sg-video__image--shorts')}" width="{thumbnailWidth}" height="{thumbnailHeight}" image="{feedItem.thumbnail}" alt="{feedItem.title}" treatIdAsReference="TRUE" />
</f:else>
</f:if>
</div>
......
......@@ -30,9 +30,10 @@ export default class SgVideoLightbox {
*/
static openLightbox(event) {
event.preventDefault();
const isShorts = event.target.closest('a')?.dataset?.isShorts === '1';
switch (event.target.closest('a')?.dataset?.videoType) {
case 'youtube': {
SgVideoLightbox.openYouTubeLightBox(event);
SgVideoLightbox.openYouTubeLightBox(event, isShorts);
break;
}
case 'vimeo': {
......@@ -51,9 +52,10 @@ export default class SgVideoLightbox {
*/
static disableLightbox(event) {
event.preventDefault();
const isShorts = event.target.closest('a')?.dataset?.isShorts === '1';
switch (event.target.closest('a')?.dataset?.videoType) {
case 'youtube': {
SgVideoLightbox.disableYouTubeLightbox(event);
SgVideoLightbox.disableYouTubeLightbox(event, isShorts);
break;
}
case 'vimeo': {
......@@ -91,9 +93,10 @@ export default class SgVideoLightbox {
/**
* Opens the lightbox with the youtube player (uses youtube-nocookie)
*
* @param event
* @param {Event} event
* @param {boolean} isShorts
*/
static openYouTubeLightBox(event) {
static openYouTubeLightBox(event, isShorts) {
let url = event.target.closest('.sg-video-item').href;
const videoId = SgVideoLightbox.getYouTubeVideoIdFromUrl(url);
url = `https://www.youtube-nocookie.com/embed/${videoId}`;
......@@ -102,10 +105,12 @@ export default class SgVideoLightbox {
event.target.closest('a').dataset.additionalUrlParameters,
);
const iframeClass = isShorts ? 'sg-video-iframe sg-video-youtube-iframe sg-video-youtube-shorts-iframe' : 'sg-video-iframe sg-video-youtube-iframe';
const instance = BasicLightbox.create(
`
<iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
class="sg-video-iframe sg-video-youtube-iframe" src="${url}"></iframe>
class="${iframeClass}" src="${url}"></iframe>
`,
{
closable: true,
......@@ -118,9 +123,11 @@ export default class SgVideoLightbox {
/**
* Replace the image with an iframe
*
* @param event
* @param {Event} event
* @param {boolean} isShorts
*
*/
static disableYouTubeLightbox(event) {
static disableYouTubeLightbox(event, isShorts) {
event.preventDefault();
const item = event.currentTarget;
item.classList.add('no-lightbox');
......@@ -130,16 +137,25 @@ export default class SgVideoLightbox {
item.dataset.additionalUrlParameters,
);
const videoImage = item.querySelector('.sg-video__image');
const height = videoImage.offsetHeight;
const width = videoImage.offsetWidth;
const originalWidth = videoImage.offsetWidth;
const originalHeight = videoImage.offsetHeight;
// Calculate iframe dimensions
let iframeWidth = originalWidth;
let iframeHeight = originalHeight;
if (isShorts) {
// Maintain 9:16 aspect ratio for Shorts
iframeWidth = originalWidth;
iframeHeight = Math.round((iframeWidth / 9) * 16);
}
const iframe = document.createElement('iframe');
iframe.width = width;
iframe.height = height;
iframe.width = iframeWidth;
iframe.height = iframeHeight;
iframe.style.border = 'none';
iframe.allowFullscreen = true;
iframe.allow =
'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.classList.add(isShorts ? 'sg-video-youtube-shorts-iframe' : '');
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.src = `https://www.youtube-nocookie.com/embed/${videoId}`;
if (videoImage.parentElement.nodeName.toLowerCase() === 'picture') {
......@@ -174,14 +190,14 @@ export default class SgVideoLightbox {
.createRange()
.createContextualFragment(
`<div class="sg-video-item sg-card-shadow" ` +
`style="height: ${height}px; width: ${width}px;">` +
`<div class="embed-container" style="padding-bottom: calc(${height} / ${width} * 100%);">` +
`<iframe ` +
`width="${width}" height="${height}" ` +
`src="${iframeUrl}&dnt=1" frameborder="0" ` +
`allow="autoplay; fullscreen; picture-in-picture" allowfullscreen>` +
`</iframe>` +
`</div></div>`,
`style="height: ${height}px; width: ${width}px;">` +
`<div class="embed-container" style="padding-bottom: calc(${height} / ${width} * 100%);">` +
`<iframe ` +
`width="${width}" height="${height}" ` +
`src="${iframeUrl}&dnt=1" frameborder="0" ` +
`allow="autoplay; fullscreen; picture-in-picture" allowfullscreen>` +
`</iframe>` +
`</div></div>`,
);
thumbnailElement?.replaceWith(nodes);
}
......@@ -205,22 +221,18 @@ export default class SgVideoLightbox {
* @return {string|null}
*/
static getYouTubeVideoIdFromUrl(url) {
let matches = url.match(/watch\?v=(.*)&list=(.*)/);
// Match standard YouTube URL or Shorts URL
const matches = url.match(/watch\?v=([^&?]*)(?:&list=([^&?]*))?|shorts\/([^&?/]+)/);
if (!matches) {
// check if the list parameter is missing
matches = url.match(/watch\?v=([^&?]*)/);
if (!matches) {
return null;
}
}
let [, videoString] = matches;
let queryParameterSeparator = '?';
if (matches[2]) {
videoString += `?list=${matches[2]}`;
queryParameterSeparator = '&';
return null;
}
return `${videoString + queryParameterSeparator}autoplay=1&rel=0`;
// Determine the video ID and construct the appropriate URL
const videoId = matches[1] || matches[3];
const listParam = matches[2] ? `?list=${matches[2]}` : '';
const autoplayParams = listParam ? '&autoplay=1&rel=0' : '?autoplay=1&rel=0';
return `${videoId}${listParam}${autoplayParams}`;
}
/**
......
......@@ -40,6 +40,14 @@ $basicLightbox__timing: ease !default;
.sg-video__image {
transition: transform 0.3s ease;
&--shorts {
height: auto;
aspect-ratio: 9 / 16;
object-fit: cover;
max-height: 100vh;
}
}
.sg-video__svg {
......@@ -146,6 +154,18 @@ iframe.sg-video-iframe,
aspect-ratio: 16/9;
}
iframe.sg-video-youtube-shorts-iframe {
aspect-ratio: 9 / 16;
&.sg-video-iframe {
width: 90vw;
max-width: 480px;
height: auto;
max-height: 100vh;
margin: 0 auto;
}
}
.basicLightbox .sg-cookie-optin-iframe-consent {
pointer-events: all;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment