PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.4
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.4
4.6.4 4.6.3 4.6.2 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 2 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 11 months ago Giphy.php 2 years ago GitHub.php 3 years ago GoogleCalendar.php 9 months ago GoogleDocs.php 2 years ago GoogleDrive.php 2 years ago GoogleMaps.php 2 years ago GooglePhotos.php 5 months ago Gumroad.php 2 years ago InstagramFeed.php 2 months ago LinkedIn.php 2 years ago Meetup.php 7 months ago NRKRadio.php 2 years ago OneDrive.php 1 year ago OpenSea.php 11 months ago SelfHosted.php 3 months ago Spreaker.php 1 year ago TikTok.php 2 years ago Twitch.php 2 years ago Wistia.php 5 months ago Wrapper.php 3 years ago X.php 2 years ago Youtube.php 2 weeks ago index.html 7 years ago
Youtube.php
1606 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 - prefer the next scheduled stream, so a
350 // /live embed keeps pointing forward between broadcasts (and
351 // switches itself over once that stream starts).
352 $upcoming_video_id = $this->get_upcoming_video_id($channelId, $api_key);
353
354 if (!empty($upcoming_video_id)) {
355 $embedUrl = 'https://www.youtube.com/embed/' . $upcoming_video_id . '?feature=oembed';
356 } else {
357 // Nothing scheduled either - show the last completed stream or latest video
358 $last_video_id = $this->get_last_stream_or_video($channelId, $api_key);
359 if (!empty($last_video_id)) {
360 $embedUrl = 'https://www.youtube.com/embed/' . $last_video_id . '?feature=oembed';
361 } else {
362 // No video found at all
363 $embedUrl = 'https://www.youtube.com/embed/live_stream?channel=' . $channelId . '&feature=oembed';
364 }
365 }
366 }
367 } else {
368 // No API key - use live_stream endpoint as fallback
369 $embedUrl = 'https://www.youtube.com/embed/live_stream?channel='.$channelId.'&feature=oembed';
370 }
371
372 $attr = [];
373 $attr[] = 'width="'.esc_attr($params['maxwidth']).'"';
374 $attr[] = 'height="'.esc_attr($params['maxheight']).'"';
375 $attr[] = 'src="' . esc_url($embedUrl) . '"';
376 $attr[] = 'frameborder="0"';
377 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
378 $attr[] = 'allowfullscreen';
379 $attr[] = 'referrerpolicy="origin"';
380
381 $results['html'] = '<iframe ' . implode(' ', $attr) . '></iframe>';
382 }
383 else if($this->isPlaylist()){
384 // User can opt-out of the playlist UI via ytPlaylistMode=single.
385 // For watch?v=…&list=… URLs, this renders just the v= video as a
386 // normal single-video oembed. For /playlist?list=… URLs (no v=),
387 // 'single' means "render the playlist's first item as a single
388 // video" — getStartVideoID() falls back to the playlist's lead
389 // item via the YouTube API.
390 $mode = isset($params['ytPlaylistMode']) ? $params['ytPlaylistMode'] : 'playlist';
391 if ($mode === 'single') {
392 $single_vid = $this->getStartVideoID();
393 if (empty($single_vid)) {
394 // /playlist?list=… URL — fetch first item from playlistItems.
395 $first = $this->get_playlist_first_video($this->getPlaylistID());
396 if (!empty($first)) {
397 $single_vid = $first;
398 }
399 }
400 if (!empty($single_vid)) {
401 $embedUrl = 'https://www.youtube.com/embed/' . rawurlencode($single_vid) . '?feature=oembed';
402 $w = esc_attr($params['maxwidth']);
403 $h = esc_attr($params['maxheight']);
404 $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>';
405 return $results;
406 }
407 // No video resolvable — fall through to playlist UI as a sane fallback.
408 }
409 $playlist = $this->getPlaylistGallery();
410 $results = array_merge($results, $playlist);
411 }
412 else if($this->isChannel()){
413 $channel = $this->getChannelGallery();
414 $results = array_merge($results, $channel);
415 }
416 else {
417 // Regular video or playlist URL — build the embed iframe
418 // deterministically. This is the fallback path that lets
419 // YT embeds keep working when YouTube's oEmbed endpoint
420 // refuses our server IP (fbs-81925).
421 //
422 // Three shapes handled:
423 // - watch?v=X&list=PL... -> /embed/X?list=PL... (video in playlist)
424 // - playlist?list=PL... -> /embed/videoseries?list= (playlist only)
425 // - watch?v=X (or shorts/youtu.be) -> /embed/X (single video)
426 $videoId = $this->extractVideoId($this->url);
427 $playlistId = $this->extractPlaylistId($this->url);
428
429 $embedPath = '';
430 if (!empty($videoId)) {
431 $embedPath = $videoId;
432 } elseif (!empty($playlistId)) {
433 $embedPath = 'videoseries';
434 }
435
436 if (!empty($embedPath)) {
437 $query = ['feature' => 'oembed'];
438 if (!empty($playlistId)) {
439 $query['list'] = $playlistId;
440 }
441 $embedUrl = 'https://www.youtube.com/embed/' . $embedPath . '?' . http_build_query($query);
442 $maxw = isset($params['maxwidth']) ? $params['maxwidth'] : 600;
443 $maxh = isset($params['maxheight']) ? $params['maxheight'] : 340;
444 $attr = [];
445 $attr[] = 'width="' . esc_attr($maxw) . '"';
446 $attr[] = 'height="' . esc_attr($maxh) . '"';
447 $attr[] = 'src="' . esc_url($embedUrl) . '"';
448 $attr[] = 'frameborder="0"';
449 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
450 $attr[] = 'allowfullscreen';
451 $attr[] = 'referrerpolicy="origin"';
452 $results['html'] = '<iframe ' . implode(' ', $attr) . '></iframe>';
453 $results['title'] = '';
454 }
455 }
456
457 return $results;
458 }
459
460 public function getChannelPlaylist(){
461 $result = [
462 "playlistID" => '',
463 "title" => '',
464 ];
465 $channel = $this->getChannel();
466 $channel_url = $this->constructUrl('channels', $this->getParams());
467 $transient_key = 'ep_embed_youtube_channel_playlist_id_' . md5($channel_url);
468 $jsonResult = get_transient($transient_key);
469
470 if(!empty($jsonResult)){
471 return $jsonResult;
472 }
473
474 if($channel['type'] == 'user' || $channel['type'] == 'c'){
475 $this->getChannelIDbyUsername();
476 $channel_url = $this->constructUrl('channels', $this->getParams());
477 }
478
479 if (empty($this->get_api_key())) {
480 $result['error'] = true;
481 $result['html'] = $this->get_api_key_error_message();
482 return $result;
483 }
484
485 $apiResult = wp_remote_get($channel_url, array('timeout' => self::$curltimeout));
486 if (is_wp_error($apiResult)) {
487 $result['error'] = true;
488 $result['html'] = $this->clean_api_error_html($apiResult->get_error_message(), true);
489 set_transient($transient_key, $result, 10);
490 return $result;
491 }
492 $jsonResult = json_decode($apiResult['body']);
493
494
495 if (isset($jsonResult->error)) {
496 $result['error'] = true;
497 if (isset($jsonResult->error->message)) {
498 $result['html'] = $this->clean_api_error_html($jsonResult->error->message, true);
499 }
500 else{
501 $result['html'] = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
502 }
503 set_transient($transient_key, $result, MINUTE_IN_SECONDS);
504 return $result;
505 }
506 elseif(!empty($jsonResult->items[0]->contentDetails->relatedPlaylists->uploads)){
507 $result['playlistID'] = $jsonResult->items[0]->contentDetails->relatedPlaylists->uploads;
508 $result['title'] = isset($jsonResult->items[0]->snippet->title) ? $jsonResult->items[0]->snippet->title : '';
509 set_transient($transient_key, $result, DAY_IN_SECONDS);
510 }
511
512 return $result;
513 }
514
515 public function get_youtube_handler($url){
516 preg_match('/^https?:\/\/(?:www\.)?youtube\.com\/@([^\/?]+)/i', $url, $matches);
517
518 $handle_name = '';
519 if(!empty($matches[1])){
520 $handle_name = $matches[1];
521 }
522
523 return $handle_name;
524 }
525
526 public function getChannelIDbyUsername(){
527 $url = $this->getUrl();
528 $apiResult = wp_remote_get($url, array('timeout' => self::$curltimeout));
529
530 if (!is_wp_error($apiResult)) {
531 $channel_html = $apiResult['body'];
532 preg_match("/<meta\s+itemprop=[\"']channelId[\"']\s+content=[\"'](.*?)[\"']\/?>/", $channel_html, $matches);
533 if(!empty($matches[1])){
534 $url = "https://www.youtube.com/channel/{$matches[1]}";
535 $url = $this->normalizeUrl(new Url($url));
536 }
537 }
538 }
539
540
541 public function get_channel_id_by_handler($handle)
542 {
543 $transient_key = 'channel_id_' . md5($handle);
544 $channel_id = get_transient($transient_key);
545
546 if (false !== $channel_id && preg_match('/^UC[\w-]+$/', $channel_id)) {
547 return $channel_id;
548 }
549
550 // Prefer the YouTube Data API when a key is configured. Scraping
551 // youtube.com from EU server IPs gets bounced to consent.youtube.com
552 // and the response contains neither the canonical channel link nor
553 // externalId, so the regex fallback below silently returns ''.
554 $api_key = $this->get_api_key();
555 if (!empty($api_key)) {
556 $api_url = self::$channel_endpoint . 'channels?' . http_build_query([
557 'part' => 'id',
558 'forHandle' => '@' . $handle,
559 'key' => $api_key,
560 ]);
561 $api_response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
562 if (!is_wp_error($api_response)) {
563 $api_body = json_decode(wp_remote_retrieve_body($api_response));
564 if (!empty($api_body->items[0]->id) && preg_match('/^UC[\w-]+$/', $api_body->items[0]->id)) {
565 $channel_id = $api_body->items[0]->id;
566 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
567 return $channel_id;
568 }
569 }
570 }
571
572 $channel_handle = "https://www.youtube.com/@{$handle}";
573
574 // Bypass YouTube's EU consent redirect by sending the CONSENT cookie
575 // and a real browser UA. Without this the response is a consent page
576 // when the request originates from an EU IP.
577 $response = wp_remote_get($channel_handle, [
578 'timeout' => self::$curltimeout,
579 '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',
580 'headers' => [
581 'Accept-Language' => 'en-US,en;q=0.9',
582 'Cookie' => 'CONSENT=YES+cb.20210328-17-p0.en+FX+000',
583 ],
584 ]);
585
586 if (is_wp_error($response)) {
587 return '';
588 }
589
590 $body = wp_remote_retrieve_body($response);
591
592 if (empty($body)) {
593 return '';
594 }
595
596 // Try canonical link first (most reliable)
597 $pattern = '/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/([^"]{1,50})">/';
598 if (preg_match($pattern, $body, $matches)) {
599 $channel_id = $matches[1];
600 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
601 return $channel_id;
602 }
603
604 // Fallback: try externalId from page data
605 if (preg_match('/"externalId"\s*:\s*"(UC[a-zA-Z0-9_-]+)"/', $body, $matches)) {
606 $channel_id = $matches[1];
607 set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
608 return $channel_id;
609 }
610
611 return '';
612 }
613
614 /**
615 * Check if a channel has an active live stream and return the video ID.
616 */
617 public function get_live_video_id($channel_id, $api_key) {
618 $transient_key = 'ep_yt_live_' . md5($channel_id);
619 $cached = get_transient($transient_key);
620
621 if (false !== $cached) {
622 return $cached;
623 }
624
625 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
626 'part' => 'id',
627 'channelId' => $channel_id,
628 'eventType' => 'live',
629 'type' => 'video',
630 'key' => $api_key,
631 ]);
632
633 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
634
635 if (is_wp_error($response)) {
636 return '';
637 }
638
639 $data = json_decode(wp_remote_retrieve_body($response));
640
641 if (!empty($data->items[0]->id->videoId)) {
642 $video_id = $data->items[0]->id->videoId;
643 set_transient($transient_key, $video_id, 2 * MINUTE_IN_SECONDS);
644 return $video_id;
645 }
646
647 // Cache empty result briefly to avoid repeated API calls
648 set_transient($transient_key, '', MINUTE_IN_SECONDS);
649 return '';
650 }
651
652 /**
653 * Get the channel's next scheduled (upcoming) live stream.
654 *
655 * youtube.com/channel/<id>/live points at the upcoming broadcast while a
656 * channel is between streams, so a /live embed has to do the same instead
657 * of dropping straight to the last completed one.
658 *
659 * search() can only order upcoming events by publish date, which is not
660 * the same as start time, so the candidates get sorted by their real
661 * scheduledStartTime and the soonest future one wins.
662 */
663 public function get_upcoming_video_id($channel_id, $api_key) {
664 $transient_key = 'ep_yt_upcoming_' . md5($channel_id);
665 $cached = get_transient($transient_key);
666
667 if (false !== $cached) {
668 return $cached;
669 }
670
671 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
672 'part' => 'id',
673 'channelId' => $channel_id,
674 'eventType' => 'upcoming',
675 'type' => 'video',
676 'order' => 'date',
677 'maxResults' => 10,
678 'key' => $api_key,
679 ]);
680
681 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
682
683 if (is_wp_error($response)) {
684 return '';
685 }
686
687 $data = json_decode(wp_remote_retrieve_body($response));
688
689 $video_ids = [];
690 if (!empty($data->items) && is_array($data->items)) {
691 foreach ($data->items as $item) {
692 if (!empty($item->id->videoId)) {
693 $video_ids[] = $item->id->videoId;
694 }
695 }
696 }
697
698 if (empty($video_ids)) {
699 set_transient($transient_key, '', MINUTE_IN_SECONDS);
700 return '';
701 }
702
703 $video_id = $this->get_soonest_scheduled_video($video_ids, $api_key);
704
705 if (false === $video_id) {
706 // Scheduling details unavailable (request failed / no usable
707 // response) — nothing was actually judged, so fall back to the
708 // newest upcoming item rather than losing the stream altogether.
709 $video_id = $video_ids[0];
710 } elseif ('' === $video_id) {
711 // Every candidate WAS judged and rejected: their scheduled start is
712 // already in the past, i.e. events YouTube still lists as upcoming
713 // that never went live. Falling back to $video_ids[0] here would
714 // re-select exactly what the filter just threw out (search orders by
715 // publish date, so the stale event is usually first) and pin the
716 // embed to a broadcast that will never start. Report "nothing
717 // upcoming" so the caller drops to the last completed stream.
718 set_transient($transient_key, '', MINUTE_IN_SECONDS);
719 return '';
720 }
721
722 set_transient($transient_key, $video_id, 2 * MINUTE_IN_SECONDS);
723 return $video_id;
724 }
725
726 /**
727 * Given a set of upcoming video IDs, return the one starting soonest.
728 *
729 * Only streams still scheduled in the future are considered. YouTube keeps
730 * an event in eventType=upcoming even when its scheduled start has already
731 * passed and it never went live, and such a stale entry would otherwise
732 * always hold the earliest timestamp and pin the embed to a broadcast that
733 * is never going to start.
734 *
735 * The two empty outcomes are NOT interchangeable, so they return distinct
736 * values — collapsing them is what let a rejected event get re-selected:
737 *
738 * false → the lookup could not be performed (request failed, unusable
739 * response). Nothing was judged; the caller may fall back.
740 * '' → candidates were judged and ALL rejected as past-dated. The
741 * caller must NOT fall back to one of them.
742 *
743 * @return string|false Video ID, '' if all rejected, false if unavailable.
744 */
745 protected function get_soonest_scheduled_video($video_ids, $api_key) {
746 $api_url = self::$channel_endpoint . 'videos?' . http_build_query([
747 'part' => 'liveStreamingDetails',
748 'id' => implode(',', $video_ids),
749 'key' => $api_key,
750 ]);
751
752 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
753
754 if (is_wp_error($response)) {
755 // Lookup unavailable — no candidate was judged.
756 return false;
757 }
758
759 $data = json_decode(wp_remote_retrieve_body($response));
760
761 if (empty($data->items) || !is_array($data->items)) {
762 // Same: nothing came back to judge.
763 return false;
764 }
765
766 $soonest = '';
767 $soonest_ts = 0;
768 $now = time();
769
770 foreach ($data->items as $item) {
771 if (empty($item->id) || empty($item->liveStreamingDetails->scheduledStartTime)) {
772 continue;
773 }
774
775 $timestamp = strtotime($item->liveStreamingDetails->scheduledStartTime);
776
777 if (!$timestamp || $timestamp < $now) {
778 continue;
779 }
780
781 if (empty($soonest) || $timestamp < $soonest_ts) {
782 $soonest = $item->id;
783 $soonest_ts = $timestamp;
784 }
785 }
786
787 return $soonest;
788 }
789
790 /**
791 * Get the last completed live stream or latest video from a channel.
792 * Tries completed streams first, falls back to latest upload.
793 */
794 public function get_last_stream_or_video($channel_id, $api_key) {
795 $transient_key = 'ep_yt_last_stream_' . md5($channel_id);
796 $cached = get_transient($transient_key);
797
798 if (false !== $cached) {
799 return $cached;
800 }
801
802 // First try: get the last completed live stream
803 $api_url = self::$channel_endpoint . 'search?' . http_build_query([
804 'part' => 'id',
805 'channelId' => $channel_id,
806 'eventType' => 'completed',
807 'type' => 'video',
808 'order' => 'date',
809 'maxResults' => 1,
810 'key' => $api_key,
811 ]);
812
813 $response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
814
815 if (!is_wp_error($response)) {
816 $data = json_decode(wp_remote_retrieve_body($response));
817 if (!empty($data->items[0]->id->videoId)) {
818 $video_id = $data->items[0]->id->videoId;
819 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
820 return $video_id;
821 }
822 }
823
824 // Fallback: get the latest video from the channel's uploads playlist
825 $channel_url = self::$channel_endpoint . 'channels?' . http_build_query([
826 'part' => 'contentDetails',
827 'id' => $channel_id,
828 'key' => $api_key,
829 ]);
830
831 $ch_response = wp_remote_get($channel_url, ['timeout' => self::$curltimeout]);
832
833 if (!is_wp_error($ch_response)) {
834 $ch_data = json_decode(wp_remote_retrieve_body($ch_response));
835 $uploads_playlist = $ch_data->items[0]->contentDetails->relatedPlaylists->uploads ?? '';
836
837 if (!empty($uploads_playlist)) {
838 $playlist_url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
839 'part' => 'snippet',
840 'playlistId' => $uploads_playlist,
841 'maxResults' => 1,
842 'key' => $api_key,
843 ]);
844
845 $pl_response = wp_remote_get($playlist_url, ['timeout' => self::$curltimeout]);
846
847 if (!is_wp_error($pl_response)) {
848 $pl_data = json_decode(wp_remote_retrieve_body($pl_response));
849 if (!empty($pl_data->items[0]->snippet->resourceId->videoId)) {
850 $video_id = $pl_data->items[0]->snippet->resourceId->videoId;
851 set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
852 return $video_id;
853 }
854 }
855 }
856 }
857
858 set_transient($transient_key, '', 2 * MINUTE_IN_SECONDS);
859 return '';
860 }
861
862 public function layout_data(){
863 $data = [];
864 $data['get_pagesize'] = $this->get_pagesize();
865 $data['get_api_key'] = $this->get_api_key();
866 $data['get_api_key_error_message'] = $this->get_api_key_error_message();
867 $data['get_channel_info'] = $this->get_channel_info();
868 $data['get_api_key'] = $this->get_api_key();
869 $data['curltimeout'] = self::$curltimeout;
870 $data['self::class'] = self::class;
871
872 return $data;
873
874 }
875
876
877 /** inline {@inheritdoc} */
878 public function getChannelGallery() {
879 $response = [];
880 $channel = $this->getChannelPlaylist();
881 if(!empty($channel['error'])){
882 return $channel;
883 }
884 if (!empty($channel["playlistID"])) {
885 return $this->buildGallery($channel["playlistID"], $channel['title']);
886 }
887 elseif ($this->isChannel() && empty($this->get_api_key()) && current_user_can('manage_options')) {
888 return [
889 "html" => "<div class='ep-player-wrap'>" . __('Please enter your YouTube API key to embed YouTube Channel.', 'embedpress') . "</div>",
890 ];
891 }
892
893 return $response;
894 }
895
896 /**
897 * Look up the first video ID inside a playlist (for 'single' mode when
898 * the URL has no v=). Cached 1d alongside the playlist info call.
899 */
900 public function get_playlist_first_video($playlist_id) {
901 $api_key = $this->get_api_key();
902 if (empty($api_key) || empty($playlist_id)) {
903 return '';
904 }
905 $transient_key = 'ep_yt_playlist_first_' . md5($playlist_id);
906 $cached = get_transient($transient_key);
907 if (is_string($cached) && $cached !== '') {
908 return $cached;
909 }
910 $url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
911 'part' => 'snippet,contentDetails',
912 'playlistId' => $playlist_id,
913 'maxResults' => 1,
914 'key' => $api_key,
915 ]);
916 $response = wp_remote_get($url, ['timeout' => self::$curltimeout]);
917 if (is_wp_error($response)) {
918 return '';
919 }
920 $body = json_decode(wp_remote_retrieve_body($response));
921 $vid = isset($body->items[0]) ? Helper::get_id($body->items[0]) : '';
922 if (!empty($vid)) {
923 set_transient($transient_key, $vid, DAY_IN_SECONDS);
924 }
925 return (string) $vid;
926 }
927
928 /**
929 * Fetch playlist metadata (title, channel, item count, thumbnail) from
930 * youtube/v3/playlists. Cached 1d. Returns [] on failure.
931 */
932 public function get_playlist_info($playlist_id) {
933 $api_key = $this->get_api_key();
934 if (empty($api_key) || empty($playlist_id)) {
935 return [];
936 }
937
938 $transient_key = 'ep_yt_playlist_info_' . md5($playlist_id);
939 $cached = get_transient($transient_key);
940 if (is_array($cached)) {
941 return $cached;
942 }
943
944 $url = self::$channel_endpoint . 'playlists?' . http_build_query([
945 'part' => 'snippet,contentDetails',
946 'id' => $playlist_id,
947 'key' => $api_key,
948 ]);
949
950 $response = wp_remote_get($url, ['timeout' => self::$curltimeout]);
951 if (is_wp_error($response)) {
952 return [];
953 }
954 $body = json_decode(wp_remote_retrieve_body($response));
955 if (empty($body->items[0])) {
956 set_transient($transient_key, [], MINUTE_IN_SECONDS);
957 return [];
958 }
959
960 $item = $body->items[0];
961 $info = [
962 'id' => $playlist_id,
963 'title' => isset($item->snippet->title) ? $item->snippet->title : '',
964 'channel_title' => isset($item->snippet->channelTitle) ? $item->snippet->channelTitle : '',
965 'channel_id' => isset($item->snippet->channelId) ? $item->snippet->channelId : '',
966 'description' => isset($item->snippet->description) ? $item->snippet->description : '',
967 'item_count' => isset($item->contentDetails->itemCount) ? (int) $item->contentDetails->itemCount : 0,
968 'thumbnail' => isset($item->snippet->thumbnails->medium->url)
969 ? $item->snippet->thumbnails->medium->url
970 : (isset($item->snippet->thumbnails->default->url) ? $item->snippet->thumbnails->default->url : ''),
971 'playlist_url' => 'https://www.youtube.com/playlist?list=' . rawurlencode($playlist_id),
972 ];
973
974 set_transient($transient_key, $info, DAY_IN_SECONDS);
975 return $info;
976 }
977
978 /**
979 * Render a playlist URL (youtube.com/playlist?list=PL…) as a queue/gallery,
980 * reusing the same layout engine as channels.
981 */
982 public function getPlaylistGallery() {
983 $playlist_id = $this->getPlaylistID();
984 if (empty($playlist_id)) {
985 return [];
986 }
987
988 if (empty($this->get_api_key())) {
989 if (current_user_can('manage_options')) {
990 return [
991 "html" => "<div class='ep-player-wrap'>" . $this->get_api_key_error_message() . "</div>",
992 ];
993 }
994 return [];
995 }
996
997 $playlist_info = $this->get_playlist_info($playlist_id);
998 $title = !empty($playlist_info['title']) ? $playlist_info['title'] : '';
999 $start_vid = $this->getStartVideoID();
1000 if (!empty($start_vid)) {
1001 $playlist_info['start_vid'] = $start_vid;
1002 }
1003
1004 return $this->buildGallery($playlist_id, $title, $playlist_info);
1005 }
1006
1007 /**
1008 * Shared gallery renderer for both channel uploads playlists and
1009 * standalone playlist URLs. Extracted from getChannelGallery() so the
1010 * layout/pagination/iframe code path stays a single source of truth.
1011 *
1012 * @param string $playlist_id YouTube playlist ID.
1013 * @param string $title Display title (channel name or playlist title).
1014 * @param array $playlist_info Optional playlist metadata from get_playlist_info() — non-empty when rendering a standalone playlist URL.
1015 */
1016 protected function buildGallery($playlist_id, $title = '', $playlist_info = []) {
1017 $params = $this->getParams();
1018 $is_playlist = !empty($playlist_info);
1019 $gallery_args = [
1020 'playlistId' => $playlist_id,
1021 ];
1022 // Carry the user-configured height through to the playlist shells
1023 // (queue/theatre) so it drives the whole wrapper, not just the
1024 // hardcoded CSS default. Width stays fluid (100%); only height is
1025 // user-controlled here.
1026 if (!empty($params['maxheight'])) {
1027 $gallery_args['maxheight'] = (int) $params['maxheight'];
1028 }
1029 if (!empty($params['pagesize'])) {
1030 $gallery_args['pagesize'] = $params['pagesize'];
1031 } elseif ($is_playlist) {
1032 // Queue layout — render enough items to fill the scrollable area
1033 // out of the box. The JS auto-fetches more on scroll regardless.
1034 $gallery_args['pagesize'] = 12;
1035 }
1036
1037 // Layout selection differs by source:
1038 // Channel URLs → ytChannelLayout (gallery/list/grid/carousel) [unchanged]
1039 // Playlist URLs → ytPlaylistLayout (queue/theatre, more to come)
1040 // Each surface has its own attribute so a saved channel pick can't
1041 // leak into a playlist render and vice versa.
1042 if ($is_playlist) {
1043 // Free layouts always render. Pro layouts (library/spotlight/
1044 // cinema/magazine) only render when Pro is active; otherwise we
1045 // fall back to queue and the inspector shows the upsell.
1046 $playlist_layouts = ['queue', 'theatre', 'library', 'spotlight', 'cinema', 'magazine'];
1047 $pro_playlist_layouts = ['library', 'spotlight', 'cinema', 'magazine'];
1048 $picked = isset($params['ytPlaylistLayout']) ? $params['ytPlaylistLayout'] : '';
1049 if (!in_array($picked, $playlist_layouts, true)) {
1050 $picked = 'queue';
1051 }
1052 if (in_array($picked, $pro_playlist_layouts, true) && !apply_filters('embedpress/is_allow_rander', false)) {
1053 $picked = 'queue';
1054 }
1055 $layout = $picked;
1056 } else {
1057 $default_layout = 'gallery';
1058 $layout = isset($params['ytChannelLayout']) && $params['ytChannelLayout'] !== ''
1059 ? $params['ytChannelLayout']
1060 : $default_layout;
1061 }
1062
1063 $layout_data = $this->layout_data();
1064 $layout_data['is_playlist'] = $is_playlist;
1065 $layout_data['playlist_info'] = $playlist_info;
1066
1067 // For playlist URLs the channel-info fetch returns a string error
1068 // ("No channel information found") because get_channel_info() targets
1069 // channel URLs. Pro's youtube_carousel_layout dereferences this as
1070 // an array, so feed it a synthetic shape sourced from playlist_info.
1071 if ($is_playlist) {
1072 $layout_data['get_channel_info'] = [
1073 'snippet' => [
1074 'title' => !empty($playlist_info['channel_title']) ? $playlist_info['channel_title'] : (!empty($playlist_info['title']) ? $playlist_info['title'] : ''),
1075 'thumbnails' => [
1076 'default' => ['url' => !empty($playlist_info['thumbnail']) ? $playlist_info['thumbnail'] : ''],
1077 'high' => ['url' => !empty($playlist_info['thumbnail']) ? $playlist_info['thumbnail'] : ''],
1078 ],
1079 ],
1080 ];
1081 }
1082
1083 $channel_layout = 'layout-' . $layout;
1084
1085 $gallery = YoutubeLayout::create_youtube_layout($gallery_args, $layout_data, $layout, $this->url);
1086
1087 $main_iframe = '';
1088 // Queue layout renders its own leading player; gallery layout uses ep-first-video block.
1089 if (!empty($gallery->first_vid) && $layout === 'gallery') {
1090 $rel = esc_url("https://www.youtube.com/embed/{$gallery->first_vid}?feature=oembed");
1091 $iframe_width = esc_attr($params['maxwidth']);
1092 $iframe_height = esc_attr($params['maxheight']);
1093 $iframe_title = esc_attr($title);
1094 $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>";
1095 }
1096
1097 if (!apply_filters('embedpress/is_allow_rander', false) && ($layout === 'grid' || $layout === 'carousel')) {
1098 return [];
1099 }
1100
1101 if (!empty($gallery->html)) {
1102 // Queue layout owns its own styling; skip the gallery/grid <style> block.
1103 $styles = $layout === 'queue' ? '' : $this->styles($params, $this->getUrl());
1104 $html_content = $main_iframe . $gallery->html . ' ' . $styles;
1105
1106 return [
1107 "title" => $title,
1108 "html" => "<div class='ep-player-wrap $channel_layout'>$html_content</div>",
1109 ];
1110 }
1111
1112 return [];
1113 }
1114
1115
1116 public function get_channel_info() {
1117 $api_key = $this->get_api_key();
1118 $channel_id = $this->getChannel($this->url);
1119 $channel_id = $channel_id['id'];
1120
1121
1122 // Create a unique transient key based on the channel ID
1123 $transient_key = 'youtube_channel_info_' . $channel_id;
1124
1125 // Attempt to get cached response
1126 $channel_info = get_transient($transient_key);
1127
1128 if ($channel_info === false) {
1129 // No cached response, make the API call
1130 $endpoint = "https://www.googleapis.com/youtube/v3/channels?part=snippet%2Cstatistics&id=$channel_id&key=$api_key";
1131 $response = wp_remote_get($endpoint);
1132
1133 if (is_wp_error($response)) {
1134 return 'Error fetching channel info';
1135 }
1136
1137 $body = wp_remote_retrieve_body($response);
1138 $data = json_decode($body, true);
1139
1140 if (empty($data['items'])) {
1141 return 'No channel information found';
1142 }
1143
1144 $channel_info = $data['items'][0];
1145
1146
1147 // Cache the response for 1 hour
1148 set_transient($transient_key, $channel_info, HOUR_IN_SECONDS);
1149 }
1150
1151 update_option('youtube_channel_info_'. md5($this->url), $channel_info);
1152
1153
1154 return $channel_info;
1155
1156 }
1157
1158
1159 public function get_layout() {
1160 $params = $this->getParams();
1161 return isset($params['ytChannelLayout']) ? $params['ytChannelLayout'] : '';
1162 }
1163
1164 /**
1165 * Undocumented function
1166 *
1167 * @param array $options
1168 * @return object
1169 */
1170 public function get_gallery_page($options) {
1171
1172 $nextPageToken = '';
1173 $prevPageToken = '';
1174 $gallobj = new \stdClass();
1175 $options = wp_parse_args($options, [
1176 'playlistId' => '',
1177 'pageToken' => '',
1178 'pagesize' => $this->get_pagesize() ? $this->get_pagesize() : 6,
1179 'currentpage' => '',
1180 'columns' => 3,
1181 'ytChannelLayout' => 'gallery',
1182 'thumbnail' => 'medium',
1183 'gallery' => true,
1184 'autonext' => true,
1185 'thumbplay' => true,
1186 'apiKey' => $this->get_api_key(),
1187 'hideprivate' => '',
1188 ]);
1189 $options['pagesize'] = $options['pagesize'] > 50 ? 50 : $options['pagesize'];
1190 $options['pagesize'] = $options['pagesize'] < 1 ? 1 : $options['pagesize'];
1191
1192 if (empty($options['apiKey'])) {
1193 $gallobj->html = $this->get_api_key_error_message();
1194 return $gallobj;
1195 }
1196
1197 $apiEndpoint = 'https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,status,contentDetails&playlistId=' . $options['playlistId']
1198 . '&maxResults=' . $options['pagesize']
1199 . '&key=' . $options['apiKey'];
1200 if ($options['pageToken'] != null) {
1201 $apiEndpoint .= '&pageToken=' . $options['pageToken'];
1202 }
1203
1204
1205
1206 $transient_key = 'ep_embed_youtube_channel_' . md5($apiEndpoint);
1207 $gallobj->transient_key = $transient_key;
1208 $jsonResult = get_transient($transient_key);
1209
1210
1211 if (empty($jsonResult)) {
1212 $apiResult = wp_remote_get($apiEndpoint, array('timeout' => self::$curltimeout));
1213 if (is_wp_error($apiResult)) {
1214 $gallobj->html = $this->clean_api_error_html($apiResult->get_error_message(), true);
1215 return $gallobj;
1216 }
1217 $jsonResult = json_decode($apiResult['body']);
1218
1219 if (empty($jsonResult->error)) {
1220 set_transient($transient_key, $jsonResult, MINUTE_IN_SECONDS * 20);
1221 }
1222 else{
1223 set_transient($transient_key, $jsonResult, 10);
1224 }
1225 }
1226
1227
1228
1229 if (isset($jsonResult->error)) {
1230 if(!empty($jsonResult->error->errors[0]->reason) && $jsonResult->error->errors[0]->reason == 'playlistNotFound'){
1231 $gallobj->html = $this->clean_api_error_html(__('There is nothing on the playlist.', 'embedpress'));
1232 return $gallobj;
1233 }
1234 if (isset($jsonResult->error->message)) {
1235 $gallobj->html = $this->clean_api_error_html($jsonResult->error->message);
1236 return $gallobj;
1237 }
1238 $gallobj->html = $this->clean_api_error_html(__('Sorry, there may be an issue with your YouTube API key.', 'embedpress'));
1239 return $gallobj;
1240 }
1241
1242
1243
1244 $resultsPerPage = $jsonResult->pageInfo->resultsPerPage;
1245 $totalResults = $jsonResult->pageInfo->totalResults;
1246 $totalPages = ceil($totalResults / $resultsPerPage);
1247
1248 if (isset($jsonResult->nextPageToken)) {
1249 $nextPageToken = $jsonResult->nextPageToken;
1250 }
1251
1252 if (isset($jsonResult->prevPageToken)) {
1253 $prevPageToken = $jsonResult->prevPageToken;
1254 }
1255
1256
1257 if (!empty($jsonResult->items) && is_array($jsonResult->items)) :
1258 if($options['gallery'] === "false"){
1259 $gallobj->html = "";
1260 if(count($jsonResult->items) === 1){
1261 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
1262 }
1263 return $gallobj;
1264 }
1265
1266 if(count($jsonResult->items) === 1 && empty($nextPageToken) && empty($prevPageToken)){
1267 $gallobj->first_vid = $this->get_id($jsonResult->items[0]);
1268 $gallobj->html = "";
1269 return $gallobj;
1270 }
1271
1272 if (strpos($options['playlistId'], 'UU') === 0) {
1273 // sort only channels
1274 usort($jsonResult->items, array(self::class, 'compare_vid_date')); // sorts in place
1275 }
1276
1277 ob_start();
1278
1279 ?>
1280
1281 <div class="ep-youtube__content__block" data-unique-id="<?php echo wp_rand(); ?>">
1282 <div class="youtube__content__body">
1283 <?php
1284 ?>
1285
1286 <div class="content__wrap">
1287
1288 <?php
1289
1290 $data = $this->layout_data();
1291
1292
1293 $channel_info = get_option('youtube_channel_info_'.md5($options['channel_url']));
1294
1295 $channelTitle = isset($channel_info['snippet']['title']) ? $channel_info['snippet']['title'] : null;
1296 $channelThumb = isset($channel_info['snippet']['thumbnails']['high']['url']) ? $channel_info['snippet']['thumbnails']['high']['url'] : null;
1297 $layout = $this->get_layout();
1298
1299
1300 if($layout === 'gallery'){
1301 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1302 }
1303 else if($layout === 'grid'){
1304 do_action('embedpress/youtube_grid_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1305 }
1306 else if($layout === 'list'){
1307 echo YoutubeLayout::create_list_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1308 }
1309 else if($layout === 'carousel'){
1310 do_action('embedpress/youtube_carousel_layout', $jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1311 }
1312 else{
1313 echo YoutubeLayout::create_gallery_layout($jsonResult, $gallobj, $options, $data, $channelTitle, $channelThumb);
1314
1315 }
1316
1317 ?>
1318 <!--
1319 <?php foreach ($jsonResult->items as $item) : ?>
1320 <?php
1321 $privacyStatus = isset($item->status->privacyStatus) ? $item->status->privacyStatus : null;
1322 $thumbnail = $this->get_thumbnail_url($item, $options['thumbnail'], $privacyStatus);
1323 $vid = $this->get_id($item);
1324 if (empty($gallobj->first_vid)) {
1325 $gallobj->first_vid = $vid;
1326 }
1327 if ($privacyStatus == 'private' && $options['hideprivate']) {
1328 continue;
1329 }
1330 ?>
1331 <div class="item" data-vid="<?php echo $vid; ?>">
1332 <div class="thumb" style="background: <?php echo "url({$thumbnail}) no-repeat center"; ?>">
1333 <div class="play-icon">
1334 <img src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube.svg'); ?>" alt="">
1335 </div>
1336 </div>
1337 <div class="body">
1338 <p><?php echo esc_html($item->snippet->title); ?></p>
1339 </div>
1340 </div>
1341
1342 <?php endforeach; ?> -->
1343
1344 <div class="item" style="height: 0"></div>
1345 </div>
1346
1347
1348 <?php
1349 $layout = $this->get_layout();
1350
1351 if ($totalPages > 1 && $layout !== 'carousel') : ?>
1352 <div class="ep-youtube__content__pagination <?php echo (empty($prevPageToken) && empty($nextPageToken)) ? ' hide ' : ''; ?>">
1353 <div
1354 class="ep-prev" <?php echo empty($prevPageToken) ? ' style="display:none" ' : ''; ?>
1355 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
1356 data-pagetoken="<?php echo esc_attr($prevPageToken) ?>"
1357 data-pagesize="<?php echo intval($options['pagesize']) ?>"
1358 >
1359 <span><?php _e("Prev", "embedpress"); ?></span>
1360 </div>
1361 <div class="is_desktop_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
1362 <?php
1363
1364 $numOfPages = $totalPages;
1365 $renderedEllipses = false;
1366
1367 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
1368
1369 for($i = 1; $i<=$numOfPages; $i++)
1370 {
1371 //render pages 1 - 3
1372 if($i < 4) {
1373 //render link
1374 $is_current = $i == (int)$currentPage? "active__current_page" : "";
1375
1376 echo wp_kses_post("<span class='page-number $is_current' data-page='$i'>$i</span>");
1377
1378 }
1379
1380 //render current page number
1381 else if($i == (int)$currentPage) {
1382 //render link
1383 echo wp_kses_post('<span class="page-number active__current_page" data-page="'.$i.'">'.$i.'</span>');
1384 //reset ellipses
1385 $renderedEllipses = false;
1386 }
1387
1388 //last page number
1389 else if ($i >= $numOfPages - 1) {
1390 //render link
1391 echo wp_kses_post('<span class="page-number" data-page="'.$i.'">'.$i.'</span>');
1392 }
1393
1394 //make sure you only do this once per ellipses group
1395 else {
1396 if (!$renderedEllipses){
1397 print("...");
1398 $renderedEllipses = true;
1399 }
1400 }
1401 }
1402 ?>
1403
1404 </div>
1405
1406 <div class="is_mobile_device ep-page-numbers <?php echo $totalPages > 1 ? '' : 'hide'; ?>">
1407 <?php
1408
1409 $numOfPages = $totalPages;
1410 $renderedEllipses = false;
1411
1412 $currentPage = !empty($options['currentpage'])?$options['currentpage'] : 1;
1413
1414 for($i = 1; $i<=$numOfPages; $i++)
1415 {
1416
1417 //render current page number
1418 if($i == (int)$currentPage) {
1419 //render link
1420 echo wp_kses_post('<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
1421 //reset ellipses
1422 $renderedEllipses = false;
1423 }
1424
1425 //last page number
1426 else if ($i >= $numOfPages ) {
1427 //render link
1428 echo wp_kses_post('...<span class="page-number-mobile" data-page="'.$i.'">'.$i.'</span>');
1429 }
1430 }
1431 ?>
1432
1433 </div>
1434
1435
1436 <div
1437 class="ep-next " <?php echo empty($nextPageToken) ? ' style="display:none" ' : ''; ?>
1438 data-playlistid="<?php echo esc_attr($options['playlistId']) ?>"
1439 data-pagetoken="<?php echo esc_attr($nextPageToken) ?>"
1440 data-pagesize="<?php echo intval($options['pagesize']) ?>"
1441 >
1442 <span><?php _e("Next ", "embedpress"); ?> </span>
1443 </div>
1444 </div>
1445 <?php endif; ?>
1446
1447 <div class="ep-loader-wrap">
1448 <div class="ep-loader"><img alt="loading" src="<?php echo esc_url(EMBEDPRESS_URL_ASSETS. 'images/youtube/spin.gif'); ?>"></div>
1449 </div>
1450
1451 </div>
1452 </div>
1453
1454 <?php
1455 $gallobj->html = ob_get_clean();
1456 else:
1457 $gallobj->html = $this->clean_api_error_html(__("There is nothing on the playlist.", 'embedpress'));
1458 endif;
1459
1460 return $gallobj;
1461 }
1462
1463 public function get_api_key_error_message(){
1464 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>';
1465 }
1466
1467 public function get_id($item){
1468 $vid = isset($item->snippet->resourceId->videoId) ? $item->snippet->resourceId->videoId : null;
1469 $vid = $vid ? $vid : (isset($item->id->videoId) ? $item->id->videoId : null);
1470 $vid = $vid ? $vid : (isset($item->id) ? $item->id : null);
1471 return $vid;
1472 }
1473 public function get_thumbnail_url($item, $quality, $privacyStatus) {
1474 $url = "";
1475 if ($privacyStatus == 'private') {
1476 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/private.png';
1477 } elseif (isset($item->snippet->thumbnails->{$quality}->url)) {
1478 $url = $item->snippet->thumbnails->{$quality}->url;
1479 } elseif (isset($item->snippet->thumbnails->medium->url)) {
1480 $url = $item->snippet->thumbnails->medium->url;
1481 } elseif (isset($item->snippet->thumbnails->default->url)) {
1482 $url = $item->snippet->thumbnails->default->url;
1483 } elseif (isset($item->snippet->thumbnails->high->url)) {
1484 $url = $item->snippet->thumbnails->high->url;
1485 } else {
1486 $url = EMBEDPRESS_URL_ASSETS. 'images/youtube/deleted-video-thumb.png';
1487 }
1488 return $url;
1489 }
1490
1491 public function compare_vid_date($a, $b) {
1492 if ($a->snippet->publishedAt == $b->snippet->publishedAt) {
1493 return 0;
1494 }
1495 return ($a->snippet->publishedAt > $b->snippet->publishedAt) ? -1 : 1;
1496 }
1497
1498 public function clean_api_error($raw_message) {
1499 return htmlspecialchars(strip_tags(preg_replace('@&key=[^& ]+@i', '&key=*******', $raw_message)));
1500 }
1501
1502 public function clean_api_error_html($raw_message) {
1503 $clean_html = '';
1504 if ((defined('REST_REQUEST') && REST_REQUEST) || current_user_can('manage_options')) {
1505 $clean_html = '<div>' . __('EmbedPress: ', 'embedpress') . $this->clean_api_error($raw_message) . '</div>';
1506 }
1507 return $clean_html;
1508 }
1509
1510 /** inline {@inheritdoc} */
1511 public function getFakeResponse() {
1512 preg_match('~v=([a-z0-9_\-]+)~i', (string) $this->url, $matches);
1513
1514 $embedUrl = 'https://www.youtube.com/embed/' . $matches['1'] . '?feature=oembed';
1515
1516 $attr = [];
1517 $attr[] = 'width="{width}"';
1518 $attr[] = 'height="{height}"';
1519 $attr[] = 'src="' . esc_url($embedUrl) . '"';
1520 $attr[] = 'frameborder="0"';
1521 $attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
1522 $attr[] = 'allowfullscreen';
1523
1524 return [
1525 'type' => 'video',
1526 'provider_name' => 'Youtube',
1527 'provider_url' => 'https://www.youtube.com',
1528 'title' => 'Unknown title',
1529 'html' => '<iframe ' . implode(' ', $attr) . '></iframe>',
1530 ];
1531 }
1532
1533 // public $num = 0;
1534
1535
1536
1537 public $x = 0;
1538
1539 public function styles($params, $url){
1540
1541 $uniqid = '.ose-youtube.ose-uid-'.md5($url);
1542
1543 ob_start();
1544 ?>
1545 <style>
1546
1547 <?php
1548 $attributes_data = $params;
1549
1550 $is_pagination = 'flex';
1551
1552 $gap = '30';
1553 $columns = '';
1554
1555 if (isset($attributes_data['ispagination']) && $attributes_data['ispagination']) {
1556 $is_pagination = 'none';
1557 }
1558 if(isset($attributes_data['gapbetweenvideos'])){
1559 $gap = $attributes_data['gapbetweenvideos'];
1560 }
1561 if(isset($attributes_data['columns'])){
1562 $columns = $attributes_data['columns'];
1563 }
1564
1565
1566 if(!empty($columns) && (int) $columns > 0){
1567 $repeatCol = 'repeat(auto-fit, minmax('.esc_html('calc('.(100 / (int) $columns).'% - '.$gap.'px)').', 1fr))';
1568 }
1569 else{
1570 $repeatCol = 'repeat(auto-fit, minmax(calc(250px - '.$gap.'px), 1fr))';
1571 }
1572
1573 ?>
1574 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1575 gap: <?php echo esc_html($gap); ?>px !important;
1576 margin-top: <?php echo esc_html($gap); ?>px !important;
1577 grid-template-columns: <?php echo $repeatCol; ?>;
1578 }
1579 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .ep-youtube__content__pagination {
1580 display: <?php echo esc_html($is_pagination); ?>!important;
1581 }
1582
1583 <?php echo esc_attr($uniqid); ?> .layout-list .ep-youtube__content__block .youtube__content__body .content__wrap{
1584 grid-template-columns: repeat(auto-fit, minmax(calc(100% - 30px), 1fr))!important;
1585 }
1586
1587 @media (max-width: 420px) {
1588 <?php echo esc_attr($uniqid); ?> .ep-youtube__content__block .youtube__content__body .content__wrap:not(.youtube-carousel) {
1589 gap: 30px !important;
1590 }
1591 }
1592
1593 <?php
1594 if($is_pagination){
1595 echo esc_attr($uniqid) ?> {
1596 height: 100%!important;
1597 }
1598 <?php
1599 }
1600 ?>
1601 </style>
1602 <?php
1603 return ob_get_clean();
1604 }
1605 }
1606