diff --git a/webui/css/style.css b/webui/css/style.css index 5e634d2..b009ab3 100644 --- a/webui/css/style.css +++ b/webui/css/style.css @@ -2116,3 +2116,139 @@ input[type="number"]::-webkit-inner-spin-button { border: 1px solid var(--border); } +/* ================================================================== + * Charts tab (iTunes / Spotify / M2O / Last.fm) + * Riusa in gran parte le classi catalog-* per stile e comportamento. + * ================================================================== */ +.charts-source-row { + display: flex; + gap: 18px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.charts-radio { + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 14px; + color: var(--text-1); + cursor: pointer; + padding: 6px 10px; + border-radius: 999px; + background: var(--bg-input); + border: 1px solid var(--border); + transition: background 0.15s ease, border-color 0.15s ease; +} + +.charts-radio:hover { + background: var(--bg-elev); +} + +.charts-radio input[type="radio"] { + accent-color: var(--indigo-hover, #6366F1); + cursor: pointer; +} + +.charts-radio input[type="radio"]:checked ~ span { + color: var(--text-1); + font-weight: 600; +} + +.charts-filters { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: flex-start; + margin-top: 8px; +} + +.charts-actions-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 10px; + flex-wrap: wrap; +} + +.charts-actions-btns { + display: flex; + gap: 8px; +} + +.charts-scroll { + max-height: 55vh; + overflow-y: auto; + overflow-x: auto; + padding-right: 6px; + border: 1px solid var(--border); + border-radius: var(--r-md); + background: var(--bg-card); +} + +.charts-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 12px; + color: var(--text-2); +} + +.charts-table thead th { + position: sticky; + top: 0; + z-index: 2; + background: var(--bg-card-2); + color: var(--text-1); + font-weight: 700; + padding: 10px 8px; + text-align: left; + font-size: 11px; + letter-spacing: 0.3px; + text-transform: uppercase; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.charts-table tbody td { + padding: 8px; + border-bottom: 1px solid var(--border); + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 260px; +} + +.charts-table tbody tr:hover { + background: var(--bg-elev); +} + +.charts-col-check { width: 30px; text-align: center; } +.charts-col-rank { width: 42px; text-align: center; font-variant-numeric: tabular-nums; color: var(--text-3); font-weight: 700; } +.charts-col-pop { width: 110px; text-align: right; font-variant-numeric: tabular-nums; color: var(--text-3); } + +.charts-check { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--indigo-hover, #6366F1); +} + +.charts-footer-bar { + position: sticky; + bottom: 0; + z-index: 5; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 12px 16px; + margin-top: 12px; + border-radius: var(--r-md); + background: var(--bg-card); + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid var(--border); +} + diff --git a/webui/index.html b/webui/index.html index c659e79..48a273f 100644 --- a/webui/index.html +++ b/webui/index.html @@ -61,6 +61,10 @@ ๐ค Appiattisci + + ๐ + Charts + โก Upgrade @@ -1497,6 +1501,170 @@ + + + + + CHARTS + Esplora le classifiche + Del momento (iTunes/Spotify), dance italiana (M2O) o storiche per genere (Last.fm) โ aggiungi in un click alla coda Scarica. + + ๐ + + + Sorgente + + + + + ๐ต Del momento (iTunes) + + + + ๐ข Spotify Charts + + + + ๐ด M2O Chart (dance italiana) + + + + ๐ป Storiche per genere (Last.fm) + + + + + + Paese + + ๐ฎ๐น Italia + ๐บ๐ธ USA + ๐ฌ๐ง UK + ๐ Mondo + + + + + + + Playlist chart + + Top 50 Global + Top 50 Italy + Top 50 USA + Top 50 UK + Viral 50 Global + Viral 50 Italy + + + + โ + + Le playlist Spotify richiedono le tue credenziali API (Impostazioni โ Spotify). + Senza credenziali questa sorgente resta vuota. + + + + + + + โ + + Chart dance italiana settimanale di M2O. Nessun filtro: premi "Aggiorna" per caricare la classifica corrente. + + + + + + + Decade + + Nessuna + Anni 70 + Anni 80 + Anni 90 + Anni 2000 + Anni 2010 + + + + Genere + + Nessuno + Pop + Rock + Dance + House + Hip Hop + Latin + R&B + Italo Disco + Reggae + Electronic + + + + โ + + Last.fm richiede una API key: nell'app c'รจ una chiave demo, se non funziona + registrane una gratuita su last.fm/api/account/create e mettila nel campo + lastfm_api_key di config.json. + + + + + + + โถ Aggiorna classifica + + + + Ignora cache (force refresh) + + + + + + + + + + Seleziona tutti + Deseleziona tutti + + + + + + + + + + # + Artista + Titolo + Album + Popolaritร + + + + + + + + + + Log + + + + + diff --git a/webui/js/app.js b/webui/js/app.js index 4f84d7c..1a1b1ee 100644 --- a/webui/js/app.js +++ b/webui/js/app.js @@ -411,6 +411,7 @@ const logEls = { dedup: () => $("#dedup-log"), catalog: () => $("#catalog-log"), flatten: () => $("#flatten-log"), + charts: () => $("#charts-log"), }; function classifyLog(msg) { @@ -542,6 +543,9 @@ async function init() { // Cataloga tab (AcoustID -> //) await CatalogUI.init(); await FlattenUI.init(); + + // Charts tab (iTunes / Spotify / M2O / Last.fm) + await ChartsUI.init(); } async function refreshRecDevices() { @@ -3770,6 +3774,360 @@ const FlattenUI = (() => { return { init }; })(); +// ============================================================ +// ChartsUI โ classifiche iTunes / Spotify / M2O / Last.fm +// ============================================================ +const ChartsUI = (() => { + const cs = { + source: "itunes", + country: "it", + playlist: "top50_global", + decade: "90s", + genre: "dance", + entries: [], + selected: new Set(), // Set di rank (int) + fetching: false, + }; + + function setStatus(text, kind) { + const el = $("#charts-status"); + if (!el) return; + el.textContent = text || ""; + el.className = "beatport-status" + (kind ? " " + kind : ""); + } + + function _showFiltersFor(source) { + const map = { + itunes: "#charts-filters-itunes", + spotify: "#charts-filters-spotify", + m2o: "#charts-filters-m2o", + lastfm: "#charts-filters-lastfm", + }; + Object.values(map).forEach((sel) => { + const el = $(sel); + if (el) el.hidden = true; + }); + const activeEl = $(map[source]); + if (activeEl) activeEl.hidden = false; + } + + function _updateSelectedCount() { + const n = cs.selected.size; + const cnt = $("#charts-selected-count"); + if (cnt) { + cnt.textContent = n === 1 + ? "1 brano selezionato" + : `${n} brani selezionati`; + } + const btn = $("#charts-add-queue-btn"); + if (btn) { + btn.disabled = n === 0 || cs.fetching; + const suffix = n > 0 + ? ` Aggiungi ${n} alla coda Scarica` + : " Aggiungi alla coda Scarica"; + btn.innerHTML = `โ${suffix}`; + } + const bar = $("#charts-footer-bar"); + if (bar) bar.hidden = cs.entries.length === 0; + } + + function _updateSummary() { + const line = $("#charts-summary-line"); + if (!line) return; + const n = cs.entries.length; + if (n === 0) { + line.textContent = ""; + return; + } + const labelBySource = { + itunes: "iTunes", + spotify: "Spotify Charts", + m2o: "M2O Chart", + lastfm: "Last.fm", + }; + const src = labelBySource[cs.source] || cs.source; + line.textContent = `${n} brani da ${src}`; + } + + function _fmtPopularity(entry) { + if (entry.popularity === null || entry.popularity === undefined || entry.popularity === "") { + return String(entry.rank || "โ"); + } + const n = Number(entry.popularity); + if (!isFinite(n)) return String(entry.popularity); + if (n >= 1000) { + // Formatta grandi numeri con separatore + return n.toLocaleString(); + } + return String(n); + } + + function _renderRow(entry) { + const tr = document.createElement("tr"); + tr.dataset.rank = String(entry.rank); + + // Checkbox + const tdCheck = document.createElement("td"); + tdCheck.className = "charts-col-check"; + const cb = document.createElement("input"); + cb.type = "checkbox"; + cb.className = "charts-check"; + cb.checked = cs.selected.has(entry.rank); + cb.addEventListener("change", () => { + if (cb.checked) cs.selected.add(entry.rank); + else cs.selected.delete(entry.rank); + _updateSelectedCount(); + }); + tdCheck.appendChild(cb); + tr.appendChild(tdCheck); + + // Rank + const tdRank = document.createElement("td"); + tdRank.className = "charts-col-rank"; + tdRank.textContent = String(entry.rank || "โ"); + tr.appendChild(tdRank); + + // Artist + const tdArtist = document.createElement("td"); + tdArtist.className = "charts-col-artist"; + tdArtist.textContent = entry.artist || "โ"; + tdArtist.title = entry.artist || ""; + tr.appendChild(tdArtist); + + // Title + const tdTitle = document.createElement("td"); + tdTitle.className = "charts-col-title"; + tdTitle.textContent = entry.title || "โ"; + tdTitle.title = entry.title || ""; + tr.appendChild(tdTitle); + + // Album + const tdAlbum = document.createElement("td"); + tdAlbum.className = "charts-col-album"; + tdAlbum.textContent = entry.album || "โ"; + tdAlbum.title = entry.album || ""; + tr.appendChild(tdAlbum); + + // Popularity + const tdPop = document.createElement("td"); + tdPop.className = "charts-col-pop"; + tdPop.textContent = _fmtPopularity(entry); + tr.appendChild(tdPop); + + return tr; + } + + function _renderTable() { + const body = $("#charts-table-body"); + const card = $("#charts-results-card"); + if (!body || !card) return; + body.innerHTML = ""; + if (cs.entries.length === 0) { + card.hidden = true; + const bar = $("#charts-footer-bar"); + if (bar) bar.hidden = true; + return; + } + for (const entry of cs.entries) { + body.appendChild(_renderRow(entry)); + } + card.hidden = false; + _updateSummary(); + _updateSelectedCount(); + } + + function _params() { + if (cs.source === "itunes") return { country: cs.country }; + if (cs.source === "spotify") return { playlist_key: cs.playlist }; + if (cs.source === "m2o") return {}; + if (cs.source === "lastfm") return { decade: cs.decade, genre: cs.genre }; + return {}; + } + + async function doFetch() { + if (cs.fetching) return; + cs.fetching = true; + cs.entries = []; + cs.selected.clear(); + _renderTable(); + + const btn = $("#charts-fetch-btn"); + if (btn) btn.disabled = true; + setStatus("Caricamento classificaโฆ", "loading"); + + const force = $("#charts-force")?.checked || false; + const payload = { source: cs.source, force, ...(_params()) }; + + let res; + try { + res = await window.pywebview.api.charts_fetch(payload); + } catch (e) { + setStatus("Errore: " + ((e && e.message) || e), "error"); + cs.fetching = false; + if (btn) btn.disabled = false; + return; + } + cs.fetching = false; + if (btn) btn.disabled = false; + + if (!res || !res.ok) { + const msg = (res && res.error) || "Impossibile caricare la classifica"; + setStatus(msg, "error"); + toast(msg, "error"); + return; + } + + cs.entries = Array.isArray(res.entries) ? res.entries : []; + _renderTable(); + + if (cs.entries.length === 0) { + setStatus("Nessun risultato. Prova a cambiare filtri o sorgente.", "ok"); + } else { + setStatus(`${cs.entries.length} brani ricevuti`, "ok"); + } + } + + async function addToQueue() { + if (cs.selected.size === 0) return; + const rankSet = cs.selected; + const selectedEntries = cs.entries + .filter((e) => rankSet.has(e.rank)) + .map((e) => ({ artist: e.artist, title: e.title })); + if (selectedEntries.length === 0) return; + + let res; + try { + res = await window.pywebview.api.charts_add_to_queue(selectedEntries); + } catch (e) { + toast("Errore: " + ((e && e.message) || e), "error"); + return; + } + if (!res || !res.ok) { + toast((res && res.error) || "Errore aggiunta", "error"); + return; + } + // Il bridge emette `charts:add_to_queue` che poi popola la tab Scarica. + // Reset selezione qui. + cs.selected.clear(); + // Deseleziona checkbox visibili + $$("#charts-table-body .charts-check").forEach((cb) => { cb.checked = false; }); + const master = $("#charts-select-all"); + if (master) master.checked = false; + _updateSelectedCount(); + } + + function _selectAll(on) { + cs.selected.clear(); + if (on) { + for (const e of cs.entries) cs.selected.add(e.rank); + } + $$("#charts-table-body .charts-check").forEach((cb) => { cb.checked = on; }); + const master = $("#charts-select-all"); + if (master) master.checked = on; + _updateSelectedCount(); + } + + async function init() { + // Ripristina scelta da config + const cfg = state.config || {}; + cs.source = cfg.charts_last_source || "itunes"; + cs.country = cfg.charts_itunes_country || "it"; + cs.playlist = cfg.charts_spotify_playlist || "top50_global"; + cs.decade = cfg.charts_lastfm_decade || "90s"; + cs.genre = cfg.charts_lastfm_genre || "dance"; + + // Applica valori ai controlli + const radio = document.querySelector(`input[name="charts-source"][value="${cs.source}"]`); + if (radio) radio.checked = true; + else { + // Fallback: seleziona iTunes + const fallback = document.querySelector('input[name="charts-source"][value="itunes"]'); + if (fallback) fallback.checked = true; + cs.source = "itunes"; + } + _showFiltersFor(cs.source); + + if ($("#charts-itunes-country")) $("#charts-itunes-country").value = cs.country; + if ($("#charts-spotify-playlist")) $("#charts-spotify-playlist").value = cs.playlist; + if ($("#charts-lastfm-decade")) $("#charts-lastfm-decade").value = cs.decade; + if ($("#charts-lastfm-genre")) $("#charts-lastfm-genre").value = cs.genre; + + // Bind eventi source radio + document.querySelectorAll('input[name="charts-source"]').forEach((r) => { + r.addEventListener("change", () => { + if (!r.checked) return; + cs.source = r.value; + _showFiltersFor(cs.source); + }); + }); + + // Bind filtri + $("#charts-itunes-country")?.addEventListener("change", (e) => { + cs.country = e.target.value; + }); + $("#charts-spotify-playlist")?.addEventListener("change", (e) => { + cs.playlist = e.target.value; + }); + $("#charts-lastfm-decade")?.addEventListener("change", (e) => { + cs.decade = e.target.value; + }); + $("#charts-lastfm-genre")?.addEventListener("change", (e) => { + cs.genre = e.target.value; + }); + + // Fetch, seleziona, aggiungi + $("#charts-fetch-btn")?.addEventListener("click", doFetch); + $("#charts-add-queue-btn")?.addEventListener("click", addToQueue); + $("#charts-select-all-btn")?.addEventListener("click", () => _selectAll(true)); + $("#charts-deselect-all-btn")?.addEventListener("click", () => _selectAll(false)); + $("#charts-select-all")?.addEventListener("change", (e) => _selectAll(!!e.target.checked)); + + // Bridge handler: quando l'utente aggiunge alla coda, popola la tab Scarica. + bridgeHandlers["charts:add_to_queue"] = (p) => { + if (!p) return; + const tracks = Array.isArray(p.tracks) ? p.tracks : null; + const text = typeof p.text === "string" ? p.text : ""; + const count = Number(p.count) || (tracks ? tracks.length : 0); + + // Popola state.loaded come "tracks" (stesso pattern del "Carica lista") + if (tracks && tracks.length) { + state.loaded = { + ok: true, + kind: "tracks", + tracks: tracks, + count: tracks.length, + name: "charts", + }; + // Aggiorna il campo URL e il badge sulla tab Scarica + const urlInput = $("#urlInput"); + if (urlInput) { + urlInput.value = "[Charts] " + count + " brani"; + } + const badge = $("#urlListBadge"); + if (badge) { + badge.textContent = `ยท ${count} tracce`; + badge.hidden = false; + } + appendLog("download", `[INFO] Caricati ${count} brani dalla tab Charts`); + } else if (text) { + // Fallback: nessun tracks strutturati, usa il testo grezzo + const urlInput = $("#urlInput"); + if (urlInput) { + urlInput.value = urlInput.value ? urlInput.value + "\n" + text : text; + } + } + + // Naviga alla tab Scarica + showView("download"); + toast(`${count} brani aggiunti alla coda Scarica`, "success"); + }; + + _updateSelectedCount(); + } + + return { init }; +})(); + // ============================================================ // Boot // ============================================================
Del momento (iTunes/Spotify), dance italiana (M2O) o storiche per genere (Last.fm) โ aggiungi in un click alla coda Scarica.
last.fm/api/account/create
lastfm_api_key
config.json