PluginProbe
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels / 2.8.1
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels v2.8.1
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
automatic-youtube-gallery / includes / functions.php

functions.php in Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels 2.8.1, at includes/functions.php

1,750 lines 60.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Helper Functions.
5 *
6 * @link https://plugins360.com
7 * @since 1.0.0
8 *
9 * @package Automatic_YouTube_Gallery
10 */
11
12 // Exit if accessed directly
13 if ( ! defined( 'WPINC' ) ) {
14 die;
15 }
16
17 /**
18 * Build gallery HTML output.
19 *
20 * @since 1.0.0
21 * @param array $args An array of gallery options.
22 * @return mixed
23 */
24 function ayg_build_gallery( $args ) {
25 $general_settings = ayg_get_option( 'ayg_general_settings' );
26 $strings_settings = ayg_get_option( 'ayg_strings_settings' );
27
28 global $post;
29
30 // Vars
31 $fields = ayg_get_editor_fields();
32 $excluded = array( 'popup' ); // Fields not part of attributes
33
34 // Defaults not backed by an editor field (kept through shortcode_atts): internal "db" source + sort.
35 $defaults = array(
36 'db' => '',
37 'sort_by' => 'date',
38 'sort_order' => 'desc',
39 'sort_seed' => '',
40 'duration_filter' => '',
41 'duration' => 0
42 );
43
44 foreach ( $fields as $key => $value ) {
45 foreach ( $value['fields'] as $field ) {
46 if ( in_array( $field['name'], $excluded ) ) {
47 continue;
48 }
49
50 $defaults[ $field['name'] ] = $field['value'];
51 }
52 }
53
54 $defaults = array_merge( $defaults, (array) $strings_settings );
55
56 // Gallery Builder mode: a numeric "id" loads the saved gallery config, layers its saved
57 // settings under $args (shortcode attributes still win, except the source), and switches
58 // the source to the internal "db" type so the existing build pipeline serves it from the DB.
59 if ( isset( $args['id'] ) && is_numeric( $args['id'] ) && (int) $args['id'] > 0 ) {
60 $config = ayg_get_gallery( (int) $args['id'] );
61
62 if ( ! $config ) {
63 return sprintf( '<div class="ayg ayg-error">%s</div>', esc_html__( 'Gallery not found.', 'automatic-youtube-gallery' ) );
64 }
65
66 $config_params = json_decode( (string) $config->params, true );
67 if ( ! is_array( $config_params ) ) {
68 $config_params = array();
69 }
70
71 // Saved settings act as defaults; explicit shortcode attributes override them.
72 $args = array_merge( $config_params, $args );
73
74 // The source always comes from the config and can't be overridden by the shortcode.
75 // Livestream and search query the live API at display time (nothing is imported/stored);
76 // everything else renders from the DB.
77 if ( 'livestream' === $config->source_type ) {
78 $args['type'] = 'livestream';
79 $args['channel'] = $config->source_value;
80 } elseif ( 'search' === $config->source_type ) {
81 $args['type'] = 'search';
82 $args['search'] = $config->source_value;
83 } elseif ( 'video' === $config->source_type ) {
84 // A single video renders live from the API (like search / livestream) — nothing is
85 // imported or stored — so serve it through the live 'video' source type.
86 $args['type'] = 'video';
87 $args['video'] = $config->source_value;
88 } else {
89 $args['type'] = 'db';
90 $args['db'] = strval( $config->id );
91 }
92
93 // Single video + popup mode → force the popup theme, mirroring the block and widget (which
94 // apply this before calling ayg_build_gallery()).
95 if ( 'video' === $config->source_type && ! empty( $args['popup'] ) ) {
96 $args['theme'] = 'popup';
97 }
98
99 // Gallery Builder relationships are keyed by the config ID as a string.
100 $args['uid'] = strval( $config->id );
101 }
102
103 $attributes = shortcode_atts( $defaults, $args, 'automatic_youtube_gallery' );
104
105 $attributes['post_id'] = 0;
106 if ( isset( $post->ID ) ) {
107 $attributes['post_id'] = (int) $post->ID;
108 }
109
110 $attributes['columns'] = min( 12, (int) $attributes['columns'] );
111 if ( empty( $attributes['columns'] ) ) {
112 $attributes['columns'] = 3;
113 }
114
115 $attributes['limit'] = min( 500, (int) $attributes['limit'] );
116 if ( empty( $attributes['limit'] ) ) {
117 $attributes['limit'] = 500;
118 }
119
120 $source_type = sanitize_text_field( $attributes['type'] );
121
122 // Videos per page. Database (Gallery Builder) galleries read from our own table, so 0 means
123 // "show all videos on a single page" (no pagination). The live-API sources (search, livestream)
124 // stay capped at 50 because that is YouTube's maximum number of results per request.
125 if ( 'db' === $source_type ) {
126 $attributes['per_page'] = max( 0, (int) $attributes['per_page'] );
127 } else {
128 $attributes['per_page'] = min( 50, (int) $attributes['per_page'] );
129 if ( empty( $attributes['per_page'] ) ) {
130 $attributes['per_page'] = 50;
131 }
132 }
133
134 if ( 'livestream' == $source_type ) {
135 $attributes['livestream'] = $attributes['channel'];
136 }
137
138 $attributes['lazyload'] = 0;
139 if ( ! empty( $general_settings['lazyload'] ) ) {
140 $attributes['lazyload'] = 1;
141 }
142
143 $source_url = sanitize_text_field( $attributes[ $source_type ] );
144
145 if ( isset( $args['id'] ) && ! empty( $args['id'] ) ) {
146 $attributes['id'] = absint( $args['id'] );
147 }
148
149 if ( isset( $args['uid'] ) && ! empty( $args['uid'] ) ) {
150 $attributes['uid'] = sanitize_text_field( $args['uid'] );
151 } else {
152 $attributes['uid'] = md5( $source_type . $source_url );
153 }
154
155 $attributes['uid'] = apply_filters( 'ayg_gallery_id', $attributes['uid'], $args );
156
157 // Deprecated since v2.5.8. Retained for backward compatibility.
158 $deprecated_uid = md5( $source_type . $source_url . sanitize_text_field( $attributes['theme'] ) ); // Deprecated
159 if ( isset( $args['deprecated_uid'] ) && ! empty( $args['deprecated_uid'] ) ) {
160 $deprecated_uid = sanitize_text_field( $args['deprecated_uid'] );
161 }
162
163 // Deeplinked video (premium): resolve before the query so DB-served galleries can pin it as
164 // the first item at the SQL level, keeping the per-page count exact. API-served (legacy)
165 // galleries keep the old prepend behavior below.
166 $gallery_id_from_url = get_query_var( 'ayg_gallery_id' );
167 $video_id_from_url = get_query_var( 'ayg_video_id' );
168 $deeplinked_video = false;
169
170 if ( ! empty( $video_id_from_url ) && ( $attributes['uid'] == $gallery_id_from_url || $deprecated_uid == $gallery_id_from_url ) ) {
171 $deeplinked_video = ayg_db_get_video( $video_id_from_url );
172 }
173
174 if ( ! $deeplinked_video ) {
175 $video_id_from_url = '';
176 }
177
178 $featured_video_id = ( 'db' === $source_type && ! empty( $video_id_from_url ) ) ? sanitize_text_field( $video_id_from_url ) : '';
179
180 // Random ordering: generate a per-render seed (once) so RAND(seed) stays stable across this
181 // gallery's own pagination/search AJAX calls. A seed hardcoded in the shortcode is respected.
182 if ( 'random' === $attributes['sort_by'] && empty( $attributes['sort_seed'] ) ) {
183 $attributes['sort_seed'] = wp_rand( 1, 2147483647 );
184 }
185
186 // Get Videos
187 $api_params = array(
188 'uid' => $attributes['uid'],
189 'type' => $source_type,
190 'src' => $source_url,
191 'featured_video_id' => $featured_video_id, // Works only when type = "db".
192 'order' => sanitize_text_field( $attributes['order'] ), // Works only when type = "search".
193 'sort_by' => sanitize_key( $attributes['sort_by'] ), // Works only when type = "db".
194 'sort_order' => sanitize_key( $attributes['sort_order'] ), // Works only when type = "db".
195 'sort_seed' => absint( $attributes['sort_seed'] ), // Works only when type = "db" + sort_by = "random".
196 'duration_filter' => sanitize_key( $attributes['duration_filter'] ), // Works only when type = "db".
197 'duration' => absint( $attributes['duration'] ), // Works only when type = "db".
198 'limit' => $attributes['limit'], // Works only when type = "search".
199 'maxResults' => $attributes['per_page'],
200 'cache' => (int) $attributes['cache']
201 );
202
203 $api_params = apply_filters( 'ayg_youtube_api_request_params', $api_params, $args );
204
205 $youtube_api = new AYG_YouTube_API();
206 $response = $youtube_api->query( $api_params );
207
208 // Process output
209 if ( ! isset( $response->error ) ) {
210 // Store Gallery ID
211 if ( $attributes['post_id'] > 0 && isset( $attributes['deeplinking'] ) && 1 == $attributes['deeplinking'] ) {
212 $pages = ayg_get_option( 'ayg_gallery_page_ids' );
213 $page_id = $attributes['post_id'];
214
215 if ( ! in_array( $page_id, $pages ) ) {
216 $pages[] = $page_id;
217 update_option( 'ayg_gallery_page_ids', $pages );
218 }
219 }
220
221 // Gallery
222 $videos = array();
223
224 if ( ! empty( $video_id_from_url ) ) {
225 // DB-served galleries pin the deeplinked video via the SQL query itself, so the result
226 // set already starts with it. Fall back to prepending it only when the pin couldn't
227 // apply: API-served (legacy) sources, or the video is no longer part of this gallery's
228 // result set (removed from the gallery, or excluded by a duration filter).
229 $is_pinned_by_sql = ! empty( $api_params['featured_video_id'] ) && ! empty( $response->videos ) && $response->videos[0]->id == $video_id_from_url;
230
231 if ( ! $is_pinned_by_sql ) {
232 $videos[] = $deeplinked_video;
233 }
234 }
235
236 if ( isset( $response->videos ) ) {
237 if ( ! empty( $videos ) ) {
238 foreach ( $response->videos as $video ) {
239 if ( $video->id != $video_id_from_url ) {
240 $videos[] = $video;
241 }
242 }
243 } else {
244 $videos = $response->videos;
245 }
246 }
247
248 // Pagination
249 if ( isset( $response->page_info ) ) {
250 $attributes = array_merge( $attributes, $api_params, $response->page_info );
251 }
252
253 // Theme
254 $theme = 'classic';
255
256 if ( 'video' == $source_type ) {
257 $theme = 'single';
258 } elseif ( 'livestream' == $source_type ) {
259 $theme = 'livestream';
260 } else {
261 if ( 1 == count( $videos ) ) {
262 $theme = 'single';
263 }
264 }
265
266 // Enqueue dependencies
267 wp_enqueue_style( AYG_SLUG . '-public' );
268
269 wp_enqueue_script( AYG_SLUG . '-public' );
270 if ( $attributes['theme'] == $theme && 'classic' == $attributes['theme'] ) {
271 wp_enqueue_script( AYG_SLUG . '-theme-classic' );
272 }
273
274 // Output
275 ob_start();
276 include ayg_get_template( AYG_DIR . "public/templates/theme-{$theme}.php", $attributes['theme'] );
277 return ob_get_clean();
278 } else {
279 return sprintf( '<div class="ayg ayg-error">%s</div>', wp_kses_post( $response->error_message ) );
280 }
281 }
282
283 /**
284 * Combine video attributes as a string.
285 *
286 * @since 2.5.0
287 * @param array $atts Array of video attributes.
288 * @param string Combined attributes string.
289 */
290 function ayg_combine_video_attributes( $atts ) {
291 $attributes = array();
292
293 foreach ( $atts as $key => $value ) {
294 if ( '' === $value ) {
295 $attributes[] = $key;
296 } else {
297 $attributes[] = sprintf( '%s="%s"', $key, $value );
298 }
299 }
300
301 return implode( ' ', $attributes );
302 }
303
304 /**
305 * Create custom database tables
306 *
307 * @since 2.1.0
308 */
309 function ayg_db_create_custom_tables() {
310 global $wpdb;
311
312 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
313 $charset_collate = $wpdb->get_charset_collate();
314
315 $videos_table = $wpdb->prefix . 'ayg_videos';
316 $rel_table = $wpdb->prefix . 'ayg_gallery_relationships';
317 $old_table = $wpdb->prefix . 'ayg_galleries';
318
319 // Rename wp_ayg_galleries (old pivot) → wp_ayg_gallery_relationships.
320 // Guard: only when the old table exists AND the new name does not yet exist AND
321 // the old table is the pivot (identified by having a video_id column).
322 $old_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old_table ) ) ) === $old_table;
323 $new_not_exist = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $rel_table ) ) ) !== $rel_table;
324
325 if ( $old_exists && $new_not_exist ) {
326 $is_old_pivot = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$old_table` LIKE 'video_id'" ) );
327
328 if ( $is_old_pivot ) {
329 $renamed = $wpdb->query( "RENAME TABLE `$old_table` TO `$rel_table`" );
330
331 if ( false === $renamed ) {
332 // Rename failed (insufficient DB privilege on some hosts).
333 // This table holds auto-generated cache data, not user content — safe to drop.
334 $wpdb->query( "DROP TABLE IF EXISTS `$old_table`" );
335 }
336 }
337 }
338
339 // Rename columns in wp_ayg_videos (NUM → id, id → video_id).
340 // Each ALTER is guarded so the block is idempotent on re-runs.
341 $videos_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $videos_table ) ) ) === $videos_table;
342
343 if ( $videos_table_exists ) {
344 // Rename 'id' → 'video_id' first. Must happen before NUM → id to avoid a duplicate
345 // column name error: if NUM were renamed to 'id' while the varchar 'id' still exists,
346 // MySQL would reject it with "Duplicate column name 'id'".
347 $old_id_col = $wpdb->get_row( "SHOW COLUMNS FROM `$videos_table` LIKE 'id'" );
348
349 if ( $old_id_col && false !== strpos( strtolower( $old_id_col->Type ), 'varchar' ) ) {
350 $wpdb->query( "ALTER TABLE `$videos_table` CHANGE `id` `video_id` varchar(100) NOT NULL" );
351 }
352
353 // Now safe to rename NUM → id (no column named 'id' exists at this point).
354 $has_num = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$videos_table` LIKE 'NUM'" ) );
355
356 if ( $has_num ) {
357 $wpdb->query( "ALTER TABLE `$videos_table` CHANGE `NUM` `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT" );
358 }
359
360 // Remove duplicate rows using the new column names (idempotent).
361 $wpdb->query(
362 "DELETE v1 FROM $videos_table v1
363 INNER JOIN $videos_table v2
364 ON v1.video_id = v2.video_id AND v1.id > v2.id"
365 );
366 }
367
368 // Create/update wp_ayg_videos — dbDelta adds the 2 new columns on existing installs.
369 $sql = "CREATE TABLE $videos_table (
370 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
371 video_id varchar(100) NOT NULL,
372 title text NOT NULL,
373 description text NOT NULL,
374 thumbnails text NOT NULL,
375 duration varchar(25) NOT NULL,
376 duration_seconds int(11) UNSIGNED NOT NULL DEFAULT 0,
377 video_type varchar(20) NOT NULL DEFAULT 'none',
378 status varchar(100) NOT NULL,
379 published_at varchar(100) NOT NULL,
380 published_at_datetime datetime NULL,
381 PRIMARY KEY (id),
382 UNIQUE KEY ayg_unique_video_id (video_id),
383 INDEX ayg_idx_published_at_datetime (published_at_datetime)
384 ) $charset_collate;";
385
386 dbDelta( $sql );
387
388 // Fallback: add UNIQUE KEY if dbDelta did not create it (edge case on existing installs).
389 $existing_keys = $wpdb->get_results( "SHOW KEYS FROM $videos_table WHERE Key_name = 'ayg_unique_video_id'" );
390
391 if ( empty( $existing_keys ) ) {
392 $wpdb->query( "ALTER TABLE $videos_table ADD UNIQUE KEY ayg_unique_video_id (video_id)" );
393 }
394
395 // Fallback: add INDEX on published_at_datetime if missing (existing installs).
396 $existing_dt_index = $wpdb->get_results( "SHOW KEYS FROM $videos_table WHERE Key_name = 'ayg_idx_published_at_datetime'" );
397
398 if ( empty( $existing_dt_index ) ) {
399 $wpdb->query( "ALTER TABLE $videos_table ADD INDEX ayg_idx_published_at_datetime (published_at_datetime)" );
400 }
401
402 // Rename NUM → id in wp_ayg_gallery_relationships (only if rename succeeded above).
403 // If the table was recreated fresh by dbDelta below, it already has 'id' — no ALTER needed.
404 $rel_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $rel_table ) ) ) === $rel_table;
405
406 if ( $rel_table_exists ) {
407 $has_num_in_rel = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$rel_table` LIKE 'NUM'" ) );
408
409 if ( $has_num_in_rel ) {
410 $wpdb->query( "ALTER TABLE `$rel_table` CHANGE `NUM` `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT" );
411 }
412 }
413
414 // Create/update wp_ayg_gallery_relationships (fresh creation if the rename fallback dropped the old table).
415 $sql = "CREATE TABLE $rel_table (
416 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
417 video_id varchar(100) NOT NULL,
418 gallery_id varchar(100) NOT NULL,
419 synced_at datetime NULL,
420 PRIMARY KEY (id),
421 UNIQUE KEY ayg_unique_video_gallery (video_id, gallery_id),
422 INDEX ayg_idx_gallery_id (gallery_id)
423 ) $charset_collate;";
424
425 dbDelta( $sql );
426
427 // Fallback: add the synced_at column if dbDelta did not (existing installs).
428 $has_synced_at = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$rel_table` LIKE 'synced_at'" ) );
429
430 if ( ! $has_synced_at ) {
431 $wpdb->query( "ALTER TABLE `$rel_table` ADD `synced_at` datetime NULL" );
432 }
433
434 // Create new wp_ayg_galleries entity table (one row per saved gallery config).
435 $galleries_table = $wpdb->prefix . 'ayg_galleries';
436
437 $sql = "CREATE TABLE $galleries_table (
438 id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
439 title text NOT NULL,
440 source_type varchar(50) NOT NULL DEFAULT 'channel',
441 source_value text NOT NULL,
442 params longtext NOT NULL,
443 import_status varchar(20) NOT NULL DEFAULT 'idle',
444 import_error text NULL,
445 import_log longtext NULL,
446 last_imported_at datetime NULL,
447 next_import_at datetime NULL,
448 video_count int(11) UNSIGNED NOT NULL DEFAULT 0,
449 created_at datetime NOT NULL,
450 updated_at datetime NOT NULL,
451 PRIMARY KEY (id),
452 INDEX idx_import (next_import_at, import_status)
453 ) $charset_collate;";
454
455 dbDelta( $sql );
456 }
457
458 /**
459 * Get a single video record from our custom database table "{$wpdb->prefix}ayg_videos"
460 *
461 * @since 2.1.0
462 * @param string $video_id YouTube Video ID.
463 * @return mixed
464 */
465 function ayg_db_get_video( $video_id ) {
466 global $wpdb;
467
468 $cache_key = 'ayg_'. $video_id;
469 $row = wp_cache_get( $cache_key );
470
471 if ( false === $row ) {
472 $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ayg_videos WHERE video_id = %s", $video_id );
473 $row = $wpdb->get_row( $query );
474
475 if ( $row ) {
476 if ( ! empty( $row->thumbnails ) ) {
477 $row->thumbnails = unserialize( $row->thumbnails );
478 }
479
480 // Backward compat: templates reference $video->id as the YouTube video ID.
481 $row->id = $row->video_id;
482
483 wp_cache_set( $cache_key, $row );
484 }
485 }
486
487 return $row;
488 }
489
490 /**
491 * Store videos in our custom database table "{$wpdb->prefix}ayg_videos"
492 *
493 * @since 2.1.0
494 * @param object $data YouTube API response object.
495 * @param array $attributes Array of user attributes.
496 */
497 function ayg_db_store_videos( $data, $attributes = array() ) {
498 if ( AYG_VERSION !== get_option( 'ayg_version' ) ) {
499 return false;
500 }
501
502 if ( isset( $data->kind ) && 'youtube#channelListResponse' == $data->kind ) {
503 return false;
504 }
505
506 if ( empty( $data->items ) || ! is_array( $data->items ) ) {
507 return false;
508 }
509
510 global $wpdb;
511
512 $items = $data->items;
513 $gallery_id = isset( $attributes['uid'] ) ? $attributes['uid'] : '';
514 $source_type = isset( $attributes['type'] ) ? $attributes['type'] : 'videos';
515 $store_gallery = ! empty( $gallery_id ) && 'livestream' !== $source_type;
516 $exclude = ( isset( $attributes['exclude'] ) && is_array( $attributes['exclude'] ) ) ? $attributes['exclude'] : array();
517
518 // Only videos.list responses carry contentDetails.duration and snippet.liveBroadcastContent.
519 // For other responses (playlistItems.list, search.list) the duration_seconds / video_type
520 // columns are left out entirely so existing values are never clobbered with defaults.
521 $has_video_details = isset( $data->kind ) && 'youtube#videoListResponse' === $data->kind;
522
523 $videos_table = $wpdb->prefix . 'ayg_videos';
524 $galleries_table = $wpdb->prefix . 'ayg_gallery_relationships';
525
526 $video_placeholders = array();
527 $video_values = array();
528 $gallery_placeholders = array();
529 $gallery_values = array();
530
531 foreach ( $items as $item ) {
532 $row = array();
533
534 // Video ID
535 $row['video_id'] = '';
536
537 if ( isset( $item->snippet->resourceId ) && isset( $item->snippet->resourceId->videoId ) ) {
538 $row['video_id'] = $item->snippet->resourceId->videoId;
539 } elseif ( isset( $item->contentDetails ) && isset( $item->contentDetails->videoId ) ) {
540 $row['video_id'] = $item->contentDetails->videoId;
541 } elseif ( isset( $item->id ) && isset( $item->id->videoId ) ) {
542 $row['video_id'] = $item->id->videoId;
543 } elseif ( isset( $item->id ) ) {
544 $row['video_id'] = $item->id;
545 }
546
547 if ( empty( $row['video_id'] ) ) {
548 continue;
549 }
550
551 // Skip videos on the gallery's exclude list (kept out of both tables)
552 if ( ayg_is_video_excluded( $row['video_id'], $exclude ) ) {
553 continue;
554 }
555
556 // Video title
557 $row['title'] = $item->snippet->title;
558
559 // Video description
560 $row['description'] = $item->snippet->description;
561
562 // Video thumbnails
563 $row['thumbnails'] = '';
564 if ( isset( $item->snippet->thumbnails ) ) {
565 $row['thumbnails'] = serialize( $item->snippet->thumbnails );
566 }
567
568 // Video duration
569 $row['duration'] = '';
570 if ( isset( $item->contentDetails ) && isset( $item->contentDetails->duration ) ) {
571 $row['duration'] = $item->contentDetails->duration;
572 }
573
574 if ( $has_video_details ) {
575 $row['duration_seconds'] = ayg_parse_duration_seconds( $row['duration'] );
576
577 // Raw liveBroadcastContent value ('live', 'upcoming', 'none'); shorts are not
578 // classified at import time — filter by duration_seconds <= 60 at query time.
579 $row['video_type'] = 'none';
580 if ( ! empty( $item->snippet->liveBroadcastContent ) ) {
581 $row['video_type'] = $item->snippet->liveBroadcastContent;
582 }
583 }
584
585 // Video status
586 $row['status'] = 'private';
587
588 if ( isset( $item->status ) && ( 'public' == $item->status->privacyStatus || 'unlisted' == $item->status->privacyStatus ) ) {
589 $row['status'] = 'public';
590 }
591
592 if ( isset( $item->snippet->status ) && ( 'public' == $item->snippet->status->privacyStatus || 'unlisted' == $item->snippet->status->privacyStatus ) ) {
593 $row['status'] = 'public';
594 }
595
596 if ( 'youtube#searchResult' == $item->kind ) {
597 $row['status'] = 'public';
598 }
599
600 // Video publish date
601 $row['published_at'] = $item->snippet->publishedAt;
602
603 $datetime = new DateTime( $item->snippet->publishedAt );
604 $row['published_at_datetime'] = date_format( $datetime, 'Y-m-d H:i:s' );
605
606 // Collect for bulk insert
607 if ( $has_video_details ) {
608 $video_placeholders[] = '(%s, %s, %s, %s, %s, %d, %s, %s, %s, %s)';
609
610 array_push(
611 $video_values,
612 $row['video_id'],
613 $row['title'],
614 $row['description'],
615 $row['thumbnails'],
616 $row['duration'],
617 $row['duration_seconds'],
618 $row['video_type'],
619 $row['status'],
620 $row['published_at'],
621 $row['published_at_datetime']
622 );
623 } else {
624 $video_placeholders[] = '(%s, %s, %s, %s, %s, %s, %s, %s)';
625
626 array_push(
627 $video_values,
628 $row['video_id'],
629 $row['title'],
630 $row['description'],
631 $row['thumbnails'],
632 $row['duration'],
633 $row['status'],
634 $row['published_at'],
635 $row['published_at_datetime']
636 );
637 }
638
639 if ( $store_gallery ) {
640 $gallery_placeholders[] = '(%s, %s)';
641 array_push( $gallery_values, $row['video_id'], $gallery_id );
642 }
643 }
644
645 // Bulk insert videos (2 queries total instead of N×2)
646 if ( ! empty( $video_placeholders ) ) {
647 if ( $has_video_details ) {
648 $sql = "INSERT INTO $videos_table (video_id, title, description, thumbnails, duration, duration_seconds, video_type, status, published_at, published_at_datetime)
649 VALUES " . implode( ', ', $video_placeholders ) . "
650 ON DUPLICATE KEY UPDATE
651 title = VALUES(title),
652 description = VALUES(description),
653 thumbnails = VALUES(thumbnails),
654 duration = VALUES(duration),
655 duration_seconds = VALUES(duration_seconds),
656 video_type = VALUES(video_type),
657 status = VALUES(status),
658 published_at = VALUES(published_at),
659 published_at_datetime = VALUES(published_at_datetime)";
660 } else {
661 $sql = "INSERT INTO $videos_table (video_id, title, description, thumbnails, duration, status, published_at, published_at_datetime)
662 VALUES " . implode( ', ', $video_placeholders ) . "
663 ON DUPLICATE KEY UPDATE
664 title = VALUES(title),
665 description = VALUES(description),
666 thumbnails = VALUES(thumbnails),
667 duration = VALUES(duration),
668 status = VALUES(status),
669 published_at = VALUES(published_at),
670 published_at_datetime = VALUES(published_at_datetime)";
671 }
672
673 $wpdb->query( $wpdb->prepare( $sql, $video_values ) );
674 }
675
676 // Bulk insert gallery relationships (2 queries total instead of N×2)
677 if ( ! empty( $gallery_placeholders ) ) {
678 $sql = "INSERT IGNORE INTO $galleries_table (video_id, gallery_id) VALUES " . implode( ', ', $gallery_placeholders );
679 $wpdb->query( $wpdb->prepare( $sql, $gallery_values ) );
680 }
681 }
682
683 /**
684 * Dump our plugin transients.
685 *
686 * @since 2.1.0
687 * @param string $uid Gallery uid to target. Empty string flushes all plugin transients.
688 */
689 function ayg_delete_cache( $uid = '' ) {
690 $uid = (string) $uid;
691 $keys = (array) ayg_get_option( 'ayg_transient_keys' );
692
693 // Targeted: delete only the keys prefixed with this gallery's uid, keep the rest in the registry.
694 if ( '' !== $uid ) {
695 $prefix = 'ayg_' . $uid . '_';
696 $remaining = array();
697
698 foreach ( $keys as $key ) {
699 if ( 0 === strpos( $key, $prefix ) ) {
700 delete_transient( $key );
701 } else {
702 $remaining[] = $key;
703 }
704 }
705
706 update_option( 'ayg_transient_keys', $remaining, false );
707 return;
708 }
709
710 // Full flush: every transient, plus the cached gallery page-id lookup.
711 delete_option( 'ayg_gallery_page_ids' );
712
713 foreach ( $keys as $key ) {
714 delete_transient( $key );
715 }
716
717 // Reset our DB value (autoload=no: not needed on every page load)
718 update_option( 'ayg_transient_keys', array(), false );
719 }
720
721 /**
722 * Remove a gallery's excluded videos from the relationship table.
723 *
724 * @since 2.8.0
725 * @param int $gallery_id Gallery ID.
726 * @param array $exclude Exclude entries (video IDs or URLs).
727 * @return int Number of relationships removed.
728 */
729 function ayg_delete_excluded_relationships( $gallery_id, $exclude ) {
730 global $wpdb;
731
732 $gallery_id = absint( $gallery_id );
733
734 if ( $gallery_id <= 0 || empty( $exclude ) || ! is_array( $exclude ) ) {
735 return 0;
736 }
737
738 // Resolve the exclude entries (IDs or URLs) to video IDs.
739 $video_ids = array();
740
741 foreach ( $exclude as $entry ) {
742 $video_id = ayg_get_youtube_video_id( $entry );
743
744 if ( '' !== $video_id ) {
745 $video_ids[] = $video_id;
746 }
747 }
748
749 $video_ids = array_values( array_unique( $video_ids ) );
750
751 if ( empty( $video_ids ) ) {
752 return 0;
753 }
754
755 $rel_table = $wpdb->prefix . 'ayg_gallery_relationships';
756 $placeholders = implode( ',', array_fill( 0, count( $video_ids ), '%s' ) );
757
758 return (int) $wpdb->query(
759 $wpdb->prepare(
760 "DELETE FROM $rel_table WHERE gallery_id = %s AND video_id IN ( $placeholders )",
761 array_merge( array( strval( $gallery_id ) ), $video_ids )
762 )
763 );
764 }
765
766 /**
767 * Get current address bar URL.
768 *
769 * @since 2.1.0
770 * @return string Current Page URL.
771 */
772 function ayg_get_current_url() {
773 global $wp;
774 $current_url = home_url( add_query_arg( array(), $wp->request ) );
775
776 return $current_url;
777 }
778
779 /**
780 * Get default plugin settings.
781 *
782 * @since 1.6.4
783 * @return array $defaults Array of plugin settings.
784 */
785 function ayg_get_default_settings() {
786 $defaults = array(
787 'ayg_general_settings' => array(
788 'force_load_assets' => array(
789 'css' => 'css'
790 ),
791 'api_key' => '',
792 'lazyload' => 0,
793 'development_mode' => 0
794 ),
795 'ayg_strings_settings' => array(
796 'more_button_label' => __( 'Load More', 'automatic-youtube-gallery' ),
797 'previous_button_label' => __( 'Previous', 'automatic-youtube-gallery' ),
798 'next_button_label' => __( 'Next', 'automatic-youtube-gallery' ),
799 'show_more_label' => __( 'Show More', 'automatic-youtube-gallery' ),
800 'show_less_label' => __( 'Show Less', 'automatic-youtube-gallery' )
801 ),
802 'ayg_gallery_settings' => array(
803 'theme' => 'classic',
804 'columns' => 3,
805 'per_page' => 12,
806 'thumb_ratio' => 56.25,
807 'thumb_title' => 1,
808 'thumb_title_length' => 0,
809 'thumb_excerpt' => 1,
810 'thumb_excerpt_length' => 75,
811 'pagination' => 1,
812 'pagination_type' => 'more',
813 'scroll_top_offset' => 10
814 ),
815 'ayg_player_settings' => array(
816 'player_type' => 'youtube',
817 'player_color' => '#00b3ff',
818 'player_width' => '',
819 'player_ratio' => 56.25,
820 'player_title' => 1,
821 'player_description' => 1,
822 'autoplay' => 0,
823 'autoadvance' => 1,
824 'loop' => 0,
825 'muted' => 0,
826 'controls' => 1,
827 'modestbranding' => 1,
828 'cc_load_policy' => 0,
829 'hl' => '',
830 'cc_lang_pref' => '',
831 'privacy_enhanced_mode' => 0,
832 'origin' => 0
833 ),
834 'ayg_livestream_settings' => array(
835 'fallback_message' => __( 'Sorry, but the channel is not currently streaming live content. Please check back later.', 'automatic-youtube-gallery' )
836 ),
837 'ayg_privacy_settings' => array(
838 'cookie_consent' => 0,
839 'consent_message' => __( 'Please accept YouTube cookies to play this video. By accepting you will be accessing content from YouTube, a service provided by an external third party.', 'automatic-youtube-gallery' ),
840 'button_label' => __( 'Accept', 'automatic-youtube-gallery' )
841 )
842 );
843
844 return $defaults;
845 }
846
847 /**
848 * Get editor fields.
849 *
850 * @since 1.0.0
851 * @return array Array of fields.
852 */
853 function ayg_get_editor_fields() {
854 $fields = array(
855 'source' => array(
856 'label' => __( 'Source Configuration', 'automatic-youtube-gallery' ),
857 'fields' => array(
858 array(
859 'name' => 'type',
860 'label' => __( 'Source Type', 'automatic-youtube-gallery' ),
861 'description' => '',
862 'type' => 'select',
863 'options' => ayg_get_source_types(),
864 'value' => 'playlist',
865 'sanitize_callback' => 'sanitize_key'
866 ),
867 array(
868 'name' => 'playlist',
869 'label' => __( 'YouTube Playlist ID (or) URL', 'automatic-youtube-gallery' ),
870 'description' => sprintf( '%s: https://www.youtube.com/playlist?list=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
871 'type' => 'url',
872 'required' => 1,
873 'value' => '',
874 'sanitize_callback' => 'sanitize_text_field'
875 ),
876 array(
877 'name' => 'channel',
878 'label' => __( 'YouTube Channel ID (or) a Video URL from the Channel', 'automatic-youtube-gallery' ),
879 'description' => sprintf( '%s: https://www.youtube.com/channel/XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
880 'type' => 'url',
881 'required' => 1,
882 'value' => '',
883 'sanitize_callback' => 'sanitize_text_field'
884 ),
885 array(
886 'name' => 'username',
887 'label' => __( 'YouTube Account Username', 'automatic-youtube-gallery' ),
888 'description' => sprintf( '%s: SanRosh', __( 'Example', 'automatic-youtube-gallery' ) ),
889 'type' => 'text',
890 'required' => 1,
891 'value' => '',
892 'sanitize_callback' => 'sanitize_text_field'
893 ),
894 array(
895 'name' => 'search',
896 'label' => __( 'Search Keywords', 'automatic-youtube-gallery' ),
897 'description' => sprintf( '%s: Cartoon (space:AND , -:NOT , |:OR)', __( 'Example', 'automatic-youtube-gallery' ) ),
898 'type' => 'text',
899 'required' => 1,
900 'value' => '',
901 'sanitize_callback' => 'sanitize_text_field'
902 ),
903 array(
904 'name' => 'video',
905 'label' => __( 'YouTube Video ID (or) URL', 'automatic-youtube-gallery' ),
906 'description' => sprintf( '%s: https://www.youtube.com/watch?v=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
907 'type' => 'url',
908 'required' => 1,
909 'value' => '',
910 'sanitize_callback' => 'sanitize_text_field'
911 ),
912 array(
913 'name' => 'videos',
914 'label' => __( 'YouTube Video IDs (or) URLs', 'automatic-youtube-gallery' ),
915 'description' => sprintf( '%s: https://www.youtube.com/watch?v=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
916 'type' => 'textarea',
917 'required' => 1,
918 'placeholder' => __( 'Enter one video per line', 'automatic-youtube-gallery' ),
919 'value' => '',
920 'sanitize_callback' => 'sanitize_textarea_field'
921 ),
922 array(
923 'name' => 'order',
924 'label' => __( 'Search Order', 'automatic-youtube-gallery' ),
925 'description' => '',
926 'type' => 'select',
927 'options' => array(
928 'date' => __( 'Date', 'automatic-youtube-gallery' ),
929 'rating' => __( 'Rating', 'automatic-youtube-gallery' ),
930 'relevance' => __( 'Relevance', 'automatic-youtube-gallery' ),
931 'title' => __( 'Title', 'automatic-youtube-gallery' ),
932 'viewCount' => __( 'View Count', 'automatic-youtube-gallery' )
933 ),
934 'value' => 'relevance',
935 'sanitize_callback' => 'sanitize_text_field'
936 ),
937 array(
938 'name' => 'limit',
939 'label' => __( 'Search Limit', 'automatic-youtube-gallery' ),
940 'description' => __( 'Enter the number of videos to display in this gallery. Set to 0 for the maximum amount (500).', 'automatic-youtube-gallery' ),
941 'type' => 'number',
942 'min' => 0,
943 'max' => 500,
944 'value' => 0,
945 'sanitize_callback' => 'intval'
946 ),
947 array(
948 'name' => 'cache',
949 'label' => __( 'Cache Duration', 'automatic-youtube-gallery' ),
950 'description' => __( 'Specifies how frequently we should check your YouTube source for new videos/updates.', 'automatic-youtube-gallery' ),
951 'type' => 'select',
952 'options' => array(
953 '0' => ''. __( 'No Caching', 'automatic-youtube-gallery' ) . '',
954 '900' => __( '15 Minutes', 'automatic-youtube-gallery' ),
955 '1800' => __( '30 Minutes', 'automatic-youtube-gallery' ),
956 '3600' => __( '1 Hour', 'automatic-youtube-gallery' ),
957 '86400' => __( '1 Day', 'automatic-youtube-gallery' ),
958 '604800' => __( '1 Week', 'automatic-youtube-gallery' ),
959 '2419200' => __( '1 Month', 'automatic-youtube-gallery' )
960 ),
961 'value' => 86400,
962 'sanitize_callback' => 'intval'
963 )
964 )
965 ),
966 'gallery' => array(
967 'label' => __( 'Gallery Options', 'automatic-youtube-gallery' ),
968 'fields' => ayg_get_gallery_settings_fields()
969 ),
970 'player' => array(
971 'label' => __( 'Player Options', 'automatic-youtube-gallery' ),
972 'fields' => ayg_get_player_settings_fields()
973 ),
974 'search' => array(
975 'label' => __( 'Search Form', 'automatic-youtube-gallery' ),
976 'fields' => array(
977 array(
978 'name' => 'search_form',
979 'label' => __( 'Search Form', 'automatic-youtube-gallery' ),
980 'description' => sprintf(
981 __( 'Check this option to enable the search form. Having issues? <a href="%s" target="_blank" rel="noopener noreferrer">Check here</a>.', 'automatic-youtube-gallery' ),
982 'https://plugins360.com/automatic-youtube-gallery/searchform/'
983 ),
984 'type' => 'checkbox',
985 'value' => 0,
986 'sanitize_callback' => 'intval'
987 )
988 )
989 )
990 );
991
992 return apply_filters( 'ayg_editor_fields', $fields );
993 }
994
995 /**
996 * Fetch a single gallery row by ID.
997 *
998 * @since 2.8.0
999 * @param int $gallery_id Gallery ID.
1000 * @return object|null
1001 */
1002 function ayg_get_gallery( $gallery_id ) {
1003 global $wpdb;
1004
1005 return $wpdb->get_row(
1006 $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}ayg_galleries` WHERE id = %d", absint( $gallery_id ) )
1007 );
1008 }
1009
1010 /**
1011 * Get gallery settings fields.
1012 *
1013 * @since 1.0.0
1014 * @return array $fields Array of fields.
1015 */
1016 function ayg_get_gallery_settings_fields() {
1017 $gallery_settings = ayg_get_option( 'ayg_gallery_settings' );
1018
1019 $fields = array(
1020 array(
1021 'name' => 'theme',
1022 'label' => __( 'Select Theme (Layout)', 'automatic-youtube-gallery' ),
1023 'description' => ( ayg_fs()->is_not_paying() ? sprintf( __( '<a href="%s">Upgrade Pro</a> for more themes (Popup, Inline, Slider, Playlist).', 'automatic-youtube-gallery' ), esc_url( ayg_fs()->get_upgrade_url() ) ) : '' ),
1024 'type' => 'select',
1025 'options' => array(
1026 'classic' => __( 'Classic', 'automatic-youtube-gallery' )
1027 ),
1028 'value' => $gallery_settings['theme'],
1029 'sanitize_callback' => 'sanitize_key'
1030 ),
1031 array(
1032 'name' => 'columns',
1033 'label' => __( 'Columns', 'automatic-youtube-gallery' ),
1034 'description' => __( 'Enter the number of columns you like to have in the gallery. Maximum of 12.', 'automatic-youtube-gallery' ),
1035 'type' => 'number',
1036 'min' => 0,
1037 'max' => 12,
1038 'value' => $gallery_settings['columns'],
1039 'sanitize_callback' => 'intval'
1040 ),
1041 array(
1042 'name' => 'per_page',
1043 'label' => __( 'Videos per Page', 'automatic-youtube-gallery' ),
1044 'description' => __( 'Enter the number of videos to show per page. Maximum of 50.', 'automatic-youtube-gallery' ),
1045 'type' => 'number',
1046 'min' => 0,
1047 'max' => 50,
1048 'value' => $gallery_settings['per_page'],
1049 'sanitize_callback' => 'intval'
1050 ),
1051 array(
1052 'name' => 'thumb_ratio',
1053 'label' => __( 'Image Height (Ratio)', 'automatic-youtube-gallery' ),
1054 'description' => __( 'Select the ratio value used to calculate the image height in the gallery thumbnails.', 'automatic-youtube-gallery' ),
1055 'type' => 'select',
1056 'options' => array(
1057 '56.25' => __( 'Standard (16:9) — Default', 'automatic-youtube-gallery' ),
1058 '177.78' => __( 'Shorts / Vertical (9:16)', 'automatic-youtube-gallery' ),
1059 '75' => __( 'Classic (4:3)', 'automatic-youtube-gallery' )
1060 ),
1061 'value' => $gallery_settings['thumb_ratio'],
1062 'sanitize_callback' => 'floatval'
1063 ),
1064 array(
1065 'name' => 'thumb_title',
1066 'label' => __( 'Show Video Title', 'automatic-youtube-gallery' ),
1067 'description' => __( 'Check this option to show the video title in each gallery item.', 'automatic-youtube-gallery' ),
1068 'type' => 'checkbox',
1069 'value' => $gallery_settings['thumb_title'],
1070 'sanitize_callback' => 'intval'
1071 ),
1072 array(
1073 'name' => 'thumb_title_length',
1074 'label' => __( 'Video Title Length', 'automatic-youtube-gallery' ),
1075 'description' => __( 'Enter the number of characters you like to show in the title. Set 0 to show the whole title.', 'automatic-youtube-gallery' ),
1076 'type' => 'number',
1077 'min' => 0,
1078 'max' => 500,
1079 'value' => $gallery_settings['thumb_title_length'],
1080 'sanitize_callback' => 'intval'
1081 ),
1082 array(
1083 'name' => 'thumb_excerpt',
1084 'label' => __( 'Show Video Excerpt (Short Description)', 'automatic-youtube-gallery' ),
1085 'description' => __( 'Check this option to show the short description of a video in each gallery item.', 'automatic-youtube-gallery' ),
1086 'type' => 'checkbox',
1087 'value' => $gallery_settings['thumb_excerpt'],
1088 'sanitize_callback' => 'intval'
1089 ),
1090 array(
1091 'name' => 'thumb_excerpt_length',
1092 'label' => __( 'Video Excerpt Length', 'automatic-youtube-gallery' ),
1093 'description' => __( 'Enter the number of characters you like to have in the video excerpt. Set 0 to show the whole description.', 'automatic-youtube-gallery' ),
1094 'type' => 'number',
1095 'min' => 0,
1096 'max' => 500,
1097 'value' => $gallery_settings['thumb_excerpt_length'],
1098 'sanitize_callback' => 'intval'
1099 ),
1100 array(
1101 'name' => 'pagination',
1102 'label' => __( 'Pagination', 'automatic-youtube-gallery' ),
1103 'description' => __( 'Check this option to show the pagination.', 'automatic-youtube-gallery' ),
1104 'type' => 'checkbox',
1105 'value' => $gallery_settings['pagination'],
1106 'sanitize_callback' => 'intval'
1107 ),
1108 array(
1109 'name' => 'pagination_type',
1110 'label' => __( 'Pagination Type', 'automatic-youtube-gallery' ),
1111 'type' => 'select',
1112 'options' => array(
1113 'more' => __( 'More Button', 'automatic-youtube-gallery' ),
1114 'pager' => __( 'Pager', 'automatic-youtube-gallery' )
1115
1116 ),
1117 'value' => $gallery_settings['pagination_type'],
1118 'sanitize_callback' => 'sanitize_key'
1119 )
1120 );
1121
1122 return apply_filters( 'ayg_gallery_settings_fields', $fields );
1123 }
1124
1125 /**
1126 * Return the schedule interval options for import scheduling.
1127 *
1128 * @since 2.8.0
1129 * @return array Value (seconds as string) => label pairs.
1130 */
1131 function ayg_get_import_schedule_options() {
1132 return array(
1133 'paused' => __( '— Pause Automatic Imports —', 'automatic-youtube-gallery' ),
1134 '0' => __( 'Only Once', 'automatic-youtube-gallery' ),
1135 '3600' => __( 'Every Hour', 'automatic-youtube-gallery' ),
1136 '86400' => __( 'Every Day', 'automatic-youtube-gallery' ),
1137 '604800' => __( 'Every Week', 'automatic-youtube-gallery' ),
1138 '2592000' => __( 'Every Month', 'automatic-youtube-gallery' )
1139 );
1140 }
1141
1142 /**
1143 * Return the human-readable label for an import status key.
1144 *
1145 * @since 2.8.0
1146 * @param string $status Import status key (idle, running, paused, error, completed).
1147 * @param int|null $video_count Optional. Imported video count, used to resolve the
1148 * ambiguous "idle" status into "Pending Import" (nothing
1149 * imported yet) or "Up to Date" (waiting for the next sync).
1150 * @param bool $resuming Optional. True when an import was capped mid-run and parked
1151 * as 'idle' for cron to resume — shown as "Importing", not idle.
1152 * @param string $context Optional. 'log' for a per-run history entry, where a successful
1153 * run (stored as 'idle' for recurring galleries) reads "Completed"
1154 * like non-recurring runs, instead of the live-state "Idle".
1155 * @return string
1156 */
1157 function ayg_get_import_status_label( $status, $video_count = null, $resuming = false, $context = '' ) {
1158 if ( 'idle' === $status ) {
1159 // History rows record a finished run: a recurring gallery's success is stored as 'idle',
1160 // so surface it as "Completed" to match non-recurring runs rather than the live "Idle".
1161 if ( 'log' === $context ) {
1162 return __( 'Completed', 'automatic-youtube-gallery' );
1163 }
1164
1165 // If the import was capped mid-run and parked as 'idle' for cron to resume, show "Importing".
1166 if ( $resuming ) {
1167 return __( 'Importing', 'automatic-youtube-gallery' );
1168 }
1169
1170 // If the import has never run, show "Pending Import" instead of "Idle".
1171 if ( null !== $video_count ) {
1172 return ( (int) $video_count > 0 ) ? __( 'Up to Date', 'automatic-youtube-gallery' ) : __( 'Pending Import', 'automatic-youtube-gallery' );
1173 }
1174 }
1175
1176 $statuses = array(
1177 'idle' => __( 'Idle', 'automatic-youtube-gallery' ),
1178 'running' => __( 'Importing', 'automatic-youtube-gallery' ),
1179 'paused' => __( 'Paused', 'automatic-youtube-gallery' ),
1180 'error' => __( 'Error', 'automatic-youtube-gallery' ),
1181 'completed' => __( 'Completed', 'automatic-youtube-gallery' )
1182 );
1183
1184 return isset( $statuses[ $status ] ) ? $statuses[ $status ] : ucfirst( $status );
1185 }
1186
1187 /**
1188 * Retrieve a plugin option with fallback to default settings.
1189 *
1190 * @since 2.7.0
1191 * @param string $option The option name to retrieve.
1192 * @return mixed
1193 */
1194 function ayg_get_option( $option ) {
1195 $defaults = ayg_get_default_settings();
1196 $default = isset( $defaults[ $option ] ) ? $defaults[ $option ] : array();
1197
1198 $saved = get_option( $option, null );
1199
1200 // Option does not exist OR corrupted
1201 if ( null === $saved || ! is_array( $saved ) ) {
1202 return $default;
1203 }
1204
1205 // Merge saved values with defaults
1206 return wp_parse_args( $saved, $default );
1207 }
1208
1209 /**
1210 * Get video description to show on top of the player.
1211 *
1212 * @since 1.0.0
1213 * @param stdClass $video YouTube video object.
1214 * @param array $attributes Array of user attributes.
1215 * @param int $words_count Number of words to show by default.
1216 * @return string Video description.
1217 */
1218 function ayg_get_player_description( $video, $attributes = array(), $words_count = 30 ) {
1219 $description = $video->description;
1220
1221 $words_array = explode( ' ', strip_tags( $description ) );
1222 if ( count( $words_array ) > $words_count ) {
1223 $show_more_label = ! empty( $attributes['show_more_label'] ) ? $attributes['show_more_label'] : __( 'Show More', 'automatic-youtube-gallery' );
1224 $words_array[ $words_count ] = '<span class="ayg-player-description-dots">...</span></span><span class="ayg-player-description-more">' . $words_array[ $words_count ];
1225
1226 $description = '<span class="ayg-player-description-less">' . implode( ' ', $words_array ) . '</span>';
1227 $description .= '<a href="#" class="ayg-player-description-toggle-btn">' . esc_html( $show_more_label ) . '</a>';
1228 }
1229
1230 $description = nl2br( $description );
1231 $description = make_clickable( $description );
1232
1233 return apply_filters( 'ayg_player_description', $description, $video, $attributes, $words_count );
1234 }
1235
1236 /**
1237 * Get player settings fields.
1238 *
1239 * @since 1.0.0
1240 * @return array $fields Array of fields.
1241 */
1242 function ayg_get_player_settings_fields() {
1243 $player_settings = ayg_get_option( 'ayg_player_settings' );
1244
1245 $fields = array(
1246 array(
1247 'name' => 'player_width',
1248 'label' => __( 'Player Width', 'automatic-youtube-gallery' ),
1249 'description' => __( 'In pixels. Maximum width of the player. Leave this field empty to scale 100% of its enclosing container/html element.', 'automatic-youtube-gallery' ),
1250 'type' => 'text',
1251 'value' => isset( $player_settings['player_width'] ) ? $player_settings['player_width'] : '',
1252 'sanitize_callback' => 'ayg_sanitize_int'
1253 ),
1254 array(
1255 'name' => 'player_ratio',
1256 'label' => __( 'Player Height (Ratio)', 'automatic-youtube-gallery' ),
1257 'description' => __( 'Select the ratio value used to calculate the player height.', 'automatic-youtube-gallery' ),
1258 'type' => 'select',
1259 'options' => array(
1260 '56.25' => __( 'Standard (16:9) — Default', 'automatic-youtube-gallery' ),
1261 '177.78' => __( 'Shorts / Vertical (9:16)', 'automatic-youtube-gallery' ),
1262 '75' => __( 'Classic (4:3)', 'automatic-youtube-gallery' )
1263 ),
1264 'value' => $player_settings['player_ratio'],
1265 'sanitize_callback' => 'floatval'
1266 ),
1267 array(
1268 'name' => 'player_title',
1269 'label' => __( 'Show Video Title', 'automatic-youtube-gallery' ),
1270 'description' => __( 'Check this option to show the current playing video title on the bottom of the player.', 'automatic-youtube-gallery' ),
1271 'type' => 'checkbox',
1272 'value' => $player_settings['player_title'],
1273 'sanitize_callback' => 'intval'
1274 ),
1275 array(
1276 'name' => 'player_description',
1277 'label' => __( 'Show Video Description', 'automatic-youtube-gallery' ),
1278 'description' => __( 'Check this option to show the current playing video description on the bottom of the player.', 'automatic-youtube-gallery' ),
1279 'type' => 'checkbox',
1280 'value' => $player_settings['player_description'],
1281 'sanitize_callback' => 'intval'
1282 ),
1283 array(
1284 'name' => 'autoplay',
1285 'label' => __( 'Autoplay', 'automatic-youtube-gallery' ),
1286 'description' => __( 'Check this option to automatically start playing the initial video when the player loads.', 'automatic-youtube-gallery' ),
1287 'type' => 'checkbox',
1288 'value' => $player_settings['autoplay'],
1289 'sanitize_callback' => 'intval'
1290 ),
1291 array(
1292 'name' => 'autoadvance',
1293 'label' => __( 'Autoplay Next Video', 'automatic-youtube-gallery' ),
1294 'description' => __( 'Check this option to automatically play the next video in the list after the previous one ends.', 'automatic-youtube-gallery' ),
1295 'type' => 'checkbox',
1296 'value' => $player_settings['autoadvance'],
1297 'sanitize_callback' => 'intval'
1298 ),
1299 array(
1300 'name' => 'loop',
1301 'label' => __( 'Loop', 'automatic-youtube-gallery' ),
1302 'description' => __( 'Check this option to loop playback. In the case of a single video player, plays the initial video again and again. In the case of a gallery, plays the entire list and then starts again at the first video.', 'automatic-youtube-gallery' ),
1303 'type' => 'checkbox',
1304 'value' => $player_settings['loop'],
1305 'sanitize_callback' => 'intval'
1306 ),
1307 array(
1308 'name' => 'muted',
1309 'label' => __( 'Muted', 'automatic-youtube-gallery' ),
1310 'description' => __( 'Check this option to turn OFF the audio output of the video by default.', 'automatic-youtube-gallery' ),
1311 'type' => 'checkbox',
1312 'value' => isset( $player_settings['muted'] ) ? $player_settings['muted'] : 0,
1313 'sanitize_callback' => 'intval'
1314 ),
1315 array(
1316 'name' => 'controls',
1317 'label' => __( 'Show Player Controls', 'automatic-youtube-gallery' ),
1318 'description' => __( 'Uncheck this option to hide the video player controls.', 'automatic-youtube-gallery' ),
1319 'type' => 'checkbox',
1320 'value' => $player_settings['controls'],
1321 'sanitize_callback' => 'intval'
1322 ),
1323 array(
1324 'name' => 'modestbranding',
1325 'label' => __( 'Hide YouTube Logo', 'automatic-youtube-gallery' ),
1326 'description' => __( "Check this option to prevent the YouTube logo from displaying in the control bar. Note that a small YouTube text label will still display in the upper-right corner of a paused video when the user's mouse pointer hovers over the player.", 'automatic-youtube-gallery' ),
1327 'type' => 'checkbox',
1328 'value' => $player_settings['modestbranding'],
1329 'sanitize_callback' => 'intval'
1330 ),
1331 array(
1332 'name' => 'cc_load_policy',
1333 'label' => __( 'Force Closed Captions', 'automatic-youtube-gallery' ),
1334 'description' => __( 'Check this option to show captions by default, even if the user has turned captions off. The default behavior is based on user preference.', 'automatic-youtube-gallery' ),
1335 'type' => 'checkbox',
1336 'value' => $player_settings['cc_load_policy'],
1337 'sanitize_callback' => 'intval'
1338 ),
1339 array(
1340 'name' => 'hl',
1341 'label' => __( 'Player Language', 'automatic-youtube-gallery' ),
1342 'description' => sprintf(
1343 __( 'Specifies the player\'s interface language. Set the field\'s value to an <a href="%s" target="_blank">ISO 639-1 two-letter language code.</a>', 'automatic-youtube-gallery' ),
1344 'http://www.loc.gov/standards/iso639-2/php/code_list.php'
1345 ),
1346 'type' => 'text',
1347 'value' => $player_settings['hl'],
1348 'sanitize_callback' => 'sanitize_text_field'
1349 ),
1350 array(
1351 'name' => 'cc_lang_pref',
1352 'label' => __( 'Default Captions Language', 'automatic-youtube-gallery' ),
1353 'description' => sprintf(
1354 __( 'Specifies the default language that the player will use to display captions. Set the field\'s value to an <a href="%s" target="_blank">ISO 639-1 two-letter language code.</a>', 'automatic-youtube-gallery' ),
1355 'http://www.loc.gov/standards/iso639-2/php/code_list.php'
1356 ),
1357 'type' => 'text',
1358 'value' => $player_settings['cc_lang_pref'],
1359 'sanitize_callback' => 'sanitize_text_field'
1360 )
1361 );
1362
1363 return $fields;
1364 }
1365
1366
1367 /**
1368 * Get a single video page URL.
1369 *
1370 * @since 2.1.0
1371 * @param stdClass $video YouTube video object.
1372 * @param array $args An array of gallery options.
1373 * @return string Single video URL.
1374 */
1375 function ayg_get_single_video_url( $video, $attributes ) {
1376 return apply_filters( 'ayg_single_video_url', '', $video, $attributes );
1377 }
1378
1379 /**
1380 * Return the human-readable label for a source type key.
1381 *
1382 * @since 2.8.0
1383 * @param string $type Source type key (channel, playlist, username, search, livestream, video, videos).
1384 * @return string
1385 */
1386 function ayg_get_source_type_label( $type ) {
1387 $types = ayg_get_source_types();
1388 return isset( $types[ $type ] ) ? $types[ $type ] : ucfirst( $type );
1389 }
1390
1391 /**
1392 * Get source types.
1393 *
1394 * @since 2.8.0
1395 * @return array
1396 */
1397 function ayg_get_source_types() {
1398 return array(
1399 'channel' => __( 'Channel', 'automatic-youtube-gallery' ),
1400 'playlist' => __( 'Playlist', 'automatic-youtube-gallery' ),
1401 'username' => __( 'Username', 'automatic-youtube-gallery' ),
1402 'search' => __( 'Search Keywords', 'automatic-youtube-gallery' ),
1403 'livestream' => __( 'Livestream', 'automatic-youtube-gallery' ),
1404 'video' => __( 'Single Video', 'automatic-youtube-gallery' ),
1405 'videos' => __( 'Custom Videos List', 'automatic-youtube-gallery' )
1406 );
1407 }
1408
1409 /**
1410 * Get filtered php template file path.
1411 *
1412 * @since 1.0.0
1413 * @param array $template PHP file path.
1414 * @param string $theme Automatic YouTube Gallery Theme.
1415 * @return string Filtered file path.
1416 */
1417 function ayg_get_template( $template, $theme = '' ) {
1418 return apply_filters( 'ayg_load_template', $template, $theme );
1419 }
1420
1421 /**
1422 * Get unique ID.
1423 *
1424 * @since 1.0.0
1425 * @return string Unique ID.
1426 */
1427 function ayg_get_uniqid() {
1428 global $ayg_uniqid;
1429
1430 if ( ! $ayg_uniqid ) {
1431 $ayg_uniqid = 0;
1432 }
1433
1434 return uniqid() . ++$ayg_uniqid;
1435 }
1436
1437 /**
1438 * Get YouTube domain.
1439 *
1440 * @since 2.3.0
1441 * @return string YouTube embed domain.
1442 */
1443 function ayg_get_youtube_domain() {
1444 $player_settings = ayg_get_option( 'ayg_player_settings' );
1445
1446 $domain = 'https://www.youtube.com';
1447 if ( isset( $player_settings['privacy_enhanced_mode'] ) && ! empty( $player_settings['privacy_enhanced_mode'] ) ) {
1448 $domain = 'https://www.youtube-nocookie.com';
1449 }
1450
1451 return $domain;
1452 }
1453
1454 /**
1455 * Get the YouTube embed URL.
1456 *
1457 * @since 2.5.0
1458 * @param string $video_id YouTube video ID.
1459 * @param array $attributes Array of user attributes.
1460 * @return string Player embed URL.
1461 */
1462 function ayg_get_youtube_embed_url( $video_id, $attributes = array() ) {
1463 $player_settings = ayg_get_option( 'ayg_player_settings' );
1464
1465 $player_website = 'https://www.youtube.com';
1466 if ( isset( $player_settings['privacy_enhanced_mode'] ) && ! empty( $player_settings['privacy_enhanced_mode'] ) ) {
1467 $player_website = 'https://www.youtube-nocookie.com';
1468 }
1469
1470 if ( empty( $video_id ) ) {
1471 return '';
1472 }
1473
1474 $url = $player_website . '/embed/' . $video_id . '?enablejsapi=1&playsinline=1&rel=0';
1475
1476 if ( isset( $player_settings['origin'] ) && ! empty( $player_settings['origin'] ) ) {
1477 $site_url_parts = parse_url( site_url() );
1478 $origin = $site_url_parts['scheme'] . '://' . $site_url_parts['host'];
1479
1480 $url = add_query_arg( 'origin', $origin, $url );
1481 }
1482
1483 if ( ! is_array( $attributes ) ) {
1484 $attributes = (array) $attributes;
1485 }
1486
1487 $autoplay = isset( $attributes['autoplay'] ) ? (int) $attributes['autoplay'] : 0;
1488 if ( 1 == $autoplay ) {
1489 $url = add_query_arg( 'autoplay', 1, $url );
1490 }
1491
1492 $loop = isset( $attributes['loop'] ) ? (int) $attributes['loop'] : 0;
1493 if ( 1 == $loop ) {
1494 $url = add_query_arg( 'playlist', $video_id, $url );
1495 $url = add_query_arg( 'loop', 1, $url );
1496 }
1497
1498 $muted = isset( $attributes['muted'] ) ? (int) $attributes['muted'] : 0;
1499 if ( 1 == $muted ) {
1500 $url = add_query_arg( 'mute', 1, $url );
1501 }
1502
1503 $controls = isset( $attributes['controls'] ) ? (int) $attributes['controls'] : 1;
1504 if ( 0 == $controls ) {
1505 $url = add_query_arg( 'controls', 0, $url );
1506 }
1507
1508 $modestbranding = isset( $attributes['modestbranding'] ) ? (int) $attributes['modestbranding'] : 0;
1509 if ( 1 == $modestbranding ) {
1510 $url = add_query_arg( 'modestbranding', 1, $url );
1511 }
1512
1513 $cc_load_policy = isset( $attributes['cc_load_policy'] ) ? (int) $attributes['cc_load_policy'] : 0;
1514 if ( 1 == $cc_load_policy ) {
1515 $url = add_query_arg( 'cc_load_policy', 1, $url );
1516 }
1517
1518 if ( isset( $attributes['hl'] ) && ! empty( $attributes['hl'] ) ) {
1519 $url = add_query_arg( 'hl', sanitize_text_field( $attributes['hl'] ), $url );
1520 }
1521
1522 if ( isset( $attributes['cc_lang_pref'] ) && ! empty( $attributes['cc_lang_pref'] ) ) {
1523 $url = add_query_arg( 'cc_lang_pref', sanitize_text_field( $attributes['cc_lang_pref'] ), $url );
1524 }
1525
1526 return apply_filters( 'ayg_youtube_embed_url', $url, $video_id, $attributes );
1527 }
1528
1529 /**
1530 * Resolve a YouTube video ID from a bare ID or a YouTube URL.
1531 *
1532 * @since 2.8.0
1533 * @param string $string A video ID or YouTube URL.
1534 * @return string The 11-char video ID, or '' if none found.
1535 */
1536 function ayg_get_youtube_video_id( $string ) {
1537 $string = trim( (string) $string );
1538
1539 if ( '' === $string ) {
1540 return '';
1541 }
1542
1543 // Already a bare video ID.
1544 if ( preg_match( '~^[A-Za-z0-9_-]{11}$~', $string ) ) {
1545 return $string;
1546 }
1547
1548 // Common YouTube URL forms.
1549 if ( preg_match( '~(?:v=|/(?:embed|v|shorts|live)/|youtu\.be/)([A-Za-z0-9_-]{11})~', $string, $matches ) ) {
1550 return $matches[1];
1551 }
1552
1553 return '';
1554 }
1555
1556 /**
1557 * Inserts a new associative array after another associative array key.
1558 *
1559 * @since 2.1.0
1560 * @param string $key The associative array key to insert after.
1561 * @param array $array An array to insert in to.
1562 * @param array $new_array An array to insert.
1563 * @return array Updated array.
1564 */
1565 function ayg_insert_array_after( $key, $array, $new_array ) {
1566 if ( array_key_exists( $key, $array ) ) {
1567 $new = array();
1568
1569 foreach ( $array as $k => $value ) {
1570 $new[ $k ] = $value;
1571
1572 if ( $k === $key ) {
1573 foreach ( $new_array as $new_key => $new_value ) {
1574 $new[ $new_key ] = $new_value;
1575 }
1576 }
1577 }
1578
1579 return $new;
1580 }
1581
1582 return $array;
1583 }
1584
1585 /**
1586 * Detect if the client is using an iOS device (iPhone, iPad, or iPod).
1587 *
1588 * @return bool True if the user agent string suggests an iOS device, false otherwise.
1589 */
1590 function ayg_is_ios() {
1591 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
1592 return false;
1593 }
1594
1595 $ua = strtolower( $_SERVER['HTTP_USER_AGENT'] );
1596
1597 if (
1598 strpos( $ua, 'iphone' ) !== false ||
1599 strpos( $ua, 'ipad' ) !== false ||
1600 strpos( $ua, 'ipod' ) !== false
1601 ) {
1602 return true;
1603 }
1604
1605 return false;
1606 }
1607
1608 /**
1609 * Check whether a video ID matches a gallery's exclude list.
1610 *
1611 * @since 2.8.0
1612 * @param string $video_id YouTube video ID.
1613 * @param array $exclude Exclude entries (video IDs or URLs).
1614 * @return bool True if the video is excluded.
1615 */
1616 function ayg_is_video_excluded( $video_id, $exclude ) {
1617 if ( empty( $video_id ) || empty( $exclude ) || ! is_array( $exclude ) ) {
1618 return false;
1619 }
1620
1621 foreach ( $exclude as $entry ) {
1622 if ( false !== strpos( $entry, $video_id ) ) {
1623 return true;
1624 }
1625 }
1626
1627 return false;
1628 }
1629
1630 /**
1631 * Parse an ISO 8601 duration (e.g. PT4M13S) into total seconds.
1632 *
1633 * @since 2.8.0
1634 * @param string $iso8601 ISO 8601 duration string from the YouTube API.
1635 * @return int Duration in seconds.
1636 */
1637 function ayg_parse_duration_seconds( $iso8601 ) {
1638 if ( empty( $iso8601 ) ) {
1639 return 0;
1640 }
1641
1642 try {
1643 $interval = new DateInterval( $iso8601 );
1644 } catch ( Exception $e ) {
1645 return 0;
1646 }
1647
1648 return ( $interval->d * DAY_IN_SECONDS ) + ( $interval->h * HOUR_IN_SECONDS ) + ( $interval->i * MINUTE_IN_SECONDS ) + $interval->s;
1649 }
1650
1651 /**
1652 * Sanitize the array inputs.
1653 *
1654 * @since 2.7.0
1655 * @param array $value Input array.
1656 * @return array Sanitized array.
1657 */
1658 function ayg_sanitize_array( $value ) {
1659 return ! empty( $value ) ? array_map( 'sanitize_text_field', $value ) : array();
1660 }
1661
1662 /**
1663 * Sanitize the integer inputs, accepts empty values.
1664 *
1665 * @since 1.0.0
1666 * @param string|int $value Input value.
1667 * @return string|int Sanitized value.
1668 */
1669 function ayg_sanitize_int( $value ) {
1670 $value = intval( $value );
1671 return ( 0 == $value ) ? '' : $value;
1672 }
1673
1674 /**
1675 * Trims text to a certain number of characters.
1676 *
1677 * @since 2.0.0
1678 * @param string $text Text to trim.
1679 * @param int $num_characters Number of characters.
1680 * @param string $append String to append to the end of the excerpt.
1681 * @return string Trimmed text.
1682 */
1683 function ayg_trim_words( $text, $num_characters, $append = '...' ) {
1684 $num_characters++;
1685
1686 $original_text = $text;
1687 $text = ( $num_characters > 1 ) ? wp_strip_all_tags( $original_text, true ) : nl2br( $original_text );
1688
1689 if ( $num_characters > 1 && mb_strlen( $text ) > $num_characters ) {
1690 $subex = mb_substr( $text, 0, $num_characters - 5 );
1691 $exwords = explode( ' ', $subex );
1692 $excut = - ( mb_strlen( $exwords[ count( $exwords ) - 1 ] ) );
1693
1694 if ( $excut < 0 ) {
1695 $text = mb_substr( $subex, 0, $excut );
1696 } else {
1697 $text = $subex;
1698 }
1699
1700 $text .= $append;
1701 }
1702
1703 return apply_filters( 'ayg_trim_words', $text, $original_text, $num_characters, $append );
1704 }
1705
1706 /**
1707 * Gallery HTML output.
1708 *
1709 * @since 1.0.0
1710 * @param array $video YouTube video object.
1711 * @param array $attributes Array of user attributes.
1712 */
1713 function the_ayg_gallery_thumbnail( $video, $attributes ) {
1714 include ayg_get_template( AYG_DIR . 'public/templates/thumbnail.php' );
1715 }
1716
1717 /**
1718 * Pagination HTML output.
1719 *
1720 * @since 1.0.0
1721 * @param array $attributes Array of user attributes.
1722 */
1723 function the_ayg_pagination( $attributes ) {
1724 if ( ! empty( $attributes['pagination'] ) ) {
1725 include ayg_get_template( AYG_DIR . 'public/templates/pagination.php' );
1726 }
1727 }
1728
1729 /**
1730 * Search Form HTML output.
1731 *
1732 * @since 2.5.7
1733 * @param array $attributes Array of user attributes.
1734 */
1735 function the_ayg_search_form( $attributes ) {
1736 if ( ! empty( $attributes['search_form'] ) ) {
1737 include ayg_get_template( AYG_DIR . 'public/templates/search-form.php' );
1738 }
1739 }
1740
1741 /**
1742 * Gallery HTML output.
1743 *
1744 * @since 2.5.0
1745 * @param array $video YouTube video object.
1746 * @param array $attributes Array of user attributes.
1747 */
1748 function the_ayg_player( $video, $attributes ) {
1749 include ayg_get_template( AYG_DIR . 'public/templates/player.php' );
1750 }