PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.5.3
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.5.3
4.6.1 4.6.0 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 5 months 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 10 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 4 months ago Gumroad.php 2 years ago InstagramFeed.php 2 months ago LinkedIn.php 2 years ago Meetup.php 6 months ago NRKRadio.php 2 years ago OneDrive.php 11 months ago OpenSea.php 10 months ago SelfHosted.php 2 months ago Spreaker.php 1 year ago TikTok.php 2 years ago Twitch.php 2 years ago Wistia.php 4 months ago Wrapper.php 3 years ago X.php 2 years ago Youtube.php 2 months ago index.html 7 years ago
Youtube.php
1100 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 = "https://www.youtube.com/embed/{$gallery->first_vid}?feature=oembed";
582 $main_iframe = "<div class='ep-first-video'><iframe width='{$params['maxwidth']}' height='{$params['maxheight']}' src='$rel' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen title='{$title}'></iframe></div>";
583 }
584
585 if (!apply_filters('embedpress/is_allow_rander', false) && isset($params['ytChannelLayout']) && ($params['ytChannelLayout'] == 'grid' || $params['ytChannelLayout'] == 'carousel')) {
586 return [];
587 }
588
589 if ($gallery->html) {
590 $styles = $this->styles($params, $this->getUrl());
591 $html_content = $main_iframe . $gallery->html . ' ' . $styles;
592
593 return [
594 "title" => $title,
595 "html" => "<div class='ep-player-wrap $channel_layout'>$html_content</div>",
596 ];
597 }
598
599 }
600 elseif ($this->isChannel() && empty($this->get_api_key()) && current_user_can('manage_options')) {
601 return [
602 "html" => "<div class='ep-player-wrap'>" . __('Please enter your YouTube API key to embed YouTube Channel.', 'embedpress') . "</div>",
603 ];
604 }
605
606 return $response;
607 }
608
609
610 public function get_channel_info() {
611 $api_key = $this->get_api_key();
612 $channel_id = $this->getChannel($this->url);
613 $channel_id = $channel_id['id'];
614
615
616 // Create a unique transient key based on the channel ID
617 $transient_key = 'youtube_channel_info_' . $channel_id;
618
619 // Attempt to get cached response
620 $channel_info = get_transient($transient_key);
621
622 if ($channel_info === false) {
623 // No cached response, make the API call
624 $endpoint = "https://www.googleapis.com/youtube/v3/channels?part=snippet%2Cstatistics&id=$channel_id&key=$api_key";
625 $response = wp_remote_get($endpoint);
626
627 if (is_wp_error($response)) {
628 return 'Error fetching channel info';
629 }
630
631 $body = wp_remote_retrieve_body($response);
632 $data = json_decode($body, true);
633
634 if (empty($data['items'])) {
635 return 'No channel information found';
636 }
637
638 $channel_info = $data['items'][0];
639
640
641 // Cache the response for 1 hour
642 set_transient($transient_key, $channel_info, HOUR_IN_SECONDS);
643 }
644
645 update_option('youtube_channel_info_'. md5($this->url), $channel_info);
646
647
648 return $channel_info;
649
650 }
651
652
653 public function get_layout() {
654 $params = $this->getParams();
655 return isset($params['ytChannelLayout']) ? $params['ytChannelLayout'] : '';
656 }
657
658 /**
659 * Undocumented function
660 *
661 * @param array $options
662 * @return object
663 */
664 public function get_gallery_page($options) {
665
666 $nextPageToken = '';
667 $prevPageToken = '';
668 $gallobj = new \stdClass();
669 $options = wp_parse_args($options, [
670 'playlistId' => '',
671 'pageToken' => '',
672 'pagesize' => $this->get_pagesize() ? $this->get_pagesize() : 6,
673 'currentpage' => '',
674 'columns' => 3,
675 'ytChannelLayout' => 'gallery',
676 'thumbnail' => 'medium',
677 'gallery' => true,
678 'autonext' => true,
679 'thumbplay' => true,
680 'apiKey' => $this->get_api_key(),
681 'hideprivate' => '',
682 ]);
683 $options['pagesize'] = $options['pagesize'] > 50 ? 50 : $options['pagesize'];
684 $options['pagesize'] = $options['pagesize'] < 1 ? 1 : $options['pagesize'];
685
686 if (empty($options['apiKey'])) {
687 $gallobj->html = $this->get_api_key_error_message();
688 return $gallobj;
689 }
690
691 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,status,contentDetails&playlistId=' . $options['playlistId']
692 . '&maxResults=' . $options['pagesize']
693 . '&key=' . $options['apiKey'];
694 if ($options['pageToken'] != null) {
695 $apiEndpoint .= '&pageToken=' . $options['pageToken'];
696 }
697
698
699
700 $transient_key = 'ep_embed_youtube_channel_' . md5($apiEndpoint);
701 $gallobj->transient_key = $transient_key;
702 $jsonResult = get_transient($transient_key);
703
704
705 if (empty($jsonResult)) {
706 $apiResult = wp_remote_get($apiEndpoint, array('timeout' => self::$curltimeout));
707 if (is_wp_error($apiResult)) {
708 $gallobj->html = $this->clean_api_error_html($apiResult->get_error_message(), true);
709 return $gallobj;
710 }
711 $jsonResult = json_decode($apiResult['body']);
712
713 if (empty($jsonResult->error)) {
714 set_transient($transient_key, $jsonResult, MINUTE_IN_SECONDS * 20);
715 }
716 else{
717 set_transient($transient_key, $jsonResult, 10);
718 }
719 }
720
721
722
723 if (isset($jsonResult->error)) {
724 if(!empty($jsonResult->error->errors[0]->reason) && $jsonResult->error->errors[0]->reason == 'playlistNotFound'){
725 $gallobj->html = $this->clean_api_error_html(__('There is nothing on the playlist.', 'embedpress'));
726 return $gallobj;
727 }
728 if (isset($jsonResult->error->message)) {
729 $gallobj->html = $this->clean_api_error_html($jsonResult->error->message);
730 return $gallobj;
731 }
732 $gallobj->html = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
733 return $gallobj;
734 }
735
736
737
738 $resultsPerPage = $jsonResult->pageInfo->resultsPerPage;
739 $totalResults = $jsonResult->pageInfo->totalResults;
740 $totalPages = ceil($totalResults / $resultsPerPage);
741
742 if (isset($jsonResult->nextPageToken)) {
743 $nextPageToken = $jsonResult->nextPageToken;
744 }
745
746 if (isset($jsonResult->prevPageToken)) {
747 $prevPageToken = $jsonResult->prevPageToken;
748 }
749
750
751 if (!empty($jsonResult->items) && is_array($jsonResult->items)) :
752 if($options['gallery'] === "false"){
753 $gallobj->html = "";
754 if(count($jsonResult->items) === 1){
755 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
756 }
757 return $gallobj;
758 }
759
760 if(count($jsonResult->items) === 1 && empty($nextPageToken) && empty($prevPageToken)){
761 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
762 $gallobj->html = "";
763 return $gallobj;
764 }
765
766 if (strpos($options['playlistId'], 'UU') === 0) {
767 // sort only channels
768 usort($jsonResult->items, array(self::class, 'compare_vid_date')); // sorts in place
769 }
770
771 ob_start();
772
773 ?>
774
775 <div class="ep-youtube__content__block" data-unique-id="<?php echo wp_rand(); ?>">
776 <div class="youtube__content__body">
777 <?php
778 ?>
779
780 <div class="content__wrap">
781
782 <?php
783
784 $data = $this->layout_data();
785
786
787 $channel_info = get_option('youtube_channel_info_'.md5($options['channel_url']));
788
789 $channelTitle = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null;
790 $channelThumb = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null;
791 $layout = $this->get_layout();
792
793
794 if($layout === 'gallery'){
795 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
796 }
797 else if($layout === 'grid'){
798 do_action('embedpress/youtube_grid_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
799 }
800 else if($layout === 'list'){
801 echo YoutubeLayout::create_list_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
802 }
803 else if($layout === 'carousel'){
804 do_action('embedpress/youtube_carousel_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
805 }
806 else{
807 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
808
809 }
810
811 ?>
812 <!--
813 <?php foreach ($jsonResult->items as $item) : ?>
814 <?php
815 $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null;
816 $thumbnail = $this->get_thumbnail_url($item, $options['thumbnail'], $privacyStatus);
817 $vid = $this->get_id($item);
818 if (empty($gallobj->first_vid)) {
819 $gallobj->first_vid = $vid;
820 }
821 if ($privacyStatus == 'private' && $options['hideprivate']) {
822 continue;
823 }
824 ?>
825 <div class="item" data-vid="<?php echo $vid; ?>">
826 <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>">
827 <div class="play-icon">
828 <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt="">
829 </div>
830 </div>
831 <div class="body">
832 <p><?php echo $item->snippet->title; ?></p>
833 </div>
834 </div>
835
836 <?php endforeach; ?> -->
837
838 <div class="item" style="height: 0"></div>
839 </div>
840
841
842 <?php
843 $layout = $this->get_layout();
844
845 if ($totalPages > 1 && $layout !== 'carousel') : ?>
846 <div class="ep-youtube__content__pagination <?php echo (empty($prevPageToken) && empty($nextPageToken)) ? ' hide ' : ''; ?>">
847 <div
848 class="ep-prev" <?php echo empty($prevPageToken) ? ' style="display:none" ' : ''; ?>
849 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
850 data-pagetoken="<?php echo esc_attr($prevPageToken) ?>"
851 data-pagesize="<?php echo intval($options['pagesize']) ?>"
852 >
853 <span><?php _e("Prev", "embedpress"); ?></span>
854 </div>
855 <div class="is_desktop_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
856 <?php
857
858 $numOfPages = $totalPages;
859 $renderedEllipses = false;
860
861 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
862
863 for($i = 1; $i<=$numOfPages; $i++)
864 {
865 //render pages 1 - 3
866 if($i < 4) {
867 //render link
868 $is_current = $i == (int)$currentPage? "active__current_page" : "";
869
870 echo wp_kses_post("<span class='page-number $is_current' data-page='$i'>$i</span>");
871
872 }
873
874 //render current page number
875 else if($i == (int)$currentPage) {
876 //render link
877 echo wp_kses_post('<span class="page-number active__current_page" data-page="'.$i.'">'.$i.'</span>');
878 //reset ellipses
879 $renderedEllipses = false;
880 }
881
882 //last page number
883 else if ($i >= $numOfPages - 1) {
884 //render link
885 echo wp_kses_post('<span class="page-number" data-page="'.$i.'">'.$i.'</span>');
886 }
887
888 //make sure you only do this once per ellipses group
889 else {
890 if (!$renderedEllipses){
891 print("...");
892 $renderedEllipses = true;
893 }
894 }
895 }
896 ?>
897
898 </div>
899
900 <div class="is_mobile_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
901 <?php
902
903 $numOfPages = $totalPages;
904 $renderedEllipses = false;
905
906 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
907
908 for($i = 1; $i<=$numOfPages; $i++)
909 {
910
911 //render current page number
912 if($i == (int)$currentPage) {
913 //render link
914 echo wp_kses_post('<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
915 //reset ellipses
916 $renderedEllipses = false;
917 }
918
919 //last page number
920 else if ($i >= $numOfPages ) {
921 //render link
922 echo wp_kses_post('...<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
923 }
924 }
925 ?>
926
927 </div>
928
929
930 <div
931 class="ep-next " <?php echo empty($nextPageToken) ? ' style="display:none" ' : ''; ?>
932 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
933 data-pagetoken="<?php echo esc_attr($nextPageToken) ?>"
934 data-pagesize="<?php echo intval($options['pagesize']) ?>"
935 >
936 <span><?php _e("Next ", "embedpress"); ?> </span>
937 </div>
938 </div>
939 <?php endif; ?>
940
941 <div class="ep-loader-wrap">
942 <div class="ep-loader"><img alt="loading" src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube/spin.gif'); ?>"></div>
943 </div>
944
945 </div>
946 </div>
947
948 <?php
949 $gallobj->html = ob_get_clean();
950 else:
951 $gallobj->html = $this->clean_api_error_html(__("There is nothing on the playlist.", 'embedpress'));
952 endif;
953
954 return $gallobj;
955 }
956
957 public function get_api_key_error_message(){
958 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>';
959 }
960
961 public function get_id($item){
962 $vid = isset($item->snippet->resourceId->videoId) ? $item->snippet->resourceId->videoId : null;
963 $vid = $vid ? $vid : (isset($item->id->videoId) ? $item->id->videoId : null);
964 $vid = $vid ? $vid : (isset($item->id) ? $item->id : null);
965 return $vid;
966 }
967 public function get_thumbnail_url($item, $quality, $privacyStatus) {
968 $url = "";
969 if ($privacyStatus == 'private') {
970 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/private.png';
971 } elseif (isset($item->snippet->thumbnails->{$quality}->url)) {
972 $url = $item->snippet->thumbnails->{$quality}->url;
973 } elseif (isset($item->snippet->thumbnails->medium->url)) {
974 $url = $item->snippet->thumbnails->medium->url;
975 } elseif (isset($item->snippet->thumbnails->default->url)) {
976 $url = $item->snippet->thumbnails->default->url;
977 } elseif (isset($item->snippet->thumbnails->high->url)) {
978 $url = $item->snippet->thumbnails->high->url;
979 } else {
980 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/deleted-video-thumb.png';
981 }
982 return $url;
983 }
984
985 public function compare_vid_date($a, $b) {
986 if ($a->snippet->publishedAt == $b->snippet->publishedAt) {
987 return 0;
988 }
989 return ($a->snippet->publishedAt > $b->snippet->publishedAt) ? -1 : 1;
990 }
991
992 public function clean_api_error($raw_message) {
993 return htmlspecialchars(strip_tags(preg_replace('@&key=[^& ]+@i', '&key=*******', $raw_message)));
994 }
995
996 public function clean_api_error_html($raw_message) {
997 $clean_html = '';
998 if ((defined('REST_REQUEST') && REST_REQUEST) || current_user_can('manage_options')) {
999 $clean_html = '<div>' . __('EmbedPress: ', 'embedpress') . $this->clean_api_error($raw_message) . '</div>';
1000 }
1001 return $clean_html;
1002 }
1003
1004 /** inline {@inheritdoc} */
1005 public function getFakeResponse() {
1006 preg_match('~v=([a-z0-9_\-]+)~i', (string) $this->url, $matches);
1007
1008 $embedUrl = 'https://www.youtube.com/embed/' . $matches['1'] . '?feature=oembed';
1009
1010 $attr = [];
1011 $attr[] = 'width="{width}"';
1012 $attr[] = 'height="{height}"';
1013 $attr[] = 'src="' . esc_url($embedUrl) . '"';
1014 $attr[] = 'frameborder="0"';
1015 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
1016 $attr[] = 'allowfullscreen';
1017
1018 return [
1019 'type' => 'video',
1020 'provider_name' => 'Youtube',
1021 'provider_url' => 'https://www.youtube.com',
1022 'title' => 'Unknown title',
1023 'html' => '<iframe ' . implode(' ', $attr) . '></iframe>',
1024 ];
1025 }
1026
1027 // public $num = 0;
1028
1029
1030
1031 public $x = 0;
1032
1033 public function styles($params, $url){
1034
1035 $uniqid = '.ose-youtube.ose-uid-'.md5($url);
1036
1037 ob_start();
1038 ?>
1039 <style>
1040
1041 <?php
1042 $attributes_data = $params;
1043
1044 $is_pagination = 'flex';
1045
1046 $gap = '30';
1047 $columns = '';
1048
1049 if (isset($attributes_data['ispagination']) && $attributes_data['ispagination']) {
1050 $is_pagination = 'none';
1051 }
1052 if(isset($attributes_data['gapbetweenvideos'])){
1053 $gap = $attributes_data['gapbetweenvideos'];
1054 }
1055 if(isset($attributes_data['columns'])){
1056 $columns = $attributes_data['columns'];
1057 }
1058
1059
1060 if(!empty($columns) && (int) $columns > 0){
1061 $repeatCol = 'repeat(auto-fit, minmax('.esc_html('calc('.(100 / (int) $columns).'% - '.$gap.'px)').', 1fr))';
1062 }
1063 else{
1064 $repeatCol = 'repeat(auto-fit, minmax(calc(250px - '.$gap.'px), 1fr))';
1065 }
1066
1067 ?>
1068 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1069 gap: <?php echo esc_html($gap); ?>px !important;
1070 margin-top: <?php echo esc_html($gap); ?>px !important;
1071 grid-template-columns: <?php echo $repeatCol; ?>;
1072 }
1073 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .ep-youtube__content__pagination {
1074 display: <?php echo esc_html($is_pagination); ?>!important;
1075 }
1076
1077 <?php echo esc_attr($uniqid); ?> .layout-list .ep-youtube__content__block .youtube__content__body .content__wrap{
1078 grid-template-columns: repeat(auto-fit, minmax(calc(100% - 30px), 1fr))!important;
1079 }
1080
1081 @media (max-width: 420px) {
1082 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1083 gap: 30px !important;
1084 }
1085 }
1086
1087 <?php
1088 if($is_pagination){
1089 echo esc_attr($uniqid) ?> {
1090 height: 100%!important;
1091 }
1092 <?php
1093 }
1094 ?>
1095 </style>
1096 <?php
1097 return ob_get_clean();
1098 }
1099 }
1100