db = $wpdb; add_action('rest_api_init', array($this, 'register_endpoint')); $this->background_color = $this->hex_to_rgb('#141617'); $this->text_color = $this->hex_to_rgb('#ececec'); $this->western_font = plugin_dir_path(__FILE__) . 'assets/co1251n.ttf'; $this->japanese_font = plugin_dir_path(__FILE__) . 'assets/BIZUDPMincho-Regular.ttf'; $this->blacklisted_artists = array('Drake', 'Mac Miller', 'Kendrick Lamar'); $this->blacklisted_albums = array('OK Computer OKNOTOK 1997 2017'); } public function register_endpoint() { register_rest_route('top-albums/v1', '/last-month', array( 'methods' => 'GET', 'callback' => array($this, 'get_or_generate_image'), 'permission_callback' => '__return_true', 'args' => array( 'month' => array( 'required' => false, 'validate_callback' => array($this, 'is_valid_month'), ), 'year' => array( 'required' => false, 'validate_callback' => array($this, 'is_valid_year'), ), 'overwrite' => array( 'required' => false, 'default' => false, 'validate_callback' => array($this, 'is_valid_overwrite'), ), ), )); } public function is_valid_month($param, $request, $key) { $month = intval($param); return $month >= 1 && $month <= 12; } public function is_valid_year($param, $request, $key) { $year = intval($param); $current_year = intval(date('Y')); return $year >= 2000 && $year <= $current_year; } public function is_valid_overwrite($param, $request, $key) { // Accept boolean or string representations if (is_bool($param)) { return true; } if (is_string($param)) { $param = strtolower($param); return in_array($param, array('1', 'true', 'yes'), true); } if (is_numeric($param)) { return in_array($param, array(1, 0), true); } return false; } private function interpret_boolean($value) { if (is_bool($value)) { return $value; } if (is_string($value)) { $value = strtolower($value); return in_array($value, array('1', 'true', 'yes'), true); } if (is_numeric($value)) { return (bool)$value; } return false; } private function hex_to_rgb($hex) { $hex = str_replace('#', '', $hex); if(strlen($hex) == 3) { $r = hexdec(substr($hex,0,1).substr($hex,0,1)); $g = hexdec(substr($hex,1,1).substr($hex,1,1)); $b = hexdec(substr($hex,2,1).substr($hex,2,1)); } else { $r = hexdec(substr($hex,0,2)); $g = hexdec(substr($hex,2,2)); $b = hexdec(substr($hex,4,2)); } return array($r, $g, $b); } private function get_last_month_data() { $last_month = date('Y-m', strtotime('last day of previous month')); return explode('-', $last_month); } private function get_top_albums($year, $month) { // Prepare placeholders for blacklisted artists $artist_placeholders = array_fill(0, count($this->blacklisted_artists), '%s'); $artist_placeholders_str = implode(', ', $artist_placeholders); // Prepare placeholders for blacklisted albums $album_placeholders = array_fill(0, count($this->blacklisted_albums), '%s'); $album_placeholders_str = implode(', ', $album_placeholders); // Initialize query parts $artist_condition = ''; $album_condition = ''; $query_params = array($year, $month); // Handle blacklisted artists if (!empty($this->blacklisted_artists)) { $artist_condition = "AND author NOT IN ($artist_placeholders_str)"; $query_params = array_merge($query_params, $this->blacklisted_artists); } // Handle blacklisted albums if (!empty($this->blacklisted_albums)) { $album_condition = "AND album_name NOT IN ($album_placeholders_str)"; $query_params = array_merge($query_params, $this->blacklisted_albums); } // Prepare the SQL query with updated ORDER BY in ranked_covers $query = $this->db->prepare( "WITH ranked_covers AS ( SELECT album_name, author, cover_url, id, -- Ensure id is selected for ordering ROW_NUMBER() OVER (PARTITION BY album_name, author ORDER BY id DESC) as rn FROM song_scrobbles WHERE cover_url != '' AND cover_url IS NOT NULL ), album_stats AS ( SELECT album_name, author, COUNT(*) as play_count, COUNT(DISTINCT song_name) as unique_song_count FROM song_scrobbles WHERE YEAR(time) = %d AND MONTH(time) = %d AND album_name != '' AND album_name IS NOT NULL $artist_condition $album_condition -- Exclude albums with '¥' symbol in album_name or author AND album_name NOT LIKE '%%¥%%' AND author NOT LIKE '%%¥%%' -- Exclude albums with Cyrillic characters in album_name or author AND album_name NOT REGEXP '[А-яЁё]' AND author NOT REGEXP '[А-яЁё]' GROUP BY album_name, author HAVING LENGTH(CONCAT(album_name, author)) <= 60 AND unique_song_count >= 1 ) SELECT a.album_name, a.author, r.cover_url, a.play_count, a.unique_song_count, CASE WHEN a.unique_song_count = 1 THEN 1.0 WHEN a.unique_song_count = 5 THEN 1.25 WHEN a.unique_song_count = 12 THEN 1.6 ELSE 1.0 + (a.unique_song_count * 0.05) END as multiplier, (a.play_count * CASE WHEN a.unique_song_count = 1 THEN 1.0 WHEN a.unique_song_count = 5 THEN 1.25 WHEN a.unique_song_count = 12 THEN 1.6 ELSE 1.0 + (a.unique_song_count * 0.05) END ) as rank_score FROM album_stats a LEFT JOIN ranked_covers r ON a.album_name = r.album_name AND a.author = r.author AND r.rn = 1 ORDER BY rank_score DESC LIMIT 25", ...$query_params ); return $this->db->get_results($query); } public function get_or_generate_image($request) { $month = $request->get_param('month'); $year = $request->get_param('year'); $overwrite = $request->get_param('overwrite'); // Retrieve 'overwrite' parameter // Convert 'overwrite' to boolean $overwrite = $this->interpret_boolean($overwrite); if ($month && $year) { $month = intval($month); $year = intval($year); // Validate that the date is not in the future $provided_date = strtotime("{$year}-{$month}-01"); $now = time(); if ($provided_date > $now) { return new WP_Error('invalid_date', 'The provided month and year cannot be in the future.', array('status' => 400)); } // Ensure the date is valid if (!checkdate($month, 1, $year)) { return new WP_Error('invalid_date', 'The provided month and year are invalid.', array('status' => 400)); } } else { list($year, $month) = $this->get_last_month_data(); } $cache_key = "top_albums_{$year}_{$month}"; // Check if we have a cached image $cached_image_id = get_option($cache_key); if ($overwrite && $cached_image_id) { // If overwrite is true and cached image exists // Delete the old attachment $deleted = wp_delete_attachment($cached_image_id, true); if ($deleted) { // Remove the cached option delete_option($cache_key); $cached_image_id = false; // Reset to indicate no cached image } else { return new WP_Error('deletion_failed', 'Failed to delete the existing image.', array('status' => 500)); } } if ($cached_image_id) { $attachment_path = get_attached_file($cached_image_id); if ($attachment_path && file_exists($attachment_path)) { // Serve the cached image directly as a response $image_data = file_get_contents($attachment_path); $mime_type = mime_content_type($attachment_path); // Send appropriate headers header('Content-Type: ' . $mime_type); header('Content-Length: ' . strlen($image_data)); header('Content-Disposition: inline; filename="' . basename($attachment_path) . '"'); // Prevent WordPress from sending additional content remove_filter('rest_pre_serve_request', 'rest_send_cors_headers'); echo $image_data; exit; } } // If no cached image, generate a new one $image_data = $this->generate_image($year, $month); // Save the generated image as an attachment $upload_dir = wp_upload_dir(); $filename = "top_albums_{$year}_{$month}.jpg"; $file_path = $upload_dir['path'] . '/' . $filename; file_put_contents($file_path, $image_data); $attachment = array( 'post_mime_type' => 'image/jpeg', 'post_title' => "Top Albums for {$year}-{$month}", 'post_content' => '', 'post_status' => 'inherit' ); $attach_id = wp_insert_attachment($attachment, $file_path); 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 attachment ID for future use update_option($cache_key, $attach_id); // Serve the newly generated image directly as a response $mime_type = mime_content_type($file_path); // Send appropriate headers header('Content-Type: ' . $mime_type); header('Content-Length: ' . strlen($image_data)); header('Content-Disposition: inline; filename="' . basename($file_path) . '"'); // Prevent WordPress from sending additional content remove_filter('rest_pre_serve_request', 'rest_send_cors_headers'); echo $image_data; exit; } private function generate_image($year, $month) { $albums = $this->get_top_albums($year, $month); $image_width = 2600; $image_height = 1680; $album_size = 300; $gap = 8; $left_margin = 100; $top_margin = 100; $image = imagecreatetruecolor($image_width, $image_height); $background_color = imagecolorallocate($image, $this->background_color[0], $this->background_color[1], $this->background_color[2]); imagefill($image, 0, 0, $background_color); $text_color = imagecolorallocate($image, $this->text_color[0], $this->text_color[1], $this->text_color[2]); // Add album covers (left side) for ($i = 0; $i < min(count($albums), 25); $i++) { $row = floor($i / 5); $col = $i % 5; $x = $left_margin + $col * ($album_size + $gap); $y = $top_margin + $row * ($album_size + $gap); // Detect the image type $image_type = @exif_imagetype($albums[$i]->cover_url); $album_cover = false; switch ($image_type) { case IMAGETYPE_JPEG: $album_cover = @imagecreatefromjpeg($albums[$i]->cover_url); break; case IMAGETYPE_PNG: $album_cover = @imagecreatefrompng($albums[$i]->cover_url); break; default: // Unsupported image type $album_cover = false; break; } if ($album_cover !== false) { // If the image is PNG, preserve transparency if ($image_type === IMAGETYPE_PNG) { imagealphablending($album_cover, true); imagesavealpha($album_cover, true); } $this->apply_retro_effects($album_cover); // If the main image has transparency and the album cover is PNG, preserve it if ($image_type === IMAGETYPE_PNG) { imagecopyresampled( $image, $album_cover, $x, $y, 0, 0, $album_size, $album_size, imagesx($album_cover), imagesy($album_cover) ); } else { imagecopyresampled( $image, $album_cover, $x, $y, 0, 0, $album_size, $album_size, imagesx($album_cover), imagesy($album_cover) ); } imagedestroy($album_cover); } else { // Draw placeholder rectangle $placeholder_color = imagecolorallocate($image, 100, 100, 100); imagefilledrectangle($image, $x, $y, $x + $album_size, $y + $album_size, $placeholder_color); } } // Add album titles (right side) $text_x = $left_margin + 5 * ($album_size + $gap) + 20; $text_y = $top_margin + 18; $line_height = 35; $font_size = 20; $group_spacing = 50; for ($i = 0; $i < min(count($albums), 25); $i++) { // Normalize album and author names to replace weird characters $normalized_author = $this->normalize_text($albums[$i]->author); $normalized_album = $this->normalize_text($albums[$i]->album_name); $album_text = "{$normalized_author} - {$normalized_album}"; $font = $this->is_japanese($album_text) ? $this->japanese_font : $this->western_font; $this->add_retro_text($image, $font_size, $text_x, $text_y, $text_color, $font, $album_text); if (($i + 1) % 5 == 0 && $i < 24) { $text_y += $line_height + $group_spacing; } else { $text_y += $line_height; } } $this->apply_overall_retro_effects($image); ob_start(); imagejpeg($image); $image_data = ob_get_clean(); imagedestroy($image); return $image_data; } /** * Normalize text by replacing accented or special characters with standard ones. * * @param string $text The input text to normalize. * @return string The normalized text. */ private function normalize_text($text) { // Check if the intl extension is loaded if (class_exists('Transliterator')) { $transliterator = \Transliterator::create('NFD; [:Nonspacing Mark:] Remove; NFC;'); if ($transliterator) { $text = $transliterator->transliterate($text); } } else { // Fallback to iconv if intl is not available $text = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); } // Replace any remaining non-ASCII characters $text = preg_replace('/[^\x20-\x7E]/', '', $text); return $text; } private function apply_retro_effects(&$image) { // Add noise $this->add_noise($image, 20); // Reduce color depth $this->reduce_color_depth($image); // Add slight blur // imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR); } private function add_noise(&$image, $amount) { $width = imagesx($image); $height = imagesy($image); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { if (rand(0, 100) < $amount) { $rgb = imagecolorat($image, $x, $y); $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; $noise = rand(-20, 20); $r = max(0, min(255, $r + $noise)); $g = max(0, min(255, $g + $noise)); $b = max(0, min(255, $b + $noise)); $color = imagecolorallocate($image, $r, $g, $b); imagesetpixel($image, $x, $y, $color); } } } } private function reduce_color_depth(&$image) { imagetruecolortopalette($image, false, 64); } private function add_retro_text(&$image, $font_size, $x, $y, $color, $font, $text) { // Add a slight shadow $shadow_color = imagecolorallocatealpha($image, 0, 0, 0, 75); imagettftext($image, $font_size, 0, $x + 1, $y + 1, $shadow_color, $font, $text); // Add main text with a slight glow $glow_color = imagecolorallocatealpha($image, 255, 255, 255, 75); imagettftext($image, $font_size, 0, $x - 1, $y - 1, $glow_color, $font, $text); imagettftext($image, $font_size, 0, $x, $y, $color, $font, $text); } private function apply_overall_retro_effects(&$image) { // Add scanlines $this->add_scanlines($image); // Add vignette effect $this->add_vignette($image); // Add slight color aberration $this->add_color_aberration($image); // Add subtle grain $this->add_grain($image); } private function add_scanlines(&$image) { $height = imagesy($image); $scanline_color = imagecolorallocatealpha($image, 0, 0, 0, 70); for ($y = 0; $y < $height; $y += 2) { imageline($image, 0, $y, imagesx($image), $y, $scanline_color); } } private function add_vignette(&$image) { $width = imagesx($image); $height = imagesy($image); $center_x = $width / 2; $center_y = $height / 2; $max_distance = sqrt($center_x * $center_x + $center_y * $center_y); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $distance = sqrt(pow($x - $center_x, 2) + pow($y - $center_y, 2)); $vignette = 1 - ($distance / $max_distance); $vignette = max(0, min(1, $vignette)); // Make vignette more subtle $vignette = 1 - (1 - $vignette) * 0.25; // Adjust 0.25 to control intensity $rgb = imagecolorat($image, $x, $y); $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; $r = (int)($r * $vignette); $g = (int)($g * $vignette); $b = (int)($b * $vignette); $color = imagecolorallocate($image, $r, $g, $b); imagesetpixel($image, $x, $y, $color); } } } private function add_color_aberration(&$image) { $width = imagesx($image); $height = imagesy($image); $aberration = 1; // Reduced from 2 to 1 for subtler effect $new_image = imagecreatetruecolor($width, $height); imagesavealpha($new_image, true); imagealphablending($new_image, false); $transparent = imagecolorallocatealpha($new_image, 0, 0, 0, 127); imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent); imagealphablending($new_image, true); for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($image, $x, $y); $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; // Shift red channel slightly to the left $red_x = max(0, $x - $aberration); $red_color = imagecolorallocate($new_image, $r, 0, 0); imagesetpixel($new_image, $red_x, $y, $red_color); // Keep green channel in place $green_color = imagecolorallocate($new_image, 0, $g, 0); imagesetpixel($new_image, $x, $y, $green_color); // Shift blue channel slightly to the right $blue_x = min($width - 1, $x + $aberration); $blue_color = imagecolorallocate($new_image, 0, 0, $b); imagesetpixel($new_image, $blue_x, $y, $blue_color); } } // Merge the aberrated image back onto the original with reduced opacity imagecopymerge($image, $new_image, 0, 0, 0, 0, $width, $height, 3); // Reduced from 50 to 30 imagedestroy($new_image); } private function add_grain(&$image) { $width = imagesx($image); $height = imagesy($image); $grain_amount = 8; // Adjust this value to control grain intensity (1-10 recommended) for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { if (rand(0, 100) < $grain_amount) { $rgb = imagecolorat($image, $x, $y); $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; $noise = rand(-10, 10); $r = max(0, min(255, $r + $noise)); $g = max(0, min(255, $g + $noise)); $b = max(0, min(255, $b + $noise)); $color = imagecolorallocate($image, $r, $g, $b); imagesetpixel($image, $x, $y, $color); } } } } private function is_japanese($text) { return preg_match('/[\x{4E00}-\x{9FBF}\x{3040}-\x{309F}\x{30A0}-\x{30FF}]/u', $text); } }