This commit is contained in:
2025-12-02 20:16:14 +01:00
parent b0c017ca91
commit d5169fbec7
6 changed files with 284 additions and 3 deletions

View File

@@ -12,3 +12,13 @@ async def search(query: str, type: str = "track"):
return JSONResponse(results) return JSONResponse(results)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=str(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))

View File

@@ -206,7 +206,7 @@ class DownloadManager:
tidal_obj=tidal, tidal_obj=tidal,
path_base=settings.data.download_base_path, path_base=settings.data.download_base_path,
fn_logger=logger_tidal, fn_logger=logger_tidal,
skip_existing=False, skip_existing=True,
progress_gui=mock_progress, progress_gui=mock_progress,
progress=mock_progress_rich, progress=mock_progress_rich,
event_abort=event_abort, event_abort=event_abort,

View File

@@ -153,9 +153,67 @@ class TidalWrapper:
return None return None
return f"https://resources.tidal.com/images/{uuid.replace('-', '/')}/{width}x{height}.jpg" 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): def get_track(self, track_id: str):
return self.session.track(track_id) return self.session.track(track_id)
def get_album(self, album_id: str): def get_album(self, album_id: str):
return self.session.album(album_id) return self.session.album(album_id)

View File

@@ -82,6 +82,106 @@ button:hover {
color: #ffff00; 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 { .status-failed {
color: #ff0000; color: #ff0000;
} }

View File

@@ -34,6 +34,19 @@
</div> </div>
</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="container">
<div class="card" style="margin-top: 20px; text-align: center; color: #888;"> <div class="card" style="margin-top: 20px; text-align: center; color: #888;">
<small>System IP: <span id="system-ip">Loading...</span></small> <small>System IP: <span id="system-ip">Loading...</span></small>
@@ -124,13 +137,19 @@
function createResultItem(item) { function createResultItem(item) {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'result-item'; 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 = ` div.innerHTML = `
<img src="${item.cover || '/static/placeholder.png'}" alt="Cover"> <img src="${item.cover || '/static/placeholder.png'}" alt="Cover">
<div class="result-info"> <div class="result-info">
<strong>${item.title}</strong><br> <strong>${item.title}</strong><br>
${item.artist ? item.artist + ' - ' : ''}${item.album || ''} ${item.artist ? item.artist + ' - ' : ''}${item.album || ''}
</div> </div>
<button onclick="download('${item.type}', '${item.id}')">Download</button> ${buttonHtml}
`; `;
return div; return div;
} }
@@ -189,6 +208,100 @@
updateQueue(); 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 // Poll queue
setInterval(updateQueue, 2000); setInterval(updateQueue, 2000);
updateQueue(); updateQueue();

View File

@@ -24,7 +24,7 @@ class Settings:
# multi_thread: bool = False # multi_thread: bool = False
download_delay: bool = True download_delay: bool = True
download_base_path: str = "~/download" download_base_path: str = "~/download"
quality_audio: Quality = Quality.low_320k quality_audio: Quality = Quality.hi_res_lossless
quality_video: QualityVideo = QualityVideo.P480 quality_video: QualityVideo = QualityVideo.P480
download_dolby_atmos: bool = False download_dolby_atmos: bool = False
format_album: str = ( format_album: str = (