1
0
antisocial-scrobble/album-collage-generator.php

627 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// Prevent direct access to the file
if (!defined('ABSPATH')) {
exit;
}
class AlbumCollageGenerator {
private $db;
private $background_color;
private $text_color;
private $western_font;
private $japanese_font;
private $blacklisted_artists;
public function __construct() {
global $wpdb;
$this->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);
}
}