From d5169fbec725f95d981ab68901ac0b18be82c7ca Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Tue, 2 Dec 2025 20:16:14 +0100 Subject: [PATCH] update --- app/routers/search.py | 10 +++ app/services/download_manager.py | 2 +- app/services/tidal_wrapper.py | 58 ++++++++++++++++ app/static/style.css | 100 +++++++++++++++++++++++++++ app/templates/index.html | 115 ++++++++++++++++++++++++++++++- tidal_dl_ng/model/cfg.py | 2 +- 6 files changed, 284 insertions(+), 3 deletions(-) diff --git a/app/routers/search.py b/app/routers/search.py index 922870d..e92a992 100644 --- a/app/routers/search.py +++ b/app/routers/search.py @@ -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)) + diff --git a/app/services/download_manager.py b/app/services/download_manager.py index 0fcb4e5..a2aab21 100644 --- a/app/services/download_manager.py +++ b/app/services/download_manager.py @@ -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, diff --git a/app/services/tidal_wrapper.py b/app/services/tidal_wrapper.py index 96f0bab..5d94773 100644 --- a/app/services/tidal_wrapper.py +++ b/app/services/tidal_wrapper.py @@ -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) + diff --git a/app/static/style.css b/app/static/style.css index e7001ea..db72d09 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -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; } diff --git a/app/templates/index.html b/app/templates/index.html index b54bb35..73bff91 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -34,6 +34,19 @@ + + +
System IP: Loading... @@ -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' + ? `` + : ``; + div.innerHTML = ` Cover
${item.title}
${item.artist ? item.artist + ' - ' : ''}${item.album || ''}
- + ${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 = '

No albums found.

'; + } 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 = ` + ${album.title} +
+ ${album.title} + ${album.tracks} tracks • ${album.release_date} +
+ `; + + 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(); diff --git a/tidal_dl_ng/model/cfg.py b/tidal_dl_ng/model/cfg.py index bfca708..b1fb92a 100644 --- a/tidal_dl_ng/model/cfg.py +++ b/tidal_dl_ng/model/cfg.py @@ -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 = (