diff --git a/Classes/Controller/YoutubeController.php b/Classes/Controller/YoutubeController.php index 4bc127b34503de6b7d4b5a61f5e589a72e7de7b1..0f4faa88aa2271e46fff3486678dc51cbce5156a 100644 --- a/Classes/Controller/YoutubeController.php +++ b/Classes/Controller/YoutubeController.php @@ -65,7 +65,7 @@ class YoutubeController extends ActionController { } $jsonArray['items'] = $this->youtubeService->mapArray( - $jsonArray['items'], $id, $aspectRatio, $thumbnailType + $jsonArray['items'], $id, $aspectRatio, $thumbnailType, $apiKey ); } catch (Exception $exception) { diff --git a/Classes/Service/YoutubeService.php b/Classes/Service/YoutubeService.php index 4a214501159ac8e9eaa6c4b0f00cba0298b635ed..e44da64d1056b1ae5f851a69d4e31238b06989bb 100644 --- a/Classes/Service/YoutubeService.php +++ b/Classes/Service/YoutubeService.php @@ -2,8 +2,6 @@ namespace SGalinski\SgYoutube\Service; -use InvalidArgumentException; - /*************************************************************** * Copyright notice * @@ -28,6 +26,16 @@ use InvalidArgumentException; * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use InvalidArgumentException; +use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException; +use TYPO3\CMS\Core\Context\LanguageAspect; +use TYPO3\CMS\Core\Http\ServerRequest; +use TYPO3\CMS\Core\Registry; +use TYPO3\CMS\Core\Site\Entity\Site; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\VersionNumberUtility; + /** * YouTube Helper Service */ @@ -37,6 +45,7 @@ class YoutubeService { const API_PLAYLIST = 'playlistItems'; const API_VIDEO = 'videos'; const API_PART = 'snippet'; + const API_PART_LOCALIZATIONS = 'localizations'; const API_ORDER_BY = 'date'; /** @@ -47,11 +56,26 @@ class YoutubeService { * @param string $youtubeId * @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 * @return array */ public function mapArray( - $jsonArray = [], $youtubeId = '', $aspectRatio = '16:9', $thumbnailType = 'byAspectRatio' + $jsonArray = [], $youtubeId = '', $aspectRatio = '16:9', $thumbnailType = 'byAspectRatio', $apiKey = '' ): array { + if (count($jsonArray) <= 0) { + return $jsonArray; + } + + // Normalize the data to video details. + if (strpos($youtubeId, 'UC') === 0 || strpos($youtubeId, 'PL') === 0) { + $result = $this->getDetailedVideoInformationForJsonArray($jsonArray, $apiKey, self::API_PART); + if (count($result) <= 0 || !isset($result['items'])) { + return $jsonArray; + } + + $jsonArray = $result['items']; + } + if (!in_array($thumbnailType, ['maxres', 'standard', 'high', 'medium', 'default', 'byAspectRatio'])) { $thumbnailType = 'byAspectRatio'; } @@ -60,22 +84,26 @@ class YoutubeService { $aspectRatio = '16:9'; } - $result = []; - foreach ($jsonArray as $field) { - if (strpos($youtubeId, 'UC') === 0) { - $youTubeIdFromArray = $field['id']['videoId']; + // Localization is just available from TYPO3 9.X.X + if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '9.0.0', '>=')) { + $context = GeneralUtility::makeInstance(Context::class); - // prevent the channel preview - if ($field['id']['kind'] !== 'youtube#video') { - continue; - } + try { + /** @var LanguageAspect $languageAspect */ + $languageAspect = $context->getAspect('language'); + $currentLanguageUid = $languageAspect->getId(); + } catch (AspectNotFoundException $e) { + // Can't be possible to land here, otherwise the whole frontend would be weird as hell.. + $currentLanguageUid = 0; + } - } elseif (strpos($youtubeId, 'PL') === 0) { - $youTubeIdFromArray = $field['snippet']['resourceId']['videoId']; - } else { - $youTubeIdFromArray = $field['id']; + if ($currentLanguageUid > 0 && $youtubeId && $apiKey) { + $jsonArray = $this->addLocalizationData($jsonArray, $apiKey, $currentLanguageUid); } + } + $result = []; + foreach ($jsonArray as $field) { $previewImage = []; $resolutionTypes = ['maxres', 'standard', 'high', 'medium', 'default']; if ($thumbnailType !== 'byAspectRatio') { @@ -105,23 +133,163 @@ class YoutubeService { 'title' => $field['snippet']['title'], 'description' => strip_tags($field['snippet']['description']), 'thumbnail' => $previewImage['url'], - 'url' => 'https://www.youtube.com/watch?v=' . $youTubeIdFromArray + 'url' => 'https://www.youtube.com/watch?v=' . $field['id'] ]; } return $result; } + /** + * Adds the localized title and description for each of the given entries in the jsonArray and returns it. + * + * @param array $jsonArray + * @param string $apiKey + * @param int $currentLanguageUid + * + * @return array + */ + protected function addLocalizationData(array $jsonArray, $apiKey, $currentLanguageUid): array { + if (!$apiKey || !$currentLanguageUid || count($jsonArray) <= 0) { + return $jsonArray; + } + + $localizationData = $this->getDetailedVideoInformationForJsonArray( + $jsonArray, $apiKey, self::API_PART_LOCALIZATIONS + ); + if (!isset($localizationData['items']) || count($localizationData['items']) <= 0) { + return $jsonArray; + } + + /** @var ServerRequest $request */ + $request = $GLOBALS['TYPO3_REQUEST']; + $attributes = $request->getAttributes(); + if (!isset($attributes['site'])) { + return $jsonArray; + } + + /** @var Site $site */ + $site = $attributes['site']; + $languages = $site->getLanguages(); + $currentSiteLanguage = $languages[$currentLanguageUid]; + if (!$currentSiteLanguage) { + return $jsonArray; + } + + $languageIsoCodes = [ + $currentSiteLanguage->getTwoLetterIsoCode() + ]; + foreach ($currentSiteLanguage->getFallbackLanguageIds() as $languageId) { + $siteLanguage = $languages[$languageId]; + if (!$siteLanguage) { + continue; + } + + $languageIsoCodes[] = $siteLanguage->getTwoLetterIsoCode(); + } + + foreach ($localizationData['items'] as $index => $localizationEntry) { + if (!isset($localizationEntry['localizations']) || count($localizationEntry['localizations']) <= 0) { + continue; + } + + $title = ''; + $description = ''; + $localizations = $localizationEntry['localizations']; + foreach ($languageIsoCodes as $languageIsoCode) { + if ($title && $description) { + break; + } + + if (!$title && isset($localizations[$languageIsoCode]['title'])) { + $title = $localizations[$languageIsoCode]['title']; + } + + if (!$description && isset($localizations[$languageIsoCode]['description'])) { + $description = $localizations[$languageIsoCode]['description']; + } + } + + if ($title) { + $jsonArray[$index]['snippet']['title'] = $title; + } + + if ($description) { + $jsonArray[$index]['snippet']['description'] = $description; + } + } + + return $jsonArray; + } + + /** + * Returns the detailed video information for the given json array and returns them as an array. + * + * @param array $jsonArray + * @param string $apiKey + * @param string $part + * + * @return array + */ + public function getDetailedVideoInformationForJsonArray(array $jsonArray, $apiKey, $part): array { + if (!$apiKey || count($jsonArray) <= 0) { + return $jsonArray; + } + + $apiUrl = self::API_URL . self::API_VIDEO; + $parameters = []; + $parameters['part'] = $part; + $parameters['key'] = $apiKey; + $query = http_build_query($parameters); + foreach ($jsonArray as $videoData) { + $videoId = 0; + if (isset($videoData['id'])) { + $videoId = $videoData['id']['videoId'] ?? $videoData['id']; + } + + if (!$videoId) { + continue; + } + + $query .= '&id=' . $videoId; + } + + $result = $this->getJsonAsArray('', '10', $apiKey, $apiUrl . '?' . $query); + if (!isset($result['items']) || count($result['items']) <= 0) { + return $jsonArray; + } + + return $result; + } + /** * Returns a JSON array with the video details (title, description, preview image, url) * * @param string $youtubeId * @param string $maxResults * @param string $apiKey + * @param string $url * @return array|mixed */ - public function getJsonAsArray($youtubeId = '', $maxResults = '10', $apiKey = '') { - $url = $this->getApiUrl($youtubeId, $maxResults, $apiKey); + public function getJsonAsArray($youtubeId = '', $maxResults = '10', $apiKey = '', $url = '') { + if (!$url) { + $url = $this->getApiUrl($youtubeId, $maxResults, $apiKey); + } + + $registry = GeneralUtility::makeInstance(Registry::class); + $currentDay = date('Y-m-d', $GLOBALS['EXEC_TIME']); + $cacheKey = sha1($url); + $disableYoutubeCache = (bool) GeneralUtility::_GP('disableYoutubeCache'); + if (!$disableYoutubeCache) { + $cachedResult = $registry->get('sg_youtube', $cacheKey); + if ($cachedResult) { + if ($cachedResult['CACHE_DATE'] === $currentDay) { + return $cachedResult; + } + + $registry->remove('sg_youtube', $cacheKey); + } + } if (function_exists('curl_init')) { $ch = curl_init(); @@ -164,6 +332,11 @@ class YoutubeService { throw new InvalidArgumentException('No items found.', 403); } + if (!$disableYoutubeCache) { + $jsonArray['CACHE_DATE'] = $currentDay; + $registry->set('sg_youtube', $cacheKey, $jsonArray); + } + return $jsonArray; } diff --git a/Configuration/TCA/Overrides/sys_template.php b/Configuration/TCA/Overrides/sys_template.php new file mode 100644 index 0000000000000000000000000000000000000000..2940744c5dd5ec35a4bf5e362bb6580ecca53546 --- /dev/null +++ b/Configuration/TCA/Overrides/sys_template.php @@ -0,0 +1,7 @@ +<?php + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile( + 'sg_youtube', + 'Configuration/TypoScript', + 'SG Youtube' +); diff --git a/Configuration/TCA/Overrides/tt_content.php b/Configuration/TCA/Overrides/tt_content.php index 64e24aa43ad2c23ec667bb24885e72a511177c80..c12eb263102d20dd0b4d942104183164def494d9 100644 --- a/Configuration/TCA/Overrides/tt_content.php +++ b/Configuration/TCA/Overrides/tt_content.php @@ -4,3 +4,9 @@ $GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['sgyoutube_yo \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue( 'sgyoutube_youtube', 'FILE:EXT:sg_youtube/Configuration/FlexForms/flexform_sgyoutube_youtube.xml' ); + +\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( + 'SGalinski.sg_youtube', + 'Youtube', + 'YouTube Videos' +); diff --git a/README.md b/README.md index 46e8c76fd43e6b15a4ef064cce2a2d084c860d25..24e870cd25a0e75757f4a89c281c068cba5c5c1e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Repository: https://gitlab.sgalinski.de/typo3/sg_youtube Please report bugs here: https://gitlab.sgalinski.de/typo3/sg_youtube -TYPO3 version: >8.7 +TYPO3 version: >9.5 ## Installation / Integration @@ -27,3 +27,33 @@ plugin.tx_sgyoutube { } } ``` + +### Registration for more than the free 10.000 quotas per day + +It's not 1 quota per 1 api call. Each api has it's own costs, which can be seen in the link below. + +Currently at the version 3.2.1 we are using the following apis: +- "search/list" for channel videos +- "playlistItems/list" for videos from a specific playlist +- "videos/list" for getting the details for each video and the localizations, if needed. + +The maximum quota costs would be "102" at the moment for rendering the latest videos from a channel with the video +details and translations. + +[Quota Calculator](https://developers.google.com/youtube/v3/determine_quota_cost) + +[YouTube API Services - Audit and Quota Extension Form](https://support.google.com/youtube/contact/yt_api_form?hl=en) + +#### Caching behaviour + +Because of the quota costs we implemented a caching for the calls for each day. The response from the apis will be +saved and used for 24 hours. Normally the site cache would do it, but it could be, that the cache will be cleared +multiple times in a row, or that the plugin is on an uncached page. The TYPO3 registry is used as a cache. The cleanup +will is handled on the fly. + +If the "?disableYoutubeCache=1" parameter is added to the url, this cache will be ignored as well. + +#### Possible way to solve the quota limit, if it's still reached + +You can use a different api-key for specific sites. You can implement a TypoScript page uid check and just change the +key from the "TypoScript integration" topic. diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000000000000000000000000000000000000..93c276900d0a58d93347fc195651d02145983279 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,3 @@ +## Version 4 Breaking Changes + +- Dropped TYPO3 8 support diff --git a/composer.json b/composer.json index 151b99970b1d4d18adb70793aac7b033fd8ce63c..c1c18084d388299bb92edccec656fa8733ec6da9 100644 --- a/composer.json +++ b/composer.json @@ -4,9 +4,9 @@ "description": "Embed YouTube Videos of a Playlist or Channel", "homepage": "https://www.sgalinski.de", "license": "GPL-2.0-or-later", - "version": "3.1.0", + "version": "4.0.0-dev", "require": { - "typo3/cms-core": "^8.7.24 || ^9.5.4" + "typo3/cms-core": "^9.5.4 || ^10.4.0" }, "require-dev": { "roave/security-advisories": "dev-master" diff --git a/ext_emconf.php b/ext_emconf.php index 4f762ff40db89782bfcbd702793b9a1069941968..803dba5ba0f8c198e6060fe37e61cfaa0df2d346 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -24,22 +24,22 @@ * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ -$EM_CONF[$_EXTKEY] = [ +$EM_CONF['sg_youtube'] = [ 'title' => 'YouTube Videos', 'description' => 'Embed YouTube Videos of a Channel or Playlist', 'category' => 'plugin', 'author' => 'Johannes Kreiner', 'author_email' => 'johannes@sgalinski.de', 'author_company' => 'sgalinski Internet Services (https://www.sgalinski.de)', - 'state' => 'stable', + 'state' => 'experimental', 'internal' => '', 'uploadfolder' => '0', 'createDirs' => '', 'clearCacheOnLoad' => 0, - 'version' => '3.1.0', + 'version' => '4.0.0-dev', 'constraints' => [ 'depends' => [ - 'typo3' => '8.7.0-9.5.99', + 'typo3' => '9.5.0-10.4.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/ext_localconf.php b/ext_localconf.php index aead3e8259fb552e76dd398107a8648e3c357239..85fbea420f1a637fb840644ac5eeaf18c85e5348 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -29,7 +29,7 @@ if (!defined('TYPO3_MODE')) { } \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin( - 'SGalinski.' . $_EXTKEY, + 'SGalinski.sg_youtube', 'Youtube', [ 'Youtube' => 'index', diff --git a/ext_tables.php b/ext_tables.php deleted file mode 100644 index fd513be1c21346d532b81a849728576c2bd70260..0000000000000000000000000000000000000000 --- a/ext_tables.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php -if (!defined('TYPO3_MODE')) { - die('Access denied.'); -} - -\TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerPlugin( - 'SGalinski.' . $_EXTKEY, - 'Youtube', - 'YouTube Videos' -); - -\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile($_EXTKEY, 'Configuration/TypoScript', 'SG Youtube');