PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more / 4.5.1
EmbedPress – PDF Embedder, Embed PDF viewer, YouTube Videos, 3D FlipBook, Social feeds & more v4.5.1
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 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 6 months 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 years 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 3 months ago index.html 7 years ago
Youtube.php
1071 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 $channel_handle = "https://www.youtube.com/@{$handle}";
339
340 $response = wp_remote_get($channel_handle, [
341 'timeout' => self::$curltimeout,
342 'user-agent' => 'Mozilla/5.0 (compatible; WordPress/' . get_bloginfo('version') . ')',
343 ]);
344
345 if (is_wp_error($response)) {
346 return '';
347 }
348
349 $body = wp_remote_retrieve_body($response);
350
351 if (empty($body)) {
352 return '';
353 }
354
355 // Try canonical link first (most reliable)
356 $pattern = '/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/([^"]{1,50})">/';
357 if (preg_match($pattern, $body, $matches)) {
358 $channel_id = $matches[1];
359 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
360 return $channel_id;
361 }
362
363 // Fallback: try externalId from page data
364 if (preg_match('/"externalId"\s*:\s*"(UC[a-zA-Z0-9_-]+)"/', $body, $matches)) {
365 $channel_id = $matches[1];
366 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
367 return $channel_id;
368 }
369
370 return '';
371 }
372
373 /**
374 * Check if a channel has an active live stream and return the video ID.
375 */
376 public function get_live_video_id($channel_id, $api_key) {
377 $transient_key = 'ep_yt_live_' . md5($channel_id);
378 $cached = get_transient($transient_key);
379
380 if (false !== $cached) {
381 return $cached;
382 }
383
384 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
385 'part' => 'id',
386 'channelId' => $channel_id,
387 'eventType' => 'live',
388 'type' => 'video',
389 'key' => $api_key,
390 ]);
391
392 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
393
394 if (is_wp_error($response)) {
395 return '';
396 }
397
398 $data = json_decode(wp_remote_retrieve_body($response));
399
400 if (!empty($data->items[0]->id->videoId)) {
401 $video_id = $data->items[0]->id->videoId;
402 set_transient($transient_key, $video_id, 2 * MINUTE_IN_SECONDS);
403 return $video_id;
404 }
405
406 // Cache empty result briefly to avoid repeated API calls
407 set_transient($transient_key, '', MINUTE_IN_SECONDS);
408 return '';
409 }
410
411 /**
412 * Get the last completed live stream or latest video from a channel.
413 * Tries completed streams first, falls back to latest upload.
414 */
415 public function get_last_stream_or_video($channel_id, $api_key) {
416 $transient_key = 'ep_yt_last_stream_' . md5($channel_id);
417 $cached = get_transient($transient_key);
418
419 if (false !== $cached) {
420 return $cached;
421 }
422
423 // First try: get the last completed live stream
424 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
425 'part' => 'id',
426 'channelId' => $channel_id,
427 'eventType' => 'completed',
428 'type' => 'video',
429 'order' => 'date',
430 'maxResults' => 1,
431 'key' => $api_key,
432 ]);
433
434 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
435
436 if (!is_wp_error($response)) {
437 $data = json_decode(wp_remote_retrieve_body($response));
438 if (!empty($data->items[0]->id->videoId)) {
439 $video_id = $data->items[0]->id->videoId;
440 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
441 return $video_id;
442 }
443 }
444
445 // Fallback: get the latest video from the channel's uploads playlist
446 $channel_url = self::$channel_endpoint . 'channels?' . http_build_query([
447 'part' => 'contentDetails',
448 'id' => $channel_id,
449 'key' => $api_key,
450 ]);
451
452 $ch_response = wp_remote_get($channel_url, ['timeout' => self::$curltimeout]);
453
454 if (!is_wp_error($ch_response)) {
455 $ch_data = json_decode(wp_remote_retrieve_body($ch_response));
456 $uploads_playlist = $ch_data->items[0]->contentDetails->relatedPlaylists->uploads ?? '';
457
458 if (!empty($uploads_playlist)) {
459 $playlist_url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
460 'part' => 'snippet',
461 'playlistId' => $uploads_playlist,
462 'maxResults' => 1,
463 'key' => $api_key,
464 ]);
465
466 $pl_response = wp_remote_get($playlist_url, ['timeout' => self::$curltimeout]);
467
468 if (!is_wp_error($pl_response)) {
469 $pl_data = json_decode(wp_remote_retrieve_body($pl_response));
470 if (!empty($pl_data->items[0]->snippet->resourceId->videoId)) {
471 $video_id = $pl_data->items[0]->snippet->resourceId->videoId;
472 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
473 return $video_id;
474 }
475 }
476 }
477 }
478
479 set_transient($transient_key, '', 2 * MINUTE_IN_SECONDS);
480 return '';
481 }
482
483 public function layout_data(){
484 $data = [];
485 $data['get_pagesize'] = $this->get_pagesize();
486 $data['get_api_key'] = $this->get_api_key();
487 $data['get_api_key_error_message'] = $this->get_api_key_error_message();
488 $data['get_channel_info'] = $this->get_channel_info();
489 $data['get_api_key'] = $this->get_api_key();
490 $data['curltimeout'] = self::$curltimeout;
491 $data['self::class'] = self::class;
492
493 return $data;
494
495 }
496
497
498 /** inline {@inheritdoc} */
499 public function getChannelGallery() {
500 $response = [];
501 $channel = $this->getChannelPlaylist();
502 if(!empty($channel['error'])){
503 return $channel;
504 }
505 if (!empty($channel["playlistID"])) {
506 $params = $this->getParams();
507 $the_playlist_id = $channel["playlistID"];
508 $rel = 'https://www.youtube.com/embed?listType=playlist&list=' . esc_attr($the_playlist_id);
509 $title = $channel['title'];
510 $main_iframe = "";
511 $gallery_args = [
512 'playlistId' => $the_playlist_id,
513 ];
514 if(!empty($params['pagesize'])){
515 $gallery_args['pagesize'] = $params['pagesize'];
516 }
517
518 $layout_data = $this->layout_data();
519
520
521 // $gallery = $this->get_gallery_page($gallery_args);
522
523 $channel_layout = 'layout-gallery';
524
525 $gallery = YoutubeLayout::create_youtube_layout($gallery_args, $layout_data, $channel_layout, $this->url);
526
527 if(isset($params['ytChannelLayout'])){
528 if($params['ytChannelLayout'] === 'gallery'){
529 $channel_layout = 'layout-gallery';
530
531 }
532 else if($params['ytChannelLayout'] === 'grid'){
533 $channel_layout = 'layout-grid';
534 }
535 else if($params['ytChannelLayout'] === 'list'){
536 $channel_layout = 'layout-list';
537
538 }
539 else if($params['ytChannelLayout'] === 'carousel'){
540 $channel_layout = 'layout-carousel';
541
542 }
543
544
545
546 $gallery = YoutubeLayout::create_youtube_layout($gallery_args, $layout_data, $params['ytChannelLayout'], $this->url);
547
548 }
549
550 $main_iframe = '';
551 if (!empty($gallery->first_vid) && empty($params['ytChannelLayout']) || $params['ytChannelLayout'] === 'gallery') {
552 $rel = "https://www.youtube.com/embed/{$gallery->first_vid}?feature=oembed";
553 $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>";
554 }
555
556 if (!apply_filters('embedpress/is_allow_rander', false) && isset($params['ytChannelLayout']) && ($params['ytChannelLayout'] == 'grid' || $params['ytChannelLayout'] == 'carousel')) {
557 return [];
558 }
559
560 if ($gallery->html) {
561 $styles = $this->styles($params, $this->getUrl());
562 $html_content = $main_iframe . $gallery->html . ' ' . $styles;
563
564 return [
565 "title" => $title,
566 "html" => "<div class='ep-player-wrap $channel_layout'>$html_content</div>",
567 ];
568 }
569
570 }
571 elseif ($this->isChannel() && empty($this->get_api_key()) && current_user_can('manage_options')) {
572 return [
573 "html" => "<div class='ep-player-wrap'>" . __('Please enter your YouTube API key to embed YouTube Channel.', 'embedpress') . "</div>",
574 ];
575 }
576
577 return $response;
578 }
579
580
581 public function get_channel_info() {
582 $api_key = $this->get_api_key();
583 $channel_id = $this->getChannel($this->url);
584 $channel_id = $channel_id['id'];
585
586
587 // Create a unique transient key based on the channel ID
588 $transient_key = 'youtube_channel_info_' . $channel_id;
589
590 // Attempt to get cached response
591 $channel_info = get_transient($transient_key);
592
593 if ($channel_info === false) {
594 // No cached response, make the API call
595 $endpoint = "https://www.googleapis.com/youtube/v3/channels?part=snippet%2Cstatistics&id=$channel_id&key=$api_key";
596 $response = wp_remote_get($endpoint);
597
598 if (is_wp_error($response)) {
599 return 'Error fetching channel info';
600 }
601
602 $body = wp_remote_retrieve_body($response);
603 $data = json_decode($body, true);
604
605 if (empty($data['items'])) {
606 return 'No channel information found';
607 }
608
609 $channel_info = $data['items'][0];
610
611
612 // Cache the response for 1 hour
613 set_transient($transient_key, $channel_info, HOUR_IN_SECONDS);
614 }
615
616 update_option('youtube_channel_info_'. md5($this->url), $channel_info);
617
618
619 return $channel_info;
620
621 }
622
623
624 public function get_layout() {
625 $params = $this->getParams();
626 return isset($params['ytChannelLayout']) ? $params['ytChannelLayout'] : '';
627 }
628
629 /**
630 * Undocumented function
631 *
632 * @param array $options
633 * @return object
634 */
635 public function get_gallery_page($options) {
636
637 $nextPageToken = '';
638 $prevPageToken = '';
639 $gallobj = new \stdClass();
640 $options = wp_parse_args($options, [
641 'playlistId' => '',
642 'pageToken' => '',
643 'pagesize' => $this->get_pagesize() ? $this->get_pagesize() : 6,
644 'currentpage' => '',
645 'columns' => 3,
646 'ytChannelLayout' => 'gallery',
647 'thumbnail' => 'medium',
648 'gallery' => true,
649 'autonext' => true,
650 'thumbplay' => true,
651 'apiKey' => $this->get_api_key(),
652 'hideprivate' => '',
653 ]);
654 $options['pagesize'] = $options['pagesize'] > 50 ? 50 : $options['pagesize'];
655 $options['pagesize'] = $options['pagesize'] < 1 ? 1 : $options['pagesize'];
656
657 if (empty($options['apiKey'])) {
658 $gallobj->html = $this->get_api_key_error_message();
659 return $gallobj;
660 }
661
662 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,status,contentDetails&playlistId=' . $options['playlistId']
663 . '&maxResults=' . $options['pagesize']
664 . '&key=' . $options['apiKey'];
665 if ($options['pageToken'] != null) {
666 $apiEndpoint .= '&pageToken=' . $options['pageToken'];
667 }
668
669
670
671 $transient_key = 'ep_embed_youtube_channel_' . md5($apiEndpoint);
672 $gallobj->transient_key = $transient_key;
673 $jsonResult = get_transient($transient_key);
674
675
676 if (empty($jsonResult)) {
677 $apiResult = wp_remote_get($apiEndpoint, array('timeout' => self::$curltimeout));
678 if (is_wp_error($apiResult)) {
679 $gallobj->html = $this->clean_api_error_html($apiResult->get_error_message(), true);
680 return $gallobj;
681 }
682 $jsonResult = json_decode($apiResult['body']);
683
684 if (empty($jsonResult->error)) {
685 set_transient($transient_key, $jsonResult, MINUTE_IN_SECONDS * 20);
686 }
687 else{
688 set_transient($transient_key, $jsonResult, 10);
689 }
690 }
691
692
693
694 if (isset($jsonResult->error)) {
695 if(!empty($jsonResult->error->errors[0]->reason) && $jsonResult->error->errors[0]->reason == 'playlistNotFound'){
696 $gallobj->html = $this->clean_api_error_html(__('There is nothing on the playlist.', 'embedpress'));
697 return $gallobj;
698 }
699 if (isset($jsonResult->error->message)) {
700 $gallobj->html = $this->clean_api_error_html($jsonResult->error->message);
701 return $gallobj;
702 }
703 $gallobj->html = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
704 return $gallobj;
705 }
706
707
708
709 $resultsPerPage = $jsonResult->pageInfo->resultsPerPage;
710 $totalResults = $jsonResult->pageInfo->totalResults;
711 $totalPages = ceil($totalResults / $resultsPerPage);
712
713 if (isset($jsonResult->nextPageToken)) {
714 $nextPageToken = $jsonResult->nextPageToken;
715 }
716
717 if (isset($jsonResult->prevPageToken)) {
718 $prevPageToken = $jsonResult->prevPageToken;
719 }
720
721
722 if (!empty($jsonResult->items) && is_array($jsonResult->items)) :
723 if($options['gallery'] === "false"){
724 $gallobj->html = "";
725 if(count($jsonResult->items) === 1){
726 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
727 }
728 return $gallobj;
729 }
730
731 if(count($jsonResult->items) === 1 && empty($nextPageToken) && empty($prevPageToken)){
732 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
733 $gallobj->html = "";
734 return $gallobj;
735 }
736
737 if (strpos($options['playlistId'], 'UU') === 0) {
738 // sort only channels
739 usort($jsonResult->items, array(self::class, 'compare_vid_date')); // sorts in place
740 }
741
742 ob_start();
743
744 ?>
745
746 <div class="ep-youtube__content__block" data-unique-id="<?php echo wp_rand(); ?>">
747 <div class="youtube__content__body">
748 <?php
749 ?>
750
751 <div class="content__wrap">
752
753 <?php
754
755 $data = $this->layout_data();
756
757
758 $channel_info = get_option('youtube_channel_info_'.md5($options['channel_url']));
759
760 $channelTitle = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null;
761 $channelThumb = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null;
762 $layout = $this->get_layout();
763
764
765 if($layout === 'gallery'){
766 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
767 }
768 else if($layout === 'grid'){
769 do_action('embedpress/youtube_grid_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
770 }
771 else if($layout === 'list'){
772 echo YoutubeLayout::create_list_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
773 }
774 else if($layout === 'carousel'){
775 do_action('embedpress/youtube_carousel_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
776 }
777 else{
778 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
779
780 }
781
782 ?>
783 <!--
784 <?php foreach ($jsonResult->items as $item) : ?>
785 <?php
786 $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null;
787 $thumbnail = $this->get_thumbnail_url($item, $options['thumbnail'], $privacyStatus);
788 $vid = $this->get_id($item);
789 if (empty($gallobj->first_vid)) {
790 $gallobj->first_vid = $vid;
791 }
792 if ($privacyStatus == 'private' && $options['hideprivate']) {
793 continue;
794 }
795 ?>
796 <div class="item" data-vid="<?php echo $vid; ?>">
797 <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>">
798 <div class="play-icon">
799 <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt="">
800 </div>
801 </div>
802 <div class="body">
803 <p><?php echo $item->snippet->title; ?></p>
804 </div>
805 </div>
806
807 <?php endforeach; ?> -->
808
809 <div class="item" style="height: 0"></div>
810 </div>
811
812
813 <?php
814 $layout = $this->get_layout();
815
816 if ($totalPages > 1 && $layout !== 'carousel') : ?>
817 <div class="ep-youtube__content__pagination <?php echo (empty($prevPageToken) && empty($nextPageToken)) ? ' hide ' : ''; ?>">
818 <div
819 class="ep-prev" <?php echo empty($prevPageToken) ? ' style="display:none" ' : ''; ?>
820 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
821 data-pagetoken="<?php echo esc_attr($prevPageToken) ?>"
822 data-pagesize="<?php echo intval($options['pagesize']) ?>"
823 >
824 <span><?php _e("Prev", "embedpress"); ?></span>
825 </div>
826 <div class="is_desktop_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
827 <?php
828
829 $numOfPages = $totalPages;
830 $renderedEllipses = false;
831
832 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
833
834 for($i = 1; $i<=$numOfPages; $i++)
835 {
836 //render pages 1 - 3
837 if($i < 4) {
838 //render link
839 $is_current = $i == (int)$currentPage? "active__current_page" : "";
840
841 echo wp_kses_post("<span class='page-number $is_current' data-page='$i'>$i</span>");
842
843 }
844
845 //render current page number
846 else if($i == (int)$currentPage) {
847 //render link
848 echo wp_kses_post('<span class="page-number active__current_page" data-page="'.$i.'">'.$i.'</span>');
849 //reset ellipses
850 $renderedEllipses = false;
851 }
852
853 //last page number
854 else if ($i >= $numOfPages - 1) {
855 //render link
856 echo wp_kses_post('<span class="page-number" data-page="'.$i.'">'.$i.'</span>');
857 }
858
859 //make sure you only do this once per ellipses group
860 else {
861 if (!$renderedEllipses){
862 print("...");
863 $renderedEllipses = true;
864 }
865 }
866 }
867 ?>
868
869 </div>
870
871 <div class="is_mobile_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
872 <?php
873
874 $numOfPages = $totalPages;
875 $renderedEllipses = false;
876
877 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
878
879 for($i = 1; $i<=$numOfPages; $i++)
880 {
881
882 //render current page number
883 if($i == (int)$currentPage) {
884 //render link
885 echo wp_kses_post('<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
886 //reset ellipses
887 $renderedEllipses = false;
888 }
889
890 //last page number
891 else if ($i >= $numOfPages ) {
892 //render link
893 echo wp_kses_post('...<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
894 }
895 }
896 ?>
897
898 </div>
899
900
901 <div
902 class="ep-next " <?php echo empty($nextPageToken) ? ' style="display:none" ' : ''; ?>
903 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
904 data-pagetoken="<?php echo esc_attr($nextPageToken) ?>"
905 data-pagesize="<?php echo intval($options['pagesize']) ?>"
906 >
907 <span><?php _e("Next ", "embedpress"); ?> </span>
908 </div>
909 </div>
910 <?php endif; ?>
911
912 <div class="ep-loader-wrap">
913 <div class="ep-loader"><img alt="loading" src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube/spin.gif'); ?>"></div>
914 </div>
915
916 </div>
917 </div>
918
919 <?php
920 $gallobj->html = ob_get_clean();
921 else:
922 $gallobj->html = $this->clean_api_error_html(__("There is nothing on the playlist.", 'embedpress'));
923 endif;
924
925 return $gallobj;
926 }
927
928 public function get_api_key_error_message(){
929 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>';
930 }
931
932 public function get_id($item){
933 $vid = isset($item->snippet->resourceId->videoId) ? $item->snippet->resourceId->videoId : null;
934 $vid = $vid ? $vid : (isset($item->id->videoId) ? $item->id->videoId : null);
935 $vid = $vid ? $vid : (isset($item->id) ? $item->id : null);
936 return $vid;
937 }
938 public function get_thumbnail_url($item, $quality, $privacyStatus) {
939 $url = "";
940 if ($privacyStatus == 'private') {
941 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/private.png';
942 } elseif (isset($item->snippet->thumbnails->{$quality}->url)) {
943 $url = $item->snippet->thumbnails->{$quality}->url;
944 } elseif (isset($item->snippet->thumbnails->medium->url)) {
945 $url = $item->snippet->thumbnails->medium->url;
946 } elseif (isset($item->snippet->thumbnails->default->url)) {
947 $url = $item->snippet->thumbnails->default->url;
948 } elseif (isset($item->snippet->thumbnails->high->url)) {
949 $url = $item->snippet->thumbnails->high->url;
950 } else {
951 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/deleted-video-thumb.png';
952 }
953 return $url;
954 }
955
956 public function compare_vid_date($a, $b) {
957 if ($a->snippet->publishedAt == $b->snippet->publishedAt) {
958 return 0;
959 }
960 return ($a->snippet->publishedAt > $b->snippet->publishedAt) ? -1 : 1;
961 }
962
963 public function clean_api_error($raw_message) {
964 return htmlspecialchars(strip_tags(preg_replace('@&key=[^& ]+@i', '&key=*******', $raw_message)));
965 }
966
967 public function clean_api_error_html($raw_message) {
968 $clean_html = '';
969 if ((defined('REST_REQUEST') && REST_REQUEST) || current_user_can('manage_options')) {
970 $clean_html = '<div>' . __('EmbedPress: ', 'embedpress') . $this->clean_api_error($raw_message) . '</div>';
971 }
972 return $clean_html;
973 }
974
975 /** inline {@inheritdoc} */
976 public function getFakeResponse() {
977 preg_match('~v=([a-z0-9_\-]+)~i', (string) $this->url, $matches);
978
979 $embedUrl = 'https://www.youtube.com/embed/' . $matches['1'] . '?feature=oembed';
980
981 $attr = [];
982 $attr[] = 'width="{width}"';
983 $attr[] = 'height="{height}"';
984 $attr[] = 'src="' . esc_url($embedUrl) . '"';
985 $attr[] = 'frameborder="0"';
986 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
987 $attr[] = 'allowfullscreen';
988
989 return [
990 'type' => 'video',
991 'provider_name' => 'Youtube',
992 'provider_url' => 'https://www.youtube.com',
993 'title' => 'Unknown title',
994 'html' => '<iframe ' . implode(' ', $attr) . '></iframe>',
995 ];
996 }
997
998 // public $num = 0;
999
1000
1001
1002 public $x = 0;
1003
1004 public function styles($params, $url){
1005
1006 $uniqid = '.ose-youtube.ose-uid-'.md5($url);
1007
1008 ob_start();
1009 ?>
1010 <style>
1011
1012 <?php
1013 $attributes_data = $params;
1014
1015 $is_pagination = 'flex';
1016
1017 $gap = '30';
1018 $columns = '';
1019
1020 if (isset($attributes_data['ispagination']) && $attributes_data['ispagination']) {
1021 $is_pagination = 'none';
1022 }
1023 if(isset($attributes_data['gapbetweenvideos'])){
1024 $gap = $attributes_data['gapbetweenvideos'];
1025 }
1026 if(isset($attributes_data['columns'])){
1027 $columns = $attributes_data['columns'];
1028 }
1029
1030
1031 if(!empty($columns) && (int) $columns > 0){
1032 $repeatCol = 'repeat(auto-fit, minmax('.esc_html('calc('.(100 / (int) $columns).'% - '.$gap.'px)').', 1fr))';
1033 }
1034 else{
1035 $repeatCol = 'repeat(auto-fit, minmax(calc(250px - '.$gap.'px), 1fr))';
1036 }
1037
1038 ?>
1039 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1040 gap: <?php echo esc_html($gap); ?>px !important;
1041 margin-top: <?php echo esc_html($gap); ?>px !important;
1042 grid-template-columns: <?php echo $repeatCol; ?>;
1043 }
1044 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .ep-youtube__content__pagination {
1045 display: <?php echo esc_html($is_pagination); ?>!important;
1046 }
1047
1048 <?php echo esc_attr($uniqid); ?> .layout-list .ep-youtube__content__block .youtube__content__body .content__wrap{
1049 grid-template-columns: repeat(auto-fit, minmax(calc(100% - 30px), 1fr))!important;
1050 }
1051
1052 @media (max-width: 420px) {
1053 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1054 gap: 30px !important;
1055 }
1056 }
1057
1058 <?php
1059 if($is_pagination){
1060 echo esc_attr($uniqid) ?> {
1061 height: 100%!important;
1062 }
1063 <?php
1064 }
1065 ?>
1066 </style>
1067 <?php
1068 return ob_get_clean();
1069 }
1070 }
1071