From 24a6bdce846e8c192dcfc9535b962f9841dee62b Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:19:27 +0200 Subject: [PATCH 1/7] Create maintenance.html --- maintenance.html | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 maintenance.html diff --git a/maintenance.html b/maintenance.html new file mode 100644 index 0000000..713b634 --- /dev/null +++ b/maintenance.html @@ -0,0 +1,83 @@ + + + + + + + Temporanemente non disponibile + + + + +
+
+ +

Servizio momentaneamente non disponibile

+ +

+ Ci scusiamo per il disagio.
L'applicazione è in fase di manutenzione o l'evento potrebbe non essere ancora iniziato!
Riprova tra qualche minuto. +

+
+
+ + + From 3619a4943931b9253e818fe6059f60c70d605e39 Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:20:50 +0200 Subject: [PATCH 2/7] Update script.js --- script.js | 90 +++++++++++++++++++++++++------------------------------ 1 file changed, 40 insertions(+), 50 deletions(-) diff --git a/script.js b/script.js index d71956e..0f53394 100644 --- a/script.js +++ b/script.js @@ -1,4 +1,4 @@ -let accessToken = null; // caricato da env cloudflare pages +let accessToken = null; const ADD_LIMIT = 3; const TIME_LIMIT_MINUTES = 10; @@ -10,15 +10,14 @@ const headersAuth = () => ({ 'Content-Type': 'application/json' }); -const toast = (msg) => alert(msg); - function canAddTrack() { const now = Date.now(); const hist = JSON.parse(localStorage.getItem('addHistory')) || []; const recent = hist.filter(ts => now - ts < TIME_LIMIT_MINUTES * 60 * 1000); if (recent.length >= ADD_LIMIT) { const minutesLeft = Math.ceil((TIME_LIMIT_MINUTES * 60 * 1000 - (now - recent[0])) / (60 * 1000)); - alert(`Hey, slow down! You can add another song in ${minutesLeft} minutes.`); + const message = `Limite raggiunto! Potrai aggiungere un'altra canzone tra ${minutesLeft} minuti.`; + window.UIBridge?.showToast?.(message); return false; } return true; @@ -35,8 +34,8 @@ window.canAddTrack = canAddTrack; window.saveTrackAddition = saveTrackAddition; async function addToQueue(trackUri) { - if (!accessToken) { toast('Error: No token available to add the song.'); return false; } - if (!trackUri) { toast('Error: invalid track.'); return false; } + if (!accessToken) { window.UIBridge?.showToast?.('Errore: Token non disponibile.'); return false; } + if (!trackUri) { window.UIBridge?.showToast?.('Errore: Traccia non valida.'); return false; } try { const res = await fetch(`${API_BASE}/me/player/queue?uri=${encodeURIComponent(trackUri)}`, { @@ -45,23 +44,22 @@ async function addToQueue(trackUri) { }); if (res.ok) { - toast('Hands up! Your song will be played soon!'); + window.UIBridge?.showToast?.('Aggiunto! Il tuo brano verrà suonato a breve!'); return true; } - if (res.status === 404) { toast('No active devices, please make sure at least one device is playing content.'); return false; } - if (res.status === 401) { toast('Token expired or invalid. Please refresh your token.'); return false; } - if (res.status === 429) { toast('Spotify rate limit hit. Try again in a bit.'); return false; } - if (res.status === 403) { toast('Permission denied. Check scopes.'); return false; } + if (res.status === 404) { window.UIBridge?.showToast?.('Nessun dispositivo attivo. Assicurati che Spotify stia suonando.'); return false; } + if (res.status === 401) { window.UIBridge?.showToast?.('Token scaduto o non valido.'); return false; } + if (res.status === 429) { window.UIBridge?.showToast?.('Limite richieste Spotify raggiunto. Riprova tra poco.'); return false; } + if (res.status === 403) { window.UIBridge?.showToast?.('Permesso negato.'); return false; } - const txt = await res.text().catch(()=> ''); - console.error('Queue error:', res.status, res.statusText, txt); - toast('Could not add to queue. Try again.'); + console.error('Errore Coda:', res.status, await res.text().catch(()=>'')); + window.UIBridge?.showToast?.('Impossibile aggiungere alla coda. Riprova.'); return false; } catch (err) { - console.error('Error adding the track to the queue:', err); - toast('Network error adding the track. Try again.'); + console.error('Errore di rete nell\'aggiunta:', err); + window.UIBridge?.showToast?.('Errore di rete. Riprova.'); return false; } } @@ -71,9 +69,9 @@ window.addToQueue = addToQueue; let searchAbort; async function doSearch(query, limit = 10) { - if (!accessToken) { toast('There was a problem. Please make sure you entered the token correctly.'); return; } + if (!accessToken) { window.UIBridge?.showToast?.('Problema di configurazione. Token non trovato.'); return; } const q = (query || '').trim(); - if (!q) { toast('Please enter a valid search term.'); return; } + if (!q) { window.UIBridge?.showToast?.('Inserisci un termine di ricerca valido.'); return; } try { if (searchAbort) searchAbort.abort(); @@ -92,44 +90,41 @@ async function doSearch(query, limit = 10) { let lastErr = null; for (const url of tries) { - const res = await fetch(url, { headers: headersAuth(), signal }); - if (res.ok) { - data = await res.json(); - break; - } else { - const bodyText = await res.text().catch(()=> ''); - lastErr = { status: res.status, statusText: res.statusText, bodyText }; - if (res.status === 401) break; + try { + const res = await fetch(url, { headers: headersAuth(), signal }); + if (res.ok) { + data = await res.json(); + break; + } else { + lastErr = { status: res.status, body: await res.text().catch(()=>'') }; + if (res.status === 401) break; + } + } catch (loopErr) { + lastErr = { status: 'NETWORK_ERROR', body: loopErr.message }; } } - + if (!data) { - if (lastErr) { - console.error('Search failed:', lastErr.status, lastErr.statusText, lastErr.bodyText); - } - toast('Song search error. Make sure the token is valid.'); - window.UIBridge?.showError?.('Song search error. Try again.'); - return; + console.error('Errore Ricerca Finale:', lastErr); + window.UIBridge?.showError?.('Errore nella ricerca. Assicurati che il token sia valido.'); + return; } const items = (data?.tracks?.items || []).map(t => ({ image: t?.album?.images?.[0]?.url || '', - title: t?.name || 'Unknown title', - subtitle:(t?.artists || []).map(a => a?.name).filter(Boolean).join(', ') || 'Unknown artist', + title: t?.name || 'Titolo Sconosciuto', + subtitle:(t?.artists || []).map(a => a?.name).filter(Boolean).join(', ') || 'Artista Sconosciuto', uri: t?.uri || '', preview: t?.preview_url || null, - onclick: () => addToQueue(t?.uri) })); window.UIBridge?.renderItems?.(items, q); } catch (err) { if (err.name !== 'AbortError') { - console.error('Song search error:', err); - toast('Search failed. Please try again.'); - window.UIBridge?.showError?.('Search failed. Please try again.'); + console.error('Errore Ricerca:', err); + window.UIBridge?.showError?.('Ricerca fallita. Riprova.'); } - } finally { } } @@ -142,29 +137,24 @@ document.addEventListener('ui:search', e => { async function fetchAccessToken() { try { const response = await fetch('/api/token'); - if (!response.ok) { - const errBody = await response.text(); - console.error('Errore dal server token:', errBody); - throw new Error('Impossibile recuperare il token di accesso dal server.'); - } + if (!response.ok) throw new Error('Impossibile recuperare il token dal server.'); + const data = await response.json(); if (data.accessToken) { accessToken = data.accessToken; - console.log('Token caricato con successo.'); + console.log('Token caricato.'); document.getElementById('song-query').disabled = false; document.getElementById('search-btn').disabled = false; } else { - throw new Error('Il token ricevuto non è valido.'); + throw new Error('Token ricevuto non valido.'); } } catch (err) { console.error(err); - alert('ERRORE CRITICO: Impossibile caricare la configurazione. L\'app non funzionerà.'); + window.UIBridge?.showError?.('ERRORE CRITICO: Impossibile caricare la configurazione. L\'app non funzionerà.'); document.getElementById('song-query').placeholder = 'Errore di configurazione'; - window.UIBridge?.showError?.('Errore di configurazione del server. Contattare l\'organizzatore.'); } } window.addEventListener('load', () => { - // La ricerca è già disabilitata via HTML, la funzione fetchAccessToken la abiliterà fetchAccessToken(); }); From 9056dfa7d79e3c1f5dc7b39a25946fa1415fd90d Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:21:41 +0200 Subject: [PATCH 3/7] Delete reset.html --- reset.html | 185 ----------------------------------------------------- 1 file changed, 185 deletions(-) delete mode 100644 reset.html diff --git a/reset.html b/reset.html deleted file mode 100644 index 5af5391..0000000 --- a/reset.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - Song Limit Reset - - - -
-
- - Spotify - Spotify Song Queue - -
-
- -
-
-
-

Song limit reset

- -
-
-

Reset the local anti-spam limit on this device/browser (3 songs / 10 minutes).

- -
- - -
- -
-
Status
-
Ready.
-
- Remaining: — - Window: 10 min -
-
-
-
-
- - - -
- - - - - From ef7506b33d1aec02cd5c23873d2ed66fc4253fad Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:23:00 +0200 Subject: [PATCH 4/7] Create _middleware.js --- functions/_middleware.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 functions/_middleware.js diff --git a/functions/_middleware.js b/functions/_middleware.js new file mode 100644 index 0000000..ea8726c --- /dev/null +++ b/functions/_middleware.js @@ -0,0 +1,23 @@ +export async function onRequest(context) { + const { request, env, next } = context; + const url = new URL(request.url); + const pathname = url.pathname; + + if (env.MAINTENANCE !== 'TRUE') { + return await next(); + } + + if (pathname.startsWith('/maintenance.html')) { + return await next(); + } + + const maintenanceAsset = await env.ASSETS.fetch(new URL('/maintenance.html', request.url)); + + return new Response(maintenanceAsset.body, { + status: 503, + statusText: 'Service Unavailable', + headers: { + 'Content-Type': 'text/html; charset=utf-8', + }, + }); +} From 55eb17c61cb55bf2e46001b13a52e6a5a814257f Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:23:30 +0200 Subject: [PATCH 5/7] Update config.js --- functions/api/config.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/functions/api/config.js b/functions/api/config.js index 8d4e42b..34f7388 100644 --- a/functions/api/config.js +++ b/functions/api/config.js @@ -1,15 +1,12 @@ -// functions/api/config.js - export async function onRequestGet(context) { - // Legge la variabile d'ambiente EVENT_NAME. Se non è definita, usa un valore di default. const eventName = context.env.EVENT_NAME || "Il Nostro Fantastico Evento"; + const googleAnalyticsId = context.env.GOOGLE_ANALYTICS_ID || null; - // Prepara i dati di configurazione da inviare al frontend const config = { eventName: eventName, + googleAnalyticsId: googleAnalyticsId, }; - // Restituisce la configurazione in formato JSON return new Response(JSON.stringify(config), { status: 200, headers: { 'Content-Type': 'application/json' }, From 3a0ff3e16e9b20ea981c35ca647c315c3a247f01 Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:24:59 +0200 Subject: [PATCH 6/7] Create queue.js --- functions/api/queue.js | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 functions/api/queue.js diff --git a/functions/api/queue.js b/functions/api/queue.js new file mode 100644 index 0000000..6689817 --- /dev/null +++ b/functions/api/queue.js @@ -0,0 +1,60 @@ +async function getAccessToken(context) { + const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } = context.env; + if (!SPOTIFY_CLIENT_ID || !SPOTIFY_CLIENT_SECRET || !SPOTIFY_REFRESH_TOKEN) { + throw new Error("Variabili d'ambiente del server non configurate."); + } + + const basicAuth = btoa(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`); + const response = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { 'Authorization': `Basic ${basicAuth}`, 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: SPOTIFY_REFRESH_TOKEN }), + }); + + if (!response.ok) throw new Error("Impossibile rinfrescare il token di Spotify."); + const data = await response.json(); + return data.access_token; +} + +export async function onRequestGet(context) { + try { + const accessToken = await getAccessToken(context); + const queueEndpoint = 'https://api.spotify.com/v1/me/player/queue'; + + const queueResponse = await fetch(queueEndpoint, { + headers: { 'Authorization': `Bearer ${accessToken}` }, + }); + + if (queueResponse.status === 204 || queueResponse.status > 400) { + return new Response(JSON.stringify({ nowPlaying: null, queue: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const data = await queueResponse.json(); + + const nowPlaying = data.currently_playing ? { + uri: data.currently_playing.uri, + } : null; + + const queue = (data.queue || []).map(item => ({ + track: item.name || 'Traccia Sconosciuta', + artists: (item.artists || []).map(a => a.name).join(', '), + albumCover: item.album?.images?.[0]?.url || '', + uri: item.uri || '', + })); + + return new Response(JSON.stringify({ nowPlaying, queue }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error) { + console.error("Errore nella funzione queue:", error.message); + return new Response(JSON.stringify({ error: error.message }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} From 0671b3e717304357ad46541707e2f4522c002228 Mon Sep 17 00:00:00 2001 From: Michele Gentilini <51083221+mrgenty@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:27:10 +0200 Subject: [PATCH 7/7] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 378a5ed..4b1f28c 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,7 @@ Built with a serverless architecture on Cloudflare Pages, it's secure, infinitel - **Duplicate Prevention**: Intelligently prevents users from adding a song that is already in the queue or currently playing. - **Consent-Driven Analytics**: Integrates with **Google Analytics** and asks for user consent via a two-step welcome modal, ensuring privacy compliance. - **Robust Search & Previews**: Instantly search Spotify's entire library and listen to 30-second audio previews before queueing. -- **Full Admin Control**: - - **Maintenance Mode**: Easily take the site offline with a single environment variable, redirecting all traffic to a maintenance page. - - **Protected Rate-Limit Reset**: A dedicated `/reset.html` page, secured by **hCaptcha**, allows users to reset their local anti-spam limits. +- **Maintenance Mode**: Easily take the site offline with a single environment variable, redirecting all traffic to a maintenance page. - **Event Customization**: Centrally manage your event's name and branding through environment variables. - **Modern UI/UX**: A polished dark-mode interface with smooth animations, toast notifications, and confetti effects for a premium user experience.