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.
This commit is contained in:
@@ -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);
|
||||
const handlePlayNow = async (artistSong, next = false) => {
|
||||
const toastId = "playNowToast"; // Unique ID for this toast
|
||||
if (!toast.isActive(toastId)) {
|
||||
toast.info("Trying...", { toastId });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error removing from queue:", error);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
? (
|
||||
<div className="flex gap-2 justify-end">
|
||||
@@ -514,7 +597,7 @@ export default function Player({ user }) {
|
||||
</div>
|
||||
|
||||
<div className="c-container">
|
||||
{user && <span className="text-lg font-semibold">Hello, {user.user}</span>}
|
||||
{/* {user && <span className="text-lg font-semibold">Hello, {user.user}</span>} */}
|
||||
|
||||
<div className="music-container mt-8">
|
||||
<section className="album-cover">
|
||||
@@ -554,7 +637,8 @@ export default function Player({ user }) {
|
||||
</div>
|
||||
|
||||
{isDJ && (
|
||||
<div className="dj-controls mt-6 flex gap-1 justify-center items-center flex-wrap">
|
||||
<div className="dj-controls mt-6 flex flex-wrap justify-center items-center gap-2" style={{ minWidth: '24rem', maxWidth: '100%', alignItems: 'flex-start' }}>
|
||||
<div className="flex flex-col items-center gap-2 w-full" style={{ minWidth: '24rem' }}>
|
||||
<AutoComplete
|
||||
value={requestInput}
|
||||
suggestions={typeaheadOptions}
|
||||
@@ -592,22 +676,24 @@ export default function Player({ user }) {
|
||||
}}
|
||||
placeholder="Request a song..."
|
||||
inputStyle={{
|
||||
width: '14rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
width: '24rem',
|
||||
minWidth: '24rem',
|
||||
padding: '0.35rem 0.75rem',
|
||||
borderRadius: '9999px',
|
||||
border: '1px solid #d1d5db',
|
||||
fontSize: '0.85rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
className="typeahead-input"
|
||||
forceSelection={false}
|
||||
/>
|
||||
<div className="flex flex-row flex-wrap justify-center gap-2 items-center" style={{ minWidth: '24rem' }}>
|
||||
<button className="px-3 py-1 rounded-full bg-blue-400 text-white hover:bg-blue-500 text-xs font-bold"
|
||||
onClick={() => handleSongRequest(requestInputArtist, requestInputSong)}>
|
||||
onClick={() => handleSongRequest(requestInput)}>
|
||||
Request
|
||||
</button>
|
||||
<button className="px-3 py-1 rounded-full bg-green-400 text-white hover:bg-green-500 text-xs font-bold"
|
||||
onClick={() => handleQueueShift(requestInputUuid, true)}>
|
||||
onClick={() => handlePlayNow(requestInput)}>
|
||||
Play Now
|
||||
</button>
|
||||
<button className="px-3 py-1 rounded-full bg-red-400 text-white hover:bg-red-500 text-xs font-bold" onClick={() => handleSkip()}>Skip</button>
|
||||
@@ -618,6 +704,8 @@ export default function Player({ user }) {
|
||||
Queue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Always show play/pause button */}
|
||||
<div className="music-control mt-4 flex justify-center">
|
||||
@@ -646,12 +734,13 @@ export default function Player({ user }) {
|
||||
<audio ref={audioElement} preload="none" />
|
||||
|
||||
<Dialog
|
||||
header="Queue"
|
||||
header={`Queue - ${activeStation}`}
|
||||
visible={isQueueVisible}
|
||||
style={{ width: "80vw", maxWidth: "1200px", height: "auto", maxHeight: "90vh" }}
|
||||
footer={queueFooter}
|
||||
onHide={() => setQueueVisible(false)}
|
||||
className={theme === "dark" ? "dark-theme" : "light-theme"}
|
||||
dismissableMask={true}
|
||||
>
|
||||
<div style={{ maxHeight: "calc(90vh - 100px)", overflow: "visible" }}>
|
||||
<div className="mb-2 flex justify-end">
|
||||
@@ -726,21 +815,24 @@ export default function Player({ user }) {
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 rounded bg-yellow-400 text-neutral-900 hover:bg-yellow-500 text-xs shadow-md cursor-pointer font-bold font-sans"
|
||||
onClick={() => handleQueueShift(rowData.uuid, true)}
|
||||
onClick={() => handleQueueShift(rowData.uuid, true, false)}
|
||||
title="Play"
|
||||
>
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 rounded bg-green-400 text-neutral-900 hover:bg-green-500 text-xs shadow-md cursor-pointer font-bold font-sans"
|
||||
onClick={() => handleQueueShift(rowData.uuid)}
|
||||
onClick={() => handleQueueShift(rowData.uuid, false, true)}
|
||||
title="Play Next"
|
||||
>
|
||||
Play Next
|
||||
</button>
|
||||
<button
|
||||
className="px-2 py-1 rounded bg-red-400 text-neutral-900 hover:bg-red-500 text-xs shadow-md cursor-pointer font-bold font-sans"
|
||||
onClick={() => handleQueueRemove(rowData.uuid)}
|
||||
onClick={() => {
|
||||
console.debug("Remove button clicked for uuid:", rowData.uuid); // Debugging log
|
||||
handleRemoveFromQueue(rowData.uuid);
|
||||
}}
|
||||
title="Remove"
|
||||
>
|
||||
Remove
|
||||
|
||||
Reference in New Issue
Block a user