Merge pull request #459 from elmerohueso/suppress-duplicate-toasts
add guards to error, complete, and cancelled toasts
This commit is contained in:
commit
7405d04900
12 changed files with 317 additions and 110 deletions
|
|
@ -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">▶</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';
|
||||
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');
|
||||
// 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
|
||||
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');
|
||||
// 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');
|
||||
showToast(`Process for ${process.playlist.name} failed!`, 'error');
|
||||
}
|
||||
} else {
|
||||
process.status = 'complete';
|
||||
updatePlaylistCardUI(playlistId); // Update card to show ready for review
|
||||
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 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 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 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 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`);
|
||||
}
|
||||
|
||||
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`);
|
||||
// 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];
|
||||
|
||||
// Reset modal to idle state to prevent "complete" phase disruption
|
||||
setTimeout(() => {
|
||||
resetWishlistModalToIdleState();
|
||||
// Server-side auto-processing will handle next cycle automatically
|
||||
}, 500);
|
||||
if (state && state.is_beatport_playlist) {
|
||||
const chartHash = state.beatport_chart_hash || urlHash;
|
||||
|
||||
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)
|
||||
let completionMessage = `Process complete for ${process.playlist.name}!`;
|
||||
let messageType = 'success';
|
||||
// Update card UI
|
||||
updateBeatportCardPhase(chartHash, 'download_complete');
|
||||
|
||||
// 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(', ')}.`;
|
||||
// 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);
|
||||
}
|
||||
|
||||
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';
|
||||
console.log(`✅ [Status Complete] Updated Beatport chart ${chartHash} to download_complete phase`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -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…
Reference in a new issue