PluginProbe
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels / trunk
Automatic YouTube Gallery – Embed Auto-Updating YouTube Video Galleries, Feeds, Playlists & Channels vtrunk
2.9.1 2.9.0 trunk 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 2.0.0 2.1.0 2.2.0 2.3.2 2.3.3 2.3.5 2.3.6 2.3.8 2.3.9 2.4.3 All 37 releases
← All changes | admin/admin.php +467 -52 2.2.0trunk View file →
@@ -21,8 +21,17 @@
21 21 */
22 22 class AYG_Admin {
23 23
24 24 /**
25 + * Gallery list table instance, shared between bulk-action processing
26 + * and rendering, to avoid instantiating it twice per request.
27 + *
28 + * @since 2.8.0
29 + * @var AYG_Admin_Gallery_List_Table
30 + */
31 + private $gallery_list_table;
32 +
33 + /**
25 34 * Insert missing plugin options.
26 35 *
27 36 * @since 1.6.4
28 37 */
@@ -29,8 +38,44 @@
29 38 public function insert_missing_options() {
30 39 if ( AYG_VERSION !== get_option( 'ayg_version' ) ) {
31 40 $defaults = ayg_get_default_settings();
32 41
42 + // Insert the gallery settings
43 + $gallery_settings = get_option( 'ayg_gallery_settings' );
44 +
45 + if ( ! is_array( $gallery_settings ) || empty( $gallery_settings ) ) {
46 + $gallery_settings = $defaults['ayg_gallery_settings'];
47 + update_option( 'ayg_gallery_settings', $gallery_settings );
48 + }
49 +
50 + // Insert the strings settings
51 + if ( false == get_option( 'ayg_strings_settings' ) ) {
52 + $strings_settings = array(
53 + 'more_button_label' => ! empty( $gallery_settings['more_button_label'] ) ? $gallery_settings['more_button_label'] : $defaults['ayg_strings_settings']['more_button_label'],
54 + 'previous_button_label' => ! empty( $gallery_settings['previous_button_label'] ) ? $gallery_settings['previous_button_label'] : $defaults['ayg_strings_settings']['previous_button_label'],
55 + 'next_button_label' => ! empty( $gallery_settings['next_button_label'] ) ? $gallery_settings['next_button_label'] : $defaults['ayg_strings_settings']['next_button_label'],
56 + 'show_more_label' => $defaults['ayg_strings_settings']['show_more_label'],
57 + 'show_less_label' => $defaults['ayg_strings_settings']['show_less_label'],
58 + );
59 +
60 + add_option( 'ayg_strings_settings', $strings_settings );
61 + }
62 +
63 + // Update the player settings
64 + $player_settings = get_option( 'ayg_player_settings' );
65 +
66 + if ( ! is_array( $player_settings ) || empty( $player_settings ) ) {
67 + $player_settings = $defaults['ayg_player_settings'];
68 + update_option( 'ayg_player_settings', $player_settings );
69 + }
70 +
71 + if ( ! array_key_exists( 'player_type', $player_settings ) ) {
72 + $player_settings['player_type'] = $defaults['ayg_player_settings']['player_type'];
73 + $player_settings['player_color'] = $defaults['ayg_player_settings']['player_color'];
74 +
75 + update_option( 'ayg_player_settings', $player_settings );
76 + }
77 +
33 78 // Insert the livestream settings
34 79 if ( false == get_option( 'ayg_livestream_settings' ) ) {
35 80 add_option( 'ayg_livestream_settings', $defaults['ayg_livestream_settings'] );
36 81 }
@@ -39,17 +84,13 @@
39 84 if ( false == get_option( 'ayg_privacy_settings' ) ) {
40 85 add_option( 'ayg_privacy_settings', $defaults['ayg_privacy_settings'] );
41 86 }
42 87
43 - // Create a custom database table "{$wpdb->prefix}ayg_videos"
44 - if ( version_compare( AYG_VERSION, '2.1.0', '<=' ) ) {
45 - ayg_db_create_videos_table();
46 - ayg_delete_cache();
47 - }
88 + // Create custom database tables
89 + ayg_db_create_custom_tables();
48 90
49 - if ( version_compare( AYG_VERSION, '2.2.0', '<=' ) ) {
50 - delete_option( 'ayg_gallery_page_ids' );
51 - }
91 + // Delete the plugin cache
92 + ayg_delete_cache();
52 93
53 94 // Update the plugin version
54 95 update_option( 'ayg_version', AYG_VERSION );
55 96 }
@@ -62,14 +103,14 @@
62 103 */
63 104 public function enqueue_styles() {
64 105 wp_enqueue_style( 'wp-color-picker' );
65 106
66 - wp_enqueue_style(
67 - AYG_SLUG . '-admin',
68 - AYG_URL . 'admin/assets/css/admin.css',
69 - array(),
70 - AYG_VERSION,
71 - 'all'
107 + wp_enqueue_style(
108 + AYG_SLUG . '-admin',
109 + AYG_URL . 'admin/assets/css/admin.min.css',
110 + array(),
111 + AYG_VERSION,
112 + 'all'
72 113 );
73 114 }
74 115
75 116 /**
@@ -80,27 +121,71 @@
80 121 public function enqueue_scripts() {
81 122 wp_enqueue_media();
82 123 wp_enqueue_script( 'wp-color-picker' );
83 124
84 - wp_enqueue_script(
85 - AYG_SLUG . '-admin',
86 - AYG_URL . 'admin/assets/js/admin.js',
87 - array( 'jquery' ),
88 - AYG_VERSION,
89 - false
125 + wp_enqueue_script(
126 + AYG_SLUG . '-admin',
127 + AYG_URL . 'admin/assets/js/admin.min.js',
128 + array( 'jquery' ),
129 + AYG_VERSION,
130 + false
90 131 );
91 132
92 - wp_localize_script(
93 - AYG_SLUG . '-admin',
94 - 'ayg_admin',
133 + wp_localize_script(
134 + AYG_SLUG . '-admin',
135 + 'ayg_admin',
95 136 array(
96 137 'ajax_nonce' => wp_create_nonce( 'ayg_ajax_nonce' ),
97 - 'i18n' => array(
98 - 'invalid_api_key' => __( 'Invalid API Key', 'automatic-youtube-gallery' ),
99 - 'cleared' => __( 'Cleared', 'automatic-youtube-gallery' )
100 - )
138 + 'admin_url' => admin_url( 'admin.php' ),
139 + 'live_types' => array( 'search', 'livestream', 'video' ), // Rendered live — source stays editable (mirrors gallery-form.php).
140 + 'i18n' => array(
141 + // API key
142 + 'invalid_api_key' => __( 'Please enter your YouTube API key.', 'automatic-youtube-gallery' ),
143 +
144 + // Settings
145 + 'cache_cleared' => __( 'Cleared', 'automatic-youtube-gallery' ),
146 +
147 + // Common
148 + 'save_failed' => __( 'Save failed. Please try again.', 'automatic-youtube-gallery' ),
149 + 'delete_failed' => __( 'Delete failed. Please try again.', 'automatic-youtube-gallery' ),
150 +
151 + // Gallery: Shortcode
152 + 'shortcode_copied' => __( 'Shortcode copied! Paste it into any post, page, or widget to display your gallery.', 'automatic-youtube-gallery' ),
153 +
154 + // Gallery: Delete confirmations
155 + 'confirm_delete' => __( 'Are you sure you want to delete this gallery? This action cannot be undone.', 'automatic-youtube-gallery' ),
156 + 'confirm_bulk_delete' => __( 'Are you sure you want to delete the selected galleries? This action cannot be undone.', 'automatic-youtube-gallery' ),
157 +
158 + // Gallery Form
159 + 'unsaved_changes' => __( 'You have unsaved changes. Are you sure you want to leave?', 'automatic-youtube-gallery' ),
160 + 'update_gallery' => __( 'Update Gallery', 'automatic-youtube-gallery' ),
161 +
162 + // Gallery Form: Source validation
163 + 'source_required' => array(
164 + 'channel' => __( 'A YouTube channel ID (or) a video URL from the channel is required.', 'automatic-youtube-gallery' ),
165 + 'playlist' => __( 'A YouTube playlist ID (or) URL is required.', 'automatic-youtube-gallery' ),
166 + 'username' => __( 'A YouTube account username is required.', 'automatic-youtube-gallery' ),
167 + 'search' => __( 'A search keyword is required.', 'automatic-youtube-gallery' ),
168 + 'livestream' => __( 'A YouTube channel ID (or) a video URL from the channel is required.', 'automatic-youtube-gallery' ),
169 + 'video' => __( 'A YouTube video ID (or) URL is required.', 'automatic-youtube-gallery' ),
170 + 'videos' => __( 'At least one YouTube video ID (or) URL is required.', 'automatic-youtube-gallery' )
171 + ),
172 + 'channel_handle' => __( 'YouTube @handle URLs aren’t supported here. Please enter a channel ID, a /channel/ URL, or a video URL from the channel.', 'automatic-youtube-gallery' ),
173 +
174 + // Source hint text, swapped live as the source-type dropdown changes. Must match
175 + // the strings rendered in gallery-form.php's .ayg-source-hint (live / importable).
176 + 'source_live' => __( 'This is a live source — you can change it anytime. Updates take effect immediately the next time the gallery is viewed.', 'automatic-youtube-gallery' ),
177 + 'source_importable' => __( 'The video source can’t be changed once videos are imported. So please make sure you select the right source before saving.', 'automatic-youtube-gallery' ),
178 +
179 + // Gallery Form: Import progress
180 + 'processing' => __( 'Processing', 'automatic-youtube-gallery' ),
181 + 'import_progress' => __( 'Imported: %imported%, Updated: %updated%. Please do not close this window until you see a success or error message', 'automatic-youtube-gallery' ),
182 + 'import_complete' => __( 'Done! Imported: %imported%, Updated: %updated%, Deleted: %deleted%. Refreshing the page', 'automatic-youtube-gallery' ),
183 + 'import_failed' => __( 'Import failed. Please try again.', 'automatic-youtube-gallery' ),
184 + 'quota_exceeded' => __( 'YouTube API quota exceeded. The import has been paused — click "Update Gallery" to resume after the quota resets.', 'automatic-youtube-gallery' )
185 + )
101 186 )
102 - );
187 + );
103 188 }
104 189
105 190 /**
106 191 * Add dashboard page link on the plugins menu.
@@ -125,19 +210,22 @@
125 210 * Add "Dashboard" menu.
126 211 *
127 212 * @since 1.3.0
128 213 */
129 - public function admin_menu() {
130 - add_menu_page(
131 - __( 'Automatic YouTube Gallery', 'automatic-youtube-gallery' ),
214 + public function admin_menu() {
215 + $hook = add_menu_page(
216 + __( 'Automatic YouTube Gallery', 'automatic-youtube-gallery' ),
132 217 __( 'YouTube Gallery', 'automatic-youtube-gallery' ),
133 - 'manage_options',
134 - 'automatic-youtube-gallery',
218 + 'manage_options',
219 + 'automatic-youtube-gallery',
135 220 array( $this, 'display_dashboard_content' ),
136 - 'dashicons-video-alt3',
137 - 10
221 + 'dashicons-format-video',
222 + 10
138 223 );
139 224
225 + // Process list table bulk actions before any HTML is output to allow wp_redirect().
226 + add_action( 'load-' . $hook, array( $this, 'process_list_table_bulk_actions' ) );
227 +
140 228 add_submenu_page(
141 229 'automatic-youtube-gallery',
142 230 __( 'Dashboard', 'automatic-youtube-gallery' ),
143 231 __( 'Dashboard', 'automatic-youtube-gallery' ),
@@ -147,22 +235,56 @@
147 235 );
148 236 }
149 237
150 238 /**
239 + * Runs before the dashboard page HTML is output: hides the Screen Options tab
240 + * and processes list-table bulk actions on the list view.
241 + *
242 + * @since 2.8.0
243 + */
244 + public function process_list_table_bulk_actions() {
245 + $general_settings = ayg_get_option( 'ayg_general_settings' );
246 +
247 + // Hide the Screen Options tab entirely. The list table would otherwise
248 + // auto-populate a "Columns" section there, which we don't want on our
249 + // custom dashboard header.
250 + add_filter( 'screen_options_show_screen', '__return_false' );
251 +
252 + // No list table is rendered on the setup screen or the gallery form view.
253 + $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : '';
254 + if ( empty( $general_settings['api_key'] ) || 'new' === $action || 'edit' === $action ) {
255 + return;
256 + }
257 +
258 + // Strip the GET search form's leftover bulk-action fields from the URL.
259 + add_filter( 'removable_query_args', array( $this, 'add_removable_query_args' ) );
260 +
261 + // Process bulk actions
262 + require_once AYG_DIR . 'admin/galleries.php';
263 +
264 + $this->gallery_list_table = new AYG_Admin_Gallery_List_Table();
265 + $this->gallery_list_table->process_bulk_action();
266 + }
267 +
268 + /**
269 + * Add the gallery list table's bulk-action fields to the list of query args
270 + * WordPress strips from the URL via wp_admin_canonical_url().
271 + *
272 + * @since 2.8.0
273 + * @param array $args Removable query argument names.
274 + * @return array
275 + */
276 + public function add_removable_query_args( $args ) {
277 + return array_merge( $args, array( 'action', 'action2', 'bulk_action', '_wpnonce', '_wp_http_referer' ) );
278 + }
279 +
280 + /**
151 281 * Display dashboard content.
152 282 *
153 283 * @since 1.3.0
154 284 */
155 285 public function display_dashboard_content() {
156 - $general_settings = get_option( 'ayg_general_settings' );
157 -
158 - $tabs = array(
159 - 'dashboard' => __( 'Build Gallery', 'automatic-youtube-gallery' )
160 - );
161 -
162 - $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'dashboard';
163 -
164 - require_once AYG_DIR . 'admin/templates/dashboard.php';
286 + require_once AYG_DIR . 'admin/templates/dashboard.php';
165 287 }
166 288
167 289 /**
168 290 * Prints admin screen notices.
@@ -169,13 +291,27 @@
169 291 *
170 292 * @since 2.0.0
171 293 */
172 294 public function admin_notices() {
173 - $general_settings = get_option( 'ayg_general_settings' );
295 + $screen = get_current_screen();
296 + $on_page = $screen && 'toplevel_page_automatic-youtube-gallery' === $screen->id;
174 297
175 - if ( isset( $general_settings['development_mode'] ) && ! empty( $general_settings['development_mode'] ) ) {
298 + // Show on all admin pages except our own gallery form (action=new|edit).
299 + if ( $on_page ) {
300 + $action = isset( $_GET['action'] ) ? sanitize_text_field( $_GET['action'] ) : '';
301 + if ( 'new' === $action || 'edit' === $action ) {
302 + return;
303 + }
304 + }
305 +
306 + $general_settings = ayg_get_option( 'ayg_general_settings' );
307 +
308 + if ( ! empty( $general_settings['development_mode'] ) ) {
309 + // WordPress relocates non-inline notices on our custom dashboard header, hiding them.
310 + // Add the "inline" class only there so the notice stays put; leave it off elsewhere.
311 + $classes = 'notice notice-info' . ( $on_page ? ' inline' : '' );
176 312 ?>
177 - <div class="notice notice-info">
313 + <div class="<?php echo esc_attr( $classes ); ?>">
178 314 <p>
179 315 <?php
180 316 printf(
181 317 __( '<strong>Automatic YouTube Gallery:</strong> You have <a href="%s">development mode</a> enabled. We do not cache API results in this mode. While this is ok when you are testing the plugin, we strongly recommend disabling this option when your site goes live.', 'automatic-youtube-gallery' ),
@@ -192,16 +328,295 @@
192 328 * Save API Key.
193 329 *
194 330 * @since 1.3.0
195 331 */
196 - public function ajax_callback_save_api_key() {
332 + public function ajax_callback_save_api_key() {
197 333 check_ajax_referer( 'ayg_ajax_nonce', 'security' );
198 -
199 - $general_settings = get_option( 'ayg_general_settings' );
334 +
335 + if ( ! current_user_can( 'manage_options' ) ) {
336 + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
337 + }
338 +
339 + $general_settings = ayg_get_option( 'ayg_general_settings' );
340 +
200 341 $general_settings['api_key'] = sanitize_text_field( $_POST['api_key'] );
342 + update_option( 'ayg_general_settings', $general_settings );
201 343
202 - update_option( 'ayg_general_settings', $general_settings );
344 + wp_send_json_success();
345 + }
203 346
204 - wp_die();
347 + /**
348 + * Save a gallery (insert or update).
349 + *
350 + * @since 2.8.0
351 + */
352 + public function ajax_callback_save_gallery() {
353 + check_ajax_referer( 'ayg_ajax_nonce', 'security' );
354 +
355 + if ( ! current_user_can( 'manage_options' ) ) {
356 + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
357 + }
358 +
359 + $gallery_id = absint( isset( $_POST['gallery_id'] ) ? $_POST['gallery_id'] : 0 );
360 + $gallery_title = sanitize_text_field( isset( $_POST['title'] ) ? $_POST['title'] : '' );
361 +
362 + $params = array();
363 + $source_locked = false;
364 + $source_type = '';
365 + $source_value = '';
366 +
367 + if ( $gallery_id > 0 ) {
368 + $existing = ayg_get_gallery( $gallery_id );
369 +
370 + if ( ! $existing ) {
371 + wp_send_json_error( array( 'message' => __( 'Gallery not found.', 'automatic-youtube-gallery' ) ) );
372 + }
373 +
374 + // Seed params from the saved row so values from disabled inputs and runtime
375 + // keys (page_token, last_video_published_at) survive the update.
376 + $saved_params = json_decode( (string) $existing->params, true );
377 + if ( is_array( $saved_params ) ) {
378 + $params = $saved_params;
379 + }
380 +
381 + // The source locks only once videos have been imported. While the gallery
382 + // still has no videos (e.g. the first import failed on a bad source), the
383 + // source stays editable so it can be corrected in place. Live sources
384 + // (search / livestream / single video) never import, so they never lock.
385 + $source_locked = ( (int) $existing->video_count > 0 && ! in_array( $existing->source_type, array( 'search', 'livestream', 'video' ), true ) );
386 +
387 + if ( $source_locked ) {
388 + $source_type = $existing->source_type;
389 + $source_value = $existing->source_value;
390 + }
391 + }
392 +
393 + // Read the source from POST for a new gallery, or for an update before the first
394 + // import. A re-edited source starts a clean import, so drop the runtime keys.
395 + if ( ! $source_locked ) {
396 + unset( $params['page_token'], $params['last_video_published_at'] );
397 +
398 + $allowed_source_types = ayg_get_source_types();
399 + $source_type = sanitize_key( isset( $_POST['type'] ) ? $_POST['type'] : 'playlist' );
400 + if ( ! array_key_exists( $source_type, $allowed_source_types ) ) {
401 + $source_type = 'playlist';
402 + }
403 +
404 + switch ( $source_type ) {
405 + case 'channel':
406 + case 'livestream':
407 + $source_value = sanitize_text_field( isset( $_POST['channel'] ) ? $_POST['channel'] : '' );
408 + break;
409 +
410 + case 'playlist':
411 + $source_value = sanitize_text_field( isset( $_POST['playlist'] ) ? $_POST['playlist'] : '' );
412 + break;
413 +
414 + case 'username':
415 + $source_value = sanitize_text_field( isset( $_POST['username'] ) ? $_POST['username'] : '' );
416 + break;
417 +
418 + case 'search':
419 + $source_value = sanitize_text_field( isset( $_POST['search'] ) ? $_POST['search'] : '' );
420 + break;
421 +
422 + case 'video':
423 + $source_value = sanitize_text_field( isset( $_POST['video'] ) ? $_POST['video'] : '' );
424 + break;
425 +
426 + case 'videos':
427 + $source_value = sanitize_textarea_field( isset( $_POST['videos'] ) ? $_POST['videos'] : '' );
428 + break;
429 +
430 + default:
431 + $source_value = '';
432 + }
433 + }
434 +
435 + $exclude = sanitize_textarea_field( isset( $_POST['exclude'] ) ? $_POST['exclude'] : '' );
436 + if ( ! empty( $exclude ) ) $exclude = array_values( array_filter( array_map( 'trim', explode( "\n", $exclude ) ) ) );
437 + $params['exclude'] = $exclude;
438 +
439 + // 'paused' is a non-numeric sentinel (stop automatic imports); every other value is seconds.
440 + $schedule = sanitize_text_field( isset( $_POST['schedule'] ) ? $_POST['schedule'] : 86400 );
441 + $params['schedule'] = ( 'paused' === $schedule ) ? 'paused' : absint( $schedule );
442 +
443 + // Display-time sort (gallery-form-only fields). Validated against whitelists; get_videos_from_db()
444 + // maps them into a safe ORDER BY. Defaults reproduce the prior hardcoded "newest first".
445 + $sort_by = sanitize_key( isset( $_POST['sort_by'] ) ? $_POST['sort_by'] : '' );
446 + $params['sort_by'] = in_array( $sort_by, array( 'date', 'title', 'duration', 'random' ), true ) ? $sort_by : 'date';
447 +
448 + $sort_order = sanitize_key( isset( $_POST['sort_order'] ) ? $_POST['sort_order'] : '' );
449 + $params['sort_order'] = ( 'asc' === $sort_order ) ? 'asc' : 'desc';
450 +
451 + // Display-time duration filter. get_videos_from_db() applies it only when a direction is set.
452 + $duration_filter = sanitize_key( isset( $_POST['duration_filter'] ) ? $_POST['duration_filter'] : '' );
453 + $params['duration_filter'] = in_array( $duration_filter, array( 'long', 'short' ), true ) ? $duration_filter : '';
454 + $params['duration'] = absint( isset( $_POST['duration'] ) ? $_POST['duration'] : 0 );
455 +
456 + // Fields stored in dedicated DB columns, not in params. 'cache' is kept (saved to params) so
457 + // the live 'search' source type can control its API cache duration.
458 + $fields = ayg_get_editor_fields();
459 + $excluded = array( 'type', 'channel', 'playlist', 'username', 'search', 'video', 'videos' );
460 +
461 + foreach ( $fields as $section ) {
462 + foreach ( $section['fields'] as $field ) {
463 + $name = $field['name'];
464 +
465 + if ( in_array( $name, $excluded, true ) ) {
466 + continue;
467 + }
468 +
469 + if ( 'checkbox' === $field['type'] ) {
470 + $params[ $name ] = isset( $_POST[ $name ] ) ? 1 : 0;
471 + } elseif ( ! isset( $_POST[ $name ] ) ) {
472 + continue;
473 + } elseif ( 'select' === $field['type'] && isset( $field['options'] ) ) {
474 + $raw = sanitize_text_field( $_POST[ $name ] );
475 + $params[ $name ] = array_key_exists( $raw, $field['options'] ) ? $raw : sanitize_text_field( $field['value'] );
476 + } else {
477 + $sanitize_callback = ! empty( $field['sanitize_callback'] ) ? $field['sanitize_callback'] : 'sanitize_text_field';
478 + $params[ $name ] = call_user_func( $sanitize_callback, $_POST[ $name ] );
479 + }
480 + }
481 + }
482 +
483 + global $wpdb;
484 + $table = $wpdb->prefix . 'ayg_galleries';
485 +
486 + $now = current_time( 'mysql' );
487 +
488 + $data = array(
489 + 'title' => $gallery_title,
490 + 'params' => wp_json_encode( $params ),
491 + 'updated_at' => $now
492 + );
493 +
494 + // Persist the source columns whenever they aren't locked (new gallery, or an
495 + // update before the first import). All columns here are strings, so the format
496 + // is left to default ('%s') instead of tracking a positional list.
497 + if ( ! $source_locked ) {
498 + $data['source_type'] = $source_type;
499 + $data['source_value'] = $source_value;
500 + }
501 +
502 + // A "paused" schedule stops automatic imports: flag the gallery paused, clear any prior
503 + // import error (so a user pause is distinguishable from a quota pause, which keeps its
504 + // message), and drop it from the cron queue. The JS save flow skips the import for this case.
505 + if ( 'paused' === $params['schedule'] ) {
506 + $data['import_status'] = 'paused';
507 + $data['import_error'] = '';
508 + $data['next_import_at'] = null;
509 + } elseif ( 0 === $params['schedule'] ) {
510 + // "Only Once" is non-recurring: ensure no future cron run stays queued. Switching here from a
511 + // recurring schedule may have left a stale next_import_at behind; clear it so a display-only
512 + // save (import skipped) can't trigger one more unexpected background import. A save that does
513 + // import will have import_batch() finalize the status / next run on completion anyway.
514 + $data['next_import_at'] = null;
515 + }
516 +
517 + if ( $gallery_id > 0 ) {
518 + if ( empty( $gallery_title ) ) {
519 + $gallery_title = sprintf( __( 'Gallery %d', 'automatic-youtube-gallery' ), $gallery_id );
520 + $data['title'] = $gallery_title;
521 + }
522 +
523 + // Drop any newly-excluded videos from this gallery's links (the videos table is left intact —
524 + // rows may be shared by other galleries) and fold the refreshed count into this same update.
525 + if ( ayg_delete_excluded_relationships( $gallery_id, $params['exclude'] ) > 0 ) {
526 + $data['video_count'] = (int) $wpdb->get_var(
527 + $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}ayg_gallery_relationships WHERE gallery_id = %s", strval( $gallery_id ) )
528 + );
529 + }
530 +
531 + $wpdb->update(
532 + $table,
533 + $data,
534 + array( 'id' => $gallery_id ),
535 + null,
536 + array( '%d' )
537 + );
538 + } else {
539 + $data['created_at'] = $now;
540 +
541 + $wpdb->insert( $table, $data );
542 +
543 + $gallery_id = (int) $wpdb->insert_id;
544 +
545 + if ( empty( $gallery_title ) ) {
546 + $gallery_title = sprintf( __( 'Gallery %d', 'automatic-youtube-gallery' ), $gallery_id );
547 +
548 + $wpdb->update(
549 + $table,
550 + array( 'title' => $gallery_title ),
551 + array( 'id' => $gallery_id ),
552 + array( '%s' ),
553 + array( '%d' )
554 + );
555 + }
556 + }
557 +
558 + // Live galleries (search / livestream / single video) render from the API and cache responses. Clear
559 + // this gallery's cache on save so the front-end reflects the latest results right after "Update Gallery".
560 + if ( in_array( $source_type, array( 'search', 'livestream', 'video' ), true ) ) {
561 + ayg_delete_cache( strval( $gallery_id ) );
562 + }
563 +
564 + wp_send_json_success( array(
565 + 'gallery_id' => $gallery_id,
566 + 'shortcode' => sprintf( '[automatic_youtube_gallery id="%d"]', $gallery_id ),
567 + ) );
568 + }
569 +
570 + /**
571 + * Import one batch of videos into a gallery via AJAX.
572 + *
573 + * The client calls this in a loop, passing back the page_token returned by
574 + * the previous call, until done = true.
575 + *
576 + * @since 2.8.0
577 + */
578 + public function ajax_callback_import_gallery() {
579 + check_ajax_referer( 'ayg_ajax_nonce', 'security' );
580 +
581 + if ( ! current_user_can( 'manage_options' ) ) {
582 + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
583 + }
584 +
585 + $gallery_id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
586 + $page_token = sanitize_text_field( isset( $_POST['page_token'] ) ? $_POST['page_token'] : '' );
587 +
588 + // The browser-driven import is always a manual run: full re-scan, uncapped, prunes deletions.
589 + $importer = new AYG_Import();
590 + $result = $importer->import_batch( $gallery_id, $page_token, true );
591 +
592 + if ( isset( $result['error'] ) ) {
593 + wp_send_json_error( array(
594 + 'message' => $result['error'],
595 + 'quota_exceeded' => ! empty( $result['quota_exceeded'] )
596 + ) );
597 + }
598 +
599 + wp_send_json_success( $result );
600 + }
601 +
602 + /**
603 + * Delete a gallery via AJAX.
604 + *
605 + * @since 2.8.0
606 + */
607 + public function ajax_callback_delete_gallery() {
608 + check_ajax_referer( 'ayg_ajax_nonce', 'security' );
609 +
610 + if ( ! current_user_can( 'manage_options' ) ) {
611 + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'automatic-youtube-gallery' ) ) );
612 + }
613 +
614 + require_once AYG_DIR . 'admin/galleries.php';
615 +
616 + $id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
617 + AYG_Admin_Gallery_List_Table::delete_gallery( $id );
618 +
619 + wp_send_json_success();
205 620 }
206 621
207 622 }