PluginProbe
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels / 2.3.8
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels v2.3.8
2.9.1 2.9.0 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 2.0.0 2.1.0 2.2.0 2.3.2 2.3.3 2.3.5 2.3.6 2.3.8 2.3.9 2.4.3 All 37 releases
← All changes | includes/youtube-api.php +106 -442 2.9.12.3.8 View file →
@@ -30,30 +30,22 @@
30 30 */
31 31 protected $api_key;
32 32
33 33 /**
34 - * Array of query params.
34 + * The YouTube API URLs.
35 35 *
36 - * @since 2.5.7
36 + * @since 1.0.0
37 37 * @access protected
38 38 * @var array
39 39 */
40 - protected $params = array();
40 + public $api_urls = array(
41 + 'playlistItems.list' => 'https://www.googleapis.com/youtube/v3/playlistItems',
42 + 'channels.list' => 'https://www.googleapis.com/youtube/v3/channels',
43 + 'search.list' => 'https://www.googleapis.com/youtube/v3/search',
44 + 'videos.list' => 'https://www.googleapis.com/youtube/v3/videos'
45 + );
41 46
42 47 /**
43 - * May this query write gallery membership rows?
44 - *
45 - * Opt-in, set from the "store" query param. Only trusted server side callers set it — the
46 - * page renderer (ayg_build_gallery), the Gallery Builder importer, and the public AJAX
47 - * endpoint once it has resolved the gallery itself. See request_api().
48 - *
49 - * @since 2.9.0
50 - * @access protected
51 - * @var bool
52 - */
53 - protected $can_store = false;
54 -
55 - /**
56 48 * Is development mode enabled?
57 49 *
58 50 * @since 2.3.0
59 51 * @access protected
@@ -61,54 +53,23 @@
61 53 */
62 54 protected $is_development_mode = false;
63 55
64 56 /**
65 - * The YouTube API URLs.
57 + * Get videos.
66 58 *
67 59 * @since 1.0.0
68 - * @access protected
69 - * @var array
70 - */
71 - public $api_urls = array(
72 - 'playlistItems.list' => 'https://www.googleapis.com/youtube/v3/playlistItems',
73 - 'channels.list' => 'https://www.googleapis.com/youtube/v3/channels',
74 - 'search.list' => 'https://www.googleapis.com/youtube/v3/search',
75 - 'videos.list' => 'https://www.googleapis.com/youtube/v3/videos'
76 - );
77 -
78 - /**
79 - * Get videos.
80 - *
81 - * Side effect: successful responses are persisted to the custom tables via
82 - * ayg_db_store_videos() (called inside request_api()) — video rows always, and
83 - * gallery relationships only when the caller passes both a "uid" and "store" => true.
84 - * This is how both legacy galleries and the Gallery Builder importer store their videos.
85 - *
86 - * "store" must never be set from user input: it decides which gallery the fetched videos
87 - * are shown in. Callers reachable by unauthenticated visitors have to resolve the gallery
88 - * server side first — see AYG_Public::ajax_callback_load_videos().
89 - *
90 - * @since 1.0.0
91 - * @param array $params Array of query params.
60 + * @param array $params Array of query params.
92 61 * @return mixed
93 62 */
94 63 public function query( $params = array() ) {
95 64 // Get YouTube API Key
96 - $general_settings = ayg_get_option( 'ayg_general_settings' );
65 + $general_settings = get_option( 'ayg_general_settings' );
97 66
98 - // DB-served responses (gallery search + the internal "db" source type) read from the
99 - // custom tables, so they're handled before the API-key guard — no key required.
100 - if ( ! empty( $params['searchTerm'] ) || ( isset( $params['type'] ) && 'db' === $params['type'] ) ) {
101 - return $this->get_videos_from_db( $params );
102 - }
103 -
104 67 if ( empty( $general_settings['api_key'] ) ) {
105 - return $this->get_error( __( 'YouTube API key not found.', 'automatic-youtube-gallery' ) . ' ' . sprintf( __( 'Kindly follow this URL <a href="%s" target="_blank" rel="noopener noreferrer">this guide</a> to get your own API key.', 'automatic-youtube-gallery' ), 'https://plugins360.com/automatic-youtube-gallery/how-to-get-youtube-api-key/' ) );
68 + return $this->get_error( __( 'YouTube API key not found.', 'automatic-youtube-gallery' ) . ' ' . sprintf( __( 'Kindly follow this URL <a href="%s" target="_blank">this guide</a> to get your own API key.', 'automatic-youtube-gallery' ), 'https://plugins360.com/automatic-youtube-gallery/how-to-get-youtube-api-key/' ) );
106 69 }
107 70
108 - $this->api_key = $general_settings['api_key'];
109 - $this->params = $params;
110 - $this->can_store = ! empty( $params['store'] );
71 + $this->api_key = $general_settings['api_key'];
111 72
112 73 // Is development mode enabled?
113 74 if ( isset( $general_settings['development_mode'] ) && ! empty( $general_settings['development_mode'] ) ) {
114 75 $this->is_development_mode = true;
@@ -113,13 +74,8 @@
113 74 if ( isset( $general_settings['development_mode'] ) && ! empty( $general_settings['development_mode'] ) ) {
114 75 $this->is_development_mode = true;
115 76 }
116 77
117 - // Advanced mode fetches duration + live broadcast details via a supplementary
118 - // videos.list call (used by the Gallery Builder importer). Defaults to off so
119 - // legacy galleries make no extra API request.
120 - $mode = isset( $params['mode'] ) ? $params['mode'] : 'basic';
121 -
122 78 // Process output
123 79 $response = array();
124 80
125 81 switch ( $params['type'] ) {
@@ -124,9 +80,9 @@
124 80
125 81 switch ( $params['type'] ) {
126 82 case 'playlist':
127 83 if ( empty( $params['src'] ) ) {
128 - return $this->get_error( __( 'A YouTube playlist ID (or) URL is required.', 'automatic-youtube-gallery' ) );
84 + return $this->get_error( __( 'YouTube Playlist ID (or) URL is required.', 'automatic-youtube-gallery' ) );
129 85 }
130 86
131 87 $response = $this->request_api_playlist_items( $params );
132 88 break;
@@ -132,21 +88,15 @@
132 88 break;
133 89
134 90 case 'channel':
135 91 if ( empty( $params['src'] ) ) {
136 - return $this->get_error( __( 'A YouTube channel ID (or) a video URL from the channel is required.', 'automatic-youtube-gallery' ) );
92 + return $this->get_error( __( 'YouTube Channel ID (or) or a YouTube Video URL from the Channel is required.', 'automatic-youtube-gallery' ) );
137 93 }
138 94
139 - // @handle URLs can't be resolved to a channel ID here (mirrors the client-side
140 - // check in admin.js / gallery-form.php).
141 - if ( false !== strpos( $params['src'], '@' ) ) {
142 - return $this->get_error( __( 'YouTube @handle URLs aren’t supported here. Please enter a channel ID, a /channel/ URL, or a video URL from the channel.', 'automatic-youtube-gallery' ) );
143 - }
144 -
145 95 $params['id'] = $this->get_channel_id( $params );
146 96
147 97 if ( empty( $params['id'] ) ) {
148 - return $this->get_error( __( 'Invalid YouTube channel ID.', 'automatic-youtube-gallery' ) );
98 + return $this->get_error( __( 'Invalid YouTube Channel ID.', 'automatic-youtube-gallery' ) );
149 99 }
150 100
151 101 // Get playlist id from the channel
152 102 $playlist_id = $this->get_playlist_id( $params );
@@ -165,9 +115,9 @@
165 115 break;
166 116
167 117 case 'username':
168 118 if ( empty( $params['src'] ) ) {
169 - return $this->get_error( __( 'A YouTube account username is required.', 'automatic-youtube-gallery' ) );
119 + return $this->get_error( __( 'YouTube Account Username is required.', 'automatic-youtube-gallery' ) );
170 120 }
171 121
172 122 // Get playlist id from the channel
173 123 $params['forUsername'] = $this->parse_youtube_id_from_url( $params['src'], 'username' );
@@ -187,9 +137,9 @@
187 137 break;
188 138
189 139 case 'search':
190 140 if ( empty( $params['src'] ) ) {
191 - return $this->get_error( __( 'A search keyword is required.', 'automatic-youtube-gallery' ) );
141 + return $this->get_error( __( 'Cannot search an empty string. A search keyword is required.', 'automatic-youtube-gallery' ) );
192 142 }
193 143
194 144 $response = $this->request_api_search( $params );
195 145 break;
@@ -195,9 +145,9 @@
195 145 break;
196 146
197 147 case 'videos':
198 148 if ( empty( $params['src'] ) ) {
199 - return $this->get_error( __( 'At least one YouTube video ID (or) URL is required.', 'automatic-youtube-gallery' ) );
149 + return $this->get_error( __( 'Atleast one YouTube Video ID (or) URL is required.', 'automatic-youtube-gallery' ) );
200 150 }
201 151
202 152 $response = $this->request_api_videos( $params );
203 153 break;
@@ -203,21 +153,15 @@
203 153 break;
204 154
205 155 case 'livestream':
206 156 if ( empty( $params['src'] ) ) {
207 - return $this->get_error( __( 'A YouTube channel ID (or) a video URL from the channel is required.', 'automatic-youtube-gallery' ) );
157 + return $this->get_error( __( 'YouTube Channel ID (or) or a YouTube Video URL from the Channel is required.', 'automatic-youtube-gallery' ) );
208 158 }
209 159
210 - // @handle URLs can't be resolved to a channel ID here (mirrors the client-side
211 - // check in admin.js / gallery-form.php).
212 - if ( false !== strpos( $params['src'], '@' ) ) {
213 - return $this->get_error( __( 'YouTube @handle URLs aren’t supported here. Please enter a channel ID, a /channel/ URL, or a video URL from the channel.', 'automatic-youtube-gallery' ) );
214 - }
215 -
216 160 $params['channelId'] = $this->get_channel_id( $params );
217 161
218 162 if ( empty( $params['channelId'] ) ) {
219 - return $this->get_error( __( 'Invalid YouTube channel ID.', 'automatic-youtube-gallery' ) );
163 + return $this->get_error( __( 'Invalid YouTube Channel ID.', 'automatic-youtube-gallery' ) );
220 164 }
221 165
222 166 // Get live video using the channel id
223 167 $response = $this->request_api_live_video( $params );
@@ -224,9 +168,9 @@
224 168 break;
225 169
226 170 default: // video
227 171 if ( empty( $params['src'] ) ) {
228 - return $this->get_error( __( 'A YouTube video ID (or) URL is required.', 'automatic-youtube-gallery' ) );
172 + return $this->get_error( __( 'YouTube Video ID (or) URL is required.', 'automatic-youtube-gallery' ) );
229 173 }
230 174
231 175 $response = $this->request_api_video( $params );
232 176 break;
@@ -231,14 +175,8 @@
231 175 $response = $this->request_api_video( $params );
232 176 break;
233 177 }
234 178
235 - // Advanced mode: enrich channel/playlist/username/search videos (which come from
236 - // playlistItems.list / search.list and lack duration + live broadcast details).
237 - if ( 'advanced' === $mode && ! isset( $response->error ) && ! empty( $response->videos ) && in_array( $params['type'], array( 'playlist', 'channel', 'username', 'search' ), true ) ) {
238 - $response->videos = $this->enrich_video_details( $response->videos );
239 - }
240 -
241 179 return $response;
242 180 }
243 181
244 182 /**
@@ -255,9 +193,9 @@
255 193 $id = $url;
256 194
257 195 switch ( $type ) {
258 196 case 'playlist':
259 - if ( preg_match( '/[?&]list=([^&]+)/', $url, $matches ) ) {
197 + if ( preg_match( '/list=(.*)&?\/?/', $url, $matches ) ) {
260 198 $id = $matches[1];
261 199 }
262 200 break;
263 201
@@ -281,18 +219,14 @@
281 219 }
282 220 break;
283 221
284 222 default: // video
285 - if ( wp_http_validate_url( $id ) ) {
286 - $id = '';
287 - }
288 -
289 223 $url = parse_url( $url );
290 224
291 225 if ( array_key_exists( 'host', $url ) ) {
292 226 if ( 0 === strcasecmp( $url['host'], 'youtu.be' ) ) {
293 227 $id = substr( $url['path'], 1 );
294 - } elseif ( 0 === strcasecmp( $url['host'], 'www.youtube.com' ) || 0 === strcasecmp( $url['host'], 'youtube.com' ) ) {
228 + } elseif ( 0 === strcasecmp( $url['host'], 'www.youtube.com' ) ) {
295 229 if ( isset( $url['query'] ) ) {
296 230 parse_str( $url['query'], $url['query'] );
297 231
298 232 if ( isset( $url['query']['v'] ) ) {
@@ -301,9 +235,10 @@
301 235 }
302 236
303 237 if ( empty( $id ) ) {
304 238 $url['path'] = explode( '/', substr( $url['path'], 1 ) );
305 - if ( in_array( $url['path'][0], array( 'e', 'embed', 'v', 'shorts', 'live' ) ) ) {
239 +
240 + if ( in_array( $url['path'][0], array( 'e', 'embed', 'v' ) ) ) {
306 241 $id = $url['path'][1];
307 242 }
308 243 }
309 244 }
@@ -329,9 +264,12 @@
329 264 // Get channel ID from a Video URL: https://www.youtube.com/watch?v=XXXXXXXXXX
330 265 $video_id = $this->parse_youtube_id_from_url( $params['src'], 'video' );
331 266
332 267 // Request from cache
333 - $channel_ids = ayg_get_option( 'ayg_channel_ids' );
268 + $channel_ids = get_option( 'ayg_channel_ids', array() );
269 + if ( ! is_array( $channel_ids ) ) {
270 + $channel_ids = (array) $channel_ids;
271 + }
334 272
335 273 if ( isset( $channel_ids[ $video_id ] ) && ! empty( $channel_ids[ $video_id ] ) ) {
336 274 return $channel_ids[ $video_id ];
337 275 }
@@ -363,9 +301,9 @@
363 301 // Process output
364 302 if ( $id = $videos[0]->channel_id ) {
365 303 // Store in cache
366 304 $channel_ids[ $video_id ] = $id;
367 - update_option( 'ayg_channel_ids', $channel_ids, false );
305 + update_option( 'ayg_channel_ids', $channel_ids );
368 306 }
369 307 }
370 308
371 309 return $id;
@@ -380,9 +318,12 @@
380 318 * @return mixed
381 319 */
382 320 private function get_playlist_id( $params = array() ) {
383 321 // Request from cache
384 - $playlist_ids = ayg_get_option( 'ayg_playlist_ids' );
322 + $playlist_ids = get_option( 'ayg_playlist_ids', array() );
323 + if ( ! is_array( $playlist_ids ) ) {
324 + $playlist_ids = (array) $playlist_ids;
325 + }
385 326
386 327 $key = '';
387 328
388 329 if ( isset( $params['forUsername'] ) && ! empty( $params['forUsername'] ) ) {
@@ -428,9 +369,9 @@
428 369 // Process output
429 370 if ( $id = $items[0]->contentDetails->relatedPlaylists->uploads ) {
430 371 // Store in cache
431 372 $playlist_ids[ $key ] = $id;
432 - update_option( 'ayg_playlist_ids', $playlist_ids, false );
373 + update_option( 'ayg_playlist_ids', $playlist_ids );
433 374
434 375 // Return
435 376 return $id;
436 377 }
@@ -490,13 +431,14 @@
490 431 */
491 432 private function request_api_search( $params = array() ) {
492 433 $api_url = $this->get_api_url( 'search.list' );
493 434
494 - // Passed through unmodified: request_api() now runs every parameter through
495 - // http_build_query(), which encodes the OR operator "|" to %7C on its own. Pre-encoding it
496 - // here would be double encoded into %257C and break the search.
497 - $params['q'] = $params['src'];
435 + $params['q'] = $params['src'];
498 436
437 + if ( ! empty( $params['q'] ) ) {
438 + $params['q'] = str_replace( '|', '%7C', $params['q'] );
439 + }
440 +
499 441 $params['type'] = 'video'; // Overrides user defined type value 'search'
500 442
501 443 $api_params = $this->safe_merge_params(
502 444 array(
@@ -505,15 +447,14 @@
505 447 'type' => 'video',
506 448 'videoEmbeddable' => true,
507 449 'part' => 'id,snippet',
508 450 'order' => 'date',
509 - 'publishedAfter' => '', // Set by incremental sync to fetch only newly published videos
510 451 'maxResults' => 50,
511 452 'pageToken' => '',
512 453 'cache' => 0
513 454 ),
514 455 $params
515 - );
456 + );
516 457
517 458 $api_response = $this->request_api( $api_url, $api_params );
518 459 if ( isset( $api_response->error ) ) {
519 460 return $api_response;
@@ -555,9 +496,9 @@
555 496 ),
556 497 $params
557 498 );
558 499
559 - $api_response = $this->request_api( $api_url, $api_params, 'live' );
500 + $api_response = $this->request_api( $api_url, $api_params );
560 501 if ( isset( $api_response->error ) ) {
561 502 return $api_response;
562 503 }
563 504
@@ -562,9 +503,9 @@
562 503 }
563 504
564 505 $videos = $this->parse_videos( $api_response );
565 506 if ( isset( $videos->error ) ) {
566 - $livestream_settings = ayg_get_option( 'ayg_livestream_settings' );
507 + $livestream_settings = get_option( 'ayg_livestream_settings' );
567 508 return $this->get_error( '<div class="ayg-livestream-fallback-message">' . $livestream_settings['fallback_message'] . '</div>' );
568 509 }
569 510
570 511 // Process output
@@ -623,14 +564,12 @@
623 564 */
624 565 private function request_api_videos( $params = array() ) {
625 566 $api_url = $this->get_api_url( 'videos.list' );
626 567
627 - // Accept the video list separated by commas, spaces, or newlines (one per line) in any
628 - // combination and line-ending style. The old "\n\r" replace looked for LF+CR (reversed),
629 - // so a one-per-line list never split — it collapsed into a single invalid ID and returned
630 - // "No videos found". Split on any run of whitespace or commas and drop empties instead.
631 - $urls = preg_split( '/[\s,]+/', trim( (string) $params['src'] ), -1, PREG_SPLIT_NO_EMPTY );
632 - $urls = is_array( $urls ) ? $urls : array();
568 + $urls = str_replace( "\n\r", ',', $params['src'] );
569 + $urls = str_replace( ' ', ',', $urls );
570 + $urls = explode( ',', $urls );
571 + $urls = array_filter( $urls );
633 572
634 573 $all_ids = array();
635 574 foreach ( $urls as $url ) {
636 575 $all_ids[] = $this->parse_youtube_id_from_url( $url, 'video' );
@@ -641,9 +580,12 @@
641 580 $current_page = isset( $params['pageToken'] ) ? (int) $params['pageToken'] : 1;
642 581 $current_page = max( $current_page, 1 );
643 582 $current_page = min( $current_page, $total_pages );
644 583
645 - $offset = max( 0, ( $current_page - 1 ) * $params['maxResults'] );
584 + $offset = ( $current_page - 1 ) * $params['maxResults'];
585 + if ( $offset < 0 ) {
586 + $offset = 0;
587 + }
646 588
647 589 $current_ids = array_slice( $all_ids, $offset, $params['maxResults'] );
648 590 $params['id'] = implode( ',', $current_ids );
649 591
@@ -670,11 +612,9 @@
670 612 $response = new stdClass();
671 613 $response->videos = $videos;
672 614
673 615 $response->page_info = array(
674 - 'videos_found' => $total_videos,
675 - 'total_pages' => $total_pages,
676 - 'paged' => $current_page
616 + 'videos_found' => $total_videos
677 617 );
678 618
679 619 if ( $current_page > 1 ) {
680 620 $response->page_info['prev_page_token'] = $current_page - 1;
@@ -685,174 +625,8 @@
685 625 }
686 626
687 627 return $response;
688 628 }
689 -
690 - /**
691 - * Get videos from our custom database table "{$wpdb->prefix}ayg_videos".
692 - *
693 - * @since 2.5.7
694 - * @access private
695 - * @param array $params Array of query params.
696 - * @return stdClass
697 - */
698 - private function get_videos_from_db( $params = array() ) {
699 - global $wpdb;
700 -
701 - $videos_table = $wpdb->prefix . 'ayg_videos';
702 - $rel_table = $wpdb->prefix . 'ayg_gallery_relationships';
703 -
704 - $gallery_id = $params['uid'];
705 -
706 - // Base query: every video linked to this gallery. An optional search term (the search
707 - // form) narrows it by title/description; the "db" source type passes none and gets all.
708 - $where = 'r.gallery_id = %s';
709 - $values = array( $gallery_id );
710 -
711 - if ( ! empty( $params['searchTerm'] ) ) {
712 - $search_term = '%' . $wpdb->esc_like( $params['searchTerm'] ) . '%';
713 -
714 - $where .= ' AND (v.title LIKE %s OR v.description LIKE %s)';
715 - $values[] = $search_term;
716 - $values[] = $search_term;
717 - }
718 -
719 - // Optional duration filter. Whitelisted operator; value parameterized. Affects count + select.
720 - $duration_filter = isset( $params['duration_filter'] ) ? $params['duration_filter'] : '';
721 - $duration = isset( $params['duration'] ) ? (int) $params['duration'] : 0;
722 -
723 - if ( $duration > 0 && in_array( $duration_filter, array( 'long', 'short' ), true ) ) {
724 - $where .= ( 'long' === $duration_filter ) ? ' AND v.duration_seconds > %d' : ' AND v.duration_seconds < %d';
725 - $values[] = $duration;
726 - }
727 -
728 - // Get Total Videos Count
729 - $total_videos = $wpdb->get_var(
730 - $wpdb->prepare(
731 - "SELECT COUNT(*)
732 - FROM $videos_table AS v
733 - INNER JOIN $rel_table AS r ON v.video_id = r.video_id
734 - WHERE $where",
735 - $values
736 - )
737 - );
738 -
739 - if ( empty( $total_videos ) ) {
740 - return $this->get_error( __( 'No videos found matching your query.', 'automatic-youtube-gallery' ) );
741 - }
742 -
743 - // Fetch Paginated Videos
744 - $limit = (int) $params['maxResults'];
745 -
746 - if ( $limit <= 0 ) {
747 - // 0 = show all videos on a single page (Gallery Builder "unlimited" per page).
748 - // A LIMIT of the total count avoids the empty result set that LIMIT 0 would return.
749 - $limit = (int) $total_videos;
750 - $total_pages = 1;
751 - $current_page = 1;
752 - $offset = 0;
753 - } else {
754 - $total_pages = ceil( $total_videos / $limit );
755 -
756 - $current_page = isset( $params['pageToken'] ) ? (int) $params['pageToken'] : 1;
757 - $current_page = max( $current_page, 1 );
758 - $current_page = min( $current_page, $total_pages );
759 -
760 - $offset = max( 0, ( $current_page - 1 ) * $limit );
761 - }
762 -
763 - // Display-time ordering. Whitelisted (ORDER BY can't be parameterized); v.id breaks ties.
764 - $orderby_map = array(
765 - 'date' => 'v.published_at_datetime',
766 - 'title' => 'v.title',
767 - 'duration' => 'v.duration_seconds'
768 - );
769 -
770 - $sort_by_raw = isset( $params['sort_by'] ) ? $params['sort_by'] : 'date';
771 - $sort_order = ( isset( $params['sort_order'] ) && 'asc' === strtolower( $params['sort_order'] ) ) ? 'ASC' : 'DESC';
772 -
773 - // Random: a seed (set per render in ayg_build_gallery) makes RAND(seed) stable across this
774 - // gallery's own pagination/search, so pages don't repeat or skip videos.
775 - $random_seed = 0;
776 -
777 - if ( 'random' === $sort_by_raw ) {
778 - $random_seed = isset( $params['sort_seed'] ) ? (int) $params['sort_seed'] : 0;
779 - $order_by = ( $random_seed > 0 ) ? 'RAND(%d)' : 'RAND()';
780 - } else {
781 - $sort_by = isset( $orderby_map[ $sort_by_raw ] ) ? $orderby_map[ $sort_by_raw ] : 'v.published_at_datetime';
782 - $order_by = "$sort_by $sort_order, v.id $sort_order";
783 - }
784 -
785 - // Deeplinked video: pin it to the top of the list so page 1 starts with the shared video
786 - // while the per-page count stays exact. The pin reorders the whole list (not just page 1),
787 - // so paginated AJAX requests passing the same id never repeat or skip videos. Ignored
788 - // while searching — search results are a fresh listing of their own.
789 - $featured_video_id = '';
790 -
791 - if ( empty( $params['searchTerm'] ) && ! empty( $params['featured_video_id'] ) ) {
792 - $featured_video_id = (string) $params['featured_video_id'];
793 - $order_by = '(v.video_id = %s) DESC, ' . $order_by;
794 - }
795 -
796 - // Assemble placeholder values in SQL order: WHERE ..., [featured video], [seed], LIMIT, OFFSET.
797 - $query_values = $values;
798 -
799 - if ( '' !== $featured_video_id ) {
800 - $query_values[] = $featured_video_id;
801 - }
802 -
803 - if ( $random_seed > 0 ) {
804 - $query_values[] = $random_seed;
805 - }
806 -
807 - $query_values[] = $limit;
808 - $query_values[] = $offset;
809 -
810 - $query = $wpdb->prepare(
811 - "SELECT v.*
812 - FROM $videos_table AS v
813 - INNER JOIN $rel_table AS r ON v.video_id = r.video_id
814 - WHERE $where
815 - ORDER BY $order_by
816 - LIMIT %d OFFSET %d",
817 - $query_values
818 - );
819 -
820 - $videos = $wpdb->get_results( $query );
821 -
822 - if ( empty( $videos ) ) {
823 - return $this->get_error( __( 'No videos found matching your query.', 'automatic-youtube-gallery' ) );
824 - }
825 -
826 - foreach ( $videos as $index => $video ) {
827 - if ( ! empty( $video->thumbnails ) ) {
828 - $videos[ $index ]->thumbnails = ayg_maybe_unserialize( $video->thumbnails );
829 - }
830 -
831 - // Backward compat: templates reference $video->id as the YouTube video ID.
832 - $videos[ $index ]->id = $video->video_id;
833 - }
834 -
835 - // Process output
836 - $response = new stdClass();
837 - $response->videos = $videos;
838 -
839 - $response->page_info = array(
840 - 'videos_found' => $total_videos,
841 - 'total_pages' => $total_pages,
842 - 'paged' => $current_page
843 - );
844 -
845 - if ( $current_page > 1 ) {
846 - $response->page_info['prev_page_token'] = $current_page - 1;
847 - }
848 -
849 - if ( $current_page < $total_pages ) {
850 - $response->page_info['next_page_token'] = $current_page + 1;
851 - }
852 -
853 - return $response;
854 - }
855 629
856 630 /**
857 631 * Get API URL by request.
858 632 *
@@ -871,34 +645,18 @@
871 645 * @since 1.0.0
872 646 * @access private
873 647 * @param string $url YouTube API URL.
874 648 * @param array $params Array of query params.
875 - * @param string $context "channel_id", "playlist_id", "videos", or "live"
649 + * @param string $context "channel_id", "playlist_id", or "videos"
876 650 * @return mixed
877 651 */
878 652 private function request_api( $url, $params, $context = 'videos' ) {
879 653 $params['key'] = $this->api_key;
880 -
881 - // Build API URL
882 - $cache_duration = 0;
883 - if ( isset( $params['cache'] ) ) {
884 - $cache_duration = (int) $params['cache'];
885 - unset( $params['cache'] );
886 - }
887 - $cache_duration = min( $cache_duration, 2419200 ); // Max cache duration: 1 Month
888 654
889 - // Every parameter — the search term "q" included — goes through http_build_query() so it is
890 - // URL encoded. "q" used to be appended to the URL raw, which meant a caller could smuggle
891 - // extra parameters into the outbound request by putting "&" in the search keywords.
892 - $api_url = $url . ( strpos( $url, '?' ) === false ? '?' : '&' ) . http_build_query( $params );
893 -
894 - // Prefix the cache key with the gallery uid so a single gallery's transients can be cleared
895 - // on demand (e.g. saving a live search/livestream gallery) via ayg_delete_cache( $uid ).
896 - $cache_uid = isset( $this->params['uid'] ) ? (string) $this->params['uid'] : '';
897 - $cache_key = 'ayg_' . ( '' !== $cache_uid ? $cache_uid . '_' : '' ) . md5( $api_url );
898 -
899 655 // Request from cache
900 - if ( ! $this->is_development_mode && $cache_duration > 0 ) {
656 + if ( ! $this->is_development_mode ) {
657 + $cache_url = $url . ( strpos( $url, '?' ) === false ? '?' : '' ) . http_build_query( $params );
658 + $cache_key = 'ayg_' . md5( $cache_url );
901 659 $cache_data = get_transient( $cache_key );
902 660
903 661 if ( ! empty( $cache_data ) ) {
904 662 return $cache_data;
@@ -905,13 +663,27 @@
905 663 }
906 664 }
907 665
908 666 // Request from API
909 - $timeout = apply_filters( 'ayg_api_request_timeout', 15 );
910 -
911 - $request = wp_remote_get( $api_url, array(
667 + $cache_duration = 0;
668 + if ( isset( $params['cache'] ) ) {
669 + $cache_duration = (int) $params['cache'];
670 + unset( $params['cache'] );
671 + }
672 +
673 + $q = '';
674 + if ( isset( $params['q'] ) ) {
675 + $q = $params['q'];
676 + unset( $params['q'] );
677 + }
678 +
679 + $url = $url . ( strpos( $url, '?' ) === false ? '?' : '' ) . http_build_query( $params );
680 + if ( ! empty( $q ) ) {
681 + $url .= '&q=' . $q;
682 + }
683 +
684 + $request = wp_remote_get( $url, array(
912 685 'headers' => [ 'referer' => home_url() ],
913 - 'timeout' => $timeout, // Increase timeout if needed
914 686 ) );
915 687
916 688 if ( is_wp_error( $request ) ) {
917 689 return $this->get_error( $request->get_error_message() );
@@ -917,14 +689,11 @@
917 689 return $this->get_error( $request->get_error_message() );
918 690 }
919 691
920 692 $body = wp_remote_retrieve_body( $request );
693 +
921 694 $data = json_decode( $body );
922 695
923 - if ( empty( $data ) ) {
924 - return $this->get_error( __( 'Empty or invalid API response', 'automatic-youtube-gallery' ) );
925 - }
926 -
927 696 if ( isset( $data->error ) ) {
928 697 $message = "Error " . $data->error->code . " " . $data->error->message;
929 698
930 699 if ( isset( $data->error->errors[0] ) ) {
@@ -934,55 +703,31 @@
934 703 return $this->get_error( $message );
935 704 }
936 705
937 706 // Store in cache (transients)
938 - $cache_enabled = false;
707 + if ( ! $this->is_development_mode && 'videos' == $context && $cache_duration > 0 ) {
708 + if ( isset( $data->items ) && is_array( $data->items ) && count( $data->items ) > 0 ) {
709 + set_transient( $cache_key, $data, $cache_duration );
939 710
940 - if ( ! $this->is_development_mode && $cache_duration > 0 ) {
941 - if ( 'videos' === $context ) {
942 - if ( ! empty( $data->items ) && is_array( $data->items ) ) {
943 - $cache_enabled = true;
711 + // Get the current list of transients
712 + $cache_keys = get_option( 'ayg_transient_keys', array() );
713 + if ( ! is_array( $cache_keys ) ) {
714 + $cache_keys = (array) $cache_keys;
944 715 }
945 - }
946 716
947 - if ( 'live' === $context ) {
948 - $cache_enabled = true;
949 - }
950 - }
717 + // Append our new one
718 + if ( ! in_array( $cache_key, $cache_keys ) ) {
719 + $cache_keys[] = $cache_key;
720 + }
951 721
952 - if ( $cache_enabled ) {
953 - set_transient( $cache_key, $data, $cache_duration );
954 -
955 - // Get the current list of transients
956 - $cache_keys = ayg_get_option( 'ayg_transient_keys' );
957 -
958 - // Append our new one
959 - if ( ! in_array( $cache_key, $cache_keys ) ) {
960 - $cache_keys[] = $cache_key;
722 + // Save it to the DB
723 + update_option( 'ayg_transient_keys', $cache_keys );
961 724 }
962 -
963 - // Save it to the DB (autoload=no: this list can grow large and is not needed on every page load)
964 - update_option( 'ayg_transient_keys', $cache_keys, false );
965 725 }
966 726
967 - // Store videos in our custom database table "{$wpdb->prefix}ayg_videos"
968 - //
969 - // What reaches the storage layer is rebuilt from an explicit allowlist instead of being
970 - // handed the raw request params, so the allowlist that governs the outbound request now
971 - // governs the database write made from the same function. Gallery membership ("uid") is
972 - // the value that decides what a gallery displays, so it is only passed along when the
973 - // caller opted in through "store" — never on the strength of a "uid" alone.
974 - $store_attributes = array(
975 - 'type' => isset( $this->params['type'] ) ? $this->params['type'] : '',
976 - 'exclude' => ( isset( $this->params['exclude'] ) && is_array( $this->params['exclude'] ) ) ? $this->params['exclude'] : array()
977 - );
978 -
979 - if ( $this->can_store && ! empty( $this->params['uid'] ) ) {
980 - $store_attributes['uid'] = (string) $this->params['uid'];
981 - }
982 -
983 - ayg_db_store_videos( $data, $store_attributes );
984 -
727 + // Store videos in our custom database table "{$wpdb->prefix}ayg_videos"
728 + ayg_db_store_videos( $data );
729 +
985 730 // Finally return the data
986 731 return $data;
987 732 }
988 733
@@ -994,27 +739,24 @@
994 739 * @param object $data YouTube API response object.
995 740 * @return mixed
996 741 */
997 742 private function parse_videos( $data ) {
998 - if ( empty( $data->items ) || ! is_array( $data->items ) ) {
999 - $error = $this->get_error( __( 'No videos found matching your query.', 'automatic-youtube-gallery' ) );
743 + if ( ! isset( $data->items ) ) {
744 + return $this->get_error( __( 'No videos found matching your query.', 'automatic-youtube-gallery' ) );
745 + }
1000 746
1001 - // Flag an empty result set so an incremental sync can tell "nothing new" apart
1002 - // from a genuine API failure (quota, bad source) and complete cleanly.
1003 - $error->no_results = true;
1004 -
1005 - return $error;
747 + $items = $data->items;
748 + if ( ! is_array( $items ) || 0 == count( $items ) ) {
749 + return $this->get_error( __( 'No videos found matching your query.', 'automatic-youtube-gallery' ) );
1006 750 }
1007 751
1008 - $items = $data->items;
1009 - $videos = array();
1010 - $exclude = ( isset( $this->params['exclude'] ) && is_array( $this->params['exclude'] ) ) ? $this->params['exclude'] : array();
752 + $videos = array();
1011 753
1012 754 foreach ( $items as $item ) {
1013 755 $video = new stdClass();
1014 756
1015 757 // Video ID
1016 - $video->id = '';
758 + $video->id = '';
1017 759
1018 760 if ( isset( $item->snippet->resourceId ) && isset( $item->snippet->resourceId->videoId ) ) {
1019 761 $video->id = $item->snippet->resourceId->videoId;
1020 762 } elseif ( isset( $item->contentDetails ) && isset( $item->contentDetails->videoId ) ) {
@@ -1022,16 +764,11 @@
1022 764 } elseif ( isset( $item->id ) && isset( $item->id->videoId ) ) {
1023 765 $video->id = $item->id->videoId;
1024 766 } elseif ( isset( $item->id ) ) {
1025 767 $video->id = $item->id;
1026 - }
768 + }
1027 769
1028 - // Skip videos on the gallery's exclude list
1029 - if ( ayg_is_video_excluded( $video->id, $exclude ) ) {
1030 - continue;
1031 - }
1032 -
1033 - // Video channel ID
770 + // Video channel ID
1034 771 $video->channel_id = '';
1035 772
1036 773 if ( isset( $item->snippet->channelId ) ) {
1037 774 $video->channel_id = $item->snippet->channelId;
@@ -1078,68 +815,8 @@
1078 815 return $videos;
1079 816 }
1080 817
1081 818 /**
1082 - * Enrich parsed videos with duration and live broadcast details.
1083 - *
1084 - * playlistItems.list / search.list responses lack contentDetails.duration and
1085 - * snippet.liveBroadcastContent, so this makes a supplementary videos.list request (1 quota
1086 - * unit per 50 IDs) — updating the stored rows and merging duration + video_type into $videos.
1087 - *
1088 - * @since 2.8.0
1089 - * @access private
1090 - * @param array $videos Parsed video objects from parse_videos().
1091 - * @return array The same videos, with duration / duration_seconds / video_type set.
1092 - */
1093 - private function enrich_video_details( $videos ) {
1094 - // Index by video ID for the merge. PHP holds objects by handle, so mutating a $map
1095 - // entry below also updates the same instance in $videos (what we return).
1096 - $map = array();
1097 -
1098 - foreach ( $videos as $video ) {
1099 - if ( ! empty( $video->id ) ) {
1100 - $map[ $video->id ] = $video;
1101 - }
1102 - }
1103 -
1104 - if ( empty( $map ) ) {
1105 - return $videos;
1106 - }
1107 -
1108 - $api_url = $this->get_api_url( 'videos.list' );
1109 -
1110 - // videos.list accepts up to 50 IDs per request.
1111 - foreach ( array_chunk( array_keys( $map ), 50 ) as $chunk ) {
1112 - $api_params = array(
1113 - 'id' => implode( ',', $chunk ),
1114 - 'part' => 'id,snippet,contentDetails,status'
1115 - );
1116 -
1117 - // request_api() also stores the enriched rows (videos.list response →
1118 - // ayg_db_store_videos() writes duration_seconds + video_type).
1119 - $api_response = $this->request_api( $api_url, $api_params );
1120 -
1121 - if ( isset( $api_response->error ) || empty( $api_response->items ) || ! is_array( $api_response->items ) ) {
1122 - continue;
1123 - }
1124 -
1125 - foreach ( $api_response->items as $item ) {
1126 - if ( empty( $item->id ) || ! isset( $map[ $item->id ] ) ) {
1127 - continue;
1128 - }
1129 -
1130 - $duration = isset( $item->contentDetails->duration ) ? $item->contentDetails->duration : '';
1131 -
1132 - $map[ $item->id ]->duration = $duration;
1133 - $map[ $item->id ]->duration_seconds = ayg_parse_duration_seconds( $duration );
1134 - $map[ $item->id ]->video_type = ! empty( $item->snippet->liveBroadcastContent ) ? $item->snippet->liveBroadcastContent : 'none';
1135 - }
1136 - }
1137 -
1138 - return $videos;
1139 - }
1140 -
1141 - /**
1142 819 * Parse page info from the YouTube API response object.
1143 820 *
1144 821 * @since 1.0.0
1145 822 * @access private
@@ -1146,27 +823,14 @@
1146 823 * @param object $data YouTube API response object.
1147 824 * @return array
1148 825 */
1149 826 private function parse_page_info( $data ) {
1150 - $page_info = array(
1151 - 'videos_found' => 0
1152 - );
827 + $page_info = array();
1153 828
1154 - // Total number of videos found
829 + // Total count of videos found
1155 830 if ( isset( $data->pageInfo ) && isset( $data->pageInfo->totalResults ) ) {
1156 - $page_info['videos_found'] = (int) $data->pageInfo->totalResults;
1157 - }
1158 -
1159 - // Calculate total number of pages
1160 - if ( $page_info['videos_found'] > 0 ) {
1161 - if ( 'search' == $this->params['type'] ) {
1162 - $limit = min( (int) $this->params['limit'], $page_info['videos_found'] );
1163 - $page_info['total_pages'] = ceil( $limit / (int) $this->params['maxResults'] );
1164 - } else {
1165 - $max_results = (int) $this->params['maxResults'];
1166 - $page_info['total_pages'] = ( $max_results <= 0 ) ? 1 : ceil( $page_info['videos_found'] / $max_results );
1167 - }
1168 - }
831 + $page_info['videos_found'] = $data->pageInfo->totalResults;
832 + }
1169 833
1170 834 // Token for the previous page
1171 835 if ( isset( $data->prevPageToken ) ) {
1172 836 $page_info['prev_page_token'] = $data->prevPageToken;