diff --git a/config/settings.py b/config/settings.py index 94105426..d94b9f53 100644 --- a/config/settings.py +++ b/config/settings.py @@ -495,6 +495,16 @@ class ConfigManager: "hifi_download": { "quality": "lossless", # Options: "low", "high", "lossless", "hires" }, + "hifi": { + "embed_tags": True, + "tags": { + "track_id": True, + "artist_id": True, + "isrc": True, + "bpm": True, + "copyright": True, + } + }, "lidarr_download": { "url": "", "api_key": "", diff --git a/core/hifi_client.py b/core/hifi_client.py index 7b3a8823..c1209716 100644 --- a/core/hifi_client.py +++ b/core/hifi_client.py @@ -282,24 +282,30 @@ class HiFiClient: def _parse_track(self, item: dict) -> Dict: artist_name = 'Unknown Artist' + artist_id = None artists_raw = item.get('artists', item.get('artist')) if isinstance(artists_raw, list): names = [] for a in artists_raw: if isinstance(a, dict): names.append(a.get('name', '')) + if artist_id is None: + artist_id = a.get('id') elif isinstance(a, str): names.append(a) artist_name = ', '.join(n for n in names if n) or 'Unknown Artist' elif isinstance(artists_raw, dict): artist_name = artists_raw.get('name', 'Unknown Artist') + artist_id = artists_raw.get('id') elif isinstance(artists_raw, str): artist_name = artists_raw album_raw = item.get('album', {}) album_name = '' + album_id = None if isinstance(album_raw, dict): album_name = album_raw.get('title', album_raw.get('name', '')) + album_id = album_raw.get('id') elif isinstance(album_raw, str): album_name = album_raw @@ -308,12 +314,16 @@ class HiFiClient: return { 'id': item.get('id'), + 'artist_id': artist_id, + 'album_id': album_id, 'title': item.get('title', item.get('name', 'Unknown')), 'artist': artist_name, 'album': album_name, 'duration_ms': int(duration_ms) if duration_ms else 0, 'track_number': item.get('trackNumber', item.get('track_number')), 'isrc': item.get('isrc'), + 'bpm': item.get('bpm'), + 'copyright': item.get('copyright'), 'explicit': item.get('explicit', False), 'quality': item.get('audioQuality', item.get('quality', '')), } @@ -549,6 +559,15 @@ class HiFiClient: title=track.get('title'), album=track.get('album'), track_number=track.get('track_number'), + _source_metadata={ + 'source': 'hifi', + 'track_id': track.get('id'), + 'artist_id': track.get('artist_id'), + 'album_id': track.get('album_id'), + 'isrc': track.get('isrc'), + 'bpm': track.get('bpm'), + 'copyright': track.get('copyright'), + }, ) async def download(self, username: str, filename: str, file_size: int = 0) -> Optional[str]: diff --git a/core/imports/context.py b/core/imports/context.py index 4ed44b56..a3e4a4c3 100644 --- a/core/imports/context.py +++ b/core/imports/context.py @@ -257,6 +257,8 @@ def get_source_tag_names(source: str) -> Dict[str, Optional[str]]: return {"track": None, "artist": None, "album": None} if source_name == "discogs": return {"track": None, "artist": None, "album": None} + if source_name == "hifi": + return {"track": "HIFI_TRACK_ID", "artist": "HIFI_ARTIST_ID", "album": None} return {"track": None, "artist": None, "album": None} @@ -272,6 +274,8 @@ def get_library_source_id_columns(source: str) -> Dict[str, Optional[str]]: return {"artist": "soul_id", "album": "soul_id", "track": "soul_id", "track_album": "album_soul_id"} if source_name == "discogs": return {"artist": "discogs_id", "album": "discogs_id", "track": None} + if source_name == "hifi": + return {"artist": "hifi_artist_id", "album": None, "track": "hifi_track_id"} return {} diff --git a/core/metadata/enrichment.py b/core/metadata/enrichment.py index 82f462d6..6317cedf 100644 --- a/core/metadata/enrichment.py +++ b/core/metadata/enrichment.py @@ -37,6 +37,7 @@ def build_metadata_enrichment_runtime( deezer_worker: Any | None = None, audiodb_worker: Any | None = None, tidal_client: Any | None = None, + hifi_client: Any | None = None, qobuz_enrichment_worker: Any | None = None, lastfm_worker: Any | None = None, genius_worker: Any | None = None, @@ -49,6 +50,7 @@ def build_metadata_enrichment_runtime( deezer_worker=deezer_worker, audiodb_worker=audiodb_worker, tidal_client=tidal_client, + hifi_client=hifi_client, qobuz_enrichment_worker=qobuz_enrichment_worker, lastfm_worker=lastfm_worker, genius_worker=genius_worker, diff --git a/core/metadata/source.py b/core/metadata/source.py index 478eeb02..0c101f5e 100644 --- a/core/metadata/source.py +++ b/core/metadata/source.py @@ -124,12 +124,14 @@ SOURCE_TAG_CONFIG = { "AUDIODB_TRACK_ID": "audiodb.tags.track_id", "TIDAL_TRACK_ID": "tidal.tags.track_id", "TIDAL_ARTIST_ID": "tidal.tags.artist_id", + "HIFI_TRACK_ID": "hifi.tags.track_id", + "HIFI_ARTIST_ID": "hifi.tags.artist_id", "QOBUZ_TRACK_ID": "qobuz.tags.track_id", "QOBUZ_ARTIST_ID": "qobuz.tags.artist_id", "GENIUS_TRACK_ID": "genius.tags.track_id", } -DEFAULT_SOURCE_ORDER = ["musicbrainz", "deezer", "audiodb", "tidal", "qobuz", "lastfm", "genius"] +DEFAULT_SOURCE_ORDER = ["musicbrainz", "deezer", "audiodb", "tidal", "hifi", "qobuz", "lastfm", "genius"] ID3_TAG_MAP = { "MUSICBRAINZ_RECORDING_ID": ("UFID", "http://musicbrainz.org"), @@ -452,6 +454,9 @@ def _process_tidal_source(pp: dict, metadata: dict, cfg, runtime, track_title: s td_details = _call_source_lookup("Tidal track details", tidal_client.get_track, str(td_track_id)) if td_details: pp["tidal_isrc"] = td_details.get("isrc") + td_bpm = td_details.get("bpm") + if td_bpm and td_bpm > 0: + pp["tidal_bpm"] = td_bpm td_copyright = td_details.get("copyright") if isinstance(td_copyright, dict): td_copyright = td_copyright.get("text", td_copyright.get("name", "")) @@ -465,6 +470,47 @@ def _process_tidal_source(pp: dict, metadata: dict, cfg, runtime, track_title: s pp["release_year"] = td_release[:4] +def _process_hifi_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None: + if cfg.get("hifi.embed_tags", True) is False: + return + if not track_title or not artist_name: + return + + hifi_client = getattr(runtime, "hifi_client", None) + if not hifi_client: + return + hifi_results = _call_source_lookup("HiFi track", hifi_client.search_tracks, track_title, artist_name) + if hifi_results and len(hifi_results) > 0: + hifi_track = hifi_results[0] + if _names_match(hifi_track.get("title", ""), track_title): + hifi_track_id = hifi_track.get("id") + if hifi_track_id: + pp["id_tags"]["HIFI_TRACK_ID"] = str(hifi_track_id) + hifi_artist_id = hifi_track.get("artist_id") + if hifi_artist_id: + pp["id_tags"]["HIFI_ARTIST_ID"] = str(hifi_artist_id) + if hifi_track_id: + hifi_details = _call_source_lookup("HiFi track details", hifi_client.get_track_info, hifi_track_id) + if hifi_details: + hifi_isrc = hifi_details.get("isrc") + if hifi_isrc: + pp["hifi_isrc"] = hifi_isrc + hifi_bpm = hifi_details.get("bpm") + if hifi_bpm and hifi_bpm > 0: + pp["hifi_bpm"] = hifi_bpm + hifi_copyright = hifi_details.get("copyright") + if hifi_copyright: + pp["hifi_copyright"] = hifi_copyright + if not pp["release_year"]: + hifi_album_id = hifi_track.get("album_id") + if hifi_album_id: + hifi_album = _call_source_lookup("HiFi album", hifi_client.get_album, hifi_album_id) + if hifi_album: + hifi_release = str(hifi_album.get("release_date", "") or "") + if len(hifi_release) >= 4 and hifi_release[:4].isdigit(): + pp["release_year"] = hifi_release[:4] + + def _process_qobuz_source(pp: dict, metadata: dict, cfg, runtime, track_title: str, artist_name: str) -> None: if cfg.get("qobuz.embed_tags", True) is False: return @@ -572,6 +618,8 @@ def _process_source_enrichment(source_name: str, pp: dict, metadata: dict, cfg, _process_audiodb_source(pp, metadata, cfg, runtime, track_title, artist_name) elif source_name == "tidal": _process_tidal_source(pp, metadata, cfg, runtime, track_title, artist_name) + elif source_name == "hifi": + _process_hifi_source(pp, metadata, cfg, runtime, track_title, artist_name) elif source_name == "qobuz": _process_qobuz_source(pp, metadata, cfg, runtime, track_title, artist_name) elif source_name == "lastfm": @@ -634,15 +682,23 @@ def _write_embedded_metadata(audio_file, metadata: dict, pp: dict, cfg, symbols) audio_file["\xa9day"] = [release_year] logger.info("Date tag: %s", release_year) - if _tag_enabled(cfg, "deezer.tags.bpm") and pp["deezer_bpm"] and pp["deezer_bpm"] > 0: - bpm_int = int(pp["deezer_bpm"]) + bpm_candidates = [] + if pp["deezer_bpm"] and pp["deezer_bpm"] > 0 and _tag_enabled(cfg, "deezer.tags.bpm"): + bpm_candidates.append(("Deezer", pp["deezer_bpm"])) + if pp["tidal_bpm"] and pp["tidal_bpm"] > 0 and _tag_enabled(cfg, "tidal.tags.bpm"): + bpm_candidates.append(("Tidal", pp["tidal_bpm"])) + if pp["hifi_bpm"] and pp["hifi_bpm"] > 0 and _tag_enabled(cfg, "hifi.tags.bpm"): + bpm_candidates.append(("HiFi", pp["hifi_bpm"])) + if bpm_candidates: + bpm_source, bpm_val = bpm_candidates[0] + bpm_int = int(bpm_val) if isinstance(audio_file.tags, symbols.ID3): audio_file.tags.add(symbols.TBPM(encoding=3, text=[str(bpm_int)])) elif is_vorbis_like(audio_file, symbols): audio_file["BPM"] = [str(bpm_int)] elif isinstance(audio_file, symbols.MP4): audio_file["tmpo"] = [bpm_int] - logger.info("BPM: %s", bpm_int) + logger.info("BPM (%s): %s", bpm_source, bpm_int) if _tag_enabled(cfg, "audiodb.tags.mood") and pp["audiodb_mood"]: if isinstance(audio_file.tags, symbols.ID3): @@ -699,6 +755,8 @@ def _write_embedded_metadata(audio_file, metadata: dict, pp: dict, cfg, symbols) isrc_candidates.append(("Deezer", pp["deezer_isrc"])) if pp["tidal_isrc"] and _tag_enabled(cfg, "tidal.tags.isrc"): isrc_candidates.append(("Tidal", pp["tidal_isrc"])) + if pp["hifi_isrc"] and _tag_enabled(cfg, "hifi.tags.isrc"): + isrc_candidates.append(("HiFi", pp["hifi_isrc"])) if pp["qobuz_isrc"] and _tag_enabled(cfg, "qobuz.tags.isrc"): isrc_candidates.append(("Qobuz", pp["qobuz_isrc"])) if isrc_candidates: @@ -716,6 +774,8 @@ def _write_embedded_metadata(audio_file, metadata: dict, pp: dict, cfg, symbols) copyright_candidates.append(("Tidal", pp["tidal_copyright"])) if pp["qobuz_copyright"] and _tag_enabled(cfg, "qobuz.tags.copyright"): copyright_candidates.append(("Qobuz", pp["qobuz_copyright"])) + if pp["hifi_copyright"] and _tag_enabled(cfg, "hifi.tags.copyright"): + copyright_candidates.append(("HiFi", pp["hifi_copyright"])) if copyright_candidates: copyright_source, final_copyright = copyright_candidates[0] if isinstance(audio_file.tags, symbols.ID3): @@ -963,11 +1023,15 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N "isrc": None, "deezer_bpm": None, "deezer_isrc": None, + "tidal_bpm": None, + "hifi_bpm": None, + "hifi_copyright": None, "audiodb_mood": None, "audiodb_style": None, "audiodb_genre": None, "tidal_isrc": None, "tidal_copyright": None, + "hifi_isrc": None, "qobuz_isrc": None, "qobuz_copyright": None, "qobuz_label": None, @@ -981,12 +1045,46 @@ def embed_source_ids(audio_file, metadata: dict, context: dict = None, runtime=N if not isinstance(source_order, list) or not source_order: source_order = DEFAULT_SOURCE_ORDER + # If this download came from HiFi, use cached metadata from the download + # pipeline instead of re-searching the HiFi API. + original_search = get_import_original_search(context) + cached_meta = original_search.get("_source_metadata") or {} + if cached_meta.get("source") == "hifi": + if _tag_enabled(cfg, "hifi.embed_tags"): + if cfg.get("hifi.tags.track_id", True) and cached_meta.get("track_id"): + pp["id_tags"]["HIFI_TRACK_ID"] = str(cached_meta["track_id"]) + if cfg.get("hifi.tags.artist_id", True) and cached_meta.get("artist_id"): + pp["id_tags"]["HIFI_ARTIST_ID"] = str(cached_meta["artist_id"]) + if cfg.get("hifi.tags.isrc", True) and cached_meta.get("isrc"): + pp["hifi_isrc"] = cached_meta["isrc"] + if cfg.get("hifi.tags.bpm", True) and cached_meta.get("bpm"): + pp["hifi_bpm"] = cached_meta["bpm"] + if cfg.get("hifi.tags.copyright", True) and cached_meta.get("copyright"): + pp["hifi_copyright"] = cached_meta["copyright"] + source_order = [s for s in source_order if s != "hifi"] + + # If this download came from Tidal, use cached metadata from the download + # pipeline instead of re-searching the Tidal API. + if cached_meta.get("source") == "tidal": + if _tag_enabled(cfg, "tidal.embed_tags"): + if cfg.get("tidal.tags.track_id", True) and cached_meta.get("track_id"): + pp["id_tags"]["TIDAL_TRACK_ID"] = str(cached_meta["track_id"]) + if cfg.get("tidal.tags.artist_id", True) and cached_meta.get("artist_id"): + pp["id_tags"]["TIDAL_ARTIST_ID"] = str(cached_meta["artist_id"]) + if cfg.get("tidal.tags.isrc", True) and cached_meta.get("isrc"): + pp["tidal_isrc"] = cached_meta["isrc"] + if cfg.get("tidal.tags.bpm", True) and cached_meta.get("bpm"): + pp["tidal_bpm"] = cached_meta["bpm"] + if cfg.get("tidal.tags.copyright", True) and cached_meta.get("copyright"): + pp["tidal_copyright"] = cached_meta["copyright"] + source_order = [s for s in source_order if s != "tidal"] + db = get_database() for source_name in source_order: _process_source_enrichment(source_name, pp, metadata, cfg, runtime, track_title, artist_name) - if not pp["id_tags"] and not pp["deezer_bpm"] and not pp["deezer_isrc"] and not pp["audiodb_mood"] and not pp["audiodb_style"]: + if not pp["id_tags"] and not pp["deezer_bpm"] and not pp["deezer_isrc"] and not pp["tidal_bpm"] and not pp["hifi_bpm"] and not pp["hifi_copyright"] and not pp["audiodb_mood"] and not pp["audiodb_style"]: return release_year = _write_embedded_metadata(audio_file, metadata, pp, cfg, symbols) diff --git a/core/soulseek_client.py b/core/soulseek_client.py index 3110c305..e69c9439 100644 --- a/core/soulseek_client.py +++ b/core/soulseek_client.py @@ -79,6 +79,7 @@ class TrackResult(SearchResult): title: Optional[str] = None album: Optional[str] = None track_number: Optional[int] = None + _source_metadata: Optional[Dict[str, Any]] = None def __post_init__(self): self.result_type = "track" diff --git a/core/tidal_download_client.py b/core/tidal_download_client.py index ca76805d..f6db0632 100644 --- a/core/tidal_download_client.py +++ b/core/tidal_download_client.py @@ -432,6 +432,14 @@ class TidalDownloadClient: title=title, album=album_name, track_number=track.track_num, + _source_metadata={ + 'source': 'tidal', + 'track_id': track.id, + 'artist_id': track.artist.id if track.artist else None, + 'isrc': track.isrc or None, + 'bpm': track.bpm if track.bpm and track.bpm > 0 else None, + 'copyright': track.copyright or None, + }, ) return track_result diff --git a/tests/metadata/test_runtime_bundle.py b/tests/metadata/test_runtime_bundle.py index 415944af..8b6bed3e 100644 --- a/tests/metadata/test_runtime_bundle.py +++ b/tests/metadata/test_runtime_bundle.py @@ -23,6 +23,7 @@ def test_build_import_pipeline_runtime_exposes_expected_contract(): "deezer_worker", "audiodb_worker", "tidal_client", + "hifi_client", "qobuz_enrichment_worker", "lastfm_worker", "genius_worker", @@ -38,6 +39,7 @@ def test_build_metadata_enrichment_runtime_exposes_expected_contract(): "deezer_worker": object(), "audiodb_worker": object(), "tidal_client": object(), + "hifi_client": object(), "qobuz_enrichment_worker": object(), "lastfm_worker": object(), "genius_worker": object(), diff --git a/web_server.py b/web_server.py index 60314e6d..2b4be31d 100644 --- a/web_server.py +++ b/web_server.py @@ -14313,7 +14313,18 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in context, artist, album_info, - runtime=metadata_runtime or _build_metadata_enrichment_runtime(), + runtime=metadata_runtime or _build_metadata_enrichment_runtime( + mb_worker=mb_worker, + deezer_worker=deezer_worker, + audiodb_worker=audiodb_worker, + tidal_client=tidal_client, + qobuz_enrichment_worker=qobuz_enrichment_worker, + lastfm_worker=lastfm_worker, + genius_worker=genius_worker, + spotify_enrichment_worker=spotify_enrichment_worker, + itunes_enrichment_worker=itunes_enrichment_worker, + hifi_client=soulseek_client.hifi if soulseek_client else None, + ), ) @@ -14407,7 +14418,18 @@ def _post_process_matched_download_with_verification(context_key, context, file_ task_id, batch_id, _build_import_pipeline_runtime(), - _build_metadata_enrichment_runtime(), + _build_metadata_enrichment_runtime( + mb_worker=mb_worker, + deezer_worker=deezer_worker, + audiodb_worker=audiodb_worker, + tidal_client=tidal_client, + qobuz_enrichment_worker=qobuz_enrichment_worker, + lastfm_worker=lastfm_worker, + genius_worker=genius_worker, + spotify_enrichment_worker=spotify_enrichment_worker, + itunes_enrichment_worker=itunes_enrichment_worker, + hifi_client=soulseek_client.hifi if soulseek_client else None, + ), ) @@ -14520,7 +14542,18 @@ def _post_process_matched_download(context_key, context, file_path): context, file_path, _build_import_pipeline_runtime(), - metadata_runtime=_build_metadata_enrichment_runtime(), + metadata_runtime=_build_metadata_enrichment_runtime( + mb_worker=mb_worker, + deezer_worker=deezer_worker, + audiodb_worker=audiodb_worker, + tidal_client=tidal_client, + qobuz_enrichment_worker=qobuz_enrichment_worker, + lastfm_worker=lastfm_worker, + genius_worker=genius_worker, + spotify_enrichment_worker=spotify_enrichment_worker, + itunes_enrichment_worker=itunes_enrichment_worker, + hifi_client=soulseek_client.hifi if soulseek_client else None, + ), ) # Track stale transfer keys (completed in slskd but no context — e.g., from before app restart) diff --git a/webui/index.html b/webui/index.html index 31f00e67..1b9c2694 100644 --- a/webui/index.html +++ b/webui/index.html @@ -5217,13 +5217,14 @@ - 4 tags + 5 tags @@ -5275,6 +5276,24 @@ + +
+
+ + + 5 tags +
+ +
+
diff --git a/webui/static/downloads.js b/webui/static/downloads.js index 1720038b..8db1bc0e 100644 --- a/webui/static/downloads.js +++ b/webui/static/downloads.js @@ -3305,127 +3305,133 @@ function processModalStatusUpdate(playlistId, data) { // Note: Auto-show logic removed - wishlist modal visibility managed by user interaction only if (data.phase === 'cancelled') { - process.status = 'cancelled'; - - // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on cancel - if (playlistId.startsWith('youtube_')) { - const urlHash = playlistId.replace('youtube_', ''); - updateYouTubeCardPhase(urlHash, 'discovered'); - if (urlHash.startsWith('mirrored_')) { - updateMirroredCardPhase(urlHash, 'discovered'); + if (process.status !== 'cancelled') { + process.status = 'cancelled'; + + // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on cancel + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'discovered'); + if (urlHash.startsWith('mirrored_')) { + updateMirroredCardPhase(urlHash, 'discovered'); + } } - } - showToast(`Process cancelled for ${process.playlist.name}.`, 'info'); + showToast(`Process cancelled for ${process.playlist.name}.`, 'info'); + } } else if (data.phase === 'error') { - process.status = 'complete'; // Treat as complete to allow cleanup - updatePlaylistCardUI(playlistId); // Update card to show ready for review - - // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on error - if (playlistId.startsWith('youtube_')) { - const urlHash = playlistId.replace('youtube_', ''); - updateYouTubeCardPhase(urlHash, 'discovered'); - if (urlHash.startsWith('mirrored_')) { - updateMirroredCardPhase(urlHash, 'discovered'); + if (process.status !== 'complete') { + process.status = 'complete'; + updatePlaylistCardUI(playlistId); // Update card to show ready for review + + // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on error + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'discovered'); + if (urlHash.startsWith('mirrored_')) { + updateMirroredCardPhase(urlHash, 'discovered'); + } } - } - showToast(`Process for ${process.playlist.name} failed!`, 'error'); - } else { - process.status = 'complete'; - updatePlaylistCardUI(playlistId); // Update card to show ready for review - - // Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist - if (playlistId.startsWith('youtube_')) { - const urlHash = playlistId.replace('youtube_', ''); - updateYouTubeCardPhase(urlHash, 'download_complete'); - if (urlHash.startsWith('mirrored_')) { - updateMirroredCardPhase(urlHash, 'download_complete'); - } + showToast(`Process for ${process.playlist.name} failed!`, 'error'); } - - // Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist - if (playlistId.startsWith('tidal_')) { - const tidalPlaylistId = playlistId.replace('tidal_', ''); - if (tidalPlaylistStates[tidalPlaylistId]) { - tidalPlaylistStates[tidalPlaylistId].phase = 'download_complete'; - // Store the download process ID for potential modal rehydration - tidalPlaylistStates[tidalPlaylistId].download_process_id = process.batchId; - updateTidalCardPhase(tidalPlaylistId, 'download_complete'); - console.log(`✅ [Status Complete] Updated Tidal playlist ${tidalPlaylistId} to download_complete phase`); + } else { + if (process.status !== 'complete') { + process.status = 'complete'; + updatePlaylistCardUI(playlistId); // Update card to show ready for review + + // Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist + if (playlistId.startsWith('youtube_')) { + const urlHash = playlistId.replace('youtube_', ''); + updateYouTubeCardPhase(urlHash, 'download_complete'); + if (urlHash.startsWith('mirrored_')) { + updateMirroredCardPhase(urlHash, 'download_complete'); + } } - } - // Update Beatport chart phase to 'download_complete' if this is a Beatport chart - if (playlistId.startsWith('beatport_')) { - const urlHash = playlistId.replace('beatport_', ''); - const state = youtubePlaylistStates[urlHash]; - - if (state && state.is_beatport_playlist) { - const chartHash = state.beatport_chart_hash || urlHash; - - // Update frontend states - state.phase = 'download_complete'; - state.download_process_id = process.batchId; - if (beatportChartStates[chartHash]) { - beatportChartStates[chartHash].phase = 'download_complete'; + // Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist + if (playlistId.startsWith('tidal_')) { + const tidalPlaylistId = playlistId.replace('tidal_', ''); + if (tidalPlaylistStates[tidalPlaylistId]) { + tidalPlaylistStates[tidalPlaylistId].phase = 'download_complete'; + // Store the download process ID for potential modal rehydration + tidalPlaylistStates[tidalPlaylistId].download_process_id = process.batchId; + updateTidalCardPhase(tidalPlaylistId, 'download_complete'); + console.log(`✅ [Status Complete] Updated Tidal playlist ${tidalPlaylistId} to download_complete phase`); } + } - // Update card UI - updateBeatportCardPhase(chartHash, 'download_complete'); - - // Update backend state - try { - fetch(`/api/beatport/charts/update-phase/${chartHash}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - phase: 'download_complete', - download_process_id: process.batchId - }) - }); - } catch (error) { - console.warn('⚠️ Error updating backend Beatport phase to download_complete:', error); + // Update Beatport chart phase to 'download_complete' if this is a Beatport chart + if (playlistId.startsWith('beatport_')) { + const urlHash = playlistId.replace('beatport_', ''); + const state = youtubePlaylistStates[urlHash]; + + if (state && state.is_beatport_playlist) { + const chartHash = state.beatport_chart_hash || urlHash; + + // Update frontend states + state.phase = 'download_complete'; + state.download_process_id = process.batchId; + if (beatportChartStates[chartHash]) { + beatportChartStates[chartHash].phase = 'download_complete'; + } + + // Update card UI + updateBeatportCardPhase(chartHash, 'download_complete'); + + // Update backend state + try { + fetch(`/api/beatport/charts/update-phase/${chartHash}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + phase: 'download_complete', + download_process_id: process.batchId + }) + }); + } catch (error) { + console.warn('⚠️ Error updating backend Beatport phase to download_complete:', error); + } + + console.log(`✅ [Status Complete] Updated Beatport chart ${chartHash} to download_complete phase`); } - - console.log(`✅ [Status Complete] Updated Beatport chart ${chartHash} to download_complete phase`); } - } - // Handle background wishlist processing completion specially - if (isBackgroundWishlist) { - console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${notFoundCount} not found, ${failedOrCancelledCount} failed`); + // Handle background wishlist processing completion specially + if (isBackgroundWishlist) { + console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${notFoundCount} not found, ${failedOrCancelledCount} failed`); - // Reset modal to idle state to prevent "complete" phase disruption - setTimeout(() => { - resetWishlistModalToIdleState(); - // Server-side auto-processing will handle next cycle automatically - }, 500); + // Reset modal to idle state to prevent "complete" phase disruption + setTimeout(() => { + resetWishlistModalToIdleState(); + // Server-side auto-processing will handle next cycle automatically + }, 500); - return; // Skip normal completion handling - } + return; // Skip normal completion handling + } - // Show completion summary with wishlist stats (matching sync.py behavior) - let completionMessage = `Process complete for ${process.playlist.name}!`; - let messageType = 'success'; - - // Check for wishlist summary from backend (added when failed/cancelled tracks are processed) - if (data.wishlist_summary) { - const summary = data.wishlist_summary; - let summaryParts = [`Downloaded: ${completedCount}`]; - if (notFoundCount > 0) summaryParts.push(`Not Found: ${notFoundCount}`); - if (failedOrCancelledCount > 0) summaryParts.push(`Failed: ${failedOrCancelledCount}`); - completionMessage = `Download process complete! ${summaryParts.join(', ')}.`; - - if (summary.tracks_added > 0) { - completionMessage += ` Added ${summary.tracks_added} failed track${summary.tracks_added !== 1 ? 's' : ''} to wishlist for automatic retry.`; - } else if (summary.total_failed > 0) { - completionMessage += ` ${summary.total_failed} track${summary.total_failed !== 1 ? 's' : ''} could not be added to wishlist.`; - messageType = 'warning'; + // Show completion summary with wishlist stats (matching sync.py behavior) + let completionMessage = `Process complete for ${process.playlist.name}!`; + let messageType = 'success'; + + // Check for wishlist summary from backend (added when failed/cancelled tracks are processed) + if (data.wishlist_summary) { + const summary = data.wishlist_summary; + let summaryParts = [`Downloaded: ${completedCount}`]; + if (notFoundCount > 0) summaryParts.push(`Not Found: ${notFoundCount}`); + if (failedOrCancelledCount > 0) summaryParts.push(`Failed: ${failedOrCancelledCount}`); + completionMessage = `Download process complete! ${summaryParts.join(', ')}.`; + + if (summary.tracks_added > 0) { + completionMessage += ` Added ${summary.tracks_added} failed track${summary.tracks_added !== 1 ? 's' : ''} to wishlist for automatic retry.`; + } else if (summary.total_failed > 0) { + completionMessage += ` ${summary.total_failed} track${summary.total_failed !== 1 ? 's' : ''} could not be added to wishlist.`; + messageType = 'warning'; + } } - } - showToast(completionMessage, messageType); + showToast(completionMessage, messageType); + } } document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none'; diff --git a/webui/static/settings.js b/webui/static/settings.js index dfa092bb..8b9aa005 100644 --- a/webui/static/settings.js +++ b/webui/static/settings.js @@ -978,6 +978,7 @@ async function loadSettingsData() { document.getElementById('embed-qobuz').checked = settings.qobuz?.embed_tags !== false; document.getElementById('embed-lastfm').checked = settings.lastfm?.embed_tags !== false; document.getElementById('embed-genius').checked = settings.genius?.embed_tags !== false; + document.getElementById('embed-hifi').checked = settings.hifi?.embed_tags !== false; // Load per-tag toggles from data-config attributes document.querySelectorAll('[data-config]').forEach(cb => { const path = cb.dataset.config.split('.'); @@ -986,7 +987,7 @@ async function loadSettingsData() { cb.checked = val !== false; }); // Apply service disabled state to child tags - ['spotify', 'itunes', 'musicbrainz', 'deezer', 'audiodb', 'tidal', 'qobuz', 'lastfm', 'genius'].forEach(svc => { + ['spotify', 'itunes', 'musicbrainz', 'deezer', 'audiodb', 'tidal', 'qobuz', 'lastfm', 'genius', 'hifi'].forEach(svc => { const master = document.getElementById('embed-' + svc); if (master) toggleServiceTags(master, svc); }); @@ -2653,6 +2654,10 @@ async function saveSettings(quiet = false) { quality: document.getElementById('hifi-download-quality').value || 'lossless', allow_fallback: document.getElementById('hifi-allow-fallback').checked, }, + hifi: { + embed_tags: document.getElementById('embed-hifi').checked, + tags: _collectServiceTags('hifi') + }, deezer_download: { quality: document.getElementById('deezer-download-quality').value || 'flac', arl: document.getElementById('deezer-download-arl').value || '',