false, 'message' => 'Please enter an API key first.', ); } $test_url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&id=dQw4w9WgXcQ&key={$api_key}"; $response = wp_remote_get( $test_url, array( 'timeout' => 45, 'headers' => array( 'Accept' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ), 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'message' => 'Connection failed: ' . $response->get_error_message(), ); } $data = json_decode( wp_remote_retrieve_body( $response ) ); if ( isset( $data->error ) ) { $error_message = ''; if ( isset( $data->error->errors[0]->reason ) ) { switch ( $data->error->errors[0]->reason ) { case 'keyInvalid': $error_message = 'Your API key is invalid. Please check your key and try again.'; break; case 'keyExpired': $error_message = 'Your API key has expired. Please generate a new key.'; break; case 'accessNotConfigured': $error_message = 'YouTube Data API v3 is not enabled for this project. Please enable it in Google Cloud Console.'; break; case 'quotaExceeded': $error_message = 'API quota exceeded. Please try again later or upgrade your quota.'; break; default: $error_message = isset( $data->error->errors[0]->message ) ? $data->error->errors[0]->message : 'Invalid API key.'; } } else { $error_message = isset( $data->error->message ) ? $data->error->message : 'Invalid API key.'; } return array( 'success' => false, 'message' => $error_message, ); } return array( 'success' => true, 'message' => 'API key is valid and working correctly!', ); } /** * Fetch all playlists belonging to the provided channels. * * @param string $api_key YouTube API key. * @param string $channel_ids_input Comma-separated channel IDs. * @return array Playlist data. */ public function fetch_playlists( $api_key, $channel_ids_input ) { if ( empty( $api_key ) ) { return array( 'success' => false, 'message' => 'API key is required.', ); } if ( empty( $channel_ids_input ) ) { return array( 'success' => false, 'message' => 'Channel ID(s) are required.', ); } $channel_ids = array_map( 'trim', explode( ',', $channel_ids_input ) ); $channel_ids = array_filter( $channel_ids ); if ( empty( $channel_ids ) ) { return array( 'success' => false, 'message' => 'No valid Channel IDs provided.', ); } $all_playlists = array(); foreach ( $channel_ids as $channel_id ) { $page_token = ''; do { $playlist_url = "https://www.googleapis.com/youtube/v3/playlists?part=snippet,contentDetails&channelId={$channel_id}&maxResults=50&key={$api_key}"; if ( ! empty( $page_token ) ) { $playlist_url .= "&pageToken={$page_token}"; } $playlist_response = wp_remote_get( $playlist_url, array( 'timeout' => 30, 'headers' => array( 'Accept' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ), 'sslverify' => true, ) ); if ( is_wp_error( $playlist_response ) ) { break; } $playlist_data = json_decode( wp_remote_retrieve_body( $playlist_response ) ); if ( isset( $playlist_data->error ) ) { return array( 'success' => false, 'message' => 'YouTube API error: ' . ( isset( $playlist_data->error->message ) ? $playlist_data->error->message : 'Unknown error' ), ); } if ( ! empty( $playlist_data->items ) ) { foreach ( $playlist_data->items as $item ) { $all_playlists[] = array( 'id' => $item->id, 'title' => $item->snippet->title, 'description' => isset( $item->snippet->description ) ? substr( wp_strip_all_tags( $item->snippet->description ), 0, 100 ) : '', 'itemCount' => $item->contentDetails->itemCount, 'channelId' => $item->snippet->channelId, 'channelTitle' => $item->snippet->channelTitle, 'publishedAt' => $item->snippet->publishedAt, 'thumbnails' => isset( $item->snippet->thumbnails ) ? $item->snippet->thumbnails : null, ); } } $page_token = isset( $playlist_data->nextPageToken ) ? $playlist_data->nextPageToken : ''; } while ( ! empty( $page_token ) ); } usort( $all_playlists, function ( $a, $b ) { return strcmp( strtolower( $a['title'] ), strtolower( $b['title'] ) ); } ); return array( 'success' => true, 'playlists' => $all_playlists, 'total' => count( $all_playlists ), ); } /** * Fetch videos from the uploads playlist of the provided channels. * * @param string $api_key YouTube API key. * @param string $channel_ids_input Comma-separated channel IDs. * @param string $page_token Optional YouTube page token. * @return array Video data. */ public function fetch_videos( $api_key, $channel_ids_input, $page_token = '' ) { if ( empty( $api_key ) ) { return array( 'success' => false, 'message' => 'API key is required.', ); } if ( empty( $channel_ids_input ) ) { return array( 'success' => false, 'message' => 'Channel ID(s) are required.', ); } $channel_ids = array_map( 'trim', explode( ',', $channel_ids_input ) ); $channel_ids = array_filter( $channel_ids ); if ( empty( $channel_ids ) ) { return array( 'success' => false, 'message' => 'No valid Channel IDs provided.', ); } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery $imported_video_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != ''", PlugeTune_Free_Feature::META_VIDEO ) ); $imported_video_ids = array_map( 'trim', $imported_video_ids ); $all_videos = array(); $next_page_token = ''; foreach ( $channel_ids as $channel_id ) { $channel_url = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails&id={$channel_id}&key={$api_key}"; $channel_response = wp_remote_get( $channel_url, array( 'timeout' => 30, 'headers' => array( 'Accept' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ), 'sslverify' => true, ) ); if ( is_wp_error( $channel_response ) ) { continue; } $channel_data = json_decode( wp_remote_retrieve_body( $channel_response ) ); if ( empty( $channel_data->items ) ) { continue; } $uploads_playlist_id = $channel_data->items[0]->contentDetails->relatedPlaylists->uploads; $videos_url = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId={$uploads_playlist_id}&key={$api_key}"; if ( ! empty( $page_token ) ) { $videos_url .= "&pageToken={$page_token}"; } $videos_response = wp_remote_get( $videos_url, array( 'timeout' => 30, 'headers' => array( 'Accept' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ), 'sslverify' => true, ) ); if ( is_wp_error( $videos_response ) ) { return array( 'success' => false, 'message' => 'Failed to fetch videos: ' . $videos_response->get_error_message(), ); } $videos_data = json_decode( wp_remote_retrieve_body( $videos_response ) ); if ( isset( $videos_data->error ) ) { return array( 'success' => false, 'message' => 'YouTube API error: ' . ( isset( $videos_data->error->message ) ? $videos_data->error->message : 'Unknown error' ), ); } if ( ! empty( $videos_data->items ) ) { foreach ( $videos_data->items as $item ) { $video_id = $item->snippet->resourceId->videoId; $is_imported = in_array( $video_id, $imported_video_ids, true ); $all_videos[] = array( 'id' => $video_id, 'title' => $item->snippet->title, 'description' => isset( $item->snippet->description ) ? substr( wp_strip_all_tags( $item->snippet->description ), 0, 100 ) : '', 'publishedAt' => $item->snippet->publishedAt, 'thumbnails' => isset( $item->snippet->thumbnails ) ? $item->snippet->thumbnails : null, 'is_imported' => $is_imported, ); } } $next_page_token = isset( $videos_data->nextPageToken ) ? $videos_data->nextPageToken : ''; } return array( 'success' => true, 'videos' => $all_videos, 'total' => count( $all_videos ), 'nextPageToken' => $next_page_token, ); } /** * Validate that a playlist belongs to one of the allowed channels. * * @param string $playlist_id YouTube playlist ID. * @param string $api_key YouTube API key. * @param array $allowed_channel_ids Allowed channel IDs. * @return array|false Playlist ownership data or false. */ public function validate_playlist_ownership( $playlist_id, $api_key, $allowed_channel_ids ) { $playlist_url = "https://www.googleapis.com/youtube/v3/playlists?part=snippet,status&id={$playlist_id}&key={$api_key}"; $playlist_response = wp_remote_get( $playlist_url, array( 'timeout' => 45, 'headers' => array( 'Accept' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ), 'sslverify' => true, ) ); if ( is_wp_error( $playlist_response ) ) { $this->log_error( $playlist_id, 'API request failed: ' . $playlist_response->get_error_message() ); return false; } $playlist_data = json_decode( wp_remote_retrieve_body( $playlist_response ) ); if ( isset( $playlist_data->error ) ) { $this->log_error( $playlist_id, 'API error: ' . wp_json_encode( $playlist_data->error ) ); return false; } if ( empty( $playlist_data->items ) ) { $this->log_error( $playlist_id, 'Playlist not found or not accessible' ); return false; } $playlist = $playlist_data->items[0]; $playlist_channel_id = $playlist->snippet->channelId; $playlist_title = $playlist->snippet->title; $playlist_channel_clean = trim( $playlist_channel_id ); $is_authorized = false; foreach ( $allowed_channel_ids as $allowed_id ) { $allowed_clean = trim( $allowed_id ); if ( strtolower( $allowed_clean ) === strtolower( $playlist_channel_clean ) ) { $is_authorized = true; break; } } if ( $is_authorized ) { return array( 'owner_channel_id' => $playlist_channel_id, 'playlist_title' => $playlist_title, ); } $this->log_error( $playlist_id, 'Ownership validation failed. Playlist owner: ' . $playlist_channel_id . ', Allowed channels: ' . implode( ', ', $allowed_channel_ids ) ); return false; } /** * Extract the YouTube video ID from a URL. * * @param string $url YouTube URL. * @return string Video ID, or an empty string when not matched. */ public function extract_youtube_id_from_url( $url ) { $patterns = array( '/(?:youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/', '/(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/', '/(?:youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/', '/(?:youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/', '/(?:v=)([a-zA-Z0-9_-]{11})/', ); foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $url, $matches ) ) { return $matches[1]; } } return ''; } /** * Log a debug message when WP_DEBUG is enabled. * * @param string $playlist_id Playlist ID associated with the message. * @param string $message Message to log. * @return void */ private function log_error( $playlist_id, $message ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log error_log( sprintf( '[PlugeTune YouTube API] Playlist ID: %s - %s', $playlist_id, $message ) ); } } /** * Render the YouTube API settings UI on the plugin settings page. * * All styling is applied via classes defined in assets/css/plugetune_admin.css. * * @return void */ public static function render_settings() { ?>