summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--player.css184
-rw-r--r--player.js147
-rw-r--r--readme.md2
-rw-r--r--spinitron-now-playing.php294
4 files changed, 627 insertions, 0 deletions
diff --git a/player.css b/player.css
new file mode 100644
index 0000000..294a914
--- /dev/null
+++ b/player.css
@@ -0,0 +1,184 @@
+/* Player container */
+.spinitron-player {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: #1a1a1a;
+ color: white;
+ padding: 1rem;
+ z-index: 10000;
+ box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
+ display: flex;
+ gap: 2rem;
+ align-items: center;
+ transition: transform 0.3s ease;
+}
+
+.spinitron-player.hidden {
+ transform: translateY(100%);
+}
+
+/* Play button */
+.spinitron-play-button {
+ background: #4CAF50;
+ border: none;
+ padding: 0.8rem 1.5rem;
+ border-radius: 5px;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center; /* Center content horizontally */
+ gap: 0.5rem;
+ min-width: 120px;
+ transition: background 0.3s ease;
+ text-align: center; /* Ensure text is centered */
+}
+
+.spinitron-play-button:hover {
+ background: #45a049;
+}
+
+/* Metadata container */
+.spinitron-metadata {
+ flex-grow: 1;
+ display: flex;
+ gap: 2rem;
+ overflow: hidden;
+}
+
+/* Song and show info */
+.spinitron-song-info,
+.spinitron-show-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ min-width: 0;
+}
+
+/* Status labels */
+.spinitron-now-playing {
+ font-size: 0.9em;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ transition: color 0.3s ease;
+}
+
+/* Title text */
+.spinitron-title {
+ font-weight: bold;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.spinitron-title a,
+#spinitronArtistAlbum a,
+#spinitronEpisodeName a,
+#spinitronSongTitle,
+#spinitronShowTitle {
+ color: #ccc !important;
+}
+
+/* Artist/album and episode info */
+#spinitronArtistAlbum,
+#spinitronEpisodeName {
+ font-size: 0.9em;
+ color: #ccc;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.spinitron-title a,
+#spinitronArtistAlbum a,
+#spinitronEpisodeName a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.spinitron-title a:hover,
+#spinitronArtistAlbum a:hover,
+#spinitronEpisodeName a:hover {
+ text-decoration: underline;
+}
+
+.listen-now a {
+ color: #4CAF50 !important;
+ font-weight: bold;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .spinitron-player {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
+ padding: 1rem;
+ }
+
+ .spinitron-metadata {
+ flex-direction: column;
+ gap: 1rem;
+ width: 100%;
+ }
+
+ .spinitron-play-button {
+ width: 100%; /* Make button full width on mobile */
+ justify-content: center; /* Keep content centered */
+ }
+}
+
+
+
+
+/* Recently played marquee */
+#spin-recent {
+ background: #222;
+ color: #fff;
+ padding: 10px 0;
+ overflow: hidden;
+ white-space: nowrap;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+}
+
+#ticker {
+ display: flex;
+ animation: ticker-scroll 20s linear infinite;
+}
+
+#ticker p {
+ margin: 0;
+ display: flex;
+ gap: 2rem;
+}
+
+#ticker span {
+ display: inline-block;
+ padding-right: 2rem;
+}
+
+#ticker a {
+ color: #4CAF50;
+ text-decoration: none;
+}
+
+#ticker a:hover {
+ text-decoration: underline;
+}
+
+.recent-link {
+ color: inherit;
+ text-decoration: none;
+}
+
+.recent-link:hover {
+ text-decoration: underline;
+}
+
+/* Marquee animation */
+@keyframes ticker-scroll {
+ 0% { transform: translateX(100%); }
+ 100% { transform: translateX(-100%); }
+}
diff --git a/player.js b/player.js
new file mode 100644
index 0000000..d26d284
--- /dev/null
+++ b/player.js
@@ -0,0 +1,147 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const audio = document.getElementById('spinitronAudioPlayer');
+ const playButton = document.getElementById('spinitronPlayButton');
+ let updateInterval;
+ let statusInterval;
+ let currentSpinId = null;
+ let currentEndTime = null;
+
+ // Update metadata from the API
+ function updateMetadata() {
+ try {
+ const response = await fetch(spinitronConfig.endpoint);
+ const data = await response.json();
+
+ if (data.status === 'ok') {
+ // Update song metadata links
+ const songTitle = document.getElementById('spinitronSongTitle');
+ songTitle.textContent = data.spin_data.song || '-';
+ songTitle.href = `https://spinitron.com/WVAU/spin/${data.spin_data.id}`;
+
+ const artistLink = document.getElementById('spinitronArtist');
+ artistLink.textContent = data.spin_data.artist || '-';
+ artistLink.href = `https://spinitron.com/WVAU/artist/${encodeURIComponent(data.spin_data.artist)}`;
+
+ const releaseLink = document.getElementById('spinitronRelease');
+ releaseLink.textContent = data.spin_data.release || '-';
+ releaseLink.href = `https://spinitron.com/WVAU/release/${encodeURIComponent(data.spin_data.release)}`;
+
+ // Update show metadata links
+ const showTitle = document.getElementById('spinitronShowTitle');
+ showTitle.textContent = data.playlist_data.title || '-';
+ showTitle.href = `https://spinitron.com/WVAU/playlist/${data.playlist_data.id}`;
+
+ const episodeLink = document.getElementById('spinitronEpisode');
+ episodeLink.textContent = data.playlist_data.episode_name || '-';
+ episodeLink.href = `https://spinitron.com/WVAU/playlist/${data.playlist_data.id}`;
+ }
+ } catch (error) {
+ console.error('Error fetching metadata:', error);
+ }
+ }
+
+ // Update the "NOW PLAYING" / "LAST PLAYED" status labels
+ function updateStatusLabels() {
+ if (!currentEndTime) return;
+
+ const now = Date.now();
+ const endTime = currentEndTime.getTime();
+ const secondsSinceEnd = (now - endTime) / 1000;
+ const labels = document.querySelectorAll('.spinitron-now-playing');
+
+ labels.forEach(label => {
+ if (secondsSinceEnd > 60) {
+ label.textContent = 'LAST PLAYED (?)';
+ label.style.color = '#ff4444';
+ } else if (secondsSinceEnd > 30) {
+ label.textContent = 'LAST PLAYED';
+ label.style.color = '#ffa500';
+ } else {
+ label.textContent = 'NOW PLAYING';
+ label.style.color = '#888';
+ }
+ });
+ }
+
+ // Toggle audio playback
+ function togglePlayback() {
+ if (audio.paused) {
+ audio.play().catch(error => console.error('Audio playback failed:', error));
+ playButton.innerHTML = '<span>⏸</span> <span class="play-text">Pause</span>';
+ updateMetadata();
+ updateInterval = setInterval(updateMetadata, spinitronConfig.cacheDuration);
+ } else {
+ audio.pause();
+ playButton.innerHTML = '<span>▶</span> <span class="play-text">Play</span>';
+ clearInterval(updateInterval);
+ }
+ }
+
+ // Initial metadata check
+ updateMetadata();
+
+ // Event listeners
+ playButton.addEventListener('click', togglePlayback);
+
+ // Handle audio ending
+ audio.addEventListener('ended', () => {
+ playButton.innerHTML = '<span>▶</span> <span class="play-text">Play</span>';
+ clearInterval(updateInterval);
+ });
+
+ // Auto-update metadata when tab becomes visible
+ document.addEventListener('visibilitychange', () => {
+ if (!document.hidden) {
+ updateMetadata();
+ updateStatusLabels();
+ }
+ });
+});
+
+
+
+
+// Recently Played Marquee Updates
+async function updateRecentlyPlayed() {
+ try {
+ const response = await fetch(spinitronConfig.endpoint);
+ const data = await response.json();
+
+ if (data.status === 'ok' && data.spin_data) {
+ const spin = data.spin_data;
+ const playlist = data.playlist_data;
+ const template = document.getElementById('recent-spin-template').innerHTML;
+ const container = document.getElementById('recent-spin-container');
+
+ const recentSpinHtml = template
+ .replace(/{time}/g, new Date(spin.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }).toLowerCase())
+ .replace(/{song}/g, spin.song || 'Unknown Song')
+ .replace(/{artist}/g, spin.artist || 'Unknown Artist')
+ .replace(/{release}/g, spin.release || 'Unknown Release')
+ .replace(/{spin_id}/g, spin.id)
+ .replace(/{artist_id}/g, encodeURIComponent(spin.artist))
+ .replace(/{release_id}/g, encodeURIComponent(spin.release));
+
+ container.innerHTML = recentSpinHtml;
+ }
+ } catch (error) {
+ console.error('Error updating recently played:', error);
+ }
+}
+
+// Start updating the recently played marquee
+if (document.getElementById('spin-recent')) {
+ updateRecentlyPlayed(); // Initial update
+ setInterval(updateRecentlyPlayed, spinitronConfig.cacheDuration); // Periodic updates
+}
+
+// Pause on hover
+const ticker = document.getElementById('ticker');
+if (ticker) {
+ ticker.addEventListener('mouseenter', () => {
+ ticker.style.animationPlayState = 'paused';
+ });
+ ticker.addEventListener('mouseleave', () => {
+ ticker.style.animationPlayState = 'running';
+ });
+} \ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..3238384
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,2 @@
+# Spinitron Now Playing
+Wordpress plugin that automatically pulls ***now playing*** data from Spinitron and registers a shortcode.
diff --git a/spinitron-now-playing.php b/spinitron-now-playing.php
new file mode 100644
index 0000000..53b51ce
--- /dev/null
+++ b/spinitron-now-playing.php
@@ -0,0 +1,294 @@
+<?php
+/*
+Plugin Name: Spinitron Now Playing
+Description: Fetches and caches now playing data from Spinitron API. Provides a REST endpoint at /wp-json/spinitron/v1/now-playing.
+Version: 1.0
+Author: Miles Wilson
+*/
+
+// Prevent direct access
+defined( 'ABSPATH' ) || exit;
+
+// Add admin menu
+add_action( 'admin_menu', 'spinitron_add_admin_menu' );
+add_action( 'admin_init', 'spinitron_settings_init' );
+
+function spinitron_add_admin_menu() {
+ add_options_page(
+ 'Spinitron Now Playing Settings',
+ 'Spinitron Now Playing',
+ 'manage_options',
+ 'spinitron-now-playing',
+ 'spinitron_options_page_html'
+ );
+}
+
+function spinitron_settings_init() {
+ register_setting( 'spinitron', 'spinitron_api_key', array(
+ 'sanitize_callback' => 'sanitize_text_field',
+ ) );
+ register_setting( 'spinitron', 'spinitron_cache_duration', array(
+ 'sanitize_callback' => 'absint',
+ ) );
+ register_setting( 'spinitron', 'spinitron_audio_stream_url', array(
+ 'sanitize_callback' => 'esc_url_raw',
+ ) );
+
+ add_settings_section(
+ 'spinitron_section',
+ 'API Settings',
+ 'spinitron_section_callback',
+ 'spinitron'
+ );
+
+ add_settings_field(
+ 'spinitron_api_key',
+ 'API Key',
+ 'spinitron_api_key_render',
+ 'spinitron',
+ 'spinitron_section'
+ );
+
+ add_settings_field(
+ 'spinitron_cache_duration',
+ 'Cache Duration (seconds)',
+ 'spinitron_cache_duration_render',
+ 'spinitron',
+ 'spinitron_section'
+ );
+
+ add_settings_field(
+ 'spinitron_audio_stream_url',
+ 'Audio Stream URL',
+ 'spinitron_audio_stream_url_render',
+ 'spinitron',
+ 'spinitron_section'
+ );
+}
+
+function spinitron_api_key_render() {
+ $api_key = get_option( 'spinitron_api_key' );
+ echo '<input type="text" name="spinitron_api_key" value="' . esc_attr( $api_key ) . '" class="regular-text">';
+}
+
+function spinitron_cache_duration_render() {
+ $cache_duration = get_option( 'spinitron_cache_duration', 60 );
+ echo '<input type="number" name="spinitron_cache_duration" value="' . esc_attr( $cache_duration ) . '" min="1">';
+}
+
+function spinitron_section_callback() {
+ echo '<p>Enter your Spinitron API credentials and cache settings below:</p>';
+}
+
+function spinitron_options_page_html() {
+ if ( ! current_user_can( 'manage_options' ) ) return;
+ ?>
+ <div class="wrap">
+ <h1>Spinitron Now Playing Settings</h1>
+ <form action="options.php" method="post">
+ <?php
+ settings_fields( 'spinitron' );
+ do_settings_sections( 'spinitron' );
+ submit_button( 'Save Settings' );
+ ?>
+ </form>
+ </div>
+ <?php
+}
+
+// Register REST API endpoint
+add_action( 'rest_api_init', function () {
+ register_rest_route( 'spinitron/v1', '/now-playing', array(
+ 'methods' => 'GET',
+ 'callback' => 'spinitron_now_playing_endpoint',
+ 'permission_callback' => '__return_true'
+ ) );
+} );
+
+function spinitron_now_playing_endpoint( $request ) {
+ $api_key = get_option( 'spinitron_api_key' );
+ $cache_duration = get_option( 'spinitron_cache_duration', 60 );
+ $cache_duration = max( 1, (int) $cache_duration );
+
+ // Check API key
+ if ( empty( $api_key ) ) {
+ return new WP_Error( 'api_key_missing', 'Spinitron API key is not configured.', array( 'status' => 500 ) );
+ }
+
+ // Check cache
+ $cached_data = get_transient( 'spinitron_now_playing_data' );
+ if ( $cached_data !== false ) {
+ return spinitron_create_response( $cached_data );
+ }
+
+ // Fetch new data
+ $spin_url = 'https://spinitron.com/api/spins?count=1&sort=start&direction=desc';
+ $args = array(
+ 'headers' => array(
+ 'Authorization' => 'Bearer ' . $api_key,
+ 'Accept' => 'application/json',
+ 'User-Agent' => 'WordPress Spinitron Now Playing',
+ ),
+ 'timeout' => 5,
+ );
+
+ // Fetch spin data
+ $spin_response = wp_remote_get( $spin_url, $args );
+ if ( is_wp_error( $spin_response ) ) {
+ $error_data = array(
+ 'status' => 'error',
+ 'error' => 'API request failed: ' . $spin_response->get_error_message(),
+ 'last_updated' => time(),
+ );
+ set_transient( 'spinitron_now_playing_data', $error_data, $cache_duration );
+ return spinitron_create_response( $error_data );
+ }
+
+ $spin_body = wp_remote_retrieve_body( $spin_response );
+ $spin_data = json_decode( $spin_body, true );
+
+ if ( empty( $spin_data['items'] ) ) {
+ $error_data = array(
+ 'status' => 'error',
+ 'error' => 'No spins found in response',
+ 'last_updated' => time(),
+ );
+ set_transient( 'spinitron_now_playing_data', $error_data, $cache_duration );
+ return spinitron_create_response( $error_data );
+ }
+
+ $spin_item = $spin_data['items'][0];
+
+ // Fetch playlist data
+ if ( empty( $spin_item['_links']['playlist']['href'] ) ) {
+ $error_data = array(
+ 'status' => 'error',
+ 'error' => 'Spin data missing playlist link',
+ 'spin_data' => $spin_item,
+ 'last_updated' => time(),
+ );
+ set_transient( 'spinitron_now_playing_data', $error_data, $cache_duration );
+ return spinitron_create_response( $error_data );
+ }
+
+ $playlist_url = $spin_item['_links']['playlist']['href'];
+ $playlist_response = wp_remote_get( $playlist_url, $args );
+
+ if ( is_wp_error( $playlist_response ) ) {
+ $error_data = array(
+ 'status' => 'error',
+ 'error' => 'Playlist request failed: ' . $playlist_response->get_error_message(),
+ 'spin_data' => $spin_item,
+ 'last_updated' => time(),
+ );
+ set_transient( 'spinitron_now_playing_data', $error_data, $cache_duration );
+ return spinitron_create_response( $error_data );
+ }
+
+ $playlist_body = wp_remote_retrieve_body( $playlist_response );
+ $playlist_data = json_decode( $playlist_body, true );
+
+ // Successful response
+ $result = array(
+ 'status' => 'ok',
+ 'spin_data' => $spin_item,
+ 'playlist_data' => $playlist_data,
+ 'error' => null,
+ 'last_updated' => time(),
+ );
+
+ set_transient( 'spinitron_now_playing_data', $result, $cache_duration );
+ return spinitron_create_response( $result );
+}
+
+function spinitron_create_response( $data ) {
+ $response = new WP_REST_Response( $data );
+ $response->header( 'Access-Control-Allow-Origin', '*' );
+ $response->header( 'Cache-Control', 'no-cache, max-age=0' );
+ return $response;
+}
+
+// Add shortcode for player
+add_shortcode('spinitron_player', 'spinitron_player_shortcode');
+
+function spinitron_player_shortcode() {
+ $audio_url = get_option('spinitron_audio_stream_url');
+
+ ob_start(); ?>
+ <div class="spinitron-player">
+ <button class="spinitron-play-button" id="spinitronPlayButton">
+ <span>▶</span> <span class="play-text">Play</span>
+ </button>
+
+ <div class="spinitron-metadata" id="spinitronMetadata">
+ <div class="spinitron-song-info">
+ <div class="spinitron-now-playing">NOW PLAYING</div>
+ <div class="spinitron-title">
+ <a href="https://spinitron.com/WVAU/" target="_blank" id="spinitronSongTitle" style="color: #ccc">-</a>
+ </div>
+ <div id="spinitronArtistAlbum">
+ by <a href="https://spinitron.com/WVAU/" target="_blank" id="spinitronArtist" style="color: #ccc">-</a>
+ from <a href="https://spinitron.com/WVAU/" target="_blank" id="spinitronRelease" style="color: #ccc">-</a>
+ </div>
+ </div>
+
+ <div class="spinitron-show-info">
+ <div class="spinitron-now-playing">CURRENT SHOW</div>
+ <div class="spinitron-title">
+ <a href="https://spinitron.com/WVAU/" target="_blank" id="spinitronShowTitle" style="color: #ccc">-</a>
+ </div>
+ <div id="spinitronEpisodeName">
+ <a href="https://spinitron.com/WVAU/" target="_blank" id="spinitronEpisode" style="color: #ccc">-</a>
+ </div>
+ </div>
+ </div>
+
+ <audio id="spinitronAudioPlayer" src="<?php echo esc_url($audio_url); ?>"></audio>
+ </div>
+ <?php
+ return ob_get_clean();
+}
+
+function spinitron_audio_stream_url_render() {
+ $audio_url = get_option('spinitron_audio_stream_url');
+ echo '<input type="url" name="spinitron_audio_stream_url" value="' . esc_url($audio_url) . '" class="regular-text">';
+}
+
+// Enqueue assets
+add_action('wp_enqueue_scripts', 'spinitron_enqueue_assets');
+
+function spinitron_enqueue_assets() {
+ wp_enqueue_style('spinitron-player', plugins_url('player.css', __FILE__));
+ wp_enqueue_script('spinitron-player', plugins_url('player.js', __FILE__), array(), false, true);
+
+ wp_localize_script('spinitron-player', 'spinitronConfig', array(
+ 'endpoint' => rest_url('spinitron/v1/now-playing'),
+ 'cacheDuration' => get_option('spinitron_cache_duration', 60) * 1000
+ ));
+}
+
+// Recently Played Marquee Shortcode
+add_shortcode('spinitron_recently_played', 'spinitron_recently_played_shortcode');
+
+function spinitron_recently_played_shortcode() {
+ ob_start(); ?>
+ <div id="spin-recent">
+ <div id="ticker">
+ <p>
+ <span id="recent-spin-template" style="display: none;">
+ <a href="https://spinitron.com/WVAU/spin/{spin_id}" target="_blank" class="recent-link">
+ {time} <em>“{song}”</em>
+ </a>
+ by <a href="https://spinitron.com/WVAU/artist/{artist_id}" target="_blank" class="recent-link"><b>{artist}</b></a>
+ from <a href="https://spinitron.com/WVAU/release/{release_id}" target="_blank" class="recent-link">{release}</a>
+ </span>
+ <span id="recent-spin-container"></span>
+ <span class="listen-now">
+ <em><a href="<?php echo esc_url(get_option('spinitron_audio_stream_url')); ?>" target="_blank">LISTEN NOW!</a></em>
+ </span>
+ </p>
+ </div>
+ </div>
+ <?php
+ return ob_get_clean();
+}