update
This commit is contained in:
@@ -12,3 +12,13 @@ async def search(query: str, type: str = "track"):
|
||||
return JSONResponse(results)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.get("/artist/{artist_id}/albums")
|
||||
async def get_artist_albums(artist_id: str):
|
||||
wrapper = TidalWrapper()
|
||||
try:
|
||||
albums = wrapper.get_artist_albums(artist_id)
|
||||
return JSONResponse(albums)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ class DownloadManager:
|
||||
tidal_obj=tidal,
|
||||
path_base=settings.data.download_base_path,
|
||||
fn_logger=logger_tidal,
|
||||
skip_existing=False,
|
||||
skip_existing=True,
|
||||
progress_gui=mock_progress,
|
||||
progress=mock_progress_rich,
|
||||
event_abort=event_abort,
|
||||
|
||||
@@ -153,9 +153,67 @@ class TidalWrapper:
|
||||
return None
|
||||
return f"https://resources.tidal.com/images/{uuid.replace('-', '/')}/{width}x{height}.jpg"
|
||||
|
||||
def get_artist_albums(self, artist_id: str, limit: int = 50):
|
||||
"""Fetch all albums for a given artist"""
|
||||
if not self.is_authenticated():
|
||||
raise Exception("Not authenticated")
|
||||
|
||||
artist = self.session.artist(artist_id)
|
||||
albums = artist.get_albums(limit=limit)
|
||||
|
||||
# Deduplicate albums by title, keeping highest quality version
|
||||
albums_dict = {}
|
||||
for album in albums:
|
||||
title = album.name
|
||||
|
||||
# If we haven't seen this album title before, add it
|
||||
if title not in albums_dict:
|
||||
albums_dict[title] = album
|
||||
else:
|
||||
# If we've seen it, keep the one with higher quality
|
||||
existing = albums_dict[title]
|
||||
|
||||
# Prioritize by audio quality (if available)
|
||||
# TIDAL albums with better quality often have higher audio_quality values
|
||||
# Also prefer explicit versions over clean versions (explicit flag)
|
||||
# And prefer newer releases (release_date)
|
||||
|
||||
# Simple heuristic: prefer albums with explicit tag and newer release date
|
||||
keep_new = False
|
||||
if hasattr(album, 'explicit') and hasattr(existing, 'explicit'):
|
||||
if album.explicit and not existing.explicit:
|
||||
keep_new = True
|
||||
|
||||
# If explicit status is same, prefer newer release
|
||||
if not keep_new and hasattr(album, 'release_date') and hasattr(existing, 'release_date'):
|
||||
if album.release_date and existing.release_date:
|
||||
if album.release_date > existing.release_date:
|
||||
keep_new = True
|
||||
|
||||
if keep_new:
|
||||
albums_dict[title] = album
|
||||
|
||||
# Convert deduplicated dict to output format
|
||||
output = []
|
||||
for album in albums_dict.values():
|
||||
output.append({
|
||||
"id": album.id,
|
||||
"title": album.name,
|
||||
"artist": album.artist.name,
|
||||
"tracks": album.num_tracks,
|
||||
"release_date": str(album.release_date) if album.release_date else "Unknown",
|
||||
"cover": self._get_image_url(album.cover),
|
||||
"type": "album"
|
||||
})
|
||||
|
||||
# Sort by release date (newest first)
|
||||
output.sort(key=lambda x: x['release_date'], reverse=True)
|
||||
|
||||
return output
|
||||
|
||||
def get_track(self, track_id: str):
|
||||
return self.session.track(track_id)
|
||||
|
||||
def get_album(self, album_id: str):
|
||||
return self.session.album(album_id)
|
||||
|
||||
|
||||
@@ -82,6 +82,106 @@ button:hover {
|
||||
color: #ffff00;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #1a1a1a;
|
||||
margin: 5% auto;
|
||||
padding: 30px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
width: 90%;
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #1db954;
|
||||
}
|
||||
|
||||
.album-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.album-item {
|
||||
background: #2d2d2d;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.album-item:hover {
|
||||
border-color: #1db954;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.album-item.selected {
|
||||
border-color: #1db954;
|
||||
background: #2a4a2a;
|
||||
}
|
||||
|
||||
.album-item img {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.album-item-info {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.album-item-info strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.album-item-info small {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modal-buttons button {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Album Selection Modal -->
|
||||
<div id="album-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h3 id="modal-artist-name">Albums</h3>
|
||||
<div id="album-grid" class="album-grid"></div>
|
||||
<div class="modal-buttons">
|
||||
<button onclick="downloadSelectedAlbums()" style="background-color: #1db954;">Download Selected</button>
|
||||
<button onclick="downloadAllAlbums()" style="background-color: #0d7a3a;">Download All</button>
|
||||
<button onclick="closeAlbumModal()" style="background-color: #666;">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card" style="margin-top: 20px; text-align: center; color: #888;">
|
||||
<small>System IP: <span id="system-ip">Loading...</span></small>
|
||||
@@ -124,13 +137,19 @@
|
||||
function createResultItem(item) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'result-item';
|
||||
|
||||
// For artists, show album selection modal instead of direct download
|
||||
const buttonHtml = item.type === 'artist'
|
||||
? `<button onclick="showArtistAlbums('${item.id}', '${item.title.replace(/'/g, "\\'")}')">View Albums</button>`
|
||||
: `<button onclick="download('${item.type}', '${item.id}')">Download</button>`;
|
||||
|
||||
div.innerHTML = `
|
||||
<img src="${item.cover || '/static/placeholder.png'}" alt="Cover">
|
||||
<div class="result-info">
|
||||
<strong>${item.title}</strong><br>
|
||||
${item.artist ? item.artist + ' - ' : ''}${item.album || ''}
|
||||
</div>
|
||||
<button onclick="download('${item.type}', '${item.id}')">Download</button>
|
||||
${buttonHtml}
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
@@ -189,6 +208,100 @@
|
||||
updateQueue();
|
||||
}
|
||||
|
||||
// Album Selection Modal Functions
|
||||
let currentAlbums = [];
|
||||
let selectedAlbumIds = new Set();
|
||||
|
||||
async function showArtistAlbums(artistId, artistName) {
|
||||
try {
|
||||
const response = await fetch(`/search/artist/${artistId}/albums`);
|
||||
currentAlbums = await response.json();
|
||||
|
||||
document.getElementById('modal-artist-name').textContent = `Albums by ${artistName}`;
|
||||
|
||||
const grid = document.getElementById('album-grid');
|
||||
grid.innerHTML = '';
|
||||
selectedAlbumIds.clear();
|
||||
|
||||
if (currentAlbums.length === 0) {
|
||||
grid.innerHTML = '<p>No albums found.</p>';
|
||||
} else {
|
||||
currentAlbums.forEach(album => {
|
||||
const albumDiv = document.createElement('div');
|
||||
albumDiv.className = 'album-item';
|
||||
albumDiv.dataset.albumId = album.id;
|
||||
albumDiv.onclick = () => toggleAlbumSelection(album.id);
|
||||
|
||||
albumDiv.innerHTML = `
|
||||
<img src="${album.cover || '/static/placeholder.png'}" alt="${album.title}">
|
||||
<div class="album-item-info">
|
||||
<strong>${album.title}</strong>
|
||||
<small>${album.tracks} tracks • ${album.release_date}</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
grid.appendChild(albumDiv);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('album-modal').style.display = 'block';
|
||||
} catch (e) {
|
||||
alert('Failed to load albums: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAlbumSelection(albumId) {
|
||||
const albumDiv = document.querySelector(`[data-album-id="${albumId}"]`);
|
||||
|
||||
if (selectedAlbumIds.has(albumId)) {
|
||||
selectedAlbumIds.delete(albumId);
|
||||
albumDiv.classList.remove('selected');
|
||||
} else {
|
||||
selectedAlbumIds.add(albumId);
|
||||
albumDiv.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadSelectedAlbums() {
|
||||
if (selectedAlbumIds.size === 0) {
|
||||
alert('Please select at least one album to download.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const albumId of selectedAlbumIds) {
|
||||
await download('album', albumId);
|
||||
}
|
||||
|
||||
closeAlbumModal();
|
||||
}
|
||||
|
||||
async function downloadAllAlbums() {
|
||||
if (currentAlbums.length === 0) {
|
||||
alert('No albums to download.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const album of currentAlbums) {
|
||||
await download('album', album.id);
|
||||
}
|
||||
|
||||
closeAlbumModal();
|
||||
}
|
||||
|
||||
function closeAlbumModal() {
|
||||
document.getElementById('album-modal').style.display = 'none';
|
||||
selectedAlbumIds.clear();
|
||||
currentAlbums = [];
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('album-modal');
|
||||
if (event.target === modal) {
|
||||
closeAlbumModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Poll queue
|
||||
setInterval(updateQueue, 2000);
|
||||
updateQueue();
|
||||
|
||||
@@ -24,7 +24,7 @@ class Settings:
|
||||
# multi_thread: bool = False
|
||||
download_delay: bool = True
|
||||
download_base_path: str = "~/download"
|
||||
quality_audio: Quality = Quality.low_320k
|
||||
quality_audio: Quality = Quality.hi_res_lossless
|
||||
quality_video: QualityVideo = QualityVideo.P480
|
||||
download_dolby_atmos: bool = False
|
||||
format_album: str = (
|
||||
|
||||
Reference in New Issue
Block a user