auth_key = get_option($this->option_name); add_action('rest_api_init', array($this, 'register_routes')); add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); register_activation_hook(__FILE__, array($this, 'activate_plugin')); // Add action to handle form submission add_action('admin_post_image_scrobble_upload', array($this, 'handle_image_upload')); } public function activate_plugin() { // Set default AUTH_KEY if not exists if (!get_option($this->option_name)) { update_option($this->option_name, wp_generate_password(32, false)); } } public function register_routes() { register_rest_route('image-scrobble/v1', '/create', array( 'methods' => 'POST', 'callback' => array($this, 'create_image'), 'permission_callback' => array($this, 'check_auth') )); register_rest_route('image-scrobble/v1', '/scrobble', array( 'methods' => 'POST', 'callback' => array($this, 'scrobble'), 'permission_callback' => array($this, 'check_auth') )); } public function check_auth($request) { $auth_header = $request->get_header('Authorization'); return $auth_header && $auth_header === "Bearer {$this->auth_key}"; } public function create_image($request) { // Retrieve the raw image data from the request body $image_data = $request->get_body(); // Sanitize and retrieve parameters from the request $author = sanitize_text_field($request->get_param('author')); $album_title = sanitize_text_field($request->get_param('album_title')); $song_title = sanitize_text_field($request->get_param('song_title')); // Validate required parameters if (empty($author)) { return new WP_Error( 'missing_author', 'Author is required.', array('status' => 400) ); } // Ensure that either album_title or song_title is provided if (empty($album_title) && empty($song_title)) { return new WP_Error( 'missing_titles', 'Either album title or song title is required.', array('status' => 400) ); } // Determine which title to use: album_title takes precedence over song_title $title = !empty($album_title) ? $album_title : $song_title; // Generate a hash of the image data to check for duplicates $image_hash = md5($image_data); // Check if an image with this hash already exists to prevent duplicates $existing_attachment = $this->get_attachment_by_hash($image_hash); if ($existing_attachment) { return new WP_REST_Response( array('url' => wp_get_attachment_url($existing_attachment->ID)), 200 ); } // Save the image and create attachment $result = $this->save_image_and_create_attachment($image_data, $author, $title, $image_hash, $album_title); if (is_wp_error($result)) { return $result; } // Return a successful response with the image URL return new WP_REST_Response( array('url' => $result), 200 ); } private function save_image_and_create_attachment($image_data, $author, $title, $image_hash, $album_title = '') { // Get the upload directory information $upload_dir = wp_upload_dir(); // Create a sanitized filename using the author and the determined title $filename = sanitize_file_name("{$author} - {$title}.jpg"); // Ensure the filename is unique within the upload directory $unique_filename = wp_unique_filename($upload_dir['path'], $filename); // Construct the full file path $file_path = trailingslashit($upload_dir['path']) . $unique_filename; // Attempt to save the image data to the specified file path if (file_put_contents($file_path, $image_data) === false) { return new WP_Error( 'file_save_failed', 'Failed to save the image file.', array('status' => 500) ); } // Prepare the attachment data $attachment = array( 'post_mime_type' => 'image/jpeg', 'post_title' => "{$author} - {$title}", 'post_content' => '', 'post_status' => 'inherit' ); // Insert the attachment into the WordPress media library $attach_id = wp_insert_attachment($attachment, $file_path); // Check for errors during attachment insertion if (is_wp_error($attach_id)) { @unlink($file_path); // Clean up the file if attachment creation failed return $attach_id; // Return the WP_Error } // Generate and update attachment metadata require_once(ABSPATH . 'wp-admin/includes/image.php'); $attach_data = wp_generate_attachment_metadata($attach_id, $file_path); wp_update_attachment_metadata($attach_id, $attach_data); // Save the image hash in the attachment's metadata for future duplicate checks update_post_meta($attach_id, 'image_hash', $image_hash); // Save the artist and album_title in attachment metadata for future lookups if (!empty($album_title)) { update_post_meta($attach_id, 'artist', strtolower($author)); update_post_meta($attach_id, 'album_title', strtolower($album_title)); } // Retrieve the URL of the newly created attachment $image_url = wp_get_attachment_url($attach_id); return $image_url; } private function get_attachment_by_hash($hash) { $args = array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => 1, 'meta_query' => array( array( 'key' => 'image_hash', 'value' => $hash, 'compare' => '=' ) ) ); $query = new WP_Query($args); if ($query->have_posts()) { return $query->posts[0]; } return null; } private function get_attachment_by_artist_album($artist, $album_title) { global $wpdb; // Access the global $wpdb object // Primary method: Query attachments based on artist and album title $args = array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => 1, 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'artist', 'value' => strtolower($artist), 'compare' => '=' ), array( 'key' => 'album_title', 'value' => strtolower($album_title), 'compare' => '=' ) ) ); $query = new WP_Query($args); if ($query->have_posts()) { // Attachment found via primary method return wp_get_attachment_url($query->posts[0]->ID); } // Sanitize and prepare the variables $album_name_lower = strtolower($album_title); $author_lower = strtolower($artist); $table_name = 'song_scrobbles'; // Prepare the SQL query using $wpdb->prepare to prevent SQL injection $prepared_query = $wpdb->prepare( " SELECT cover_url FROM {$table_name} WHERE LOWER(album_name) = %s AND LOWER(author) = %s ORDER BY id DESC LIMIT 1 ", $album_name_lower, $author_lower ); // Execute the query and retrieve the cover URL $cover_url = $wpdb->get_var($prepared_query); if ($cover_url) { // Optional: Validate the URL or process it as needed return esc_url($cover_url); } // Try fetching from Apple iTunes $remote_album_url = $this->fetch_and_save_album_cover($artist, $album_title); if ($remote_album_url) { return esc_url($remote_album_url); } // If both methods fail, return null return null; } function normalize_names(string $author, string $songName, string $albumName): array { // Define the album name mapping $albumNameMapping = [ 'Nirvana' => [ 'In Utero (20th Anniversary Remaster)' => 'In Utero', 'Nevermind (Deluxe Edition)' => 'Nevermind', 'Bleach: Deluxe Edition' => 'Bleach' ] ]; // Apply the album name mapping if applicable if ($albumName && isset($albumNameMapping[$author])) { foreach ($albumNameMapping[$author] as $variant => $standardized) { // Use case-insensitive comparison if (strcasecmp($albumName, $variant) === 0) { $albumName = $standardized; break; } } } // Define a regex pattern to remove remaster annotations like "(2022 Remaster)" $remasterPattern = '/\(\d+\s+Remaster\)/i'; // Remove remaster annotations from song and album names $songName = preg_replace($remasterPattern, '', $songName); $albumName = preg_replace($remasterPattern, '', $albumName); // Define a regex pattern to remove "(Deluxe Edition)" and "(Deluxe Version)" (case-insensitive) $deluxePattern = '/\(deluxe\s+(edition|version)\)/i'; // Remove "(Deluxe Edition)" and "(Deluxe Version)" from album names $albumName = preg_replace($deluxePattern, '', $albumName); // === Added Normalization for "COLLECTORS EDITION." === // Define a regex pattern to remove "COLLECTORS EDITION." at the end of album names $collectorsPattern = '/\bCollectors\s+Edition\.?$/i'; // Remove "COLLECTORS EDITION." from album names $albumName = preg_replace($collectorsPattern, '', $albumName); // ==================================================== // Trim any extra whitespace that might have resulted from the removal $songName = trim($songName); $albumName = trim($albumName); return [ 'song_name' => $songName, 'album_name' => $albumName, ]; } public function scrobble($request) { global $wpdb; $data = $request->get_json_params(); // Updated required fields: 'album_name' is no longer required $required_fields = ['song_name', 'author']; // Check for missing required fields foreach ($required_fields as $field) { if (!isset($data[$field])) { return new WP_Error('missing_field', 'Missing required field: ' . $field, array('status' => 400)); } } $table_name = 'song_scrobbles'; $author = isset($data['author']) ? $data['author'] : null; $cover_url = isset($data['cover_url']) ? $data['cover_url'] : null; $normalized = $this->normalize_names($data['author'], $data['song_name'], $data['album_name']); $song_name = $normalized['song_name']; $album_name = $normalized['album_name']; // If 'album_name' is not provided, 'cover_url' must be present if (empty($album_name)) { if (empty($cover_url)) { return new WP_Error( 'missing_cover_url', 'Cover URL must be provided if album name is not present', array('status' => 400) ); } } else { // If 'album_name' is provided but 'cover_url' is not, attempt to retrieve it from existing entries if (empty($cover_url)) { // Prepare case-insensitive search $author_lower = strtolower($data['author']); $album_name_lower = strtolower($album_name); // Try to find attachment $cover_url = $this->get_attachment_by_artist_album($data['author'], $album_name); if (!isset($cover_url)) { // If no matching entry is found, return an error return new WP_Error( 'missing_cover_url', $cover_url, array('status' => 400) ); } } } // Insert the scrobble data into the database $result = $wpdb->insert( $table_name, array( 'song_name' => $song_name, 'album_name' => $album_name, // This can be null 'cover_url' => $cover_url, 'author' => $data['author'], 'length_seconds' => isset($data['length_seconds']) ? intval($data['length_seconds']) : null ), array('%s', '%s', '%s', '%s', '%d') ); // Handle potential database insertion errors if ($result === false) { return new WP_Error('db_error', 'Error saving scrobble', array('status' => 500)); } // Return a successful response return new WP_REST_Response(array('message' => 'Scrobble saved successfully'), 201); } public function add_admin_menu() { add_options_page( 'Image Scrobble Settings', 'Image Scrobble', 'manage_options', 'image-scrobble-settings', array($this, 'settings_page') ); // Add submenu page for uploading attachments add_options_page( 'Upload Album Cover', 'Upload Album Cover', 'manage_options', 'image-scrobble-upload', array($this, 'upload_form_page') ); } public function upload_form_page() { ?>

Upload Album Cover

Album cover uploaded successfully.

'; } elseif ($message == 'duplicate_image') { echo '

An image with this content already exists.

'; } elseif ($message == 'upload_error') { echo '

Error during file upload.

'; } elseif ($message == 'not_an_image') { echo '

The uploaded file is not a valid image.

'; } elseif ($message == 'missing_fields') { echo '

Please fill in all required fields.

'; } elseif ($message == 'upload_failed') { echo '

Failed to upload image.

'; } else { echo '

An unknown error occurred.

'; } } ?>
Artist
Album Title
Album Cover Image
[ 'Gasoline EP' => 'Gasoline EP - EP' ] ]; // Check if there's an exception for this artist and album if (isset($album_exceptions[$artist_name]) && isset($album_exceptions[$artist_name][$album_name])) { $album_name = $album_exceptions[$artist_name][$album_name]; } // Encode the artist and album name for the URL $search_term = urlencode($artist_name . ' ' . $album_name); // Construct the iTunes Search API URL $api_url = "https://itunes.apple.com/search?term={$search_term}&entity=album&limit=10"; // Fetch the API response $response = wp_remote_get($api_url); // Check for errors in the API request if (is_wp_error($response)) { return new WP_Error( 'api_request_failed', 'Failed to connect to the iTunes API.', array('status' => 500) ); } // Decode the JSON response $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // Check if the API returned any results if (empty($data['results'])) { return new WP_Error( 'no_results', 'No album found matching the provided artist and album name.', array('status' => 404) ); } // Initialize a variable to store the matched album $matched_album = null; // Iterate through the results to find an exact match foreach ($data['results'] as $album) { $api_artist_name = strtolower($album['artistName']); $api_album_name = strtolower($album['collectionName']); // or 'collectionCensoredName' // Normalize the input and API data for comparison $input_artist_name = strtolower($artist_name); $input_album_name = strtolower($album_name); // Remove any extra whitespace and special characters $api_artist_name = preg_replace('/\s+/', ' ', trim($api_artist_name)); $api_album_name = preg_replace('/\s+/', ' ', trim($api_album_name)); $input_artist_name = preg_replace('/\s+/', ' ', trim($input_artist_name)); $input_album_name = preg_replace('/\s+/', ' ', trim($input_album_name)); // Compare the artist and album names if ($api_artist_name === $input_artist_name && $api_album_name === $input_album_name) { $matched_album = $album; break; } } // Check if a matched album was found if (!$matched_album) { return new WP_Error( 'no_exact_match', 'No exact match found for the provided artist and album name.', array('status' => 404) ); } // Get the artwork URL and modify it $artwork_url = $matched_album['artworkUrl100']; $artwork_url_500 = str_replace('100x100bb', '1000x1000bb', $artwork_url); // Fetch the image data $image_response = wp_remote_get($artwork_url_500); // Check for errors in the image request if (is_wp_error($image_response)) { return new WP_Error( 'image_request_failed', 'Failed to fetch the album cover image.', array('status' => 500) ); } // Retrieve the image data $image_data = wp_remote_retrieve_body($image_response); // Check if image data was retrieved if (empty($image_data)) { return new WP_Error( 'empty_image_data', 'No image data was returned from the iTunes API.', array('status' => 500) ); } // Generate a hash of the image data to check for duplicates $image_hash = md5($image_data); // Call the provided method to save the image and create an attachment return $this->save_image_and_create_attachment( $image_data, $artist_name, $album_name, $image_hash, $album_name // Passing the album title ); } public function handle_image_upload() { // Check if current user has permission if (!current_user_can('manage_options')) { wp_die('Unauthorized user'); } // Verify nonce check_admin_referer('image_scrobble_upload', 'image_scrobble_upload_nonce'); // Get the posted data $artist = sanitize_text_field($_POST['artist']); $album_title = sanitize_text_field($_POST['album_title']); // Check if required fields are present if (empty($artist) || empty($album_title) || empty($_FILES['album_cover'])) { wp_redirect(add_query_arg('message', 'missing_fields', wp_get_referer())); exit; } // Handle the uploaded file $file = $_FILES['album_cover']; // Check for upload errors if ($file['error'] !== UPLOAD_ERR_OK) { wp_redirect(add_query_arg('message', 'upload_error', wp_get_referer())); exit; } // Check if the file is an image $check = getimagesize($file['tmp_name']); if ($check === false) { wp_redirect(add_query_arg('message', 'not_an_image', wp_get_referer())); exit; } // Read the image data $image_data = file_get_contents($file['tmp_name']); // Generate a hash of the image data to check for duplicates $image_hash = md5($image_data); // Check if an image with this hash already exists to prevent duplicates $existing_attachment = $this->get_attachment_by_hash($image_hash); if ($existing_attachment) { wp_redirect(add_query_arg('message', 'duplicate_image', wp_get_referer())); exit; } // Save the image and create attachment (using same logic as create_image) $image_url = $this->save_image_and_create_attachment($image_data, $artist, $album_title, $image_hash, $album_title); if (is_wp_error($image_url)) { wp_redirect(add_query_arg('message', 'upload_failed', wp_get_referer())); exit; } // Redirect back with success message wp_redirect(add_query_arg('message', 'upload_success', wp_get_referer())); exit; } public function register_settings() { register_setting('image_scrobble_settings', $this->option_name); } public function settings_page() { ?>

Image Scrobble Settings

AUTH_KEY