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

530 lines
20 KiB
PHP
Raw Normal View History

2024-10-11 19:38:13 +02:00
<?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'); // placeholder values
}
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
$placeholders = array_fill(0, count($this->blacklisted_artists), '%s');
$placeholders_str = implode(', ', $placeholders);
// Merge all parameters for $wpdb->prepare
$query_params = array($year, $month);
$query_params = array_merge($query_params, $this->blacklisted_artists);
$query = $this->db->prepare(
"WITH ranked_covers AS (
SELECT
album_name,
author,
cover_url,
ROW_NUMBER() OVER (PARTITION BY album_name, author ORDER BY time 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
-- Exclude blacklisted artists
AND author NOT IN ($placeholders_str)
-- 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);
$album_cover = @imagecreatefromjpeg($albums[$i]->cover_url);
if ($album_cover !== false) {
$this->apply_retro_effects($album_cover);
imagecopyresampled($image, $album_cover, $x, $y, 0, 0, $album_size, $album_size, imagesx($album_cover), imagesy($album_cover));
imagedestroy($album_cover);
} else {
$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++) {
$album_text = $albums[$i]->author . ' - ' . $albums[$i]->album_name;
$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;
}
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);
}
}
new AlbumCollageGenerator();