# automatic-youtube-gallery/2.9.1/includes/functions.php

Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists &amp; Channels, version 2.9.1. 2,016 lines.

- Page: https://pluginprobe.com/plugins/automatic-youtube-gallery/2.9.1/code/includes/functions.php
- Raw: https://pluginprobe.com/plugins/automatic-youtube-gallery/2.9.1/raw/includes/functions.php
- Modified: 2026-08-10T06:33:08+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/automatic-youtube-gallery/2.9.1/code/includes/functions.php#L10-L20`.

```php
<?php

/**
 * Helper Functions.
 *
 * @link    https://plugins360.com
 * @since   1.0.0
 *
 * @package Automatic_YouTube_Gallery
 */

// Exit if accessed directly
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Build gallery HTML output.
 *
 * @since  1.0.0
 * @param  array $args An array of gallery options.
 * @return mixed
 */
function ayg_build_gallery( $args ) {
	$general_settings = ayg_get_option( 'ayg_general_settings' );
	$strings_settings = ayg_get_option( 'ayg_strings_settings' );

	global $post;

	// Vars
	$fields   = ayg_get_editor_fields();
	$excluded = array( 'popup' ); // Fields not part of attributes

	// Defaults not backed by an editor field (kept through shortcode_atts): internal "db" source + sort.
	$defaults = array(
		'db'              => '',
		'sort_by'         => 'date',
		'sort_order'      => 'desc',
		'sort_seed'       => '',
		'duration_filter' => '',
		'duration'        => 0
	);

	foreach ( $fields as $key => $value ) {
		foreach ( $value['fields'] as $field ) {
			if ( in_array( $field['name'], $excluded ) ) {
				continue;
			}

			$defaults[ $field['name'] ] = $field['value'];
		}
	}

	$defaults = array_merge( $defaults, (array) $strings_settings );

	// Gallery Builder mode: a numeric "id" loads the saved gallery config, layers its saved
	// settings under $args (shortcode attributes still win, except the source), and switches
	// the source to the internal "db" type so the existing build pipeline serves it from the DB.
	if ( isset( $args['id'] ) && is_numeric( $args['id'] ) && (int) $args['id'] > 0 ) {
		$config = ayg_get_gallery( (int) $args['id'] );

		if ( ! $config ) {
			return sprintf( '<div class="ayg ayg-error">%s</div>', esc_html__( 'Gallery not found.', 'automatic-youtube-gallery' ) );
		}

		$config_params = json_decode( (string) $config->params, true );
		if ( ! is_array( $config_params ) ) {
			$config_params = array();
		}

		// Saved settings act as defaults; explicit shortcode attributes override them.
		$args = array_merge( $config_params, $args );

		// The source always comes from the config and can't be overridden by the shortcode.
		// Livestream, search and single video query the live API at display time (nothing is
		// imported/stored); everything else renders from the DB. ayg_get_gallery_source() owns that
		// mapping so the public AJAX endpoint resolves the same source from the same saved row.
		$source = ayg_get_gallery_source( $config );

		$args['type']             = $source['type'];
		$args[ $source['field'] ] = $source['src'];

		// Single video + popup mode → force the popup theme, mirroring the block and widget (which
		// apply this before calling ayg_build_gallery()).
		if ( 'video' === $config->source_type && ! empty( $args['popup'] ) ) {
			$args['theme'] = 'popup';
		}

		// Gallery Builder relationships are keyed by the config ID as a string.
		$args['uid'] = strval( $config->id );
	}

	$attributes = shortcode_atts( $defaults, $args, 'automatic_youtube_gallery' );

	$attributes['post_id'] = 0;
	if ( isset( $post->ID ) ) {
		$attributes['post_id'] = (int) $post->ID;
	}

	$attributes['columns'] = min( 12, (int) $attributes['columns'] );
	if ( empty( $attributes['columns'] ) ) {
		$attributes['columns'] = 3;
	}

	$attributes['limit'] = min( 500, (int) $attributes['limit'] );
	if ( empty( $attributes['limit'] ) ) {
		$attributes['limit'] = 500;
	}
	
	$source_type = sanitize_text_field( $attributes['type'] );

	// Videos per page. Database (Gallery Builder) galleries read from our own table, so 0 means
	// "show all videos on a single page" (no pagination). The live-API sources (search, livestream)
	// stay capped at 50 because that is YouTube's maximum number of results per request.
	if ( 'db' === $source_type ) {
		$attributes['per_page'] = max( 0, (int) $attributes['per_page'] );
	} else {
		$attributes['per_page'] = min( 50, (int) $attributes['per_page'] );
		if ( empty( $attributes['per_page'] ) ) {
			$attributes['per_page'] = 50;
		}
	}

	if ( 'livestream' == $source_type ) {
		$attributes['livestream'] = $attributes['channel'];
	}

	$attributes['lazyload'] = 0;
	if ( ! empty( $general_settings['lazyload'] ) ) {
		$attributes['lazyload'] = 1;
	}

	$source_url = sanitize_text_field( $attributes[ $source_type ] );	

	if ( isset( $args['id'] ) && ! empty( $args['id'] ) ) {
		$attributes['id'] = absint( $args['id'] );
	}

	if ( isset( $args['uid'] ) && ! empty( $args['uid'] ) ) {
		$attributes['uid'] = sanitize_text_field( $args['uid'] );
	} else {
		$attributes['uid'] = md5( $source_type . $source_url );
	}

	$attributes['uid'] = apply_filters( 'ayg_gallery_id', $attributes['uid'], $args );

	// Deprecated since v2.5.8. Retained for backward compatibility.
	$deprecated_uid = md5( $source_type . $source_url . sanitize_text_field( $attributes['theme'] ) ); // Deprecated
	if ( isset( $args['deprecated_uid'] ) && ! empty( $args['deprecated_uid'] ) ) {
		$deprecated_uid = sanitize_text_field( $args['deprecated_uid'] );
	}

	// Deeplinked video (premium): resolve before the query so DB-served galleries can pin it as
	// the first item at the SQL level, keeping the per-page count exact. API-served (legacy)
	// galleries keep the old prepend behavior below.
	$gallery_id_from_url = get_query_var( 'ayg_gallery_id' );
	$video_id_from_url   = get_query_var( 'ayg_video_id' );
	$deeplinked_video    = false;

	if ( ! empty( $video_id_from_url ) && ( $attributes['uid'] == $gallery_id_from_url || $deprecated_uid == $gallery_id_from_url ) ) {
		$deeplinked_video = ayg_db_get_video( $video_id_from_url );
	}

	if ( ! $deeplinked_video ) {
		$video_id_from_url = '';
	}

	$featured_video_id = ( 'db' === $source_type && ! empty( $video_id_from_url ) ) ? sanitize_text_field( $video_id_from_url ) : '';

	// Random ordering: generate a per-render seed (once) so RAND(seed) stays stable across this
	// gallery's own pagination/search AJAX calls. A seed hardcoded in the shortcode is respected.
	if ( 'random' === $attributes['sort_by'] && empty( $attributes['sort_seed'] ) ) {
		$attributes['sort_seed'] = wp_rand( 1, 2147483647 );
	}
	
	// Get Videos
	$api_params = array(
		'uid'               => $attributes['uid'],
		'type'              => $source_type,
		'src'               => $source_url,
		'store'             => true,                                           // Trusted context: the source comes from the rendered shortcode, not from a visitor's request.
		'featured_video_id' => $featured_video_id,                             // Works only when type = "db".
		'order'             => sanitize_text_field( $attributes['order'] ),    // Works only when type = "search".
		'sort_by'           => sanitize_key( $attributes['sort_by'] ),         // Works only when type = "db".
		'sort_order'        => sanitize_key( $attributes['sort_order'] ),      // Works only when type = "db".
		'sort_seed'         => absint( $attributes['sort_seed'] ),             // Works only when type = "db" + sort_by = "random".
		'duration_filter'   => sanitize_key( $attributes['duration_filter'] ), // Works only when type = "db".
		'duration'          => absint( $attributes['duration'] ),              // Works only when type = "db".
		'limit'             => $attributes['limit'],                           // Works only when type = "search".
		'maxResults'        => $attributes['per_page'],
		'cache'             => (int) $attributes['cache']
	);

	$api_params = apply_filters( 'ayg_youtube_api_request_params', $api_params, $args );

	$youtube_api = new AYG_YouTube_API();
	$response = $youtube_api->query( $api_params );

	// Process output
	if ( ! isset( $response->error ) ) {
		// Store Gallery ID
		if ( $attributes['post_id'] > 0 && isset( $attributes['deeplinking'] ) && 1 == $attributes['deeplinking'] ) {
			$pages   = ayg_get_option( 'ayg_gallery_page_ids' );
			$page_id = $attributes['post_id'];

			if ( ! in_array( $page_id, $pages ) ) {
				$pages[] = $page_id;
				update_option( 'ayg_gallery_page_ids', $pages );
			}		
		}
		
		// Gallery
		$videos = array();

		if ( ! empty( $video_id_from_url ) ) {
			// DB-served galleries pin the deeplinked video via the SQL query itself, so the result
			// set already starts with it. Fall back to prepending it only when the pin couldn't
			// apply: API-served (legacy) sources, or the video is no longer part of this gallery's
			// result set (removed from the gallery, or excluded by a duration filter).
			$is_pinned_by_sql = ! empty( $api_params['featured_video_id'] )	&& ! empty( $response->videos )	&& $response->videos[0]->id == $video_id_from_url;

			if ( ! $is_pinned_by_sql ) {
				$videos[] = $deeplinked_video;
			}
		}

		if ( isset( $response->videos ) ) {
			if ( ! empty( $videos ) ) {
				foreach ( $response->videos as $video ) {
					if ( $video->id != $video_id_from_url ) {
						$videos[] = $video;
					}
				}
			} else {
				$videos = $response->videos;
			}
		}

		// Pagination
		if ( isset( $response->page_info ) ) {
			$page_info = $response->page_info;

			// Sign the page tokens on their way out to the browser so the public AJAX endpoint can
			// tell the tokens this site issued from ones a visitor made up.
			if ( ayg_page_token_is_signed( $source_type ) ) {
				$page_info = ayg_sign_page_tokens( $page_info, $attributes['uid'] );
			}

			$attributes = array_merge( $attributes, $api_params, $page_info );
		}

		// Theme
		$theme = 'classic';

		if ( 'video' == $source_type ) {
			$theme = 'single';
		} elseif ( 'livestream' == $source_type ) {
			$theme = 'livestream';
		} else {
			if ( 1 == count( $videos ) ) {
				$theme = 'single';
			}
		}

		// Enqueue dependencies
		wp_enqueue_style( AYG_SLUG . '-public' );

		wp_enqueue_script( AYG_SLUG . '-public' );		
		if ( $attributes['theme'] == $theme && 'classic' == $attributes['theme'] ) {
			wp_enqueue_script( AYG_SLUG . '-theme-classic' );
		}

		// Output
		ob_start();
		include ayg_get_template( AYG_DIR . "public/templates/theme-{$theme}.php", $attributes['theme'] );
		return ob_get_clean();
	} else {
		return sprintf(	'<div class="ayg ayg-error">%s</div>', wp_kses_post( $response->error_message )	);
	}
}

/**
 * Combine video attributes as a string.
 * 
 * @since 2.5.0
 * @param array  $atts Array of video attributes.
 * @param string       Combined attributes string.
 */
function ayg_combine_video_attributes( $atts ) {
	$attributes = array();
	
	foreach ( $atts as $key => $value ) {
		if ( '' === $value ) {
			$attributes[] = $key;
		} else {
			$attributes[] = sprintf( '%s="%s"', $key, $value );
		}
	}
	
	return implode( ' ', $attributes );
}

/**
 * Create custom database tables
 *
 * @since 2.1.0
 */
function ayg_db_create_custom_tables() {
	global $wpdb;

	require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
	$charset_collate = $wpdb->get_charset_collate();

	$videos_table = $wpdb->prefix . 'ayg_videos';
	$rel_table    = $wpdb->prefix . 'ayg_gallery_relationships';
	$old_table    = $wpdb->prefix . 'ayg_galleries';

	// Rename wp_ayg_galleries (old pivot) → wp_ayg_gallery_relationships.
	// Guard: only when the old table exists AND the new name does not yet exist AND
	// the old table is the pivot (identified by having a video_id column).
	$old_exists    = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old_table ) ) ) === $old_table;
	$new_not_exist = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $rel_table ) ) ) !== $rel_table;

	if ( $old_exists && $new_not_exist ) {
		$is_old_pivot = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$old_table` LIKE 'video_id'" ) );

		if ( $is_old_pivot ) {
			$renamed = $wpdb->query( "RENAME TABLE `$old_table` TO `$rel_table`" );

			if ( false === $renamed ) {
				// Rename failed (insufficient DB privilege on some hosts).
				// This table holds auto-generated cache data, not user content — safe to drop.
				$wpdb->query( "DROP TABLE IF EXISTS `$old_table`" );
			}
		}
	}

	// Rename columns in wp_ayg_videos (NUM → id, id → video_id).
	// Each ALTER is guarded so the block is idempotent on re-runs.
	$videos_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $videos_table ) ) ) === $videos_table;

	if ( $videos_table_exists ) {
		// Rename 'id' → 'video_id' first. Must happen before NUM → id to avoid a duplicate
		// column name error: if NUM were renamed to 'id' while the varchar 'id' still exists,
		// MySQL would reject it with "Duplicate column name 'id'".
		$old_id_col = $wpdb->get_row( "SHOW COLUMNS FROM `$videos_table` LIKE 'id'" );

		if ( $old_id_col && false !== strpos( strtolower( $old_id_col->Type ), 'varchar' ) ) {
			$wpdb->query( "ALTER TABLE `$videos_table` CHANGE `id` `video_id` varchar(100) NOT NULL" );
		}

		// Now safe to rename NUM → id (no column named 'id' exists at this point).
		$has_num = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$videos_table` LIKE 'NUM'" ) );

		if ( $has_num ) {
			$wpdb->query( "ALTER TABLE `$videos_table` CHANGE `NUM` `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT" );
		}

		// Remove duplicate rows using the new column names (idempotent).
		$wpdb->query(
			"DELETE v1 FROM $videos_table v1
			INNER JOIN $videos_table v2
			ON v1.video_id = v2.video_id AND v1.id > v2.id"
		);
	}

	// Create/update wp_ayg_videos — dbDelta adds the 2 new columns on existing installs.
	$sql = "CREATE TABLE $videos_table (
		id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
		video_id varchar(100) NOT NULL,
		title text NOT NULL,
		description text NOT NULL,
		thumbnails text NOT NULL,
		duration varchar(25) NOT NULL,
		duration_seconds int(11) UNSIGNED NOT NULL DEFAULT 0,
		video_type varchar(20) NOT NULL DEFAULT 'none',
		status varchar(100) NOT NULL,
		published_at varchar(100) NOT NULL,
		published_at_datetime datetime NULL,
		PRIMARY KEY  (id),
		UNIQUE KEY ayg_unique_video_id (video_id),
		INDEX ayg_idx_published_at_datetime (published_at_datetime)
	) $charset_collate;";

	dbDelta( $sql );

	// Fallback: add UNIQUE KEY if dbDelta did not create it (edge case on existing installs).
	$existing_keys = $wpdb->get_results( "SHOW KEYS FROM $videos_table WHERE Key_name = 'ayg_unique_video_id'" );
	
	if ( empty( $existing_keys ) ) {
		$wpdb->query( "ALTER TABLE $videos_table ADD UNIQUE KEY ayg_unique_video_id (video_id)" );
	}

	// Fallback: add INDEX on published_at_datetime if missing (existing installs).
	$existing_dt_index = $wpdb->get_results( "SHOW KEYS FROM $videos_table WHERE Key_name = 'ayg_idx_published_at_datetime'" );
	
	if ( empty( $existing_dt_index ) ) {
		$wpdb->query( "ALTER TABLE $videos_table ADD INDEX ayg_idx_published_at_datetime (published_at_datetime)" );
	}

	// Rename NUM → id in wp_ayg_gallery_relationships (only if rename succeeded above).
	// If the table was recreated fresh by dbDelta below, it already has 'id' — no ALTER needed.
	$rel_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $rel_table ) ) ) === $rel_table;

	if ( $rel_table_exists ) {
		$has_num_in_rel = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$rel_table` LIKE 'NUM'" ) );
		
		if ( $has_num_in_rel ) {
			$wpdb->query( "ALTER TABLE `$rel_table` CHANGE `NUM` `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT" );
		}
	}

	// Create/update wp_ayg_gallery_relationships (fresh creation if the rename fallback dropped the old table).
	$sql = "CREATE TABLE $rel_table (
		id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
		video_id varchar(100) NOT NULL,
		gallery_id varchar(100) NOT NULL,
		synced_at datetime NULL,
		PRIMARY KEY  (id),
		UNIQUE KEY ayg_unique_video_gallery (video_id, gallery_id),
		INDEX ayg_idx_gallery_id (gallery_id)
	) $charset_collate;";

	dbDelta( $sql );

	// Fallback: add the synced_at column if dbDelta did not (existing installs).
	$has_synced_at = ! empty( $wpdb->get_results( "SHOW COLUMNS FROM `$rel_table` LIKE 'synced_at'" ) );

	if ( ! $has_synced_at ) {
		$wpdb->query( "ALTER TABLE `$rel_table` ADD `synced_at` datetime NULL" );
	}

	// Create new wp_ayg_galleries entity table (one row per saved gallery config).
	$galleries_table = $wpdb->prefix . 'ayg_galleries';

	$sql = "CREATE TABLE $galleries_table (
		id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
		title text NOT NULL,
		source_type varchar(50) NOT NULL DEFAULT 'channel',
		source_value text NOT NULL,
		params longtext NOT NULL,
		import_status varchar(20) NOT NULL DEFAULT 'idle',
		import_error text NULL,
		import_log longtext NULL,
		last_imported_at datetime NULL,
		next_import_at datetime NULL,
		video_count int(11) UNSIGNED NOT NULL DEFAULT 0,
		created_at datetime NOT NULL,
		updated_at datetime NOT NULL,
		PRIMARY KEY  (id),
		INDEX idx_import (next_import_at, import_status)
	) $charset_collate;";

	dbDelta( $sql );
}

/**
 * Check whether any videos are linked to the given gallery UID.
 *
 * Lets the public AJAX endpoint tell a gallery this site actually displays from a UID a visitor
 * simply made up: a gallery that has been rendered even once always has its videos in the
 * relationship table, an invented UID never does.
 *
 * @since  2.9.0
 * @param  string $gallery_id Gallery UID.
 * @return bool               True when at least one video is linked to the UID.
 */
function ayg_db_gallery_has_videos( $gallery_id ) {
	global $wpdb;

	$gallery_id = (string) $gallery_id;

	if ( '' === $gallery_id ) {
		return false;
	}

	$found = $wpdb->get_var(
		$wpdb->prepare(
			"SELECT 1 FROM {$wpdb->prefix}ayg_gallery_relationships WHERE gallery_id = %s LIMIT 1",
			$gallery_id
		)
	);

	return ! empty( $found );
}

/**
 * Get a single video record from our custom database table "{$wpdb->prefix}ayg_videos"
 *
 * @since  2.1.0
 * @param  string $video_id YouTube Video ID.
 * @return mixed
 */
function ayg_db_get_video( $video_id ) {
	global $wpdb;

	$cache_key = 'ayg_'. $video_id;
	$row = wp_cache_get( $cache_key );

	if ( false === $row ) {
		$query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ayg_videos WHERE video_id = %s", $video_id );
		$row = $wpdb->get_row( $query );

		if ( $row ) {
			if ( ! empty( $row->thumbnails ) ) {
				$row->thumbnails = ayg_maybe_unserialize( $row->thumbnails );
			}

			// Backward compat: templates reference $video->id as the YouTube video ID.
			$row->id = $row->video_id;

			wp_cache_set( $cache_key, $row );
		}
	}

	return $row;
}

/**
 * Store videos in our custom database table "{$wpdb->prefix}ayg_videos" 
 *
 * @since 2.1.0
 * @param object $data       YouTube API response object.
 * @param array  $attributes Array of user attributes.
 */
function ayg_db_store_videos( $data, $attributes = array() ) {
	if ( AYG_VERSION !== get_option( 'ayg_version' ) ) {
		return false;
	}

	if ( isset( $data->kind ) && 'youtube#channelListResponse' == $data->kind ) {
		return false;				
	}

	if ( empty( $data->items ) || ! is_array( $data->items ) ) {
		return false;
	}

	global $wpdb;

	$items                = $data->items;
	$gallery_id           = isset( $attributes['uid'] ) ? $attributes['uid'] : '';
	$source_type          = isset( $attributes['type'] ) ? $attributes['type'] : 'videos';
	$store_gallery        = ! empty( $gallery_id ) && 'livestream' !== $source_type;
	$exclude              = ( isset( $attributes['exclude'] ) && is_array( $attributes['exclude'] ) ) ? $attributes['exclude'] : array();

	// Only videos.list responses carry contentDetails.duration and snippet.liveBroadcastContent.
	// For other responses (playlistItems.list, search.list) the duration_seconds / video_type
	// columns are left out entirely so existing values are never clobbered with defaults.
	$has_video_details    = isset( $data->kind ) && 'youtube#videoListResponse' === $data->kind;

	$videos_table         = $wpdb->prefix . 'ayg_videos';
	$galleries_table      = $wpdb->prefix . 'ayg_gallery_relationships';

	$video_placeholders   = array();
	$video_values         = array();
	$gallery_placeholders = array();
	$gallery_values       = array();

	foreach ( $items as $item ) {
		$row = array();

		// Video ID
		$row['video_id'] = '';

		if ( isset( $item->snippet->resourceId ) && isset( $item->snippet->resourceId->videoId ) ) {
			$row['video_id'] = $item->snippet->resourceId->videoId;
		} elseif ( isset( $item->contentDetails ) && isset( $item->contentDetails->videoId ) ) {
			$row['video_id'] = $item->contentDetails->videoId;
		} elseif ( isset( $item->id ) && isset( $item->id->videoId ) ) {
			$row['video_id'] = $item->id->videoId;
		} elseif ( isset( $item->id ) ) {
			$row['video_id'] = $item->id;
		}

		if ( empty( $row['video_id'] ) ) {
			continue;
		}

		// Skip videos on the gallery's exclude list (kept out of both tables)
		if ( ayg_is_video_excluded( $row['video_id'], $exclude ) ) {
			continue;
		}

		// Video title
		$row['title'] = $item->snippet->title;

		// Video description
		$row['description'] = $item->snippet->description;

		// Video thumbnails
		$row['thumbnails'] = '';
		if ( isset( $item->snippet->thumbnails ) ) {
			$row['thumbnails'] = serialize( $item->snippet->thumbnails );
		}

		// Video duration
		$row['duration'] = '';
		if ( isset( $item->contentDetails ) && isset( $item->contentDetails->duration ) ) {
			$row['duration'] = $item->contentDetails->duration;
		}

		if ( $has_video_details ) {
			$row['duration_seconds'] = ayg_parse_duration_seconds( $row['duration'] );

			// Raw liveBroadcastContent value ('live', 'upcoming', 'none'); shorts are not
			// classified at import time — filter by duration_seconds <= 60 at query time.
			$row['video_type'] = 'none';
			if ( ! empty( $item->snippet->liveBroadcastContent ) ) {
				$row['video_type'] = $item->snippet->liveBroadcastContent;
			}
		}

		// Video status
		$row['status'] = 'private';

		if ( isset( $item->status ) && ( 'public' == $item->status->privacyStatus || 'unlisted' == $item->status->privacyStatus ) ) {
			$row['status'] = 'public';
		}

		if ( isset( $item->snippet->status ) && ( 'public' == $item->snippet->status->privacyStatus || 'unlisted' == $item->snippet->status->privacyStatus ) ) {
			$row['status'] = 'public';
		}

		if ( 'youtube#searchResult' == $item->kind ) {
			$row['status'] = 'public';
		}

		// Video publish date
		$row['published_at'] = $item->snippet->publishedAt;

		$datetime = new DateTime( $item->snippet->publishedAt );
		$row['published_at_datetime'] = date_format( $datetime, 'Y-m-d H:i:s' );

		// Collect for bulk insert
		if ( $has_video_details ) {
			$video_placeholders[] = '(%s, %s, %s, %s, %s, %d, %s, %s, %s, %s)';

			array_push(
				$video_values,
				$row['video_id'],
				$row['title'],
				$row['description'],
				$row['thumbnails'],
				$row['duration'],
				$row['duration_seconds'],
				$row['video_type'],
				$row['status'],
				$row['published_at'],
				$row['published_at_datetime']
			);
		} else {
			$video_placeholders[] = '(%s, %s, %s, %s, %s, %s, %s, %s)';

			array_push(
				$video_values,
				$row['video_id'],
				$row['title'],
				$row['description'],
				$row['thumbnails'],
				$row['duration'],
				$row['status'],
				$row['published_at'],
				$row['published_at_datetime']
			);
		}

		if ( $store_gallery ) {
			$gallery_placeholders[] = '(%s, %s)';
			array_push( $gallery_values, $row['video_id'], $gallery_id );
		}
	}

	// Bulk insert videos (2 queries total instead of N×2)
	if ( ! empty( $video_placeholders ) ) {
		if ( $has_video_details ) {
			$sql = "INSERT INTO $videos_table (video_id, title, description, thumbnails, duration, duration_seconds, video_type, status, published_at, published_at_datetime)
				VALUES " . implode( ', ', $video_placeholders ) . "
				ON DUPLICATE KEY UPDATE
				title = VALUES(title),
				description = VALUES(description),
				thumbnails = VALUES(thumbnails),
				duration = VALUES(duration),
				duration_seconds = VALUES(duration_seconds),
				video_type = VALUES(video_type),
				status = VALUES(status),
				published_at = VALUES(published_at),
				published_at_datetime = VALUES(published_at_datetime)";
		} else {
			$sql = "INSERT INTO $videos_table (video_id, title, description, thumbnails, duration, status, published_at, published_at_datetime)
				VALUES " . implode( ', ', $video_placeholders ) . "
				ON DUPLICATE KEY UPDATE
				title = VALUES(title),
				description = VALUES(description),
				thumbnails = VALUES(thumbnails),
				duration = VALUES(duration),
				status = VALUES(status),
				published_at = VALUES(published_at),
				published_at_datetime = VALUES(published_at_datetime)";
		}

		$wpdb->query( $wpdb->prepare( $sql, $video_values ) );
	}

	// Bulk insert gallery relationships (2 queries total instead of N×2)
	if ( ! empty( $gallery_placeholders ) ) {
		$sql = "INSERT IGNORE INTO $galleries_table (video_id, gallery_id) VALUES " . implode( ', ', $gallery_placeholders );
		$wpdb->query( $wpdb->prepare( $sql, $gallery_values ) );
	}
}

/**
 * Dump our plugin transients.
 *
 * @since 2.1.0
 * @param string $uid Gallery uid to target. Empty string flushes all plugin transients.
 */
function ayg_delete_cache( $uid = '' ) {
	$uid  = (string) $uid;
	$keys = (array) ayg_get_option( 'ayg_transient_keys' );

	// Targeted: delete only the keys prefixed with this gallery's uid, keep the rest in the registry.
	if ( '' !== $uid ) {
		$prefix    = 'ayg_' . $uid . '_';
		$remaining = array();

		foreach ( $keys as $key ) {
			if ( 0 === strpos( $key, $prefix ) ) {
				delete_transient( $key );
			} else {
				$remaining[] = $key;
			}
		}

		update_option( 'ayg_transient_keys', $remaining, false );
		return;
	}

	// Full flush: every transient, plus the cached gallery page-id lookup.
	delete_option( 'ayg_gallery_page_ids' );

	foreach ( $keys as $key ) {
		delete_transient( $key );
	}

	// Reset our DB value (autoload=no: not needed on every page load)
	update_option( 'ayg_transient_keys', array(), false );
}

/**
 * Remove a gallery's excluded videos from the relationship table.
 *
 * @since  2.8.0
 * @param  int   $gallery_id Gallery ID.
 * @param  array $exclude    Exclude entries (video IDs or URLs).
 * @return int               Number of relationships removed.
 */
function ayg_delete_excluded_relationships( $gallery_id, $exclude ) {
	global $wpdb;

	$gallery_id = absint( $gallery_id );

	if ( $gallery_id <= 0 || empty( $exclude ) || ! is_array( $exclude ) ) {
		return 0;
	}

	// Resolve the exclude entries (IDs or URLs) to video IDs.
	$video_ids = array();

	foreach ( $exclude as $entry ) {
		$video_id = ayg_get_youtube_video_id( $entry );

		if ( '' !== $video_id ) {
			$video_ids[] = $video_id;
		}
	}

	$video_ids = array_values( array_unique( $video_ids ) );

	if ( empty( $video_ids ) ) {
		return 0;
	}

	$rel_table    = $wpdb->prefix . 'ayg_gallery_relationships';
	$placeholders = implode( ',', array_fill( 0, count( $video_ids ), '%s' ) );

	return (int) $wpdb->query(
		$wpdb->prepare(
			"DELETE FROM $rel_table WHERE gallery_id = %s AND video_id IN ( $placeholders )",
			array_merge( array( strval( $gallery_id ) ), $video_ids )
		)
	);
}

/** 
 * Get current address bar URL.
 *
 * @since  2.1.0
 * @return string Current Page URL.
 */
function ayg_get_current_url() {
    global $wp;
	$current_url = home_url( add_query_arg( array(), $wp->request ) );
	
    return $current_url;	
}

/**
 * Get default plugin settings.
 *
 * @since  1.6.4
 * @return array $defaults Array of plugin settings.
 */
function ayg_get_default_settings() {
	$defaults = array(
		'ayg_general_settings' => array(
			'force_load_assets' => array(
				'css' => 'css'
			),
			'api_key'           => '',
			'lazyload'          => 0,
			'development_mode'  => 0
		),
		'ayg_strings_settings' => array(
			'more_button_label'     => __( 'Load More', 'automatic-youtube-gallery' ),
			'previous_button_label' => __( 'Previous', 'automatic-youtube-gallery' ),
			'next_button_label'     => __( 'Next', 'automatic-youtube-gallery' ),
			'show_more_label'       => __( 'Show More', 'automatic-youtube-gallery' ),
			'show_less_label'       => __( 'Show Less', 'automatic-youtube-gallery' )
		),
		'ayg_gallery_settings' => array(
			'theme'                 => 'classic',
			'columns'               => 3,
			'per_page'              => 12,
			'thumb_ratio'           => 56.25,
			'thumb_title'           => 1,
			'thumb_title_length'    => 0,
			'thumb_excerpt'         => 1,
			'thumb_excerpt_length'  => 75,
			'pagination'            => 1,
			'pagination_type'       => 'more',
			'scroll_top_offset'     => 10
		),
		'ayg_player_settings' => array(
			'player_type'           => 'youtube',
			'player_color'          => '#00b3ff',
			'player_width'          => '',
			'player_ratio'          => 56.25,
			'player_title'          => 1,
			'player_description'    => 1,
			'autoplay'              => 0,
			'autoadvance'           => 1,
			'loop'                  => 0,
			'muted'                 => 0,
			'controls'              => 1,
			'modestbranding'        => 1,
			'cc_load_policy'        => 0,
			'hl'                    => '',
			'cc_lang_pref'          => '',
			'privacy_enhanced_mode' => 0,
			'origin'                => 0
		),
		'ayg_livestream_settings' => array(
			'fallback_message' => __( 'Sorry, but the channel is not currently streaming live content. Please check back later.', 'automatic-youtube-gallery' )
		),
		'ayg_privacy_settings' => array(			
			'cookie_consent'  => 0,
			'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' ),
			'button_label'    => __( 'Accept', 'automatic-youtube-gallery' )
		)
	);

	return $defaults;
}

/**
 * Get editor fields.
 *
 * @since  1.0.0
 * @return array Array of fields.
 */
function ayg_get_editor_fields() {
	$fields = array(
		'source' => array(
			'label'  => __( 'Source Configuration', 'automatic-youtube-gallery' ),
			'fields' => array(
				array(
					'name'              => 'type',
					'label'             => __( 'Source Type', 'automatic-youtube-gallery' ),
					'description'       => '',
					'type'              => 'select',
					'options'           => ayg_get_source_types(),
					'value'             => 'playlist',
					'sanitize_callback' => 'sanitize_key'
				),
				array(
					'name'              => 'playlist',
					'label'             => __( 'YouTube Playlist ID (or) URL', 'automatic-youtube-gallery' ),					
					'description'       => sprintf( '%s: https://www.youtube.com/playlist?list=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
					'type'              => 'url',
					'required'          => 1,
					'value'             => '',
					'sanitize_callback' => 'sanitize_text_field'
				),
				array(
					'name'              => 'channel',
					'label'             => __( 'YouTube Channel ID (or) a Video URL from the Channel', 'automatic-youtube-gallery' ),
					'description'       => sprintf( '%s: https://www.youtube.com/channel/XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
					'type'              => 'url',
					'required'          => 1,
					'value'             => '',
					'sanitize_callback' => 'sanitize_text_field'
				),
				array(
					'name'              => 'username',
					'label'             => __( 'YouTube Account Username', 'automatic-youtube-gallery' ),
					'description'       => sprintf( '%s: SanRosh', __( 'Example', 'automatic-youtube-gallery' ) ),
					'type'              => 'text',
					'required'          => 1,
					'value'             => '',
					'sanitize_callback' => 'sanitize_text_field'
				),
				array(
					'name'              => 'search',
					'label'             => __( 'Search Keywords', 'automatic-youtube-gallery' ),
					'description'       => sprintf( '%s: Cartoon (space:AND , -:NOT , |:OR)', __( 'Example', 'automatic-youtube-gallery' ) ),
					'type'              => 'text',
					'required'          => 1,
					'value'             => '',
					'sanitize_callback' => 'sanitize_text_field'
				),
				array(
					'name'              => 'video',
					'label'             => __( 'YouTube Video ID (or) URL', 'automatic-youtube-gallery' ),
					'description'       => sprintf( '%s: https://www.youtube.com/watch?v=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
					'type'              => 'url',
					'required'          => 1,
					'value'             => '',
					'sanitize_callback' => 'sanitize_text_field'
				),				
				array(
					'name'              => 'videos',
					'label'             => __( 'YouTube Video IDs (or) URLs', 'automatic-youtube-gallery' ),
					'description'       => sprintf( '%s: https://www.youtube.com/watch?v=XXXXXXXXXX', __( 'Example', 'automatic-youtube-gallery' ) ),
					'type'              => 'textarea',					
					'required'          => 1,
					'placeholder'       => __( 'Enter one video per line', 'automatic-youtube-gallery' ),
					'value'             => '',
					'sanitize_callback' => 'sanitize_textarea_field'
				),				
				array(
					'name'              => 'order',
					'label'             => __( 'Search Order', 'automatic-youtube-gallery' ),
					'description'       => '',
					'type'              => 'select',
					'options' => array(
						'date'      => __( 'Date', 'automatic-youtube-gallery' ),
						'rating'    => __( 'Rating', 'automatic-youtube-gallery' ),
						'relevance' => __( 'Relevance', 'automatic-youtube-gallery' ),
						'title'     => __( 'Title', 'automatic-youtube-gallery' ),
						'viewCount' => __( 'View Count', 'automatic-youtube-gallery' )
					),
					'value'             => 'relevance',
					'sanitize_callback' => 'sanitize_text_field'
				),
				array(
					'name'              => 'limit',
					'label'             => __( 'Search Limit', 'automatic-youtube-gallery' ),
					'description'       => __( 'Enter the number of videos to display in this gallery. Set to 0 for the maximum amount (500).', 'automatic-youtube-gallery' ),
					'type'              => 'number',					
					'min'               => 0,
					'max'               => 500,
					'value'             => 0,
					'sanitize_callback' => 'intval'
				),
				array(
					'name'              => 'cache',
					'label'             => __( 'Cache Duration', 'automatic-youtube-gallery' ),
					'description'       => __( 'Specifies how frequently we should check your YouTube source for new videos/updates.', 'automatic-youtube-gallery' ),
					'type'              => 'select',
					'options' => array(
						'0'       => '— '. __( 'No Caching', 'automatic-youtube-gallery' ) . ' —',
						'900'     => __( '15 Minutes', 'automatic-youtube-gallery' ),
						'1800'    => __( '30 Minutes', 'automatic-youtube-gallery' ),
						'3600'    => __( '1 Hour', 'automatic-youtube-gallery' ),
						'86400'   => __( '1 Day', 'automatic-youtube-gallery' ),
						'604800'  => __( '1 Week', 'automatic-youtube-gallery' ),
						'2419200' => __( '1 Month', 'automatic-youtube-gallery' )
					),
					'value'             => 86400,
					'sanitize_callback' => 'intval'
				)
			)			
		),
		'gallery' => array(
			'label'  => __( 'Gallery Options', 'automatic-youtube-gallery' ),
			'fields' => ayg_get_gallery_settings_fields()
		),
		'player' => array(
			'label'  => __( 'Player Options', 'automatic-youtube-gallery' ),
			'fields' => ayg_get_player_settings_fields()
		),
		'search' => array(
			'label'  => __( 'Search Form', 'automatic-youtube-gallery' ),
			'fields' => array(
				array(
					'name'              => 'search_form',
					'label'             => __( 'Search Form', 'automatic-youtube-gallery' ),			
					'description'       => sprintf(
						__( 'Check this option to enable the search form. Having issues? <a href="%s" target="_blank" rel="noopener noreferrer">Check here</a>.', 'automatic-youtube-gallery' ),
						'https://plugins360.com/automatic-youtube-gallery/searchform/'
					),
					'type'              => 'checkbox',
					'value'             => 0,
					'sanitize_callback' => 'intval'
				)
			)
		)
	);

	return apply_filters( 'ayg_editor_fields', $fields );
}

/**
 * Fetch a single gallery row by ID.
 *
 * @since  2.8.0
 * @param  int         $gallery_id Gallery ID.
 * @return object|null
 */
function ayg_get_gallery( $gallery_id ) {
	global $wpdb;

	return $wpdb->get_row(
		$wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}ayg_galleries` WHERE id = %d", absint( $gallery_id ) )
	);
}

/**
 * Get gallery settings fields.
 *
 * @since  1.0.0
 * @return array $fields Array of fields.
 */
function ayg_get_gallery_settings_fields() {
	$gallery_settings = ayg_get_option( 'ayg_gallery_settings' );

	$fields = array(
		array(
			'name'              => 'theme',
			'label'             => __( 'Select Theme (Layout)', 'automatic-youtube-gallery' ),
			'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() ) ) : '' ),
			'type'              => 'select',
			'options'           => array( 
				'classic' => __( 'Classic', 'automatic-youtube-gallery' )
			),
			'value'             => $gallery_settings['theme'],
			'sanitize_callback' => 'sanitize_key'
		),		
		array(
			'name'              => 'columns',
			'label'             => __( 'Columns', 'automatic-youtube-gallery' ),
			'description'       => __( 'Enter the number of columns you like to have in the gallery. Maximum of 12.', 'automatic-youtube-gallery' ),			
			'type'              => 'number',
			'min'               => 0,
			'max'               => 12,
			'value'             => $gallery_settings['columns'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'per_page',
			'label'             => __( 'Videos per Page', 'automatic-youtube-gallery' ),
			'description'       => __( 'Enter the number of videos to show per page. Maximum of 50.', 'automatic-youtube-gallery' ),
			'type'              => 'number',
			'min'               => 0,
			'max'               => 50,
			'value'             => $gallery_settings['per_page'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'thumb_ratio',
			'label'             => __( 'Image Height (Ratio)', 'automatic-youtube-gallery' ),
			'description'       => __( 'Select the ratio value used to calculate the image height in the gallery thumbnails.', 'automatic-youtube-gallery' ),			
			'type'              => 'select',
			'options'           => array(
				'56.25'  => __( 'Standard (16:9) — Default', 'automatic-youtube-gallery' ),
				'177.78' => __( 'Shorts / Vertical (9:16)', 'automatic-youtube-gallery' ),
				'75'     => __( 'Classic (4:3)', 'automatic-youtube-gallery' )				
			),
			'value'             => $gallery_settings['thumb_ratio'],
			'sanitize_callback' => 'floatval'
		),
		array(
			'name'              => 'thumb_title',
			'label'             => __( 'Show Video Title', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to show the video title in each gallery item.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $gallery_settings['thumb_title'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'thumb_title_length',
			'label'             => __( 'Video Title Length', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Enter the number of characters you like to show in the title. Set 0 to show the whole title.', 'automatic-youtube-gallery' ),
			'type'              => 'number',
			'min'               => 0,
			'max'               => 500,
			'value'             => $gallery_settings['thumb_title_length'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'thumb_excerpt',
			'label'             => __( 'Show Video Excerpt (Short Description)', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to show the short description of a video in each gallery item.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $gallery_settings['thumb_excerpt'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'thumb_excerpt_length',
			'label'             => __( 'Video Excerpt Length', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Enter the number of characters you like to have in the video excerpt. Set 0 to show the whole description.', 'automatic-youtube-gallery' ),
			'type'              => 'number',
			'min'               => 0,
			'max'               => 500,
			'value'             => $gallery_settings['thumb_excerpt_length'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'pagination',
			'label'             => __( 'Pagination', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to show the pagination.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $gallery_settings['pagination'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'pagination_type',
			'label'             => __( 'Pagination Type', 'automatic-youtube-gallery' ),			
			'type'              => 'select',
			'options'           => array(
				'more'  => __( 'More Button', 'automatic-youtube-gallery' ),
				'pager' => __( 'Pager', 'automatic-youtube-gallery' )
						
			),
			'value'             => $gallery_settings['pagination_type'],
			'sanitize_callback' => 'sanitize_key'
		)
	);

	return apply_filters( 'ayg_gallery_settings_fields', $fields );
}

/**
 * Build the signature that ties a gallery UID to the source and cache duration it was rendered
 * with.
 *
 * A gallery UID is printed into the page, so it is not a secret and cannot be trusted on its own.
 * What this protects is the *pairing*: a request may only ask for the source that was rendered
 * alongside that UID, at the cache duration that gallery was configured with.
 *
 * It exists because a UID cannot always be recomputed. ayg_build_gallery() derives it as
 * md5( type + source ), but a gallery can override that through the "uid" shortcode attribute or
 * the ayg_gallery_id filter, and neither is knowable from the request alone. Signing the whole set
 * at render time lets the public AJAX endpoint accept such galleries without also having to accept
 * whatever source — or cache duration — the caller claims goes with them.
 *
 * Including the cache duration is what lets the "Cache Duration" gallery option keep working
 * exactly as the site owner set it, right down to "No Caching", while still making it impossible
 * for a visitor to bypass the cache and force live API requests.
 *
 * Each part is length prefixed so that no two different sets of values can produce the same
 * string to sign.
 *
 * @since  2.9.0
 * @param  string     $uid   Gallery UID.
 * @param  string     $type  Gallery source type.
 * @param  string     $src   Gallery source value.
 * @param  string|int $cache Gallery cache duration in seconds.
 * @return string            Signature.
 */
function ayg_get_gallery_signature( $uid, $type, $src, $cache ) {
	$parts = array( (string) $uid, (string) $type, (string) $src, (string) (int) $cache );
	$data  = 'ayg_gallery';

	foreach ( $parts as $part ) {
		$data .= '|' . strlen( $part ) . ':' . $part;
	}

	return substr( wp_hash( $data ), 0, 16 );
}

/**
 * Resolve a saved gallery's source type and value.
 *
 * Livestream, search and single video galleries query the YouTube API at display time — nothing
 * is imported for them — so they keep their own source type. Everything else is served from the
 * custom tables through the internal "db" source type, keyed by the gallery ID.
 *
 * Both the renderer (ayg_build_gallery) and the public AJAX endpoint resolve a saved gallery's
 * source through this function, so an untrusted request can never point a gallery at a source
 * other than the one stored in its own row.
 *
 * @since  2.9.0
 * @param  object $gallery Gallery row from "{$wpdb->prefix}ayg_galleries".
 * @return array           Source "type", the matching attribute name in "field", and the
 *                         source value in "src".
 */
function ayg_get_gallery_source( $gallery ) {
	$source_type  = isset( $gallery->source_type ) ? $gallery->source_type : '';
	$source_value = isset( $gallery->source_value ) ? $gallery->source_value : '';

	if ( 'livestream' === $source_type ) {
		return array(
			'type'  => 'livestream',
			'field' => 'channel',
			'src'   => $source_value
		);
	}

	if ( 'search' === $source_type ) {
		return array(
			'type'  => 'search',
			'field' => 'search',
			'src'   => $source_value
		);
	}

	if ( 'video' === $source_type ) {
		return array(
			'type'  => 'video',
			'field' => 'video',
			'src'   => $source_value
		);
	}

	return array(
		'type'  => 'db',
		'field' => 'db',
		'src'   => strval( $gallery->id )
	);
}

/**
 * Return the schedule interval options for import scheduling.
 *
 * @since  2.8.0
 * @return array Value (seconds as string) => label pairs.
 */
function ayg_get_import_schedule_options() {
	return array(
		'paused'  => __( '— Pause Automatic Imports —', 'automatic-youtube-gallery' ),
		'0'       => __( 'Only Once', 'automatic-youtube-gallery' ),
		'3600'    => __( 'Every Hour', 'automatic-youtube-gallery' ),
		'86400'   => __( 'Every Day', 'automatic-youtube-gallery' ),
		'604800'  => __( 'Every Week', 'automatic-youtube-gallery' ),
		'2592000' => __( 'Every Month', 'automatic-youtube-gallery' )
	);
}

/**
 * Return the human-readable label for an import status key.
 *
 * @since  2.8.0
 * @param  string   $status      Import status key (idle, running, paused, error, completed).
 * @param  int|null $video_count Optional. Imported video count, used to resolve the
 *                               ambiguous "idle" status into "Pending Import" (nothing
 *                               imported yet) or "Up to Date" (waiting for the next sync).
 * @param  bool     $resuming    Optional. True when an import was capped mid-run and parked
 *                               as 'idle' for cron to resume — shown as "Importing", not idle.
 * @param  string   $context     Optional. 'log' for a per-run history entry, where a successful
 *                               run (stored as 'idle' for recurring galleries) reads "Completed"
 *                               like non-recurring runs, instead of the live-state "Idle".
 * @return string
 */
function ayg_get_import_status_label( $status, $video_count = null, $resuming = false, $context = '' ) {
	if ( 'idle' === $status ) {
		// History rows record a finished run: a recurring gallery's success is stored as 'idle',
		// so surface it as "Completed" to match non-recurring runs rather than the live "Idle".
		if ( 'log' === $context ) {
			return __( 'Completed', 'automatic-youtube-gallery' );
		}

		// If the import was capped mid-run and parked as 'idle' for cron to resume, show "Importing".
		if ( $resuming ) {
			return __( 'Importing', 'automatic-youtube-gallery' );
		}

		// If the import has never run, show "Pending Import" instead of "Idle".
		if ( null !== $video_count ) {
			return ( (int) $video_count > 0 ) ? __( 'Up to Date', 'automatic-youtube-gallery' ) : __( 'Pending Import', 'automatic-youtube-gallery' );
		}
	}

	$statuses = array(
		'idle'      => __( 'Idle', 'automatic-youtube-gallery' ),
		'running'   => __( 'Importing', 'automatic-youtube-gallery' ),
		'paused'    => __( 'Paused', 'automatic-youtube-gallery' ),
		'error'     => __( 'Error', 'automatic-youtube-gallery' ),
		'completed' => __( 'Completed', 'automatic-youtube-gallery' )
	);

	return isset( $statuses[ $status ] ) ? $statuses[ $status ] : ucfirst( $status );
}

/**
 * Retrieve a plugin option with fallback to default settings.
 *
 * @since  2.7.0
 * @param  string $option The option name to retrieve.
 * @return mixed
 */
function ayg_get_option( $option ) {
    $defaults = ayg_get_default_settings();
    $default  = isset( $defaults[ $option ] ) ? $defaults[ $option ] : array();

    $saved = get_option( $option, null );

    // Option does not exist OR corrupted
    if ( null === $saved || ! is_array( $saved ) ) {
        return $default;
    }

    // Merge saved values with defaults
    return wp_parse_args( $saved, $default );
}

/**
 * Build the signature for a pagination token.
 *
 * Keyed on the site's own salts through wp_hash(), and bound to the gallery so a token issued for
 * one gallery cannot be replayed against another. Unlike a nonce this never expires, so tokens
 * printed into a page that is held by a full page cache keep working.
 *
 * @since  2.9.0
 * @param  string $token Raw page token.
 * @param  string $uid   Gallery UID the token belongs to.
 * @return string        Signature.
 */
function ayg_get_page_token_signature( $token, $uid ) {
	return substr( wp_hash( 'ayg_page_token|' . (string) $uid . '|' . (string) $token ), 0, 16 );
}

/**
 * Get video description to show on top of the player.
 *
 * @since  1.0.0
 * @param  stdClass $video       YouTube video object.
 * @param  array    $attributes  Array of user attributes.
 * @param  int      $words_count Number of words to show by default.
 * @return string                Video description.
 */
function ayg_get_player_description( $video, $attributes = array(), $words_count = 30 ) {
	$description = $video->description;

	$words_array = explode( ' ', strip_tags( $description ) );	
	if ( count( $words_array ) > $words_count ) {
		$show_more_label = ! empty( $attributes['show_more_label'] ) ? $attributes['show_more_label'] : __( 'Show More', 'automatic-youtube-gallery' );
		$words_array[ $words_count ] = '<span class="ayg-player-description-dots">...</span></span><span class="ayg-player-description-more">' . $words_array[ $words_count ];

		$description  = '<span class="ayg-player-description-less">' . implode( ' ', $words_array ) . '</span>';
		$description .= '<a href="#" class="ayg-player-description-toggle-btn">' . esc_html( $show_more_label ) . '</a>';
	}

	$description = nl2br( $description );
	$description = make_clickable( $description );

	return apply_filters( 'ayg_player_description', $description, $video, $attributes, $words_count );	
}

/**
 * Get player settings fields.
 *
 * @since  1.0.0
 * @return array $fields Array of fields.
 */
function ayg_get_player_settings_fields() {
	$player_settings = ayg_get_option( 'ayg_player_settings' );

	$fields = array(
		array(
			'name'              => 'player_width',
			'label'             => __( 'Player Width', 'automatic-youtube-gallery' ),
			'description'       => __( 'In pixels. Maximum width of the player. Leave this field empty to scale 100% of its enclosing container/html element.', 'automatic-youtube-gallery' ),
			'type'              => 'text',
			'value'             => isset( $player_settings['player_width'] ) ? $player_settings['player_width'] : '',
			'sanitize_callback' => 'ayg_sanitize_int'
		),
		array(
			'name'              => 'player_ratio',
			'label'             => __( 'Player Height (Ratio)', 'automatic-youtube-gallery' ),	
			'description'       => __( 'Select the ratio value used to calculate the player height.', 'automatic-youtube-gallery' ),		
			'type'              => 'select',
			'options'           => array(
				'56.25'  => __( 'Standard (16:9) — Default', 'automatic-youtube-gallery' ),
				'177.78' => __( 'Shorts / Vertical (9:16)', 'automatic-youtube-gallery' ),
				'75'     => __( 'Classic (4:3)', 'automatic-youtube-gallery' )
			),
			'value'             => $player_settings['player_ratio'],
			'sanitize_callback' => 'floatval'
		),
		array(
			'name'              => 'player_title',
			'label'             => __( 'Show Video Title', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to show the current playing video title on the bottom of the player.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['player_title'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'player_description',
			'label'             => __( 'Show Video Description', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to show the current playing video description on the bottom of the player.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['player_description'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'autoplay',
			'label'             => __( 'Autoplay', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to automatically start playing the initial video when the player loads.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['autoplay'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'autoadvance',
			'label'             => __( 'Autoplay Next Video', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to automatically play the next video in the list after the previous one ends.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['autoadvance'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'loop',
			'label'             => __( 'Loop', 'automatic-youtube-gallery' ),			
			'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' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['loop'],
			'sanitize_callback' => 'intval'
		),	
		array(
			'name'              => 'muted',
			'label'             => __( 'Muted', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Check this option to turn OFF the audio output of the video by default.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => isset( $player_settings['muted'] ) ? $player_settings['muted'] : 0,
			'sanitize_callback' => 'intval'
		),	
		array(
			'name'              => 'controls',
			'label'             => __( 'Show Player Controls', 'automatic-youtube-gallery' ),			
			'description'       => __( 'Uncheck this option to hide the video player controls.', 'automatic-youtube-gallery' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['controls'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'modestbranding',
			'label'             => __( 'Hide YouTube Logo', 'automatic-youtube-gallery' ),			
			'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' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['modestbranding'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'cc_load_policy',
			'label'             => __( 'Force Closed Captions', 'automatic-youtube-gallery' ),			
			'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' ),
			'type'              => 'checkbox',
			'value'             => $player_settings['cc_load_policy'],
			'sanitize_callback' => 'intval'
		),
		array(
			'name'              => 'hl',
			'label'             => __( 'Player Language', 'automatic-youtube-gallery' ),			
			'description'       => sprintf( 
				__( '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' ),
				'http://www.loc.gov/standards/iso639-2/php/code_list.php'
			),
			'type'              => 'text',
			'value'             => $player_settings['hl'],
			'sanitize_callback' => 'sanitize_text_field'
		),
		array(
			'name'              => 'cc_lang_pref',
			'label'             => __( 'Default Captions Language', 'automatic-youtube-gallery' ),			
			'description'       => sprintf( 
				__( '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' ),
				'http://www.loc.gov/standards/iso639-2/php/code_list.php'
			),
			'type'              => 'text',
			'value'             => $player_settings['cc_lang_pref'],
			'sanitize_callback' => 'sanitize_text_field'
		)
	);

	return $fields;
}


/**
 * Get a single video page URL.
 *
 * @since  2.1.0
 * @param  stdClass $video YouTube video object.
 * @param  array    $args  An array of gallery options.
 * @return string          Single video URL.
 */
function ayg_get_single_video_url( $video, $attributes ) {
	return apply_filters( 'ayg_single_video_url', '', $video, $attributes );
}

/**
 * Return the human-readable label for a source type key.
 *
 * @since  2.8.0
 * @param  string $type Source type key (channel, playlist, username, search, livestream, video, videos).
 * @return string
 */
function ayg_get_source_type_label( $type ) {
	$types = ayg_get_source_types();
	return isset( $types[ $type ] ) ? $types[ $type ] : ucfirst( $type );
}

/**
 * Get source types.
 *
 * @since  2.8.0
 * @return array
 */
function ayg_get_source_types() {
	return array(
		'channel'    => __( 'Channel', 'automatic-youtube-gallery' ),
		'playlist'   => __( 'Playlist', 'automatic-youtube-gallery' ),
		'username'   => __( 'Username', 'automatic-youtube-gallery' ),
		'search'     => __( 'Search Keywords', 'automatic-youtube-gallery' ),
		'livestream' => __( 'Livestream', 'automatic-youtube-gallery' ),
		'video'      => __( 'Single Video', 'automatic-youtube-gallery' ),						
		'videos'     => __( 'Custom Videos List', 'automatic-youtube-gallery' )
	);
}

/**
 * Get filtered php template file path.
 *
 * @since  1.0.0
 * @param  array  $template PHP file path.
 * @param  string $theme    Automatic YouTube Gallery Theme.
 * @return string           Filtered file path.
 */
function ayg_get_template( $template, $theme = '' ) {
	return apply_filters( 'ayg_load_template', $template, $theme );
}

/**
 * Get unique ID.
 *
 * @since  1.0.0
 * @return string Unique ID.
 */
function ayg_get_uniqid() {
	global $ayg_uniqid;

	if ( ! $ayg_uniqid ) {
		$ayg_uniqid = 0;
	}

	return uniqid() . ++$ayg_uniqid;
}

/**
 * Get YouTube domain.
 *
 * @since  2.3.0
 * @return string YouTube embed domain.
 */
function ayg_get_youtube_domain() {
	$player_settings = ayg_get_option( 'ayg_player_settings' );

	$domain = 'https://www.youtube.com';
	if ( isset( $player_settings['privacy_enhanced_mode'] ) && ! empty( $player_settings['privacy_enhanced_mode'] ) ) {
		$domain = 'https://www.youtube-nocookie.com';
	}

	return $domain;
}

/**
 * Get the YouTube embed URL.
 *
 * @since  2.5.0
 * @param  string $video_id   YouTube video ID.
 * @param  array  $attributes Array of user attributes.
 * @return string             Player embed URL.
 */
function ayg_get_youtube_embed_url( $video_id, $attributes = array() ) {
	$player_settings = ayg_get_option( 'ayg_player_settings' );

	$player_website = 'https://www.youtube.com';
	if ( isset( $player_settings['privacy_enhanced_mode'] ) && ! empty( $player_settings['privacy_enhanced_mode'] ) ) {
		$player_website = 'https://www.youtube-nocookie.com';
	}

	if ( empty( $video_id ) ) {
		return '';
	}

	$url = $player_website . '/embed/' . $video_id . '?enablejsapi=1&playsinline=1&rel=0';

	if ( isset( $player_settings['origin'] ) && ! empty( $player_settings['origin'] ) ) {
		$site_url_parts = parse_url( site_url() );
		$origin = $site_url_parts['scheme'] . '://' . $site_url_parts['host'];

		$url = add_query_arg( 'origin', $origin, $url );
	}

	if ( ! is_array( $attributes ) ) {
		$attributes = (array) $attributes;
	}

	$autoplay = isset( $attributes['autoplay'] ) ? (int) $attributes['autoplay'] : 0;
	if ( 1 == $autoplay ) {
		$url = add_query_arg( 'autoplay', 1, $url );
	}

	$loop = isset( $attributes['loop'] ) ? (int) $attributes['loop'] : 0;
	if ( 1 == $loop ) {
		$url = add_query_arg( 'playlist', $video_id, $url );
		$url = add_query_arg( 'loop', 1, $url );
	}

	$muted = isset( $attributes['muted'] ) ? (int) $attributes['muted'] : 0;
	if ( 1 == $muted ) {
		$url = add_query_arg( 'mute', 1, $url );
	}

	$controls = isset( $attributes['controls'] ) ? (int) $attributes['controls'] : 1;
	if ( 0 == $controls ) {
		$url = add_query_arg( 'controls', 0, $url );
	}

	$modestbranding = isset( $attributes['modestbranding'] ) ? (int) $attributes['modestbranding'] : 0;
	if ( 1 == $modestbranding ) {
		$url = add_query_arg( 'modestbranding', 1, $url );
	}

	$cc_load_policy = isset( $attributes['cc_load_policy'] ) ? (int) $attributes['cc_load_policy'] : 0;
	if ( 1 == $cc_load_policy ) {
		$url = add_query_arg( 'cc_load_policy', 1, $url );
	}

	if ( isset( $attributes['hl'] ) && ! empty( $attributes['hl'] ) ) {
		$url = add_query_arg( 'hl', sanitize_text_field( $attributes['hl'] ), $url );
	}

	if ( isset( $attributes['cc_lang_pref'] ) && ! empty( $attributes['cc_lang_pref'] ) ) {
		$url = add_query_arg( 'cc_lang_pref', sanitize_text_field( $attributes['cc_lang_pref'] ), $url );
	}

	return apply_filters( 'ayg_youtube_embed_url', $url, $video_id, $attributes );
}

/**
 * Resolve a YouTube video ID from a bare ID or a YouTube URL.
 *
 * @since  2.8.0
 * @param  string $string A video ID or YouTube URL.
 * @return string         The 11-char video ID, or '' if none found.
 */
function ayg_get_youtube_video_id( $string ) {
	$string = trim( (string) $string );

	if ( '' === $string ) {
		return '';
	}

	// Already a bare video ID.
	if ( preg_match( '~^[A-Za-z0-9_-]{11}$~', $string ) ) {
		return $string;
	}

	// Common YouTube URL forms.
	if ( preg_match( '~(?:v=|/(?:embed|v|shorts|live)/|youtu\.be/)([A-Za-z0-9_-]{11})~', $string, $matches ) ) {
		return $matches[1];
	}

	return '';
}

/**
 * Inserts a new associative array after another associative array key.
 *
 * @since  2.1.0
 * @param  string $key       The associative array key to insert after.
 * @param  array  $array     An array to insert in to.
 * @param  array  $new_array An array to insert.
 * @return array             Updated array.
 */
function ayg_insert_array_after( $key, $array, $new_array ) {
	if ( array_key_exists( $key, $array ) ) {
    	$new = array();

    	foreach ( $array as $k => $value ) {
      		$new[ $k ] = $value;

      		if ( $k === $key ) {
				foreach ( $new_array as $new_key => $new_value ) {
        			$new[ $new_key ] = $new_value;
				}
      		}
    	}
		
    	return $new;
  	}
		
  	return $array;  
}

/**
 * Detect if the client is using an iOS device (iPhone, iPad, or iPod).
 *
 * @return bool True if the user agent string suggests an iOS device, false otherwise.
 */
function ayg_is_ios() {
    if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
        return false;
    }

    $ua = strtolower( $_SERVER['HTTP_USER_AGENT'] );

    if ( 
		strpos( $ua, 'iphone' ) !== false ||
        strpos( $ua, 'ipad' ) !== false ||
        strpos( $ua, 'ipod' ) !== false 
	) {
        return true;
    }

    return false;
}

/**
 * Check whether a video ID matches a gallery's exclude list.
 *
 * @since  2.8.0
 * @param  string $video_id YouTube video ID.
 * @param  array  $exclude  Exclude entries (video IDs or URLs).
 * @return bool             True if the video is excluded.
 */
function ayg_is_video_excluded( $video_id, $exclude ) {
	if ( empty( $video_id ) || empty( $exclude ) || ! is_array( $exclude ) ) {
		return false;
	}

	foreach ( $exclude as $entry ) {
		if ( false !== strpos( $entry, $video_id ) ) {
			return true;
		}
	}

	return false;
}

/**
 * Unserialize a stored value without letting it instantiate arbitrary classes.
 *
 * Our serialized columns only ever hold the thumbnail set from an API response — plain data plus
 * stdClass objects — so nothing else is allowed through. This keeps a row that was tampered with
 * or carried over from another install from building an object of some other class.
 *
 * @since  2.9.0
 * @param  string $value Serialized value.
 * @return mixed         Unserialized value, or the value unchanged when it isn't serialized.
 */
function ayg_maybe_unserialize( $value ) {
	if ( ! is_serialized( $value ) ) {
		return $value;
	}

	// is_serialized() trims the value before testing it, so trim here as well — otherwise a padded
	// value passes the test and then fails to unserialize. Mirrors core's maybe_unserialize().
	$value = trim( $value );

	// PHP 7.0+ can restrict this natively.
	if ( version_compare( PHP_VERSION, '7.0', '>=' ) ) {
		return @unserialize( $value, array( 'allowed_classes' => array( 'stdClass' ) ) );
	}

	// PHP 5.6 has no "allowed_classes" option, so screen the payload instead: "O:" introduces a
	// plain object and "C:" a Serializable one, so refuse the value outright if it names any class
	// other than stdClass. Same protection as the call above, without raising the minimum PHP
	// version and cutting those sites off from plugin updates.
	if ( preg_match_all( '/(?:^|[;{])([OC]):\d+:"([^"]*)"/', $value, $matches, PREG_SET_ORDER ) ) {
		foreach ( $matches as $match ) {
			if ( 'O' !== $match[1] || 'stdClass' !== $match[2] ) {
				return false;
			}
		}
	}

	return @unserialize( $value );
}

/**
 * Does this source type hand out signed pagination tokens?
 *
 * Only the sources that page through the live YouTube API do, because for those every distinct
 * page token means another API call. The rest either never reach the API at all (the Gallery
 * Builder "db" source) or page through a list already known to the site ("videos"), where the
 * page number is simply clamped to the real range and so cannot force extra calls.
 *
 * @since  2.9.0
 * @param  string $source_type Gallery source type.
 * @return bool                True when tokens of this source type are signed.
 */
function ayg_page_token_is_signed( $source_type ) {
	return in_array( $source_type, array( 'playlist', 'channel', 'username', 'search' ), true );
}

/**
 * Parse an ISO 8601 duration (e.g. PT4M13S) into total seconds.
 *
 * @since  2.8.0
 * @param  string $iso8601 ISO 8601 duration string from the YouTube API.
 * @return int             Duration in seconds.
 */
function ayg_parse_duration_seconds( $iso8601 ) {
	if ( empty( $iso8601 ) ) {
		return 0;
	}

	try {
		$interval = new DateInterval( $iso8601 );
	} catch ( Exception $e ) {
		return 0;
	}

	return ( $interval->d * DAY_IN_SECONDS ) + ( $interval->h * HOUR_IN_SECONDS ) + ( $interval->i * MINUTE_IN_SECONDS ) + $interval->s;
}

/**
 * Sanitize the array inputs.
 *
 * @since  2.7.0
 * @param  array $value Input array.
 * @return array        Sanitized array.
 */
function ayg_sanitize_array( $value ) {
	return ! empty( $value ) ? array_map( 'sanitize_text_field', $value ) : array();
}

/**
 * Sanitize the integer inputs, accepts empty values.
 *
 * @since  1.0.0
 * @param  string|int $value Input value.
 * @return string|int        Sanitized value.
 */
function ayg_sanitize_int( $value ) {
	$value = intval( $value );
	return ( 0 == $value ) ? '' : $value;	
}

/**
 * Sign a pagination token before it is handed to the browser.
 *
 * Page tokens travel out to the visitor and are posted back to the public AJAX endpoint, where
 * each distinct token means another live call to the YouTube Data API. Signing them means only
 * the tokens this site actually issued are ever acted on, so a visitor cannot invent an endless
 * stream of new ones and burn through the site's daily API quota.
 *
 * @since  2.9.0
 * @param  string $token Raw page token from the API response.
 * @param  string $uid   Gallery UID the token belongs to.
 * @return string        Signed token, or an empty string when there is no token.
 */
function ayg_sign_page_token( $token, $uid ) {
	$token = (string) $token;

	if ( '' === $token ) {
		return '';
	}

	return $token . '.' . ayg_get_page_token_signature( $token, $uid );
}

/**
 * Sign the pagination tokens in a page info array.
 *
 * @since  2.9.0
 * @param  array  $page_info Page info array from the API response.
 * @param  string $uid       Gallery UID the tokens belong to.
 * @return array             The same array with its tokens signed.
 */
function ayg_sign_page_tokens( $page_info, $uid ) {
	foreach ( array( 'next_page_token', 'prev_page_token' ) as $key ) {
		if ( ! empty( $page_info[ $key ] ) ) {
			$page_info[ $key ] = ayg_sign_page_token( $page_info[ $key ], $uid );
		}
	}

	return $page_info;
}

/**
 * Trims text to a certain number of characters.
 *
 * @since  2.0.0
 * @param  string $text           Text to trim.
 * @param  int    $num_characters Number of characters.
 * @param  string $append         String to append to the end of the excerpt.
 * @return string                 Trimmed text.
 */
function ayg_trim_words( $text, $num_characters, $append = '...' ) {
	$num_characters++;

	$original_text = $text;
	$text = ( $num_characters > 1 ) ? wp_strip_all_tags( $original_text, true ) : nl2br( $original_text );

	if ( $num_characters > 1 && mb_strlen( $text ) > $num_characters ) {
		$subex = mb_substr( $text, 0, $num_characters - 5 );
		$exwords = explode( ' ', $subex );
		$excut = - ( mb_strlen( $exwords[ count( $exwords ) - 1 ] ) );

		if ( $excut < 0 ) {
			$text = mb_substr( $subex, 0, $excut );
		} else {
			$text = $subex;
		}

		$text .= $append;
	}

	return apply_filters( 'ayg_trim_words', $text, $original_text, $num_characters, $append );
}

/**
 * Verify a pagination token that came back from the browser.
 *
 * @since  2.9.0
 * @param  string $token Signed token as posted by the browser.
 * @param  string $uid   Gallery UID the token is claimed to belong to.
 * @return string|false  The raw token when the signature matches, false when it does not. An
 *                       empty token is the first page and needs no signature.
 */
function ayg_verify_page_token( $token, $uid ) {
	$token = (string) $token;

	if ( '' === $token ) {
		return '';
	}

	// The signature is appended last, and a YouTube page token can itself contain almost anything,
	// so split on the final separator rather than the first.
	$position = strrpos( $token, '.' );

	if ( false === $position ) {
		return false;
	}

	$raw       = substr( $token, 0, $position );
	$signature = substr( $token, $position + 1 );

	if ( '' === $raw || ! hash_equals( ayg_get_page_token_signature( $raw, $uid ), $signature ) ) {
		return false;
	}

	return $raw;
}

/**
 * Gallery HTML output.
 *
 * @since 1.0.0
 * @param array $video      YouTube video object.
 * @param array $attributes Array of user attributes.
 */
function the_ayg_gallery_thumbnail( $video, $attributes ) {
	include ayg_get_template( AYG_DIR . 'public/templates/thumbnail.php' );
}

/**
 * Pagination HTML output.
 *
 * @since 1.0.0
 * @param array $attributes Array of user attributes.
 */
function the_ayg_pagination( $attributes ) {
	if ( ! empty( $attributes['pagination'] ) ) {
		include ayg_get_template( AYG_DIR . 'public/templates/pagination.php' );
	}	
}

/**
 * Search Form HTML output.
 *
 * @since 2.5.7
 * @param array $attributes Array of user attributes.
 */
function the_ayg_search_form( $attributes ) {
	if ( ! empty( $attributes['search_form'] ) ) {
		include ayg_get_template( AYG_DIR . 'public/templates/search-form.php' );
	}	
}

/**
 * Gallery HTML output.
 *
 * @since 2.5.0
 * @param array $video      YouTube video object.
 * @param array $attributes Array of user attributes.
 */
function the_ayg_player( $video, $attributes ) {
	include ayg_get_template( AYG_DIR . 'public/templates/player.php' );
}
```
