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

add guards to error, complete, and cancelled toasts
This commit is contained in:
BoulderBadgeDad 2026-05-02 10:07:52 -07:00 committed by GitHub
commit 7405d04900
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 317 additions and 110 deletions

View file

@ -495,6 +495,16 @@ class ConfigManager:
"hifi_download": { "hifi_download": {
"quality": "lossless", # Options: "low", "high", "lossless", "hires" "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": { "lidarr_download": {
"url": "", "url": "",
"api_key": "", "api_key": "",

View file

@ -282,24 +282,30 @@ class HiFiClient:
def _parse_track(self, item: dict) -> Dict: def _parse_track(self, item: dict) -> Dict:
artist_name = 'Unknown Artist' artist_name = 'Unknown Artist'
artist_id = None
artists_raw = item.get('artists', item.get('artist')) artists_raw = item.get('artists', item.get('artist'))
if isinstance(artists_raw, list): if isinstance(artists_raw, list):
names = [] names = []
for a in artists_raw: for a in artists_raw:
if isinstance(a, dict): if isinstance(a, dict):
names.append(a.get('name', '')) names.append(a.get('name', ''))
if artist_id is None:
artist_id = a.get('id')
elif isinstance(a, str): elif isinstance(a, str):
names.append(a) names.append(a)
artist_name = ', '.join(n for n in names if n) or 'Unknown Artist' artist_name = ', '.join(n for n in names if n) or 'Unknown Artist'
elif isinstance(artists_raw, dict): elif isinstance(artists_raw, dict):
artist_name = artists_raw.get('name', 'Unknown Artist') artist_name = artists_raw.get('name', 'Unknown Artist')
artist_id = artists_raw.get('id')
elif isinstance(artists_raw, str): elif isinstance(artists_raw, str):
artist_name = artists_raw artist_name = artists_raw
album_raw = item.get('album', {}) album_raw = item.get('album', {})
album_name = '' album_name = ''
album_id = None
if isinstance(album_raw, dict): if isinstance(album_raw, dict):
album_name = album_raw.get('title', album_raw.get('name', '')) album_name = album_raw.get('title', album_raw.get('name', ''))
album_id = album_raw.get('id')
elif isinstance(album_raw, str): elif isinstance(album_raw, str):
album_name = album_raw album_name = album_raw
@ -308,12 +314,16 @@ class HiFiClient:
return { return {
'id': item.get('id'), 'id': item.get('id'),
'artist_id': artist_id,
'album_id': album_id,
'title': item.get('title', item.get('name', 'Unknown')), 'title': item.get('title', item.get('name', 'Unknown')),
'artist': artist_name, 'artist': artist_name,
'album': album_name, 'album': album_name,
'duration_ms': int(duration_ms) if duration_ms else 0, 'duration_ms': int(duration_ms) if duration_ms else 0,
'track_number': item.get('trackNumber', item.get('track_number')), 'track_number': item.get('trackNumber', item.get('track_number')),
'isrc': item.get('isrc'), 'isrc': item.get('isrc'),
'bpm': item.get('bpm'),
'copyright': item.get('copyright'),
'explicit': item.get('explicit', False), 'explicit': item.get('explicit', False),
'quality': item.get('audioQuality', item.get('quality', '')), 'quality': item.get('audioQuality', item.get('quality', '')),
} }
@ -549,6 +559,15 @@ class HiFiClient:
title=track.get('title'), title=track.get('title'),
album=track.get('album'), album=track.get('album'),
track_number=track.get('track_number'), 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]: async def download(self, username: str, filename: str, file_size: int = 0) -> Optional[str]:

View file

@ -257,6 +257,8 @@ def get_source_tag_names(source: str) -> Dict[str, Optional[str]]:
return {"track": None, "artist": None, "album": None} return {"track": None, "artist": None, "album": None}
if source_name == "discogs": if source_name == "discogs":
return {"track": None, "artist": None, "album": None} 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} 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"} return {"artist": "soul_id", "album": "soul_id", "track": "soul_id", "track_album": "album_soul_id"}
if source_name == "discogs": if source_name == "discogs":
return {"artist": "discogs_id", "album": "discogs_id", "track": None} 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 {} return {}

View file

@ -37,6 +37,7 @@ def build_metadata_enrichment_runtime(
deezer_worker: Any | None = None, deezer_worker: Any | None = None,
audiodb_worker: Any | None = None, audiodb_worker: Any | None = None,
tidal_client: Any | None = None, tidal_client: Any | None = None,
hifi_client: Any | None = None,
qobuz_enrichment_worker: Any | None = None, qobuz_enrichment_worker: Any | None = None,
lastfm_worker: Any | None = None, lastfm_worker: Any | None = None,
genius_worker: Any | None = None, genius_worker: Any | None = None,
@ -49,6 +50,7 @@ def build_metadata_enrichment_runtime(
deezer_worker=deezer_worker, deezer_worker=deezer_worker,
audiodb_worker=audiodb_worker, audiodb_worker=audiodb_worker,
tidal_client=tidal_client, tidal_client=tidal_client,
hifi_client=hifi_client,
qobuz_enrichment_worker=qobuz_enrichment_worker, qobuz_enrichment_worker=qobuz_enrichment_worker,
lastfm_worker=lastfm_worker, lastfm_worker=lastfm_worker,
genius_worker=genius_worker, genius_worker=genius_worker,

View file

@ -124,12 +124,14 @@ SOURCE_TAG_CONFIG = {
"AUDIODB_TRACK_ID": "audiodb.tags.track_id", "AUDIODB_TRACK_ID": "audiodb.tags.track_id",
"TIDAL_TRACK_ID": "tidal.tags.track_id", "TIDAL_TRACK_ID": "tidal.tags.track_id",
"TIDAL_ARTIST_ID": "tidal.tags.artist_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_TRACK_ID": "qobuz.tags.track_id",
"QOBUZ_ARTIST_ID": "qobuz.tags.artist_id", "QOBUZ_ARTIST_ID": "qobuz.tags.artist_id",
"GENIUS_TRACK_ID": "genius.tags.track_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 = { ID3_TAG_MAP = {
"MUSICBRAINZ_RECORDING_ID": ("UFID", "http://musicbrainz.org"), "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)) td_details = _call_source_lookup("Tidal track details", tidal_client.get_track, str(td_track_id))
if td_details: if td_details:
pp["tidal_isrc"] = td_details.get("isrc") 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") td_copyright = td_details.get("copyright")
if isinstance(td_copyright, dict): if isinstance(td_copyright, dict):
td_copyright = td_copyright.get("text", td_copyright.get("name", "")) 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] 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: 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: if cfg.get("qobuz.embed_tags", True) is False:
return 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) _process_audiodb_source(pp, metadata, cfg, runtime, track_title, artist_name)
elif source_name == "tidal": elif source_name == "tidal":
_process_tidal_source(pp, metadata, cfg, runtime, track_title, artist_name) _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": elif source_name == "qobuz":
_process_qobuz_source(pp, metadata, cfg, runtime, track_title, artist_name) _process_qobuz_source(pp, metadata, cfg, runtime, track_title, artist_name)
elif source_name == "lastfm": 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] audio_file["\xa9day"] = [release_year]
logger.info("Date tag: %s", 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_candidates = []
bpm_int = int(pp["deezer_bpm"]) 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): if isinstance(audio_file.tags, symbols.ID3):
audio_file.tags.add(symbols.TBPM(encoding=3, text=[str(bpm_int)])) audio_file.tags.add(symbols.TBPM(encoding=3, text=[str(bpm_int)]))
elif is_vorbis_like(audio_file, symbols): elif is_vorbis_like(audio_file, symbols):
audio_file["BPM"] = [str(bpm_int)] audio_file["BPM"] = [str(bpm_int)]
elif isinstance(audio_file, symbols.MP4): elif isinstance(audio_file, symbols.MP4):
audio_file["tmpo"] = [bpm_int] 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 _tag_enabled(cfg, "audiodb.tags.mood") and pp["audiodb_mood"]:
if isinstance(audio_file.tags, symbols.ID3): 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"])) isrc_candidates.append(("Deezer", pp["deezer_isrc"]))
if pp["tidal_isrc"] and _tag_enabled(cfg, "tidal.tags.isrc"): if pp["tidal_isrc"] and _tag_enabled(cfg, "tidal.tags.isrc"):
isrc_candidates.append(("Tidal", pp["tidal_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"): if pp["qobuz_isrc"] and _tag_enabled(cfg, "qobuz.tags.isrc"):
isrc_candidates.append(("Qobuz", pp["qobuz_isrc"])) isrc_candidates.append(("Qobuz", pp["qobuz_isrc"]))
if isrc_candidates: 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"])) copyright_candidates.append(("Tidal", pp["tidal_copyright"]))
if pp["qobuz_copyright"] and _tag_enabled(cfg, "qobuz.tags.copyright"): if pp["qobuz_copyright"] and _tag_enabled(cfg, "qobuz.tags.copyright"):
copyright_candidates.append(("Qobuz", pp["qobuz_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: if copyright_candidates:
copyright_source, final_copyright = copyright_candidates[0] copyright_source, final_copyright = copyright_candidates[0]
if isinstance(audio_file.tags, symbols.ID3): 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, "isrc": None,
"deezer_bpm": None, "deezer_bpm": None,
"deezer_isrc": None, "deezer_isrc": None,
"tidal_bpm": None,
"hifi_bpm": None,
"hifi_copyright": None,
"audiodb_mood": None, "audiodb_mood": None,
"audiodb_style": None, "audiodb_style": None,
"audiodb_genre": None, "audiodb_genre": None,
"tidal_isrc": None, "tidal_isrc": None,
"tidal_copyright": None, "tidal_copyright": None,
"hifi_isrc": None,
"qobuz_isrc": None, "qobuz_isrc": None,
"qobuz_copyright": None, "qobuz_copyright": None,
"qobuz_label": 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: if not isinstance(source_order, list) or not source_order:
source_order = DEFAULT_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() db = get_database()
for source_name in source_order: for source_name in source_order:
_process_source_enrichment(source_name, pp, metadata, cfg, runtime, track_title, artist_name) _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 return
release_year = _write_embedded_metadata(audio_file, metadata, pp, cfg, symbols) release_year = _write_embedded_metadata(audio_file, metadata, pp, cfg, symbols)

View file

@ -79,6 +79,7 @@ class TrackResult(SearchResult):
title: Optional[str] = None title: Optional[str] = None
album: Optional[str] = None album: Optional[str] = None
track_number: Optional[int] = None track_number: Optional[int] = None
_source_metadata: Optional[Dict[str, Any]] = None
def __post_init__(self): def __post_init__(self):
self.result_type = "track" self.result_type = "track"

View file

@ -432,6 +432,14 @@ class TidalDownloadClient:
title=title, title=title,
album=album_name, album=album_name,
track_number=track.track_num, 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 return track_result

View file

@ -23,6 +23,7 @@ def test_build_import_pipeline_runtime_exposes_expected_contract():
"deezer_worker", "deezer_worker",
"audiodb_worker", "audiodb_worker",
"tidal_client", "tidal_client",
"hifi_client",
"qobuz_enrichment_worker", "qobuz_enrichment_worker",
"lastfm_worker", "lastfm_worker",
"genius_worker", "genius_worker",
@ -38,6 +39,7 @@ def test_build_metadata_enrichment_runtime_exposes_expected_contract():
"deezer_worker": object(), "deezer_worker": object(),
"audiodb_worker": object(), "audiodb_worker": object(),
"tidal_client": object(), "tidal_client": object(),
"hifi_client": object(),
"qobuz_enrichment_worker": object(), "qobuz_enrichment_worker": object(),
"lastfm_worker": object(), "lastfm_worker": object(),
"genius_worker": object(), "genius_worker": object(),

View file

@ -14313,7 +14313,18 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
context, context,
artist, artist,
album_info, 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, task_id,
batch_id, batch_id,
_build_import_pipeline_runtime(), _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, context,
file_path, file_path,
_build_import_pipeline_runtime(), _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) # Track stale transfer keys (completed in slskd but no context — e.g., from before app restart)

View file

@ -5217,13 +5217,14 @@
<label class="checkbox-label" onclick="event.stopPropagation()"> <label class="checkbox-label" onclick="event.stopPropagation()">
<input type="checkbox" id="embed-tidal" checked onchange="toggleServiceTags(this, 'tidal')"> Tidal <input type="checkbox" id="embed-tidal" checked onchange="toggleServiceTags(this, 'tidal')"> Tidal
</label> </label>
<span class="tag-service-count">4 tags</span> <span class="tag-service-count">5 tags</span>
</div> </div>
<div class="tag-service-body" style="display:none;"> <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.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.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.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.copyright" checked> Copyright</label>
<label class="checkbox-label"><input type="checkbox" data-config="tidal.tags.bpm" checked> BPM</label>
</div> </div>
</div> </div>
@ -5275,6 +5276,24 @@
</div> </div>
</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 --> <!-- General -->
<div class="tag-service-group"> <div class="tag-service-group">
<div class="tag-service-header" onclick="toggleTagGroup(this)"> <div class="tag-service-header" onclick="toggleTagGroup(this)">

View file

@ -3305,127 +3305,133 @@ function processModalStatusUpdate(playlistId, data) {
// Note: Auto-show logic removed - wishlist modal visibility managed by user interaction only // Note: Auto-show logic removed - wishlist modal visibility managed by user interaction only
if (data.phase === 'cancelled') { if (data.phase === 'cancelled') {
process.status = 'cancelled'; if (process.status !== 'cancelled') {
process.status = 'cancelled';
// Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on cancel // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on cancel
if (playlistId.startsWith('youtube_')) { if (playlistId.startsWith('youtube_')) {
const urlHash = playlistId.replace('youtube_', ''); const urlHash = playlistId.replace('youtube_', '');
updateYouTubeCardPhase(urlHash, 'discovered'); updateYouTubeCardPhase(urlHash, 'discovered');
if (urlHash.startsWith('mirrored_')) { if (urlHash.startsWith('mirrored_')) {
updateMirroredCardPhase(urlHash, 'discovered'); updateMirroredCardPhase(urlHash, 'discovered');
}
} }
}
showToast(`Process cancelled for ${process.playlist.name}.`, 'info'); showToast(`Process cancelled for ${process.playlist.name}.`, 'info');
}
} else if (data.phase === 'error') { } else if (data.phase === 'error') {
process.status = 'complete'; // Treat as complete to allow cleanup if (process.status !== 'complete') {
updatePlaylistCardUI(playlistId); // Update card to show ready for review 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 // Reset YouTube playlist phase to 'discovered' if this is a YouTube playlist on error
if (playlistId.startsWith('youtube_')) { if (playlistId.startsWith('youtube_')) {
const urlHash = playlistId.replace('youtube_', ''); const urlHash = playlistId.replace('youtube_', '');
updateYouTubeCardPhase(urlHash, 'discovered'); updateYouTubeCardPhase(urlHash, 'discovered');
if (urlHash.startsWith('mirrored_')) { if (urlHash.startsWith('mirrored_')) {
updateMirroredCardPhase(urlHash, 'discovered'); updateMirroredCardPhase(urlHash, 'discovered');
}
} }
}
showToast(`Process for ${process.playlist.name} failed!`, 'error'); showToast(`Process for ${process.playlist.name} failed!`, 'error');
}
} else { } else {
process.status = 'complete'; if (process.status !== 'complete') {
updatePlaylistCardUI(playlistId); // Update card to show ready for review 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 // Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist
if (playlistId.startsWith('youtube_')) { if (playlistId.startsWith('youtube_')) {
const urlHash = playlistId.replace('youtube_', ''); const urlHash = playlistId.replace('youtube_', '');
updateYouTubeCardPhase(urlHash, 'download_complete'); updateYouTubeCardPhase(urlHash, 'download_complete');
if (urlHash.startsWith('mirrored_')) { if (urlHash.startsWith('mirrored_')) {
updateMirroredCardPhase(urlHash, 'download_complete'); updateMirroredCardPhase(urlHash, '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 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 // Update Tidal playlist phase to 'download_complete' if this is a Tidal playlist
updateBeatportCardPhase(chartHash, 'download_complete'); if (playlistId.startsWith('tidal_')) {
const tidalPlaylistId = playlistId.replace('tidal_', '');
// Update backend state if (tidalPlaylistStates[tidalPlaylistId]) {
try { tidalPlaylistStates[tidalPlaylistId].phase = 'download_complete';
fetch(`/api/beatport/charts/update-phase/${chartHash}`, { // Store the download process ID for potential modal rehydration
method: 'POST', tidalPlaylistStates[tidalPlaylistId].download_process_id = process.batchId;
headers: { 'Content-Type': 'application/json' }, updateTidalCardPhase(tidalPlaylistId, 'download_complete');
body: JSON.stringify({ console.log(`✅ [Status Complete] Updated Tidal playlist ${tidalPlaylistId} to download_complete phase`);
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`);
} }
}
// Handle background wishlist processing completion specially // Update Beatport chart phase to 'download_complete' if this is a Beatport chart
if (isBackgroundWishlist) { if (playlistId.startsWith('beatport_')) {
console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${notFoundCount} not found, ${failedOrCancelledCount} failed`); const urlHash = playlistId.replace('beatport_', '');
const state = youtubePlaylistStates[urlHash];
// Reset modal to idle state to prevent "complete" phase disruption if (state && state.is_beatport_playlist) {
setTimeout(() => { const chartHash = state.beatport_chart_hash || urlHash;
resetWishlistModalToIdleState();
// Server-side auto-processing will handle next cycle automatically
}, 500);
return; // Skip normal completion handling // Update frontend states
} state.phase = 'download_complete';
state.download_process_id = process.batchId;
if (beatportChartStates[chartHash]) {
beatportChartStates[chartHash].phase = 'download_complete';
}
// Show completion summary with wishlist stats (matching sync.py behavior) // Update card UI
let completionMessage = `Process complete for ${process.playlist.name}!`; updateBeatportCardPhase(chartHash, 'download_complete');
let messageType = 'success';
// Check for wishlist summary from backend (added when failed/cancelled tracks are processed) // Update backend state
if (data.wishlist_summary) { try {
const summary = data.wishlist_summary; fetch(`/api/beatport/charts/update-phase/${chartHash}`, {
let summaryParts = [`Downloaded: ${completedCount}`]; method: 'POST',
if (notFoundCount > 0) summaryParts.push(`Not Found: ${notFoundCount}`); headers: { 'Content-Type': 'application/json' },
if (failedOrCancelledCount > 0) summaryParts.push(`Failed: ${failedOrCancelledCount}`); body: JSON.stringify({
completionMessage = `Download process complete! ${summaryParts.join(', ')}.`; phase: 'download_complete',
download_process_id: process.batchId
})
});
} catch (error) {
console.warn('⚠️ Error updating backend Beatport phase to download_complete:', error);
}
if (summary.tracks_added > 0) { console.log(`✅ [Status Complete] Updated Beatport chart ${chartHash} to download_complete phase`);
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); // 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);
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';
}
}
showToast(completionMessage, messageType);
}
} }
document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none'; document.getElementById(`cancel-all-btn-${playlistId}`).style.display = 'none';

View file

@ -978,6 +978,7 @@ async function loadSettingsData() {
document.getElementById('embed-qobuz').checked = settings.qobuz?.embed_tags !== false; document.getElementById('embed-qobuz').checked = settings.qobuz?.embed_tags !== false;
document.getElementById('embed-lastfm').checked = settings.lastfm?.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-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 // Load per-tag toggles from data-config attributes
document.querySelectorAll('[data-config]').forEach(cb => { document.querySelectorAll('[data-config]').forEach(cb => {
const path = cb.dataset.config.split('.'); const path = cb.dataset.config.split('.');
@ -986,7 +987,7 @@ async function loadSettingsData() {
cb.checked = val !== false; cb.checked = val !== false;
}); });
// Apply service disabled state to child tags // 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); const master = document.getElementById('embed-' + svc);
if (master) toggleServiceTags(master, svc); if (master) toggleServiceTags(master, svc);
}); });
@ -2653,6 +2654,10 @@ async function saveSettings(quiet = false) {
quality: document.getElementById('hifi-download-quality').value || 'lossless', quality: document.getElementById('hifi-download-quality').value || 'lossless',
allow_fallback: document.getElementById('hifi-allow-fallback').checked, allow_fallback: document.getElementById('hifi-allow-fallback').checked,
}, },
hifi: {
embed_tags: document.getElementById('embed-hifi').checked,
tags: _collectServiceTags('hifi')
},
deezer_download: { deezer_download: {
quality: document.getElementById('deezer-download-quality').value || 'flac', quality: document.getElementById('deezer-download-quality').value || 'flac',
arl: document.getElementById('deezer-download-arl').value || '', arl: document.getElementById('deezer-download-arl').value || '',