@@ -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