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/README.md b/README.md index ff266c44fd9b42d023d321bad061d529413744e9..24e870cd25a0e75757f4a89c281c068cba5c5c1e 100644 --- a/README.md +++ b/README.md @@ -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.