Add defensible orphaned audio detection

This commit is contained in:
Philip Guzman
2026-07-02 16:17:11 -07:00
parent 4d04939846
commit 22c4f58646
9 changed files with 133 additions and 11 deletions
+15 -2
View File
@@ -36,9 +36,13 @@ def analyze_health(library: Library) -> HealthReport:
for group in duplicate_groups
if group.kind is DuplicateKind.CLOUD_CONFLICT
]
referenced_names = {reference.filename for reference in library.references}
referenced_names = {
normalize(reference.filename) for reference in library.references
}
unused_count = sum(
1 for track in library.tracks if track.filename not in referenced_names
1
for track in library.tracks
if normalize(track.filename) not in referenced_names
)
matcher = MatchingEngine(library.tracks)
@@ -73,6 +77,15 @@ def analyze_health(library: Library) -> HealthReport:
group.extra_files for group in cloud_conflicts
),
unused_tracks=unused_count,
orphan_candidates=(
sum(
normalize(track.filename) not in referenced_names
and normalize(track.filename) not in database_names
for track in library.tracks
)
if library.database
else None
),
suggested_matches=suggested_count,
broken_symlinks=len(library.broken_symlinks),
static_crates=sum(
+1
View File
@@ -18,6 +18,7 @@ class HealthReport:
suspected_cloud_conflict_groups: int
suspected_cloud_conflict_files: int
unused_tracks: int
orphan_candidates: Optional[int]
suggested_matches: int
broken_symlinks: int
static_crates: int
+37 -5
View File
@@ -143,10 +143,24 @@ def diagnostic_details(library: Library, limit: int = DETAIL_LIMIT) -> dict:
database_filename_counts = Counter(
normalize(track.filename) for track in missing_database_tracks
)
referenced_names = {reference.filename for reference in library.references}
referenced_names = {
normalize(reference.filename) for reference in library.references
}
database_names = {normalize(track.filename) for track in database_tracks}
unused_tracks = [
track for track in library.tracks if track.filename not in referenced_names
track
for track in library.tracks
if normalize(track.filename) not in referenced_names
]
orphan_candidates = (
[
track
for track in unused_tracks
if normalize(track.filename) not in database_names
]
if library.database
else None
)
return {
"database_missing_tracks": {
@@ -246,10 +260,10 @@ def diagnostic_details(library: Library, limit: int = DETAIL_LIMIT) -> dict:
],
},
"unused_tracks": {
"title": "Unused tracks",
"title": "Not in crates",
"summary": (
"These scanned files were not referenced by any loaded crate. "
"That does not mean they should be deleted."
"These files are not in any loaded crate, but may still be "
"normal tracks in Serato's main library."
),
"total": len(unused_tracks),
"items": [
@@ -257,6 +271,24 @@ def diagnostic_details(library: Library, limit: int = DETAIL_LIMIT) -> dict:
for track in unused_tracks[:limit]
],
},
"orphan_candidates": {
"title": "Possible orphan files",
"summary": (
"These files were found in the music folder but not by filename "
"in loaded crates or Serato's database. Review only; this is "
"never an automatic deletion recommendation."
),
"total": len(orphan_candidates) if orphan_candidates is not None else None,
"assessed": orphan_candidates is not None,
"items": [
{
"filename": track.filename,
"path": _display_path(track.path),
**_file_preview(track.path),
}
for track in (orphan_candidates or ())[:limit]
],
},
}
+11 -1
View File
@@ -111,6 +111,11 @@ function detailLines(item) {
return lines.map((line) => `<li>${escapeHtml(line)}</li>`).join('');
}
function detailActions(item) {
if (!item.audio_url || !item.reveal_token) return '';
return `<div class="file-actions detail-actions"><button type="button" data-audio-url="${escapeHtml(item.audio_url)}" data-audio-name="${escapeHtml(item.path)}">▶ Play preview</button><button type="button" data-reveal-token="${escapeHtml(item.reveal_token)}">Show in Finder</button></div>`;
}
function renderDetail(key) {
const detail = latestAnalysis?.details?.[key];
if (!detail) return;
@@ -119,7 +124,7 @@ function renderDetail(key) {
trigger.classList.toggle('selected', trigger.dataset.detail === key);
});
drilldownTitle.textContent = detail.title;
drilldownCount.textContent = `${detail.total ?? 0} found`;
drilldownCount.textContent = detail.assessed === false ? 'Not assessed' : `${detail.total ?? 0} found`;
drilldownSummary.textContent = detail.summary;
repairPanel.hidden = true;
@@ -127,6 +132,10 @@ function renderDetail(key) {
previewedRepair = null;
latestBackup = null;
restoreRepairButton.hidden = true;
if (detail.assessed === false) {
drilldownList.innerHTML = '<div class="empty-detail">Seratos database V2 was not available, so orphan status cannot be assessed safely.</div>';
return;
}
if (!detail.items?.length) {
drilldownList.innerHTML = '<div class="empty-detail">Nothing to review here. Tiny victory parade, very tasteful.</div>';
return;
@@ -143,6 +152,7 @@ function renderDetail(key) {
<article class="detail-item">
<strong>${escapeHtml(item.filename || item.path || 'Untitled item')}</strong>
<ul>${detailLines(item)}</ul>
${detailActions(item)}
</article>
`).join('');
}
+3 -2
View File
@@ -69,7 +69,7 @@
<div class="metrics-grid">
<article class="metric panel"><button class="info-button" type="button" aria-label="About tracks" aria-expanded="false" data-info="Audio files found inside the music folder you selected. This is the collection Serato Doctor compared with your crates and database.">i</button><span>Tracks scanned</span><strong data-field="disk_tracks"></strong><small>audio files found</small></article>
<article class="metric panel warning drill-trigger" role="button" tabindex="0" data-detail="database_missing_tracks"><button class="info-button" type="button" aria-label="About missing tracks in Serato" aria-expanded="false" data-info="Tracks in Serato's database whose saved file location no longer exists. This should be close to the orange or unmapped track count you see in Serato.">i</button><span>Missing tracks in Serato</span><strong data-field="database_missing_paths"></strong><small><b data-field="database_missing_unique_filenames"></b> unique filenames · click for list</small></article>
<article class="metric panel drill-trigger" role="button" tabindex="0" data-detail="unused_tracks"><button class="info-button" type="button" aria-label="About unused tracks" aria-expanded="false" data-info="Files in the selected music folder whose filename is not used by any loaded crate. They may still be valid library tracks; this is informational, not a deletion recommendation.">i</button><span>Unused tracks</span><strong data-field="unused_tracks"></strong><small>not referenced by crates · click for list</small></article>
<article class="metric panel drill-trigger" role="button" tabindex="0" data-detail="orphan_candidates"><button class="info-button" type="button" aria-label="About possible orphan files" aria-expanded="false" data-info="Files found in the music folder but not by filename in loaded crates or Serato's database. They require review and are never automatic deletion recommendations.">i</button><span>Possible orphan files</span><strong data-field="orphan_candidates"></strong><small>outside crates and database · click for list</small></article>
<article class="metric panel drill-trigger" role="button" tabindex="0" data-detail="suggested_matches"><button class="info-button" type="button" aria-label="About suggested matches" aria-expanded="false" data-info="Missing crate entries with a filename-related candidate, such as an added OneDrive conflict number. Suggestions are evidence for review, never automatic repairs.">i</button><span>Suggested matches</span><strong data-field="suggested_matches"></strong><small>explainable candidates · click for list</small></article>
</div>
</div>
@@ -82,6 +82,7 @@
<div class="drill-trigger" role="button" tabindex="0" data-detail="cloud_conflicts"><span class="diag-icon blue"></span><p><strong>Possible cloud conflicts</strong><small><b data-field="suspected_cloud_conflict_groups"></b> groups · <b data-field="suspected_cloud_conflict_files"></b> extra files · click for list</small></p><button class="info-button" type="button" aria-label="About cloud conflicts" aria-expanded="false" data-info="Filename families such as Track.mp3 and Track 2.mp3. OneDrive often creates these during sync conflicts, but numbered song titles can also be legitimate.">i</button></div>
<div class="drill-trigger" role="button" tabindex="0" data-detail="broken_symlinks"><span class="diag-icon red"></span><p><strong>Broken shortcuts</strong><small><b data-field="broken_symlinks"></b> unresolved symbolic links · click for list</small></p><button class="info-button" type="button" aria-label="About broken shortcuts" aria-expanded="false" data-info="Shortcut-style symbolic links whose destination no longer exists. Serato Doctor reports them but never removes or recreates them automatically.">i</button></div>
<div class="drill-trigger" role="button" tabindex="0" data-detail="old_crate_references"><span class="diag-icon violet"></span><p><strong>Old crate references</strong><small><b data-field="missing_references"></b> appearances · <b data-field="unique_missing_filenames"></b> unique filenames · click for list</small></p><button class="info-button" type="button" aria-label="About old crate references" aria-expanded="false" data-info="Saved spots in regular crates whose exact filename was not found in the selected music folder. The same track can appear in several crates, so appearances are higher than unique filenames. These are separate from Serato's unmapped-track count.">i</button></div>
<div class="drill-trigger" role="button" tabindex="0" data-detail="unused_tracks"><span class="diag-icon blue"></span><p><strong>Not in crates</strong><small><b data-field="unused_tracks"></b> tracks still potentially in Serato · click for list</small></p><button class="info-button" type="button" aria-label="About tracks not in crates" aria-expanded="false" data-info="Tracks that do not appear in loaded crates. They may still be normal, active tracks in Serato's main library and should not be treated as orphans.">i</button></div>
<div><span class="diag-icon amber"></span><p><strong>Database coverage</strong><small><b data-field="database_entries"></b> Serato entries · <b data-field="tracks_missing_from_database"></b> scanned tracks absent</small></p><button class="info-button" type="button" aria-label="About database coverage" aria-expanded="false" data-info="Compares filenames in Serato's database with the selected music folder. A scanned track absent from the database may not have been imported, or may be represented under another filename.">i</button></div>
</div>
</div>
@@ -130,6 +131,6 @@
</dialog>
</main>
</div>
<script src="/app.js?v=6" defer></script>
<script src="/app.js?v=7" defer></script>
</body>
</html>