Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
a9ff5434ed
|
|||
676fed9f2c
|
|||
d66f112971
|
|||
1e75266545
|
86
README.MD
Normal file
86
README.MD
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# AmNP For Wordpress
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* REST API endpoint for now playing data & separate endpoint for artwork updates
|
||||||
|
* Bearer authentication
|
||||||
|
* Admin panel for key management
|
||||||
|
* Shortcode: `[now-playing-widget]`
|
||||||
|
* Sidebar/Footer widget support
|
||||||
|
* Graceful fallback for NoScript peeps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REST API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
| --------------------------------------- | ------ | ---------------------------- |
|
||||||
|
| `/wp-json/nowplaying/v1/update` | `POST` | Updates now playing metadata |
|
||||||
|
| `/wp-json/nowplaying/v1/update/artwork` | `POST` | Updates album artwork |
|
||||||
|
|
||||||
|
All requests must include an `Authorization` header in the format:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer 73757065725f7365637572655f746f6b656e
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Setup
|
||||||
|
|
||||||
|
After installing the plugin:
|
||||||
|
|
||||||
|
1. Go to **Settings -> Now Playing API**
|
||||||
|
2. Set or generate a secure 64-character API key
|
||||||
|
3. Use the displayed endpoint URLs for POST requests from the [AmNP Extention](https://git.oldgate.org/amnp/ext-amnp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Shortcode
|
||||||
|
|
||||||
|
Add the widget to any post or page using:
|
||||||
|
|
||||||
|
```
|
||||||
|
[now-playing-widget]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget
|
||||||
|
|
||||||
|
Go to **Appearance → Widgets**, then drag the **Now Playing Widget** into your sidebar or footer.
|
||||||
|
|
||||||
|
### Example of Widget
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
The widget displays:
|
||||||
|
|
||||||
|
* Album artwork (and when you hover over it, it shows the album name!)
|
||||||
|
* Song title
|
||||||
|
* Artist name
|
||||||
|
* Playback status
|
||||||
|
* Progress bar and time
|
||||||
|
|
||||||
|
If JavaScript is disabled, a static fallback will display the last known song and artwork.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Output
|
||||||
|
|
||||||
|
Two JSON files are saved to the plugin directory:
|
||||||
|
|
||||||
|
* `nowplaying.json` -> song metadata
|
||||||
|
* `nowplaying-art.json` -> album artwork (base64 or URL)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
@ -2,121 +2,203 @@
|
|||||||
/*
|
/*
|
||||||
Plugin Name: Apple Music Now Playing API
|
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
|
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.1
|
||||||
Author: Sophia Atkinson
|
Author: Sophia Atkinson
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined("ABSPATH") || exit();
|
||||||
|
|
||||||
// === Register REST API endpoint ===
|
// === Register REST API endpoints ===
|
||||||
add_action('rest_api_init', function () {
|
add_action("rest_api_init", function () {
|
||||||
register_rest_route('nowplaying/v1', '/update', [
|
register_rest_route("nowplaying/v1", "/update", [
|
||||||
'methods' => 'POST',
|
"methods" => "POST",
|
||||||
'callback' => 'nowplaying_api_handle_post',
|
"callback" => "nowplaying_api_handle_post",
|
||||||
'permission_callback' => '__return_true',
|
"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) {
|
function nowplaying_api_handle_post(WP_REST_Request $request)
|
||||||
$stored_key = get_option('nowplaying_api_key');
|
{
|
||||||
$auth_header = $request->get_header('authorization');
|
$stored_key = get_option("nowplaying_api_key");
|
||||||
|
$auth_header = $request->get_header("authorization");
|
||||||
|
|
||||||
if (!$auth_header || $auth_header !== "Bearer $stored_key") {
|
$expected_prefix = "bearer ";
|
||||||
return new WP_REST_Response('Forbidden: Invalid API key.', 403);
|
$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);
|
$data = json_decode($request->get_body(), true);
|
||||||
|
|
||||||
if ($data === null) {
|
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 {
|
try {
|
||||||
$written = file_put_contents($file_path, json_encode($data, JSON_PRETTY_PRINT));
|
$written = file_put_contents(
|
||||||
if (!$written) throw new Exception();
|
$file_path,
|
||||||
|
json_encode($data, JSON_PRETTY_PRINT)
|
||||||
|
);
|
||||||
|
if ($written === false) {
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} 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 ===
|
// === Admin Settings Page ===
|
||||||
add_action('admin_menu', function () {
|
add_action("admin_menu", function () {
|
||||||
add_options_page(
|
add_options_page(
|
||||||
'Now Playing API Settings',
|
"Now Playing API Settings",
|
||||||
'Now Playing API',
|
"Now Playing API",
|
||||||
'manage_options',
|
"manage_options",
|
||||||
'nowplaying-api',
|
"nowplaying-api",
|
||||||
'nowplaying_api_settings_page'
|
"nowplaying_api_settings_page"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function nowplaying_api_settings_page() {
|
function nowplaying_api_settings_page()
|
||||||
if (!current_user_can('manage_options')) return;
|
{
|
||||||
|
if (!current_user_can("manage_options")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($_POST['nowplaying_api_key']) && check_admin_referer('nowplaying_save_key', 'nowplaying_nonce')) {
|
if (
|
||||||
$new_key = sanitize_text_field($_POST['nowplaying_api_key']);
|
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) {
|
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>';
|
echo '<div class="updated"><p>API key updated.</p></div>';
|
||||||
} else {
|
} else {
|
||||||
echo '<div class="error"><p>API key must be exactly 64 characters.</p></div>';
|
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));
|
$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>';
|
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) {
|
if (!$current_key) {
|
||||||
$current_key = bin2hex(random_bytes(32));
|
$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"));
|
||||||
|
$artwork_url = esc_url(site_url("/wp-json/nowplaying/v1/update/artwork"));
|
||||||
?>
|
?>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>Apple Music Now Playing API Settings</h1>
|
<h1>Apple Music Now Playing API Settings</h1>
|
||||||
<p>This is the key you will use for the web extension to communicate with this server. The POST address is:</p>
|
<p>This is the key you will use for the web extension to communicate with this server. The POST addresses are:</p>
|
||||||
<code><?php echo $url; ?></code>
|
Now playing Endpoint: <code><?php echo $url; ?></code>
|
||||||
|
<br />
|
||||||
|
Artwork Endpoint: <code><?php echo $artwork_url; ?></code>
|
||||||
|
|
||||||
<form method="post">
|
<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">
|
<table class="form-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th><label for="nowplaying_api_key">Current API Key</label></th>
|
<th><label for="nowplaying_api_key">Current API Key</label></th>
|
||||||
<td>
|
<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>
|
<p class="description">Keep this key secret. <br> Must be 64 characters.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<?php submit_button('Save API Key'); ?>
|
<?php submit_button("Save API Key"); ?>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="post" style="margin-top: 20px;">
|
<form method="post" style="margin-top: 20px;">
|
||||||
<?php wp_nonce_field('nowplaying_save_key', 'nowplaying_nonce'); ?>
|
<?php wp_nonce_field("nowplaying_save_key", "nowplaying_nonce"); ?>
|
||||||
<?php submit_button('Generate New API Key', 'secondary', 'generate_key'); ?>
|
<?php submit_button(
|
||||||
|
"Generate New API Key",
|
||||||
|
"secondary",
|
||||||
|
"generate_key"
|
||||||
|
); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Shortcode [now-playing-widget] ===
|
// === Shortcode [now-playing-widget] ===
|
||||||
add_shortcode('now-playing-widget', function () {
|
add_shortcode("now-playing-widget", function () {
|
||||||
$plugin_url = plugin_dir_url(__FILE__) . 'nowplaying.json';
|
$plugin_url = plugin_dir_url(__FILE__) . "nowplaying.json";
|
||||||
|
$art_url = plugin_dir_url(__FILE__) . "nowplaying-art.json";
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
?>
|
||||||
<?php $css_url = plugin_dir_url(__FILE__) . 'assets/css/amnp-style.css'; ?>
|
<?php $placeholder_image = 'data:image/svg+xml;base64,' . base64_encode(file_get_contents('https://placehold.co/150/1E1F22/FFF?text=Nothing%20playing')); ?>
|
||||||
|
<?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); ?>">
|
<link rel="stylesheet" type="text/css" href="<?php echo esc_url($css_url); ?>">
|
||||||
<div class="amnp hasjs" id="widget">
|
<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" />
|
<img title="" id="cover" src="<?php echo $placeholder_image; ?>" alt="Album cover" />
|
||||||
<div title="" id="title">Loading...</div>
|
<div title="" id="title">Loading...</div>
|
||||||
<div title="" id="artist"></div>
|
<div title="" id="artist"></div>
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
@ -124,17 +206,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 id="time-info"><span id="current-time">0:00</span><span id="total-time">0:00</span></div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
$json_path = plugin_dir_path(__FILE__) . 'nowplaying.json';
|
$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) ?? [];
|
$nowplaying_data = @json_decode(file_get_contents($json_path), true) ?? [];
|
||||||
$np_details = esc_html($nowplaying_data['details'] ?? 'Not playing');
|
$art_data = @json_decode(file_get_contents($art_path), true) ?? [];
|
||||||
$np_artist = esc_html($nowplaying_data['state'] ?? '');
|
$np_details = esc_html($nowplaying_data["details"] ?? "Not playing");
|
||||||
$np_status = isset($nowplaying_data['paused']) && $nowplaying_data['paused'] ? 'Paused' : 'Playing';
|
$np_artist = esc_html($nowplaying_data["artist"] ?? "");
|
||||||
$np_cover = esc_attr($nowplaying_data['artworkBase64'] ?? '');
|
$np_status = isset($nowplaying_data["paused"]) && $nowplaying_data["paused"] ? "Paused" : "Playing";
|
||||||
$np_album = esc_attr($nowplaying_data['album'] ?? '');
|
$np_cover = esc_attr($art_data["image"] ?? ($nowplaying_data["artworkBase64"] ?? ""));
|
||||||
|
$np_album = esc_attr($nowplaying_data["album"] ?? "");
|
||||||
?>
|
?>
|
||||||
<noscript>
|
<noscript>
|
||||||
<div class="amnp" id="widget">
|
<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 ?: $placeholder_image; ?>" alt="Album cover" title="<?php echo $np_album; ?>" />
|
||||||
<div title="<?php echo $np_details; ?>"><?php echo $np_details; ?></div>
|
<div title="<?php echo $np_details; ?>"><?php echo $np_details; ?></div>
|
||||||
<div title="<?php echo $np_artist; ?>"><?php echo $np_artist; ?></div>
|
<div title="<?php echo $np_artist; ?>"><?php echo $np_artist; ?></div>
|
||||||
<div><?php echo $np_status; ?></div>
|
<div><?php echo $np_status; ?></div>
|
||||||
@ -142,84 +226,136 @@ add_shortcode('now-playing-widget', function () {
|
|||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let lastData={}, intervalId=null;
|
let lastNowPlaying = 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}
|
let lastSongKey = null;
|
||||||
function deepEqual(a,b){return JSON.stringify(a)===JSON.stringify(b)}
|
let lastArtImage = null;
|
||||||
async function fetchNowPlaying(){
|
let intervalId = null;
|
||||||
if(document.hidden)return;
|
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 {
|
try {
|
||||||
const res=await fetch('<?php echo esc_url($plugin_url); ?>?'+Date.now(),{cache:'no-store'});
|
const res = await fetch(url + '?' + Date.now(), { cache: 'no-store' });
|
||||||
if(!res.ok)throw new Error('Network error');
|
if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
|
||||||
const data=await res.json();
|
return await res.json();
|
||||||
if(deepEqual(data,lastData))return;
|
} catch (e) {
|
||||||
lastData=data;
|
console.error('Fetch error:', e);
|
||||||
const {details="Not playing", state="", paused, artworkBase64='', currentTime=0, duration=0, album=""} = data;
|
return null;
|
||||||
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)}
|
|
||||||
}
|
}
|
||||||
function startFetching(){if(intervalId===null){fetchNowPlaying();intervalId=setInterval(fetchNowPlaying,1000)}}
|
}
|
||||||
function stopFetching(){if(intervalId!==null){clearInterval(intervalId);intervalId=null}}
|
async function updateWidget() {
|
||||||
const widget=document.getElementById('widget');
|
if (document.hidden) return;
|
||||||
let widgetVisible=false;
|
const nowPlaying = await fetchJson('<?php echo esc_url($plugin_url); ?>');
|
||||||
const observer=new IntersectionObserver((entries)=>{entries.forEach(entry=>{widgetVisible=entry.isIntersecting;updateFetchingState()})},{threshold:0.1});
|
if (!nowPlaying) return;
|
||||||
observer.observe(widget);
|
const currentSongKey = (nowPlaying.details || '') + '|' + (nowPlaying.album || '');
|
||||||
document.addEventListener('visibilitychange',updateFetchingState);
|
if (JSON.stringify(nowPlaying) !== JSON.stringify(lastNowPlaying)) {
|
||||||
function updateFetchingState(){if(!document.hidden&&widgetVisible){startFetching()}else{stopFetching()}}
|
lastNowPlaying = nowPlaying;
|
||||||
updateFetchingState();
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
return ob_get_clean();
|
|
||||||
});
|
|
||||||
|
|
||||||
// === Register Customizer Widget Support with Title Option ===
|
document.getElementById('title').textContent = nowPlaying.details || 'Not playing';
|
||||||
class Now_Playing_Widget extends WP_Widget {
|
document.getElementById('title').title = nowPlaying.details || '';
|
||||||
public function __construct() {
|
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(
|
parent::__construct(
|
||||||
'now_playing_widget',
|
"now_playing_widget",
|
||||||
__('Now Playing Widget', 'nowplaying'),
|
__("Now Playing Widget", "nowplaying"),
|
||||||
['description' => __('Displays the current Apple Music song from your API.', 'nowplaying')]
|
[
|
||||||
|
"description" => __(
|
||||||
|
"Displays the current Apple Music song from your API.",
|
||||||
|
"nowplaying"
|
||||||
|
),
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
public function widget($args, $instance)
|
||||||
public function widget($args, $instance) {
|
{
|
||||||
$title = apply_filters('widget_title', $instance['title'] ?? '');
|
$title = apply_filters("widget_title", $instance["title"] ?? "");
|
||||||
echo $args['before_widget'];
|
echo $args["before_widget"];
|
||||||
if (!empty($title)) {
|
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 do_shortcode("[now-playing-widget]");
|
||||||
echo $args['after_widget'];
|
echo $args["after_widget"];
|
||||||
}
|
}
|
||||||
|
public function form($instance)
|
||||||
public function form($instance) {
|
{
|
||||||
$title = esc_attr($instance['title'] ?? '');
|
$title = esc_attr($instance["title"] ?? ""); ?>
|
||||||
?>
|
|
||||||
<p>
|
<p>
|
||||||
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php _e('Title:'); ?></label>
|
<label for="<?php echo esc_attr(
|
||||||
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>"
|
$this->get_field_id("title")
|
||||||
name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text"
|
); ?>"><?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; ?>" />
|
value="<?php echo $title; ?>" />
|
||||||
</p>
|
</p>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
public function update($new_instance, $old_instance)
|
||||||
public function update($new_instance, $old_instance) {
|
{
|
||||||
$instance = [];
|
$instance = [];
|
||||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
$instance["title"] = sanitize_text_field($new_instance["title"] ?? "");
|
||||||
return $instance;
|
return $instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
add_action("widgets_init", function () {
|
||||||
add_action('widgets_init', function () {
|
register_widget("Now_Playing_Widget");
|
||||||
register_widget('Now_Playing_Widget');
|
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user