Skip to content
Snippets Groups Projects
Commit 717decea authored by Georgi's avatar Georgi
Browse files

[TASK] Merge youtube shorts support

parents 7da1b54d e522b3c9
No related branches found
Tags 8.1.1
1 merge request!15[TASK] move flexform field down, add max-height for shorts videos, add backend...
......@@ -35,6 +35,7 @@ use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Service\ImageService;
......@@ -76,6 +77,7 @@ class YoutubeController extends ActionController {
$aspectRatio = $this->settings['aspectRatio'] ?? '16:9';
$showApiResult = (bool) ($this->settings['showApiResult'] ?? FALSE);
$queryString = $this->settings['queryString'] ?? '';
$isShorts = (bool) ($this->settings['isShorts'] ?? FALSE);
$debugOutput = '';
$youtubeParameters = [
......@@ -88,6 +90,7 @@ class YoutubeController extends ActionController {
foreach ($filterIds as &$filterId) {
$filterId = trim($filterId, ' ');
}
unset($filterId);
// Get input values
......@@ -150,7 +153,8 @@ class YoutubeController extends ActionController {
$id,
$aspectRatio,
$thumbnailType,
$apiKey
$apiKey,
$isShorts
);
$jsonArray['items'] = $this->mapJsonArrayWithPossibleCustomThumbnails($jsonArray['items']);
......
......@@ -46,26 +46,28 @@ class PreviewService {
$pluginConfiguration = GeneralUtility::xml2array(
$row['pi_flexform'],
'T3DataStructure'
)['data']['sDEF']['lDEF'];
);
// Extract values from different sections of the Flexform, check for existence of 'sAppearance' and 'sBehavior'
$settingsDef = $pluginConfiguration['data']['sDEF']['lDEF'] ?? [];
// Fallback to sDEF if sAppearance doesn't exist
$settingsAppearance = $pluginConfiguration['data']['sAppearance']['lDEF'] ?? $settingsDef;
// Fallback to sDEF if sBehavior doesn't exist
$settingsBehavior = $pluginConfiguration['data']['sBehavior']['lDEF'] ?? $settingsDef;
$templateData = [
'youtubeId' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.id'),
'maxResults' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.maxResults'),
'showTitle' => (int) ($this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.showTitle') ?? 1),
'showDescription' => (int) ($this->passVDefOnKeyToTemplate(
$pluginConfiguration,
'settings.showDescription'
) ?? 1),
'disableLightbox' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.disableLightbox'),
'disableLightboxMobile' => $this->passVDefOnKeyToTemplate(
$pluginConfiguration,
'settings.disableLightboxMobile'
),
'aspectRatio' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.aspectRatio'),
'thumbnailType' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.thumbnailType'),
'thumbnailImagesCount' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.thumbnailImages'),
'showApiResult' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.showApiResult'),
'urlParameters' => $this->passVDefOnKeyToTemplate($pluginConfiguration, 'settings.urlParameters'),
'youtubeId' => $this->passVDefOnKeyToTemplate($settingsDef, 'settings.id'),
'maxResults' => $this->passVDefOnKeyToTemplate($settingsDef, 'settings.maxResults'),
'isShorts' => (int) ($this->passVDefOnKeyToTemplate($settingsDef, 'settings.isShorts') ?? 1),
'showTitle' => (int) ($this->passVDefOnKeyToTemplate($settingsAppearance, 'settings.showTitle') ?? 1),
'showDescription' => (int) ($this->passVDefOnKeyToTemplate($settingsAppearance, 'settings.showDescription') ?? 1),
'disableLightbox' => (int) ($this->passVDefOnKeyToTemplate($settingsBehavior, 'settings.disableLightbox') ?? 1),
'disableLightboxMobile' => (int) ($this->passVDefOnKeyToTemplate($settingsBehavior, 'settings.disableLightboxMobile') ?? 1),
'aspectRatio' => $this->passVDefOnKeyToTemplate($settingsAppearance, 'settings.aspectRatio'),
'thumbnailType' => $this->passVDefOnKeyToTemplate($settingsAppearance, 'settings.thumbnailType'),
'thumbnailImagesCount' => $this->passVDefOnKeyToTemplate($settingsAppearance, 'settings.thumbnailImages'),
'showApiResult' => $this->passVDefOnKeyToTemplate($settingsBehavior, 'settings.showApiResult'),
'urlParameters' => $this->passVDefOnKeyToTemplate($settingsDef, 'settings.urlParameters'),
'header' => $row['header'],
];
......
......@@ -75,6 +75,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
* @throws Exception
*/
......@@ -83,7 +84,8 @@ class YoutubeService {
$youtubeId = '',
$aspectRatio = '16:9',
$thumbnailType = 'byAspectRatio',
$apiKey = ''
$apiKey = '',
$isShorts = FALSE
): array {
if (count($jsonArray) <= 0) {
return $jsonArray;
......@@ -147,12 +149,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'],
];
}
......
......@@ -25,6 +25,17 @@
<eval>trim,required</eval>
</config>
</settings.id>
<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>
<default>0</default>
</config>
</settings.isShorts>
<settings.filterId>
<exclude>0</exclude>
<label>LLL:EXT:sg_youtube/Resources/Private/Language/locallang.xlf:flexform.filterIds
......
......@@ -137,6 +137,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.queryString" resname="flexform.queryString" approved="yes">
<source><![CDATA[Query String]]></source>
<target><![CDATA[Abfragetext]]></target>
......
......@@ -113,6 +113,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>
......
......@@ -163,9 +163,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>
......@@ -175,7 +173,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>
......@@ -185,10 +183,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>
......
......@@ -87,6 +87,15 @@
</td>
</tr>
<tr>
<th scope="row">
<f:translate key="flexform.isShorts" extensionName="SgYoutube"/>
</th>
<td>
<f:render partial="BooleanIcon" arguments="{boolValue: '{data.isShorts}'}"/>
</td>
</tr>
<tr>
<th scope="row">
<f:translate key="flexform.thumbnailType" extensionName="SgYoutube"/>
......
......@@ -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: 900px;
}
}
.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: 900px;
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