Compare commits

..

No commits in common. "61570fe2e67bae537a3bb434f04a94c468c78d81" and "f691a1edc8089d29407868715c526d1594821fad" have entirely different histories.

9 changed files with 51 additions and 387 deletions

View File

@ -196,27 +196,6 @@
("message" . "Playlist not found"))
:status 404)))))
;; Recently played tracks API endpoint
(define-api asteroid/recently-played () ()
"Get the last 3 played tracks with AllMusic links"
(with-error-handling
(let ((tracks (get-recently-played)))
(api-output `(("status" . "success")
("tracks" . ,(mapcar (lambda (track)
(let* ((title (getf track :title))
(timestamp (getf track :timestamp))
(unix-timestamp (universal-time-to-unix timestamp))
(parsed (parse-track-title title))
(artist (getf parsed :artist))
(song (getf parsed :song))
(search-url (generate-music-search-url artist song)))
`(("title" . ,title)
("artist" . ,artist)
("song" . ,song)
("timestamp" . ,unix-timestamp)
("search_url" . ,search-url))))
tracks)))))))
;; API endpoint to get all tracks (for web player)
(define-api asteroid/tracks () ()
"Get all tracks for web player"

View File

@ -157,59 +157,3 @@
(api-output `(("status" . "success")
("message" . ,(format nil "Password reset for user: ~a" username)))))))
(define-api asteroid/user/activate (user-id active) ()
"API endpoint for setting the active state of an user account"
(format t "Activation of user: #~a set to ~a~%" user-id active)
(require-role :admin)
(with-error-handling
(let ((user (when user-id
(find-user-by-id user-id)))
(active (if (stringp active)
(parse-integer active)
active)))
(unless user
(error 'not-found-error :message "User not found"))
;; Change user active state
(let ((result (if (= 0 active)
(deactivate-user user-id)
(activate-user user-id))))
(if result
(api-output `(("status" . "success")
("message" . ,(format nil "User '~a' ~a."
(dm:field user "username")
(if (= 0 active)
"deactivated"
"activated")))))
(api-output `(("status" . "error")
("message" . ,(format nil "Could not ~a user '~a'."
(if (= 0 active)
"deactivated"
"activated")
(dm:field user "username"))))))))))
(define-api asteroid/user/role (user-id role) ()
"API endpoint for setting the access role of an user account"
(format t "Role of user: #~a set to ~a~%" user-id role)
(require-role :admin)
(with-error-handling
(let ((user (when user-id
(find-user-by-id user-id)))
(user-role (intern (string-upcase role) :keyword)))
(unless user
(error 'not-found-error :message "User not found"))
;; Change user role
(let ((result (update-user-role user-id user-role)))
(if result
(api-output `(("status" . "success")
("message" . ,(format nil "User '~a' is now a ~a."
(dm:field user "username")
role))))
(api-output `(("status" . "error")
("message" . ,(format nil "Could not set user '~a' as ~a."
(dm:field user "username")
role)))))))))

View File

@ -91,21 +91,18 @@
;; Update user role
(defun update-user-role (user-id new-role)
(let ((form-data (ps:new (-form-data))))
(ps:chain form-data (append "user-id" user-id))
(ps:chain form-data (append "role" new-role))
(ps:chain
(fetch "/api/asteroid/user/role"
(fetch (+ "/api/asteroid/users/" user-id "/role")
(ps:create :method "POST" :body form-data))
(then (lambda (response) (ps:chain response (json))))
(then (lambda (result)
;; Handle Radiance API data wrapping
(let ((data (or (ps:@ result data) result)))
(if (= (ps:@ data status) "success")
(if (= (ps:@ result status) "success")
(progn
(load-user-stats)
(alert (ps:@ data message)))
(alert (+ "Error updating user role: " (ps:@ data message)))))))
(alert "User role updated successfully"))
(alert (+ "Error updating user role: " (ps:@ result message))))))
(catch (lambda (error)
(ps:chain console (error "Error updating user role:" error))
(alert "Error updating user role. Please try again."))))))
@ -115,52 +112,37 @@
(when (not (confirm "Are you sure you want to deactivate this user?"))
(return))
(let ((form-data (ps:new (-form-data))))
(ps:chain form-data (append "user-id" user-id))
(ps:chain form-data (append "active" 0))
(ps:chain
(fetch "/api/asteroid/user/activate"
(ps:create :method "POST" :body form-data))
(fetch (+ "/api/asteroid/users/" user-id "/deactivate")
(ps:create :method "POST"))
(then (lambda (response) (ps:chain response (json))))
(then (lambda (result)
;; Handle Radiance API data wrapping
(let ((data (or (ps:@ result data) result)))
(if (= (ps:@ data status) "success")
(if (= (ps:@ result status) "success")
(progn
(load-users)
(load-user-stats)
(alert (ps:@ data message)))
(alert (+ "Error deactivating user: " (ps:@ data message)))))))
(alert "User deactivated successfully"))
(alert (+ "Error deactivating user: " (ps:@ result message))))))
(catch (lambda (error)
(ps:chain console (error "Error deactivating user:" error))
(alert "Error deactivating user. Please try again."))))))
(alert "Error deactivating user. Please try again.")))))
;; Activate user
(defun activate-user (user-id)
(when (not (confirm "Are you sure you want to activate this user?"))
(return))
(let ((form-data (ps:new (-form-data))))
(ps:chain form-data (append "user-id" user-id))
(ps:chain form-data (append "active" 1))
(ps:chain
(fetch "/api/asteroid/user/activate"
(ps:create :method "POST" :body form-data))
(fetch (+ "/api/asteroid/users/" user-id "/activate")
(ps:create :method "POST"))
(then (lambda (response) (ps:chain response (json))))
(then (lambda (result)
;; Handle Radiance API data wrapping
(let ((data (or (ps:@ result data) result)))
(if (= (ps:@ data status) "success")
(if (= (ps:@ result status) "success")
(progn
(load-users)
(load-user-stats)
(alert (ps:@ data message)))
(alert (+ "Error activating user: " (ps:@ data message)))))))
(alert "User activated successfully"))
(alert (+ "Error activating user: " (ps:@ result message))))))
(catch (lambda (error)
(ps:chain console (error "Error activating user:" error))
(alert "Error activating user. Please try again."))))))
(alert "Error activating user. Please try again.")))))
;; Toggle create user form
(defun toggle-create-user-form ()

View File

@ -143,131 +143,6 @@ body .now-playing{
overflow: auto;
}
body .recently-played-panel{
background: #1a2332;
border: 1px solid #2a3441;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
body .recently-played-panel h3{
margin: 0 0 15px 0;
color: #00ff00;
font-size: 1.2em;
font-weight: 600;
}
body .recently-played-panel .recently-played-list{
min-height: 100px;
}
body .recently-played-panel .recently-played-list .loading,
body .recently-played-panel .recently-played-list .no-tracks,
body .recently-played-panel .recently-played-list .error{
text-align: center;
color: #888;
padding: 20px;
font-style: italic;
}
body .recently-played-panel .recently-played-list .error{
color: #ff4444;
}
body .recently-played-panel .recently-played-list.track-list{
list-style: none;
padding: 0;
margin: 0;
border: none;
max-height: none;
overflow-y: visible;
}
body .recently-played-panel .recently-played-list.track-item{
padding: 6px 12px;
border-bottom: 1px solid #2a3441;
-moz-transition: background-color 0.2s;
-o-transition: background-color 0.2s;
-webkit-transition: background-color 0.2s;
-ms-transition: background-color 0.2s;
transition: background-color 0.2s;
}
body .recently-played-panel .recently-played-list.track-item LAST-CHILD{
border-bottom: none;
}
body .recently-played-panel .recently-played-list.track-item HOVER{
background-color: #2a3441;
}
body .recently-played-panel .recently-played-list.track-info{
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
gap: 2px 20px;
align-items: center;
}
body .recently-played-panel .recently-played-list.track-title{
color: #4488ff;
font-weight: 500;
font-size: 1em;
grid-column: 1;
grid-row: 1;
}
body .recently-played-panel .recently-played-list.track-artist{
color: #4488ff;
font-size: 0.9em;
grid-column: 1;
grid-row: 2;
}
body .recently-played-panel .recently-played-list.track-time{
color: #888;
font-size: 0.85em;
grid-column: 2;
grid-row: 1;
text-align: right;
}
body .recently-played-panel .recently-played-list.track-meta{
grid-column: 2;
grid-row: 2;
text-align: right;
}
body .recently-played-list .allmusic-link{
display: inline-flex;
align-items: center;
gap: 4px;
color: #4488ff;
text-decoration: none;
font-size: 0.85em;
letter-spacing: 0.08rem;
-moz-transition: all 0.2s;
-o-transition: all 0.2s;
-webkit-transition: all 0.2s;
-ms-transition: all 0.2s;
transition: all 0.2s;
white-space: nowrap;
}
body .recently-played-list .allmusic-link HOVER{
color: #00ff00;
text-decoration: underline;
text-underline-offset: 5px;
}
body .recently-played-list .allmusic-link svg{
width: 14px;
height: 14px;
}
body .back{
color: #00ffff;
text-decoration: none;

View File

@ -210,6 +210,7 @@
:grid-column 2
:grid-row 1
:text-align right))
(.back
:color "#00ffff"
:text-decoration none
@ -306,7 +307,7 @@
:text-align center)
((:and audio |::-webkit-media-controls-panel|)
:background-color "#1a2332")
:background-color "#1a1a1a")
;; ((:and audio (:or |::-webkit-media-controls-mute-button|
;; |::-webkit-media-controls-play-button|

View File

@ -1,86 +0,0 @@
// Recently Played Tracks functionality
async function updateRecentlyPlayed() {
try {
const response = await fetch('/api/asteroid/recently-played');
const result = await response.json();
// Radiance wraps API responses in a data envelope
const data = result.data || result;
if (data.status === 'success' && data.tracks && data.tracks.length > 0) {
const listEl = document.getElementById('recently-played-list');
if (!listEl) return;
// Build HTML for tracks
let html = '<ul class="track-list">';
data.tracks.forEach((track, index) => {
const timeAgo = formatTimeAgo(track.timestamp);
html += `
<li class="track-item">
<div class="track-info">
<div class="track-title">${escapeHtml(track.song)}</div>
<div class="track-artist">${escapeHtml(track.artist)}</div>
<span class="track-time">${timeAgo}</span>
<div class="track-meta">
<a href="${track.search_url}" target="_blank" rel="noopener noreferrer" class="allmusic-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
MusicBrainz
</a>
</div>
</div>
</li>
`;
});
html += '</ul>';
listEl.innerHTML = html;
} else {
const listEl = document.getElementById('recently-played-list');
if (listEl) {
listEl.innerHTML = '<p class="no-tracks">No tracks played yet</p>';
}
}
} catch (error) {
console.error('Error fetching recently played:', error);
const listEl = document.getElementById('recently-played-list');
if (listEl) {
listEl.innerHTML = '<p class="error">Error loading recently played tracks</p>';
}
}
}
function formatTimeAgo(timestamp) {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return `${Math.floor(diff / 86400)}d ago`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
const panel = document.getElementById('recently-played-panel');
if (panel) {
updateRecentlyPlayed();
// Update every 30 seconds
setInterval(updateRecentlyPlayed, 30000);
} else {
const list = document.getElementById('recently-played-list');
if (list) {
updateRecentlyPlayed();
setInterval(updateRecentlyPlayed, 30000);
}
}
});

View File

@ -90,23 +90,20 @@ function hideUsersTable() {
async function updateUserRole(userId, newRole) {
try {
const formData = new FormData();
formData.append('user-id', userId);
formData.append('role', newRole);
const response = await fetch('/api/asteroid/user/role', {
const response = await fetch(`/api/asteroid/users/${userId}/role`, {
method: 'POST',
body: formData
});
const result = await response.json();
// Handle Radiance API data wrapping
const data = result.data || result;
if (data.status === 'success') {
if (result.status === 'success') {
loadUserStats();
alert(data.message);
alert('User role updated successfully');
} else {
alert('Error updating user role: ' + data.message);
alert('Error updating user role: ' + result.message);
}
} catch (error) {
console.error('Error updating user role:', error);
@ -120,25 +117,18 @@ async function deactivateUser(userId) {
}
try {
const formData = new FormData();
formData.append('user-id', userId);
formData.append('active', 0);
const response = await fetch('/api/asteroid/user/activate', {
method: 'POST',
body: formData
const response = await fetch(`/api/asteroid/users/${userId}/deactivate`, {
method: 'POST'
});
const result = await response.json();
// Handle Radiance API data wrapping
const data = result.data || result;
if (data.status === 'success') {
if (result.status === 'success') {
loadUsers();
loadUserStats();
alert(data.message);
alert('User deactivated successfully');
} else {
alert('Error deactivating user: ' + data.message);
alert('Error deactivating user: ' + result.message);
}
} catch (error) {
console.error('Error deactivating user:', error);
@ -147,31 +137,19 @@ async function deactivateUser(userId) {
}
async function activateUser(userId) {
if (!confirm('Are you sure you want to activate this user?')) {
return;
}
try {
const formData = new FormData();
formData.append('user-id', userId);
formData.append('active', 1);
const response = await fetch('/api/asteroid/user/activate', {
method: 'POST',
body: formData
const response = await fetch(`/api/asteroid/users/${userId}/activate`, {
method: 'POST'
});
const result = await response.json();
// Handle Radiance API data wrapping
const data = result.data || result;
if (data.status === 'success') {
if (result.status === 'success') {
loadUsers();
loadUserStats();
alert(data.message);
alert('User activated successfully');
} else {
alert('Error activating user: ' + data.message);
alert('Error activating user: ' + result.message);
}
} catch (error) {
console.error('Error activating user:', error);

View File

@ -1,6 +0,0 @@
<div id="recently-played-panel" class="recently-played-panel">
<h3>Recently Played</h3>
<div id="recently-played-list" class="recently-played-list">
<p class="loading">Loading...</p>
</div>
</div>

View File

@ -40,10 +40,7 @@
(defun find-user-by-id (user-id)
"Find a user by ID"
(format t "Looking for user with ID: ~a (type: ~a)~%" user-id (type-of user-id))
(let* ((user-id (if (stringp user-id)
(parse-integer user-id)
user-id))
(user (dm:get-one "USERS" (db:query (:= '_id user-id)))))
(let ((user (dm:get-one "USERS" (db:query (:= '_id user-id)))))
(when user
(format t "Found user '~a' with id #~a~%"
(dm:field user "username")