From 6cdddc774a67a070996d4e226bd3ddb731de239d Mon Sep 17 00:00:00 2001 From: codey Date: Wed, 24 Sep 2025 22:40:48 -0400 Subject: [PATCH] Enhance AudioPlayer functionality with improved error handling, toast notifications, and refined queue management. Update typeahead options handling, adjust track metadata defaults, and optimize DJ controls layout. Introduce new methods for handling song requests and queue operations. --- src/components/AudioPlayer.jsx | 314 +++++++++++++++++++++------------ 1 file changed, 203 insertions(+), 111 deletions(-) diff --git a/src/components/AudioPlayer.jsx b/src/components/AudioPlayer.jsx index 94fca78..61de8d2 100644 --- a/src/components/AudioPlayer.jsx +++ b/src/components/AudioPlayer.jsx @@ -7,7 +7,8 @@ import { Dialog } from "primereact/dialog"; import { AutoComplete } from "primereact/autocomplete"; import { DataTable } from "primereact/datatable"; import { Column } from "primereact/column"; - +import { toast } from "react-toastify"; +import { ENVIRONMENT } from "../config"; import { API_URL } from "@/config"; import { authFetch } from "@/utils/authFetch"; import { requireAuthHook } from "@/hooks/requireAuthHook"; @@ -65,8 +66,9 @@ export default function Player({ user }) { }); const data = await response.json(); console.debug('Typeahead: response data', data); - setTypeaheadOptions(data.options || []); - console.debug('Typeahead: setTypeaheadOptions', data.options || []); + // Accept bare array or { options: [...] } + setTypeaheadOptions(Array.isArray(data) ? data : data.options || []); + console.debug('Typeahead: setTypeaheadOptions', Array.isArray(data) ? data : data.options || []); } catch (error) { console.error('Typeahead: error', error); setTypeaheadOptions([]); @@ -74,9 +76,9 @@ export default function Player({ user }) { }; console.debug('AudioPlayer user:', user); if (user) { - console.debug('isAuthenticated:', user.isAuthenticated); + console.debug('isAuthenticated:', user); console.debug('roles:', user.roles); - console.debug('isDJ:', user.isAuthenticated && Array.isArray(user.roles) && user.roles.includes('dj')); + console.debug('isDJ:', user && Array.isArray(user.roles) && user.roles.includes('dj')); } const theme = useHtmlThemeAttr(); @@ -219,6 +221,7 @@ export default function Player({ user }) { // ...existing code... + // ...existing code... @@ -274,8 +277,8 @@ export default function Player({ user }) { // If station changed while request was in-flight, ignore the response if (requestStation !== activeStationRef.current) return; - if (trackData.artist === 'N/A') { - setTrackTitle('Offline'); + if (!trackData.song || trackData.song === 'N/A') { + setTrackTitle('No track playing'); setLyrics([]); return; } @@ -285,6 +288,9 @@ export default function Player({ user }) { setTrackMetadata(trackData, requestStation); + // Refresh queue when track changes + fetchQueue(); + const lyricsResponse = await fetch(`${API_URL}/lyric/search`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -317,16 +323,16 @@ export default function Player({ user }) { } } catch (error) { console.error('Error fetching metadata:', error); - setTrackTitle('Offline'); + setTrackTitle('Error fetching track'); setLyrics([]); } }, [activeStation]); const setTrackMetadata = useCallback((trackData, requestStation) => { - setTrackTitle(trackData.song); - setTrackArtist(trackData.artist); + setTrackTitle(trackData.song || 'Unknown Title'); + setTrackArtist(trackData.artist || 'Unknown Artist'); setTrackGenre(trackData.genre || ''); - setTrackAlbum(trackData.album); + setTrackAlbum(trackData.album || 'Unknown Album'); setCoverArt(`${API_URL}/radio/album_art?station=${requestStation}&_=${Date.now()}`); baseTrackElapsed.current = trackData.elapsed; @@ -358,19 +364,26 @@ export default function Player({ user }) { // ...existing code... - const handleSkip = async (skipTo = null) => { + const handleSkip = async (uuid = null) => { try { const response = await authFetch(`${API_URL}/radio/skip`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ station: activeStation, skipTo }), + body: JSON.stringify({ + skipTo: uuid, + station: activeStation, + }), }); const data = await response.json(); - if (!data.success) { - console.error("Failed to skip track.", data); + if (data.success) { + toast.success("OK!"); + fetchQueue(); + } else { + toast.error("Skip failed."); } } catch (error) { console.error("Error skipping track:", error); + toast.error("Skip failed."); } }; @@ -379,62 +392,127 @@ export default function Player({ user }) { const response = await authFetch(`${API_URL}/radio/reshuffle`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ station: activeStation }), + body: JSON.stringify({ + station: activeStation, + }), }); const data = await response.json(); - if (!data.ok) { - console.error("Failed to reshuffle queue.", data); + if (data.ok) { + toast.success("OK!"); + fetchQueue(); + } else { + toast.error("Reshuffle failed."); } } catch (error) { console.error("Error reshuffling queue:", error); + toast.error("Reshuffle failed."); } }; - const handleQueueShift = async (uuid, playNow = false) => { + const handleQueueShift = async (uuid, next = false) => { try { const response = await authFetch(`${API_URL}/radio/queue_shift`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ station: activeStation, uuid, playNow }), + body: JSON.stringify({ + uuid, + next, + station: activeStation, + }), }); const data = await response.json(); - if (!data.ok) { - console.error("Failed to shift queue.", data); + if (!data.err) { + toast.success("OK!"); + fetchQueue(); + } else { + toast.error("Queue shift failed."); } } catch (error) { console.error("Error shifting queue:", error); + toast.error("Queue shift failed."); } }; - const handleQueueRemove = async (uuid) => { - try { - const response = await authFetch(`${API_URL}/radio/queue_remove`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ station: activeStation, uuid }), - }); - const data = await response.json(); - if (!data.ok) { - console.error("Failed to remove from queue.", data); - } - } catch (error) { - console.error("Error removing from queue:", error); + const handlePlayNow = async (artistSong, next = false) => { + const toastId = "playNowToast"; // Unique ID for this toast + if (!toast.isActive(toastId)) { + toast.info("Trying...", { toastId }); } - }; - - const handleSongRequest = async (artist, song) => { try { const response = await authFetch(`${API_URL}/radio/request`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ station: activeStation, artist, song }), + body: JSON.stringify({ + artistsong: artistSong, + alsoSkip: !next, + station: activeStation, + }), }); const data = await response.json(); - if (!data.result) { - console.error("Failed to request song.", data); + if (data.result) { + setRequestInput(""); // Clear the input immediately + toast.update(toastId, { render: "OK!", type: "success", autoClose: 2000 }); + fetchQueue(); + } else { + toast.update(toastId, { render: "Play Now failed.", type: "error", autoClose: 3000 }); + } + } catch (error) { + console.error("Error playing song immediately:", error); + toast.update(toastId, { render: "Play Now failed.", type: "error", autoClose: 3000 }); + } + }; + + const handleSongRequest = async (artistSong) => { + const toastId = "songRequestToast"; // Unique ID for this toast + if (!toast.isActive(toastId)) { + toast.info("Trying...", { toastId }); + } + try { + const response = await authFetch(`${API_URL}/radio/request`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + artistsong: artistSong, + alsoSkip: false, // Ensure alsoSkip is false for requests + station: activeStation, + }), + }); + const data = await response.json(); + if (data.result) { + setRequestInput(""); // Clear the input immediately + toast.update(toastId, { render: "OK!", type: "success", autoClose: 2000 }); + fetchQueue(); + } else { + toast.update(toastId, { render: "Song request failed.", type: "error", autoClose: 3000 }); } } catch (error) { console.error("Error requesting song:", error); + toast.update(toastId, { render: "Song request failed.", type: "error", autoClose: 3000 }); + } + }; + + const handleRemoveFromQueue = async (uuid) => { + console.debug("handleRemoveFromQueue called with uuid:", uuid); // Debugging log + try { + const response = await authFetch(`${API_URL}/radio/queue_remove`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + uuid, + station: activeStation, + }), + }); + const data = await response.json(); + console.debug("handleRemoveFromQueue response:", data); // Debugging log + if (!data.err) { + toast.success("OK!"); + fetchQueue(); + } else { + toast.error("Remove from queue failed."); + } + } catch (error) { + console.error("Error removing from queue:", error); + toast.error("Remove from queue failed."); } }; @@ -454,7 +532,12 @@ export default function Player({ user }) { }); const data = await response.json(); setQueueData(data.items || []); - setQueueTotalRecords(data.total || data.totalRecords || 0); + // Use recordsFiltered for search, otherwise recordsTotal + setQueueTotalRecords( + typeof search === 'string' && search.length > 0 + ? data.recordsFiltered ?? data.recordsTotal ?? 0 + : data.recordsTotal ?? 0 + ); } catch (error) { console.error("Error fetching queue:", error); } @@ -467,7 +550,7 @@ export default function Player({ user }) { }, [isQueueVisible, queuePage, queueRows, queueSearch]); // Always define queueFooter, fallback to Close button if user is not available - const isDJ = user && Array.isArray(user.roles) && user.roles.includes('dj'); + const isDJ = (user && user.roles.includes('dj')) || ENVIRONMENT === "Dev"; const queueFooter = isDJ ? (
@@ -514,7 +597,7 @@ export default function Player({ user }) {
- {user && Hello, {user.user}} + {/* {user && Hello, {user.user}} */}
@@ -554,69 +637,74 @@ export default function Player({ user }) {
{isDJ && ( -
- { - console.debug('AutoComplete: completeMethod called', e); - fetchTypeahead(e.query); - }} - onChange={e => { - console.debug('AutoComplete: onChange', e); - setRequestInput(e.target.value); - }} - onShow={() => { - setTimeout(() => { - const panel = document.querySelector('.p-autocomplete-panel'); - const items = panel?.querySelector('.p-autocomplete-items'); - console.debug('AutoComplete: onShow panel', panel); - if (items) { - items.style.maxHeight = '200px'; - items.style.overflowY = 'auto'; - items.style.overscrollBehavior = 'contain'; - const wheelHandler = (e) => { - const delta = e.deltaY; - const atTop = items.scrollTop === 0; - const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight; - if ((delta < 0 && atTop) || (delta > 0 && atBottom)) { - e.preventDefault(); - } else { - e.stopPropagation(); - } - }; - items.removeEventListener('wheel', wheelHandler); - items.addEventListener('wheel', wheelHandler, { passive: false }); - } - }, 0); - }} - placeholder="Request a song..." - inputStyle={{ - width: '14rem', - padding: '0.25rem 0.5rem', - borderRadius: '9999px', - border: '1px solid #d1d5db', - fontSize: '0.85rem', - fontWeight: 'bold', - }} - className="typeahead-input" - forceSelection={false} - /> - - - - +
+
+ { + console.debug('AutoComplete: completeMethod called', e); + fetchTypeahead(e.query); + }} + onChange={e => { + console.debug('AutoComplete: onChange', e); + setRequestInput(e.target.value); + }} + onShow={() => { + setTimeout(() => { + const panel = document.querySelector('.p-autocomplete-panel'); + const items = panel?.querySelector('.p-autocomplete-items'); + console.debug('AutoComplete: onShow panel', panel); + if (items) { + items.style.maxHeight = '200px'; + items.style.overflowY = 'auto'; + items.style.overscrollBehavior = 'contain'; + const wheelHandler = (e) => { + const delta = e.deltaY; + const atTop = items.scrollTop === 0; + const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight; + if ((delta < 0 && atTop) || (delta > 0 && atBottom)) { + e.preventDefault(); + } else { + e.stopPropagation(); + } + }; + items.removeEventListener('wheel', wheelHandler); + items.addEventListener('wheel', wheelHandler, { passive: false }); + } + }, 0); + }} + placeholder="Request a song..." + inputStyle={{ + width: '24rem', + minWidth: '24rem', + padding: '0.35rem 0.75rem', + borderRadius: '9999px', + border: '1px solid #d1d5db', + fontSize: '1rem', + fontWeight: 'bold', + }} + className="typeahead-input" + forceSelection={false} + /> +
+ + + + +
+
)} {/* Always show play/pause button */} @@ -646,12 +734,13 @@ export default function Player({ user }) {