Add files via upload
This commit is contained in:
399
frontend/css/main.css
Normal file
399
frontend/css/main.css
Normal file
@@ -0,0 +1,399 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
||||
|
||||
:root {
|
||||
--primary-accent: #1DB954;
|
||||
--background-dark: #121212;
|
||||
--surface-dark: #1E1E1E;
|
||||
--border-dark: #333;
|
||||
--text-primary: #e0e0e0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
background-color: var(--background-dark);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background: var(--surface-dark);
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.25);
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border-dark);
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
select, input, button {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
select, input {
|
||||
background: #2D2D2D;
|
||||
border: 1px solid var(--border-dark);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container-result {
|
||||
background: #2D2D2D;
|
||||
border: 1px solid #3A3A3A;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container-result:hover {
|
||||
border-color: #4A4A4A;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
background: transparent;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.item-info > div {
|
||||
flex: 1;
|
||||
min-width: 0; /* Prevent text overflow */
|
||||
}
|
||||
|
||||
.item-info h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.explicit {
|
||||
color: #ff4d4d;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 0.5rem;
|
||||
background: rgba(255,77,77,0.1);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.download-btn img {
|
||||
filter: invert(1);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.download-btn.loading img {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.download-btn.completed img {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.category summary {
|
||||
padding: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
background: #2D2D2D;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.category[open] summary {
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.search-form {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
select, input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
width: auto;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
html {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.item-image {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.search-container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.category summary {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.container-result {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.item-info {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0.4rem;
|
||||
margin-left: 0;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.download-btn img {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
input, select, button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23e0e0e0'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.75rem center;
|
||||
background-size: 1em;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--primary-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.download-progress {
|
||||
margin-top: 1rem;
|
||||
height: 20px;
|
||||
background: var(--surface-dark);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid var(--border-dark);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-accent), #1ED760);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
mix-blend-mode: difference;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.download-btn.completed {
|
||||
background: rgba(29, 185, 84, 0.2);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.download-btn.completed img {
|
||||
filter: invert(0.8);
|
||||
}
|
||||
|
||||
@keyframes checkmark-appear {
|
||||
0% { transform: scale(0); }
|
||||
90% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.download-btn.completed img.checkmark {
|
||||
animation: checkmark-appear 0.3s ease-out;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.download-progress {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.7rem;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.category {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.items-grid {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.container-result {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.item-info p {
|
||||
margin: 0.25rem 0;
|
||||
line-height: 1.4;
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.item-info p:last-child {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.items-grid {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.container-result {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.items-grid {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.container-result {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-gif {
|
||||
display: block;
|
||||
margin: 2rem auto;
|
||||
border-radius: 12px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
BIN
frontend/favicon.ico
Normal file
BIN
frontend/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
24
frontend/index.html
Normal file
24
frontend/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zotifarr</title>
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="search-container">
|
||||
<h1>Zotifarr</h1>
|
||||
<form class="search-form" id="searchForm">
|
||||
<select id="accountSelect" required>
|
||||
<option value="">Loading accounts...</option>
|
||||
</select>
|
||||
<input type="text" id="searchQuery" placeholder="Enter search query" required>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<div id="resultsContainer" class="results-section"></div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
frontend/js/app.js
Normal file
9
frontend/js/app.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { loadAccounts } from './components/load.js';
|
||||
import { initSearch } from './components/search.js';
|
||||
import { initDownload } from './components/download.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAccounts();
|
||||
initSearch();
|
||||
initDownload();
|
||||
});
|
||||
228
frontend/js/components/download.js
Normal file
228
frontend/js/components/download.js
Normal file
@@ -0,0 +1,228 @@
|
||||
export function initSearch() {
|
||||
document.getElementById('searchForm').addEventListener('submit', handleSearch);
|
||||
}
|
||||
|
||||
async function handleSearch(e) {
|
||||
e.preventDefault();
|
||||
const account = document.getElementById('accountSelect').value;
|
||||
const query = document.getElementById('searchQuery').value;
|
||||
|
||||
if (!account || !query) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search?account=${encodeURIComponent(account)}&query=${encodeURIComponent(query)}`);
|
||||
const data = await response.json();
|
||||
displayResults(data);
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
document.getElementById('resultsContainer').innerHTML = '<div class="error">Search failed. Please try again.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(data) {
|
||||
const container = document.getElementById('resultsContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
['albums', 'tracks'].forEach(type => {
|
||||
if (data[type]?.length > 0) {
|
||||
const section = document.createElement('details');
|
||||
section.className = 'category';
|
||||
section.innerHTML = `
|
||||
<summary>${type.charAt(0).toUpperCase() + type.slice(1)} (${data[type].length})</summary>
|
||||
<div class="items-grid">
|
||||
${data[type].map(item => `
|
||||
<div class="container-result">
|
||||
${createItemCard(item)}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(section);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createItemCard(item) {
|
||||
return `
|
||||
<div class="item-card"
|
||||
data-spotify-id="${item.spotify_id}"
|
||||
data-type="${item.type}"
|
||||
data-total-tracks="${item.total_tracks || 0}">
|
||||
${item.image_url ? `<img src="${item.image_url}" alt="${item.name}" class="item-image">` : ''}
|
||||
<div class="item-info">
|
||||
<div>
|
||||
<h3>${item.name}${item.explicit ? '<span class="explicit">E</span>' : ''}</h3>
|
||||
${item.artists ? `<p>Artists: ${item.artists.join(', ')}</p>` : ''}
|
||||
${item.owner ? `<p>Owner: ${item.owner}</p>` : ''}
|
||||
${item.total_tracks ? `<p>Total Tracks: ${item.total_tracks}</p>` : ''}
|
||||
</div>
|
||||
${['album', 'track'].includes(item.type) ? `
|
||||
<button class="download-btn">
|
||||
<img src="https://brandeps.com/icon-download/D/Download-icon-vector-09.svg"
|
||||
alt="Download"
|
||||
width="20"
|
||||
height="20">
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function initDownload() {
|
||||
document.getElementById('resultsContainer').addEventListener('click', async (e) => {
|
||||
if (e.target.closest('.download-btn')) {
|
||||
const card = e.target.closest('.item-card');
|
||||
const btn = card.querySelector('.download-btn');
|
||||
const account = document.getElementById('accountSelect').value;
|
||||
const { spotifyId, type } = card.dataset;
|
||||
const originalBtnHTML = btn.innerHTML;
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
btn.innerHTML = `<img src="https://i.gifer.com/5RT9.gif"
|
||||
alt="Loading"
|
||||
style="width: 20px; height: 20px; object-fit: cover">`;
|
||||
btn.classList.add('loading');
|
||||
btn.style.pointerEvents = 'none';
|
||||
|
||||
const statusLine = document.createElement('div');
|
||||
statusLine.className = 'status-line';
|
||||
card.appendChild(statusLine);
|
||||
|
||||
const response = await fetch(
|
||||
`/api/download?account=${encodeURIComponent(account)}&type=${type}&id=${spotifyId}`
|
||||
);
|
||||
|
||||
const reader = response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.getReader();
|
||||
|
||||
let buffer = '';
|
||||
let lastPercentage = -1;
|
||||
let totalTracks = 0;
|
||||
let completedTracks = 0;
|
||||
let isArtistDownload = type === 'artist';
|
||||
let firstAlbumStart = isArtistDownload;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
// Update to checkmark and stop animation
|
||||
btn.innerHTML = `<img src="https://www.svgrepo.com/show/166018/check-mark.svg"
|
||||
alt="Completed"
|
||||
style="width: 20px; height: 20px; filter: invert(1)">`;
|
||||
btn.classList.remove('loading');
|
||||
btn.classList.add('completed');
|
||||
btn.style.pointerEvents = 'none';
|
||||
|
||||
if (isArtistDownload) {
|
||||
statusLine.textContent = 'Artist download complete!';
|
||||
statusLine.classList.add('completed');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += value;
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
|
||||
switch(data.type) {
|
||||
case 'track_start':
|
||||
statusLine.textContent = 'Starting track download...';
|
||||
break;
|
||||
|
||||
case 'album_start':
|
||||
totalTracks = data.total_tracks;
|
||||
if (isArtistDownload && firstAlbumStart) {
|
||||
statusLine.textContent = `Downloading ${data.artist}`;
|
||||
firstAlbumStart = false;
|
||||
} else {
|
||||
statusLine.textContent = `Downloading album: ${data.album}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'playlist_start':
|
||||
statusLine.textContent = `Starting playlist: ${data.name}`;
|
||||
break;
|
||||
|
||||
case 'metadata':
|
||||
if (type === 'track') {
|
||||
statusLine.textContent = `Downloading: ${data.name} by ${data.artists[0]}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'download_start':
|
||||
statusLine.textContent += ` (${formatBytes(data.total_size)})`;
|
||||
break;
|
||||
|
||||
case 'download_progress':
|
||||
if (Math.floor(data.percentage) > lastPercentage) {
|
||||
lastPercentage = Math.floor(data.percentage);
|
||||
const progress = `${lastPercentage}% of ${formatBytes(data.total)}`;
|
||||
statusLine.textContent = type === 'track'
|
||||
? `Downloading: ${progress}`
|
||||
: `Downloading track ${completedTracks + 1}: ${progress}`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'album_track_start':
|
||||
statusLine.textContent = `Downloading track ${data.number}/${totalTracks}: ${data.track}`;
|
||||
break;
|
||||
|
||||
case 'playlist_track_start':
|
||||
statusLine.textContent = `Downloading track ${data.position}: ${data.track}`;
|
||||
break;
|
||||
|
||||
case 'conversion_start':
|
||||
statusLine.textContent = `Converting to ${data.format}...`;
|
||||
break;
|
||||
|
||||
case 'track_complete':
|
||||
completedTracks++;
|
||||
statusLine.textContent = `Completed: ${data.output_path.split('/').pop()}`;
|
||||
statusLine.classList.add('completed');
|
||||
break;
|
||||
|
||||
case 'track_skip':
|
||||
completedTracks++;
|
||||
statusLine.textContent = `Skipped: ${data.reason === 'already_exists' ? 'Already exists' : data.reason}`;
|
||||
statusLine.classList.add('warning');
|
||||
break;
|
||||
|
||||
case 'album_complete':
|
||||
statusLine.textContent = `Album complete! ${data.successful}/${data.total} tracks`;
|
||||
statusLine.classList.add('completed');
|
||||
break;
|
||||
|
||||
case 'playlist_complete':
|
||||
statusLine.textContent = `Playlist complete! ${data.downloaded} tracks`;
|
||||
statusLine.classList.add('completed');
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
// Restore original button state
|
||||
btn.innerHTML = originalBtnHTML;
|
||||
btn.classList.remove('loading', 'completed');
|
||||
btn.style.pointerEvents = 'auto';
|
||||
|
||||
const statusLine = card.querySelector('.status-line');
|
||||
statusLine.textContent = `Download failed: ${error.message}`;
|
||||
statusLine.classList.add('error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
14
frontend/js/components/load.js
Normal file
14
frontend/js/components/load.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Account loading functionality
|
||||
export async function loadAccounts() {
|
||||
try {
|
||||
const response = await fetch('/api/check');
|
||||
const data = await response.json();
|
||||
const select = document.getElementById('accountSelect');
|
||||
|
||||
select.innerHTML = data.accounts.length
|
||||
? data.accounts.map(acc => `<option value="${acc}">${acc}</option>`).join('')
|
||||
: '<option value="">No accounts found</option>';
|
||||
} catch (error) {
|
||||
console.error('Error loading accounts:', error);
|
||||
}
|
||||
}
|
||||
75
frontend/js/components/search.js
Normal file
75
frontend/js/components/search.js
Normal file
@@ -0,0 +1,75 @@
|
||||
export function initSearch() {
|
||||
document.getElementById('searchForm').addEventListener('submit', handleSearch);
|
||||
}
|
||||
|
||||
async function handleSearch(e) {
|
||||
e.preventDefault();
|
||||
const account = document.getElementById('accountSelect').value;
|
||||
const query = document.getElementById('searchQuery').value;
|
||||
|
||||
if (!account || !query) return;
|
||||
|
||||
const container = document.getElementById('resultsContainer');
|
||||
container.innerHTML = '<img src="https://i.imgur.com/VUjU03O.gif" alt="Loading..." class="loading-gif">';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/search?account=${encodeURIComponent(account)}&query=${encodeURIComponent(query)}`);
|
||||
const data = await response.json();
|
||||
displayResults(data);
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error);
|
||||
container.innerHTML = '<div class="error">Search failed. Please try again.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayResults(data) {
|
||||
const container = document.getElementById('resultsContainer');
|
||||
container.innerHTML = '';
|
||||
|
||||
['albums', 'tracks'].forEach(type => {
|
||||
if (data[type]?.length > 0) {
|
||||
const section = document.createElement('details');
|
||||
section.className = 'category';
|
||||
section.innerHTML = `
|
||||
<summary>${type.charAt(0).toUpperCase() + type.slice(1)} (${data[type].length})</summary>
|
||||
<div class="items-grid">
|
||||
${data[type].map(item => `
|
||||
<div class="container-result">
|
||||
${createItemCard(item)}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(section);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createItemCard(item) {
|
||||
return `
|
||||
<div class="container-result">
|
||||
<div class="item-card"
|
||||
data-spotify-id="${item.spotify_id}"
|
||||
data-type="${item.type}"
|
||||
data-total-tracks="${item.total_tracks || 0}">
|
||||
${item.image_url ? `<img src="${item.image_url}" alt="${item.name}" class="item-image">` : ''}
|
||||
<div class="item-info">
|
||||
<div>
|
||||
<h3>${item.name}${item.explicit ? '<span class="explicit">E</span>' : ''}</h3>
|
||||
${item.artists ? `<p>Artists: ${item.artists.join(', ')}</p>` : ''}
|
||||
${item.owner ? `<p>Owner: ${item.owner}</p>` : ''}
|
||||
${item.total_tracks ? `<p>Total Tracks: ${item.total_tracks}</p>` : ''}
|
||||
</div>
|
||||
${['album', 'track'].includes(item.type) ? `
|
||||
<button class="download-btn">
|
||||
<img src="https://brandeps.com/icon-download/D/Download-icon-vector-09.svg"
|
||||
alt="Download"
|
||||
width="20"
|
||||
height="20">
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user