initial commit
This commit is contained in:
commit
d00edefc95
529
album-collage-generator.php
Normal file
529
album-collage-generator.php
Normal file
@ -0,0 +1,529 @@
|
||||
<?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();
|
BIN
assets/BIZUDPMincho-Regular.ttf
Normal file
BIN
assets/BIZUDPMincho-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/co1251n.ttf
Normal file
BIN
assets/co1251n.ttf
Normal file
Binary file not shown.
BIN
assets/font.ttf
Normal file
BIN
assets/font.ttf
Normal file
Binary file not shown.
20
plugin.php
Normal file
20
plugin.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: antisocial-scrobble
|
||||
* Description: scrobble your music, with album cover support
|
||||
* Version: 20241011
|
||||
* Author: antisocial.moe
|
||||
*/
|
||||
|
||||
// Prevent direct access to the file
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Include the separate plugins
|
||||
require_once(plugin_dir_path(__FILE__) . 'album-collage-generator.php');
|
||||
require_once(plugin_dir_path(__FILE__) . 'scrobble-handler.php');
|
||||
|
||||
// Initialize both classes
|
||||
new AlbumCollageGenerator();
|
||||
new ScrobbleHandler();
|
72
readme.md
Normal file
72
readme.md
Normal file
@ -0,0 +1,72 @@
|
||||
# antisocial-scrobble
|
||||
|
||||
i made this wordpress plugin because im a snowflake that suffers from chronic not-invented-here syndrome. it lets you scrobble your music with album cover support and generates monthly album collages.
|
||||
|
||||
## installation
|
||||
|
||||
- throw the plugin into `wp-content/plugins/`
|
||||
- activate it in the wordpress admin
|
||||
|
||||
## usage
|
||||
|
||||
### scrobbling
|
||||
|
||||
there's a REST API endpoint to scrobble your songs.
|
||||
|
||||
**endpoint:** `POST /wp-json/image-scrobble/v1/scrobble`
|
||||
|
||||
**parameters:**
|
||||
|
||||
- `song_name` (required)
|
||||
- `author` (required)
|
||||
- `album_name` (optional)
|
||||
- `cover_url` (required if `album_name` isn't provided)
|
||||
- `length_seconds` (optional)
|
||||
|
||||
**authentication:**
|
||||
|
||||
you need to include an `Authorization` header: `Bearer YOUR_AUTH_KEY`.
|
||||
|
||||
the `AUTH_KEY` is generated when you activate the plugin. you can find it or change it under `settings > image scrobble` in the wordpress admin.
|
||||
|
||||
**example:**
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://yourwebsite.com/wp-json/image-scrobble/v1/scrobble' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer YOUR_AUTH_KEY' \
|
||||
-d '{
|
||||
"song_name": "Song Title",
|
||||
"author": "Artist Name",
|
||||
"album_name": "Album Title",
|
||||
"cover_url": "https://example.com/cover.jpg",
|
||||
"length_seconds": 240
|
||||
}'
|
||||
```
|
||||
|
||||
### generating album collage
|
||||
|
||||
you can generate a collage of your top albums for any month.
|
||||
|
||||
**endpoint:** `GET /wp-json/top-albums/v1/last-month`
|
||||
|
||||
**parameters:**
|
||||
|
||||
- `month` (optional)
|
||||
- `year` (optional)
|
||||
- `overwrite` (optional, default `false`)
|
||||
|
||||
if you don't provide `month` and `year`, it defaults to last month.
|
||||
|
||||
**example:**
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://yourwebsite.com/wp-json/top-albums/v1/last-month?month=9&year=2023'
|
||||
```
|
||||
|
||||
this returns the collage image. if you want to force regenerate it (maybe you added more scrobbles), set `overwrite=true`.
|
||||
|
||||
### notes
|
||||
|
||||
- the collage ignores some blacklisted artists. you can change that in the code if you care (`$this->blacklisted_artists`).
|
||||
- the plugin might break in unexpected ways. use at your own risk.
|
293
scrobble-handler.php
Normal file
293
scrobble-handler.php
Normal file
@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
// Prevent direct access to the file
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Include necessary files
|
||||
require_once(ABSPATH . 'wp-admin/includes/image.php');
|
||||
require_once(ABSPATH . 'wp-admin/includes/file.php');
|
||||
require_once(ABSPATH . 'wp-admin/includes/media.php');
|
||||
|
||||
class ScrobbleHandler {
|
||||
private $auth_key;
|
||||
private $option_name = 'image_scrobble_auth_key';
|
||||
|
||||
public function __construct() {
|
||||
$this->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'));
|
||||
}
|
||||
|
||||
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')); // New parameter
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Retrieve the URL of the newly created attachment
|
||||
$image_url = wp_get_attachment_url($attach_id);
|
||||
|
||||
// Return a successful response with the image URL
|
||||
return new WP_REST_Response(
|
||||
array('url' => $image_url),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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';
|
||||
$cover_url = isset($data['cover_url']) ? $data['cover_url'] : null;
|
||||
$album_name = isset($data['album_name']) ? $data['album_name'] : null;
|
||||
|
||||
// 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
|
||||
$song_name_lower = strtolower($data['song_name']);
|
||||
$album_name_lower = strtolower($album_name);
|
||||
$author_lower = strtolower($data['author']);
|
||||
|
||||
// Query to find the most recent matching entry
|
||||
$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
|
||||
);
|
||||
|
||||
$cover_url = $wpdb->get_var($query);
|
||||
|
||||
// If no matching entry is found, return an error
|
||||
if (!$cover_url) {
|
||||
return new WP_Error(
|
||||
'missing_cover_url',
|
||||
'Cover URL not provided and no existing entry found for the given album and author',
|
||||
array('status' => 400)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the scrobble data into the database
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
array(
|
||||
'song_name' => $data['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')
|
||||
);
|
||||
}
|
||||
|
||||
public function register_settings() {
|
||||
register_setting('image_scrobble_settings', $this->option_name);
|
||||
}
|
||||
|
||||
public function settings_page() {
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Image Scrobble Settings</h1>
|
||||
<form method="post" action="options.php">
|
||||
<?php
|
||||
settings_fields('image_scrobble_settings');
|
||||
do_settings_sections('image_scrobble_settings');
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr valign="top">
|
||||
<th scope="row">AUTH_KEY</th>
|
||||
<td>
|
||||
<input type="text" name="<?php echo $this->option_name; ?>" value="<?php echo esc_attr(get_option($this->option_name)); ?>" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the plugin
|
||||
new ScrobbleHandler();
|
Loading…
Reference in New Issue
Block a user