Skip to content

Commit a766adb

Browse files
committed
Merge branch 'card-80877' into latest
2 parents 560536f + e30e002 commit a766adb

8 files changed

Lines changed: 318 additions & 59 deletions

File tree

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# EmbedPress
2+
3+
## Project Management (Zoobbe)
4+
- **Account:** akash@wpdeveloper.com (office)
5+
- **Workspace:** Team Startise
6+
- **Board:** Development - EmbedPress (`nN1NU8sH`)
7+
- **CLI:** Use `zb` instead of `zoobbe` — it auto-switches account/workspace based on this directory
8+
- **Workflow:** Backlog → To-Do → On-Going → Need Feedback → Client End Fixed → Tested & Closed → Released
9+
- **Card creation:** Always assign to current user, set priority, add label, include description
10+
- **Labels:** Bug Fix, New Feature, Feature Request, Improvements, Critical, Security Issue, Client Issue, Pro, Free, WordPress ORG

Core/AssetManager.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ class AssetManager
311311
'init-plyr-js' => [
312312
'file' => 'js/initplyr.js',
313313
'deps' => ['jquery', 'embedpress-plyr'],
314-
'contexts' => ['frontend', 'elementor'],
314+
'contexts' => ['editor', 'frontend', 'elementor'],
315315
'type' => 'script',
316316
'footer' => true,
317317
'handle' => 'embedpress-init-plyr',
@@ -721,10 +721,21 @@ private static function should_load_asset($asset)
721721
if (!self::check_asset_condition($asset['condition'])) {
722722
return false;
723723
}
724-
}
725724

726-
// Check provider-specific loading
727-
if (isset($asset['providers']) && !empty($asset['providers'])) {
725+
// When a condition like 'custom_player' already passed, skip the
726+
// provider check — the condition itself proves these scripts are
727+
// needed. Provider detection is fragile (missing URL attrs,
728+
// widget-name typos, etc.) and should not block explicitly-enabled
729+
// features.
730+
if ($asset['condition'] === 'custom_player') {
731+
// Provider check not needed; fall through to context check
732+
} elseif (isset($asset['providers']) && !empty($asset['providers'])) {
733+
if (!self::check_provider_match($asset['providers'])) {
734+
return false;
735+
}
736+
}
737+
} elseif (isset($asset['providers']) && !empty($asset['providers'])) {
738+
// No condition set — still check providers
728739
if (!self::check_provider_match($asset['providers'])) {
729740
return false;
730741
}
@@ -1488,8 +1499,9 @@ private static function detect_types_from_elementor_data($data)
14881499
}
14891500

14901501
// Check if this is an EmbedPress widget
1502+
// Note: widget name is 'embedpres_elementor' (legacy typo without double 's')
14911503
$widget_type = $element['widgetType'] ?? '';
1492-
if ($widget_type && (strpos($widget_type, 'embedpress') !== false || strpos($widget_type, 'Embedpress') !== false)) {
1504+
if ($widget_type && (strpos($widget_type, 'embedpres') !== false || strpos($widget_type, 'Embedpress') !== false)) {
14931505
// Get the embed source
14941506
$settings = $element['settings'] ?? [];
14951507
$source = $settings['embedpress_pro_embeded_source'] ?? '';
@@ -1577,7 +1589,9 @@ private static function detect_type_from_url($url)
15771589

15781590
// YouTube special cases (channel, live, shorts)
15791591
if (strpos($url_lower, 'youtube.com') !== false || strpos($url_lower, 'youtu.be') !== false) {
1580-
if (strpos($url_lower, '/channel/') !== false || strpos($url_lower, '/c/') !== false || strpos($url_lower, '/@') !== false) {
1592+
if (preg_match('#/(channel|c|user)/[\w-]+/live$|/@[\w-]+/live$#i', $url_lower)) {
1593+
$types[] = 'youtube-live';
1594+
} elseif (strpos($url_lower, '/channel/') !== false || strpos($url_lower, '/c/') !== false || strpos($url_lower, '/@') !== false) {
15811595
$types[] = 'youtube-channel';
15821596
} elseif (strpos($url_lower, '/live') !== false) {
15831597
$types[] = 'youtube-live';

EmbedPress/Includes/Classes/Feature_Enhancer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ public function ytValidateUrl($url)
251251
//Check is YouTube live video
252252
public function ytValidateLiveUrl($url)
253253
{
254-
return (bool) (preg_match('/^https?:\/\/(?:www\.)?youtube\.com\/(?:channel\/[\w-]+|@[\w-]+)\/live$/', (string) $url));
254+
return (bool) (preg_match('/^https?:\/\/(?:www\.)?youtube\.com\/(?:channel\/[\w-]+|c\/[\w-]+|user\/[\w-]+|@[\w-]+)\/live$/', (string) $url));
255255
}
256256

257257

EmbedPress/Providers/Youtube.php

Lines changed: 189 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ public function getChannel($url = null) {
9595
if(empty($matches[1])){
9696
preg_match('~\/(@)(\w+)~i', (string) $url, $matches);
9797
if(!empty($matches[1])){
98-
if(!empty($this->get_youtube_handler($this->url))){
99-
if(!empty($this->get_channel_id_by_handler($this->get_youtube_handler($this->url)))){
100-
$channelId = $this->get_channel_id_by_handler($this->get_youtube_handler($this->url));
98+
$handle = $this->get_youtube_handler($this->url);
99+
if(!empty($handle)){
100+
$resolved = $this->get_channel_id_by_handler($handle);
101+
if(!empty($resolved)){
102+
$channelId = $resolved;
101103
}
102104
}
103105
return [
@@ -180,27 +182,58 @@ public function getStaticResponse() {
180182

181183
if (preg_match("/^https?:\/\/(?:www\.)?youtube\.com\/channel\/([\w-]+)\/live$/", $this->url, $matches) || $this->validateTYLiveUrl($this->url)) {
182184

185+
$channelId = '';
186+
183187
if(!empty($matches[1])){
184188
$channelId = $matches[1];
185189
}
186-
187-
if(!empty($this->get_youtube_handler($this->url))){
188-
if(!empty($this->get_channel_id_by_handler($this->get_youtube_handler($this->url)))){
189-
$channelId = $this->get_channel_id_by_handler($this->get_youtube_handler($this->url));
190+
191+
if(empty($channelId)){
192+
$handle = $this->get_youtube_handler($this->url);
193+
if(!empty($handle)){
194+
$resolved = $this->get_channel_id_by_handler($handle);
195+
if(!empty($resolved)){
196+
$channelId = $resolved;
197+
}
190198
}
191199
}
192200

201+
if(empty($channelId)){
202+
return $results;
203+
}
193204

194-
195-
$embedUrl = 'https://www.youtube.com/embed/live_stream?channel='.$channelId.'&feature=oembed';
205+
$api_key = $this->get_api_key();
206+
207+
// When API key is available, check for active live stream
208+
if (!empty($api_key)) {
209+
$live_video_id = $this->get_live_video_id($channelId, $api_key);
210+
211+
if (!empty($live_video_id)) {
212+
// Channel is live - embed the live video directly
213+
$embedUrl = 'https://www.youtube.com/embed/' . $live_video_id . '?feature=oembed';
214+
} else {
215+
// Channel is not live - show the last completed stream or latest video
216+
$last_video_id = $this->get_last_stream_or_video($channelId, $api_key);
217+
if (!empty($last_video_id)) {
218+
$embedUrl = 'https://www.youtube.com/embed/' . $last_video_id . '?feature=oembed';
219+
} else {
220+
// No video found at all
221+
$embedUrl = 'https://www.youtube.com/embed/live_stream?channel=' . $channelId . '&feature=oembed';
222+
}
223+
}
224+
} else {
225+
// No API key - use live_stream endpoint as fallback
226+
$embedUrl = 'https://www.youtube.com/embed/live_stream?channel='.$channelId.'&feature=oembed';
227+
}
196228

197229
$attr = [];
198-
$attr[] = 'width="'.esc_attr($params['maxheight']).'"';
199-
$attr[] = 'height="'.esc_attr($params['maxheight']).'";';
230+
$attr[] = 'width="'.esc_attr($params['maxwidth']).'"';
231+
$attr[] = 'height="'.esc_attr($params['maxheight']).'"';
200232
$attr[] = 'src="' . esc_url($embedUrl) . '"';
201233
$attr[] = 'frameborder="0"';
202234
$attr[] = 'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"';
203235
$attr[] = 'allowfullscreen';
236+
$attr[] = 'referrerpolicy="origin"';
204237

205238
$results['html'] = '<iframe ' . implode(' ', $attr) . '></iframe>';
206239
}
@@ -268,9 +301,7 @@ public function getChannelPlaylist(){
268301
}
269302

270303
public function get_youtube_handler($url){
271-
// preg_match('/^https:\/\/www.youtube.com\/@(.+)\/live$/i', $url, $matches);
272-
preg_match('/^https:\/\/www.youtube.com\/@([^\/?]+)/i', $url, $matches);
273-
304+
preg_match('/^https?:\/\/(?:www\.)?youtube\.com\/@([^\/?]+)/i', $url, $matches);
274305

275306
$handle_name = '';
276307
if(!empty($matches[1])){
@@ -299,35 +330,154 @@ public function get_channel_id_by_handler($handle)
299330
{
300331
$transient_key = 'channel_id_' . md5($handle);
301332
$channel_id = get_transient($transient_key);
302-
303-
if (false === $channel_id) {
304-
$ch = curl_init();
305-
306-
$channel_handle = "https://www.youtube.com/@{$handle}";
307-
308-
curl_setopt($ch, CURLOPT_URL, $channel_handle);
309-
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
310-
311-
$response = curl_exec($ch);
312-
313-
if (curl_errno($ch)) {
314-
return 'cURL error: ' . curl_error($ch);
333+
334+
if (false !== $channel_id && preg_match('/^UC[\w-]+$/', $channel_id)) {
335+
return $channel_id;
336+
}
337+
338+
$channel_handle = "https://www.youtube.com/@{$handle}";
339+
340+
$response = wp_remote_get($channel_handle, [
341+
'timeout' => self::$curltimeout,
342+
'user-agent' => 'Mozilla/5.0 (compatible; WordPress/' . get_bloginfo('version') . ')',
343+
]);
344+
345+
if (is_wp_error($response)) {
346+
return '';
347+
}
348+
349+
$body = wp_remote_retrieve_body($response);
350+
351+
if (empty($body)) {
352+
return '';
353+
}
354+
355+
// Try canonical link first (most reliable)
356+
$pattern = '/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/([^"]{1,50})">/';
357+
if (preg_match($pattern, $body, $matches)) {
358+
$channel_id = $matches[1];
359+
set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
360+
return $channel_id;
361+
}
362+
363+
// Fallback: try externalId from page data
364+
if (preg_match('/"externalId"\s*:\s*"(UC[a-zA-Z0-9_-]+)"/', $body, $matches)) {
365+
$channel_id = $matches[1];
366+
set_transient($transient_key, $channel_id, 7 * DAY_IN_SECONDS);
367+
return $channel_id;
368+
}
369+
370+
return '';
371+
}
372+
373+
/**
374+
* Check if a channel has an active live stream and return the video ID.
375+
*/
376+
public function get_live_video_id($channel_id, $api_key) {
377+
$transient_key = 'ep_yt_live_' . md5($channel_id);
378+
$cached = get_transient($transient_key);
379+
380+
if (false !== $cached) {
381+
return $cached;
382+
}
383+
384+
$api_url = self::$channel_endpoint . 'search?' . http_build_query([
385+
'part' => 'id',
386+
'channelId' => $channel_id,
387+
'eventType' => 'live',
388+
'type' => 'video',
389+
'key' => $api_key,
390+
]);
391+
392+
$response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
393+
394+
if (is_wp_error($response)) {
395+
return '';
396+
}
397+
398+
$data = json_decode(wp_remote_retrieve_body($response));
399+
400+
if (!empty($data->items[0]->id->videoId)) {
401+
$video_id = $data->items[0]->id->videoId;
402+
set_transient($transient_key, $video_id, 2 * MINUTE_IN_SECONDS);
403+
return $video_id;
404+
}
405+
406+
// Cache empty result briefly to avoid repeated API calls
407+
set_transient($transient_key, '', MINUTE_IN_SECONDS);
408+
return '';
409+
}
410+
411+
/**
412+
* Get the last completed live stream or latest video from a channel.
413+
* Tries completed streams first, falls back to latest upload.
414+
*/
415+
public function get_last_stream_or_video($channel_id, $api_key) {
416+
$transient_key = 'ep_yt_last_stream_' . md5($channel_id);
417+
$cached = get_transient($transient_key);
418+
419+
if (false !== $cached) {
420+
return $cached;
421+
}
422+
423+
// First try: get the last completed live stream
424+
$api_url = self::$channel_endpoint . 'search?' . http_build_query([
425+
'part' => 'id',
426+
'channelId' => $channel_id,
427+
'eventType' => 'completed',
428+
'type' => 'video',
429+
'order' => 'date',
430+
'maxResults' => 1,
431+
'key' => $api_key,
432+
]);
433+
434+
$response = wp_remote_get($api_url, ['timeout' => self::$curltimeout]);
435+
436+
if (!is_wp_error($response)) {
437+
$data = json_decode(wp_remote_retrieve_body($response));
438+
if (!empty($data->items[0]->id->videoId)) {
439+
$video_id = $data->items[0]->id->videoId;
440+
set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
441+
return $video_id;
315442
}
316-
317-
curl_close($ch);
318-
319-
$pattern = '/(<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/)(.{1,50})(">)/';
320-
if (preg_match($pattern, $response, $matches)) {
321-
$channel_id = $matches[2];
322-
set_transient($transient_key, $channel_id, 30 * DAY_IN_SECONDS);
443+
}
323444

324-
return $channel_id;
325-
} else {
326-
return "Not a channel URL";
445+
// Fallback: get the latest video from the channel's uploads playlist
446+
$channel_url = self::$channel_endpoint . 'channels?' . http_build_query([
447+
'part' => 'contentDetails',
448+
'id' => $channel_id,
449+
'key' => $api_key,
450+
]);
451+
452+
$ch_response = wp_remote_get($channel_url, ['timeout' => self::$curltimeout]);
453+
454+
if (!is_wp_error($ch_response)) {
455+
$ch_data = json_decode(wp_remote_retrieve_body($ch_response));
456+
$uploads_playlist = $ch_data->items[0]->contentDetails->relatedPlaylists->uploads ?? '';
457+
458+
if (!empty($uploads_playlist)) {
459+
$playlist_url = self::$channel_endpoint . 'playlistItems?' . http_build_query([
460+
'part' => 'snippet',
461+
'playlistId' => $uploads_playlist,
462+
'maxResults' => 1,
463+
'key' => $api_key,
464+
]);
465+
466+
$pl_response = wp_remote_get($playlist_url, ['timeout' => self::$curltimeout]);
467+
468+
if (!is_wp_error($pl_response)) {
469+
$pl_data = json_decode(wp_remote_retrieve_body($pl_response));
470+
if (!empty($pl_data->items[0]->snippet->resourceId->videoId)) {
471+
$video_id = $pl_data->items[0]->snippet->resourceId->videoId;
472+
set_transient($transient_key, $video_id, 5 * MINUTE_IN_SECONDS);
473+
return $video_id;
474+
}
475+
}
327476
}
328-
} else {
329-
return $channel_id;
330477
}
478+
479+
set_transient($transient_key, '', 2 * MINUTE_IN_SECONDS);
480+
return '';
331481
}
332482

333483
public function layout_data(){
@@ -411,13 +561,6 @@ public function getChannelGallery() {
411561
$styles = $this->styles($params, $this->getUrl());
412562
$html_content = $main_iframe . $gallery->html . ' ' . $styles;
413563

414-
if ($this->validateTYLiveUrl($this->getUrl())) {
415-
return [
416-
"title" => $title,
417-
"html" => "<div class='ep-player-wrap'>$main_iframe $styles</div>",
418-
];
419-
}
420-
421564
return [
422565
"title" => $title,
423566
"html" => "<div class='ep-player-wrap $channel_layout'>$html_content</div>",

0 commit comments

Comments
 (0)