From 0b7b88e8928b7a1b2e0d80775daf69cb7128450a Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Mon, 10 Nov 2025 19:47:26 -0800 Subject: [PATCH] duplicate cleaner --- webui/index.html | 33 +++++-- webui/static/script.js | 194 +++++++++++++++++++++++++++++++++++++++++ webui/static/style.css | 163 ++++++++++++++++++++++++++++++++++ 3 files changed, 385 insertions(+), 5 deletions(-) diff --git a/webui/index.html b/webui/index.html index a6fcf3d9..6a390a1d 100644 --- a/webui/index.html +++ b/webui/index.html @@ -240,7 +240,10 @@

Tools & Operations

-

Database Updater

+
+

Database Updater

+ +

Last Full Refresh: Never

@@ -277,8 +280,9 @@
-
-

Metadata Updater

+
+

Metadata Updater

+
@@ -302,7 +306,10 @@
-

Quality Scanner

+
+

Quality Scanner

+ +

Scan library for tracks below quality preferences

@@ -339,7 +346,10 @@
-

Duplicate Cleaner

+
+

Duplicate Cleaner

+ +

Detect and remove duplicate tracks in Transfer folder

@@ -2212,6 +2222,19 @@
+ +
+
+
+

Tool Information

+ +
+
+ +
+
+
+ \ No newline at end of file diff --git a/webui/static/script.js b/webui/static/script.js index 94cd4579..fa2e9ae0 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -9049,6 +9049,197 @@ function stopDuplicateCleanerPolling() { } } +// ============================================ +// == TOOL HELP MODAL == +// ============================================ + +const TOOL_HELP_CONTENT = { + 'db-updater': { + title: 'Database Updater', + content: ` +

What does this tool do?

+

The Database Updater syncs your media server library (Plex, Jellyfin, or Navidrome) with SoulSync's internal database.

+ +

Update Modes

+
    +
  • Incremental Update: Only scans for new artists, albums, and tracks that have been added since the last update. Fast and efficient for regular updates.
  • +
  • Full Refresh: Completely rebuilds the database from scratch. Use this if you've made significant changes to your library or if data seems out of sync.
  • +
+ +

When to use it?

+
    +
  • After adding new music to your media server
  • +
  • When library statistics seem incorrect
  • +
  • After changing media server settings
  • +
+ +

Progress Persistence

+

The update runs in the background. You can close this page and return later - progress will be preserved and continue where it left off.

+ ` + }, + 'metadata-updater': { + title: 'Metadata Updater', + content: ` +

What does this tool do?

+

The Metadata Updater enhances your library by fetching artist photos, genres, and album artwork from Spotify.

+ +

Refresh Interval Options

+
    +
  • 6 months: Only updates metadata for artists not updated in the last 180 days
  • +
  • 3 months: Updates metadata for artists not updated in the last 90 days
  • +
  • 1 month: Updates metadata for artists not updated in the last 30 days
  • +
  • Force All: Updates all artists regardless of when they were last updated
  • +
+ +

What gets updated?

+
    +
  • Artist profile photos
  • +
  • Music genres
  • +
  • Album cover artwork
  • +
  • Spotify popularity scores
  • +
+ +

Note

+

This tool is only available for Plex media servers. It requires Spotify authentication to fetch metadata.

+ ` + }, + 'quality-scanner': { + title: 'Quality Scanner', + content: ` +

What does this tool do?

+

The Quality Scanner identifies tracks in your library that don't meet your preferred quality settings and automatically matches them to Spotify to add to your wishlist for re-downloading.

+ +

Scan Scope

+
    +
  • Watchlist Artists Only: Only scans tracks from artists you're watching. Faster and more focused.
  • +
  • All Library Tracks: Scans your entire music library. Comprehensive but takes longer.
  • +
+ +

How it works

+
    +
  1. Scans tracks and checks file format against your quality preferences
  2. +
  3. Identifies tracks below your quality threshold (e.g., MP3 when you prefer FLAC)
  4. +
  5. Uses fuzzy matching to find the track on Spotify (70% confidence minimum)
  6. +
  7. Automatically adds matched tracks to your wishlist for re-download
  8. +
+ +

Quality Tiers

+
    +
  • Tier 1 (Best): FLAC, WAV, ALAC, AIFF - Lossless formats
  • +
  • Tier 2: OPUS, OGG - High quality lossy
  • +
  • Tier 3: M4A, AAC - Standard lossy
  • +
  • Tier 4: MP3, WMA - Lower quality lossy
  • +
+ +

Stats Explained

+
    +
  • Processed: Total tracks scanned so far
  • +
  • Quality Met: Tracks that meet your quality standards
  • +
  • Low Quality: Tracks below your quality threshold
  • +
  • Matched: Low quality tracks successfully matched to Spotify and added to wishlist
  • +
+ ` + }, + 'duplicate-cleaner': { + title: 'Duplicate Cleaner', + content: ` +

What does this tool do?

+

The Duplicate Cleaner scans your Transfer folder for duplicate audio files and automatically removes lower-quality versions, keeping only the best copy.

+ +

How it detects duplicates

+

Files are considered duplicates when:

+
    +
  • They are in the same folder
  • +
  • They have the exact same filename (ignoring file extension)
  • +
+

Example: Song.flac and Song.mp3 in the same folder = duplicates ✓

+

Example: Song.flac and Song (Remaster).flac = NOT duplicates ✗

+ +

Which file is kept?

+

Priority order (best to worst):

+
    +
  1. Format priority: FLAC/Lossless > OPUS/OGG > M4A/AAC > MP3/WMA
  2. +
  3. If same format: Larger file size is kept (usually indicates better bitrate)
  4. +
+ +

Where do deleted files go?

+

Removed files are moved to Transfer/deleted/ folder (not permanently deleted). You can review and recover them if needed.

+ +

Safety Features

+
    +
  • Only processes audio files (FLAC, MP3, M4A, etc.)
  • +
  • Only removes files with identical names in the same folder
  • +
  • Files are moved, not deleted - fully recoverable
  • +
  • Preserves original folder structure in the deleted folder
  • +
+ +

Stats Explained

+
    +
  • Files Scanned: Total audio files checked
  • +
  • Duplicates Found: Number of duplicate files detected
  • +
  • Deleted: Files moved to deleted folder
  • +
  • Space Freed: Total disk space reclaimed
  • +
+ ` + } +}; + +function initializeToolHelpButtons() { + const helpButtons = document.querySelectorAll('.tool-help-button'); + const modal = document.getElementById('tool-help-modal'); + const closeButton = modal.querySelector('.tool-help-modal-close'); + + // Attach click handlers to all help buttons + helpButtons.forEach(button => { + button.addEventListener('click', (e) => { + e.stopPropagation(); + const toolId = button.getAttribute('data-tool'); + openToolHelpModal(toolId); + }); + }); + + // Close modal when clicking close button + closeButton.addEventListener('click', closeToolHelpModal); + + // Close modal when clicking outside content + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeToolHelpModal(); + } + }); + + // Close modal on Escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.classList.contains('active')) { + closeToolHelpModal(); + } + }); +} + +function openToolHelpModal(toolId) { + const modal = document.getElementById('tool-help-modal'); + const titleElement = document.getElementById('tool-help-modal-title'); + const bodyElement = document.getElementById('tool-help-modal-body'); + + const helpData = TOOL_HELP_CONTENT[toolId]; + if (!helpData) { + console.warn(`No help content found for tool: ${toolId}`); + return; + } + + titleElement.textContent = helpData.title; + bodyElement.innerHTML = helpData.content; + + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; // Prevent background scrolling +} + +function closeToolHelpModal() { + const modal = document.getElementById('tool-help-modal'); + modal.classList.remove('active'); + document.body.style.overflow = ''; // Restore scrolling +} + function stopWishlistCountPolling() { if (wishlistCountInterval) { clearInterval(wishlistCountInterval); @@ -9160,6 +9351,9 @@ async function loadDashboardData() { duplicateCleanButton.addEventListener('click', handleDuplicateCleanButtonClick); } + // Attach event listeners for tool help buttons + initializeToolHelpButtons(); + // Attach event listener for the wishlist button const wishlistButton = document.getElementById('wishlist-button'); if (wishlistButton) { diff --git a/webui/static/style.css b/webui/static/style.css index f4aff17c..d7fa1d4f 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -15937,3 +15937,166 @@ body { font-size: 13px; } } + +/* ==================================== + Tool Help Button & Modal + ==================================== */ + +.tool-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.tool-card-header .tool-card-title { + margin: 0; +} + +.tool-help-button { + width: 24px; + height: 24px; + border-radius: 50%; + border: 2px solid #666; + background-color: transparent; + color: #666; + font-size: 14px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.tool-help-button:hover { + background-color: #1db954; + border-color: #1db954; + color: #fff; + transform: scale(1.1); +} + +.tool-help-modal { + display: none; + position: fixed; + z-index: 10000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + animation: fadeIn 0.2s ease; +} + +.tool-help-modal.active { + display: flex; + align-items: center; + justify-content: center; +} + +.tool-help-modal-content { + background-color: #1a1a1a; + border-radius: 12px; + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + animation: slideUp 0.3s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + transform: translateY(30px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.tool-help-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #333; +} + +.tool-help-modal-header h3 { + margin: 0; + font-size: 20px; + color: #fff; +} + +.tool-help-modal-close { + background: none; + border: none; + color: #999; + font-size: 32px; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s ease; + line-height: 1; +} + +.tool-help-modal-close:hover { + color: #fff; +} + +.tool-help-modal-body { + padding: 24px; + color: #ccc; + line-height: 1.6; +} + +.tool-help-modal-body h4 { + color: #1db954; + margin-top: 20px; + margin-bottom: 10px; + font-size: 16px; +} + +.tool-help-modal-body h4:first-child { + margin-top: 0; +} + +.tool-help-modal-body ul { + margin: 10px 0; + padding-left: 20px; +} + +.tool-help-modal-body li { + margin: 8px 0; +} + +.tool-help-modal-body p { + margin: 10px 0; +} + +.tool-help-modal-body code { + background-color: #2a2a2a; + padding: 2px 6px; + border-radius: 4px; + font-family: 'Courier New', monospace; + font-size: 13px; + color: #1db954; +} + +.tool-help-modal-body strong { + color: #fff; +}