PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / 4.5.4
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more v4.5.4
4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / EmbedPress / Providers / Youtube.php
embedpress / EmbedPress / Providers Last commit date
TemplateLayouts 1 month ago AirTable.php 1 year ago Boomplay.php 1 year ago Calendly.php 2 years ago Canva.php 1 year ago FITE.php 1 year ago GettyImages.php 9 months ago Giphy.php 2 years ago GitHub.php 3 years ago GoogleCalendar.php 8 months ago GoogleDocs.php 2 years ago GoogleDrive.php 2 years ago GoogleMaps.php 2 years ago GooglePhotos.php 3 months ago Gumroad.php 2 years ago InstagramFeed.php 1 month ago LinkedIn.php 2 years ago Meetup.php 6 months ago NRKRadio.php 2 years ago OneDrive.php 11 months ago OpenSea.php 9 months ago SelfHosted.php 2 months ago Spreaker.php 1 year ago TikTok.php 2 years ago Twitch.php 2 years ago Wistia.php 3 months ago Wrapper.php 3 years ago X.php 2 years ago Youtube.php 1 month ago index.html 7 years ago
Youtube.php
1103 lines
1 <?php
2
3 /**
4 * Youtube.php
5 *
6 * @package Embera
7 * @author Michael Pratt <yo@michael-pratt.com>
8 * @link http://www.michael-pratt.com/
9 *
10 * For the full copyright and license information, please view the LICENSE
11 * file that was distributed with this source code.
12 */
13
14 namespace EmbedPress\Providers;
15
16 use Embera\Provider\ProviderAdapter;
17 use Embera\Provider\ProviderInterface;
18 use Embera\Url;
19
20 use EmbedPress\Includes\Classes\Helper;
21
22 use EmbedPress\Providers\TemplateLayouts\YoutubeLayout;
23
24 /**
25 * youtube.com Provider
26 * @link https://youtube.com
27 * @link https://youtube-eng.googleblog.com/2009/10/oembed-support_9.html
28 */
29
30
31 class Youtube extends ProviderAdapter implements ProviderInterface {
32 /** inline {@inheritdoc} */
33 protected $shouldSendRequest = false;
34 public static $curltimeout = 30;
35 /** inline {@inheritdoc} */
36 protected $endpoint = 'https://www.youtube.com/oembed?format=json&scheme=https';
37 protected static $channel_endpoint = 'https://www.googleapis.com/youtube/v3/';
38 /** @var array Array with allowed params for the current Provider */
39 protected $allowedParams = [ 'maxwidth', 'maxheight', 'pagesize', 'thumbnail', 'gallery', 'hideprivate', 'columns', 'ispagination', 'gapbetweenvideos', 'ytChannelLayout' ];
40
41 /** inline {@inheritdoc} */
42 protected static $hosts = [
43 'm.youtube.com', 'youtube.com', 'youtu.be',
44 ];
45
46 /** inline {@inheritdoc} */
47 protected $httpsSupport = true;
48
49 public function getAllowedParams(){
50 return $this->allowedParams;
51 }
52
53 /** inline {@inheritdoc} */
54 public function validateUrl(Url $url) {
55 return (bool) (preg_match('~\/channel\/|\/c\/|\/user\/|\/@\w+|(?:https?:\/\/)?(?:www\.)?(?:youtube.com\/)(\w+)[^?\/]*$~i', (string) $url));
56 }
57
58 public function validateTYLiveUrl($url) {
59 return (bool) (preg_match('~(?:https?:\/\/)?(?:www\.)?(?:youtube.com\/(?:channel|c|user)\/\w+\/live|@\w+\/live)~i', (string) $url));
60 }
61
62 /** inline {@inheritdoc} */
63 public function normalizeUrl(Url $url) {
64 return $url;
65 }
66
67
68 public function isChannel($url = null) {
69 if (empty($url)) {
70 $url = $this->url;
71 }
72 $channel = $this->getChannel($url);
73 return !empty($channel['id']);
74 }
75
76 public function getChannel($url = null) {
77 $channelId = 'unknown_id'; // temporarily assigned a placeholder value for demonstration purposes
78
79 if (empty($url)) {
80 $url = $this->url;
81 }
82 preg_match('~\/(channel|c|user)\/(.+)~i', (string) $url, $matches);
83 if(empty($matches[1])){
84 preg_match('~(?:https?:\/\/)?(?:www\.)?(?:youtube.com\/)(\w+)[^?\/]*$~i', (string) $url, $matches);
85 if(!empty($matches[1])){
86 return [
87 "type" => 'user',
88 "id" => $matches[1],
89 ];
90 }
91 }
92
93
94
95 if(empty($matches[1])){
96 preg_match('~\/(@)(\w+)~i', (string) $url, $matches);
97 if(!empty($matches[1])){
98 $handle = $this->get_youtube_handler($this->url);
99 if(!empty($handle)){
100 $resolved = $this->get_channel_id_by_handler($handle);
101 if(!empty($resolved)){
102 $channelId = $resolved;
103 }
104 }
105 return [
106 "type" => 'user',
107 "id" => $channelId,
108 ];
109 }
110 }
111 return [
112 "type" => isset($matches[1]) ? $matches[1] : '',
113 "id" => isset($matches[2]) ? $matches[2] : '',
114 ];
115 }
116
117 /** inline {@inheritdoc} */
118 public function getEndpoint() {
119 if ($this->isChannel()) {
120 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/channels';
121 return $apiEndpoint;
122 }
123 return (string) $this->endpoint;
124 }
125
126 protected function get_api_key() {
127 $settings = (array) get_option(EMBEDPRESS_PLG_NAME . ':youtube', []);
128 return !empty($settings['api_key']) ? $settings['api_key'] : '';
129 }
130
131 protected function get_pagesize() {
132 $settings = (array) get_option(EMBEDPRESS_PLG_NAME . ':youtube', []);
133 return !empty($settings['pagesize']) ? $settings['pagesize'] : '';
134 }
135
136 /** inline {@inheritdoc} */
137 public function getParams() {
138 $params = parent::getParams();
139
140 if ($this->isChannel() && $this->get_api_key()) {
141 $channel = $this->getChannel();
142 $params['part'] = 'contentDetails,snippet';
143 $params['key'] = $this->get_api_key();
144 if ($channel['type'] == 'c') {
145 $params['forUsername'] = $channel['id'];
146 } else {
147 $params['id'] = $channel['id'];
148 }
149 unset($params['url']);
150 }
151 return $params;
152 }
153
154 /**
155 * Builds a valid Oembed query string based on the given parameters,
156 * Since this method uses the http_build_query function, there is no
157 * need to pass urlencoded parameters, http_build_query already does
158 * this for us.
159 *
160 * @param string $endpoint The Url to the Oembed endpoint
161 * @param array $params Parameters for the query string
162 * @return string
163 */
164
165 protected function constructUrl($endpoint, array $params = array())
166 {
167 $endpoint = self::$channel_endpoint . $endpoint;
168
169 return $endpoint . ((strpos($endpoint, '?') === false) ? '?' : '&') . http_build_query(array_filter($params));
170 }
171
172 public function getStaticResponse() {
173 $results = [
174 "title" => "",
175 "type" => "video",
176 'provider_name' => $this->getProviderName(),
177 "provider_url" => "https://www.youtube.com/",
178 'html' => '',
179 ];
180
181 $params = $this->getParams();
182
183 if (preg_match("/^https?:\/\/(?:www\.)?youtube\.com\/channel\/([\w-]+)\/live$/", $this->url, $matches) || $this->validateTYLiveUrl($this->url)) {
184
185 $channelId = '';
186
187 if(!empty($matches[1])){
188 $channelId = $matches[1];
189 }
190
191 if(empty($channelId)){
192 $handle = $this->get_youtube_handler($this->url);
193 if(!empty($handle)){
194 $resolved = $this->get_channel_id_by_handler($handle);
195 if(!empty($resolved)){
196 $channelId = $resolved;
197 }
198 }
199 }
200
201 if(empty($channelId)){
202 return $results;
203 }
204
205 $api_key = $this->get_api_key();
206
207 // When API key is available, check for active live stream
208 if (!empty($api_key)) {
209 $live_video_id = $this->get_live_video_id($channelId, $api_key);
210
211 if (!empty($live_video_id)) {
212 // Channel is live - embed the live video directly
213 $embedUrl = 'https://www.youtube.com/embed/' . $live_video_id . '?feature=oembed';
214 } else {
215 // Channel is not live - show the last completed stream or latest video
216 $last_video_id = $this->get_last_stream_or_video($channelId, $api_key);
217 if (!empty($last_video_id)) {
218 $embedUrl = 'https://www.youtube.com/embed/' . $last_video_id . '?feature=oembed';
219 } else {
220 // No video found at all
221 $embedUrl = 'https://www.youtube.com/embed/live_stream?channel=' . $channelId . '&feature=oembed';
222 }
223 }
224 } else {
225 // No API key - use live_stream endpoint as fallback
226 $embedUrl = 'https://www.youtube.com/embed/live_stream?channel='.$channelId.'&feature=oembed';
227 }
228
229 $attr = [];
230 $attr[] = 'width="'.esc_attr($params['maxwidth']).'"';
231 $attr[] = 'height="'.esc_attr($params['maxheight']).'"';
232 $attr[] = 'src="' . esc_url($embedUrl) . '"';
233 $attr[] = 'frameborder="0"';
234 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
235 $attr[] = 'allowfullscreen';
236 $attr[] = 'referrerpolicy="origin"';
237
238 $results['html'] = '<iframe ' . implode(' ', $attr) . '></iframe>';
239 }
240 else if($this->isChannel()){
241 $channel = $this->getChannelGallery();
242 $results = array_merge($results, $channel);
243 }
244
245 return $results;
246 }
247
248 public function getChannelPlaylist(){
249 $result = [
250 "playlistID" => '',
251 "title" => '',
252 ];
253 $channel = $this->getChannel();
254 $channel_url = $this->constructUrl('channels', $this->getParams());
255 $transient_key = 'ep_embed_youtube_channel_playlist_id_' . md5($channel_url);
256 $jsonResult = get_transient($transient_key);
257
258 if(!empty($jsonResult)){
259 return $jsonResult;
260 }
261
262 if($channel['type'] == 'user' || $channel['type'] == 'c'){
263 $this->getChannelIDbyUsername();
264 $channel_url = $this->constructUrl('channels', $this->getParams());
265 }
266
267 if (empty($this->get_api_key())) {
268 $result['error'] = true;
269 $result['html'] = $this->get_api_key_error_message();
270 return $result;
271 }
272
273 $apiResult = wp_remote_get($channel_url, array('timeout' => self::$curltimeout));
274 if (is_wp_error($apiResult)) {
275 $result['error'] = true;
276 $result['html'] = $this->clean_api_error_html($apiResult->get_error_message(), true);
277 set_transient($transient_key, $result, 10);
278 return $result;
279 }
280 $jsonResult = json_decode($apiResult['body']);
281
282
283 if (isset($jsonResult->error)) {
284 $result['error'] = true;
285 if (isset($jsonResult->error->message)) {
286 $result['html'] = $this->clean_api_error_html($jsonResult->error->message, true);
287 }
288 else{
289 $result['html'] = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
290 }
291 set_transient($transient_key, $result, MINUTE_IN_SECONDS);
292 return $result;
293 }
294 elseif(!empty($jsonResult->items[0]->contentDetails->relatedPlaylists->uploads)){
295 $result['playlistID'] = $jsonResult->items[0]->contentDetails->relatedPlaylists->uploads;
296 $result['title'] = isset($jsonResult->items[0]->snippet->title) ? $jsonResult->items[0]->snippet->title : '';
297 set_transient($transient_key, $result, DAY_IN_SECONDS);
298 }
299
300 return $result;
301 }
302
303 public function get_youtube_handler($url){
304 preg_match('/^https?:\/\/(?:www\.)?youtube\.com\/@([^\/?]+)/i', $url, $matches);
305
306 $handle_name = '';
307 if(!empty($matches[1])){
308 $handle_name = $matches[1];
309 }
310
311 return $handle_name;
312 }
313
314 public function getChannelIDbyUsername(){
315 $url = $this->getUrl();
316 $apiResult = wp_remote_get($url, array('timeout' => self::$curltimeout));
317
318 if (!is_wp_error($apiResult)) {
319 $channel_html = $apiResult['body'];
320 preg_match("/<meta\s+itemprop=[\"']channelId[\"']\s+content=[\"'](.*?)[\"']\/?>/", $channel_html, $matches);
321 if(!empty($matches[1])){
322 $url = "https://www.youtube.com/channel/{$matches[1]}";
323 $url = $this->normalizeUrl(new Url($url));
324 }
325 }
326 }
327
328
329 public function get_channel_id_by_handler($handle)
330 {
331 $transient_key = 'channel_id_' . md5($handle);
332 $channel_id = get_transient($transient_key);
333
334 if (false !== $channel_id && preg_match('/^UC[\w-]+$/', $channel_id)) {
335 return $channel_id;
336 }
337
338 // Prefer the YouTube Data API when a key is configured. Scraping
339 // youtube.com from EU server IPs gets bounced to consent.youtube.com
340 // and the response contains neither the canonical channel link nor
341 // externalId, so the regex fallback below silently returns ''.
342 $api_key = $this->get_api_key();
343 if (!empty($api_key)) {
344 $api_url = self::$channel_endpoint . 'channels?' . http_build_query([
345 'part' => 'id',
346 'forHandle' => '@' . $handle,
347 'key' => $api_key,
348 ]);
349 $api_response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
350 if (!is_wp_error($api_response)) {
351 $api_body = json_decode(wp_remote_retrieve_body($api_response));
352 if (!empty($api_body->items[0]->id) && preg_match('/^UC[\w-]+$/', $api_body->items[0]->id)) {
353 $channel_id = $api_body->items[0]->id;
354 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
355 return $channel_id;
356 }
357 }
358 }
359
360 $channel_handle = "https://www.youtube.com/@{$handle}";
361
362 // Bypass YouTube's EU consent redirect by sending the CONSENT cookie
363 // and a real browser UA. Without this the response is a consent page
364 // when the request originates from an EU IP.
365 $response = wp_remote_get($channel_handle, [
366 'timeout' => self::$curltimeout,
367 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
368 'headers' => [
369 'Accept-Language' => 'en-US,en;q=0.9',
370 'Cookie' => 'CONSENT=YES+cb.20210328-17-p0.en+FX+000',
371 ],
372 ]);
373
374 if (is_wp_error($response)) {
375 return '';
376 }
377
378 $body = wp_remote_retrieve_body($response);
379
380 if (empty($body)) {
381 return '';
382 }
383
384 // Try canonical link first (most reliable)
385 $pattern = '/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/([^"]{1,50})">/';
386 if (preg_match($pattern, $body, $matches)) {
387 $channel_id = $matches[1];
388 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
389 return $channel_id;
390 }
391
392 // Fallback: try externalId from page data
393 if (preg_match('/"externalId"\s*:\s*"(UC[a-zA-Z0-9_-]+)"/', $body, $matches)) {
394 $channel_id = $matches[1];
395 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
396 return $channel_id;
397 }
398
399 return '';
400 }
401
402 /**
403 * Check if a channel has an active live stream and return the video ID.
404 */
405 public function get_live_video_id($channel_id, $api_key) {
406 $transient_key = 'ep_yt_live_' . md5($channel_id);
407 $cached = get_transient($transient_key);
408
409 if (false !== $cached) {
410 return $cached;
411 }
412
413 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
414 'part' => 'id',
415 'channelId' => $channel_id,
416 'eventType' => 'live',
417 'type' => 'video',
418 'key' => $api_key,
419 ]);
420
421 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
422
423 if (is_wp_error($response)) {
424 return '';
425 }
426
427 $data = json_decode(wp_remote_retrieve_body($response));
428
429 if (!empty($data->items[0]->id->videoId)) {
430 $video_id = $data->items[0]->id->videoId;
431 set_transient($transient_key, $video_id, 2 * MINUTE_IN_SECONDS);
432 return $video_id;
433 }
434
435 // Cache empty result briefly to avoid repeated API calls
436 set_transient($transient_key, '', MINUTE_IN_SECONDS);
437 return '';
438 }
439
440 /**
441 * Get the last completed live stream or latest video from a channel.
442 * Tries completed streams first, falls back to latest upload.
443 */
444 public function get_last_stream_or_video($channel_id, $api_key) {
445 $transient_key = 'ep_yt_last_stream_' . md5($channel_id);
446 $cached = get_transient($transient_key);
447
448 if (false !== $cached) {
449 return $cached;
450 }
451
452 // First try: get the last completed live stream
453 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
454 'part' => 'id',
455 'channelId' => $channel_id,
456 'eventType' => 'completed',
457 'type' => 'video',
458 'order' => 'date',
459 'maxResults' => 1,
460 'key' => $api_key,
461 ]);
462
463 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
464
465 if (!is_wp_error($response)) {
466 $data = json_decode(wp_remote_retrieve_body($response));
467 if (!empty($data->items[0]->id->videoId)) {
468 $video_id = $data->items[0]->id->videoId;
469 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
470 return $video_id;
471 }
472 }
473
474 // Fallback: get the latest video from the channel's uploads playlist
475 $channel_url = self::$channel_endpoint . 'channels?' . http_build_query([
476 'part' => 'contentDetails',
477 'id' => $channel_id,
478 'key' => $api_key,
479 ]);
480
481 $ch_response = wp_remote_get($channel_url, ['timeout' => self::$curltimeout]);
482
483 if (!is_wp_error($ch_response)) {
484 $ch_data = json_decode(wp_remote_retrieve_body($ch_response));
485 $uploads_playlist = $ch_data->items[0]->contentDetails->relatedPlaylists->uploads ?? '';
486
487 if (!empty($uploads_playlist)) {
488 $playlist_url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
489 'part' => 'snippet',
490 'playlistId' => $uploads_playlist,
491 'maxResults' => 1,
492 'key' => $api_key,
493 ]);
494
495 $pl_response = wp_remote_get($playlist_url, ['timeout' => self::$curltimeout]);
496
497 if (!is_wp_error($pl_response)) {
498 $pl_data = json_decode(wp_remote_retrieve_body($pl_response));
499 if (!empty($pl_data->items[0]->snippet->resourceId->videoId)) {
500 $video_id = $pl_data->items[0]->snippet->resourceId->videoId;
501 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
502 return $video_id;
503 }
504 }
505 }
506 }
507
508 set_transient($transient_key, '', 2 * MINUTE_IN_SECONDS);
509 return '';
510 }
511
512 public function layout_data(){
513 $data = [];
514 $data['get_pagesize'] = $this->get_pagesize();
515 $data['get_api_key'] = $this->get_api_key();
516 $data['get_api_key_error_message'] = $this->get_api_key_error_message();
517 $data['get_channel_info'] = $this->get_channel_info();
518 $data['get_api_key'] = $this->get_api_key();
519 $data['curltimeout'] = self::$curltimeout;
520 $data['self::class'] = self::class;
521
522 return $data;
523
524 }
525
526
527 /** inline {@inheritdoc} */
528 public function getChannelGallery() {
529 $response = [];
530 $channel = $this->getChannelPlaylist();
531 if(!empty($channel['error'])){
532 return $channel;
533 }
534 if (!empty($channel["playlistID"])) {
535 $params = $this->getParams();
536 $the_playlist_id = $channel["playlistID"];
537 $rel = 'https://www.youtube.com/embed?listType=playlist&list=' . esc_attr($the_playlist_id);
538 $title = $channel['title'];
539 $main_iframe = "";
540 $gallery_args = [
541 'playlistId' => $the_playlist_id,
542 ];
543 if(!empty($params['pagesize'])){
544 $gallery_args['pagesize'] = $params['pagesize'];
545 }
546
547 $layout_data = $this->layout_data();
548
549
550 // $gallery = $this->get_gallery_page($gallery_args);
551
552 $channel_layout = 'layout-gallery';
553
554 $gallery = YoutubeLayout::create_youtube_layout($gallery_args, $layout_data, $channel_layout, $this->url);
555
556 if(isset($params['ytChannelLayout'])){
557 if($params['ytChannelLayout'] === 'gallery'){
558 $channel_layout = 'layout-gallery';
559
560 }
561 else if($params['ytChannelLayout'] === 'grid'){
562 $channel_layout = 'layout-grid';
563 }
564 else if($params['ytChannelLayout'] === 'list'){
565 $channel_layout = 'layout-list';
566
567 }
568 else if($params['ytChannelLayout'] === 'carousel'){
569 $channel_layout = 'layout-carousel';
570
571 }
572
573
574
575 $gallery = YoutubeLayout::create_youtube_layout($gallery_args, $layout_data, $params['ytChannelLayout'], $this->url);
576
577 }
578
579 $main_iframe = '';
580 if (!empty($gallery->first_vid) && empty($params['ytChannelLayout']) || $params['ytChannelLayout'] === 'gallery') {
581 $rel = esc_url("https://www.youtube.com/embed/{$gallery->first_vid}?feature=oembed");
582 $iframe_width = esc_attr($params['maxwidth']);
583 $iframe_height = esc_attr($params['maxheight']);
584 $iframe_title = esc_attr($title);
585 $main_iframe = "<div class='ep-first-video'><iframe width='{$iframe_width}' height='{$iframe_height}' src='{$rel}' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen title='{$iframe_title}'></iframe></div>";
586 }
587
588 if (!apply_filters('embedpress/is_allow_rander', false) && isset($params['ytChannelLayout']) && ($params['ytChannelLayout'] == 'grid' || $params['ytChannelLayout'] == 'carousel')) {
589 return [];
590 }
591
592 if ($gallery->html) {
593 $styles = $this->styles($params, $this->getUrl());
594 $html_content = $main_iframe . $gallery->html . ' ' . $styles;
595
596 return [
597 "title" => $title,
598 "html" => "<div class='ep-player-wrap $channel_layout'>$html_content</div>",
599 ];
600 }
601
602 }
603 elseif ($this->isChannel() && empty($this->get_api_key()) && current_user_can('manage_options')) {
604 return [
605 "html" => "<div class='ep-player-wrap'>" . __('Please enter your YouTube API key to embed YouTube Channel.', 'embedpress') . "</div>",
606 ];
607 }
608
609 return $response;
610 }
611
612
613 public function get_channel_info() {
614 $api_key = $this->get_api_key();
615 $channel_id = $this->getChannel($this->url);
616 $channel_id = $channel_id['id'];
617
618
619 // Create a unique transient key based on the channel ID
620 $transient_key = 'youtube_channel_info_' . $channel_id;
621
622 // Attempt to get cached response
623 $channel_info = get_transient($transient_key);
624
625 if ($channel_info === false) {
626 // No cached response, make the API call
627 $endpoint = "https://www.googleapis.com/youtube/v3/channels?part=snippet%2Cstatistics&id=$channel_id&key=$api_key";
628 $response = wp_remote_get($endpoint);
629
630 if (is_wp_error($response)) {
631 return 'Error fetching channel info';
632 }
633
634 $body = wp_remote_retrieve_body($response);
635 $data = json_decode($body, true);
636
637 if (empty($data['items'])) {
638 return 'No channel information found';
639 }
640
641 $channel_info = $data['items'][0];
642
643
644 // Cache the response for 1 hour
645 set_transient($transient_key, $channel_info, HOUR_IN_SECONDS);
646 }
647
648 update_option('youtube_channel_info_'. md5($this->url), $channel_info);
649
650
651 return $channel_info;
652
653 }
654
655
656 public function get_layout() {
657 $params = $this->getParams();
658 return isset($params['ytChannelLayout']) ? $params['ytChannelLayout'] : '';
659 }
660
661 /**
662 * Undocumented function
663 *
664 * @param array $options
665 * @return object
666 */
667 public function get_gallery_page($options) {
668
669 $nextPageToken = '';
670 $prevPageToken = '';
671 $gallobj = new \stdClass();
672 $options = wp_parse_args($options, [
673 'playlistId' => '',
674 'pageToken' => '',
675 'pagesize' => $this->get_pagesize() ? $this->get_pagesize() : 6,
676 'currentpage' => '',
677 'columns' => 3,
678 'ytChannelLayout' => 'gallery',
679 'thumbnail' => 'medium',
680 'gallery' => true,
681 'autonext' => true,
682 'thumbplay' => true,
683 'apiKey' => $this->get_api_key(),
684 'hideprivate' => '',
685 ]);
686 $options['pagesize'] = $options['pagesize'] > 50 ? 50 : $options['pagesize'];
687 $options['pagesize'] = $options['pagesize'] < 1 ? 1 : $options['pagesize'];
688
689 if (empty($options['apiKey'])) {
690 $gallobj->html = $this->get_api_key_error_message();
691 return $gallobj;
692 }
693
694 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,status,contentDetails&playlistId=' . $options['playlistId']
695 . '&maxResults=' . $options['pagesize']
696 . '&key=' . $options['apiKey'];
697 if ($options['pageToken'] != null) {
698 $apiEndpoint .= '&pageToken=' . $options['pageToken'];
699 }
700
701
702
703 $transient_key = 'ep_embed_youtube_channel_' . md5($apiEndpoint);
704 $gallobj->transient_key = $transient_key;
705 $jsonResult = get_transient($transient_key);
706
707
708 if (empty($jsonResult)) {
709 $apiResult = wp_remote_get($apiEndpoint, array('timeout' => self::$curltimeout));
710 if (is_wp_error($apiResult)) {
711 $gallobj->html = $this->clean_api_error_html($apiResult->get_error_message(), true);
712 return $gallobj;
713 }
714 $jsonResult = json_decode($apiResult['body']);
715
716 if (empty($jsonResult->error)) {
717 set_transient($transient_key, $jsonResult, MINUTE_IN_SECONDS * 20);
718 }
719 else{
720 set_transient($transient_key, $jsonResult, 10);
721 }
722 }
723
724
725
726 if (isset($jsonResult->error)) {
727 if(!empty($jsonResult->error->errors[0]->reason) && $jsonResult->error->errors[0]->reason == 'playlistNotFound'){
728 $gallobj->html = $this->clean_api_error_html(__('There is nothing on the playlist.', 'embedpress'));
729 return $gallobj;
730 }
731 if (isset($jsonResult->error->message)) {
732 $gallobj->html = $this->clean_api_error_html($jsonResult->error->message);
733 return $gallobj;
734 }
735 $gallobj->html = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
736 return $gallobj;
737 }
738
739
740
741 $resultsPerPage = $jsonResult->pageInfo->resultsPerPage;
742 $totalResults = $jsonResult->pageInfo->totalResults;
743 $totalPages = ceil($totalResults / $resultsPerPage);
744
745 if (isset($jsonResult->nextPageToken)) {
746 $nextPageToken = $jsonResult->nextPageToken;
747 }
748
749 if (isset($jsonResult->prevPageToken)) {
750 $prevPageToken = $jsonResult->prevPageToken;
751 }
752
753
754 if (!empty($jsonResult->items) && is_array($jsonResult->items)) :
755 if($options['gallery'] === "false"){
756 $gallobj->html = "";
757 if(count($jsonResult->items) === 1){
758 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
759 }
760 return $gallobj;
761 }
762
763 if(count($jsonResult->items) === 1 && empty($nextPageToken) && empty($prevPageToken)){
764 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
765 $gallobj->html = "";
766 return $gallobj;
767 }
768
769 if (strpos($options['playlistId'], 'UU') === 0) {
770 // sort only channels
771 usort($jsonResult->items, array(self::class, 'compare_vid_date')); // sorts in place
772 }
773
774 ob_start();
775
776 ?>
777
778 <div class="ep-youtube__content__block" data-unique-id="<?php echo wp_rand(); ?>">
779 <div class="youtube__content__body">
780 <?php
781 ?>
782
783 <div class="content__wrap">
784
785 <?php
786
787 $data = $this->layout_data();
788
789
790 $channel_info = get_option('youtube_channel_info_'.md5($options['channel_url']));
791
792 $channelTitle = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null;
793 $channelThumb = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null;
794 $layout = $this->get_layout();
795
796
797 if($layout === 'gallery'){
798 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
799 }
800 else if($layout === 'grid'){
801 do_action('embedpress/youtube_grid_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
802 }
803 else if($layout === 'list'){
804 echo YoutubeLayout::create_list_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
805 }
806 else if($layout === 'carousel'){
807 do_action('embedpress/youtube_carousel_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
808 }
809 else{
810 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
811
812 }
813
814 ?>
815 <!--
816 <?php foreach ($jsonResult->items as $item) : ?>
817 <?php
818 $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null;
819 $thumbnail = $this->get_thumbnail_url($item, $options['thumbnail'], $privacyStatus);
820 $vid = $this->get_id($item);
821 if (empty($gallobj->first_vid)) {
822 $gallobj->first_vid = $vid;
823 }
824 if ($privacyStatus == 'private' && $options['hideprivate']) {
825 continue;
826 }
827 ?>
828 <div class="item" data-vid="<?php echo $vid; ?>">
829 <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>">
830 <div class="play-icon">
831 <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt="">
832 </div>
833 </div>
834 <div class="body">
835 <p><?php echo esc_html($item->snippet->title); ?></p>
836 </div>
837 </div>
838
839 <?php endforeach; ?> -->
840
841 <div class="item" style="height: 0"></div>
842 </div>
843
844
845 <?php
846 $layout = $this->get_layout();
847
848 if ($totalPages > 1 && $layout !== 'carousel') : ?>
849 <div class="ep-youtube__content__pagination <?php echo (empty($prevPageToken) && empty($nextPageToken)) ? ' hide ' : ''; ?>">
850 <div
851 class="ep-prev" <?php echo empty($prevPageToken) ? ' style="display:none" ' : ''; ?>
852 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
853 data-pagetoken="<?php echo esc_attr($prevPageToken) ?>"
854 data-pagesize="<?php echo intval($options['pagesize']) ?>"
855 >
856 <span><?php _e("Prev", "embedpress"); ?></span>
857 </div>
858 <div class="is_desktop_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
859 <?php
860
861 $numOfPages = $totalPages;
862 $renderedEllipses = false;
863
864 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
865
866 for($i = 1; $i<=$numOfPages; $i++)
867 {
868 //render pages 1 - 3
869 if($i < 4) {
870 //render link
871 $is_current = $i == (int)$currentPage? "active__current_page" : "";
872
873 echo wp_kses_post("<span class='page-number $is_current' data-page='$i'>$i</span>");
874
875 }
876
877 //render current page number
878 else if($i == (int)$currentPage) {
879 //render link
880 echo wp_kses_post('<span class="page-number active__current_page" data-page="'.$i.'">'.$i.'</span>');
881 //reset ellipses
882 $renderedEllipses = false;
883 }
884
885 //last page number
886 else if ($i >= $numOfPages - 1) {
887 //render link
888 echo wp_kses_post('<span class="page-number" data-page="'.$i.'">'.$i.'</span>');
889 }
890
891 //make sure you only do this once per ellipses group
892 else {
893 if (!$renderedEllipses){
894 print("...");
895 $renderedEllipses = true;
896 }
897 }
898 }
899 ?>
900
901 </div>
902
903 <div class="is_mobile_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
904 <?php
905
906 $numOfPages = $totalPages;
907 $renderedEllipses = false;
908
909 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
910
911 for($i = 1; $i<=$numOfPages; $i++)
912 {
913
914 //render current page number
915 if($i == (int)$currentPage) {
916 //render link
917 echo wp_kses_post('<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
918 //reset ellipses
919 $renderedEllipses = false;
920 }
921
922 //last page number
923 else if ($i >= $numOfPages ) {
924 //render link
925 echo wp_kses_post('...<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
926 }
927 }
928 ?>
929
930 </div>
931
932
933 <div
934 class="ep-next " <?php echo empty($nextPageToken) ? ' style="display:none" ' : ''; ?>
935 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
936 data-pagetoken="<?php echo esc_attr($nextPageToken) ?>"
937 data-pagesize="<?php echo intval($options['pagesize']) ?>"
938 >
939 <span><?php _e("Next ", "embedpress"); ?> </span>
940 </div>
941 </div>
942 <?php endif; ?>
943
944 <div class="ep-loader-wrap">
945 <div class="ep-loader"><img alt="loading" src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube/spin.gif'); ?>"></div>
946 </div>
947
948 </div>
949 </div>
950
951 <?php
952 $gallobj->html = ob_get_clean();
953 else:
954 $gallobj->html = $this->clean_api_error_html(__("There is nothing on the playlist.", 'embedpress'));
955 endif;
956
957 return $gallobj;
958 }
959
960 public function get_api_key_error_message(){
961 return '<div>' . sprintf(__("EmbedPress: Please enter your YouTube API key at <a class='ep-link' href='%s' target='_blank' style='color: #5b4e96; text-decoration: none'>EmbedPress > Platforms > YouTube</a> to embed YouTube Channel.", "embedpress"), admin_url('?page=embedpress&page_type=youtube#api_key')) . '</div>';
962 }
963
964 public function get_id($item){
965 $vid = isset($item->snippet->resourceId->videoId) ? $item->snippet->resourceId->videoId : null;
966 $vid = $vid ? $vid : (isset($item->id->videoId) ? $item->id->videoId : null);
967 $vid = $vid ? $vid : (isset($item->id) ? $item->id : null);
968 return $vid;
969 }
970 public function get_thumbnail_url($item, $quality, $privacyStatus) {
971 $url = "";
972 if ($privacyStatus == 'private') {
973 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/private.png';
974 } elseif (isset($item->snippet->thumbnails->{$quality}->url)) {
975 $url = $item->snippet->thumbnails->{$quality}->url;
976 } elseif (isset($item->snippet->thumbnails->medium->url)) {
977 $url = $item->snippet->thumbnails->medium->url;
978 } elseif (isset($item->snippet->thumbnails->default->url)) {
979 $url = $item->snippet->thumbnails->default->url;
980 } elseif (isset($item->snippet->thumbnails->high->url)) {
981 $url = $item->snippet->thumbnails->high->url;
982 } else {
983 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/deleted-video-thumb.png';
984 }
985 return $url;
986 }
987
988 public function compare_vid_date($a, $b) {
989 if ($a->snippet->publishedAt == $b->snippet->publishedAt) {
990 return 0;
991 }
992 return ($a->snippet->publishedAt > $b->snippet->publishedAt) ? -1 : 1;
993 }
994
995 public function clean_api_error($raw_message) {
996 return htmlspecialchars(strip_tags(preg_replace('@&key=[^& ]+@i', '&key=*******', $raw_message)));
997 }
998
999 public function clean_api_error_html($raw_message) {
1000 $clean_html = '';
1001 if ((defined('REST_REQUEST') && REST_REQUEST) || current_user_can('manage_options')) {
1002 $clean_html = '<div>' . __('EmbedPress: ', 'embedpress') . $this->clean_api_error($raw_message) . '</div>';
1003 }
1004 return $clean_html;
1005 }
1006
1007 /** inline {@inheritdoc} */
1008 public function getFakeResponse() {
1009 preg_match('~v=([a-z0-9_\-]+)~i', (string) $this->url, $matches);
1010
1011 $embedUrl = 'https://www.youtube.com/embed/' . $matches['1'] . '?feature=oembed';
1012
1013 $attr = [];
1014 $attr[] = 'width="{width}"';
1015 $attr[] = 'height="{height}"';
1016 $attr[] = 'src="' . esc_url($embedUrl) . '"';
1017 $attr[] = 'frameborder="0"';
1018 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
1019 $attr[] = 'allowfullscreen';
1020
1021 return [
1022 'type' => 'video',
1023 'provider_name' => 'Youtube',
1024 'provider_url' => 'https://www.youtube.com',
1025 'title' => 'Unknown title',
1026 'html' => '<iframe ' . implode(' ', $attr) . '></iframe>',
1027 ];
1028 }
1029
1030 // public $num = 0;
1031
1032
1033
1034 public $x = 0;
1035
1036 public function styles($params, $url){
1037
1038 $uniqid = '.ose-youtube.ose-uid-'.md5($url);
1039
1040 ob_start();
1041 ?>
1042 <style>
1043
1044 <?php
1045 $attributes_data = $params;
1046
1047 $is_pagination = 'flex';
1048
1049 $gap = '30';
1050 $columns = '';
1051
1052 if (isset($attributes_data['ispagination']) && $attributes_data['ispagination']) {
1053 $is_pagination = 'none';
1054 }
1055 if(isset($attributes_data['gapbetweenvideos'])){
1056 $gap = $attributes_data['gapbetweenvideos'];
1057 }
1058 if(isset($attributes_data['columns'])){
1059 $columns = $attributes_data['columns'];
1060 }
1061
1062
1063 if(!empty($columns) && (int) $columns > 0){
1064 $repeatCol = 'repeat(auto-fit, minmax('.esc_html('calc('.(100 / (int) $columns).'% - '.$gap.'px)').', 1fr))';
1065 }
1066 else{
1067 $repeatCol = 'repeat(auto-fit, minmax(calc(250px - '.$gap.'px), 1fr))';
1068 }
1069
1070 ?>
1071 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1072 gap: <?php echo esc_html($gap); ?>px !important;
1073 margin-top: <?php echo esc_html($gap); ?>px !important;
1074 grid-template-columns: <?php echo $repeatCol; ?>;
1075 }
1076 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .ep-youtube__content__pagination {
1077 display: <?php echo esc_html($is_pagination); ?>!important;
1078 }
1079
1080 <?php echo esc_attr($uniqid); ?> .layout-list .ep-youtube__content__block .youtube__content__body .content__wrap{
1081 grid-template-columns: repeat(auto-fit, minmax(calc(100% - 30px), 1fr))!important;
1082 }
1083
1084 @media (max-width: 420px) {
1085 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1086 gap: 30px !important;
1087 }
1088 }
1089
1090 <?php
1091 if($is_pagination){
1092 echo esc_attr($uniqid) ?> {
1093 height: 100%!important;
1094 }
1095 <?php
1096 }
1097 ?>
1098 </style>
1099 <?php
1100 return ob_get_clean();
1101 }
1102 }
1103