// ==UserScript==
// @name Riffusion Multitool
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Adds robust song deletion (Selective & Bulk) and a Download Queue tool with multi-format selection and delay. Features: Main Menu, Independent Lists, Keyword Filters, Liked Filters/Selectors, Draggable & Minimizable UI. USE WITH CAUTION.
// @author Graph1ks (assisted by GoogleAI)
// @match https://www.riffusion.com/library/my-songs
// @grant GM_addStyle
// @grant GM_info
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const INITIAL_VIEW = 'menu';
const DELETION_DELAY = 500;
const DOWNLOAD_MENU_DELAY = 450;
const DOWNLOAD_ACTION_DELAY = 500;
const DEFAULT_INTRA_FORMAT_DELAY_SECONDS = 6;
const DROPDOWN_DELAY = 300;
const DEFAULT_INTER_SONG_DELAY_SECONDS = 6;
const MAX_RETRIES = 3;
const MAX_EMPTY_CHECKS = 3;
const EMPTY_RETRY_DELAY = 6000;
const KEYWORD_FILTER_DEBOUNCE = 500;
const UI_INITIAL_TOP = '60px';
const UI_INITIAL_RIGHT = '20px';
const INITIAL_IGNORE_LIKED_DELETE = true;
const MINIMIZED_ICON_SIZE = '40px';
const MINIMIZED_ICON_TOP = '15px';
const MINIMIZED_ICON_RIGHT = '15px';
// --- State Variables ---
let debugMode = false;
let isDeleting = false;
let isDownloading = false;
let currentView = INITIAL_VIEW;
let ignoreLikedSongsDeleteState = INITIAL_IGNORE_LIKED_DELETE;
let downloadInterSongDelaySeconds = DEFAULT_INTER_SONG_DELAY_SECONDS;
let downloadIntraFormatDelaySeconds = DEFAULT_INTRA_FORMAT_DELAY_SECONDS;
let keywordFilterDebounceTimer = null;
// --- State for Minimize/Restore ---
let isMinimized = true;
let lastUiTop = UI_INITIAL_TOP;
let lastUiLeft = null;
let uiElement = null;
let minimizedIconElement = null;
// --- Styling (Compact Adjustments) ---
GM_addStyle(`
#riffControlUI {
position: fixed; background: linear-gradient(145deg, #2a2a2a, #1e1e1e); border: 1px solid #444; border-radius: 10px; padding: 0; z-index: 10000; width: 300px; /* Slightly narrower */ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; user-select: none; overflow: hidden;
display: ${isMinimized ? 'none' : 'block'};
}
#riffControlHeader {
background: linear-gradient(90deg, #3a3a3a, #2c2c2c); padding: 8px 12px; /* Reduced padding */ cursor: move; border-bottom: 1px solid #444; border-radius: 10px 10px 0 0; position: relative;
}
#riffControlHeader h3 { margin: 0; font-size: 15px; /* Slightly smaller */ font-weight: 600; color: #ffffff; text-align: center; text-shadow: 0 1px 1px rgba(0,0,0,0.2); padding-right: 25px; }
#minimizeButton {
position: absolute; top: 4px; /* Adjusted */ right: 6px; /* Adjusted */ background: none; border: none; color: #aaa; font-size: 18px; /* Slightly smaller */ font-weight: bold; line-height: 1; cursor: pointer; padding: 2px 4px; border-radius: 4px; transition: color 0.2s, background-color 0.2s;
}
#minimizeButton:hover { color: #fff; background-color: rgba(255, 255, 255, 0.1); }
#riffControlMinimizedIcon {
position: fixed; top: ${MINIMIZED_ICON_TOP}; right: ${MINIMIZED_ICON_RIGHT}; width: ${MINIMIZED_ICON_SIZE}; height: ${MINIMIZED_ICON_SIZE}; background: linear-gradient(145deg, #3a3a3a, #2c2c2c); border: 1px solid #555; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: bold; display: ${isMinimized ? 'flex' : 'none'}; align-items: center; justify-content: center; cursor: pointer; z-index: 10001; transition: background 0.2s; user-select: none;
}
#riffControlMinimizedIcon:hover { background: linear-gradient(145deg, #4a4a4a, #3c3c3c); }
#riffControlContent { padding: 12px; /* Reduced padding */ }
.riffControlButton { display: block; border: none; border-radius: 6px; /* Slightly smaller radius */ padding: 8px; /* Reduced padding */ font-size: 13px; /* Slightly smaller */ font-weight: 500; text-align: center; cursor: pointer; transition: transform 0.15s, background 0.15s; width: 100%; margin-bottom: 8px; /* Reduced margin */ }
.riffControlButton:hover:not(:disabled) { transform: translateY(-1px); } /* Less hover effect */
.riffControlButton:disabled { background: #555 !important; cursor: not-allowed; transform: none; opacity: 0.7; }
.riffMenuButton { background: linear-gradient(90deg, #4d94ff, #3385ff); color: #fff; }
.riffMenuButton:hover:not(:disabled) { background: linear-gradient(90deg, #3385ff, #1a75ff); }
.riffBackButton { background: linear-gradient(90deg, #888, #666); color: #fff; margin-top: 12px; /* Reduced */ margin-bottom: 0; } /* No bottom margin on back */
.riffBackButton:hover:not(:disabled) { background: linear-gradient(90deg, #666, #444); }
#deleteAllButton, #deleteButton { background: linear-gradient(90deg, #ff4d4d, #e63939); color: #fff; }
#deleteAllButton:hover:not(:disabled), #deleteButton:hover:not(:disabled) { background: linear-gradient(90deg, #e63939, #cc3333); }
#startDownloadQueueButton { background: linear-gradient(90deg, #1db954, #17a34a); color: #fff; }
#startDownloadQueueButton:hover:not(:disabled) { background: linear-gradient(90deg, #17a34a, #158a3f); }
#reloadDeleteButton, #reloadDownloadButton { background: linear-gradient(90deg, #ff9800, #e68a00); color: #fff; }
#reloadDeleteButton:hover:not(:disabled), #reloadDownloadButton:hover:not(:disabled) { background: linear-gradient(90deg, #e68a00, #cc7a00); }
#debugToggle { background: linear-gradient(90deg, #6666ff, #4d4dff); color: #fff; margin-top: 10px; /* Space before debug */ }
#debugToggle:hover:not(:disabled) { background: linear-gradient(90deg, #4d4dff, #3333cc); }
#statusMessage { margin-top: 8px; /* Reduced margin */ font-size: 12px; /* Slightly smaller */ color: #1db954; text-align: center; min-height: 1.1em; word-wrap: break-word; }
.section-controls { display: none; }
.songListContainer { margin-bottom: 10px; /* Reduced margin */ max-height: 22vh; /* Reduced height */ overflow-y: auto; padding-right: 5px; border: 1px solid #444; border-radius: 5px; background-color: rgba(0,0,0,0.1); padding: 6px; /* Reduced padding */ }
.songListContainer label { display: flex; align-items: center; margin: 6px 0; /* Reduced margin */ color: #d0d0d0; font-size: 13px; /* Slightly smaller */ transition: color 0.2s; }
.songListContainer label:hover:not(.ignored) { color: #ffffff; }
.songListContainer input[type="checkbox"] { margin-right: 8px; accent-color: #1db954; width: 15px; height: 15px; /* Slightly smaller */ cursor: pointer; flex-shrink: 0; }
.songListContainer input[type="checkbox"]:disabled { cursor: not-allowed; accent-color: #555; }
.songListContainer label.ignored { color: #777; cursor: not-allowed; font-style: italic; }
.songListContainer label.liked { font-weight: bold; color: #8c8cff; }
.songListContainer label.liked:hover { color: #a0a0ff; }
.selectAllContainer { margin-bottom: 8px; /* Reduced margin */ display: flex; align-items: center; color: #d0d0d0; font-size: 13px; /* Slightly smaller */ font-weight: 500; cursor: pointer; }
.selectAllContainer input[type="checkbox"] { margin-right: 8px; accent-color: #1db954; width: 15px; height: 15px; /* Slightly smaller */ }
.selectAllContainer:hover { color: #ffffff; }
.counterDisplay { margin-bottom: 8px; /* Reduced margin */ font-size: 13px; /* Slightly smaller */ color: #1db954; text-align: center; }
.songListContainer::-webkit-scrollbar { width: 6px; /* Narrower scrollbar */ }
.songListContainer::-webkit-scrollbar-track { background: #333; border-radius: 3px; }
.songListContainer::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
.songListContainer::-webkit-scrollbar-thumb:hover { background: #777; }
.filterSettings { margin-top: 8px; margin-bottom: 8px; padding-top: 8px; border-top: 1px solid #444; }
.filterSettings label, .downloadFormatContainer label, .downloadDelayContainer label { display: flex; align-items: center; font-size: 12px; /* Slightly smaller */ color: #ccc; cursor: pointer; margin-bottom: 6px; /* Reduced margin */ }
.filterSettings label:hover, .downloadFormatContainer label:hover, .downloadDelayContainer label:hover { color: #fff; }
.filterSettings input[type="checkbox"], .downloadFormatContainer input[type="checkbox"] { margin-right: 6px; accent-color: #1db954; width: 14px; height: 14px; cursor: pointer; }
.filterSettings input[type="text"], .filterSettings input[type="number"] { width: 100%; background-color: #333; border: 1px solid #555; color: #ddd; padding: 5px 8px; /* Reduced padding */ border-radius: 5px; font-size: 12px; /* Slightly smaller */ box-sizing: border-box; margin-top: 4px; /* Reduced margin */ }
.filterSettings input[type="text"]:focus, .filterSettings input[type="number"]:focus { outline: none; border-color: #777; }
#downloadSelectLiked { background: linear-gradient(90deg, #6666ff, #4d4dff); color: #fff; }
#downloadSelectLiked:hover:not(:disabled) { background: linear-gradient(90deg, #4d4dff, #3333cc); }
.downloadButtonRow { display: flex; align-items: center; gap: 6px; /* Reduced gap */ margin-bottom: 8px; /* Reduced margin */ }
#downloadSelectLiked { flex-grow: 1; }
#downloadClearSelection {
background: linear-gradient(90deg, #ff4d4d, #e63939); color: #fff;
width: 28px; height: 28px; /* Slightly smaller */ padding: 0; font-size: 15px; font-weight: bold; line-height: 1; border: none; border-radius: 5px; cursor: pointer; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 0; transition: transform 0.15s, background 0.15s;
}
#downloadClearSelection:hover:not(:disabled) { background: linear-gradient(90deg, #e63939, #cc3333); transform: translateY(-1px); }
#downloadClearSelection:disabled { background: #555 !important; cursor: not-allowed; transform: none; opacity: 0.7; }
.downloadFormatContainer { margin-top: 8px; padding-top: 8px; border-top: 1px solid #444; }
.downloadFormatContainer > label { margin-bottom: 4px; /* Reduced margin */ justify-content: center; display: block; text-align: center;}
.downloadFormatContainer div { display: flex; justify-content: space-around; margin-top: 4px; }
.downloadFormatContainer label { margin-bottom: 0; } /* Ensure format labels themselves have no extra bottom margin */
.downloadDelayContainer { margin-top: 8px; padding-top: 8px; border-top: 1px solid #444; display: flex; justify-content: space-between; gap: 10px; /* Reduced gap */ }
.downloadDelayContainer > div { flex: 1; }
.downloadDelayContainer label { margin-bottom: 2px; display: block; }
.downloadDelayContainer input[type="number"] { margin-top: 0; }
/* Bulk delete description */
#bulkModeControls p { font-size: 11px; color:#aaa; text-align:center; margin-top:4px; margin-bottom: 8px; }
`);
// --- Helper Functions ---
function debounce(func, wait) { let t; return function(...a) { const l=()=> { clearTimeout(t); func.apply(this,a); }; clearTimeout(t); t=setTimeout(l, wait); }; }
function log(m, l='info') { const p="[RiffTool]"; if(l==='error') console.error(`${p} ${m}`); else if(l==='warn') console.warn(`${p} ${m}`); else console.log(`${p} ${m}`); updateStatusMessage(m); }
function logDebug(m, e=null) { if(!debugMode) return; console.log(`[RiffTool DEBUG] ${m}`, e instanceof Element ? e.outerHTML.substring(0,250)+'...' : e !== null ? e : ''); }
function updateStatusMessage(m) { const s=document.getElementById('statusMessage'); if(s) s.textContent = m.length > 100 ? `... ${m.substring(m.length - 100)}` : m; }
function simulateClick(e) { if (!e) { logDebug('Element null for click'); return false; } try { ['pointerdown','mousedown','pointerup','mouseup','click'].forEach(t => e.dispatchEvent(new MouseEvent(t,{bubbles:true,cancelable:true}))); if(typeof e.click==='function') e.click(); logDebug('Sim Click:', e); return true; } catch (err) { log(`Click fail: ${err.message}`, 'error'); console.error('[RiffTool] Click details:', err, e); return false; } }
function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
// --- UI Functions ---
function createMainUI() {
uiElement = document.createElement('div');
uiElement.id = 'riffControlUI';
// Position calculation remains the same
if (UI_INITIAL_RIGHT) {
const rightPx = parseInt(UI_INITIAL_RIGHT, 10);
const widthPx = 300; // Match style width
lastUiLeft = `${Math.max(0, window.innerWidth - rightPx - widthPx)}px`;
uiElement.style.left = lastUiLeft;
uiElement.style.right = 'auto';
} else {
lastUiLeft = '20px';
uiElement.style.left = lastUiLeft;
}
lastUiTop = UI_INITIAL_TOP;
uiElement.style.top = lastUiTop;
uiElement.style.display = isMinimized ? 'none' : 'block';
uiElement.innerHTML = `
<div id="riffControlHeader">
<h3>Riffusion Multitool v${GM_info.script.version}</h3>
<button id="minimizeButton" title="Minimize UI">_</button>
</div>
<div id="riffControlContent">
<!-- Main Menu -->
<div id="mainMenuControls" class="section-controls">
<button id="goToSelectiveDelete" class="riffMenuButton riffControlButton">Selective Deletion</button> <!-- Shortened -->
<button id="goToBulkDelete" class="riffMenuButton riffControlButton">Bulk Deletion</button> <!-- Shortened -->
<button id="goToDownloadQueue" class="riffMenuButton riffControlButton">Download Queue</button> <!-- Shortened -->
</div>
<!-- Selective Deletion -->
<div id="selectiveModeControls" class="section-controls">
<button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button> <!-- Shortened -->
<label class="selectAllContainer"><input type="checkbox" id="deleteSelectAll"> Select All Visible</label> <!-- Shortened -->
<div id="deleteSongList" class="songListContainer">Loading...</div>
<div id="deleteCounter" class="counterDisplay">Deleted: 0 / 0</div>
<button id="deleteButton" class="riffControlButton">Delete Selected</button>
<button id="reloadDeleteButton" class="riffControlButton">Reload List</button> <!-- Shortened -->
<div class="filterSettings">
<label><input type="checkbox" id="ignoreLikedToggleDelete"> Ignore Liked</label> <!-- Shortened -->
<input type="text" id="deleteKeywordFilterInput" placeholder="Keywords to ignore (comma-sep)...">
</div>
</div>
<!-- Bulk Deletion -->
<div id="bulkModeControls" class="section-controls">
<button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button>
<button id="deleteAllButton" class="riffControlButton">Delete Entire Library</button>
<p>Deletes all songs without scrolling. Retries if needed.</p> <!-- Shortened -->
</div>
<!-- Download Queue -->
<div id="downloadQueueControls" class="section-controls">
<button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button>
<label class="selectAllContainer"><input type="checkbox" id="downloadSelectAll"> Select All</label>
<div class="downloadButtonRow">
<button id="downloadSelectLiked" class="riffControlButton">Select/Deselect Liked</button> <!-- Shortened -->
<button id="downloadClearSelection" title="Clear Selection" class="riffControlButton">C</button>
</div>
<div id="downloadSongList" class="songListContainer">Loading...</div>
<div id="downloadCounter" class="counterDisplay">Downloaded: 0 / 0</div>
<button id="startDownloadQueueButton" class="riffControlButton">Start Download Queue</button>
<button id="reloadDownloadButton" class="riffControlButton">Reload List</button> <!-- Shortened -->
<div class="filterSettings">
<label for="downloadKeywordFilterInput">Filter list by keywords:</label>
<input type="text" id="downloadKeywordFilterInput" placeholder="Keywords to show (comma-sep)...">
<div class="downloadFormatContainer">
<label>Download Formats:</label>
<div>
<label><input type="checkbox" id="formatMP3" value="MP3" checked> MP3</label>
<label><input type="checkbox" id="formatM4A" value="M4A"> M4A</label>
<label><input type="checkbox" id="formatWAV" value="WAV"> WAV</label>
</div>
</div>
<div class="downloadDelayContainer">
<div>
<label for="downloadIntraFormatDelayInput">Format Delay (s):</label> <!-- Shortened -->
<input type="number" id="downloadIntraFormatDelayInput" min="0" step="0.1" value="${DEFAULT_INTRA_FORMAT_DELAY_SECONDS}">
</div>
<div>
<label for="downloadInterSongDelayInput">Song Delay (s):</label> <!-- Shortened -->
<input type="number" id="downloadInterSongDelayInput" min="1" value="${DEFAULT_INTER_SONG_DELAY_SECONDS}">
</div>
</div>
</div>
</div>
<!-- Footer -->
<button id="debugToggle" class="riffControlButton">${debugMode?'Disable Debug':'Enable Debug'}</button>
<div id="statusMessage">Ready.</div>
</div>`;
document.body.appendChild(uiElement);
minimizedIconElement = document.createElement('div');
minimizedIconElement.id = 'riffControlMinimizedIcon';
minimizedIconElement.textContent = 'RM';
minimizedIconElement.title = 'Restore Riffusion Multitool';
minimizedIconElement.style.display = isMinimized ? 'flex' : 'none';
document.body.appendChild(minimizedIconElement);
// --- Event Listeners ---
const header = uiElement.querySelector('#riffControlHeader');
enableDrag(uiElement, header);
document.getElementById('minimizeButton')?.addEventListener('click', minimizeUI);
minimizedIconElement?.addEventListener('click', restoreUI);
// Menu Navigation
document.getElementById('goToSelectiveDelete')?.addEventListener('click', () => navigateToView('selective'));
document.getElementById('goToBulkDelete')?.addEventListener('click', () => navigateToView('bulk'));
document.getElementById('goToDownloadQueue')?.addEventListener('click', () => navigateToView('download'));
uiElement.querySelectorAll('.backToMenuButton').forEach(btn => btn.addEventListener('click', () => navigateToView('menu')));
// Selective Delete Controls
document.getElementById('deleteSelectAll')?.addEventListener('change', (e) => toggleSelectAll(e, '#deleteSongList'));
document.getElementById('deleteButton')?.addEventListener('click', deleteSelectedSongs);
document.getElementById('reloadDeleteButton')?.addEventListener('click', () => { if (currentView === 'selective') populateDeleteSongList(); });
const ignoreLikedToggle = document.getElementById('ignoreLikedToggleDelete');
if (ignoreLikedToggle) { ignoreLikedToggle.checked = ignoreLikedSongsDeleteState; ignoreLikedToggle.addEventListener('change', (e) => { ignoreLikedSongsDeleteState = e.target.checked; log(`Ignore Liked Songs (Delete): ${ignoreLikedSongsDeleteState}`); populateDeleteSongList(); });}
const deleteKeywordInput = document.getElementById('deleteKeywordFilterInput');
if (deleteKeywordInput) { deleteKeywordInput.addEventListener('input', debounce(() => { log('Delete keywords changed, refreshing list...'); populateDeleteSongList(); }, KEYWORD_FILTER_DEBOUNCE)); }
// Bulk Delete Controls
document.getElementById('deleteAllButton')?.addEventListener('click', () => {if (isDeleting || isDownloading) { log("Operation already in progress.", "warn"); return; } if (confirm("ARE YOU SURE? This will attempt to delete ALL songs in your library without scrolling. NO UNDO.")) { deleteAllSongsInLibrary(); }});
// Download Queue Controls
document.getElementById('downloadSelectAll')?.addEventListener('change', (e) => toggleSelectAll(e, '#downloadSongList'));
document.getElementById('downloadSelectLiked')?.addEventListener('click', toggleSelectLiked);
document.getElementById('downloadClearSelection')?.addEventListener('click', clearDownloadSelection);
document.getElementById('startDownloadQueueButton')?.addEventListener('click', startDownloadQueue);
document.getElementById('reloadDownloadButton')?.addEventListener('click', () => { if (currentView === 'download') populateDownloadSongList(); });
const downloadKeywordInput = document.getElementById('downloadKeywordFilterInput');
if (downloadKeywordInput) { downloadKeywordInput.addEventListener('input', debounce(() => { log('Download filter changed, refreshing list...'); populateDownloadSongList(); }, KEYWORD_FILTER_DEBOUNCE)); }
const interSongDelayInput = document.getElementById('downloadInterSongDelayInput');
if (interSongDelayInput) { interSongDelayInput.value = downloadInterSongDelaySeconds; interSongDelayInput.addEventListener('input', (e) => { const val = parseInt(e.target.value, 10); if (!isNaN(val) && val >= 0) { downloadInterSongDelaySeconds = val; log(`Inter-Song delay set to: ${downloadInterSongDelaySeconds}s`); } }); }
const intraFormatDelayInput = document.getElementById('downloadIntraFormatDelayInput');
if (intraFormatDelayInput) { intraFormatDelayInput.value = downloadIntraFormatDelaySeconds; intraFormatDelayInput.addEventListener('input', (e) => { const val = parseFloat(e.target.value); if (!isNaN(val) && val >= 0) { downloadIntraFormatDelaySeconds = val; log(`Intra-Format delay set to: ${downloadIntraFormatDelaySeconds}s`); } }); }
// Global Controls
document.getElementById('debugToggle').addEventListener('click', toggleDebug);
updateUIVisibility();
}
function minimizeUI() {
if (!uiElement || !minimizedIconElement) return;
if (!isMinimized) {
lastUiTop = uiElement.style.top || UI_INITIAL_TOP;
lastUiLeft = uiElement.style.left || lastUiLeft;
}
uiElement.style.display = 'none';
minimizedIconElement.style.display = 'flex';
isMinimized = true;
logDebug("UI Minimized");
}
function restoreUI() {
if (!uiElement || !minimizedIconElement) return;
minimizedIconElement.style.display = 'none';
uiElement.style.display = 'block';
uiElement.style.top = lastUiTop;
uiElement.style.left = lastUiLeft;
uiElement.style.right = 'auto';
isMinimized = false;
logDebug("UI Restored to:", { top: lastUiTop, left: lastUiLeft });
updateUIVisibility();
}
function navigateToView(view) {
if (isDeleting || isDownloading) {
log("Cannot switch views while an operation is in progress.", "warn");
return;
}
logDebug(`Navigating to view: ${view}`);
currentView = view;
updateUIVisibility();
}
function updateUIVisibility() {
if (isMinimized || !uiElement) return;
const sections = {
menu: document.getElementById('mainMenuControls'),
selective: document.getElementById('selectiveModeControls'),
bulk: document.getElementById('bulkModeControls'),
download: document.getElementById('downloadQueueControls')
};
const headerTitle = uiElement.querySelector('#riffControlHeader h3');
const statusMsg = document.getElementById('statusMessage');
let title = `Riffusion Multitool v${GM_info.script.version}`;
Object.values(sections).forEach(section => {
if (section) section.style.display = 'none';
});
if (sections[currentView]) {
sections[currentView].style.display = 'block';
switch (currentView) {
case 'menu':
title += " - Menu";
updateStatusMessage("Select a tool.");
break;
case 'selective':
title += " - Selective Deletion";
populateDeleteSongListIfNeeded();
updateStatusMessage("Select songs to delete.");
break;
case 'bulk':
title += " - Bulk Deletion";
updateStatusMessage("Warning: Deletes entire library.");
break;
case 'download':
title += " - Download Queue";
populateDownloadSongListIfNeeded();
updateStatusMessage("Select songs to download.");
break;
}
} else {
log(`View '${currentView}' not found, showing menu.`, 'warn');
sections.menu.style.display = 'block';
currentView = 'menu';
title += " - Menu";
updateStatusMessage("Select a tool.");
}
if (headerTitle) headerTitle.textContent = title;
if (statusMsg) statusMsg.style.display = 'block';
logDebug(`UI Visibility Updated. Current View: ${currentView}`);
}
function toggleDebug() {
debugMode = !debugMode;
const btn = document.getElementById('debugToggle');
if (btn) btn.textContent = debugMode ? 'Disable Debug' : 'Enable Debug';
log(`Debug mode ${debugMode ? 'enabled' : 'disabled'}.`);
}
function enableDrag(element, handle) {
let isDragging = false, offsetX, offsetY;
handle.addEventListener('mousedown', (e) => {
if (e.button !== 0 || e.target.closest('button')) return;
if (isMinimized) return;
isDragging = true;
const rect = element.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
element.style.cursor = 'grabbing';
handle.style.cursor = 'grabbing';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp, { once: true });
e.preventDefault();
});
function onMouseMove(e) {
if (!isDragging) return;
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const elWidth = element.offsetWidth;
const elHeight = element.offsetHeight;
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX + elWidth > winWidth) newX = winWidth - elWidth;
if (newY + elHeight > winHeight) newY = winHeight - elHeight;
element.style.left = `${newX}px`;
element.style.top = `${newY}px`;
element.style.right = 'auto';
}
function onMouseUp(e) {
if (e.button !== 0 || !isDragging) return;
isDragging = false;
element.style.cursor = 'default';
handle.style.cursor = 'move';
document.removeEventListener('mousemove', onMouseMove);
if (!isMinimized) {
lastUiTop = element.style.top;
lastUiLeft = element.style.left;
logDebug("Stored new position after drag:", { top: lastUiTop, left: lastUiLeft });
}
}
}
// --- Song List Population & Filtering ---
function getSongDataFromPage() {
const songElements = getCurrentSongElements();
logDebug(`Found ${songElements.length} song elements on page.`);
const songs = [];
songElements.forEach((songElement, index) => {
const titleLink = songElement.querySelector('a[href^="/song/"]');
const titleH4 = titleLink ? titleLink.querySelector('h4.text-primary') : null;
const title = titleH4 ? titleH4.textContent.trim() : `Untitled Song ${index + 1}`;
let songId = null;
if (titleLink) {
const match = titleLink.href.match(/\/song\/([a-f0-9-]+)/);
if (match && match[1]) songId = match[1];
}
if (!songId) {
log(`Could not extract song ID for element at index ${index} ('${title}'). Skipping.`, "warn");
return;
}
const likedIcon = songElement.querySelector('svg[data-prefix="fas"][data-icon="heart"]');
const isLiked = likedIcon !== null;
songs.push({
id: songId,
title: title,
titleLower: title.toLowerCase(),
isLiked: isLiked,
element: songElement
});
});
return songs;
}
function populateDeleteSongListIfNeeded() {
const songListDiv = document.getElementById('deleteSongList');
if (!songListDiv) return;
if (songListDiv.innerHTML === '' || songListDiv.innerHTML === 'Loading...' || songListDiv.children.length === 0) {
populateDeleteSongList();
}
}
function populateDeleteSongList() {
if (currentView !== 'selective' || isMinimized) return;
logDebug('Populating DELETE song list with filters...');
const songListDiv = document.getElementById('deleteSongList');
const deleteCounter = document.getElementById('deleteCounter');
if (!songListDiv) return;
songListDiv.innerHTML = 'Loading...';
if(deleteCounter) deleteCounter.textContent = 'Deleted: 0 / 0';
const selectAllCheckbox = document.getElementById('deleteSelectAll');
if (selectAllCheckbox) selectAllCheckbox.checked = false;
const ignoreLikedCheckbox = document.getElementById('ignoreLikedToggleDelete');
if(ignoreLikedCheckbox) ignoreLikedCheckbox.checked = ignoreLikedSongsDeleteState;
const keywordInput = document.getElementById('deleteKeywordFilterInput');
const keywordString = keywordInput ? keywordInput.value : '';
const dynamicIgnoreKeywords = keywordString.split(',').map(k => k.trim().toLowerCase()).filter(k => k !== '');
logDebug(`Keywords to ignore for delete: [${dynamicIgnoreKeywords.join(', ')}]`);
setTimeout(() => {
const songs = getSongDataFromPage();
songListDiv.innerHTML = '';
if (songs.length === 0) {
songListDiv.innerHTML = '<p style="color:#d0d0d0;text-align:center;font-size:12px;">No songs found.</p>'; // Adjusted message
updateStatusMessage("No songs found.");
return;
}
let ignoredCount = 0;
let visibleCount = 0;
songs.forEach(song => {
const keywordMatch = dynamicIgnoreKeywords.length > 0 && dynamicIgnoreKeywords.some(keyword => song.titleLower.includes(keyword));
const likedMatch = ignoreLikedSongsDeleteState && song.isLiked;
const shouldIgnore = keywordMatch || likedMatch;
let ignoreReason = '';
if (keywordMatch) ignoreReason += 'Keyword';
if (likedMatch) ignoreReason += (keywordMatch ? ' & Liked' : 'Liked');
const label = document.createElement('label');
label.innerHTML = `<input type="checkbox" data-song-id="${song.id}" ${shouldIgnore ? 'disabled' : ''}> ${song.title}`;
if (song.isLiked) label.classList.add('liked');
if (shouldIgnore) {
label.classList.add('ignored');
label.title = `Ignoring for delete: ${ignoreReason}`;
ignoredCount++;
} else {
visibleCount++;
}
songListDiv.appendChild(label);
});
logDebug(`Populated DELETE list: ${songs.length} total, ${visibleCount} selectable, ${ignoredCount} ignored.`);
updateStatusMessage(`Loaded ${songs.length} songs (${ignoredCount} ignored).`); // Simplified
}, 100);
}
function populateDownloadSongListIfNeeded() {
const songListDiv = document.getElementById('downloadSongList');
if (!songListDiv) return;
if (songListDiv.innerHTML === '' || songListDiv.innerHTML === 'Loading...' || songListDiv.children.length === 0) {
populateDownloadSongList();
}
}
function populateDownloadSongList() {
if (currentView !== 'download' || isMinimized) return;
logDebug('Populating DOWNLOAD song list with filters...');
const songListDiv = document.getElementById('downloadSongList');
const downloadCounter = document.getElementById('downloadCounter');
if (!songListDiv) return;
songListDiv.innerHTML = 'Loading...';
if(downloadCounter) downloadCounter.textContent = 'Downloaded: 0 / 0';
const selectAllCheckbox = document.getElementById('downloadSelectAll');
if (selectAllCheckbox) selectAllCheckbox.checked = false;
const keywordInput = document.getElementById('downloadKeywordFilterInput');
const keywordString = keywordInput ? keywordInput.value : '';
const filterKeywords = keywordString.split(',').map(k => k.trim().toLowerCase()).filter(k => k !== '');
logDebug(`Keywords to filter for download: [${filterKeywords.join(', ')}]`);
setTimeout(() => {
const songs = getSongDataFromPage();
songListDiv.innerHTML = '';
if (songs.length === 0) {
songListDiv.innerHTML = '<p style="color:#d0d0d0;text-align:center;font-size:12px;">No songs found.</p>'; // Adjusted message
updateStatusMessage("No songs found.");
updateSelectLikedButtonText();
return;
}
let displayedCount = 0;
songs.forEach(song => {
const keywordMatch = filterKeywords.length === 0 || filterKeywords.some(keyword => song.titleLower.includes(keyword));
if (keywordMatch) {
const label = document.createElement('label');
label.innerHTML = `<input type="checkbox" data-song-id="${song.id}" data-is-liked="${song.isLiked}"> ${song.title}`;
if (song.isLiked) {
label.classList.add('liked');
}
songListDiv.appendChild(label);
displayedCount++;
} else {
logDebug(`Filtering out song for download view ${song.id} ('${song.title}') due to keywords.`);
}
});
logDebug(`Populated DOWNLOAD list: ${songs.length} total, ${displayedCount} displayed after filtering.`);
updateStatusMessage(`Showing ${displayedCount} of ${songs.length} songs.`); // Simplified
updateSelectLikedButtonText();
}, 100);
}
function toggleSelectAll(event, listSelector) {
if (isMinimized) return;
const isChecked = event.target.checked;
const checkboxes = document.querySelectorAll(`${listSelector} input[type="checkbox"]:not(:disabled)`);
checkboxes.forEach(cb => cb.checked = isChecked);
logDebug(`Select All Toggled in ${listSelector}: ${isChecked} (${checkboxes.length} items affected)`);
if(listSelector === '#downloadSongList') {
updateSelectLikedButtonText();
}
}
function updateSelectLikedButtonText() {
if (currentView !== 'download' || isMinimized) return;
const button = document.getElementById('downloadSelectLiked');
if (!button) return;
const checkboxes = document.querySelectorAll('#downloadSongList input[type="checkbox"]:not(:disabled)');
if (checkboxes.length === 0) {
button.textContent = 'Select Liked'; // Simpler text when none available
return;
}
let shouldSelect = false;
checkboxes.forEach(cb => {
if (cb.dataset.isLiked === 'true' && !cb.checked) {
shouldSelect = true;
}
});
button.textContent = shouldSelect ? 'Select Liked' : 'Deselect Liked'; // Simpler text
}
function toggleSelectLiked() {
if (currentView !== 'download' || isMinimized) return;
const checkboxes = document.querySelectorAll('#downloadSongList input[type="checkbox"]:not(:disabled)');
if (checkboxes.length === 0) {
log("No songs available to select.", "warn");
return;
}
let shouldSelect = false;
checkboxes.forEach(cb => {
if (cb.dataset.isLiked === 'true' && !cb.checked) {
shouldSelect = true;
}
});
let changedCount = 0;
checkboxes.forEach(cb => {
if (cb.dataset.isLiked === 'true') {
if (cb.checked !== shouldSelect) {
cb.checked = shouldSelect;
changedCount++;
}
}
});
log(`Toggled selection for ${changedCount} liked songs. Action: ${shouldSelect ? 'Select' : 'Deselect'}`);
updateStatusMessage(`${shouldSelect ? 'Selected' : 'Deselected'} ${changedCount} liked songs.`);
const selectAllCheckbox = document.getElementById('downloadSelectAll');
if (selectAllCheckbox) {
const allVisibleCheckboxes = document.querySelectorAll('#downloadSongList input[type="checkbox"]:not(:disabled)');
const allVisibleChecked = document.querySelectorAll('#downloadSongList input[type="checkbox"]:not(:disabled):checked');
selectAllCheckbox.checked = allVisibleCheckboxes.length > 0 && allVisibleCheckboxes.length === allVisibleChecked.length;
}
updateSelectLikedButtonText();
}
function clearDownloadSelection() {
if (currentView !== 'download' || isMinimized) return;
const checkboxes = document.querySelectorAll('#downloadSongList input[type="checkbox"]:checked');
if (checkboxes.length === 0) {
log("No songs currently selected.", "info");
return;
}
checkboxes.forEach(cb => cb.checked = false);
const selectAllCheckbox = document.getElementById('downloadSelectAll');
if (selectAllCheckbox) selectAllCheckbox.checked = false;
log(`Cleared selection for ${checkboxes.length} songs.`);
updateStatusMessage("Selection cleared.");
updateSelectLikedButtonText();
}
function updateCounter(type, count, total) {
if (isMinimized) return;
let counterElementId = '';
if (type === 'delete') counterElementId = 'deleteCounter';
else if (type === 'download') counterElementId = 'downloadCounter';
else return;
const counterElement = document.getElementById(counterElementId);
if (counterElement) {
const prefix = type === 'delete' ? 'Deleted' : 'Downloaded';
counterElement.textContent = `${prefix}: ${count} / ${total}`;
}
logDebug(`${type} Counter Updated: ${count}/${total}`);
}
// --- Deletion Logic ---
function getCurrentSongElements() {
const listContainer = document.querySelector('div[data-sentry-component="InfiniteScroll"] > div.grow');
if(listContainer) {
return listContainer.querySelectorAll(':scope > div[data-sentry-component="DraggableRiffRow"]');
}
log("Warning: Specific list container not found, using fallback selector.", "warn");
return document.querySelectorAll('div[data-sentry-component="DraggableRiffRow"]');
}
async function deleteSelectedSongs() {
if (isMinimized) { log("Please restore the UI to delete.", "warn"); return; }
if (currentView !== 'selective') { log("Selective delete only available in Selective View.", "warn"); return; }
if (isDeleting || isDownloading) { log("Another operation is already in progress.", "warn"); return; }
const checkboxes = document.querySelectorAll('#deleteSongList input[type="checkbox"]:checked:not(:disabled)');
const totalToDelete = checkboxes.length;
if (totalToDelete === 0) {
updateCounter('delete', 0, 0);
log('No valid songs selected for deletion.');
updateStatusMessage('No songs selected or all selected are ignored.');
return;
}
isDeleting = true;
setAllButtonsDisabled(true);
const songIdsToDelete = Array.from(checkboxes).map(cb => cb.dataset.songId);
log(`Starting deletion for ${totalToDelete} selected song IDs: [${songIdsToDelete.join(', ')}]`);
updateCounter('delete', 0, totalToDelete);
updateStatusMessage(`Deleting ${totalToDelete} selected...`);
let deletedCount = 0;
let criticalErrorOccurred = false;
for (const songId of songIdsToDelete) {
logDebug(`Processing Song ID for delete: ${songId}`);
if (criticalErrorOccurred || !isDeleting) {
log(`Stopping deletion loop. CritErr: ${criticalErrorOccurred}, IsDeleting: ${isDeleting}`, "warn");
break;
}
const songElement = document.querySelector(`div[data-sentry-component="DraggableRiffRow"] a[href="/song/${songId}"]`)?.closest('div[data-sentry-component="DraggableRiffRow"]');
if (!songElement) {
log(`Song row for ID ${songId} not found on page (already deleted?). Skipping.`, "warn");
const checkboxToRemove = document.querySelector(`#deleteSongList input[data-song-id="${songId}"]`);
checkboxToRemove?.closest('label')?.remove();
continue;
}
const titleH4 = songElement.querySelector('h4.text-primary');
const title = titleH4 ? titleH4.textContent.trim() : `song ID ${songId}`;
logDebug(`Found element for ${title} (ID: ${songId}). Attempting delete...`);
const success = await processSingleAction(songElement, 'delete', songId);
logDebug(`processSingleAction(delete) result for ID ${songId}: ${success}`);
if (success) {
deletedCount++;
updateCounter('delete', deletedCount, totalToDelete);
updateStatusMessage(`Deleted ${deletedCount}/${totalToDelete}...`);
logDebug(`Successfully processed deletion for ID ${songId}. Count: ${deletedCount}`);
const checkboxToRemove = document.querySelector(`#deleteSongList input[data-song-id="${songId}"]`);
checkboxToRemove?.closest('label')?.remove();
} else {
log(`Failed to delete ${title} (ID: ${songId}). Stopping selective delete process.`, "error");
updateStatusMessage(`Error deleting ${title}. Stopped.`);
criticalErrorOccurred = true;
}
logDebug(`Delete loop iteration end for ID: ${songId}. Critical Error: ${criticalErrorOccurred}`);
await delay(50);
}
log(`Selective deletion loop finished. ${deletedCount} of ${totalToDelete} songs attempted.`);
updateStatusMessage(criticalErrorOccurred ? `Deletion stopped due to error. ${deletedCount} deleted.` : `Selected deletion complete. ${deletedCount} deleted.`);
isDeleting = false;
setAllButtonsDisabled(false);
}
async function deleteAllSongsInLibrary() {
if (isMinimized) { log("Please restore the UI to delete.", "warn"); return; }
if (currentView !== 'bulk') { log("Bulk delete only available in Bulk View.", "warn"); return; }
if (isDeleting || isDownloading) { log("Another operation is already in progress.", "warn"); return; }
isDeleting = true;
setAllButtonsDisabled(true);
log("--- STARTING LIBRARY DELETION (Bulk Mode / No-Scroll) ---");
updateStatusMessage("Starting full library deletion...");
let totalDeleted = 0;
let emptyChecks = 0;
while (isDeleting) {
await delay(500);
if (!isDeleting) { log("Deletion stopped externally.", "warn"); break; }
let currentElements = getCurrentSongElements();
let currentSize = currentElements.length;
log(`Checking for songs... Found ${currentSize}.`);
if (currentSize === 0) {
log(`No songs found. Waiting ${EMPTY_RETRY_DELAY / 1000}s (Check ${emptyChecks + 1}/${MAX_EMPTY_CHECKS})...`);
updateStatusMessage(`No songs. Re-checking in ${EMPTY_RETRY_DELAY / 1000}s...`);
await delay(EMPTY_RETRY_DELAY);
if (!isDeleting) { log("Deletion stopped during empty wait.", "warn"); break; }
currentElements = getCurrentSongElements();
currentSize = currentElements.length;
log(`Re-checking after delay... Found ${currentSize} songs.`);
if (currentSize > 0) {
log("Songs found after wait. Continuing deletion.");
updateStatusMessage(`Found ${currentSize} songs after wait. Resuming...`);
emptyChecks = 0;
} else {
emptyChecks++;
log(`Still empty (Check ${emptyChecks}/${MAX_EMPTY_CHECKS}).`);
updateStatusMessage(`Still empty (Check ${emptyChecks}/${MAX_EMPTY_CHECKS}).`);
if (emptyChecks >= MAX_EMPTY_CHECKS) {
log("No songs found after multiple retries. Assuming library is empty or cannot load more.");
updateStatusMessage("Library appears empty after retries.");
isDeleting = false;
break;
}
continue;
}
}
emptyChecks = 0;
log(`Processing batch of ${currentSize} songs...`);
let batchDeleted = 0;
while (currentSize > 0 && isDeleting) {
if (!isDeleting) { log("Deletion stopped during batch processing.", "warn"); break; }
const firstElement = getCurrentSongElements()[0];
if (!firstElement || !firstElement.parentNode) {
log(`Top song element disappeared unexpectedly. Re-evaluating list...`, "warn");
await delay(100);
currentSize = getCurrentSongElements().length;
continue;
}
const titleH4 = firstElement.querySelector('h4.text-primary');
const title = titleH4 ? titleH4.textContent.trim() : `Top song`;
const deletionIdentifier = `Bulk ${totalDeleted + batchDeleted + 1}`;
log(`Deleting: ${title} (${deletionIdentifier})...`);
updateStatusMessage(`Deleting ${title} (${totalDeleted + batchDeleted + 1} total...)`);
const success = await processSingleAction(firstElement, 'delete', deletionIdentifier);
if (success) {
batchDeleted++;
await delay(50);
currentSize = getCurrentSongElements().length;
} else {
log(`Failed to delete ${title}. Stopping bulk delete.`, "error");
updateStatusMessage(`Error deleting ${title}. Stopped.`);
isDeleting = false;
break;
}
await delay(50);
}
totalDeleted += batchDeleted;
if (isDeleting) {
log(`Batch attempt complete. Deleted ${batchDeleted} this round. Total: ${totalDeleted}. Checking for more...`);
updateStatusMessage(`Batch complete. Total: ${totalDeleted}. Checking for more...`);
}
}
const finalReason = !isDeleting && emptyChecks < MAX_EMPTY_CHECKS ? 'INTERRUPTED' : 'COMPLETE';
log(`--- LIBRARY DELETION ${finalReason} (Bulk Mode) --- Total deleted: ${totalDeleted}`);
updateStatusMessage(finalReason === 'INTERRUPTED' ? `Deletion stopped. Total: ${totalDeleted}` : `Deletion complete! Total: ${totalDeleted}`);
isDeleting = false;
setAllButtonsDisabled(false);
}
// --- Download Logic ---
async function startDownloadQueue() {
if (isMinimized) { log("Please restore the UI to download.", "warn"); return; }
if (currentView !== 'download') { log("Download only available in Download View.", "warn"); return; }
if (isDeleting || isDownloading) { log("Another operation is already in progress.", "warn"); return; }
const checkboxes = document.querySelectorAll('#downloadSongList input[type="checkbox"]:checked:not(:disabled)');
const totalSongsToDownload = checkboxes.length;
if (totalSongsToDownload === 0) {
updateCounter('download', 0, 0);
log('No valid songs selected for download.');
updateStatusMessage('No songs selected for download.');
return;
}
const selectedFormats = [];
if (document.getElementById('formatMP3')?.checked) selectedFormats.push('MP3');
if (document.getElementById('formatM4A')?.checked) selectedFormats.push('M4A');
if (document.getElementById('formatWAV')?.checked) selectedFormats.push('WAV');
if (selectedFormats.length === 0) {
log('No download formats selected.', 'error');
updateStatusMessage('Please select at least one download format.');
return;
}
isDownloading = true;
setAllButtonsDisabled(true);
const songIdsToDownload = Array.from(checkboxes).map(cb => cb.dataset.songId);
const interSongDelayMs = downloadInterSongDelaySeconds * 1000;
const intraFormatDelayMs = downloadIntraFormatDelaySeconds * 1000;
log(`Starting download queue for ${totalSongsToDownload} songs. Formats: [${selectedFormats.join(', ')}], Inter-Song Delay: ${downloadInterSongDelaySeconds}s, Intra-Format Delay: ${downloadIntraFormatDelaySeconds}s.`);
updateCounter('download', 0, totalSongsToDownload);
updateStatusMessage(`Downloading ${totalSongsToDownload} songs (${selectedFormats.join('/')})...`);
let songsProcessedCount = 0;
let criticalErrorOccurred = false;
for (const songId of songIdsToDownload) {
logDebug(`Processing Song ID for download: ${songId}`);
if (criticalErrorOccurred || !isDownloading) {
log(`Stopping download loop. CritErr: ${criticalErrorOccurred}, IsDownloading: ${isDownloading}`, "warn");
break;
}
const songElement = document.querySelector(`div[data-sentry-component="DraggableRiffRow"] a[href="/song/${songId}"]`)?.closest('div[data-sentry-component="DraggableRiffRow"]');
if (!songElement) {
log(`Song row for ID ${songId} not found on page. Skipping download for this song.`, "warn");
continue;
}
const titleH4 = songElement.querySelector('h4.text-primary');
const title = titleH4 ? titleH4.textContent.trim() : `song ID ${songId}`;
let songDownloadAttempted = false;
let songDownloadSuccess = false;
let formatIndex = 0;
for (const format of selectedFormats) {
const currentSongElementCheck = document.querySelector(`div[data-sentry-component="DraggableRiffRow"] a[href="/song/${songId}"]`)?.closest('div[data-sentry-component="DraggableRiffRow"]');
if (!currentSongElementCheck) {
log(`${title} (ID: ${songId}) element disappeared before downloading format ${format}. Skipping remaining formats for this song.`, "warn");
break;
}
logDebug(`Attempting download for ${title} (ID: ${songId}) - Format: ${format}`);
updateStatusMessage(`Downloading ${songsProcessedCount + 1}/${totalSongsToDownload}: ${title} (${format})...`);
songDownloadAttempted = true;
const success = await processSingleAction(currentSongElementCheck, 'download', `${songId}-${format}`, format);
logDebug(`processSingleAction(download) result for ID ${songId}, Format ${format}: ${success}`);
if (success) {
songDownloadSuccess = true;
log(`Successfully initiated download for ${title} (${format}).`);
formatIndex++;
if (formatIndex < selectedFormats.length && isDownloading && intraFormatDelayMs > 0) { // Check delay > 0
logDebug(`Waiting ${downloadIntraFormatDelaySeconds}s before next format...`);
await delay(intraFormatDelayMs);
} else if (isDownloading && intraFormatDelayMs <= 0 && formatIndex < selectedFormats.length) {
await delay(50); // Add a minimal delay even if set to 0 to prevent race conditions
}
} else {
log(`Failed to download ${title} (ID: ${songId}) - Format: ${format}. Stopping queue.`, "error");
updateStatusMessage(`Error downloading ${title} (${format}). Stopped.`);
criticalErrorOccurred = true;
break;
}
if (!isDownloading) {
log(`Download stopped externally during format loop for ${songId}.`, "warn");
break;
}
} // End inner format loop
if (songDownloadAttempted) {
songsProcessedCount++;
if (songDownloadSuccess) {
updateCounter('download', songsProcessedCount, totalSongsToDownload);
}
}
if (criticalErrorOccurred || !isDownloading) {
break; // Exit outer song loop
}
// Delay *after* processing all formats for one song, before the next song
if (songsProcessedCount < totalSongsToDownload && isDownloading) {
log(`Waiting ${downloadInterSongDelaySeconds}s before next song...`);
updateStatusMessage(`Waiting ${downloadInterSongDelaySeconds}s before next song...`);
await delay(interSongDelayMs);
}
} // End outer song loop
log(`Download queue finished. Processed ${songsProcessedCount} of ${totalSongsToDownload} selected songs.`);
updateStatusMessage(criticalErrorOccurred ? `Download stopped due to error. ${songsProcessedCount} songs processed.` : `Download queue complete. ${songsProcessedCount} songs processed.`);
isDownloading = false;
setAllButtonsDisabled(false);
}
// --- Generic Action Processor (Handles Delete or Download) ---
async function processSingleAction(songElement, actionType, identifier, format = null, retryCount = 0) {
const logPrefix = `(${actionType} - ID/Index: ${identifier}) -`;
if (!songElement || !songElement.parentNode) {
log(`${logPrefix} Song element is already gone. Assuming success for ${actionType}.`, "warn");
return true;
}
const menuButton = songElement.querySelector('button[aria-label^="More options for"]');
if (!menuButton) {
log(`${logPrefix} 'More options' button not found. Cannot proceed.`, "error");
logDebug(`${logPrefix} Searched within element:`, songElement);
return false;
}
logDebug(`${logPrefix} Clicking 'More options' button:`, menuButton);
if (!simulateClick(menuButton)) {
log(`${logPrefix} Failed to simulate click on 'More options'.`, "error");
return false;
}
await delay(DROPDOWN_DELAY);
// Find the Primary Action Item ('Delete' or 'Download')
let primaryActionText = actionType === 'delete' ? 'delete' : 'download';
let primaryActionItem = null;
let downloadMenuItemId = null;
const popperWrapper = document.querySelector(`div[data-radix-popper-content-wrapper][style*="transform: translate"]`);
let potentialItems = [];
if (popperWrapper) {
const menuContent = popperWrapper.querySelector('div[data-radix-menu-content][data-state="open"]');
if (menuContent) {
potentialItems = menuContent.querySelectorAll('[role="menuitem"]');
logDebug(`${logPrefix} Found ${potentialItems.length} potential menu items in active popper.`);
} else { logDebug(`${logPrefix} No open menu content in active popper.`); }
} else {
logDebug(`${logPrefix} No active popper wrapper found. Searching globally.`);
potentialItems = document.querySelectorAll('div[data-radix-popper-content-wrapper] div[data-radix-menu-content][data-state="open"] [role="menuitem"]');
}
primaryActionItem = Array.from(potentialItems).find(el => {
const textContentLower = el.textContent.trim().toLowerCase();
const isVisible = el.offsetParent !== null;
if (isVisible && textContentLower === 'download') {
downloadMenuItemId = el.getAttribute('aria-controls');
logDebug(`${logPrefix} Found Download item, controls submenu ID: ${downloadMenuItemId}`);
}
return isVisible && textContentLower === primaryActionText;
});
// Retry logic
if (!primaryActionItem && retryCount < MAX_RETRIES) {
log(`${logPrefix} '${primaryActionText}' option not found (Attempt ${retryCount + 1}/${MAX_RETRIES}). Retrying click sequence...`, "warn");
try { document.body.click(); await delay(150); } catch(e){}
if (!songElement || !songElement.parentNode) {
log(`${logPrefix} Song element disappeared before retry could occur.`, "warn");
return true;
}
const checkMenuButton = songElement.querySelector('button[aria-label^="More options for"]');
if (!checkMenuButton) {
log(`${logPrefix} Song element menu button disappeared before retry could occur.`, "warn");
return false;
}
return processSingleAction(songElement, actionType, identifier, format, retryCount + 1);
}
if (!primaryActionItem) {
log(`${logPrefix} '${primaryActionText}' option not found after ${MAX_RETRIES} retries. Aborting action.`, "error");
try { document.body.click(); } catch(e){}
return false;
}
// Click the Primary Action Item
logDebug(`${logPrefix} Clicking '${primaryActionText}' option (controls: ${downloadMenuItemId || 'N/A'}):`, primaryActionItem);
if (!simulateClick(primaryActionItem)) {
log(`${logPrefix} Failed to simulate click on '${primaryActionText}' option.`, "error");
try { document.body.click(); } catch(e){}
return false;
}
// Handle Subsequent Steps
if (actionType === 'delete') {
await delay(DELETION_DELAY);
logDebug(`--- Finished processing ${logPrefix} (Assumed Success after Delete Click) ---`);
try { document.body.click(); await delay(50); } catch(e){}
return true;
}
else if (actionType === 'download') {
if (!downloadMenuItemId) {
log(`${logPrefix} Submenu ID for 'Download' was not captured. Aborting download format ${format}.`, "error");
try { document.body.click(); } catch(e){}
return false;
}
await delay(DOWNLOAD_MENU_DELAY);
// Find the Format Item in the *Specific* Sub-Menu
let formatItem = null;
const formatTextUpper = format.toUpperCase();
const subMenuContent = document.getElementById(downloadMenuItemId);
let foundInSubMenu = false;
if (subMenuContent && subMenuContent.getAttribute('data-state') === 'open') {
foundInSubMenu = true;
logDebug(`${logPrefix} Found specific sub-menu container (ID: ${downloadMenuItemId}). Searching for format '${formatTextUpper}' within it.`);
const potentialFormatItems = subMenuContent.querySelectorAll('[role="menuitem"]');
formatItem = Array.from(potentialFormatItems).find(el => {
const textDiv = el.querySelector('.line-clamp-2');
const itemText = textDiv ? textDiv.textContent.trim().toUpperCase() : el.textContent.trim().toUpperCase();
logDebug(`${logPrefix} Checking potential format item in sub-menu: text='${itemText}', visible=${el.offsetParent !== null}`);
return itemText === formatTextUpper && el.offsetParent !== null && !el.querySelector('svg[data-icon="angle-right"]');
});
}
// If not found in specific submenu (or submenu wasn't found open), try delayed check / broader search
if (!formatItem) {
if(foundInSubMenu) {
log(`${logPrefix} Format '${formatTextUpper}' not in specific sub-menu (ID: ${downloadMenuItemId}). Checking again after delay...`);
} else {
log(`${logPrefix} Specific sub-menu (ID: ${downloadMenuItemId}) not found open. Checking again after delay...`);
}
await delay(250); // Extra delay for submenu appearance
const subMenuContentAgain = document.getElementById(downloadMenuItemId);
if (subMenuContentAgain && subMenuContentAgain.getAttribute('data-state') === 'open') {
const potentialFormatItemsAgain = subMenuContentAgain.querySelectorAll('[role="menuitem"]');
formatItem = Array.from(potentialFormatItemsAgain).find(el => {
const textDiv = el.querySelector('.line-clamp-2');
const itemText = textDiv ? textDiv.textContent.trim().toUpperCase() : el.textContent.trim().toUpperCase();
return itemText === formatTextUpper && el.offsetParent !== null && !el.querySelector('svg[data-icon="angle-right"]');
});
if(formatItem) {
logDebug(`${logPrefix} Found format '${formatTextUpper}' in specific sub-menu after delay.`);
}
}
}
// Final check if still not found
if (!formatItem) {
log(`${logPrefix} Format option '${formatTextUpper}' not found after checks. Aborting download format ${format}.`, "error");
logDebug(`${logPrefix} Submenu (ID: ${downloadMenuItemId}) content checked:`, subMenuContent ? subMenuContent.innerHTML.substring(0, 500) + '...' : 'Not Found or Not Open');
try { document.body.click(); } catch(e){}
return false;
}
// Click the Format Item
logDebug(`${logPrefix} Clicking format '${formatTextUpper}' option:`, formatItem);
if (!simulateClick(formatItem)) {
log(`${logPrefix} Failed to simulate click on format '${formatTextUpper}' option. Aborting format ${format}.`, "error");
try { document.body.click(); } catch(e){}
return false;
}
await delay(DOWNLOAD_ACTION_DELAY);
logDebug(`--- Finished processing ${logPrefix} (Assumed Success after Format Click) ---`);
// Close menus *after* download action delay
try { document.body.click(); await delay(50); } catch(e){}
return true;
}
log(`${logPrefix} Reached unexpected end of function.`, "error");
return false;
}
// --- Utility ---
function setAllButtonsDisabled(disabled) {
if (!uiElement || (isMinimized && disabled)) return;
const buttons = uiElement.querySelectorAll('#riffControlContent button');
buttons.forEach(btn => {
if (btn.id !== 'minimizeButton') {
btn.disabled = disabled;
}
});
const inputs = uiElement.querySelectorAll('#riffControlContent input, #riffControlContent select');
inputs.forEach(input => {
// Keep format and delay inputs always enabled for user interaction
if (input.id.startsWith('format') || input.id.includes('DelayInput')) {
input.disabled = false;
} else {
input.disabled = disabled;
}
});
const labels = uiElement.querySelectorAll('#riffControlContent label');
labels.forEach(label => {
const isFormatLabel = label.closest('.downloadFormatContainer') !== null;
const isDelayLabel = label.closest('.downloadDelayContainer') !== null;
// Only disable non-format/non-delay labels when 'disabled' is true
label.style.cursor = (disabled && !isFormatLabel && !isDelayLabel) ? 'not-allowed' : 'pointer';
label.style.opacity = (disabled && !isFormatLabel && !isDelayLabel) ? '0.7' : '1';
});
// Handle song list checkboxes separately
if(!disabled) {
// Re-enable based on view logic when controls are enabled
if(currentView === 'selective') populateDeleteSongListIfNeeded();
if(currentView === 'download') populateDownloadSongListIfNeeded();
} else {
// Disable song list checkboxes when other controls are disabled
uiElement.querySelectorAll('.songListContainer input[type="checkbox"]').forEach(cb => cb.disabled = true);
}
const status = document.getElementById('statusMessage');
if (status) status.style.pointerEvents = disabled ? 'none' : 'auto';
logDebug(`Controls ${disabled ? 'mostly disabled' : 'enabled'} (formats/delays always interactive)`);
}
// --- Initialization ---
function waitForPageLoad(callback) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(callback, 500);
} else {
window.addEventListener('load', () => { setTimeout(callback, 500); }, { once: true });
}
}
function init() {
if (window.location.pathname.includes('/library/my-songs')) {
try {
log(`Riffusion Multitool Script Loaded (v${GM_info.script.version}).`);
createMainUI();
log(`Initialized. Current View: ${currentView}. UI is ${isMinimized ? 'minimized (Top-Right)' : 'visible'}.`);
} catch (e) {
console.error("[RiffTool] Initialization failed:", e);
alert("[RiffTool] Failed to initialize script. See console for errors.");
}
} else {
logDebug("Not on the target /library/my-songs page.");
}
}
waitForPageLoad(init);
})();