Updated to handle the separate post requests for artwork, and other now playing data, hope it works lol

This commit is contained in:
2025-06-21 22:58:53 -07:00
parent 1e75266545
commit d66f112971

View File

@ -2,84 +2,155 @@
/*
Plugin Name: Apple Music Now Playing API
Description: Accepts JSON now-playing updates via REST, lets user set a secure API key on the plugin page, a shortcode widget, that can be added to Footer
Version: 1.3.4
Version: 1.4.0
Author: Sophia Atkinson
*/
defined('ABSPATH') || exit;
defined("ABSPATH") || exit();
// === Register REST API endpoint ===
add_action('rest_api_init', function () {
register_rest_route('nowplaying/v1', '/update', [
'methods' => 'POST',
'callback' => 'nowplaying_api_handle_post',
'permission_callback' => '__return_true',
// === Register REST API endpoints ===
add_action("rest_api_init", function () {
register_rest_route("nowplaying/v1", "/update", [
"methods" => "POST",
"callback" => "nowplaying_api_handle_post",
"permission_callback" => "__return_true",
]);
register_rest_route("nowplaying/v1", "/update/artwork", [
"methods" => "POST",
"callback" => "nowplaying_api_handle_art_post",
"permission_callback" => "__return_true",
]);
});
function nowplaying_api_handle_post(WP_REST_Request $request) {
$stored_key = get_option('nowplaying_api_key');
$auth_header = $request->get_header('authorization');
function nowplaying_api_handle_post(WP_REST_Request $request)
{
$stored_key = get_option("nowplaying_api_key");
$auth_header = $request->get_header("authorization");
if (!$auth_header || $auth_header !== "Bearer $stored_key") {
return new WP_REST_Response('Forbidden: Invalid API key.', 403);
$expected_prefix = "bearer ";
$auth_header_normalized = strtolower(trim($auth_header ?? ""));
if (
!$auth_header ||
stripos($auth_header_normalized, $expected_prefix) !== 0 ||
substr(trim($auth_header), strlen($expected_prefix)) !== $stored_key
) {
return new WP_REST_Response("Forbidden: Invalid API key.", 403);
}
$data = json_decode($request->get_body(), true);
if ($data === null) {
return new WP_REST_Response('Invalid JSON.', 400);
return new WP_REST_Response("Invalid JSON.", 400);
}
$file_path = plugin_dir_path(__FILE__) . 'nowplaying.json';
$file_path = plugin_dir_path(__FILE__) . "nowplaying.json";
$written = file_put_contents(
$file_path,
json_encode($data, JSON_PRETTY_PRINT)
);
try {
$written = file_put_contents(
$file_path,
json_encode($data, JSON_PRETTY_PRINT)
);
if ($written === false) {
throw new Exception();
}
} catch (Exception $e) {
return new WP_REST_Response("Failed to write to nowplaying.json.", 500);
}
return new WP_REST_Response("Now Playing updated.", 200);
}
function nowplaying_api_handle_art_post(WP_REST_Request $request)
{
$stored_key = get_option("nowplaying_api_key");
$auth_header = $request->get_header("authorization");
if (!$auth_header || $auth_header !== "Bearer $stored_key") {
return new WP_REST_Response("Forbidden: Invalid API key.", 403);
}
$data = json_decode($request->get_body(), true);
if ($data === null) {
return new WP_REST_Response("Invalid JSON.", 400);
}
if (!isset($data["image"]) || !is_string($data["image"])) {
return new WP_REST_Response(
'Missing or invalid "image" property.',
400
);
}
$file_path = plugin_dir_path(__FILE__) . "nowplaying-art.json";
try {
$written = file_put_contents($file_path, json_encode($data, JSON_PRETTY_PRINT));
if (!$written) throw new Exception();
$written = file_put_contents(
$file_path,
json_encode($data, JSON_PRETTY_PRINT)
);
if ($written === false) {
throw new Exception();
}
} catch (Exception $e) {
return new WP_REST_Response('Failed to write to nowplaying.json.', 500);
return new WP_REST_Response(
"Failed to write to nowplaying-art.json.",
500
);
}
return new WP_REST_Response('Now Playing updated.', 200);
return new WP_REST_Response("Artwork updated.", 200);
}
// === Admin Settings Page ===
add_action('admin_menu', function () {
add_action("admin_menu", function () {
add_options_page(
'Now Playing API Settings',
'Now Playing API',
'manage_options',
'nowplaying-api',
'nowplaying_api_settings_page'
"Now Playing API Settings",
"Now Playing API",
"manage_options",
"nowplaying-api",
"nowplaying_api_settings_page"
);
});
function nowplaying_api_settings_page() {
if (!current_user_can('manage_options')) return;
function nowplaying_api_settings_page()
{
if (!current_user_can("manage_options")) {
return;
}
if (isset($_POST['nowplaying_api_key']) && check_admin_referer('nowplaying_save_key', 'nowplaying_nonce')) {
$new_key = sanitize_text_field($_POST['nowplaying_api_key']);
if (
isset($_POST["nowplaying_api_key"]) &&
check_admin_referer("nowplaying_save_key", "nowplaying_nonce")
) {
$new_key = sanitize_text_field($_POST["nowplaying_api_key"]);
if (strlen($new_key) === 64) {
update_option('nowplaying_api_key', $new_key);
update_option("nowplaying_api_key", $new_key);
echo '<div class="updated"><p>API key updated.</p></div>';
} else {
echo '<div class="error"><p>API key must be exactly 64 characters.</p></div>';
}
}
if (isset($_POST['generate_key']) && check_admin_referer('nowplaying_save_key', 'nowplaying_nonce')) {
if (
isset($_POST["generate_key"]) &&
check_admin_referer("nowplaying_save_key", "nowplaying_nonce")
) {
$generated_key = bin2hex(random_bytes(32));
update_option('nowplaying_api_key', $generated_key);
update_option("nowplaying_api_key", $generated_key);
echo '<div class="updated"><p>New API key generated.</p></div>';
}
$current_key = get_option('nowplaying_api_key');
$current_key = get_option("nowplaying_api_key");
if (!$current_key) {
$current_key = bin2hex(random_bytes(32));
update_option('nowplaying_api_key', $current_key);
update_option("nowplaying_api_key", $current_key);
}
$url = esc_url(site_url('/wp-json/nowplaying/v1/update'));
$url = esc_url(site_url("/wp-json/nowplaying/v1/update"));
?>
<div class="wrap">
<h1>Apple Music Now Playing API Settings</h1>
@ -87,33 +158,40 @@ function nowplaying_api_settings_page() {
<code><?php echo $url; ?></code>
<form method="post">
<?php wp_nonce_field('nowplaying_save_key', 'nowplaying_nonce'); ?>
<?php wp_nonce_field("nowplaying_save_key", "nowplaying_nonce"); ?>
<table class="form-table">
<tr>
<th><label for="nowplaying_api_key">Current API Key</label></th>
<td>
<input type="text" name="nowplaying_api_key" value="<?php echo esc_attr($current_key); ?>" class="regular-text" maxlength="64" />
<input type="text" name="nowplaying_api_key" value="<?php echo esc_attr(
$current_key
); ?>" class="regular-text" maxlength="64" />
<p class="description">Keep this key secret. <br> Must be 64 characters.</p>
</td>
</tr>
</table>
<?php submit_button('Save API Key'); ?>
<?php submit_button("Save API Key"); ?>
</form>
<form method="post" style="margin-top: 20px;">
<?php wp_nonce_field('nowplaying_save_key', 'nowplaying_nonce'); ?>
<?php submit_button('Generate New API Key', 'secondary', 'generate_key'); ?>
<?php wp_nonce_field("nowplaying_save_key", "nowplaying_nonce"); ?>
<?php submit_button(
"Generate New API Key",
"secondary",
"generate_key"
); ?>
</form>
</div>
<?php
}
// === Shortcode [now-playing-widget] ===
add_shortcode('now-playing-widget', function () {
$plugin_url = plugin_dir_url(__FILE__) . 'nowplaying.json';
add_shortcode("now-playing-widget", function () {
$plugin_url = plugin_dir_url(__FILE__) . "nowplaying.json";
$art_url = plugin_dir_url(__FILE__) . "nowplaying-art.json";
ob_start();
?>
<?php $css_url = plugin_dir_url(__FILE__) . 'assets/css/amnp-style.css'; ?>
<?php $css_url = plugin_dir_url(__FILE__) . "assets/css/amnp-style.css"; ?>
<link rel="stylesheet" type="text/css" href="<?php echo esc_url($css_url); ?>">
<div class="amnp hasjs" id="widget">
<img title="" id="cover" src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22150%22%20height%3D%22150%22%20viewBox%3D%220%200%20150%20150%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%231E1F22%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M31.66%2073.75h1.61q.59%200%201.03-.15t.73-.42q.29-.28.44-.68.14-.39.14-.88%200-.46-.14-.84-.15-.38-.43-.64-.29-.26-.73-.4t-1.04-.14h-1.61zm-2.15-5.82h3.76q1.16%200%202.01.27.84.28%201.4.76.55.49.82%201.17.26.68.26%201.49%200%20.84-.28%201.54t-.84%201.2q-.56.51-1.4.79t-1.97.28h-1.61v4.07h-2.15zm9.61-.32h1.98V79.5h-1.98zm8.29%209.77V76q-.85.04-1.44.14-.58.11-.93.28-.36.17-.51.39-.15.23-.15.49%200%20.52.31.74.31.23.8.23.61%200%201.05-.22.45-.22.87-.67m-4.17-4.31-.36-.63q1.42-1.3%203.41-1.3.72%200%201.29.24.57.23.96.65t.6%201.01q.2.58.2%201.28v5.18h-.9q-.28%200-.43-.08-.15-.09-.24-.34l-.17-.6q-.32.28-.61.5-.3.21-.62.35-.32.15-.68.22-.37.08-.81.08-.52%200-.96-.14t-.76-.42-.49-.7q-.18-.41-.18-.97%200-.31.11-.62.1-.3.34-.58.23-.28.61-.53.37-.25.92-.43.55-.19%201.28-.3.73-.12%201.66-.14v-.48q0-.83-.35-1.22-.35-.4-1.02-.4-.48%200-.79.11-.32.11-.56.25t-.43.26q-.2.11-.44.11-.21%200-.35-.11-.15-.11-.23-.25m13.83-1.78h1.58l-4.47%2010.47q-.09.2-.23.31t-.43.11h-1.47l1.54-3.3-3.32-7.59H52q.24%200%20.37.11.13.12.19.26l1.75%204.26q.09.21.15.43.06.21.11.44.07-.23.15-.45.07-.22.16-.43l1.64-4.25q.07-.16.22-.26.14-.11.33-.11m2.63%200h1.98v8.21H59.7zm2.26-2.39q0%20.26-.11.48-.1.22-.27.39-.18.17-.41.27t-.49.1-.49-.1-.39-.27q-.17-.17-.27-.39t-.1-.48.1-.5q.1-.23.27-.4.16-.16.39-.26t.49-.1.49.1.41.26q.17.17.27.4.11.24.11.5m3.45%202.75.14.65q.25-.25.52-.46.28-.21.59-.36.3-.15.66-.23.35-.09.76-.09.68%200%201.2.23t.86.64q.35.41.53.99.18.57.18%201.26v5.22h-1.97v-5.22q0-.76-.35-1.17-.34-.41-1.05-.41-.51%200-.96.23-.44.23-.84.63v5.94H63.7v-8.21h1.21q.38%200%20.5.36m10.31%203.55q.37%200%20.64-.1.28-.1.46-.28t.27-.42q.09-.25.09-.55%200-.61-.36-.96-.37-.36-1.1-.36-.72%200-1.09.36-.36.35-.36.96%200%20.29.09.54t.27.43.46.28q.27.1.63.1m2.24%204.67q0-.24-.15-.39-.14-.16-.39-.24t-.58-.12-.7-.06l-.77-.04q-.4-.02-.77-.06-.33.18-.54.43-.2.25-.2.57%200%20.22.11.41t.34.32q.24.14.61.21.38.08.92.08.56%200%20.96-.08.4-.09.66-.24.26-.14.38-.35.12-.2.12-.44m-.4-8.3h2.36v.74q0%20.35-.42.43l-.74.14q.17.42.17.92%200%20.61-.24%201.1-.25.5-.68.84t-1.02.53-1.27.19q-.24%200-.46-.02-.22-.03-.44-.07-.38.23-.38.52%200%20.25.22.37.23.11.61.16.37.05.85.06t.99.05q.5.04.98.14t.86.32q.37.21.6.59.23.37.23.95%200%20.55-.27%201.06t-.77.91q-.51.4-1.25.64-.74.25-1.69.25-.92%200-1.61-.18t-1.15-.48q-.45-.3-.68-.69-.22-.4-.22-.82%200-.58.35-.96.35-.39.95-.62-.32-.17-.52-.45-.19-.28-.19-.74%200-.18.07-.38.07-.19.2-.39.13-.19.33-.36t.47-.31q-.62-.33-.98-.89-.35-.56-.35-1.32%200-.6.24-1.1.25-.49.68-.84.44-.34%201.03-.53.6-.18%201.3-.18.53%200%201%20.11.46.1.84.31m15.67-3.64h1.89V79.5h-1.1q-.26%200-.43-.08-.17-.09-.33-.29l-6.04-7.71q.05.53.05.98v7.1h-1.9V67.93h1.13q.14%200%20.24.01.1.02.17.05.08.04.15.11.07.06.16.18l6.06%207.74-.04-.55q-.01-.27-.01-.51zm7.79%203.23q.91%200%201.66.3t1.28.84.82%201.33q.29.78.29%201.75%200%20.98-.29%201.76t-.82%201.34q-.53.55-1.28.84-.75.3-1.66.3-.92%200-1.67-.3-.75-.29-1.29-.84-.53-.56-.82-1.34t-.29-1.76q0-.97.29-1.75.29-.79.82-1.33.54-.54%201.29-.84t1.67-.3m0%206.94q1.02%200%201.52-.69.49-.69.49-2.01%200-1.33-.49-2.03-.5-.69-1.52-.69-1.04%200-1.54.7t-.5%202.02.5%202.01%201.54.69m10.66-6.81h1.97v8.21h-1.21q-.39%200-.49-.36l-.14-.66q-.5.52-1.11.83-.61.32-1.43.32-.67%200-1.19-.23t-.87-.64q-.35-.42-.53-.99t-.18-1.26v-5.22h1.98v5.22q0%20.75.34%201.16.35.41%201.05.41.51%200%20.96-.22.45-.23.85-.63zm6.66%208.34q-1.07%200-1.64-.61-.58-.6-.58-1.66v-4.59h-.84q-.16%200-.27-.1-.11-.11-.11-.31v-.79l1.32-.21.42-2.24q.04-.16.15-.25t.29-.09h1.02v2.58h2.19v1.41h-2.19v4.45q0%20.38.19.6t.51.22q.19%200%20.31-.05.13-.04.22-.09l.16-.09q.07-.05.15-.05t.14.05q.06.04.12.13l.59.96q-.43.36-.99.54-.56.19-1.16.19%22%2F%3E%3C%2Fsvg%3E" alt="Album cover" />
@ -124,17 +202,19 @@ add_shortcode('now-playing-widget', function () {
<div id="time-info"><span id="current-time">0:00</span><span id="total-time">0:00</span></div>
</div>
<?php
$json_path = plugin_dir_path(__FILE__) . 'nowplaying.json';
$nowplaying_data = @json_decode(file_get_contents($json_path), true) ?? [];
$np_details = esc_html($nowplaying_data['details'] ?? 'Not playing');
$np_artist = esc_html($nowplaying_data['state'] ?? '');
$np_status = isset($nowplaying_data['paused']) && $nowplaying_data['paused'] ? 'Paused' : 'Playing';
$np_cover = esc_attr($nowplaying_data['artworkBase64'] ?? '');
$np_album = esc_attr($nowplaying_data['album'] ?? '');
$json_path = plugin_dir_path(__FILE__) . "nowplaying.json";
$art_path = plugin_dir_path(__FILE__) . "nowplaying-art.json";
$nowplaying_data = @json_decode(file_get_contents($json_path), true) ?? [];
$art_data = @json_decode(file_get_contents($art_path), true) ?? [];
$np_details = esc_html($nowplaying_data["details"] ?? "Not playing");
$np_artist = esc_html($nowplaying_data["artist"] ?? "");
$np_status = isset($nowplaying_data["paused"]) && $nowplaying_data["paused"] ? "Paused" : "Playing";
$np_cover = esc_attr($art_data["image"] ?? ($nowplaying_data["artworkBase64"] ?? ""));
$np_album = esc_attr($nowplaying_data["album"] ?? "");
?>
<noscript>
<div class="amnp" id="widget">
<img src="<?php echo $np_cover ?: '...' ?>" alt="Album cover" title="<?php echo $np_album; ?>" />
<img src="<?php echo $np_cover ?: "data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22150%22%20height%3D%22150%22%20viewBox%3D%220%200%20150%20150%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%231E1F22%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M31.66%2073.75h1.61q.59%200%201.03-.15t.73-.42q.29-.28.44-.68.14-.39.14-.88%200-.46-.14-.84-.15-.38-.43-.64-.29-.26-.73-.4t-1.04-.14h-1.61zm-2.15-5.82h3.76q1.16%200%202.01.27.84.28%201.4.76.55.49.82%201.17.26.68.26%201.49%200%20.84-.28%201.54t-.84%201.2q-.56.51-1.4.79t-1.97.28h-1.61v4.07h-2.15zm9.61-.32h1.98V79.5h-1.98zm8.29%209.77V76q-.85.04-1.44.14-.58.11-.93.28-.36.17-.51.39-.15.23-.15.49%200%20.52.31.74.31.23.8.23.61%200%201.05-.22.45-.22.87-.67m-4.17-4.31-.36-.63q1.42-1.3%203.41-1.3.72%200%201.29.24.57.23.96.65t.6%201.01q.2.58.2%201.28v5.18h-.9q-.28%200-.43-.08-.15-.09-.24-.34l-.17-.6q-.32.28-.61.5-.3.21-.62.35-.32.15-.68.22-.37.08-.81.08-.52%200-.96-.14t-.76-.42-.49-.7q-.18-.41-.18-.97%200-.31.11-.62.1-.3.34-.58.23-.28.61-.53.37-.25.92-.43.55-.19%201.28-.3.73-.12%201.66-.14v-.48q0-.83-.35-1.22-.35-.4-1.02-.4-.48%200-.79.11-.32.11-.56.25t-.43.26q-.2.11-.44.11-.21%200-.35-.11-.15-.11-.23-.25m13.83-1.78h1.58l-4.47%2010.47q-.09.2-.23.31t-.43.11h-1.47l1.54-3.3-3.32-7.59H52q.24%200%20.37.11.13.12.19.26l1.75%204.26q.09.21.15.43.06.21.11.44.07-.23.15-.45.07-.22.16-.43l1.64-4.25q.07-.16.22-.26.14-.11.33-.11m2.63%200h1.98v8.21H59.7zm2.26-2.39q0%20.26-.11.48-.1.22-.27.39-.18.17-.41.27t-.49.1-.49-.1-.39-.27q-.17-.17-.27-.39t-.1-.48.1-.5q.1-.23.27-.4.16-.16.39-.26t.49-.1.49.1.41.26q.17.17.27.4.11.24.11.5m3.45%202.75.14.65q.25-.25.52-.46.28-.21.59-.36.3-.15.66-.23.35-.09.76-.09.68%200%201.2.23t.86.64q.35.41.53.99.18.57.18%201.26v5.22h-1.97v-5.22q0-.76-.35-1.17-.34-.41-1.05-.41-.51%200-.96.23-.44.23-.84.63v5.94H63.7v-8.21h1.21q.38%200%20.5.36m10.31%203.55q.37%200%20.64-.1.28-.1.46-.28t.27-.42q.09-.25.09-.55%200-.61-.36-.96-.37-.36-1.1-.36-.72%200-1.09.36-.36.35-.36.96%200%20.29.09.54t.27.43.46.28q.27.1.63.1m2.24%204.67q0-.24-.15-.39-.14-.16-.39-.24t-.58-.12-.7-.06l-.77-.04q-.4-.02-.77-.06-.33.18-.54.43-.2.25-.2.57%200%20.22.11.41t.34.32q.24.14.61.21.38.08.92.08.56%200%20.96-.08.4-.09.66-.24.26-.14.38-.35.12-.2.12-.44m-.4-8.3h2.36v.74q0%20.35-.42.43l-.74.14q.17.42.17.92%200%20.61-.24%201.1-.25.5-.68.84t-1.02.53-1.27.19q-.24%200-.46-.02-.22-.03-.44-.07-.38.23-.38.52%200%20.25.22.37.23.11.61.16.37.05.85.06t.99.05q.5.04.98.14t.86.32q.37.21.6.59.23.37.23.95%200%20.55-.27%201.06t-.77.91q-.51.4-1.25.64-.74.25-1.69.25-.92%200-1.61-.18t-1.15-.48q-.45-.3-.68-.69-.22-.4-.22-.82%200-.58.35-.96.35-.39.95-.62-.32-.17-.52-.45-.19-.28-.19-.74%200-.18.07-.38.07-.19.2-.39.13-.19.33-.36t.47-.31q-.62-.33-.98-.89-.35-.56-.35-1.32%200-.6.24-1.1.25-.49.68-.84.44-.34%201.03-.53.6-.18%201.3-.18.53%200%201%20.11.46.1.84.31m15.67-3.64h1.89V79.5h-1.1q-.26%200-.43-.08-.17-.09-.33-.29l-6.04-7.71q.05.53.05.98v7.1h-1.9V67.93h1.13q.14%200%20.24.01.1.02.17.05.08.04.15.11.07.06.16.18l6.06%207.74-.04-.55q-.01-.27-.01-.51zm7.79%203.23q.91%200%201.66.3t1.28.84.82%201.33q.29.78.29%201.75%200%20.98-.29%201.76t-.82%201.34q-.53.55-1.28.84-.75.3-1.66.3-.92%200-1.67-.3-.75-.29-1.29-.84-.53-.56-.82-1.34t-.29-1.76q0-.97.29-1.75.29-.79.82-1.33.54-.54%201.29-.84t1.67-.3m0%206.94q1.02%200%201.52-.69.49-.69.49-2.01%200-1.33-.49-2.03-.5-.69-1.52-.69-1.04%200-1.54.7t-.5%202.02.5%202.01%201.54.69m10.66-6.81h1.97v8.21h-1.21q-.39%200-.49-.36l-.14-.66q-.5.52-1.11.83-.61.32-1.43.32-.67%200-1.19-.23t-.87-.64q-.35-.42-.53-.99t-.18-1.26v-5.22h1.98v5.22q0%20.75.34%201.16.35.41%201.05.41.51%200%20.96-.22.45-.23.85-.63zm6.66%208.34q-1.07%200-1.64-.61-.58-.6-.58-1.66v-4.59h-.84q-.16%200-.27-.1-.11-.11-.11-.31v-.79l1.32-.21.42-2.24q.04-.16.15-.25t.29-.09h1.02v2.58h2.19v1.41h-2.19v4.45q0%20.38.19.6t.51.22q.19%200%20.31-.05.13-.04.22-.09l.16-.09q.07-.05.15-.05t.14.05q.06.04.12.13l.59.96q-.43.36-.99.54-.56.19-1.16.19%22%2F%3E%3C%2Fsvg%3E"; ?>" alt="Album cover" title="<?php echo $np_album; ?>" />
<div title="<?php echo $np_details; ?>"><?php echo $np_details; ?></div>
<div title="<?php echo $np_artist; ?>"><?php echo $np_artist; ?></div>
<div><?php echo $np_status; ?></div>
@ -142,84 +222,136 @@ add_shortcode('now-playing-widget', function () {
</noscript>
<script>
let lastData={}, intervalId=null;
function formatTime(s){if(!s||isNaN(s))return"0:00";let m=Math.floor(s/60),sec=Math.floor(s%60);return m+":"+(sec<10?"0":"")+sec}
function deepEqual(a,b){return JSON.stringify(a)===JSON.stringify(b)}
async function fetchNowPlaying(){
if(document.hidden)return;
try{
const res=await fetch('<?php echo esc_url($plugin_url); ?>?'+Date.now(),{cache:'no-store'});
if(!res.ok)throw new Error('Network error');
const data=await res.json();
if(deepEqual(data,lastData))return;
lastData=data;
const {details="Not playing", state="", paused, artworkBase64='', currentTime=0, duration=0, album=""} = data;
document.getElementById('title').textContent = details;
document.getElementById('title').title = details;
document.getElementById('artist').textContent = state;
document.getElementById('artist').title = state;
if (artworkBase64) document.getElementById('cover').src = artworkBase64;
document.getElementById('cover').alt = details + " cover";
document.getElementById('cover').title = album;
document.getElementById('status').textContent = paused ? "Paused" : "Playing";
document.getElementById('progress-bar').style.width = (duration > 0 ? (currentTime / duration) * 100 : 0) + '%';
document.getElementById('current-time').textContent = formatTime(currentTime);
document.getElementById('total-time').textContent = formatTime(duration);
}catch(err){console.error("Fetch failed:",err)}
let lastNowPlaying = null;
let lastSongKey = null;
let lastArtImage = null;
let intervalId = null;
function formatTime(s){if(!s||isNaN(s)){return "0:00"}let m=Math.floor(s/60),sec=Math.floor(s%60);return m+":"+(sec<10?"0":"")+sec}
async function fetchJson(url) {
try {
const res = await fetch(url + '?' + Date.now(), { cache: 'no-store' });
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
return await res.json();
} catch (e) {
console.error('Fetch error:', e);
return null;
}
function startFetching(){if(intervalId===null){fetchNowPlaying();intervalId=setInterval(fetchNowPlaying,1000)}}
function stopFetching(){if(intervalId!==null){clearInterval(intervalId);intervalId=null}}
const widget=document.getElementById('widget');
let widgetVisible=false;
const observer=new IntersectionObserver((entries)=>{entries.forEach(entry=>{widgetVisible=entry.isIntersecting;updateFetchingState()})},{threshold:0.1});
observer.observe(widget);
document.addEventListener('visibilitychange',updateFetchingState);
function updateFetchingState(){if(!document.hidden&&widgetVisible){startFetching()}else{stopFetching()}}
updateFetchingState();
</script>
<?php
return ob_get_clean();
});
}
async function updateWidget() {
if (document.hidden) return;
const nowPlaying = await fetchJson('<?php echo esc_url($plugin_url); ?>');
if (!nowPlaying) return;
const currentSongKey = (nowPlaying.details || '') + '|' + (nowPlaying.album || '');
if (JSON.stringify(nowPlaying) !== JSON.stringify(lastNowPlaying)) {
lastNowPlaying = nowPlaying;
// === Register Customizer Widget Support with Title Option ===
class Now_Playing_Widget extends WP_Widget {
public function __construct() {
document.getElementById('title').textContent = nowPlaying.details || 'Not playing';
document.getElementById('title').title = nowPlaying.details || '';
document.getElementById('artist').textContent = nowPlaying.state || '';
document.getElementById('artist').title = nowPlaying.state || '';
document.getElementById('status').textContent = nowPlaying.paused ? 'Paused' : 'Playing';
const currentTime = typeof nowPlaying.currentTime === 'number' ? nowPlaying.currentTime : 0;
const duration = typeof nowPlaying.duration === 'number' ? nowPlaying.duration : 0;
document.getElementById('progress-bar').style.width =
(duration > 0 ? (currentTime / duration) * 100 : 0) + '%';
document.getElementById('current-time').textContent = formatTime(nowPlaying.currentTime);
document.getElementById('total-time').textContent = formatTime(nowPlaying.duration);
}
if (currentSongKey !== lastSongKey) {
lastSongKey = currentSongKey;
const artData = await fetchJson('<?php echo esc_url($art_url); ?>');
let image = null;
if (artData?.image) {
image = artData.image;
} else if (nowPlaying.artworkBase64) {
image = nowPlaying.artworkBase64;
}
if (image && image !== lastArtImage) {
lastArtImage = image;
const cover = document.getElementById('cover');
cover.src = image;
cover.alt = nowPlaying.details || 'Album cover';
cover.title = nowPlaying.album || '';
}
}
}
function startLoop() {
if (intervalId === null) {
updateWidget();
intervalId = setInterval(updateWidget, 1000);
}
}
function stopLoop(){if (intervalId !== null) {clearInterval(intervalId);intervalId = null;}}
const widget = document.getElementById('widget');
let isVisible = false;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {isVisible = entry.isIntersecting;updateFetchState();});
}, { threshold: 0.1 });
function updateFetchState() {
if (!document.hidden && isVisible) {
startLoop();
} else {
stopLoop();
}
}
observer.observe(widget);
document.addEventListener('visibilitychange', updateFetchState);
updateFetchState();
</script>
<?php return ob_get_clean();
}); // === Register Customizer Widget Support with Title Option ===
class Now_Playing_Widget extends WP_Widget
{
public function __construct()
{
parent::__construct(
'now_playing_widget',
__('Now Playing Widget', 'nowplaying'),
['description' => __('Displays the current Apple Music song from your API.', 'nowplaying')]
"now_playing_widget",
__("Now Playing Widget", "nowplaying"),
[
"description" => __(
"Displays the current Apple Music song from your API.",
"nowplaying"
),
]
);
}
public function widget($args, $instance) {
$title = apply_filters('widget_title', $instance['title'] ?? '');
echo $args['before_widget'];
public function widget($args, $instance)
{
$title = apply_filters("widget_title", $instance["title"] ?? "");
echo $args["before_widget"];
if (!empty($title)) {
echo $args['before_title'] . esc_html($title) . $args['after_title'];
echo $args["before_title"] .
esc_html($title) .
$args["after_title"];
}
echo do_shortcode('[now-playing-widget]');
echo $args['after_widget'];
echo do_shortcode("[now-playing-widget]");
echo $args["after_widget"];
}
public function form($instance) {
$title = esc_attr($instance['title'] ?? '');
?>
public function form($instance)
{
$title = esc_attr($instance["title"] ?? ""); ?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php _e('Title:'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>"
name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text"
<label for="<?php echo esc_attr(
$this->get_field_id("title")
); ?>"><?php _e("Title:"); ?></label>
<input class="widefat" id="<?php echo esc_attr(
$this->get_field_id("title")
); ?>"
name="<?php echo esc_attr(
$this->get_field_name("title")
); ?>" type="text"
value="<?php echo $title; ?>" />
</p>
<?php
}
public function update($new_instance, $old_instance) {
public function update($new_instance, $old_instance)
{
$instance = [];
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
$instance["title"] = sanitize_text_field($new_instance["title"] ?? "");
return $instance;
}
}
add_action('widgets_init', function () {
register_widget('Now_Playing_Widget');
add_action("widgets_init", function () {
register_widget("Now_Playing_Widget");
});