';
}
} catch (error) {
- console.error('[DELETE] Error deleting file:', error);
+ // Debug messages disabled
showToast(`Error deleting "${fileToDelete}": ${error.message}`, 'error');
// Reset the button state if there was an error
@@ -575,7 +586,7 @@ function initFileUpload() {
const fileInput = document.getElementById('fileInputUser');
if (!uploadArea || !fileInput) {
- console.warn('[UPLOAD] Required elements not found for file upload');
+ // Debug messages disabled
return;
}
@@ -630,7 +641,7 @@ function initFileUpload() {
}
} catch (error) {
- console.error('[UPLOAD] Error uploading file:', error);
+ // Debug messages disabled
showToast(`Upload failed: ${error.message}`, 'error');
} finally {
// Reset file input and restore upload area text
@@ -679,9 +690,15 @@ function initFileUpload() {
}
// Main initialization when the DOM is fully loaded
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('DOMContentLoaded', async () => {
// Initialize dashboard components
- initDashboard(); // initFileUpload is called from within initDashboard
+ await initDashboard(); // initFileUpload is called from within initDashboard
+
+ // Update quota display if user is logged in
+ const uid = localStorage.getItem('uid');
+ if (uid) {
+ updateQuotaDisplay(uid);
+ }
// Delegated event listener for clicks on the document
document.addEventListener('click', (e) => {
@@ -701,10 +718,10 @@ document.addEventListener('DOMContentLoaded', () => {
const listItem = deleteButton.closest('.file-item');
if (!listItem) return;
- const uid = localStorage.getItem('uid') || localStorage.getItem('confirmed_uid');
+ const uid = localStorage.getItem('uid');
if (!uid) {
showToast('You need to be logged in to delete files', 'error');
- console.error('[DELETE] No UID found in localStorage');
+ // Debug messages disabled
return;
}
@@ -715,8 +732,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
- // Make fetchAndDisplayFiles available globally
+ // Make dashboard functions available globally
window.fetchAndDisplayFiles = fetchAndDisplayFiles;
+ window.initDashboard = initDashboard;
// Login/Register (guest)
const regForm = document.getElementById('register-form');
@@ -757,7 +775,7 @@ document.addEventListener('DOMContentLoaded', () => {
regForm.reset();
} else {
showToast(`Error: ${data.detail || 'Unknown error occurred'}`, 'error');
- console.error('Registration failed:', data);
+ // Debug messages disabled
}
} catch (parseError) {
console.error('Error parsing response:', parseError);
diff --git a/static/file-display.js b/static/file-display.js
new file mode 100644
index 0000000..9ebbea4
--- /dev/null
+++ b/static/file-display.js
@@ -0,0 +1,220 @@
+// This function is responsible for rendering the list of files to the DOM.
+// It is globally accessible via window.displayUserFiles.
+
+window.displayUserFiles = function(uid, files) {
+ const fileList = document.getElementById('file-list');
+ if (!fileList) {
+ // Debug messages disabled
+ return;
+ }
+
+ if (!files || files.length === 0) {
+ fileList.innerHTML = '
You have no uploaded files yet.
';
+ return;
+ }
+
+ const fragment = document.createDocumentFragment();
+ const displayedFiles = new Set();
+
+ files.forEach(file => {
+ // Use original_name for display, stored_name for operations.
+ let displayName = file.original_name || file.stored_name || 'Unnamed File';
+ const storedFileName = file.stored_name || file.original_name;
+ // No UUID pattern replacement: always show the original_name from backend.
+
+ // Skip if no valid identifier is found or if it's a duplicate.
+ if (!storedFileName || displayedFiles.has(storedFileName)) {
+ return;
+ }
+ displayedFiles.add(storedFileName);
+
+ const listItem = document.createElement('li');
+ const fileUrl = `/user-uploads/${uid}/${encodeURIComponent(storedFileName)}`;
+ const fileSize = file.size ? (file.size / 1024 / 1024).toFixed(2) + ' MB' : 'N/A';
+
+ let fileIcon = '🎵'; // Default icon
+ const fileExt = displayName.split('.').pop().toLowerCase();
+ if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(fileExt)) {
+ fileIcon = '🎵';
+ } else if (['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(fileExt)) {
+ fileIcon = '🖼️';
+ } else if (['pdf', 'doc', 'docx', 'txt'].includes(fileExt)) {
+ fileIcon = '📄';
+ }
+
+ listItem.innerHTML = `
+
+
+ ${displayName}
+ ${fileSize}
+
+
+
+ `;
+
+ fragment.appendChild(listItem);
+ });
+
+ fileList.appendChild(fragment);
+};
+
+// Function to handle file deletion
+async function deleteFile(uid, fileName, listItem, displayName = '') {
+ const fileToDelete = displayName || fileName;
+ if (!confirm(`Are you sure you want to delete "${fileToDelete}"?`)) {
+ return;
+ }
+
+ // Show loading state
+ if (listItem) {
+ listItem.style.opacity = '0.6';
+ listItem.style.pointerEvents = 'none';
+ const deleteButton = listItem.querySelector('.delete-file');
+ if (deleteButton) {
+ deleteButton.disabled = true;
+ deleteButton.textContent = '⏳';
+ }
+ }
+
+ try {
+ if (!uid) {
+ throw new Error('User not authenticated. Please log in again.');
+ }
+
+ // Debug messages disabled
+ const authToken = localStorage.getItem('authToken');
+ const headers = { 'Content-Type': 'application/json' };
+
+ if (authToken) {
+ headers['Authorization'] = `Bearer ${authToken}`;
+ }
+
+ // Get the email from localStorage (it's the UID)
+ const email = localStorage.getItem('uid');
+ if (!email) {
+ throw new Error('User not authenticated');
+ }
+
+ // The backend expects the full email as the UID in the path
+ // We need to ensure it's properly encoded for the URL
+ const username = email;
+ // Debug messages disabled
+
+ // Check if the filename is just a UUID (without log ID prefix)
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.\w+$/i;
+ let fileToDelete = fileName;
+
+ // If the filename is just a UUID, try to find the actual file with log ID prefix
+ if (uuidPattern.test(fileName)) {
+ // Debug messages disabled
+ try {
+ // First try to get the list of files to find the one with the matching UUID
+ const filesResponse = await fetch(`/user-files/${uid}`, {
+ method: 'GET',
+ headers: headers,
+ credentials: 'include'
+ });
+
+ if (filesResponse.ok) {
+ const filesData = await filesResponse.json();
+ if (filesData.files && Array.isArray(filesData.files)) {
+ // Look for a file that contains our UUID in its name
+ const matchingFile = filesData.files.find(f =>
+ f.stored_name && f.stored_name.includes(fileName)
+ );
+
+ if (matchingFile && matchingFile.stored_name) {
+ // Debug messages disabled
+ fileToDelete = matchingFile.stored_name;
+ }
+ }
+ }
+ } catch (e) {
+ // Debug messages disabled
+ // Continue with the original filename if there's an error
+ }
+ }
+
+ // Use the username in the URL with the correct filename
+ // Debug messages disabled
+ const response = await fetch(`/uploads/${username}/${encodeURIComponent(fileToDelete)}`, {
+ method: 'DELETE',
+ headers: headers,
+ credentials: 'include'
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
+ }
+
+ // Remove the file from the UI immediately
+ if (listItem && listItem.parentNode) {
+ listItem.parentNode.removeChild(listItem);
+ }
+
+ // Show success message
+ window.showToast(`Successfully deleted "${fileToDelete}"`, 'success');
+
+ // If the file list is now empty, show a message
+ const fileList = document.getElementById('file-list');
+ if (fileList && fileList.children.length === 0) {
+ fileList.innerHTML = '
No files uploaded yet.
';
+ }
+
+ // Refresh the file list and stream
+ const uid_current = localStorage.getItem('uid');
+ if (window.fetchAndDisplayFiles) {
+ // Use email-based UID for file operations if available, fallback to uid_current
+ const fileOperationUid = localStorage.getItem('uid') || uid_current; // uid is now email-based
+ // Debug messages disabled
+ await window.fetchAndDisplayFiles(fileOperationUid);
+ }
+ if (window.loadProfileStream) {
+ await window.loadProfileStream(uid_current);
+ }
+ } catch (error) {
+ // Debug messages disabled
+ window.showToast(`Error deleting "${fileToDelete}": ${error.message}`, 'error');
+
+ // Reset the button state if there was an error
+ if (listItem) {
+ listItem.style.opacity = '';
+ listItem.style.pointerEvents = '';
+ const deleteButton = listItem.querySelector('.delete-file');
+ if (deleteButton) {
+ deleteButton.disabled = false;
+ deleteButton.textContent = '🗑️';
+ }
+ }
+ }
+}
+
+// Add event delegation for delete buttons
+document.addEventListener('DOMContentLoaded', () => {
+ const fileList = document.getElementById('file-list');
+ if (fileList) {
+ fileList.addEventListener('click', (e) => {
+ const deleteButton = e.target.closest('.delete-file');
+ if (deleteButton) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const listItem = deleteButton.closest('li');
+ if (!listItem) return;
+
+ const uid = localStorage.getItem('uid');
+ if (!uid) {
+ window.showToast('You need to be logged in to delete files', 'error');
+ // Debug messages disabled
+ return;
+ }
+
+ const fileName = deleteButton.getAttribute('data-filename');
+ const displayName = deleteButton.getAttribute('data-display-name') || fileName;
+
+ deleteFile(uid, fileName, listItem, displayName);
+ }
+ });
+ }
+});
diff --git a/static/global-audio-manager.js b/static/global-audio-manager.js
index 619b18d..be503a5 100644
--- a/static/global-audio-manager.js
+++ b/static/global-audio-manager.js
@@ -23,7 +23,7 @@ class GlobalAudioManager {
* @param {Object} playerInstance - Reference to the player instance
*/
startPlayback(playerType, uid, playerInstance = null) {
- console.log(`[GlobalAudioManager] startPlayback called by: ${playerType} for UID: ${uid}`);
+ // Debug messages disabled
// If the same player is already playing the same UID, allow it
if (this.currentPlayer === playerType && this.currentUid === uid) {
return true;
@@ -38,7 +38,7 @@ class GlobalAudioManager {
this.currentPlayer = playerType;
this.currentUid = uid;
- console.log(`Global Audio Manager: ${playerType} player started playing UID: ${uid}`);
+ // Debug messages disabled
return true;
}
@@ -48,7 +48,7 @@ class GlobalAudioManager {
*/
stopPlayback(playerType) {
if (this.currentPlayer === playerType) {
- console.log(`Global Audio Manager: ${playerType} player stopped`);
+ // Debug messages disabled
this.currentPlayer = null;
this.currentUid = null;
}
@@ -93,7 +93,7 @@ class GlobalAudioManager {
* Notify a specific player type to stop
*/
notifyStop(playerType) {
- console.log(`Global Audio Manager: Notifying ${playerType} player to stop`);
+ // Debug messages disabled
this.listeners.forEach(listener => {
if (listener.playerType === playerType) {
try {
diff --git a/static/index.html b/static/index.html
index b74bf7b..80478d3 100644
--- a/static/index.html
+++ b/static/index.html
@@ -21,9 +21,11 @@
}
+
+
-
-
+
+
@@ -66,12 +68,12 @@
-
-
Quota: 0 MB
-
Uploaded Files
+
+
Uploaded Files
Loading files...
+
Quota: 0 MB
@@ -194,7 +196,6 @@
-
diff --git a/static/magic-login.js b/static/magic-login.js
index 8c465fd..68bba97 100644
--- a/static/magic-login.js
+++ b/static/magic-login.js
@@ -1,90 +1,43 @@
-// static/magic-login.js — handles magic‑link token UI
+/**
+ * Simplified Magic Login Module
+ *
+ * This file now uses the centralized AuthManager for authentication logic.
+ * The token-based magic login is handled by the AuthManager.
+ */
+
+import authManager from './auth-manager.js';
import { showSection } from './nav.js';
let magicLoginSubmitted = false;
+/**
+ * Initialize magic login - now delegated to AuthManager
+ * This function is kept for backward compatibility but the actual
+ * magic login logic is handled by the AuthManager during initialization.
+ */
export async function initMagicLogin() {
- console.debug('[magic-login] initMagicLogin called');
+ // Debug messages disabled
+
+ // The AuthManager handles both URL-based and token-based magic login
+ // during its initialization, so we just need to ensure it's initialized
+ if (!window.authManager) {
+ // Debug messages disabled
+ await authManager.initialize();
+ }
+
+ // Check if there was a magic login processed
const params = new URLSearchParams(location.search);
const token = params.get('token');
- if (!token) {
- console.debug('[magic-login] No token in URL');
- return;
- }
- // Remove token from URL immediately to prevent loops
- const url = new URL(window.location.href);
- url.searchParams.delete('token');
- window.history.replaceState({}, document.title, url.pathname + url.search);
- try {
- const formData = new FormData();
- formData.append('token', token);
- const res = await fetch('/magic-login', {
- method: 'POST',
- body: formData,
- });
- if (res.redirected) {
- // If redirected, backend should set cookie; but set localStorage for SPA
- const url = new URL(res.url);
- const confirmedUid = url.searchParams.get('confirmed_uid');
- if (confirmedUid) {
- // Generate a simple auth token (in a real app, this would come from the server)
- const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
-
- // Set cookies and localStorage for SPA session logic
- document.cookie = `uid=${encodeURIComponent(confirmedUid)}; path=/; SameSite=Lax`;
- document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
-
- // Store in localStorage for client-side access
- localStorage.setItem('uid', confirmedUid);
- localStorage.setItem('confirmed_uid', confirmedUid);
- localStorage.setItem('authToken', authToken);
- localStorage.setItem('uid_time', Date.now().toString());
- }
- window.location.href = res.url;
- return;
- }
- // If not redirected, show error (shouldn't happen in normal flow)
- let data;
- const contentType = res.headers.get('content-type');
- if (contentType && contentType.includes('application/json')) {
- data = await res.json();
- if (data && data.confirmed_uid) {
- // Generate a simple auth token (in a real app, this would come from the server)
- const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
-
- // Set cookies and localStorage for SPA session logic
- document.cookie = `uid=${encodeURIComponent(data.confirmed_uid)}; path=/; SameSite=Lax`;
- document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
-
- // Store in localStorage for client-side access
- localStorage.setItem('uid', data.confirmed_uid);
- localStorage.setItem('confirmed_uid', data.confirmed_uid);
- localStorage.setItem('authToken', authToken);
- localStorage.setItem('uid_time', Date.now().toString());
- import('./toast.js').then(({ showToast }) => {
- showToast('✅ Login successful!');
- // Update UI state after login
- const guestDashboard = document.getElementById('guest-dashboard');
- const userDashboard = document.getElementById('user-dashboard');
- const registerPage = document.getElementById('register-page');
-
- if (guestDashboard) guestDashboard.style.display = 'none';
- if (userDashboard) userDashboard.style.display = 'block';
- if (registerPage) registerPage.style.display = 'none';
-
- // Show the user's stream page
- if (typeof showSection === 'function') {
- showSection('me-page');
- }
- });
- return;
- }
- alert(data.detail || 'Login failed.');
- } else {
- const text = await res.text();
- alert(text || 'Login failed.');
- }
- } catch (err) {
- alert('Network error: ' + err);
+
+ if (token) {
+ // Debug messages disabled
+ } else {
+ // Debug messages disabled
}
}
+
+// Export for backward compatibility
+export { magicLoginSubmitted };
+
+// Make showSection available globally for AuthManager
+window.showSection = showSection;
diff --git a/static/personal-player.js b/static/personal-player.js
index 9df5b89..6614fe8 100644
--- a/static/personal-player.js
+++ b/static/personal-player.js
@@ -1,81 +1,57 @@
import { showToast } from "./toast.js";
-import { globalAudioManager } from './global-audio-manager.js';
+import { SharedAudioPlayer } from './shared-audio-player.js';
-// Module-level state for the personal player
-let audio = null;
+function getPersonalStreamUrl(uid) {
+ return `/audio/${encodeURIComponent(uid)}/stream.opus`;
+}
+
+function updatePlayPauseButton(button, isPlaying) {
+ if (button) button.textContent = isPlaying ? '⏸️' : '▶️';
+ // Optionally, update other UI elements here
+}
+
+const personalPlayer = new SharedAudioPlayer({
+ playerType: 'personal',
+ getStreamUrl: getPersonalStreamUrl,
+ onUpdateButton: updatePlayPauseButton
+});
/**
* Finds or creates the audio element for the personal stream.
* @returns {HTMLAudioElement | null}
*/
-function getOrCreateAudioElement() {
- if (audio) {
- return audio;
- }
-
- audio = document.createElement('audio');
- audio.id = 'me-audio';
- audio.preload = 'metadata';
- audio.crossOrigin = 'use-credentials';
- document.body.appendChild(audio);
-
- // --- Setup Event Listeners (only once) ---
- audio.addEventListener('error', (e) => {
- console.error('Personal Player: Audio Element Error', e);
- const error = audio.error;
- let errorMessage = 'An unknown audio error occurred.';
- if (error) {
- switch (error.code) {
- case error.MEDIA_ERR_ABORTED:
- errorMessage = 'Audio playback was aborted.';
- break;
- case error.MEDIA_ERR_NETWORK:
- errorMessage = 'A network error caused the audio to fail.';
- break;
- case error.MEDIA_ERR_DECODE:
- errorMessage = 'The audio could not be decoded.';
- break;
- case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
- errorMessage = 'The audio format is not supported by your browser.';
- break;
- default:
- errorMessage = `An unexpected error occurred (Code: ${error.code}).`;
- break;
+function cleanupPersonalAudio() {
+ if (audioElement) {
+ try {
+ if (audioElement._eventHandlers) {
+ const { onPlay, onPause, onEnded, onError } = audioElement._eventHandlers;
+ if (onPlay) audioElement.removeEventListener('play', onPlay);
+ if (onPause) audioElement.removeEventListener('pause', onPause);
+ if (onEnded) audioElement.removeEventListener('ended', onEnded);
+ if (onError) audioElement.removeEventListener('error', onError);
}
+ audioElement.pause();
+ audioElement.removeAttribute('src');
+ audioElement.load();
+ if (audioElement._eventHandlers) delete audioElement._eventHandlers;
+ // Remove from DOM
+ if (audioElement.parentNode) audioElement.parentNode.removeChild(audioElement);
+ } catch (e) {
+ console.warn('[personal-player.js] Error cleaning up audio element:', e);
}
- showToast(errorMessage, 'error');
- });
-
- audio.addEventListener('play', () => updatePlayPauseButton(true));
- audio.addEventListener('pause', () => updatePlayPauseButton(false));
- audio.addEventListener('ended', () => updatePlayPauseButton(false));
-
- // The canplaythrough listener is removed as it violates autoplay policies.
- // The user will perform a second click to play the media after it's loaded.
-
- return audio;
-}
-
-/**
- * Updates the play/pause button icon based on audio state.
- * @param {boolean} isPlaying - Whether the audio is currently playing.
- */
-function updatePlayPauseButton(isPlaying) {
- const playPauseBtn = document.querySelector('#me-page .play-pause-btn');
- if (playPauseBtn) {
- playPauseBtn.textContent = isPlaying ? '⏸️' : '▶️';
+ audioElement = null;
}
}
-/**
- * Loads the user's personal audio stream into the player.
- * @param {string} uid - The user's unique ID.
- */
-export async function loadProfileStream(uid) {
- const audioElement = getOrCreateAudioElement();
- const audioSrc = `/audio/${uid}/stream.opus?t=${Date.now()}`;
- console.log(`[personal-player.js] Setting personal audio source to: ${audioSrc}`);
- audioElement.src = audioSrc;
+
+
+// Use the shared player for loading and playing the personal stream
+export function loadProfileStream(uid, playPauseBtn) {
+ if (!uid) {
+ showToast('No UID provided for profile stream', 'error');
+ return;
+ }
+ personalPlayer.play(uid, playPauseBtn);
}
/**
@@ -91,50 +67,19 @@ export function initPersonalPlayer() {
if (!playPauseBtn) return;
e.stopPropagation();
- const audio = getOrCreateAudioElement();
- if (!audio) return;
-
- try {
- if (audio.paused) {
- if (!audio.src || audio.src.endsWith('/#')) {
- showToast('No audio file available. Please upload one first.', 'info');
- return;
- }
-
- console.log('Attempting to play...');
- globalAudioManager.startPlayback('personal', localStorage.getItem('uid') || 'personal');
-
- const playPromise = audio.play();
- if (playPromise !== undefined) {
- playPromise.catch(error => {
- console.error(`Initial play() failed: ${error.name}. This is expected on first load.`);
- // If play fails, it's because the content isn't loaded.
- // The recovery is to call load(). The user will need to click play again.
- console.log('Calling load() to fetch media...');
- audio.load();
- showToast('Stream is loading. Please click play again in a moment.', 'info');
- });
- }
- } else {
- console.log('Attempting to pause...');
- audio.pause();
- }
- } catch (err) {
- console.error('A synchronous error occurred in handlePlayPause:', err);
- showToast('An unexpected error occurred with the audio player.', 'error');
+ const uid = localStorage.getItem('uid');
+ if (!uid) {
+ showToast('Please log in to play audio.', 'error');
+ return;
+ }
+ // Toggle play/pause
+ if (personalPlayer.audioElement && !personalPlayer.audioElement.paused && !personalPlayer.audioElement.ended) {
+ personalPlayer.pause();
+ } else {
+ loadProfileStream(uid, playPauseBtn);
}
});
- // Listen for stop requests from the global manager
- globalAudioManager.addListener('personal', () => {
- console.log('[personal-player.js] Received stop request from global audio manager.');
- const audio = getOrCreateAudioElement();
- if (audio && !audio.paused) {
- console.log('[personal-player.js] Pausing personal audio player.');
- audio.pause();
- }
- });
-
- // Initial setup
- getOrCreateAudioElement();
+ // Make loadProfileStream globally accessible for upload.js
+ window.loadProfileStream = loadProfileStream;
}
diff --git a/static/remove-confirmed-uid.js b/static/remove-confirmed-uid.js
new file mode 100644
index 0000000..a009aa6
--- /dev/null
+++ b/static/remove-confirmed-uid.js
@@ -0,0 +1,70 @@
+/**
+ * Cleanup Script: Remove Redundant confirmed_uid from localStorage
+ *
+ * This script removes the redundant confirmed_uid field from localStorage
+ * for users who might have it stored from the old authentication system.
+ */
+
+(function() {
+ 'use strict';
+
+ console.log('[CONFIRMED_UID_CLEANUP] Starting cleanup of redundant confirmed_uid field...');
+
+ // Check if confirmed_uid exists in localStorage
+ const confirmedUid = localStorage.getItem('confirmed_uid');
+ const currentUid = localStorage.getItem('uid');
+
+ if (confirmedUid) {
+ console.log(`[CONFIRMED_UID_CLEANUP] Found confirmed_uid: ${confirmedUid}`);
+ console.log(`[CONFIRMED_UID_CLEANUP] Current uid: ${currentUid}`);
+
+ // Verify that uid exists and is properly set
+ if (!currentUid) {
+ console.warn('[CONFIRMED_UID_CLEANUP] No uid found, setting uid from confirmed_uid');
+ localStorage.setItem('uid', confirmedUid);
+ } else if (currentUid !== confirmedUid) {
+ console.warn(`[CONFIRMED_UID_CLEANUP] UID mismatch - uid: ${currentUid}, confirmed_uid: ${confirmedUid}`);
+ console.log('[CONFIRMED_UID_CLEANUP] Keeping current uid value');
+ }
+
+ // Remove the redundant confirmed_uid
+ localStorage.removeItem('confirmed_uid');
+ console.log('[CONFIRMED_UID_CLEANUP] Removed redundant confirmed_uid from localStorage');
+
+ // Log the cleanup action
+ console.log('[CONFIRMED_UID_CLEANUP] Cleanup completed successfully');
+ } else {
+ console.log('[CONFIRMED_UID_CLEANUP] No confirmed_uid found, no cleanup needed');
+ }
+
+ // Also check for any other potential redundant fields
+ const redundantFields = [
+ 'confirmed_uid', // Main target
+ 'confirmedUid', // Camel case variant
+ 'confirmed-uid' // Hyphenated variant
+ ];
+
+ let removedCount = 0;
+ redundantFields.forEach(field => {
+ if (localStorage.getItem(field)) {
+ localStorage.removeItem(field);
+ removedCount++;
+ console.log(`[CONFIRMED_UID_CLEANUP] Removed redundant field: ${field}`);
+ }
+ });
+
+ if (removedCount > 0) {
+ console.log(`[CONFIRMED_UID_CLEANUP] Removed ${removedCount} redundant authentication fields`);
+ }
+
+ console.log('[CONFIRMED_UID_CLEANUP] Cleanup process completed');
+})();
+
+// Export for manual execution if needed
+if (typeof window !== 'undefined') {
+ window.removeConfirmedUidCleanup = function() {
+ const script = document.createElement('script');
+ script.src = '/static/remove-confirmed-uid.js';
+ document.head.appendChild(script);
+ };
+}
diff --git a/static/shared-audio-player.js b/static/shared-audio-player.js
new file mode 100644
index 0000000..8781e90
--- /dev/null
+++ b/static/shared-audio-player.js
@@ -0,0 +1,162 @@
+// shared-audio-player.js
+// Unified audio player logic for both streams and personal player
+
+import { globalAudioManager } from './global-audio-manager.js';
+
+export class SharedAudioPlayer {
+ constructor({ playerType, getStreamUrl, onUpdateButton }) {
+ this.playerType = playerType; // 'streams' or 'personal'
+ this.getStreamUrl = getStreamUrl; // function(uid) => url
+ this.onUpdateButton = onUpdateButton; // function(button, isPlaying)
+ this.audioElement = null;
+ this.currentUid = null;
+ this.isPlaying = false;
+ this.currentButton = null;
+ this._eventHandlers = {};
+
+ // Register stop listener
+ globalAudioManager.addListener(playerType, () => {
+ this.stop();
+ });
+ }
+
+ pause() {
+ if (this.audioElement && !this.audioElement.paused && !this.audioElement.ended) {
+ this.audioElement.pause();
+ this.isPlaying = false;
+ if (this.onUpdateButton && this.currentButton) {
+ this.onUpdateButton(this.currentButton, false);
+ }
+ }
+ }
+
+ async play(uid, button) {
+ const ctx = `[SharedAudioPlayer][${this.playerType}]${uid ? `[${uid}]` : ''}`;
+ const isSameUid = this.currentUid === uid;
+ const isActive = this.audioElement && !this.audioElement.paused && !this.audioElement.ended;
+
+ // Guard: If already playing the requested UID and not paused/ended, do nothing
+ if (isSameUid && isActive) {
+ if (this.onUpdateButton) this.onUpdateButton(button || this.currentButton, true);
+ return;
+ }
+
+ // If same UID but paused, resume
+ if (isSameUid && this.audioElement && this.audioElement.paused && !this.audioElement.ended) {
+ try {
+ await this.audioElement.play();
+ this.isPlaying = true;
+ if (this.onUpdateButton) this.onUpdateButton(button || this.currentButton, true);
+ globalAudioManager.startPlayback(this.playerType, uid);
+ } catch (err) {
+ this.isPlaying = false;
+ if (this.onUpdateButton) this.onUpdateButton(button || this.currentButton, false);
+ console.error(`${ctx} play() resume failed:`, err);
+ }
+ return;
+ }
+
+ // Otherwise, stop current and start new
+ if (!isSameUid && this.audioElement) {
+ } else {
+ }
+ this.stop();
+ this.currentUid = uid;
+ this.currentButton = button;
+ const url = this.getStreamUrl(uid);
+ this.audioElement = new Audio(url);
+ this.audioElement.preload = 'auto';
+ this.audioElement.crossOrigin = 'anonymous';
+ this.audioElement.style.display = 'none';
+ document.body.appendChild(this.audioElement);
+ this._attachEventHandlers();
+ try {
+ await this.audioElement.play();
+ this.isPlaying = true;
+ if (this.onUpdateButton) this.onUpdateButton(button, true);
+ globalAudioManager.startPlayback(this.playerType, uid);
+ } catch (err) {
+ this.isPlaying = false;
+ if (this.onUpdateButton) this.onUpdateButton(button, false);
+ console.error(`${ctx} play() failed:`, err);
+ }
+ }
+
+ stop() {
+ if (this.audioElement) {
+ this._removeEventHandlers();
+ try {
+ this.audioElement.pause();
+ this.audioElement.removeAttribute('src');
+ this.audioElement.load();
+ if (this.audioElement.parentNode) {
+ this.audioElement.parentNode.removeChild(this.audioElement);
+ }
+ } catch (e) {
+ console.warn('[shared-audio-player] Error cleaning up audio element:', e);
+ }
+ this.audioElement = null;
+ }
+ this.isPlaying = false;
+ this.currentUid = null;
+ if (this.currentButton && this.onUpdateButton) {
+ this.onUpdateButton(this.currentButton, false);
+ }
+ this.currentButton = null;
+ }
+
+ _attachEventHandlers() {
+ if (!this.audioElement) return;
+ const ctx = `[SharedAudioPlayer][${this.playerType}]${this.currentUid ? `[${this.currentUid}]` : ''}`;
+ const logEvent = (event) => {
+ // Debug logging disabled
+ };
+ // Core handlers
+ const onPlay = (e) => {
+ logEvent(e);
+ this.isPlaying = true;
+ if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, true);
+ };
+ const onPause = (e) => {
+ logEvent(e);
+ // console.trace(`${ctx} Audio pause stack trace:`);
+ this.isPlaying = false;
+ if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, false);
+ };
+ const onEnded = (e) => {
+ logEvent(e);
+ this.isPlaying = false;
+ if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, false);
+ };
+ const onError = (e) => {
+ logEvent(e);
+ this.isPlaying = false;
+ if (this.currentButton && this.onUpdateButton) this.onUpdateButton(this.currentButton, false);
+ console.error(`${ctx} Audio error:`, e);
+ };
+ // Attach handlers
+ this.audioElement.addEventListener('play', onPlay);
+ this.audioElement.addEventListener('pause', onPause);
+ this.audioElement.addEventListener('ended', onEnded);
+ this.audioElement.addEventListener('error', onError);
+ // Attach debug logging for all relevant events
+ const debugEvents = [
+ 'abort','canplay','canplaythrough','durationchange','emptied','encrypted','loadeddata','loadedmetadata',
+ 'loadstart','playing','progress','ratechange','seeked','seeking','stalled','suspend','timeupdate','volumechange','waiting'
+ ];
+ debugEvents.forEach(evt => {
+ this.audioElement.addEventListener(evt, logEvent);
+ }); // Logging now disabled
+ this._eventHandlers = { onPlay, onPause, onEnded, onError, debugEvents, logEvent };
+ }
+
+ _removeEventHandlers() {
+ if (!this.audioElement || !this._eventHandlers) return;
+ const { onPlay, onPause, onEnded, onError } = this._eventHandlers;
+ if (onPlay) this.audioElement.removeEventListener('play', onPlay);
+ if (onPause) this.audioElement.removeEventListener('pause', onPause);
+ if (onEnded) this.audioElement.removeEventListener('ended', onEnded);
+ if (onError) this.audioElement.removeEventListener('error', onError);
+ this._eventHandlers = {};
+ }
+}
diff --git a/static/sound.js b/static/sound.js
index 4227281..34335c5 100644
--- a/static/sound.js
+++ b/static/sound.js
@@ -1,17 +1,30 @@
// sound.js — reusable Web Audio beep
export function playBeep(frequency = 432, duration = 0.2, type = 'sine') {
- const ctx = new (window.AudioContext || window.webkitAudioContext)();
- const osc = ctx.createOscillator();
- const gain = ctx.createGain();
+ try {
+ // Validate parameters to prevent audio errors
+ if (!Number.isFinite(frequency) || frequency <= 0) {
+ frequency = 432; // fallback to default
+ }
+ if (!Number.isFinite(duration) || duration <= 0) {
+ duration = 0.2; // fallback to default
+ }
+
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
- osc.type = type;
- osc.frequency.value = frequency;
+ osc.type = type;
+ osc.frequency.value = frequency;
- osc.connect(gain);
- gain.connect(ctx.destination);
+ osc.connect(gain);
+ gain.connect(ctx.destination);
- gain.gain.setValueAtTime(0.1, ctx.currentTime); // subtle volume
- osc.start();
- osc.stop(ctx.currentTime + duration);
+ gain.gain.setValueAtTime(0.1, ctx.currentTime); // subtle volume
+ osc.start();
+ osc.stop(ctx.currentTime + duration);
+ } catch (error) {
+ // Silently handle audio errors to prevent breaking upload flow
+ console.warn('[SOUND] Audio beep failed:', error.message);
+ }
}
diff --git a/static/streams-ui.js b/static/streams-ui.js
index 2320f16..c65588f 100644
--- a/static/streams-ui.js
+++ b/static/streams-ui.js
@@ -28,7 +28,7 @@ export function initStreamsUI() {
// Register with global audio manager to handle stop requests from other players
globalAudioManager.addListener('streams', () => {
- console.log('[streams-ui] Received stop request from global audio manager');
+ // Debug messages disabled
stopPlayback();
});
}
@@ -79,10 +79,10 @@ document.addEventListener('DOMContentLoaded', () => {
function loadAndRenderStreams() {
const ul = document.getElementById('stream-list');
if (!ul) {
- console.error('[STREAMS-UI] Stream list element not found');
+ // Debug messages disabled
return;
}
- console.log('[STREAMS-UI] loadAndRenderStreams called, shouldForceRefresh:', shouldForceRefresh);
+ // Debug messages disabled
// Don't start a new connection if one is already active and we're not forcing a refresh
if (activeSSEConnection && !shouldForceRefresh) {
@@ -140,7 +140,7 @@ function loadAndRenderStreams() {
window.location.hostname === '127.0.0.1';
if (isLocalDevelopment || window.DEBUG_STREAMS) {
const duration = Date.now() - connectionStartTime;
- console.group('[streams-ui] Connection timeout reached');
+ // Debug messages disabled
console.log(`Duration: ${duration}ms`);
console.log('Current time:', new Date().toISOString());
console.log('Streams received:', streams.length);
@@ -203,18 +203,18 @@ function loadAndRenderStreams() {
// Process the stream
function processStream({ done, value }) {
- console.log('[STREAMS-UI] processStream called with done:', done);
+ // Debug messages disabled
if (done) {
- console.log('[STREAMS-UI] Stream processing complete');
+ // Debug messages disabled
// Process any remaining data in the buffer
if (buffer.trim()) {
- console.log('[STREAMS-UI] Processing remaining buffer data');
+ // Debug messages disabled
try {
const data = JSON.parse(buffer);
- console.log('[STREAMS-UI] Parsed data from buffer:', data);
+ // Debug messages disabled
processSSEEvent(data);
} catch (e) {
- console.error('[STREAMS-UI] Error parsing buffer data:', e);
+ // Debug messages disabled
}
}
return;
@@ -237,7 +237,7 @@ function loadAndRenderStreams() {
const data = JSON.parse(dataMatch[1]);
processSSEEvent(data);
} catch (e) {
- console.error('[streams-ui] Error parsing event data:', e, 'Event:', event);
+ // Debug messages disabled
}
}
}
@@ -298,7 +298,7 @@ function loadAndRenderStreams() {
// Function to process SSE events
function processSSEEvent(data) {
- console.log('[STREAMS-UI] Processing SSE event:', data);
+ // Debug messages disabled
if (data.end) {
if (streams.length === 0) {
ul.innerHTML = '
No active streams.
';
@@ -356,7 +356,7 @@ function loadAndRenderStreams() {
// Function to handle SSE errors
function handleSSEError(error) {
- console.error('[streams-ui] SSE error:', error);
+ // Debug messages disabled
// Only show error if we haven't already loaded any streams
if (streams.length === 0) {
@@ -386,11 +386,11 @@ function loadAndRenderStreams() {
export function renderStreamList(streams) {
const ul = document.getElementById('stream-list');
if (!ul) {
- console.warn('[STREAMS-UI] renderStreamList: #stream-list not found');
+ // Debug messages disabled
return;
}
- console.log('[STREAMS-UI] Rendering stream list with', streams.length, 'streams');
- console.debug('[STREAMS-UI] Streams data:', streams);
+ // Debug messages disabled
+ // Debug messages disabled
if (Array.isArray(streams)) {
if (streams.length) {
// Sort by mtime descending (most recent first)
@@ -409,10 +409,10 @@ export function renderStreamList(streams) {
}
} else {
ul.innerHTML = '
Error: Invalid stream data.
';
- console.error('[streams-ui] renderStreamList: streams is not an array', streams);
+ // Debug messages disabled
}
highlightActiveProfileLink();
- console.debug('[streams-ui] renderStreamList complete');
+ // Debug messages disabled
}
export function highlightActiveProfileLink() {
@@ -463,12 +463,7 @@ function escapeHtml(unsafe) {
.replace(/'/g, "'");
}
-// Function to update play/pause button state
-function updatePlayPauseButton(button, isPlaying) {
- if (!button) return;
- button.textContent = isPlaying ? '⏸️' : '▶️';
- button.setAttribute('aria-label', isPlaying ? 'Pause' : 'Play');
-}
+
// Audio context for Web Audio API
let audioContext = null;
@@ -492,7 +487,7 @@ function getAudioContext() {
// Stop current playback completely
function stopPlayback() {
- console.log('[streams-ui] Stopping playback');
+ // Debug messages disabled
// Stop Web Audio API if active
if (audioSource) {
@@ -561,120 +556,28 @@ function stopPlayback() {
currentlyPlayingAudio = null;
}
-// Load and play audio using HTML5 Audio element for Opus
-async function loadAndPlayAudio(uid, playPauseBtn) {
- // If we already have an audio element for this UID and it's paused, just resume it
- if (audioElement && currentUid === uid && audioElement.paused) {
- try {
- await audioElement.play();
- isPlaying = true;
- updatePlayPauseButton(playPauseBtn, true);
- return;
- } catch (error) {
- // Fall through to reload if resume fails
- }
- }
-
- // Stop any current playback
- stopPlayback();
-
- // Notify global audio manager that streams player is starting
- globalAudioManager.startPlayback('streams', uid);
-
- // Update UI
- updatePlayPauseButton(playPauseBtn, true);
- currentlyPlayingButton = playPauseBtn;
- currentUid = uid;
-
- try {
- // Create a new audio element with the correct MIME type
- const audioUrl = `/audio/${encodeURIComponent(uid)}/stream.opus`;
-
- // Create a new audio element with a small delay to prevent race conditions
- await new Promise(resolve => setTimeout(resolve, 50));
-
- audioElement = new Audio(audioUrl);
- audioElement.preload = 'auto';
- audioElement.crossOrigin = 'anonymous'; // Important for CORS
-
- // Set up event handlers with proper binding
- const onPlay = () => {
- isPlaying = true;
- updatePlayPauseButton(playPauseBtn, true);
- };
-
- const onPause = () => {
- isPlaying = false;
- updatePlayPauseButton(playPauseBtn, false);
- };
-
- const onEnded = () => {
- isPlaying = false;
- cleanupAudio();
- };
-
- const onError = (e) => {
- // Ignore errors from previous audio elements that were cleaned up
- if (!audioElement || audioElement.readyState === 0) {
- return;
- }
-
- isPlaying = false;
- updatePlayPauseButton(playPauseBtn, false);
-
- // Don't show error to user for aborted requests
- if (audioElement.error && audioElement.error.code === MediaError.MEDIA_ERR_ABORTED) {
- return;
- }
-
- // Show error to user for other errors
- if (typeof showToast === 'function') {
- showToast('Error playing audio. The format may not be supported.', 'error');
- }
- };
-
- // Add event listeners
- audioElement.addEventListener('play', onPlay, { once: true });
- audioElement.addEventListener('pause', onPause);
- audioElement.addEventListener('ended', onEnded, { once: true });
- audioElement.addEventListener('error', onError);
-
- // Store references for cleanup
- audioElement._eventHandlers = { onPlay, onPause, onEnded, onError };
-
- // Start playback with error handling
- try {
- const playPromise = audioElement.play();
-
- if (playPromise !== undefined) {
- await playPromise.catch(error => {
- // Ignore abort errors when switching between streams
- if (error.name !== 'AbortError') {
- throw error;
- }
- });
- }
-
- isPlaying = true;
- } catch (error) {
- // Only log unexpected errors
- if (error.name !== 'AbortError') {
- console.error('[streams-ui] Error during playback:', error);
- throw error;
- }
- }
-
- } catch (error) {
- console.error('[streams-ui] Error loading/playing audio:', error);
- if (playPauseBtn) {
- updatePlayPauseButton(playPauseBtn, false);
- }
-
- // Only show error if it's not an abort error
- if (error.name !== 'AbortError' && typeof showToast === 'function') {
- showToast('Error playing audio. Please try again.', 'error');
- }
- }
+// --- Shared Audio Player Integration ---
+import { SharedAudioPlayer } from './shared-audio-player.js';
+
+function getStreamUrl(uid) {
+ return `/audio/${encodeURIComponent(uid)}/stream.opus`;
+}
+
+function updatePlayPauseButton(button, isPlaying) {
+ if (button) button.textContent = isPlaying ? '⏸️' : '▶️';
+ // Optionally, update other UI elements here
+}
+// Only this definition should remain; remove any other updatePlayPauseButton functions.
+
+const streamsPlayer = new SharedAudioPlayer({
+ playerType: 'streams',
+ getStreamUrl,
+ onUpdateButton: updatePlayPauseButton
+});
+
+// Load and play audio using SharedAudioPlayer
+function loadAndPlayAudio(uid, playPauseBtn) {
+ streamsPlayer.play(uid, playPauseBtn);
}
// Handle audio ended event
@@ -688,7 +591,7 @@ function handleAudioEnded() {
// Clean up audio resources
function cleanupAudio() {
- console.log('[streams-ui] Cleaning up audio resources');
+ // Debug messages disabled
// Clean up Web Audio API resources if they exist
if (audioSource) {
@@ -756,32 +659,14 @@ if (streamList) {
e.preventDefault();
const uid = playPauseBtn.dataset.uid;
- if (!uid) {
- return;
+ if (!uid) return;
+
+ // Toggle play/pause using SharedAudioPlayer
+ if (streamsPlayer.currentUid === uid && streamsPlayer.audioElement && !streamsPlayer.audioElement.paused && !streamsPlayer.audioElement.ended) {
+ streamsPlayer.pause();
+ } else {
+ await loadAndPlayAudio(uid, playPauseBtn);
}
-
- // If clicking the currently playing button, toggle pause/play
- if (currentUid === uid) {
- if (isPlaying) {
- await audioElement.pause();
- isPlaying = false;
- updatePlayPauseButton(playPauseBtn, false);
- } else {
- try {
- await audioElement.play();
- isPlaying = true;
- updatePlayPauseButton(playPauseBtn, true);
- } catch (error) {
- // If resume fails, try reloading the audio
- await loadAndPlayAudio(uid, playPauseBtn);
- }
- }
- return;
- }
-
- // If a different stream is playing, stop it and start the new one
- stopPlayback();
- await loadAndPlayAudio(uid, playPauseBtn);
});
}
diff --git a/static/style.css b/static/style.css
index 299721a..9e06803 100644
--- a/static/style.css
+++ b/static/style.css
@@ -490,7 +490,7 @@ nav#guest-dashboard.dashboard-nav {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
margin-top: 0.8em;
opacity: 0;
- animation: fadeInOut 3.5s both;
+ animation: fadeInOut 15s both;
font-size: 1.1em;
pointer-events: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
@@ -580,7 +580,7 @@ nav#guest-dashboard.dashboard-nav {
}
/* Quota meter and uploaded files section */
-#quota-meter {
+#uploaded-files {
background: var(--surface); /* Match article background */
border: 1px solid var(--border);
border-radius: 8px;
@@ -593,19 +593,19 @@ nav#guest-dashboard.dashboard-nav {
color: var(--text-light);
}
-#quota-meter {
+#uploaded-files {
transition: all 0.2s ease;
}
-#quota-meter h4 {
+#uploaded-files h3 {
font-weight: 400;
text-align: center;
- margin: 1.5rem 0 0.75rem;
+ margin: 0 0 27px 0;
color: var(--text);
}
-#quota-meter > h4 {
- margin-top: 1.5rem;
+#uploaded-files > h3 {
+ margin: 0 0 27px 0;
text-align: center;
font-weight: 400;
color: var(--text);
@@ -732,7 +732,7 @@ nav#guest-dashboard.dashboard-nav {
border-bottom: none;
}
-#quota-meter:hover {
+#uploaded-files:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
@@ -740,7 +740,7 @@ nav#guest-dashboard.dashboard-nav {
.quota-meter {
font-size: 0.9em;
color: var(--text-muted);
- margin: 0 0 1rem 0;
+ margin: 1rem 0 0 0;
}
#file-list {
diff --git a/static/toast.js b/static/toast.js
index aa80272..0d3576b 100644
--- a/static/toast.js
+++ b/static/toast.js
@@ -14,6 +14,6 @@ export function showToast(message) {
setTimeout(() => {
toast.remove();
// Do not remove the container; let it persist for stacking
- }, 3500);
+ }, 15000);
}
diff --git a/static/uid-validator.js b/static/uid-validator.js
new file mode 100644
index 0000000..bd92200
--- /dev/null
+++ b/static/uid-validator.js
@@ -0,0 +1,169 @@
+/**
+ * UID Validation Utility
+ *
+ * Provides comprehensive UID format validation and sanitization
+ * to ensure all UIDs are properly formatted as email addresses.
+ */
+
+export class UidValidator {
+ constructor() {
+ // RFC 5322 compliant email regex (basic validation)
+ this.emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+
+ // Common invalid patterns to check against
+ this.invalidPatterns = [
+ /^devuser$/i, // Legacy username pattern
+ /^user\d+$/i, // Generic user patterns
+ /^test$/i, // Test user
+ /^admin$/i, // Admin user
+ /^\d+$/, // Pure numeric
+ /^[a-zA-Z]+$/, // Pure alphabetic (no @ symbol)
+ ];
+ }
+
+ /**
+ * Validate UID format - must be a valid email address
+ */
+ isValidFormat(uid) {
+ if (!uid || typeof uid !== 'string') {
+ return {
+ valid: false,
+ error: 'UID must be a non-empty string',
+ code: 'INVALID_TYPE'
+ };
+ }
+
+ const trimmed = uid.trim();
+ if (trimmed.length === 0) {
+ return {
+ valid: false,
+ error: 'UID cannot be empty',
+ code: 'EMPTY_UID'
+ };
+ }
+
+ // Check against invalid patterns
+ for (const pattern of this.invalidPatterns) {
+ if (pattern.test(trimmed)) {
+ return {
+ valid: false,
+ error: `UID matches invalid pattern: ${pattern}`,
+ code: 'INVALID_PATTERN'
+ };
+ }
+ }
+
+ // Validate email format
+ if (!this.emailRegex.test(trimmed)) {
+ return {
+ valid: false,
+ error: 'UID must be a valid email address',
+ code: 'INVALID_EMAIL_FORMAT'
+ };
+ }
+
+ return {
+ valid: true,
+ sanitized: trimmed.toLowerCase()
+ };
+ }
+
+ /**
+ * Sanitize and validate UID - ensures consistent format
+ */
+ sanitize(uid) {
+ const validation = this.isValidFormat(uid);
+
+ if (!validation.valid) {
+ console.error('[UID-VALIDATOR] Validation failed:', validation.error, { uid });
+ return null;
+ }
+
+ return validation.sanitized;
+ }
+
+ /**
+ * Validate and throw error if invalid
+ */
+ validateOrThrow(uid, context = 'UID') {
+ const validation = this.isValidFormat(uid);
+
+ if (!validation.valid) {
+ throw new Error(`${context} validation failed: ${validation.error} (${validation.code})`);
+ }
+
+ return validation.sanitized;
+ }
+
+ /**
+ * Check if a UID needs migration (legacy format)
+ */
+ needsMigration(uid) {
+ if (!uid || typeof uid !== 'string') {
+ return false;
+ }
+
+ const trimmed = uid.trim();
+
+ // Check if it's already a valid email
+ if (this.emailRegex.test(trimmed)) {
+ return false;
+ }
+
+ // Check if it matches known legacy patterns
+ for (const pattern of this.invalidPatterns) {
+ if (pattern.test(trimmed)) {
+ return true;
+ }
+ }
+
+ return true; // Any non-email format needs migration
+ }
+
+ /**
+ * Get validation statistics for debugging
+ */
+ getValidationStats(uids) {
+ const stats = {
+ total: uids.length,
+ valid: 0,
+ invalid: 0,
+ needsMigration: 0,
+ errors: {}
+ };
+
+ uids.forEach(uid => {
+ const validation = this.isValidFormat(uid);
+
+ if (validation.valid) {
+ stats.valid++;
+ } else {
+ stats.invalid++;
+ const code = validation.code || 'UNKNOWN';
+ stats.errors[code] = (stats.errors[code] || 0) + 1;
+ }
+
+ if (this.needsMigration(uid)) {
+ stats.needsMigration++;
+ }
+ });
+
+ return stats;
+ }
+}
+
+// Create singleton instance
+export const uidValidator = new UidValidator();
+
+// Legacy exports for backward compatibility
+export function validateUidFormat(uid) {
+ return uidValidator.isValidFormat(uid).valid;
+}
+
+export function sanitizeUid(uid) {
+ return uidValidator.sanitize(uid);
+}
+
+export function validateUidOrThrow(uid, context) {
+ return uidValidator.validateOrThrow(uid, context);
+}
diff --git a/static/upload.js b/static/upload.js
index be33409..670c02c 100644
--- a/static/upload.js
+++ b/static/upload.js
@@ -1,266 +1,178 @@
-// upload.js — Frontend file upload handler
-
import { showToast } from "./toast.js";
import { playBeep } from "./sound.js";
-import { logToServer } from "./logger.js";
-// Initialize upload system when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
+ // This module handles the file upload functionality, including drag-and-drop,
+ // progress indication, and post-upload actions like refreshing the file list.
+
+ // DOM elements are fetched once the DOM is ready
const dropzone = document.getElementById("user-upload-area");
- if (dropzone) {
- dropzone.setAttribute("aria-label", "Upload area. Click or drop an audio file to upload.");
- }
const fileInput = document.getElementById("fileInputUser");
- const fileInfo = document.createElement("div");
- fileInfo.id = "file-info";
- fileInfo.style.textAlign = "center";
- if (fileInput) {
- fileInput.parentNode.insertBefore(fileInfo, fileInput.nextSibling);
- }
- const streamInfo = document.getElementById("stream-info");
- const streamUrlEl = document.getElementById("streamUrl");
- const spinner = document.getElementById("spinner") || { style: { display: 'none' } };
- let abortController;
+ const fileList = document.getElementById("file-list");
- // Upload function
- const upload = async (file) => {
- if (abortController) abortController.abort();
- abortController = new AbortController();
- fileInfo.innerText = `📁 ${file.name} • ${(file.size / 1024 / 1024).toFixed(2)} MB`;
- if (file.size > 100 * 1024 * 1024) {
- showToast("❌ File too large. Please upload a file smaller than 100MB.");
- return;
- }
- spinner.style.display = "block";
- showToast('📡 Uploading…');
+ // Early exit if critical UI elements are missing
+ if (!dropzone || !fileInput || !fileList) {
+ // Debug messages disabled
+ return;
+ }
- fileInput.disabled = true;
- dropzone.classList.add("uploading");
- const formData = new FormData();
- const sessionUid = localStorage.getItem("uid");
- formData.append("uid", sessionUid);
- formData.append("file", file);
+ // Attach all event listeners
+ initializeUploadListeners();
- const res = await fetch("/upload", {
- signal: abortController.signal,
- method: "POST",
- body: formData,
- });
-
- let data, parseError;
- try {
- data = await res.json();
- } catch (e) {
- parseError = e;
- }
- if (!data) {
- showToast("❌ Upload failed: " + (parseError && parseError.message ? parseError.message : "Unknown error"));
- spinner.style.display = "none";
- fileInput.disabled = false;
- dropzone.classList.remove("uploading");
- return;
- }
- if (res.ok) {
- if (data.quota && data.quota.used_mb !== undefined) {
- const bar = document.getElementById("quota-bar");
- const text = document.getElementById("quota-text");
- const quotaSec = document.getElementById("quota-meter");
- if (bar && text && quotaSec) {
- quotaSec.hidden = false;
- const used = parseFloat(data.quota.used_mb);
- bar.value = used;
- bar.max = 100;
- text.textContent = `${used.toFixed(1)} MB used`;
- }
- }
- spinner.style.display = "none";
- fileInput.disabled = false;
- dropzone.classList.remove("uploading");
- showToast("✅ Upload successful.");
-
- // Refresh the audio player and file list
- const uid = localStorage.getItem("uid");
- if (uid) {
- try {
- if (window.loadProfileStream) {
- await window.loadProfileStream(uid);
- }
- // Refresh the file list
- if (window.fetchAndDisplayFiles) {
- await window.fetchAndDisplayFiles(uid);
- }
-
- // Refresh the stream list to update the last update time
- if (window.refreshStreamList) {
- await window.refreshStreamList();
- }
- } catch (e) {
- console.error('Failed to refresh:', e);
- }
- }
-
- playBeep(432, 0.25, "sine");
- } else {
- if (streamInfo) streamInfo.hidden = true;
- if (spinner) spinner.style.display = "none";
- if ((data.detail || data.error || "").includes("music")) {
- showToast("🎵 Upload rejected: singing or music detected.");
- } else {
- showToast(`❌ Upload failed: ${data.detail || data.error}`);
- }
-
- if (fileInput) fileInput.value = null;
- if (dropzone) dropzone.classList.remove("uploading");
- if (fileInput) fileInput.disabled = false;
- if (streamInfo) streamInfo.classList.remove("visible", "slide-in");
- }
- };
-
- // Function to fetch and display uploaded files
- async function fetchAndDisplayFiles(uidFromParam) {
- console.log('[UPLOAD] fetchAndDisplayFiles called with uid:', uidFromParam);
-
- // Get the file list element
- const fileList = document.getElementById('file-list');
- if (!fileList) {
- const errorMsg = 'File list element not found in DOM';
- console.error(errorMsg);
- return showErrorInUI(errorMsg);
- }
-
- // Get UID from parameter, localStorage, or cookie
- const uid = uidFromParam || localStorage.getItem('uid') || getCookie('uid');
- const authToken = localStorage.getItem('authToken');
- const headers = {
- 'Accept': 'application/json',
- };
-
- // Include auth token in headers if available, but don't fail if it's not
- // The server should handle both token-based and UID-based auth
- if (authToken) {
- headers['Authorization'] = `Bearer ${authToken}`;
- } else {
- console.debug('[UPLOAD] No auth token available, using UID-only authentication');
- }
-
- console.log('[UPLOAD] Auth state - UID:', uid, 'Token exists:', !!authToken);
+ /**
+ * Main upload function
+ * @param {File} file - The file to upload
+ */
+ async function upload(file) {
+ // Get user ID from localStorage or cookie
+ const uid = localStorage.getItem('uid') || getCookie('uid');
if (!uid) {
- console.error('[UPLOAD] No UID found in any source');
- fileList.innerHTML = '
User session expired. Please refresh the page.
';
+ // Debug messages disabled
+ showToast("You must be logged in to upload files.", "error");
return;
}
- // Log the authentication method being used
- if (!authToken) {
- console.debug('[UPLOAD] No auth token found, using UID-only authentication');
- } else {
- console.debug('[UPLOAD] Using token-based authentication');
- }
+ // Debug messages disabled
- // Show loading state
- fileList.innerHTML = '
';
- }
-
- // Delete button handling is now managed by dashboard.js
-
- // Update quota display if available
- if (data.quota !== undefined) {
- const bar = document.getElementById('quota-bar');
- const text = document.getElementById('quota-text');
- const quotaSec = document.getElementById('quota-meter');
- if (bar && text && quotaSec) {
- quotaSec.hidden = false;
- bar.value = data.quota;
- bar.max = 100;
- text.textContent = `${data.quota.toFixed(1)} MB`;
- }
+ const errorData = await response.json().catch(() => ({ detail: 'Upload failed with non-JSON response.' }));
+ throw new Error(errorData.detail || 'Unknown upload error');
}
+
+ const result = await response.json();
+ // Debug messages disabled
+ playBeep(800, 0.2); // Success beep - higher frequency
+
+ // Update UI to show success
+ statusText.textContent = 'Success!';
+ progressBar.style.width = '100%';
+ progressBar.style.backgroundColor = 'var(--success-color)';
+
+ // Remove the status indicator after a short delay
+ setTimeout(() => {
+ statusDiv.remove();
+ }, 2000);
+
+ // --- Post-Upload Actions ---
+ await postUploadActions(uid);
+
} catch (error) {
- const errorMessage = `Error loading file list: ${error.message || 'Unknown error'}`;
- console.error('[ERROR]', errorMessage, error);
- showErrorInUI(errorMessage, fileList);
- }
-
- // Helper function to show error messages in the UI
- function showErrorInUI(message, targetElement = null) {
- const errorHtml = `
-
-
Error loading files
-
${message}
-
- Check browser console for details
-
-
- `;
-
- if (targetElement) {
- targetElement.innerHTML = errorHtml;
- } else {
- // If no target element, try to find it
- const fileList = document.getElementById('file-list');
- if (fileList) fileList.innerHTML = errorHtml;
- }
+ // Debug messages disabled
+ playBeep(200, 0.5); // Error beep - lower frequency, longer duration
+ statusText.textContent = `Error: ${error.message}`;
+ progressBar.style.backgroundColor = 'var(--error-color)';
+ statusDiv.classList.add('upload-error');
}
}
- // Helper function to get cookie value by name
+ /**
+ * Actions to perform after a successful upload.
+ * @param {string} uid - The user's ID
+ */
+ async function postUploadActions(uid) {
+ // 1. Refresh the user's personal stream if the function is available
+ if (window.loadProfileStream) {
+ await window.loadProfileStream(uid);
+ }
+ // 2. Refresh the file list by re-fetching and then displaying.
+ if (window.fetchAndDisplayFiles) {
+ // Use email-based UID for file operations if available, fallback to uid
+ const fileOperationUid = localStorage.getItem('uid') || uid; // uid is now email-based
+ // Debug messages disabled
+ await window.fetchAndDisplayFiles(fileOperationUid);
+ }
+ // 3. Update quota display after upload
+ if (window.updateQuotaDisplay) {
+ const quotaUid = localStorage.getItem('uid') || uid;
+ // Debug messages disabled
+ await window.updateQuotaDisplay(quotaUid);
+ }
+ // 4. Refresh the public stream list to update the last update time
+ if (window.refreshStreamList) {
+ await window.refreshStreamList();
+ }
+ }
+
+ /**
+ * Creates the DOM element for the upload status indicator.
+ * @param {string} fileName - The name of the file being uploaded.
+ * @returns {HTMLElement}
+ */
+ function createStatusIndicator(fileName) {
+ const statusDiv = document.createElement('div');
+ statusDiv.className = 'upload-status-indicator';
+ statusDiv.innerHTML = `
+
+ ${fileName}
+ Uploading...
+
+
+
+
+ `;
+ return statusDiv;
+ }
+
+ /**
+ * Initializes all event listeners for the upload UI.
+ */
+ function initializeUploadListeners() {
+ dropzone.addEventListener("click", () => {
+ fileInput.click();
+ });
+
+ dropzone.addEventListener("dragover", (e) => {
+ e.preventDefault();
+ dropzone.classList.add("dragover");
+ });
+
+ dropzone.addEventListener("dragleave", () => {
+ dropzone.classList.remove("dragover");
+ });
+
+ dropzone.addEventListener("drop", (e) => {
+ e.preventDefault();
+ dropzone.classList.remove("dragover");
+ const file = e.dataTransfer.files[0];
+ if (file) {
+ upload(file);
+ }
+ });
+
+ fileInput.addEventListener("change", (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ upload(file);
+ }
+ });
+ }
+
+ /**
+ * Helper function to get a cookie value by name.
+ * @param {string} name - The name of the cookie.
+ * @returns {string|null}
+ */
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
@@ -268,35 +180,6 @@ document.addEventListener('DOMContentLoaded', () => {
return null;
}
- // Export functions for use in other modules
+ // Make the upload function globally accessible if needed by other scripts
window.upload = upload;
- window.fetchAndDisplayFiles = fetchAndDisplayFiles;
-
- if (dropzone && fileInput) {
- dropzone.addEventListener("click", () => {
- console.log("[DEBUG] Dropzone clicked");
- fileInput.click();
- console.log("[DEBUG] fileInput.click() called");
- });
- dropzone.addEventListener("dragover", (e) => {
- e.preventDefault();
- dropzone.classList.add("dragover");
- dropzone.style.transition = "background-color 0.3s ease";
- });
- dropzone.addEventListener("dragleave", () => {
- dropzone.classList.remove("dragover");
- });
- dropzone.addEventListener("drop", (e) => {
- dropzone.classList.add("pulse");
- setTimeout(() => dropzone.classList.remove("pulse"), 400);
- e.preventDefault();
- dropzone.classList.remove("dragover");
- const file = e.dataTransfer.files[0];
- if (file) upload(file);
- });
- fileInput.addEventListener("change", (e) => {
- const file = e.target.files[0];
- if (file) upload(file);
- });
- }
});
diff --git a/upload.py b/upload.py
index 1df43f6..7467462 100644
--- a/upload.py
+++ b/upload.py
@@ -23,7 +23,8 @@ DATA_ROOT = Path("./data")
@limiter.limit("5/minute")
@router.post("/upload")
-async def upload(request: Request, db = Depends(get_db), uid: str = Form(...), file: UploadFile = Form(...)):
+def upload(request: Request, uid: str = Form(...), file: UploadFile = Form(...)):
+ # Import here to avoid circular imports
from log import log_violation
import time
@@ -32,183 +33,259 @@ async def upload(request: Request, db = Depends(get_db), uid: str = Form(...), f
log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Starting upload of {file.filename}")
try:
- # First, verify the user exists and is confirmed
- user = db.exec(select(User).where((User.username == uid) | (User.email == uid))).first()
- if user is not None and not isinstance(user, User) and hasattr(user, "__getitem__"):
- user = user[0]
-
- log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] User check - found: {user is not None}, confirmed: {getattr(user, 'confirmed', False) if user else 'N/A'}")
-
- if not user or not hasattr(user, "confirmed") or not user.confirmed:
- raise HTTPException(status_code=403, detail="Account not confirmed")
-
- # Check quota before doing any file operations
- quota = db.get(UserQuota, uid) or UserQuota(uid=uid, storage_bytes=0)
- if quota.storage_bytes >= 100 * 1024 * 1024:
- raise HTTPException(status_code=400, detail="Quota exceeded")
-
- # Create user directory if it doesn't exist
- user_dir = DATA_ROOT / uid
- user_dir.mkdir(parents=True, exist_ok=True)
-
- # Generate a unique filename for the processed file first
- import uuid
- unique_name = f"{uuid.uuid4()}.opus"
- raw_ext = file.filename.split(".")[-1].lower()
- raw_path = user_dir / ("raw." + raw_ext)
- processed_path = user_dir / unique_name
-
- # Clean up any existing raw files first (except the one we're about to create)
- for old_file in user_dir.glob('raw.*'):
+ # Use the database session context manager to handle the session
+ with get_db() as db:
try:
- if old_file != raw_path: # Don't delete the file we're about to create
- old_file.unlink(missing_ok=True)
- log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Cleaned up old file: {old_file}")
- except Exception as e:
- log_violation("UPLOAD_ERROR", request.client.host, uid, f"[{request_id}] Failed to clean up {old_file}: {e}")
-
- # Save the uploaded file temporarily
- log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Saving temporary file to {raw_path}")
- try:
- with open(raw_path, "wb") as f:
- content = await file.read()
- if not content:
- raise ValueError("Uploaded file is empty")
- f.write(content)
- log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Successfully wrote {len(content)} bytes to {raw_path}")
- except Exception as e:
- log_violation("UPLOAD_ERROR", request.client.host, uid, f"[{request_id}] Failed to save {raw_path}: {e}")
- raise HTTPException(status_code=500, detail=f"Failed to save uploaded file: {e}")
-
- # Ollama music/singing check is disabled for this release
- log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Ollama music/singing check is disabled")
-
- try:
- convert_to_opus(str(raw_path), str(processed_path))
- except Exception as e:
- raw_path.unlink(missing_ok=True)
- raise HTTPException(status_code=500, detail=str(e))
-
- original_size = raw_path.stat().st_size
- raw_path.unlink(missing_ok=True) # cleanup
-
- # First, verify the file was created and has content
- if not processed_path.exists() or processed_path.stat().st_size == 0:
- raise HTTPException(status_code=500, detail="Failed to process audio file")
-
- # Concatenate all .opus files in random order to stream.opus for public playback
- # This is now done after the file is in its final location with log ID
- from concat_opus import concat_opus_files
- def update_stream_opus():
- try:
- concat_opus_files(user_dir, user_dir / "stream.opus")
- except Exception as e:
- # fallback: just use the latest processed file if concat fails
- import shutil
- stream_path = user_dir / "stream.opus"
- shutil.copy2(processed_path, stream_path)
- log_violation("STREAM_UPDATE", request.client.host, uid,
- f"[fallback] Updated stream.opus with {processed_path}")
-
- # We'll call this after the file is in its final location
-
- # Get the final file size
- size = processed_path.stat().st_size
-
- # Start a transaction
- try:
- # Create a log entry with the original filename
- log = UploadLog(
- uid=uid,
- ip=request.client.host,
- filename=file.filename, # Store original filename
- processed_filename=unique_name, # Store the processed filename
- size_bytes=size
- )
- db.add(log)
- db.flush() # Get the log ID without committing
-
- # Rename the processed file to include the log ID for better tracking
- processed_with_id = user_dir / f"{log.id}_{unique_name}"
- if processed_path.exists():
- # First check if there's already a file with the same UUID but different prefix
- for existing_file in user_dir.glob(f"*_{unique_name}"):
- if existing_file != processed_path:
- log_violation("CLEANUP", request.client.host, uid,
- f"[UPLOAD] Removing duplicate file: {existing_file}")
- existing_file.unlink(missing_ok=True)
+ # First, verify the user exists and is confirmed
+ user = db.query(User).filter(
+ (User.username == uid) | (User.email == uid)
+ ).first()
- # Now do the rename
- if processed_path != processed_with_id:
- if processed_with_id.exists():
- processed_with_id.unlink(missing_ok=True)
- processed_path.rename(processed_with_id)
- processed_path = processed_with_id
+ if user is not None and not isinstance(user, User) and hasattr(user, "__getitem__"):
+ user = user[0]
+ if not user:
+ log_violation("UPLOAD", request.client.host, uid, f"User {uid} not found")
+ raise HTTPException(status_code=404, detail="User not found")
- # Only clean up raw.* files, not previously uploaded opus files
- for old_temp_file in user_dir.glob('raw.*'):
+ log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] User check - found: {user is not None}, confirmed: {getattr(user, 'confirmed', False) if user else 'N/A'}")
+
+ # Check if user is confirmed
+ if not hasattr(user, 'confirmed') or not user.confirmed:
+ raise HTTPException(status_code=403, detail="Account not confirmed")
+
+ # Use user.email as the proper UID for quota and directory operations
+ user_email = user.email
+ quota = db.get(UserQuota, user_email) or UserQuota(uid=user_email, storage_bytes=0)
+
+ if quota.storage_bytes >= 100 * 1024 * 1024:
+ raise HTTPException(status_code=400, detail="Quota exceeded")
+
+ # Create user directory using email (proper UID) - not the uid parameter which could be username
+ user_dir = DATA_ROOT / user_email
+ user_dir.mkdir(parents=True, exist_ok=True)
+
+ # Generate a unique filename for the processed file first
+ import uuid
+ unique_name = f"{uuid.uuid4()}.opus"
+ raw_ext = file.filename.split(".")[-1].lower()
+ raw_path = user_dir / ("raw." + raw_ext)
+ processed_path = user_dir / unique_name
+
+ # Clean up any existing raw files first (except the one we're about to create)
+ for old_file in user_dir.glob('raw.*'):
try:
- old_temp_file.unlink(missing_ok=True)
- log_violation("CLEANUP", request.client.host, uid, f"[{request_id}] Cleaned up temp file: {old_temp_file}")
+ if old_file != raw_path: # Don't delete the file we're about to create
+ old_file.unlink(missing_ok=True)
+ log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Cleaned up old file: {old_file}")
except Exception as e:
- log_violation("CLEANUP_ERROR", request.client.host, uid, f"[{request_id}] Failed to clean up {old_temp_file}: {e}")
-
- # Get or create quota
- quota = db.query(UserQuota).filter(UserQuota.uid == uid).first()
- if not quota:
- quota = UserQuota(uid=uid, storage_bytes=0)
- db.add(quota)
+ log_violation("UPLOAD_ERROR", request.client.host, uid, f"[{request_id}] Failed to clean up {old_file}: {e}")
- # Update quota with the new file size
- quota.storage_bytes = sum(
- f.stat().st_size
- for f in user_dir.glob('*.opus')
- if f.name != 'stream.opus' and f != processed_path
- ) + size
-
- # Update public streams
- update_public_streams(uid, quota.storage_bytes, db)
-
- # Commit the transaction
- db.commit()
-
- # Now that the transaction is committed and files are in their final location,
- # update the stream.opus file to include all files
- update_stream_opus()
-
- except Exception as e:
- db.rollback()
- # Clean up the processed file if something went wrong
- if processed_path.exists():
- processed_path.unlink(missing_ok=True)
- raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
+ # Save the uploaded file temporarily
+ log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Saving temporary file to {raw_path}")
+
+ try:
+ with open(raw_path, "wb") as f:
+ content = file.file.read()
+ if not content:
+ raise ValueError("Uploaded file is empty")
+ f.write(content)
+ log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Successfully wrote {len(content)} bytes to {raw_path}")
+
+ # EARLY DB RECORD CREATION: after upload completes, before processing
+ early_log = UploadLog(
+ uid=user_email,
+ ip=request.client.host,
+ filename=file.filename, # original filename from user
+ processed_filename=None, # not yet processed
+ size_bytes=None # not yet known
+ )
+ db.add(early_log)
+ log_violation("UPLOAD_DEBUG", request.client.host, uid, f"[FORCE FLUSH] Before db.flush() after early_log add")
+ db.flush()
+ log_violation("UPLOAD_DEBUG", request.client.host, uid, f"[FORCE FLUSH] After db.flush() after early_log add")
+ db.commit()
+ log_violation("UPLOAD_DEBUG", request.client.host, uid, f"[FORCE COMMIT] After db.commit() after early_log add")
+ early_log_id = early_log.id
+ log_violation("UPLOAD_DEBUG", request.client.host, uid, f"[DEBUG] Early UploadLog created: id={early_log_id}, filename={file.filename}, UploadLog.filename={early_log.filename}")
+ except Exception as e:
+ log_violation("UPLOAD_ERROR", request.client.host, uid, f"[{request_id}] Failed to save {raw_path}: {e}")
+ raise HTTPException(status_code=500, detail=f"Failed to save uploaded file: {e}")
- return {
- "filename": file.filename,
- "original_size": round(original_size / 1024, 1),
- "quota": {
- "used_mb": round(quota.storage_bytes / (1024 * 1024), 2)
- }
- }
+ # Ollama music/singing check is disabled for this release
+ log_violation("UPLOAD", request.client.host, uid, f"[{request_id}] Ollama music/singing check is disabled")
+
+ try:
+ convert_to_opus(str(raw_path), str(processed_path))
+ except Exception as e:
+ raw_path.unlink(missing_ok=True)
+ raise HTTPException(status_code=500, detail=str(e))
+
+ original_size = raw_path.stat().st_size
+ raw_path.unlink(missing_ok=True) # cleanup
+
+ # First, verify the file was created and has content
+ if not processed_path.exists() or processed_path.stat().st_size == 0:
+ raise HTTPException(status_code=500, detail="Failed to process audio file")
+
+ # Get the final file size
+ size = processed_path.stat().st_size
+
+ # Concatenate all .opus files in random order to stream.opus for public playback
+ # This is now done after the file is in its final location with log ID
+ from concat_opus import concat_opus_files
+
+ def update_stream_opus():
+ try:
+ concat_opus_files(user_dir, user_dir / "stream.opus")
+ except Exception as e:
+ # fallback: just use the latest processed file if concat fails
+ import shutil
+ stream_path = user_dir / "stream.opus"
+ shutil.copy2(processed_path, stream_path)
+ log_violation("STREAM_UPDATE", request.client.host, uid,
+ f"[fallback] Updated stream.opus with {processed_path}")
+
+ # Start a transaction
+ try:
+ # Update the early DB record with processed filename and size
+ log = db.get(UploadLog, early_log_id)
+ log.processed_filename = unique_name
+ log.size_bytes = size
+ db.add(log)
+ db.flush() # Ensure update is committed
+
+ # Assert that log.filename is still the original filename, never overwritten
+ if log.filename is None or (log.filename.endswith('.opus') and log.filename == log.processed_filename):
+ log_violation("UPLOAD_ERROR", request.client.host, uid,
+ f"[ASSERTION FAILED] UploadLog.filename was overwritten! id={log.id}, filename={log.filename}, processed_filename={log.processed_filename}")
+ raise RuntimeError(f"UploadLog.filename was overwritten! id={log.id}, filename={log.filename}, processed_filename={log.processed_filename}")
+ else:
+ log_violation("UPLOAD_DEBUG", request.client.host, uid,
+ f"[ASSERTION OK] After update: id={log.id}, filename={log.filename}, processed_filename={log.processed_filename}")
+
+ log_violation("UPLOAD_DEBUG", request.client.host, uid, f"[COMMIT] Committing UploadLog for id={log.id}")
+ db.commit()
+ log_violation("UPLOAD_DEBUG", request.client.host, uid, f"[COMMIT OK] UploadLog committed for id={log.id}")
+
+ # Rename the processed file to include the log ID for better tracking
+ processed_with_id = user_dir / f"{log.id}_{unique_name}"
+
+ if processed_path.exists():
+ # First check if there's already a file with the same UUID but different prefix
+ for existing_file in user_dir.glob(f"*_{unique_name}"):
+ if existing_file != processed_path:
+ log_violation("CLEANUP", request.client.host, uid,
+ f"[UPLOAD] Removing duplicate file: {existing_file}")
+ existing_file.unlink(missing_ok=True)
+
+ # Now do the rename
+ if processed_path != processed_with_id:
+ if processed_with_id.exists():
+ processed_with_id.unlink(missing_ok=True)
+ processed_path.rename(processed_with_id)
+ processed_path = processed_with_id
+
+ # Only clean up raw.* files, not previously uploaded opus files
+ for old_temp_file in user_dir.glob('raw.*'):
+ try:
+ old_temp_file.unlink(missing_ok=True)
+ log_violation("CLEANUP", request.client.host, uid, f"[{request_id}] Cleaned up temp file: {old_temp_file}")
+ except Exception as e:
+ log_violation("CLEANUP_ERROR", request.client.host, uid, f"[{request_id}] Failed to clean up {old_temp_file}: {e}")
+
+ # Get or create quota
+ quota = db.query(UserQuota).filter(UserQuota.uid == user_email).first()
+ if not quota:
+ quota = UserQuota(uid=user_email, storage_bytes=0)
+ db.add(quota)
+
+ # Update quota with the new file size
+ quota.storage_bytes = sum(
+ f.stat().st_size
+ for f in user_dir.glob('*.opus')
+ if f.name != 'stream.opus' and f != processed_path
+ ) + size
+
+ # Update public streams
+ update_public_streams(user_email, quota.storage_bytes, db)
+
+ # The context manager will handle commit/rollback
+ # Now that the transaction is committed and files are in their final location,
+ # update the stream.opus file to include all files
+ update_stream_opus()
+
+ return {
+ "filename": file.filename,
+ "original_size": round(original_size / 1024, 1),
+ "quota": {
+ "used_mb": round(quota.storage_bytes / (1024 * 1024), 2)
+ }
+ }
+
+ except HTTPException as e:
+ # Re-raise HTTP exceptions as they are already properly formatted
+ db.rollback()
+ raise e
+
+ except Exception as e:
+ # Log the error and return a 500 response
+ db.rollback()
+ import traceback
+ tb = traceback.format_exc()
+ # Try to log the error
+ try:
+ log_violation("UPLOAD_ERROR", request.client.host, uid, f"Error processing upload: {str(e)}\n{tb}")
+ except Exception:
+ pass # If logging fails, continue with the error response
+
+ # Clean up the processed file if it exists
+ if 'processed_path' in locals() and processed_path.exists():
+ processed_path.unlink(missing_ok=True)
+
+ raise HTTPException(status_code=500, detail=f"Error processing upload: {str(e)}")
+
+ except HTTPException as e:
+ # Re-raise HTTP exceptions as they are already properly formatted
+ db.rollback()
+ raise e
+
+ except Exception as e:
+ # Log the error and return a 500 response
+ db.rollback()
+ import traceback
+ tb = traceback.format_exc()
+ # Try to log the error
+ try:
+ log_violation("UPLOAD_ERROR", request.client.host, uid, f"Error processing upload: {str(e)}\n{tb}")
+ except Exception:
+ pass # If logging fails, continue with the error response
+
+ # Clean up the processed file if it exists
+ if 'processed_path' in locals() and processed_path.exists():
+ processed_path.unlink(missing_ok=True)
+
+ raise HTTPException(status_code=500, detail=f"Error processing upload: {str(e)}")
+
except HTTPException as e:
- # Already a JSON response, just re-raise
+ # Re-raise HTTP exceptions as they are already properly formatted
raise e
+
except Exception as e:
+ # Catch any other exceptions that might occur outside the main processing block
import traceback
tb = traceback.format_exc()
- # Log and return a JSON error
try:
- log_violation("UPLOAD", request.client.host, uid, f"Unexpected error: {type(e).__name__}: {str(e)}\n{tb}")
- except Exception:
- pass
- return {"detail": f"Server error: {type(e).__name__}: {str(e)}"}
+ log_violation("UPLOAD_ERROR", request.client.host, uid, f"Unhandled error in upload handler: {str(e)}\n{tb}")
+ except:
+ pass # If logging fails, continue with the error response
+ raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
def update_public_streams(uid: str, storage_bytes: int, db: Session):
"""Update the public streams list in the database with the latest user upload info"""
try:
- # Get the user's info
- user = db.query(User).filter(User.username == uid).first()
+ # Get the user's info - uid is now email-based
+ user = db.query(User).filter(User.email == uid).first()
if not user:
print(f"[WARNING] User {uid} not found when updating public streams")
return
@@ -221,7 +298,6 @@ def update_public_streams(uid: str, storage_bytes: int, db: Session):
# Update the public stream info
public_stream.username = user.username
- public_stream.display_name = user.display_name or user.username
public_stream.storage_bytes = storage_bytes
public_stream.last_updated = datetime.utcnow()