From 37431ea82b28365eddfd7c56c4555772c708d783 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 12 Jun 2026 13:28:26 -0700 Subject: [PATCH] Downloads: additively surface each track's real file path (analysis + completed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The download analysis already matches every track to a library row via check_track_exists / manual match, then discarded the result. Keep it: each analysis_results entry now carries matched_file_path + matched_track_id (the owned file's real location, or None). Symmetrically, a completed download task now records final_file_path (where the import landed). Purely additive, no behavior change, no new matching, zero perf cost — just stops throwing away what the pipeline already computed. This is the foundation for playlist materialization: owned + downloaded tracks both report where their real file is, so the folder can be built by name match, not source IDs. --- core/downloads/master.py | 27 +++++++++++++++++++++++---- core/imports/pipeline.py | 5 +++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/downloads/master.py b/core/downloads/master.py index ce30b8d5..7a00ebf5 100644 --- a/core/downloads/master.py +++ b/core/downloads/master.py @@ -505,13 +505,19 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma track_name = track_data.get('name', '') artists = track_data.get('artists', []) found, confidence = False, 0.0 + # Additive payload: the owned library track (DatabaseTrack) when this + # item is found in the library, so downstream (playlist materialization) + # knows WHERE the real file is without re-matching. None when not owned. + matched_track = None # Manual library matches are authoritative unless the user explicitly # requested a force re-download from the normal download modal. _stid = track_data.get('spotify_track_id') or track_data.get('source_track_id') or track_data.get('id', '') - if not ignore_manual_matches and _stid and _mlm.get_match_for_track( - db, batch_profile_id, track_data, default_source=batch_source - ): + _manual_match = ( + _mlm.get_match_for_track(db, batch_profile_id, track_data, default_source=batch_source) + if (not ignore_manual_matches and _stid) else None + ) + if _manual_match: logger.info(f"[Manual Match] '{track_name}' already matched in library — skipping download") try: deps.check_and_remove_track_from_wishlist_by_metadata(track_data) @@ -523,6 +529,8 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma 'found': True, 'confidence': 1.0, 'match_reason': 'manual_library_match', + 'matched_file_path': _manual_match.get('library_file_path'), + 'matched_track_id': _manual_match.get('library_track_id'), }) continue @@ -568,8 +576,10 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma # Direct title match (try both raw and normalized) if track_name_lower in album_tracks_map: found, confidence = True, 1.0 + matched_track = album_tracks_map[track_name_lower] elif _normalized_source_title and _normalized_source_title in album_tracks_map: found, confidence = True, 1.0 + matched_track = album_tracks_map[_normalized_source_title] else: # Fuzzy match against album tracks using string similarity. # Compare BOTH the raw and normalized source titles — @@ -577,14 +587,17 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma # matching when the album doesn't imply version # context (helper returns the input unchanged). best_sim = 0.0 + best_track = None for db_title_lower, _db_track in album_tracks_map.items(): sim_raw = db._string_similarity(track_name_lower, db_title_lower) sim_norm = db._string_similarity(_normalized_source_title, db_title_lower) if _normalized_source_title else 0.0 sim = max(sim_raw, sim_norm) if sim > best_sim: best_sim = sim + best_track = _db_track if best_sim >= 0.7: found, confidence = True, best_sim + matched_track = best_track else: # Fall back to global per-track search for this track # When allow_duplicates is on for album downloads, skip global @@ -605,6 +618,7 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma ) if db_track and track_confidence >= 0.7: found, confidence = True, track_confidence + matched_track = db_track break elif allow_duplicates and batch_is_album: # Allow duplicates + album download + album not in DB yet → treat all as missing @@ -624,10 +638,15 @@ def run_full_missing_tracks_process(batch_id, playlist_id, tracks_json, deps: Ma ) if db_track and track_confidence >= 0.7: found, confidence = True, track_confidence + matched_track = db_track break analysis_results.append({ - 'track_index': track_index, 'track': track_data, 'found': found, 'confidence': confidence + 'track_index': track_index, 'track': track_data, 'found': found, 'confidence': confidence, + # Additive: real on-disk location of the owned track (None when not + # owned), so playlist materialization links the right file. + 'matched_file_path': getattr(matched_track, 'file_path', None), + 'matched_track_id': getattr(matched_track, 'id', None), }) # WISHLIST REMOVAL: If track is found in database, check if it should be removed from wishlist diff --git a/core/imports/pipeline.py b/core/imports/pipeline.py index ec027f06..22117377 100644 --- a/core/imports/pipeline.py +++ b/core/imports/pipeline.py @@ -695,6 +695,7 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta if task_id in download_tasks: download_tasks[task_id]['stream_processed'] = True download_tasks[task_id]['status'] = 'completed' + download_tasks[task_id]['final_file_path'] = context.get('_final_processed_path') logger.info(f"[Playlist Folder Mode] Marked task {task_id} as completed") _notify_download_completed(batch_id, task_id, success=True) return @@ -1026,6 +1027,10 @@ def post_process_matched_download(context_key, context, file_path, runtime, meta if task_id in download_tasks: download_tasks[task_id]['stream_processed'] = True download_tasks[task_id]['status'] = 'completed' + # Additive: record where the imported file landed so downstream + # (playlist materialization) knows the real path of a freshly + # downloaded track without re-resolving it. + download_tasks[task_id]['final_file_path'] = context.get('_final_processed_path') logger.info(f"[Post-Process] Marked task {task_id} as completed") _notify_download_completed(batch_id, task_id, success=True)