Merge pull request #459 from elmerohueso/suppress-duplicate-toasts

add guards to error, complete, and cancelled toasts
pull/468/head
BoulderBadgeDad 2 months ago committed by GitHub
commit 7405d04900
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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": "",

@ -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]:

@ -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 {}

@ -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,

@ -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)

@ -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"

@ -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

@ -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(),

@ -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)

@ -5217,13 +5217,14 @@
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-tidal" checked onchange="toggleServiceTags(this, 'tidal')"> Tidal
</label>
<span class="tag-service-count">4 tags</span>
<span class="tag-service-count">5 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.copyright" checked> Copyright</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.bpm" checked> BPM</label>
</div>
</div>
@ -5275,6 +5276,24 @@
</div>
</div>
<!-- HiFi -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">
<span class="tag-group-arrow">&#9654;</span>
<label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-hifi" checked onchange="toggleServiceTags(this, 'hifi')"> HiFi
</label>
<span class="tag-service-count">5 tags</span>
</div>
<div class="tag-service-body" style="display:none;">
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.track_id" checked> Track ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.artist_id" checked> Artist ID</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.isrc" checked> ISRC</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.bpm" checked> BPM</label>
<label class="checkbox-label"><input type="checkbox" data-config="hifi.tags.copyright" checked> Copyright</label>
</div>
</div>
<!-- General -->
<div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)">

@ -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';

@ -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 || '',

Loading…
Cancel
Save