Initial commit
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Sophia Atkinson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
61
assets/css/amnp-style.css
Normal file
61
assets/css/amnp-style.css
Normal file
@ -0,0 +1,61 @@
|
||||
.amnp #widget {
|
||||
background: #2a2a2a;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.amnp #title {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.amnp #artist {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 12px;
|
||||
color: #bbb;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.amnp #status {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.amnp #cover {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.amnp #progress-container {
|
||||
background: #444;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 12px 0 6px 0;
|
||||
}
|
||||
|
||||
.amnp #progress-bar {
|
||||
background: #F5A9B8;
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
transition: width 0.3s linear;
|
||||
}
|
||||
|
||||
.amnp #time-info {
|
||||
font-size: 0.8rem;
|
||||
color: #aaa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: monospace;
|
||||
}
|
225
nowplaying-api.php
Normal file
225
nowplaying-api.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?php
|
||||
/*
|
||||
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
|
||||
Author: Sophia Atkinson
|
||||
*/
|
||||
|
||||
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',
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$data = json_decode($request->get_body(), true);
|
||||
|
||||
if ($data === null) {
|
||||
return new WP_REST_Response('Invalid JSON.', 400);
|
||||
}
|
||||
|
||||
$file_path = plugin_dir_path(__FILE__) . 'nowplaying.json';
|
||||
|
||||
try {
|
||||
$written = file_put_contents($file_path, json_encode($data, JSON_PRETTY_PRINT));
|
||||
if (!$written) 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);
|
||||
}
|
||||
|
||||
// === Admin Settings Page ===
|
||||
add_action('admin_menu', function () {
|
||||
add_options_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;
|
||||
|
||||
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);
|
||||
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')) {
|
||||
$generated_key = bin2hex(random_bytes(32));
|
||||
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');
|
||||
if (!$current_key) {
|
||||
$current_key = bin2hex(random_bytes(32));
|
||||
update_option('nowplaying_api_key', $current_key);
|
||||
}
|
||||
|
||||
$url = esc_url(site_url('/wp-json/nowplaying/v1/update'));
|
||||
?>
|
||||
<div class="wrap">
|
||||
<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>
|
||||
<code><?php echo $url; ?></code>
|
||||
|
||||
<form method="post">
|
||||
<?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" />
|
||||
<p class="description">Keep this key secret. <br> Must be 64 characters.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?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'); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// === Shortcode [now-playing-widget] ===
|
||||
add_shortcode('now-playing-widget', function () {
|
||||
$plugin_url = plugin_dir_url(__FILE__) . 'nowplaying.json';
|
||||
ob_start();
|
||||
?>
|
||||
<?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" />
|
||||
<div title="" id="title">Loading...</div>
|
||||
<div title="" id="artist"></div>
|
||||
<div id="status"></div>
|
||||
<div id="progress-container"><div id="progress-bar"></div></div>
|
||||
<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'] ?? '');
|
||||
?>
|
||||
<noscript>
|
||||
<div class="amnp" id="widget">
|
||||
<img src="<?php echo $np_cover ?: '...' ?>" 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>
|
||||
</div>
|
||||
</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)}
|
||||
}
|
||||
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();
|
||||
});
|
||||
|
||||
// === 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')]
|
||||
);
|
||||
}
|
||||
|
||||
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 do_shortcode('[now-playing-widget]');
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
|
||||
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"
|
||||
value="<?php echo $title; ?>" />
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance) {
|
||||
$instance = [];
|
||||
$instance['title'] = sanitize_text_field($new_instance['title'] ?? '');
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
add_action('widgets_init', function () {
|
||||
register_widget('Now_Playing_Widget');
|
||||
});
|
Reference in New Issue
Block a user