PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
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 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
1459 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', 'ytPlaylistLayout', 'ytPlaylistMode' ];
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 $u = $this->normalize_yt_url((string) $url);
56 // Channel / handle / user landing URLs go through this provider so
57 // getStaticResponse() can render a gallery for them.
58 if (preg_match('~\/channel\/|\/c\/|\/user\/|\/@\w+~i', $u)) {
59 return true;
60 }
61 // /playlist?list=PL… and /watch?…&list=PL|RD|UU… all render as a queue (fbs-72610).
62 if (preg_match('~^https?://(?:www\.)?youtube\.com/(?:playlist|watch)\?(?:[^#]*&)?list=([\w-]+)~i', $u)) {
63 return true;
64 }
65 // Video URLs — `watch?v=`, `embed/`, `shorts/`, `live/`, and the
66 // short `youtu.be/<id>` form. Embera builds the iframe locally
67 // via getStaticResponse(), so we don't depend on YouTube's
68 // oEmbed endpoint (fbs-81925: YT 401s our server IP).
69 if (preg_match('~(?:youtu\.be\/[A-Za-z0-9_\-]+|youtube\.com\/(?:watch\?|embed\/|shorts\/|live\/|playlist\?))~i', $u)) {
70 return true;
71 }
72 // Bare /<word> paths (preserved original behaviour — covers the
73 // legacy short-link shapes that some providers still emit).
74 return (bool) preg_match('~(?:https?:\/\/)?(?:www\.)?(?:youtube.com\/)(\w+)[^?\/]*$~i', $u);
75 }
76
77 /**
78 * Pull the 11-char video ID out of any supported YouTube video URL
79 * shape. Returns '' if no ID is present (e.g. pure playlist URL).
80 */
81 protected function extractVideoId($url) {
82 // Shortcode::parseContent runs the URL through esc_url() which
83 // converts `&` to `&#038;` — undo that here so the regexes see
84 // a normal query string.
85 $url = html_entity_decode((string) $url, ENT_QUOTES | ENT_HTML5);
86 $patterns = [
87 '~[?&]v=([A-Za-z0-9_\-]{6,})~',
88 '~youtu\.be\/([A-Za-z0-9_\-]{6,})~',
89 '~youtube\.com\/embed\/([A-Za-z0-9_\-]{6,})~',
90 '~youtube\.com\/shorts\/([A-Za-z0-9_\-]{6,})~',
91 '~youtube\.com\/live\/([A-Za-z0-9_\-]{6,})~',
92 ];
93 foreach ($patterns as $p) {
94 if (preg_match($p, $url, $m)) {
95 return $m[1];
96 }
97 }
98 return '';
99 }
100
101 /**
102 * Pull the playlist ID out of any supported playlist URL shape —
103 * both `watch?v=X&list=PL...` (video in playlist) and
104 * `playlist?list=PL...` (standalone playlist). Returns '' if
105 * none. We filter out the `RD*` auto-radio shapes that YouTube
106 * itself can't render in /embed/.
107 */
108 protected function extractPlaylistId($url) {
109 $url = html_entity_decode((string) $url, ENT_QUOTES | ENT_HTML5);
110 if (preg_match('~[?&]list=([A-Za-z0-9_\-]+)~', $url, $m)) {
111 $id = $m[1];
112 // YouTube's auto-generated radio mixes (`RD*`) only play on
113 // youtube.com proper and 404 in iframe embeds. Skip them so
114 // we fall back to a single-video embed instead.
115 if (strpos($id, 'RD') === 0) {
116 return '';
117 }
118 return $id;
119 }
120 return '';
121 }
122
123 public function validateTYLiveUrl($url) {
124 return (bool) (preg_match('~(?:https?:\/\/)?(?:www\.)?(?:youtube.com\/(?:channel|c|user)\/\w+\/live|@\w+\/live)~i', (string) $url));
125 }
126
127 /** inline {@inheritdoc} */
128 public function normalizeUrl(Url $url) {
129 return $url;
130 }
131
132
133 public function isChannel($url = null) {
134 if (empty($url)) {
135 $url = $this->url;
136 }
137 $channel = $this->getChannel($url);
138 return !empty($channel['id']);
139 }
140
141 public function isPlaylist($url = null) {
142 if (empty($url)) {
143 $url = $this->url;
144 }
145 return !empty($this->getPlaylistID($url));
146 }
147
148 /**
149 * Extract a playlist ID from any YouTube URL that exposes one:
150 * - /playlist?list=PL… (landing page)
151 * - /watch?v=…&list=PL… (video inside a playlist)
152 * - /watch?v=…&list=RD… (auto-generated Mix / Radio)
153 * - /watch?v=…&list=UU… (channel uploads via list= param)
154 *
155 * Single-video watch URLs (no list=) return '' so the normal oembed path
156 * still handles them.
157 */
158 /**
159 * Normalize a YouTube URL for query-string regex matching: WP often
160 * runs URLs through esc_url() which encodes & → &#038;, so we decode
161 * back to a single canonical form before pattern-matching.
162 */
163 protected function normalize_yt_url($url) {
164 return str_replace(['&#038;', '&amp;'], '&', (string) $url);
165 }
166
167 public function getPlaylistID($url = null) {
168 if (empty($url)) {
169 $url = $this->url;
170 }
171 $url = $this->normalize_yt_url($url);
172 if (preg_match('~^https?://(?:www\.)?youtube\.com/playlist\?(?:[^#]*&)?list=([\w-]+)~i', $url, $m)) {
173 return $m[1];
174 }
175 if (preg_match('~^https?://(?:www\.)?youtube\.com/watch\?(?:[^#]*&)?list=([\w-]+)~i', $url, $m)) {
176 return $m[1];
177 }
178 return '';
179 }
180
181 /**
182 * Optional starting video ID from a watch?v=…&list=… URL. The queue
183 * opens at this video instead of item #1.
184 */
185 public function getStartVideoID($url = null) {
186 if (empty($url)) {
187 $url = $this->url;
188 }
189 $url = $this->normalize_yt_url($url);
190 if (preg_match('~^https?://(?:www\.)?youtube\.com/watch\?(?:[^#]*&)?v=([\w-]{6,15})~i', $url, $m)) {
191 return $m[1];
192 }
193 return '';
194 }
195
196 public function getChannel($url = null) {
197 $channelId = 'unknown_id'; // temporarily assigned a placeholder value for demonstration purposes
198
199 if (empty($url)) {
200 $url = $this->url;
201 }
202 preg_match('~\/(channel|c|user)\/(.+)~i', (string) $url, $matches);
203 if(empty($matches[1])){
204 preg_match('~(?:https?:\/\/)?(?:www\.)?(?:youtube.com\/)(\w+)[^?\/]*$~i', (string) $url, $matches);
205 if(!empty($matches[1])){
206 return [
207 "type" => 'user',
208 "id" => $matches[1],
209 ];
210 }
211 }
212
213
214
215 if(empty($matches[1])){
216 preg_match('~\/(@)(\w+)~i', (string) $url, $matches);
217 if(!empty($matches[1])){
218 $handle = $this->get_youtube_handler($this->url);
219 if(!empty($handle)){
220 $resolved = $this->get_channel_id_by_handler($handle);
221 if(!empty($resolved)){
222 $channelId = $resolved;
223 }
224 }
225 return [
226 "type" => 'user',
227 "id" => $channelId,
228 ];
229 }
230 }
231 return [
232 "type" => isset($matches[1]) ? $matches[1] : '',
233 "id" => isset($matches[2]) ? $matches[2] : '',
234 ];
235 }
236
237 /** inline {@inheritdoc} */
238 public function getEndpoint() {
239 if ($this->isChannel()) {
240 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/channels';
241 return $apiEndpoint;
242 }
243 return (string) $this->endpoint;
244 }
245
246 protected function get_api_key() {
247 $settings = (array) get_option(EMBEDPRESS_PLG_NAME . ':youtube', []);
248 return !empty($settings['api_key']) ? $settings['api_key'] : '';
249 }
250
251 protected function get_pagesize() {
252 $settings = (array) get_option(EMBEDPRESS_PLG_NAME . ':youtube', []);
253 return !empty($settings['pagesize']) ? $settings['pagesize'] : '';
254 }
255
256 /** inline {@inheritdoc} */
257 public function getParams() {
258 $params = parent::getParams();
259
260 // WP lowercases shortcode attribute names. Re-map lowercase keys
261 // to camelCase ones so `[embedpress ytplaylistlayout="theatre"]`
262 // flows through the same code path as block/Elementor attrs.
263 $aliases = [
264 'ytplaylistlayout' => 'ytPlaylistLayout',
265 'ytplaylistmode' => 'ytPlaylistMode',
266 'ytchannellayout' => 'ytChannelLayout',
267 ];
268 foreach ($aliases as $lower => $canon) {
269 if (isset($this->config[$lower]) && !isset($params[$canon])) {
270 $params[$canon] = $this->config[$lower];
271 }
272 }
273
274 if ($this->isChannel() && $this->get_api_key()) {
275 $channel = $this->getChannel();
276 $params['part'] = 'contentDetails,snippet';
277 $params['key'] = $this->get_api_key();
278 if ($channel['type'] == 'c') {
279 $params['forUsername'] = $channel['id'];
280 } else {
281 $params['id'] = $channel['id'];
282 }
283 unset($params['url']);
284 }
285 return $params;
286 }
287
288 /**
289 * Builds a valid Oembed query string based on the given parameters,
290 * Since this method uses the http_build_query function, there is no
291 * need to pass urlencoded parameters, http_build_query already does
292 * this for us.
293 *
294 * @param string $endpoint The Url to the Oembed endpoint
295 * @param array $params Parameters for the query string
296 * @return string
297 */
298
299 protected function constructUrl($endpoint, array $params = array())
300 {
301 $endpoint = self::$channel_endpoint . $endpoint;
302
303 return $endpoint . ((strpos($endpoint, '?') === false) ? '?' : '&') . http_build_query(array_filter($params));
304 }
305
306 public function getStaticResponse() {
307 $results = [
308 "title" => "",
309 "type" => "video",
310 'provider_name' => $this->getProviderName(),
311 "provider_url" => "https://www.youtube.com/",
312 'html' => '',
313 ];
314
315 $params = $this->getParams();
316
317 if (preg_match("/^https?:\/\/(?:www\.)?youtube\.com\/channel\/([\w-]+)\/live$/", $this->url, $matches) || $this->validateTYLiveUrl($this->url)) {
318
319 $channelId = '';
320
321 if(!empty($matches[1])){
322 $channelId = $matches[1];
323 }
324
325 if(empty($channelId)){
326 $handle = $this->get_youtube_handler($this->url);
327 if(!empty($handle)){
328 $resolved = $this->get_channel_id_by_handler($handle);
329 if(!empty($resolved)){
330 $channelId = $resolved;
331 }
332 }
333 }
334
335 if(empty($channelId)){
336 return $results;
337 }
338
339 $api_key = $this->get_api_key();
340
341 // When API key is available, check for active live stream
342 if (!empty($api_key)) {
343 $live_video_id = $this->get_live_video_id($channelId, $api_key);
344
345 if (!empty($live_video_id)) {
346 // Channel is live - embed the live video directly
347 $embedUrl = 'https://www.youtube.com/embed/' . $live_video_id . '?feature=oembed';
348 } else {
349 // Channel is not live - show the last completed stream or latest video
350 $last_video_id = $this->get_last_stream_or_video($channelId, $api_key);
351 if (!empty($last_video_id)) {
352 $embedUrl = 'https://www.youtube.com/embed/' . $last_video_id . '?feature=oembed';
353 } else {
354 // No video found at all
355 $embedUrl = 'https://www.youtube.com/embed/live_stream?channel=' . $channelId . '&feature=oembed';
356 }
357 }
358 } else {
359 // No API key - use live_stream endpoint as fallback
360 $embedUrl = 'https://www.youtube.com/embed/live_stream?channel='.$channelId.'&feature=oembed';
361 }
362
363 $attr = [];
364 $attr[] = 'width="'.esc_attr($params['maxwidth']).'"';
365 $attr[] = 'height="'.esc_attr($params['maxheight']).'"';
366 $attr[] = 'src="' . esc_url($embedUrl) . '"';
367 $attr[] = 'frameborder="0"';
368 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
369 $attr[] = 'allowfullscreen';
370 $attr[] = 'referrerpolicy="origin"';
371
372 $results['html'] = '<iframe ' . implode(' ', $attr) . '></iframe>';
373 }
374 else if($this->isPlaylist()){
375 // User can opt-out of the playlist UI via ytPlaylistMode=single.
376 // For watch?v=…&list=… URLs, this renders just the v= video as a
377 // normal single-video oembed. For /playlist?list=… URLs (no v=),
378 // 'single' means "render the playlist's first item as a single
379 // video" — getStartVideoID() falls back to the playlist's lead
380 // item via the YouTube API.
381 $mode = isset($params['ytPlaylistMode']) ? $params['ytPlaylistMode'] : 'playlist';
382 if ($mode === 'single') {
383 $single_vid = $this->getStartVideoID();
384 if (empty($single_vid)) {
385 // /playlist?list=… URL — fetch first item from playlistItems.
386 $first = $this->get_playlist_first_video($this->getPlaylistID());
387 if (!empty($first)) {
388 $single_vid = $first;
389 }
390 }
391 if (!empty($single_vid)) {
392 $embedUrl = 'https://www.youtube.com/embed/' . rawurlencode($single_vid) . '?feature=oembed';
393 $w = esc_attr($params['maxwidth']);
394 $h = esc_attr($params['maxheight']);
395 $results['html'] = '<iframe width="'.$w.'" height="'.$h.'" src="'.esc_url($embedUrl).'" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
396 return $results;
397 }
398 // No video resolvable — fall through to playlist UI as a sane fallback.
399 }
400 $playlist = $this->getPlaylistGallery();
401 $results = array_merge($results, $playlist);
402 }
403 else if($this->isChannel()){
404 $channel = $this->getChannelGallery();
405 $results = array_merge($results, $channel);
406 }
407 else {
408 // Regular video or playlist URL — build the embed iframe
409 // deterministically. This is the fallback path that lets
410 // YT embeds keep working when YouTube's oEmbed endpoint
411 // refuses our server IP (fbs-81925).
412 //
413 // Three shapes handled:
414 // - watch?v=X&list=PL... -> /embed/X?list=PL... (video in playlist)
415 // - playlist?list=PL... -> /embed/videoseries?list= (playlist only)
416 // - watch?v=X (or shorts/youtu.be) -> /embed/X (single video)
417 $videoId = $this->extractVideoId($this->url);
418 $playlistId = $this->extractPlaylistId($this->url);
419
420 $embedPath = '';
421 if (!empty($videoId)) {
422 $embedPath = $videoId;
423 } elseif (!empty($playlistId)) {
424 $embedPath = 'videoseries';
425 }
426
427 if (!empty($embedPath)) {
428 $query = ['feature' => 'oembed'];
429 if (!empty($playlistId)) {
430 $query['list'] = $playlistId;
431 }
432 $embedUrl = 'https://www.youtube.com/embed/' . $embedPath . '?' . http_build_query($query);
433 $maxw = isset($params['maxwidth']) ? $params['maxwidth'] : 600;
434 $maxh = isset($params['maxheight']) ? $params['maxheight'] : 340;
435 $attr = [];
436 $attr[] = 'width="' . esc_attr($maxw) . '"';
437 $attr[] = 'height="' . esc_attr($maxh) . '"';
438 $attr[] = 'src="' . esc_url($embedUrl) . '"';
439 $attr[] = 'frameborder="0"';
440 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
441 $attr[] = 'allowfullscreen';
442 $attr[] = 'referrerpolicy="origin"';
443 $results['html'] = '<iframe ' . implode(' ', $attr) . '></iframe>';
444 $results['title'] = '';
445 }
446 }
447
448 return $results;
449 }
450
451 public function getChannelPlaylist(){
452 $result = [
453 "playlistID" => '',
454 "title" => '',
455 ];
456 $channel = $this->getChannel();
457 $channel_url = $this->constructUrl('channels', $this->getParams());
458 $transient_key = 'ep_embed_youtube_channel_playlist_id_' . md5($channel_url);
459 $jsonResult = get_transient($transient_key);
460
461 if(!empty($jsonResult)){
462 return $jsonResult;
463 }
464
465 if($channel['type'] == 'user' || $channel['type'] == 'c'){
466 $this->getChannelIDbyUsername();
467 $channel_url = $this->constructUrl('channels', $this->getParams());
468 }
469
470 if (empty($this->get_api_key())) {
471 $result['error'] = true;
472 $result['html'] = $this->get_api_key_error_message();
473 return $result;
474 }
475
476 $apiResult = wp_remote_get($channel_url, array('timeout' => self::$curltimeout));
477 if (is_wp_error($apiResult)) {
478 $result['error'] = true;
479 $result['html'] = $this->clean_api_error_html($apiResult->get_error_message(), true);
480 set_transient($transient_key, $result, 10);
481 return $result;
482 }
483 $jsonResult = json_decode($apiResult['body']);
484
485
486 if (isset($jsonResult->error)) {
487 $result['error'] = true;
488 if (isset($jsonResult->error->message)) {
489 $result['html'] = $this->clean_api_error_html($jsonResult->error->message, true);
490 }
491 else{
492 $result['html'] = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
493 }
494 set_transient($transient_key, $result, MINUTE_IN_SECONDS);
495 return $result;
496 }
497 elseif(!empty($jsonResult->items[0]->contentDetails->relatedPlaylists->uploads)){
498 $result['playlistID'] = $jsonResult->items[0]->contentDetails->relatedPlaylists->uploads;
499 $result['title'] = isset($jsonResult->items[0]->snippet->title) ? $jsonResult->items[0]->snippet->title : '';
500 set_transient($transient_key, $result, DAY_IN_SECONDS);
501 }
502
503 return $result;
504 }
505
506 public function get_youtube_handler($url){
507 preg_match('/^https?:\/\/(?:www\.)?youtube\.com\/@([^\/?]+)/i', $url, $matches);
508
509 $handle_name = '';
510 if(!empty($matches[1])){
511 $handle_name = $matches[1];
512 }
513
514 return $handle_name;
515 }
516
517 public function getChannelIDbyUsername(){
518 $url = $this->getUrl();
519 $apiResult = wp_remote_get($url, array('timeout' => self::$curltimeout));
520
521 if (!is_wp_error($apiResult)) {
522 $channel_html = $apiResult['body'];
523 preg_match("/<meta\s+itemprop=[\"']channelId[\"']\s+content=[\"'](.*?)[\"']\/?>/", $channel_html, $matches);
524 if(!empty($matches[1])){
525 $url = "https://www.youtube.com/channel/{$matches[1]}";
526 $url = $this->normalizeUrl(new Url($url));
527 }
528 }
529 }
530
531
532 public function get_channel_id_by_handler($handle)
533 {
534 $transient_key = 'channel_id_' . md5($handle);
535 $channel_id = get_transient($transient_key);
536
537 if (false !== $channel_id && preg_match('/^UC[\w-]+$/', $channel_id)) {
538 return $channel_id;
539 }
540
541 // Prefer the YouTube Data API when a key is configured. Scraping
542 // youtube.com from EU server IPs gets bounced to consent.youtube.com
543 // and the response contains neither the canonical channel link nor
544 // externalId, so the regex fallback below silently returns ''.
545 $api_key = $this->get_api_key();
546 if (!empty($api_key)) {
547 $api_url = self::$channel_endpoint . 'channels?' . http_build_query([
548 'part' => 'id',
549 'forHandle' => '@' . $handle,
550 'key' => $api_key,
551 ]);
552 $api_response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
553 if (!is_wp_error($api_response)) {
554 $api_body = json_decode(wp_remote_retrieve_body($api_response));
555 if (!empty($api_body->items[0]->id) && preg_match('/^UC[\w-]+$/', $api_body->items[0]->id)) {
556 $channel_id = $api_body->items[0]->id;
557 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
558 return $channel_id;
559 }
560 }
561 }
562
563 $channel_handle = "https://www.youtube.com/@{$handle}";
564
565 // Bypass YouTube's EU consent redirect by sending the CONSENT cookie
566 // and a real browser UA. Without this the response is a consent page
567 // when the request originates from an EU IP.
568 $response = wp_remote_get($channel_handle, [
569 'timeout' => self::$curltimeout,
570 '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',
571 'headers' => [
572 'Accept-Language' => 'en-US,en;q=0.9',
573 'Cookie' => 'CONSENT=YES+cb.20210328-17-p0.en+FX+000',
574 ],
575 ]);
576
577 if (is_wp_error($response)) {
578 return '';
579 }
580
581 $body = wp_remote_retrieve_body($response);
582
583 if (empty($body)) {
584 return '';
585 }
586
587 // Try canonical link first (most reliable)
588 $pattern = '/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/([^"]{1,50})">/';
589 if (preg_match($pattern, $body, $matches)) {
590 $channel_id = $matches[1];
591 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
592 return $channel_id;
593 }
594
595 // Fallback: try externalId from page data
596 if (preg_match('/"externalId"\s*:\s*"(UC[a-zA-Z0-9_-]+)"/', $body, $matches)) {
597 $channel_id = $matches[1];
598 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
599 return $channel_id;
600 }
601
602 return '';
603 }
604
605 /**
606 * Check if a channel has an active live stream and return the video ID.
607 */
608 public function get_live_video_id($channel_id, $api_key) {
609 $transient_key = 'ep_yt_live_' . md5($channel_id);
610 $cached = get_transient($transient_key);
611
612 if (false !== $cached) {
613 return $cached;
614 }
615
616 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
617 'part' => 'id',
618 'channelId' => $channel_id,
619 'eventType' => 'live',
620 'type' => 'video',
621 'key' => $api_key,
622 ]);
623
624 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
625
626 if (is_wp_error($response)) {
627 return '';
628 }
629
630 $data = json_decode(wp_remote_retrieve_body($response));
631
632 if (!empty($data->items[0]->id->videoId)) {
633 $video_id = $data->items[0]->id->videoId;
634 set_transient($transient_key, $video_id, 2 * MINUTE_IN_SECONDS);
635 return $video_id;
636 }
637
638 // Cache empty result briefly to avoid repeated API calls
639 set_transient($transient_key, '', MINUTE_IN_SECONDS);
640 return '';
641 }
642
643 /**
644 * Get the last completed live stream or latest video from a channel.
645 * Tries completed streams first, falls back to latest upload.
646 */
647 public function get_last_stream_or_video($channel_id, $api_key) {
648 $transient_key = 'ep_yt_last_stream_' . md5($channel_id);
649 $cached = get_transient($transient_key);
650
651 if (false !== $cached) {
652 return $cached;
653 }
654
655 // First try: get the last completed live stream
656 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
657 'part' => 'id',
658 'channelId' => $channel_id,
659 'eventType' => 'completed',
660 'type' => 'video',
661 'order' => 'date',
662 'maxResults' => 1,
663 'key' => $api_key,
664 ]);
665
666 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
667
668 if (!is_wp_error($response)) {
669 $data = json_decode(wp_remote_retrieve_body($response));
670 if (!empty($data->items[0]->id->videoId)) {
671 $video_id = $data->items[0]->id->videoId;
672 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
673 return $video_id;
674 }
675 }
676
677 // Fallback: get the latest video from the channel's uploads playlist
678 $channel_url = self::$channel_endpoint . 'channels?' . http_build_query([
679 'part' => 'contentDetails',
680 'id' => $channel_id,
681 'key' => $api_key,
682 ]);
683
684 $ch_response = wp_remote_get($channel_url, ['timeout' => self::$curltimeout]);
685
686 if (!is_wp_error($ch_response)) {
687 $ch_data = json_decode(wp_remote_retrieve_body($ch_response));
688 $uploads_playlist = $ch_data->items[0]->contentDetails->relatedPlaylists->uploads ?? '';
689
690 if (!empty($uploads_playlist)) {
691 $playlist_url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
692 'part' => 'snippet',
693 'playlistId' => $uploads_playlist,
694 'maxResults' => 1,
695 'key' => $api_key,
696 ]);
697
698 $pl_response = wp_remote_get($playlist_url, ['timeout' => self::$curltimeout]);
699
700 if (!is_wp_error($pl_response)) {
701 $pl_data = json_decode(wp_remote_retrieve_body($pl_response));
702 if (!empty($pl_data->items[0]->snippet->resourceId->videoId)) {
703 $video_id = $pl_data->items[0]->snippet->resourceId->videoId;
704 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
705 return $video_id;
706 }
707 }
708 }
709 }
710
711 set_transient($transient_key, '', 2 * MINUTE_IN_SECONDS);
712 return '';
713 }
714
715 public function layout_data(){
716 $data = [];
717 $data['get_pagesize'] = $this->get_pagesize();
718 $data['get_api_key'] = $this->get_api_key();
719 $data['get_api_key_error_message'] = $this->get_api_key_error_message();
720 $data['get_channel_info'] = $this->get_channel_info();
721 $data['get_api_key'] = $this->get_api_key();
722 $data['curltimeout'] = self::$curltimeout;
723 $data['self::class'] = self::class;
724
725 return $data;
726
727 }
728
729
730 /** inline {@inheritdoc} */
731 public function getChannelGallery() {
732 $response = [];
733 $channel = $this->getChannelPlaylist();
734 if(!empty($channel['error'])){
735 return $channel;
736 }
737 if (!empty($channel["playlistID"])) {
738 return $this->buildGallery($channel["playlistID"], $channel['title']);
739 }
740 elseif ($this->isChannel() && empty($this->get_api_key()) && current_user_can('manage_options')) {
741 return [
742 "html" => "<div class='ep-player-wrap'>" . __('Please enter your YouTube API key to embed YouTube Channel.', 'embedpress') . "</div>",
743 ];
744 }
745
746 return $response;
747 }
748
749 /**
750 * Look up the first video ID inside a playlist (for 'single' mode when
751 * the URL has no v=). Cached 1d alongside the playlist info call.
752 */
753 public function get_playlist_first_video($playlist_id) {
754 $api_key = $this->get_api_key();
755 if (empty($api_key) || empty($playlist_id)) {
756 return '';
757 }
758 $transient_key = 'ep_yt_playlist_first_' . md5($playlist_id);
759 $cached = get_transient($transient_key);
760 if (is_string($cached) && $cached !== '') {
761 return $cached;
762 }
763 $url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
764 'part' => 'snippet,contentDetails',
765 'playlistId' => $playlist_id,
766 'maxResults' => 1,
767 'key' => $api_key,
768 ]);
769 $response = wp_remote_get($url, ['timeout' => self::$curltimeout]);
770 if (is_wp_error($response)) {
771 return '';
772 }
773 $body = json_decode(wp_remote_retrieve_body($response));
774 $vid = isset($body->items[0]) ? Helper::get_id($body->items[0]) : '';
775 if (!empty($vid)) {
776 set_transient($transient_key, $vid, DAY_IN_SECONDS);
777 }
778 return (string) $vid;
779 }
780
781 /**
782 * Fetch playlist metadata (title, channel, item count, thumbnail) from
783 * youtube/v3/playlists. Cached 1d. Returns [] on failure.
784 */
785 public function get_playlist_info($playlist_id) {
786 $api_key = $this->get_api_key();
787 if (empty($api_key) || empty($playlist_id)) {
788 return [];
789 }
790
791 $transient_key = 'ep_yt_playlist_info_' . md5($playlist_id);
792 $cached = get_transient($transient_key);
793 if (is_array($cached)) {
794 return $cached;
795 }
796
797 $url = self::$channel_endpoint . 'playlists?' . http_build_query([
798 'part' => 'snippet,contentDetails',
799 'id' => $playlist_id,
800 'key' => $api_key,
801 ]);
802
803 $response = wp_remote_get($url, ['timeout' => self::$curltimeout]);
804 if (is_wp_error($response)) {
805 return [];
806 }
807 $body = json_decode(wp_remote_retrieve_body($response));
808 if (empty($body->items[0])) {
809 set_transient($transient_key, [], MINUTE_IN_SECONDS);
810 return [];
811 }
812
813 $item = $body->items[0];
814 $info = [
815 'id' => $playlist_id,
816 'title' => isset($item->snippet->title) ? $item->snippet->title : '',
817 'channel_title' => isset($item->snippet->channelTitle) ? $item->snippet->channelTitle : '',
818 'channel_id' => isset($item->snippet->channelId) ? $item->snippet->channelId : '',
819 'description' => isset($item->snippet->description) ? $item->snippet->description : '',
820 'item_count' => isset($item->contentDetails->itemCount) ? (int) $item->contentDetails->itemCount : 0,
821 'thumbnail' => isset($item->snippet->thumbnails->medium->url)
822 ? $item->snippet->thumbnails->medium->url
823 : (isset($item->snippet->thumbnails->default->url) ? $item->snippet->thumbnails->default->url : ''),
824 'playlist_url' => 'https://www.youtube.com/playlist?list=' . rawurlencode($playlist_id),
825 ];
826
827 set_transient($transient_key, $info, DAY_IN_SECONDS);
828 return $info;
829 }
830
831 /**
832 * Render a playlist URL (youtube.com/playlist?list=PL…) as a queue/gallery,
833 * reusing the same layout engine as channels.
834 */
835 public function getPlaylistGallery() {
836 $playlist_id = $this->getPlaylistID();
837 if (empty($playlist_id)) {
838 return [];
839 }
840
841 if (empty($this->get_api_key())) {
842 if (current_user_can('manage_options')) {
843 return [
844 "html" => "<div class='ep-player-wrap'>" . $this->get_api_key_error_message() . "</div>",
845 ];
846 }
847 return [];
848 }
849
850 $playlist_info = $this->get_playlist_info($playlist_id);
851 $title = !empty($playlist_info['title']) ? $playlist_info['title'] : '';
852 $start_vid = $this->getStartVideoID();
853 if (!empty($start_vid)) {
854 $playlist_info['start_vid'] = $start_vid;
855 }
856
857 return $this->buildGallery($playlist_id, $title, $playlist_info);
858 }
859
860 /**
861 * Shared gallery renderer for both channel uploads playlists and
862 * standalone playlist URLs. Extracted from getChannelGallery() so the
863 * layout/pagination/iframe code path stays a single source of truth.
864 *
865 * @param string $playlist_id YouTube playlist ID.
866 * @param string $title Display title (channel name or playlist title).
867 * @param array $playlist_info Optional playlist metadata from get_playlist_info() — non-empty when rendering a standalone playlist URL.
868 */
869 protected function buildGallery($playlist_id, $title = '', $playlist_info = []) {
870 $params = $this->getParams();
871 $is_playlist = !empty($playlist_info);
872 $gallery_args = [
873 'playlistId' => $playlist_id,
874 ];
875 // Carry the user-configured height through to the playlist shells
876 // (queue/theatre) so it drives the whole wrapper, not just the
877 // hardcoded CSS default. Width stays fluid (100%); only height is
878 // user-controlled here.
879 if (!empty($params['maxheight'])) {
880 $gallery_args['maxheight'] = (int) $params['maxheight'];
881 }
882 if (!empty($params['pagesize'])) {
883 $gallery_args['pagesize'] = $params['pagesize'];
884 } elseif ($is_playlist) {
885 // Queue layout — render enough items to fill the scrollable area
886 // out of the box. The JS auto-fetches more on scroll regardless.
887 $gallery_args['pagesize'] = 12;
888 }
889
890 // Layout selection differs by source:
891 // Channel URLs → ytChannelLayout (gallery/list/grid/carousel) [unchanged]
892 // Playlist URLs → ytPlaylistLayout (queue/theatre, more to come)
893 // Each surface has its own attribute so a saved channel pick can't
894 // leak into a playlist render and vice versa.
895 if ($is_playlist) {
896 // Free layouts always render. Pro layouts (library/spotlight/
897 // cinema/magazine) only render when Pro is active; otherwise we
898 // fall back to queue and the inspector shows the upsell.
899 $playlist_layouts = ['queue', 'theatre', 'library', 'spotlight', 'cinema', 'magazine'];
900 $pro_playlist_layouts = ['library', 'spotlight', 'cinema', 'magazine'];
901 $picked = isset($params['ytPlaylistLayout']) ? $params['ytPlaylistLayout'] : '';
902 if (!in_array($picked, $playlist_layouts, true)) {
903 $picked = 'queue';
904 }
905 if (in_array($picked, $pro_playlist_layouts, true) && !apply_filters('embedpress/is_allow_rander', false)) {
906 $picked = 'queue';
907 }
908 $layout = $picked;
909 } else {
910 $default_layout = 'gallery';
911 $layout = isset($params['ytChannelLayout']) && $params['ytChannelLayout'] !== ''
912 ? $params['ytChannelLayout']
913 : $default_layout;
914 }
915
916 $layout_data = $this->layout_data();
917 $layout_data['is_playlist'] = $is_playlist;
918 $layout_data['playlist_info'] = $playlist_info;
919
920 // For playlist URLs the channel-info fetch returns a string error
921 // ("No channel information found") because get_channel_info() targets
922 // channel URLs. Pro's youtube_carousel_layout dereferences this as
923 // an array, so feed it a synthetic shape sourced from playlist_info.
924 if ($is_playlist) {
925 $layout_data['get_channel_info'] = [
926 'snippet' => [
927 'title' => !empty($playlist_info['channel_title']) ? $playlist_info['channel_title'] : (!empty($playlist_info['title']) ? $playlist_info['title'] : ''),
928 'thumbnails' => [
929 'default' => ['url' => !empty($playlist_info['thumbnail']) ? $playlist_info['thumbnail'] : ''],
930 'high' => ['url' => !empty($playlist_info['thumbnail']) ? $playlist_info['thumbnail'] : ''],
931 ],
932 ],
933 ];
934 }
935
936 $channel_layout = 'layout-' . $layout;
937
938 $gallery = YoutubeLayout::create_youtube_layout($gallery_args, $layout_data, $layout, $this->url);
939
940 $main_iframe = '';
941 // Queue layout renders its own leading player; gallery layout uses ep-first-video block.
942 if (!empty($gallery->first_vid) && $layout === 'gallery') {
943 $rel = esc_url("https://www.youtube.com/embed/{$gallery->first_vid}?feature=oembed");
944 $iframe_width = esc_attr($params['maxwidth']);
945 $iframe_height = esc_attr($params['maxheight']);
946 $iframe_title = esc_attr($title);
947 $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>";
948 }
949
950 if (!apply_filters('embedpress/is_allow_rander', false) && ($layout === 'grid' || $layout === 'carousel')) {
951 return [];
952 }
953
954 if (!empty($gallery->html)) {
955 // Queue layout owns its own styling; skip the gallery/grid <style> block.
956 $styles = $layout === 'queue' ? '' : $this->styles($params, $this->getUrl());
957 $html_content = $main_iframe . $gallery->html . ' ' . $styles;
958
959 return [
960 "title" => $title,
961 "html" => "<div class='ep-player-wrap $channel_layout'>$html_content</div>",
962 ];
963 }
964
965 return [];
966 }
967
968
969 public function get_channel_info() {
970 $api_key = $this->get_api_key();
971 $channel_id = $this->getChannel($this->url);
972 $channel_id = $channel_id['id'];
973
974
975 // Create a unique transient key based on the channel ID
976 $transient_key = 'youtube_channel_info_' . $channel_id;
977
978 // Attempt to get cached response
979 $channel_info = get_transient($transient_key);
980
981 if ($channel_info === false) {
982 // No cached response, make the API call
983 $endpoint = "https://www.googleapis.com/youtube/v3/channels?part=snippet%2Cstatistics&id=$channel_id&key=$api_key";
984 $response = wp_remote_get($endpoint);
985
986 if (is_wp_error($response)) {
987 return 'Error fetching channel info';
988 }
989
990 $body = wp_remote_retrieve_body($response);
991 $data = json_decode($body, true);
992
993 if (empty($data['items'])) {
994 return 'No channel information found';
995 }
996
997 $channel_info = $data['items'][0];
998
999
1000 // Cache the response for 1 hour
1001 set_transient($transient_key, $channel_info, HOUR_IN_SECONDS);
1002 }
1003
1004 update_option('youtube_channel_info_'. md5($this->url), $channel_info);
1005
1006
1007 return $channel_info;
1008
1009 }
1010
1011
1012 public function get_layout() {
1013 $params = $this->getParams();
1014 return isset($params['ytChannelLayout']) ? $params['ytChannelLayout'] : '';
1015 }
1016
1017 /**
1018 * Undocumented function
1019 *
1020 * @param array $options
1021 * @return object
1022 */
1023 public function get_gallery_page($options) {
1024
1025 $nextPageToken = '';
1026 $prevPageToken = '';
1027 $gallobj = new \stdClass();
1028 $options = wp_parse_args($options, [
1029 'playlistId' => '',
1030 'pageToken' => '',
1031 'pagesize' => $this->get_pagesize() ? $this->get_pagesize() : 6,
1032 'currentpage' => '',
1033 'columns' => 3,
1034 'ytChannelLayout' => 'gallery',
1035 'thumbnail' => 'medium',
1036 'gallery' => true,
1037 'autonext' => true,
1038 'thumbplay' => true,
1039 'apiKey' => $this->get_api_key(),
1040 'hideprivate' => '',
1041 ]);
1042 $options['pagesize'] = $options['pagesize'] > 50 ? 50 : $options['pagesize'];
1043 $options['pagesize'] = $options['pagesize'] < 1 ? 1 : $options['pagesize'];
1044
1045 if (empty($options['apiKey'])) {
1046 $gallobj->html = $this->get_api_key_error_message();
1047 return $gallobj;
1048 }
1049
1050 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,status,contentDetails&playlistId=' . $options['playlistId']
1051 . '&maxResults=' . $options['pagesize']
1052 . '&key=' . $options['apiKey'];
1053 if ($options['pageToken'] != null) {
1054 $apiEndpoint .= '&pageToken=' . $options['pageToken'];
1055 }
1056
1057
1058
1059 $transient_key = 'ep_embed_youtube_channel_' . md5($apiEndpoint);
1060 $gallobj->transient_key = $transient_key;
1061 $jsonResult = get_transient($transient_key);
1062
1063
1064 if (empty($jsonResult)) {
1065 $apiResult = wp_remote_get($apiEndpoint, array('timeout' => self::$curltimeout));
1066 if (is_wp_error($apiResult)) {
1067 $gallobj->html = $this->clean_api_error_html($apiResult->get_error_message(), true);
1068 return $gallobj;
1069 }
1070 $jsonResult = json_decode($apiResult['body']);
1071
1072 if (empty($jsonResult->error)) {
1073 set_transient($transient_key, $jsonResult, MINUTE_IN_SECONDS * 20);
1074 }
1075 else{
1076 set_transient($transient_key, $jsonResult, 10);
1077 }
1078 }
1079
1080
1081
1082 if (isset($jsonResult->error)) {
1083 if(!empty($jsonResult->error->errors[0]->reason) && $jsonResult->error->errors[0]->reason == 'playlistNotFound'){
1084 $gallobj->html = $this->clean_api_error_html(__('There is nothing on the playlist.', 'embedpress'));
1085 return $gallobj;
1086 }
1087 if (isset($jsonResult->error->message)) {
1088 $gallobj->html = $this->clean_api_error_html($jsonResult->error->message);
1089 return $gallobj;
1090 }
1091 $gallobj->html = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
1092 return $gallobj;
1093 }
1094
1095
1096
1097 $resultsPerPage = $jsonResult->pageInfo->resultsPerPage;
1098 $totalResults = $jsonResult->pageInfo->totalResults;
1099 $totalPages = ceil($totalResults / $resultsPerPage);
1100
1101 if (isset($jsonResult->nextPageToken)) {
1102 $nextPageToken = $jsonResult->nextPageToken;
1103 }
1104
1105 if (isset($jsonResult->prevPageToken)) {
1106 $prevPageToken = $jsonResult->prevPageToken;
1107 }
1108
1109
1110 if (!empty($jsonResult->items) && is_array($jsonResult->items)) :
1111 if($options['gallery'] === "false"){
1112 $gallobj->html = "";
1113 if(count($jsonResult->items) === 1){
1114 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
1115 }
1116 return $gallobj;
1117 }
1118
1119 if(count($jsonResult->items) === 1 && empty($nextPageToken) && empty($prevPageToken)){
1120 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
1121 $gallobj->html = "";
1122 return $gallobj;
1123 }
1124
1125 if (strpos($options['playlistId'], 'UU') === 0) {
1126 // sort only channels
1127 usort($jsonResult->items, array(self::class, 'compare_vid_date')); // sorts in place
1128 }
1129
1130 ob_start();
1131
1132 ?>
1133
1134 <div class="ep-youtube__content__block" data-unique-id="<?php echo wp_rand(); ?>">
1135 <div class="youtube__content__body">
1136 <?php
1137 ?>
1138
1139 <div class="content__wrap">
1140
1141 <?php
1142
1143 $data = $this->layout_data();
1144
1145
1146 $channel_info = get_option('youtube_channel_info_'.md5($options['channel_url']));
1147
1148 $channelTitle = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null;
1149 $channelThumb = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null;
1150 $layout = $this->get_layout();
1151
1152
1153 if($layout === 'gallery'){
1154 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1155 }
1156 else if($layout === 'grid'){
1157 do_action('embedpress/youtube_grid_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1158 }
1159 else if($layout === 'list'){
1160 echo YoutubeLayout::create_list_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1161 }
1162 else if($layout === 'carousel'){
1163 do_action('embedpress/youtube_carousel_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1164 }
1165 else{
1166 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1167
1168 }
1169
1170 ?>
1171 <!--
1172 <?php foreach ($jsonResult->items as $item) : ?>
1173 <?php
1174 $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null;
1175 $thumbnail = $this->get_thumbnail_url($item, $options['thumbnail'], $privacyStatus);
1176 $vid = $this->get_id($item);
1177 if (empty($gallobj->first_vid)) {
1178 $gallobj->first_vid = $vid;
1179 }
1180 if ($privacyStatus == 'private' && $options['hideprivate']) {
1181 continue;
1182 }
1183 ?>
1184 <div class="item" data-vid="<?php echo $vid; ?>">
1185 <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>">
1186 <div class="play-icon">
1187 <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt="">
1188 </div>
1189 </div>
1190 <div class="body">
1191 <p><?php echo esc_html($item->snippet->title); ?></p>
1192 </div>
1193 </div>
1194
1195 <?php endforeach; ?> -->
1196
1197 <div class="item" style="height: 0"></div>
1198 </div>
1199
1200
1201 <?php
1202 $layout = $this->get_layout();
1203
1204 if ($totalPages > 1 && $layout !== 'carousel') : ?>
1205 <div class="ep-youtube__content__pagination <?php echo (empty($prevPageToken) && empty($nextPageToken)) ? ' hide ' : ''; ?>">
1206 <div
1207 class="ep-prev" <?php echo empty($prevPageToken) ? ' style="display:none" ' : ''; ?>
1208 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
1209 data-pagetoken="<?php echo esc_attr($prevPageToken) ?>"
1210 data-pagesize="<?php echo intval($options['pagesize']) ?>"
1211 >
1212 <span><?php _e("Prev", "embedpress"); ?></span>
1213 </div>
1214 <div class="is_desktop_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
1215 <?php
1216
1217 $numOfPages = $totalPages;
1218 $renderedEllipses = false;
1219
1220 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
1221
1222 for($i = 1; $i<=$numOfPages; $i++)
1223 {
1224 //render pages 1 - 3
1225 if($i < 4) {
1226 //render link
1227 $is_current = $i == (int)$currentPage? "active__current_page" : "";
1228
1229 echo wp_kses_post("<span class='page-number $is_current' data-page='$i'>$i</span>");
1230
1231 }
1232
1233 //render current page number
1234 else if($i == (int)$currentPage) {
1235 //render link
1236 echo wp_kses_post('<span class="page-number active__current_page" data-page="'.$i.'">'.$i.'</span>');
1237 //reset ellipses
1238 $renderedEllipses = false;
1239 }
1240
1241 //last page number
1242 else if ($i >= $numOfPages - 1) {
1243 //render link
1244 echo wp_kses_post('<span class="page-number" data-page="'.$i.'">'.$i.'</span>');
1245 }
1246
1247 //make sure you only do this once per ellipses group
1248 else {
1249 if (!$renderedEllipses){
1250 print("...");
1251 $renderedEllipses = true;
1252 }
1253 }
1254 }
1255 ?>
1256
1257 </div>
1258
1259 <div class="is_mobile_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
1260 <?php
1261
1262 $numOfPages = $totalPages;
1263 $renderedEllipses = false;
1264
1265 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
1266
1267 for($i = 1; $i<=$numOfPages; $i++)
1268 {
1269
1270 //render current page number
1271 if($i == (int)$currentPage) {
1272 //render link
1273 echo wp_kses_post('<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
1274 //reset ellipses
1275 $renderedEllipses = false;
1276 }
1277
1278 //last page number
1279 else if ($i >= $numOfPages ) {
1280 //render link
1281 echo wp_kses_post('...<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
1282 }
1283 }
1284 ?>
1285
1286 </div>
1287
1288
1289 <div
1290 class="ep-next " <?php echo empty($nextPageToken) ? ' style="display:none" ' : ''; ?>
1291 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
1292 data-pagetoken="<?php echo esc_attr($nextPageToken) ?>"
1293 data-pagesize="<?php echo intval($options['pagesize']) ?>"
1294 >
1295 <span><?php _e("Next ", "embedpress"); ?> </span>
1296 </div>
1297 </div>
1298 <?php endif; ?>
1299
1300 <div class="ep-loader-wrap">
1301 <div class="ep-loader"><img alt="loading" src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube/spin.gif'); ?>"></div>
1302 </div>
1303
1304 </div>
1305 </div>
1306
1307 <?php
1308 $gallobj->html = ob_get_clean();
1309 else:
1310 $gallobj->html = $this->clean_api_error_html(__("There is nothing on the playlist.", 'embedpress'));
1311 endif;
1312
1313 return $gallobj;
1314 }
1315
1316 public function get_api_key_error_message(){
1317 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>';
1318 }
1319
1320 public function get_id($item){
1321 $vid = isset($item->snippet->resourceId->videoId) ? $item->snippet->resourceId->videoId : null;
1322 $vid = $vid ? $vid : (isset($item->id->videoId) ? $item->id->videoId : null);
1323 $vid = $vid ? $vid : (isset($item->id) ? $item->id : null);
1324 return $vid;
1325 }
1326 public function get_thumbnail_url($item, $quality, $privacyStatus) {
1327 $url = "";
1328 if ($privacyStatus == 'private') {
1329 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/private.png';
1330 } elseif (isset($item->snippet->thumbnails->{$quality}->url)) {
1331 $url = $item->snippet->thumbnails->{$quality}->url;
1332 } elseif (isset($item->snippet->thumbnails->medium->url)) {
1333 $url = $item->snippet->thumbnails->medium->url;
1334 } elseif (isset($item->snippet->thumbnails->default->url)) {
1335 $url = $item->snippet->thumbnails->default->url;
1336 } elseif (isset($item->snippet->thumbnails->high->url)) {
1337 $url = $item->snippet->thumbnails->high->url;
1338 } else {
1339 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/deleted-video-thumb.png';
1340 }
1341 return $url;
1342 }
1343
1344 public function compare_vid_date($a, $b) {
1345 if ($a->snippet->publishedAt == $b->snippet->publishedAt) {
1346 return 0;
1347 }
1348 return ($a->snippet->publishedAt > $b->snippet->publishedAt) ? -1 : 1;
1349 }
1350
1351 public function clean_api_error($raw_message) {
1352 return htmlspecialchars(strip_tags(preg_replace('@&key=[^& ]+@i', '&key=*******', $raw_message)));
1353 }
1354
1355 public function clean_api_error_html($raw_message) {
1356 $clean_html = '';
1357 if ((defined('REST_REQUEST') && REST_REQUEST) || current_user_can('manage_options')) {
1358 $clean_html = '<div>' . __('EmbedPress: ', 'embedpress') . $this->clean_api_error($raw_message) . '</div>';
1359 }
1360 return $clean_html;
1361 }
1362
1363 /** inline {@inheritdoc} */
1364 public function getFakeResponse() {
1365 preg_match('~v=([a-z0-9_\-]+)~i', (string) $this->url, $matches);
1366
1367 $embedUrl = 'https://www.youtube.com/embed/' . $matches['1'] . '?feature=oembed';
1368
1369 $attr = [];
1370 $attr[] = 'width="{width}"';
1371 $attr[] = 'height="{height}"';
1372 $attr[] = 'src="' . esc_url($embedUrl) . '"';
1373 $attr[] = 'frameborder="0"';
1374 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
1375 $attr[] = 'allowfullscreen';
1376
1377 return [
1378 'type' => 'video',
1379 'provider_name' => 'Youtube',
1380 'provider_url' => 'https://www.youtube.com',
1381 'title' => 'Unknown title',
1382 'html' => '<iframe ' . implode(' ', $attr) . '></iframe>',
1383 ];
1384 }
1385
1386 // public $num = 0;
1387
1388
1389
1390 public $x = 0;
1391
1392 public function styles($params, $url){
1393
1394 $uniqid = '.ose-youtube.ose-uid-'.md5($url);
1395
1396 ob_start();
1397 ?>
1398 <style>
1399
1400 <?php
1401 $attributes_data = $params;
1402
1403 $is_pagination = 'flex';
1404
1405 $gap = '30';
1406 $columns = '';
1407
1408 if (isset($attributes_data['ispagination']) && $attributes_data['ispagination']) {
1409 $is_pagination = 'none';
1410 }
1411 if(isset($attributes_data['gapbetweenvideos'])){
1412 $gap = $attributes_data['gapbetweenvideos'];
1413 }
1414 if(isset($attributes_data['columns'])){
1415 $columns = $attributes_data['columns'];
1416 }
1417
1418
1419 if(!empty($columns) && (int) $columns > 0){
1420 $repeatCol = 'repeat(auto-fit, minmax('.esc_html('calc('.(100 / (int) $columns).'% - '.$gap.'px)').', 1fr))';
1421 }
1422 else{
1423 $repeatCol = 'repeat(auto-fit, minmax(calc(250px - '.$gap.'px), 1fr))';
1424 }
1425
1426 ?>
1427 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1428 gap: <?php echo esc_html($gap); ?>px !important;
1429 margin-top: <?php echo esc_html($gap); ?>px !important;
1430 grid-template-columns: <?php echo $repeatCol; ?>;
1431 }
1432 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .ep-youtube__content__pagination {
1433 display: <?php echo esc_html($is_pagination); ?>!important;
1434 }
1435
1436 <?php echo esc_attr($uniqid); ?> .layout-list .ep-youtube__content__block .youtube__content__body .content__wrap{
1437 grid-template-columns: repeat(auto-fit, minmax(calc(100% - 30px), 1fr))!important;
1438 }
1439
1440 @media (max-width: 420px) {
1441 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1442 gap: 30px !important;
1443 }
1444 }
1445
1446 <?php
1447 if($is_pagination){
1448 echo esc_attr($uniqid) ?> {
1449 height: 100%!important;
1450 }
1451 <?php
1452 }
1453 ?>
1454 </style>
1455 <?php
1456 return ob_get_clean();
1457 }
1458 }
1459