Riffusion Multitool

Manage Riffusion songs: selective/bulk delete, download queue (MP3/M4A/WAV), privacy settings. Draggable/minimizable UI, filters, auto-reload & more. USE WITH CAUTION.

Size

111.9 KB

Version

1.70.1

Created

Oct 31, 2025

Updated

about 1 month ago

1// ==UserScript==
2// @name		Riffusion Multitool
3// @namespace		http://tampermonkey.net/
4// @version		1.70.1
5// @description		Manage Riffusion songs: selective/bulk delete, download queue (MP3/M4A/WAV), privacy settings. Draggable/minimizable UI, filters, auto-reload & more. USE WITH CAUTION.
6// @author		Graph1ks (assisted by GoogleAI)
7// @match		https://www.classic.riffusion.com/*
8// @grant		GM_addStyle
9// @grant		GM_info
10// @downloadURL		https://update.greasyfork.org/scripts/534612/Riffusion%20Multitool.user.js
11// @updateURL		https://update.greasyfork.org/scripts/534612/Riffusion%20Multitool.meta.js
12// ==/UserScript==
13(function() {
14    'use strict';
15
16    // --- Configuration ---
17    const SCRIPT_PREFIX = 'Graph1ksRiffTool_'; // Unique prefix for all IDs
18    const MAIN_UI_ID = `${SCRIPT_PREFIX}MainUI`;
19    const MINIMIZED_ICON_ID = `${SCRIPT_PREFIX}MinimizedIcon`;
20
21    const INITIAL_VIEW = 'menu';
22    const DELETION_DELAY = 500;
23    const DOWNLOAD_MENU_DELAY = 550;
24    const DOWNLOAD_ACTION_DELAY = 500;
25    const DEFAULT_INTRA_FORMAT_DELAY_SECONDS = 6;
26    const DROPDOWN_DELAY = 400; // General delay for main dropdowns to open
27    const DEFAULT_INTER_SONG_DELAY_SECONDS = 6;
28
29    // New Privacy Delays
30    const PRIVACY_MENU_OPEN_DELAY = 400; // Delay to open the "More Options" menu
31    const PRIVACY_SUBMENU_TRIGGER_CLICK_DELAY = 150; // Delay after clicking "Privacy" for submenu to register
32    const PRIVACY_SUBMENU_OPEN_DELAY = 450; // Delay for the privacy submenu itself to open
33    const PRIVACY_ACTION_CLICK_DELAY = 500; // Delay after clicking the specific privacy option
34    const DEFAULT_PRIVACY_INTER_SONG_DELAY_SECONDS = 6;
35
36
37    const MAX_RETRIES = 3;
38    const MAX_SUB_MENU_OPEN_RETRIES = 4; // Applies to download and privacy submenus
39    const MAX_EMPTY_CHECKS = 3;
40    const EMPTY_RETRY_DELAY = 6000;
41    const KEYWORD_FILTER_DEBOUNCE = 500;
42    const UI_INITIAL_TOP = '60px';
43    const UI_INITIAL_RIGHT = '20px';
44    const INITIAL_IGNORE_LIKED_DELETE = true;
45    const MINIMIZED_ICON_SIZE = '40px';
46    const MINIMIZED_ICON_TOP = '15px';
47    const MINIMIZED_ICON_RIGHT = '15px';
48    const AUTO_RELOAD_INTERVAL = 3000;
49    const DEFAULT_SONGLIST_HEIGHT = '22vh'; // Unified default height
50
51    // --- State Variables ---
52    let debugMode = false;
53    let isDeleting = false;
54    let isDownloading = false;
55    let isChangingPrivacy = false; // New state
56    let currentView = INITIAL_VIEW;
57    let ignoreLikedSongsDeleteState = INITIAL_IGNORE_LIKED_DELETE;
58
59    let downloadInterSongDelaySeconds = DEFAULT_INTER_SONG_DELAY_SECONDS;
60    let downloadIntraFormatDelaySeconds = DEFAULT_INTRA_FORMAT_DELAY_SECONDS;
61
62    // New Privacy State Variables
63    let privacyInterSongDelaySeconds = DEFAULT_PRIVACY_INTER_SONG_DELAY_SECONDS;
64
65    let stopBulkDeletionSignal = false;
66    let stopPrivacyChangeSignal = false; // New signal
67
68    let isMinimized = true;
69    let lastUiTop = UI_INITIAL_TOP;
70    let lastUiLeft = null;
71    let uiElement = null;
72    let minimizedIconElement = null;
73
74    let autoReloadEnabled = true;
75    let autoReloadTimer = null;
76    let lastKnownSongIdsDelete = [];
77    let lastKnownSongIdsDownload = [];
78    let lastKnownSongIdsPrivacy = []; // New
79    let selectedSongIdsDelete = new Set();
80    let selectedSongIdsDownload = new Set();
81    let selectedSongIdsPrivacy = new Set(); // New
82
83    let currentDeleteListHeight = DEFAULT_SONGLIST_HEIGHT;
84    let currentDownloadListHeight = DEFAULT_SONGLIST_HEIGHT;
85    let currentPrivacyListHeight = DEFAULT_SONGLIST_HEIGHT; // New
86
87    // Privacy button text map
88    const privacyButtonTextMap = {
89        'Only Me': 'Set to Only Me',
90        'Link Only': 'Set to Link Only',
91        'Publish': 'Publish Selected'
92    };
93    const privacySubmenuTextMap = { // Maps UI dropdown value to the exact text in the submenu
94        'Only Me': 'Only me',
95        'Link Only': 'Anyone with the link',
96        'Publish': 'Publish'
97    };
98
99
100    GM_addStyle(`
101        #${MAIN_UI_ID} {
102            position: fixed; background: linear-gradient(145deg, #2a2a2a, #1e1e1e); border: 1px solid #444; border-radius: 10px; padding: 0; z-index: 10000; width: 300px; 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;
103            display: ${isMinimized ? 'none' : 'block'};
104        }
105        #${MAIN_UI_ID} #riffControlHeader { /* Scoped to main UI */
106            background: linear-gradient(90deg, #3a3a3a, #2c2c2c); padding: 8px 12px; cursor: move; border-bottom: 1px solid #444; border-radius: 10px 10px 0 0; position: relative;
107        }
108        #${MAIN_UI_ID} #riffControlHeader h3 { margin: 0; font-size: 15px; font-weight: 600; color: #ffffff; text-align: center; text-shadow: 0 1px 1px rgba(0,0,0,0.2); padding-right: 25px; /* Space for minimize button */ }
109
110        #${MAIN_UI_ID} #minimizeButton { /* Scoped to main UI */
111            position: absolute; top: 4px; right: 6px; background: none; border: none; color: #aaa; font-size: 18px; font-weight: bold; line-height: 1; cursor: pointer; padding: 2px 4px; border-radius: 4px; transition: color 0.2s, background-color 0.2s;
112        }
113        #${MAIN_UI_ID} #minimizeButton:hover { color: #fff; background-color: rgba(255, 255, 255, 0.1); }
114
115        #${MINIMIZED_ICON_ID} {
116            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;
117        }
118        #${MINIMIZED_ICON_ID}:hover { background: linear-gradient(145deg, #4a4a4a, #3c3c3c); }
119
120        /* Scoping general rules to within the main UI container */
121        #${MAIN_UI_ID} #riffControlContent { padding: 12px; }
122        #${MAIN_UI_ID} .riffControlButton { display: block; border: none; border-radius: 6px; padding: 8px; font-size: 13px; font-weight: 500; text-align: center; cursor: pointer; transition: transform 0.15s, background 0.15s; width: 100%; margin-bottom: 8px; }
123        #${MAIN_UI_ID} .riffControlButton:hover:not(:disabled) { transform: translateY(-1px); }
124        #${MAIN_UI_ID} .riffControlButton:disabled { background: #555 !important; cursor: not-allowed; transform: none; opacity: 0.7; }
125        #${MAIN_UI_ID} .riffMenuButton { background: linear-gradient(90deg, #4d94ff, #3385ff); color: #fff; }
126        #${MAIN_UI_ID} .riffMenuButton:hover:not(:disabled) { background: linear-gradient(90deg, #3385ff, #1a75ff); }
127        #${MAIN_UI_ID} .riffBackButton { background: linear-gradient(90deg, #888, #666); color: #fff; margin-top: 12px; margin-bottom: 0; }
128        #${MAIN_UI_ID} .riffBackButton:hover:not(:disabled) { background: linear-gradient(90deg, #666, #444); }
129        #${MAIN_UI_ID} #deleteAllButton, #${MAIN_UI_ID} #deleteButton { background: linear-gradient(90deg, #ff4d4d, #e63939); color: #fff; }
130        #${MAIN_UI_ID} #deleteAllButton:hover:not(:disabled), #${MAIN_UI_ID} #deleteButton:hover:not(:disabled) { background: linear-gradient(90deg, #e63939, #cc3333); }
131        #${MAIN_UI_ID} #startDownloadQueueButton { background: linear-gradient(90deg, #1db954, #17a34a); color: #fff; }
132        #${MAIN_UI_ID} #startDownloadQueueButton:hover:not(:disabled) { background: linear-gradient(90deg, #17a34a, #158a3f); }
133        #${MAIN_UI_ID} #changePrivacyStatusButton { background: linear-gradient(90deg, #8A2BE2, #6A1B9A); color: #fff; } /* Purple for privacy */
134        #${MAIN_UI_ID} #changePrivacyStatusButton:hover:not(:disabled) { background: linear-gradient(90deg, #6A1B9A, #4A148C); }
135        #${MAIN_UI_ID} #reloadDeleteButton, #${MAIN_UI_ID} #reloadDownloadButton, #${MAIN_UI_ID} #reloadPrivacyButton { background: linear-gradient(90deg, #ff9800, #e68a00); color: #fff; }
136        #${MAIN_UI_ID} #reloadDeleteButton:hover:not(:disabled), #${MAIN_UI_ID} #reloadDownloadButton:hover:not(:disabled), #${MAIN_UI_ID} #reloadPrivacyButton:hover:not(:disabled) { background: linear-gradient(90deg, #e68a00, #cc7a00); }
137
138
139        #${MAIN_UI_ID} #statusMessage { margin-top: 8px; font-size: 12px; color: #1db954; text-align: center; min-height: 1.1em; word-wrap: break-word; }
140        #${MAIN_UI_ID} .section-controls { display: none; }
141        #${MAIN_UI_ID} .songListContainer { margin-bottom: 0px; overflow-y: auto; padding-right: 5px; /* For scrollbar */ border: 1px solid #444; border-radius: 5px; background-color: rgba(0,0,0,0.1); padding: 6px; }
142        #${MAIN_UI_ID} .songListContainer label { display: flex; align-items: center; margin: 6px 0; color: #d0d0d0; font-size: 13px; transition: color 0.2s; }
143        #${MAIN_UI_ID} .songListContainer label:hover:not(.ignored) { color: #ffffff; }
144        #${MAIN_UI_ID} .songListContainer input[type="checkbox"] { margin-right: 8px; accent-color: #1db954; width: 15px; height: 15px; cursor: pointer; flex-shrink: 0; }
145        #${MAIN_UI_ID} .songListContainer input[type="checkbox"]:disabled { cursor: not-allowed; accent-color: #555; }
146        #${MAIN_UI_ID} .songListContainer label.ignored { color: #777; cursor: not-allowed; font-style: italic; }
147        #${MAIN_UI_ID} .songListContainer label.liked { font-weight: bold; color: #8c8cff; /* Light purple/blue for liked */ }
148        #${MAIN_UI_ID} .songListContainer label.liked:hover { color: #a0a0ff; }
149
150        #${MAIN_UI_ID} .listResizer {
151            width: 100%; height: 8px; background-color: #4a4a4a; cursor: ns-resize;
152            border-radius: 3px; margin-top: 2px; margin-bottom: 10px; display: block;
153            transition: background-color 0.2s;
154        }
155        #${MAIN_UI_ID} .listResizer:hover { background-color: #5c5c5c; }
156
157        #${MAIN_UI_ID} .selectAllContainer { margin-bottom: 8px; display: flex; align-items: center; color: #d0d0d0; font-size: 13px; font-weight: 500; cursor: pointer; }
158        #${MAIN_UI_ID} .selectAllContainer input[type="checkbox"] { margin-right: 8px; accent-color: #1db954; width: 15px; height: 15px; }
159        #${MAIN_UI_ID} .selectAllContainer:hover { color: #ffffff; }
160        #${MAIN_UI_ID} .counterDisplay { margin-bottom: 8px; font-size: 13px; color: #1db954; text-align: center; }
161        #${MAIN_UI_ID} .songListContainer::-webkit-scrollbar { width: 6px; }
162        #${MAIN_UI_ID} .songListContainer::-webkit-scrollbar-track { background: #333; border-radius: 3px; }
163        #${MAIN_UI_ID} .songListContainer::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; }
164        #${MAIN_UI_ID} .songListContainer::-webkit-scrollbar-thumb:hover { background: #777; }
165
166        #${MAIN_UI_ID} .filterSettings { margin-top: 8px; margin-bottom: 8px; padding-top: 8px; border-top: 1px solid #444; }
167        #${MAIN_UI_ID} .settings-checkbox-label { display: flex; align-items: center; font-size: 12px; color: #ccc; cursor: pointer; margin-bottom: 6px; }
168        #${MAIN_UI_ID} .settings-checkbox-label:hover { color: #fff; }
169        #${MAIN_UI_ID} .settings-checkbox-label input[type="checkbox"] { margin-right: 6px; accent-color: #1db954; width: 14px; height: 14px; cursor: pointer; }
170
171        #${MAIN_UI_ID} .filterSettings input[type="text"], #${MAIN_UI_ID} .filterSettings input[type="number"] { width: 100%; background-color: #333; border: 1px solid #555; color: #ddd; padding: 5px 8px; border-radius: 5px; font-size: 12px; box-sizing: border-box; margin-top: 4px; }
172        #${MAIN_UI_ID} .filterSettings input[type="text"]:focus, #${MAIN_UI_ID} .filterSettings input[type="number"]:focus { outline: none; border-color: #777; }
173
174        #${MAIN_UI_ID} #downloadSelectLiked, #${MAIN_UI_ID} #privacySelectLiked { background: linear-gradient(90deg, #6666ff, #4d4dff); color: #fff; }
175        #${MAIN_UI_ID} #downloadSelectLiked:hover:not(:disabled), #${MAIN_UI_ID} #privacySelectLiked:hover:not(:disabled) { background: linear-gradient(90deg, #4d4dff, #3333cc); }
176        #${MAIN_UI_ID} .downloadButtonRow, #${MAIN_UI_ID} .privacyButtonRow { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
177        #${MAIN_UI_ID} #downloadSelectLiked, #${MAIN_UI_ID} #privacySelectLiked { flex-grow: 1; }
178        #${MAIN_UI_ID} #downloadClearSelection, #${MAIN_UI_ID} #privacyClearSelection {
179             background: linear-gradient(90deg, #ff4d4d, #e63939); color: #fff;
180             width: 28px; height: 28px; 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;
181        }
182         #${MAIN_UI_ID} #downloadClearSelection:hover:not(:disabled), #${MAIN_UI_ID} #privacyClearSelection:hover:not(:disabled) { background: linear-gradient(90deg, #e63939, #cc3333); transform: translateY(-1px); }
183         #${MAIN_UI_ID} #downloadClearSelection:disabled, #${MAIN_UI_ID} #privacyClearSelection:disabled { background: #555 !important; cursor: not-allowed; transform: none; opacity: 0.7; }
184
185         #${MAIN_UI_ID} .downloadFormatContainer { margin-top: 8px; padding-top: 8px; border-top: 1px solid #444; }
186         #${MAIN_UI_ID} .downloadFormatContainer > label.settings-checkbox-label { margin-bottom: 4px; justify-content: center; display: block; text-align: center;}
187         #${MAIN_UI_ID} .downloadFormatContainer div { display: flex; justify-content: space-around; margin-top: 4px; }
188
189         #${MAIN_UI_ID} .downloadDelayContainer { margin-top: 8px; padding-top: 8px; border-top: 1px solid #444; display: flex; justify-content: space-between; gap: 10px; }
190         #${MAIN_UI_ID} .downloadDelayContainer > div { flex: 1; }
191         #${MAIN_UI_ID} .downloadDelayContainer label.settings-checkbox-label { margin-bottom: 2px; display: block; }
192         #${MAIN_UI_ID} .downloadDelayContainer input[type="number"] { margin-top: 0; }
193
194         #${MAIN_UI_ID} #bulkModeControls p { font-size: 11px; color:#aaa; text-align:center; margin-top:4px; margin-bottom: 8px; }
195         #${MAIN_UI_ID} #commonSettingsFooter { margin-top: 10px; padding-top: 8px; border-top: 1px solid #444; }
196
197         /* Privacy Specific Settings */
198         #${MAIN_UI_ID} .privacySettingsContainer { margin-top: 8px; padding-top: 8px; border-top: 1px solid #444; }
199         #${MAIN_UI_ID} .privacySettingsContainer > label.settings-checkbox-label { display: block; margin-bottom: 4px; text-align: left; }
200         #${MAIN_UI_ID} .privacySettingsContainer select,
201         #${MAIN_UI_ID} .privacySettingsContainer input[type="number"] {
202            width: 100%; background-color: #333; border: 1px solid #555; color: #ddd;
203            padding: 5px 8px; border-radius: 5px; font-size: 12px;
204            box-sizing: border-box; margin-bottom: 8px;
205         }
206         #${MAIN_UI_ID} .privacySettingsContainer select:focus,
207         #${MAIN_UI_ID} .privacySettingsContainer input[type="number"]:focus { outline: none; border-color: #777; }
208         #${MAIN_UI_ID} .privacyDelayContainer { display: flex; justify-content: space-between; gap: 10px; } /* This class might be unused if only one delay input */
209         #${MAIN_UI_ID} .privacyDelayContainer > div { flex: 1; }
210    `);
211
212    // --- Helper Functions ---
213    function debounce(func, wait) { let t; return function(...a) { const l=()=> { clearTimeout(t); func.apply(this,a); }; clearTimeout(t); t=setTimeout(l, wait); }; }
214    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); }
215    function logDebug(m, e=null) { if(!debugMode) return; console.log(`[RiffTool DEBUG] ${m}`, e instanceof Element ? e.outerHTML.substring(0,250)+'...' : e !== null ? e : ''); }
216    function logWarn(m, e=null) { console.warn(`[RiffTool WARN] ${m}`, e instanceof Element ? e.outerHTML.substring(0,250)+'...' : e !== null ? e : ''); }
217    function updateStatusMessage(m) {
218        if (!uiElement) return;
219        const s = uiElement.querySelector('#statusMessage'); // Scoped query
220        if(s) s.textContent = m.length > 100 ? `... ${m.substring(m.length - 100)}` : m;
221    }
222
223    function simulateClick(e) {
224        if (!e) { logDebug('Element null for click'); return false; }
225        try {
226            ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach(t =>
227                e.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, composed: true }))
228            );
229            logDebug('Sim Click (full event sequence):', e);
230            return true;
231        } catch (err) {
232            log(`Click simulation failed: ${err.message}`, 'error');
233            console.error('[RiffTool] Click details:', err, e);
234            return false;
235        }
236    }
237    function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
238
239
240    // --- UI Functions ---
241    function createMainUI() {
242        uiElement = document.createElement('div');
243        uiElement.id = MAIN_UI_ID; // Use new unique ID
244        if (UI_INITIAL_RIGHT) {
245            const rightPx = parseInt(UI_INITIAL_RIGHT, 10);
246            const widthPx = parseInt(uiElement.style.width, 10) || 300; // Default width if not set
247            lastUiLeft = `${Math.max(0, window.innerWidth - rightPx - widthPx)}px`;
248            uiElement.style.left = lastUiLeft;
249            uiElement.style.right = 'auto';
250        } else {
251            lastUiLeft = '20px'; // Default left if right is not specified
252            uiElement.style.left = lastUiLeft;
253        }
254        lastUiTop = UI_INITIAL_TOP;
255        uiElement.style.top = lastUiTop;
256        uiElement.style.display = isMinimized ? 'none' : 'block';
257
258        uiElement.innerHTML = `
259            <div id="riffControlHeader">
260                <h3>Riffusion Multitool v${GM_info.script.version}</h3>
261                <button id="minimizeButton" title="Minimize UI">_</button>
262            </div>
263            <div id="riffControlContent">
264                <div id="mainMenuControls" class="section-controls">
265                    <button id="goToSelectiveDelete" class="riffMenuButton riffControlButton">Selective Deletion</button>
266                    <button id="goToBulkDelete" class="riffMenuButton riffControlButton">Bulk Deletion</button>
267                    <button id="goToDownloadQueue" class="riffMenuButton riffControlButton">Download Queue</button>
268                    <button id="goToPrivacyStatus" class="riffMenuButton riffControlButton">Privacy Status</button>
269                </div>
270                <div id="selectiveModeControls" class="section-controls">
271                    <button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button>
272                    <label class="selectAllContainer"><input type="checkbox" id="deleteSelectAll"> Select All Visible</label>
273                    <div id="deleteSongList" class="songListContainer" style="max-height: ${currentDeleteListHeight};">Loading...</div>
274                    <div class="listResizer" data-list-id="deleteSongList" data-height-var-name="currentDeleteListHeight"></div>
275                    <div id="deleteCounter" class="counterDisplay">Deleted: 0 / 0</div>
276                    <button id="deleteButton" class="riffControlButton">Delete Selected</button>
277                    <button id="reloadDeleteButton" class="riffControlButton" style="display: ${autoReloadEnabled ? 'none' : 'block'};">Reload List</button>
278                    <div class="filterSettings">
279                        <label class="settings-checkbox-label"><input type="checkbox" id="ignoreLikedToggleDelete"> Ignore Liked</label>
280                        <input type="text" id="deleteKeywordFilterInput" placeholder="Keywords to ignore (comma-sep)...">
281                    </div>
282                </div>
283                <div id="bulkModeControls" class="section-controls">
284                     <button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button>
285                    <button id="deleteAllButton" class="riffControlButton">Delete Entire Library</button>
286                    <p>Deletes all songs without scrolling. Retries if needed. Click "Stop Deletion" to halt.</p>
287                </div>
288                <div id="downloadQueueControls" class="section-controls">
289                    <button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button>
290                    <label class="selectAllContainer"><input type="checkbox" id="downloadSelectAll"> Select All</label>
291                    <div class="downloadButtonRow">
292                        <button id="downloadSelectLiked" class="riffControlButton">Select/Deselect Liked</button>
293                        <button id="downloadClearSelection" title="Clear Selection" class="riffControlButton">C</button>
294                    </div>
295                    <div id="downloadSongList" class="songListContainer" style="max-height: ${currentDownloadListHeight};">Loading...</div>
296                    <div class="listResizer" data-list-id="downloadSongList" data-height-var-name="currentDownloadListHeight"></div>
297                     <div id="downloadCounter" class="counterDisplay">Downloaded: 0 / 0</div>
298                    <button id="startDownloadQueueButton" class="riffControlButton">Start Download Queue</button>
299                    <button id="reloadDownloadButton" class="riffControlButton" style="display: ${autoReloadEnabled ? 'none' : 'block'};">Reload List</button>
300                    <div class="filterSettings">
301                        <label class="settings-checkbox-label" for="downloadKeywordFilterInput">Filter list by keywords:</label>
302                        <input type="text" id="downloadKeywordFilterInput" placeholder="Keywords to show (comma-sep)...">
303                        <div class="downloadFormatContainer">
304                            <label class="settings-checkbox-label">Download Formats:</label>
305                            <div>
306                                <label class="settings-checkbox-label"><input type="checkbox" id="formatMP3" value="MP3" checked> MP3</label>
307                                <label class="settings-checkbox-label"><input type="checkbox" id="formatM4A" value="M4A"> M4A</label>
308                                <label class="settings-checkbox-label"><input type="checkbox" id="formatWAV" value="WAV"> WAV</label>
309                            </div>
310                        </div>
311                        <div class="downloadDelayContainer">
312                             <div>
313                                 <label class="settings-checkbox-label" for="downloadIntraFormatDelayInput">Format Delay (s):</label>
314                                 <input type="number" id="downloadIntraFormatDelayInput" min="0" step="0.1" value="${DEFAULT_INTRA_FORMAT_DELAY_SECONDS}">
315                             </div>
316                             <div>
317                                 <label class="settings-checkbox-label" for="downloadInterSongDelayInput">Song Delay (s):</label>
318                                 <input type="number" id="downloadInterSongDelayInput" min="1" value="${DEFAULT_INTER_SONG_DELAY_SECONDS}">
319                             </div>
320                        </div>
321                    </div>
322                </div>
323
324                <div id="privacyStatusControls" class="section-controls">
325                    <button class="riffBackButton riffControlButton backToMenuButton">Back to Menu</button>
326                    <label class="selectAllContainer"><input type="checkbox" id="privacySelectAll"> Select All Visible</label>
327                    <div class="privacyButtonRow">
328                        <button id="privacySelectLiked" class="riffControlButton">Select/Deselect Liked</button>
329                        <button id="privacyClearSelection" title="Clear Selection" class="riffControlButton">C</button>
330                    </div>
331                    <div id="privacySongList" class="songListContainer" style="max-height: ${currentPrivacyListHeight};">Loading...</div>
332                    <div class="listResizer" data-list-id="privacySongList" data-height-var-name="currentPrivacyListHeight"></div>
333                    <div id="privacyCounter" class="counterDisplay">Updated: 0 / 0</div>
334                    <button id="changePrivacyStatusButton" class="riffControlButton">Change Status</button>
335                    <button id="reloadPrivacyButton" class="riffControlButton" style="display: ${autoReloadEnabled ? 'none' : 'block'};">Reload List</button>
336                    <div class="filterSettings privacySettingsContainer">
337                        <label class="settings-checkbox-label" for="privacyKeywordFilterInput">Filter list by keywords:</label>
338                        <input type="text" id="privacyKeywordFilterInput" placeholder="Keywords to show (comma-sep)...">
339
340                        <label class="settings-checkbox-label" for="privacyLevelSelect">Set selected songs to:</label>
341                        <select id="privacyLevelSelect">
342                            <option value="Only Me">Only Me</option>
343                            <option value="Link Only">Link Only</option>
344                            <option value="Publish">Publish</option>
345                        </select>
346
347                        <div> <!-- Removed privacyDelayContainer class for single input -->
348                             <label class="settings-checkbox-label" for="privacyInterSongDelayInput">Song Delay (s):</label>
349                             <input type="number" id="privacyInterSongDelayInput" min="1" value="${DEFAULT_PRIVACY_INTER_SONG_DELAY_SECONDS}">
350                        </div>
351                    </div>
352                </div>
353
354                <div id="commonSettingsFooter">
355                     <label id="autoReloadToggleContainer" class="settings-checkbox-label" style="display: none;"><input type="checkbox" id="autoReloadToggle"> Auto-Update Lists</label>
356                     <label id="debugToggleContainer" class="settings-checkbox-label" style="display: none;"><input type="checkbox" id="debugToggleCheckbox"> Enable Debug</label>
357                </div>
358                <div id="statusMessage">Ready.</div>
359            </div>`;
360        document.body.appendChild(uiElement);
361
362        minimizedIconElement = document.createElement('div');
363        minimizedIconElement.id = MINIMIZED_ICON_ID; // Use new unique ID
364        minimizedIconElement.textContent = 'RM';
365        minimizedIconElement.title = 'Restore Riffusion Multitool';
366        minimizedIconElement.style.display = isMinimized ? 'flex' : 'none';
367        document.body.appendChild(minimizedIconElement);
368
369        const header = uiElement.querySelector('#riffControlHeader');
370        enableDrag(uiElement, header);
371        uiElement.querySelector('#minimizeButton')?.addEventListener('click', minimizeUI); // Scoped query
372        minimizedIconElement?.addEventListener('click', restoreUI);
373
374        uiElement.querySelector('#goToSelectiveDelete')?.addEventListener('click', () => navigateToView('selective'));
375        uiElement.querySelector('#goToBulkDelete')?.addEventListener('click', () => navigateToView('bulk'));
376        uiElement.querySelector('#goToDownloadQueue')?.addEventListener('click', () => navigateToView('download'));
377        uiElement.querySelector('#goToPrivacyStatus')?.addEventListener('click', () => navigateToView('privacy'));
378        uiElement.querySelectorAll('.backToMenuButton').forEach(btn => btn.addEventListener('click', () => navigateToView('menu')));
379
380        // Selective Delete Listeners
381        uiElement.querySelector('#deleteSelectAll')?.addEventListener('change', (e) => toggleSelectAll(e, '#deleteSongList', selectedSongIdsDelete, 'delete'));
382        uiElement.querySelector('#deleteButton')?.addEventListener('click', deleteSelectedSongs);
383        uiElement.querySelector('#reloadDeleteButton')?.addEventListener('click', () => { if (currentView === 'selective') populateDeleteSongList(); });
384        const ignoreLikedToggle = uiElement.querySelector('#ignoreLikedToggleDelete');
385        if (ignoreLikedToggle) { ignoreLikedToggle.checked = ignoreLikedSongsDeleteState; ignoreLikedToggle.addEventListener('change', (e) => { ignoreLikedSongsDeleteState = e.target.checked; log(`Ignore Liked Songs (Delete): ${ignoreLikedSongsDeleteState}`); populateDeleteSongList(); });}
386        const deleteKeywordInput = uiElement.querySelector('#deleteKeywordFilterInput');
387        if (deleteKeywordInput) { deleteKeywordInput.addEventListener('input', debounce(() => { log('Delete keywords changed, refreshing list...'); populateDeleteSongList(); }, KEYWORD_FILTER_DEBOUNCE)); }
388        initListResizer(uiElement.querySelector('.listResizer[data-list-id="deleteSongList"]'), uiElement.querySelector('#deleteSongList'), 'currentDeleteListHeight');
389
390        // Bulk Delete Listeners
391        uiElement.querySelector('#deleteAllButton')?.addEventListener('click', () => {
392            if (currentView === 'bulk') {
393                if (isDeleting) { stopBulkDeletionByUser(); }
394                else { if (isDownloading || isChangingPrivacy) { log('Another operation in progress. Cannot start deletion.', 'warn'); return; }
395                    if (confirm('ARE YOU SURE? This will attempt to delete ALL songs in your library without scrolling. NO UNDO.')) { deleteAllSongsInLibrary(); }
396                }
397            }
398        });
399
400        // Download Queue Listeners
401        uiElement.querySelector('#downloadSelectAll')?.addEventListener('change', (e) => toggleSelectAll(e, '#downloadSongList', selectedSongIdsDownload, 'download'));
402        uiElement.querySelector('#downloadSelectLiked')?.addEventListener('click', () => toggleSelectLikedGeneric('#downloadSongList', selectedSongIdsDownload, '#downloadSelectAll', updateDownloadSelectLikedButtonText));
403        uiElement.querySelector('#downloadClearSelection')?.addEventListener('click', () => clearSelectionGeneric('#downloadSongList', selectedSongIdsDownload, '#downloadSelectAll', updateDownloadSelectLikedButtonText));
404        uiElement.querySelector('#startDownloadQueueButton')?.addEventListener('click', startDownloadQueue);
405        uiElement.querySelector('#reloadDownloadButton')?.addEventListener('click', () => { if (currentView === 'download') populateDownloadSongList(); });
406        const downloadKeywordInput = uiElement.querySelector('#downloadKeywordFilterInput');
407        if (downloadKeywordInput) { downloadKeywordInput.addEventListener('input', debounce(() => { log('Download filter changed, refreshing list...'); populateDownloadSongList(); }, KEYWORD_FILTER_DEBOUNCE)); }
408        const interSongDelayInput = uiElement.querySelector('#downloadInterSongDelayInput');
409        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 (Download) set to: ${downloadInterSongDelaySeconds}s`); } }); }
410        const intraFormatDelayInput = uiElement.querySelector('#downloadIntraFormatDelayInput');
411        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`); } }); }
412        initListResizer(uiElement.querySelector('.listResizer[data-list-id="downloadSongList"]'), uiElement.querySelector('#downloadSongList'), 'currentDownloadListHeight');
413
414        // Privacy Status Listeners
415        uiElement.querySelector('#privacySelectAll')?.addEventListener('change', (e) => toggleSelectAll(e, '#privacySongList', selectedSongIdsPrivacy, 'privacy'));
416        uiElement.querySelector('#privacySelectLiked')?.addEventListener('click', () => toggleSelectLikedGeneric('#privacySongList', selectedSongIdsPrivacy, '#privacySelectAll', updatePrivacySelectLikedButtonText));
417        uiElement.querySelector('#privacyClearSelection')?.addEventListener('click', () => clearSelectionGeneric('#privacySongList', selectedSongIdsPrivacy, '#privacySelectAll', updatePrivacySelectLikedButtonText));
418        uiElement.querySelector('#changePrivacyStatusButton')?.addEventListener('click', changeSelectedSongsPrivacy);
419        uiElement.querySelector('#reloadPrivacyButton')?.addEventListener('click', () => { if (currentView === 'privacy') populatePrivacySongList(); });
420        const privacyKeywordInput = uiElement.querySelector('#privacyKeywordFilterInput');
421        if (privacyKeywordInput) { privacyKeywordInput.addEventListener('input', debounce(() => { log('Privacy filter changed, refreshing list...'); populatePrivacySongList(); }, KEYWORD_FILTER_DEBOUNCE)); }
422        const privacyLevelSelect = uiElement.querySelector('#privacyLevelSelect');
423        if (privacyLevelSelect) { privacyLevelSelect.addEventListener('change', updateChangePrivacyButtonText); }
424        const privacyInterSongDelayInput = uiElement.querySelector('#privacyInterSongDelayInput');
425        if (privacyInterSongDelayInput) {
426            privacyInterSongDelayInput.value = privacyInterSongDelaySeconds;
427            privacyInterSongDelayInput.addEventListener('input', (e) => {
428                const val = parseInt(e.target.value, 10);
429                if (!isNaN(val) && val >= 0) {
430                    privacyInterSongDelaySeconds = val;
431                    log(`Inter-Song delay (Privacy) set to: ${privacyInterSongDelaySeconds}s`);
432                }
433            });
434        }
435        initListResizer(uiElement.querySelector('.listResizer[data-list-id="privacySongList"]'), uiElement.querySelector('#privacySongList'), 'currentPrivacyListHeight');
436        updateChangePrivacyButtonText();
437
438
439        // Common Settings Listeners
440        const autoReloadCheckbox = uiElement.querySelector('#autoReloadToggle');
441        if (autoReloadCheckbox) { autoReloadCheckbox.checked = autoReloadEnabled; autoReloadCheckbox.addEventListener('change', handleAutoReloadToggle); }
442        const debugCheckbox = uiElement.querySelector('#debugToggleCheckbox');
443        if (debugCheckbox) { debugCheckbox.checked = debugMode; debugCheckbox.addEventListener('change', handleDebugToggle); }
444
445        updateUIVisibility();
446        startAutoReloadPolling();
447    }
448
449    function minimizeUI() {
450        if (!uiElement || !minimizedIconElement) return;
451        if (!isMinimized) {
452            lastUiTop = uiElement.style.top || UI_INITIAL_TOP;
453            lastUiLeft = uiElement.style.left || lastUiLeft;
454        }
455        uiElement.style.display = 'none';
456        minimizedIconElement.style.display = 'flex';
457        isMinimized = true;
458        logDebug('UI Minimized');
459    }
460
461    function restoreUI() {
462        if (!uiElement || !minimizedIconElement) return;
463        minimizedIconElement.style.display = 'none';
464        uiElement.style.display = 'block';
465        uiElement.style.top = lastUiTop;
466        uiElement.style.left = lastUiLeft;
467        uiElement.style.right = 'auto';
468        isMinimized = false;
469        logDebug('UI Restored to:', { top: lastUiTop, left: lastUiLeft });
470        updateUIVisibility();
471    }
472
473
474    function navigateToView(view) {
475        if (isDeleting || isDownloading || isChangingPrivacy) {
476            if (!(currentView === 'bulk' && isDeleting)) {
477                log('Cannot switch views while an operation is in progress.', 'warn');
478                return;
479            }
480        }
481        logDebug(`Navigating to view: ${view}`);
482        const oldView = currentView;
483        currentView = view;
484        updateUIVisibility();
485
486        const noAutoReloadOrEmptyList = !autoReloadEnabled ||
487            (view === 'selective' && lastKnownSongIdsDelete.length === 0) ||
488            (view === 'download' && lastKnownSongIdsDownload.length === 0) ||
489            (view === 'privacy' && lastKnownSongIdsPrivacy.length === 0);
490
491        if (noAutoReloadOrEmptyList) {
492            if (view === 'selective') populateDeleteSongListIfNeeded();
493            else if (view === 'download') populateDownloadSongListIfNeeded();
494            else if (view === 'privacy') populatePrivacySongListIfNeeded();
495        }
496        if (autoReloadEnabled && oldView !== currentView) {
497            checkAndReloadLists(true); // Force check current view's list if auto-reload is on
498        }
499    }
500
501    function updateUIVisibility() {
502        if (!uiElement) return; // Guard against calls before uiElement is ready
503
504        if (isMinimized) {
505            uiElement.style.display = 'none';
506            if (minimizedIconElement) minimizedIconElement.style.display = 'flex';
507            return;
508        }
509        if (minimizedIconElement) minimizedIconElement.style.display = 'none';
510        uiElement.style.display = 'block';
511
512        const sections = {
513            menu: uiElement.querySelector('#mainMenuControls'),
514            selective: uiElement.querySelector('#selectiveModeControls'),
515            bulk: uiElement.querySelector('#bulkModeControls'),
516            download: uiElement.querySelector('#downloadQueueControls'),
517            privacy: uiElement.querySelector('#privacyStatusControls')
518        };
519        const headerTitle = uiElement.querySelector('#riffControlHeader h3');
520        const statusMsg = uiElement.querySelector('#statusMessage');
521        let title = `Riffusion Multitool v${GM_info.script.version}`;
522
523        Object.values(sections).forEach(section => {
524            if (section) section.style.display = 'none';
525        });
526
527        const autoReloadContainer = uiElement.querySelector('#autoReloadToggleContainer');
528        const debugContainer = uiElement.querySelector('#debugToggleContainer');
529
530        if (autoReloadContainer) autoReloadContainer.style.display = 'none';
531        if (debugContainer) debugContainer.style.display = 'none';
532
533        const opInProgress = isDeleting || isDownloading || isChangingPrivacy;
534
535        if (sections[currentView]) {
536            sections[currentView].style.display = 'block';
537            switch (currentView) {
538            case 'menu':
539                title += ' - Menu';
540                if (!opInProgress) updateStatusMessage('Select a tool.');
541                if (debugContainer) debugContainer.style.display = 'flex';
542                break;
543            case 'selective':
544                title += ' - Selective Deletion';
545                populateDeleteSongListIfNeeded();
546                if (!opInProgress) updateStatusMessage('Select songs to delete.');
547                if (autoReloadContainer) autoReloadContainer.style.display = 'flex';
548                break;
549            case 'bulk':
550                title += ' - Bulk Deletion';
551                const deleteAllBtn = uiElement.querySelector('#deleteAllButton');
552                if (deleteAllBtn) {
553                    deleteAllBtn.textContent = isDeleting ? 'Stop Deletion' : 'Delete Entire Library';
554                }
555                if (!opInProgress) updateStatusMessage('Warning: Deletes entire library.');
556                break;
557            case 'download':
558                title += ' - Download Queue';
559                populateDownloadSongListIfNeeded();
560                if (!opInProgress) updateStatusMessage('Select songs to download.');
561                if (autoReloadContainer) autoReloadContainer.style.display = 'flex';
562                break;
563            case 'privacy':
564                title += ' - Privacy Status';
565                populatePrivacySongListIfNeeded();
566                if (!opInProgress) updateStatusMessage('Select songs and privacy level.');
567                if (autoReloadContainer) autoReloadContainer.style.display = 'flex';
568                updateChangePrivacyButtonText(); // Ensure button text is correct on view switch
569                break;
570            }
571        } else {
572            log(`View '${currentView}' not found, showing menu.`, 'warn');
573            if (sections.menu) sections.menu.style.display = 'block'; // Fallback check
574            currentView = 'menu'; // Default to menu if view is invalid
575            title += ' - Menu';
576            if (!opInProgress) updateStatusMessage('Select a tool.');
577            if (debugContainer) debugContainer.style.display = 'flex';
578        }
579        if (headerTitle) headerTitle.textContent = title;
580        if (statusMsg) statusMsg.style.display = 'block'; // Always show status message area
581
582        // Toggle reload buttons based on autoReloadEnabled state
583        const reloadDelBtn = uiElement.querySelector('#reloadDeleteButton');
584        if (reloadDelBtn) reloadDelBtn.style.display = autoReloadEnabled ? 'none' : 'block';
585        const reloadDownBtn = uiElement.querySelector('#reloadDownloadButton');
586        if (reloadDownBtn) reloadDownBtn.style.display = autoReloadEnabled ? 'none' : 'block';
587        const reloadPrivBtn = uiElement.querySelector('#reloadPrivacyButton');
588        if (reloadPrivBtn) reloadPrivBtn.style.display = autoReloadEnabled ? 'none' : 'block';
589
590        logDebug(`UI Visibility Updated. Current View: ${currentView}`);
591    }
592
593
594    function handleDebugToggle(event) {
595        debugMode = event.target.checked;
596        log(`Debug mode ${debugMode ? 'enabled' : 'disabled'}.`);
597    }
598
599    function enableDrag(element, handle) {
600        let isDragging = false, offsetX, offsetY;
601        handle.addEventListener('mousedown', (e) => {
602            if (e.button !== 0 || e.target.closest('button')) return; // Ignore clicks on buttons within the header
603            if (isMinimized) return; // Don't drag if minimized
604            isDragging = true;
605            const rect = element.getBoundingClientRect();
606            offsetX = e.clientX - rect.left;
607            offsetY = e.clientY - rect.top;
608            element.style.cursor = 'grabbing';
609            handle.style.cursor = 'grabbing'; // Change handle cursor too
610            document.addEventListener('mousemove', onMouseMove);
611            document.addEventListener('mouseup', onMouseUp, { once: true });
612            e.preventDefault(); // Prevent text selection or other default actions
613        });
614        function onMouseMove(e) {
615            if (!isDragging) return;
616            let newX = e.clientX - offsetX;
617            let newY = e.clientY - offsetY;
618            const winWidth = window.innerWidth;
619            const winHeight = window.innerHeight;
620            const elWidth = element.offsetWidth;
621            const elHeight = element.offsetHeight;
622            // Constrain to viewport
623            if (newX < 0) newX = 0;
624            if (newY < 0) newY = 0;
625            if (newX + elWidth > winWidth) newX = winWidth - elWidth;
626            if (newY + elHeight > winHeight) newY = winHeight - elHeight;
627            element.style.left = `${newX}px`;
628            element.style.top = `${newY}px`;
629            element.style.right = 'auto'; // Important: override fixed right if it was set
630        }
631        function onMouseUp(e) {
632            if (e.button !== 0 || !isDragging) return; // Ensure it's the left mouse button releasing
633            isDragging = false;
634            element.style.cursor = 'default';
635            handle.style.cursor = 'move';
636            document.removeEventListener('mousemove', onMouseMove);
637            if (!isMinimized) { // Store position only if not minimized
638                lastUiTop = element.style.top;
639                lastUiLeft = element.style.left;
640                logDebug('Stored new position after drag:', { top: lastUiTop, left: lastUiLeft });
641            }
642        }
643    }
644
645    function initListResizer(resizerElem, listContentElem, heightVarName) {
646        if (!resizerElem || !listContentElem) return;
647        let startY, startHeight;
648
649        resizerElem.addEventListener('mousedown', function(e) {
650            if (e.button !== 0) return; // Only left click
651            e.preventDefault();
652            startY = e.clientY;
653            // Use current style.maxHeight if set, otherwise current offsetHeight
654            startHeight = parseInt(window.getComputedStyle(listContentElem).maxHeight, 10);
655            if (isNaN(startHeight) || startHeight === 0) { // Fallback if maxHeight is not set or 'none'
656                startHeight = listContentElem.offsetHeight;
657            }
658
659            document.addEventListener('mousemove', onMouseMove);
660            document.addEventListener('mouseup', onMouseUp, { once: true });
661        });
662
663        function onMouseMove(e) {
664            if (e.buttons === 0) { // If mouse button was released outside window
665                onMouseUp();
666                return;
667            }
668            const dy = e.clientY - startY;
669            let newHeight = startHeight + dy;
670            newHeight = Math.max(50, newHeight); // Min height
671            newHeight = Math.min(window.innerHeight * 0.8, newHeight); // Max height (80% of viewport)
672
673            listContentElem.style.maxHeight = newHeight + 'px';
674        }
675
676        function onMouseUp() {
677            document.removeEventListener('mousemove', onMouseMove);
678            const finalHeight = listContentElem.style.maxHeight;
679            // Update the corresponding global state variable for height
680            if (heightVarName === 'currentDeleteListHeight') currentDeleteListHeight = finalHeight;
681            else if (heightVarName === 'currentDownloadListHeight') currentDownloadListHeight = finalHeight;
682            else if (heightVarName === 'currentPrivacyListHeight') currentPrivacyListHeight = finalHeight;
683            logDebug(`List ${listContentElem.id} height set to ${finalHeight}`);
684        }
685    }
686
687
688    // --- Song List Population & Filtering ---
689    function getSongDataFromPage() {
690        let listContainer = document.querySelector('div[data-sentry-component="InfiniteScroll"] > div.grow');
691        if (!listContainer || listContainer.children.length === 0) {
692            const allRiffRows = document.querySelectorAll('div[data-sentry-component="DraggableRiffRow"]');
693            if (allRiffRows.length > 0 && allRiffRows[0].parentElement.childElementCount > 1) {
694                if (Array.from(allRiffRows[0].parentElement.children).every(child => child.getAttribute('data-sentry-component') === 'DraggableRiffRow' || child.tagName === 'HR')) {
695                    listContainer = allRiffRows[0].parentElement;
696                }
697            }
698        }
699        const songElements = listContainer
700            ? listContainer.querySelectorAll(':scope > div[data-sentry-component="DraggableRiffRow"]')
701            : document.querySelectorAll('div[data-sentry-component="DraggableRiffRow"]');
702
703        const songs = [];
704        songElements.forEach((songElement, index) => {
705            const titleLink = songElement.querySelector('a[href^="/song/"]');
706            let titleElement = titleLink ? titleLink.querySelector('h4.text-primary') : null;
707            if (!titleElement) titleElement = songElement.querySelector('[data-sentry-element="RiffTitle"]'); // Generic title element
708            if (!titleElement && titleLink) titleElement = titleLink.querySelector('div[class*="truncate"], h4'); // Fallback for different structures under link
709            const title = titleElement ? titleElement.textContent.trim() : `Untitled Song ${index + 1}`;
710
711            let songId = null;
712            if (titleLink) {
713                const match = titleLink.href.match(/\/song\/([a-f0-9-]+)/);
714                if (match && match[1]) songId = match[1];
715            }
716            if (!songId) songId = songElement.dataset.songId; // Fallback to data attribute on row
717            if (!songId) { // Try to extract from menu trigger if available and ID follows a pattern
718                const menuTrigger = songElement.querySelector('button[data-sentry-element="MenuTrigger"]');
719                if (menuTrigger && menuTrigger.id) {
720                    // Example id: radix-:r2gH:-trigger-songshare:song:bff4d67c-51e1-487e-9bca-a641d5c2df55
721                    const idParts = menuTrigger.id.split(':');
722                    if (idParts.length > 2 && idParts[idParts.length-2] === 'song') { // Check if 'song' is the second to last part
723                        songId = idParts[idParts.length-1];
724                    }
725                }
726            }
727            if (!songId) { logDebug(`Could not determine songId for element at index ${index}:`, songElement); return; } // Skip if no ID can be found
728
729            const unfavoriteButton = songElement.querySelector('button[aria-label^="Unfavorite"]');
730            const solidHeartIcon = songElement.querySelector('button svg[data-prefix="fas"][data-icon="heart"]'); // Solid heart for liked
731            let isLiked = !!unfavoriteButton || (!!solidHeartIcon && !songElement.querySelector('button[aria-label^="Favorite"]')); // Liked if unfav exists, or solid heart without fav button
732
733            songs.push({ id: songId, title: title, titleLower: title.toLowerCase(), isLiked: isLiked, element: songElement });
734        });
735        return songs;
736    }
737
738    function populateDeleteSongListIfNeeded() {
739        if (!uiElement) return;
740        const songListDiv = uiElement.querySelector('#deleteSongList');
741        if (!songListDiv) return;
742        if (isDeleting || isDownloading || isChangingPrivacy) { logDebug('Skipping delete list population during active operation.'); return; }
743        if (songListDiv.innerHTML === '' || songListDiv.innerHTML === 'Loading...' || songListDiv.children.length === 0 || (songListDiv.children.length === 1 && songListDiv.firstElementChild.tagName === 'P')) {
744            populateDeleteSongList();
745        }
746    }
747    function populateDeleteSongList() {
748        if (!uiElement || currentView !== 'selective' || isMinimized) return;
749        if (isDeleting || isDownloading || isChangingPrivacy) { logDebug('Skipping delete list population during active operation.'); return; }
750        logDebug('Populating DELETE song list...');
751        const songListDiv = uiElement.querySelector('#deleteSongList');
752        const deleteCounter = uiElement.querySelector('#deleteCounter');
753        if (!songListDiv || !deleteCounter) return;
754
755        songListDiv.innerHTML = 'Loading...';
756        deleteCounter.textContent = 'Deleted: 0 / 0';
757
758        const selectAllCheckbox = uiElement.querySelector('#deleteSelectAll');
759        if (selectAllCheckbox) selectAllCheckbox.checked = false;
760        const ignoreLikedCheckbox = uiElement.querySelector('#ignoreLikedToggleDelete');
761        if(ignoreLikedCheckbox) ignoreLikedCheckbox.checked = ignoreLikedSongsDeleteState;
762
763        const keywordInput = uiElement.querySelector('#deleteKeywordFilterInput');
764        const keywordString = keywordInput ? keywordInput.value : '';
765        const dynamicIgnoreKeywords = keywordString.split(',').map(k => k.trim().toLowerCase()).filter(k => k !== '');
766
767        setTimeout(() => { // Delay to allow DOM to update if called rapidly
768            const songsFromPage = getSongDataFromPage();
769            songListDiv.innerHTML = ''; // Clear loading message
770            songListDiv.style.maxHeight = currentDeleteListHeight; // Apply current height
771
772            if (songsFromPage.length === 0) {
773                songListDiv.innerHTML = '<p style="color:#d0d0d0;text-align:center;font-size:12px;">No songs found on page.</p>';
774                if(!(isDeleting || isDownloading || isChangingPrivacy)) updateStatusMessage('No songs found.');
775                lastKnownSongIdsDelete = [];
776                return;
777            }
778
779            let ignoredCount = 0;
780            let visibleCount = 0;
781
782            songsFromPage.forEach(song => {
783                const keywordMatch = dynamicIgnoreKeywords.length > 0 && dynamicIgnoreKeywords.some(keyword => song.titleLower.includes(keyword));
784                const likedMatch = ignoreLikedSongsDeleteState && song.isLiked;
785                const shouldIgnore = keywordMatch || likedMatch;
786
787                let ignoreReason = '';
788                if (keywordMatch) ignoreReason += 'Keyword';
789                if (likedMatch) ignoreReason += (keywordMatch ? ' & Liked' : 'Liked');
790
791                const label = document.createElement('label');
792                const checkbox = document.createElement('input');
793                checkbox.type = 'checkbox';
794                checkbox.dataset.songId = song.id;
795                checkbox.disabled = shouldIgnore;
796                checkbox.checked = selectedSongIdsDelete.has(song.id) && !shouldIgnore;
797
798                checkbox.addEventListener('change', (event) => {
799                    if (event.target.checked) selectedSongIdsDelete.add(song.id);
800                    else selectedSongIdsDelete.delete(song.id);
801                    updateSelectAllCheckboxState('#deleteSelectAll', '#deleteSongList');
802                });
803
804                label.appendChild(checkbox);
805                label.appendChild(document.createTextNode(` ${song.title}`));
806                if (song.isLiked) label.classList.add('liked');
807
808                if (shouldIgnore) {
809                    label.classList.add('ignored');
810                    label.title = `Ignoring for delete: ${ignoreReason}`;
811                    ignoredCount++;
812                } else {
813                    visibleCount++;
814                }
815                songListDiv.appendChild(label);
816            });
817            updateSelectAllCheckboxState('#deleteSelectAll', '#deleteSongList');
818
819            logDebug(`Populated DELETE list: ${songsFromPage.length} total, ${visibleCount} selectable, ${ignoredCount} ignored. ${selectedSongIdsDelete.size} selected.`);
820            if(!(isDeleting || isDownloading || isChangingPrivacy)) updateStatusMessage(`Loaded ${songsFromPage.length} songs (${ignoredCount} ignored).`);
821            lastKnownSongIdsDelete = songsFromPage.map(s => s.id);
822        }, 100);
823    }
824
825    function populateDownloadSongListIfNeeded() {
826        if (!uiElement) return;
827        const songListDiv = uiElement.querySelector('#downloadSongList');
828        if (!songListDiv) return;
829        if (isDeleting || isDownloading || isChangingPrivacy) { logDebug('Skipping download list population during active operation.'); return; }
830        if (songListDiv.innerHTML === '' || songListDiv.innerHTML === 'Loading...' || songListDiv.children.length === 0 || (songListDiv.children.length === 1 && songListDiv.firstElementChild.tagName === 'P')) {
831            populateDownloadSongList();
832        }
833    }
834    function populateDownloadSongList() {
835        if (!uiElement || currentView !== 'download' || isMinimized) return;
836        if (isDeleting || isDownloading || isChangingPrivacy) { logDebug('Skipping download list population during active operation.'); return; }
837        logDebug('Populating DOWNLOAD song list...');
838        const songListDiv = uiElement.querySelector('#downloadSongList');
839        const downloadCounter = uiElement.querySelector('#downloadCounter');
840        if (!songListDiv || !downloadCounter) return;
841
842        songListDiv.innerHTML = 'Loading...';
843        downloadCounter.textContent = 'Downloaded: 0 / 0';
844
845        const selectAllCheckbox = uiElement.querySelector('#downloadSelectAll');
846        if (selectAllCheckbox) selectAllCheckbox.checked = false;
847
848        const keywordInput = uiElement.querySelector('#downloadKeywordFilterInput');
849        const keywordString = keywordInput ? keywordInput.value : '';
850        const filterKeywords = keywordString.split(',').map(k => k.trim().toLowerCase()).filter(k => k !== '');
851
852        setTimeout(() => {
853            const songsFromPage = getSongDataFromPage();
854            songListDiv.innerHTML = '';
855            songListDiv.style.maxHeight = currentDownloadListHeight;
856
857            if (songsFromPage.length === 0) {
858                songListDiv.innerHTML = '<p style="color:#d0d0d0;text-align:center;font-size:12px;">No songs found on page.</p>';
859                if(!(isDeleting || isDownloading || isChangingPrivacy)) updateStatusMessage('No songs found.');
860                updateDownloadSelectLikedButtonText();
861                lastKnownSongIdsDownload = [];
862                return;
863            }
864
865            let displayedCount = 0;
866
867            songsFromPage.forEach(song => {
868                const keywordMatch = filterKeywords.length === 0 || filterKeywords.some(keyword => song.titleLower.includes(keyword));
869
870                if (keywordMatch) {
871                    const label = document.createElement('label');
872                    const checkbox = document.createElement('input');
873                    checkbox.type = 'checkbox';
874                    checkbox.dataset.songId = song.id;
875                    checkbox.dataset.isLiked = song.isLiked.toString(); // Store liked status
876                    checkbox.checked = selectedSongIdsDownload.has(song.id);
877
878                    checkbox.addEventListener('change', (event) => {
879                        if (event.target.checked) selectedSongIdsDownload.add(song.id);
880                        else selectedSongIdsDownload.delete(song.id);
881                        updateSelectAllCheckboxState('#downloadSelectAll', '#downloadSongList');
882                        updateDownloadSelectLikedButtonText();
883                    });
884
885                    label.appendChild(checkbox);
886                    label.appendChild(document.createTextNode(` ${song.title}`));
887                    if (song.isLiked) label.classList.add('liked');
888
889                    songListDiv.appendChild(label);
890                    displayedCount++;
891                }
892            });
893            updateSelectAllCheckboxState('#downloadSelectAll', '#downloadSongList');
894
895            logDebug(`Populated DOWNLOAD list: ${songsFromPage.length} total, ${displayedCount} displayed. ${selectedSongIdsDownload.size} selected.`);
896            if(!(isDeleting || isDownloading || isChangingPrivacy)) updateStatusMessage(`Showing ${displayedCount} of ${songsFromPage.length} songs.`);
897            updateDownloadSelectLikedButtonText();
898            lastKnownSongIdsDownload = songsFromPage.map(s => s.id);
899        }, 100);
900    }
901
902    function populatePrivacySongListIfNeeded() {
903        if (!uiElement) return;
904        const songListDiv = uiElement.querySelector('#privacySongList');
905        if (!songListDiv) return;
906        if (isDeleting || isDownloading || isChangingPrivacy) { logDebug('Skipping privacy list population during active operation.'); return; }
907        if (songListDiv.innerHTML === '' || songListDiv.innerHTML === 'Loading...' || songListDiv.children.length === 0 || (songListDiv.children.length === 1 && songListDiv.firstElementChild.tagName === 'P')) {
908            populatePrivacySongList();
909        }
910    }
911
912    function populatePrivacySongList() {
913        if (!uiElement || currentView !== 'privacy' || isMinimized) return;
914        if (isDeleting || isDownloading || isChangingPrivacy) { logDebug('Skipping privacy list population during active operation.'); return; }
915        logDebug('Populating PRIVACY song list...');
916        const songListDiv = uiElement.querySelector('#privacySongList');
917        const privacyCounter = uiElement.querySelector('#privacyCounter');
918        if (!songListDiv || !privacyCounter) return;
919
920        songListDiv.innerHTML = 'Loading...';
921        privacyCounter.textContent = 'Updated: 0 / 0';
922
923        const selectAllCheckbox = uiElement.querySelector('#privacySelectAll');
924        if (selectAllCheckbox) selectAllCheckbox.checked = false;
925
926        const keywordInput = uiElement.querySelector('#privacyKeywordFilterInput');
927        const keywordString = keywordInput ? keywordInput.value : '';
928        const filterKeywords = keywordString.split(',').map(k => k.trim().toLowerCase()).filter(k => k !== '');
929
930        setTimeout(() => {
931            const songsFromPage = getSongDataFromPage();
932            songListDiv.innerHTML = '';
933            songListDiv.style.maxHeight = currentPrivacyListHeight;
934
935            if (songsFromPage.length === 0) {
936                songListDiv.innerHTML = '<p style="color:#d0d0d0;text-align:center;font-size:12px;">No songs found on page.</p>';
937                if(!(isDeleting || isDownloading || isChangingPrivacy)) updateStatusMessage('No songs found.');
938                updatePrivacySelectLikedButtonText();
939                lastKnownSongIdsPrivacy = [];
940                return;
941            }
942
943            let displayedCount = 0;
944            songsFromPage.forEach(song => {
945                const keywordMatch = filterKeywords.length === 0 || filterKeywords.some(keyword => song.titleLower.includes(keyword));
946
947                if (keywordMatch) {
948                    const label = document.createElement('label');
949                    const checkbox = document.createElement('input');
950                    checkbox.type = 'checkbox';
951                    checkbox.dataset.songId = song.id;
952                    checkbox.dataset.isLiked = song.isLiked.toString();
953                    checkbox.checked = selectedSongIdsPrivacy.has(song.id);
954
955                    checkbox.addEventListener('change', (event) => {
956                        if (event.target.checked) selectedSongIdsPrivacy.add(song.id);
957                        else selectedSongIdsPrivacy.delete(song.id);
958                        updateSelectAllCheckboxState('#privacySelectAll', '#privacySongList');
959                        updatePrivacySelectLikedButtonText();
960                    });
961
962                    label.appendChild(checkbox);
963                    label.appendChild(document.createTextNode(` ${song.title}`));
964                    if (song.isLiked) label.classList.add('liked');
965
966                    songListDiv.appendChild(label);
967                    displayedCount++;
968                }
969            });
970            updateSelectAllCheckboxState('#privacySelectAll', '#privacySongList');
971
972            logDebug(`Populated PRIVACY list: ${songsFromPage.length} total, ${displayedCount} displayed. ${selectedSongIdsPrivacy.size} selected.`);
973            if(!(isDeleting || isDownloading || isChangingPrivacy)) updateStatusMessage(`Showing ${displayedCount} of ${songsFromPage.length} songs.`);
974            updatePrivacySelectLikedButtonText();
975            lastKnownSongIdsPrivacy = songsFromPage.map(s => s.id);
976        }, 100);
977    }
978
979
980    function updateSelectAllCheckboxState(selectAllSelector, listSelector) {
981        if (!uiElement) return;
982        const selectAllCb = uiElement.querySelector(selectAllSelector);
983        if (!selectAllCb) return;
984        const visibleCheckboxes = uiElement.querySelectorAll(`${listSelector} input[type="checkbox"]:not(:disabled)`);
985        const checkedVisibleCheckboxes = uiElement.querySelectorAll(`${listSelector} input[type="checkbox"]:not(:disabled):checked`);
986        selectAllCb.checked = visibleCheckboxes.length > 0 && visibleCheckboxes.length === checkedVisibleCheckboxes.length;
987    }
988
989    function toggleSelectAll(event, listSelector, selectionSet, type) {
990        if (!uiElement || isMinimized || isDeleting || isDownloading || isChangingPrivacy) return;
991        const isChecked = event.target.checked;
992        const checkboxes = uiElement.querySelectorAll(`${listSelector} input[type="checkbox"]:not(:disabled)`);
993        checkboxes.forEach(cb => {
994            cb.checked = isChecked;
995            const songId = cb.dataset.songId;
996            if (isChecked) selectionSet.add(songId);
997            else selectionSet.delete(songId);
998        });
999        logDebug(`Select All Toggled in ${listSelector}: ${isChecked} (${checkboxes.length} items). Selection Set: ${selectionSet.size}`);
1000        if (type === 'download') updateDownloadSelectLikedButtonText();
1001        else if (type === 'privacy') updatePrivacySelectLikedButtonText();
1002    }
1003
1004    function toggleSelectLikedGeneric(listSelector, selectionSet, selectAllSelector, updateButtonTextFn) {
1005        if (!uiElement || isMinimized || isDeleting || isDownloading || isChangingPrivacy) return;
1006        const checkboxes = uiElement.querySelectorAll(`${listSelector} input[type="checkbox"]:not(:disabled)`);
1007        if (checkboxes.length === 0) { logWarn('No songs available in list for liked toggle.'); return; }
1008
1009        let shouldSelect = false; // Determine if we should select or deselect liked songs
1010        for (const cb of checkboxes) {
1011            if (cb.dataset.isLiked === 'true' && !cb.checked) { shouldSelect = true; break; }
1012        }
1013
1014        let changedCount = 0;
1015        checkboxes.forEach(cb => {
1016            if (cb.dataset.isLiked === 'true') {
1017                if (cb.checked !== shouldSelect) { // Only change if current state is different from target state
1018                    cb.checked = shouldSelect;
1019                    const songId = cb.dataset.songId;
1020                    if (shouldSelect) selectionSet.add(songId); else selectionSet.delete(songId);
1021                    changedCount++;
1022                }
1023            }
1024        });
1025        log(`Toggled selection for ${changedCount} liked songs. Action: ${shouldSelect ? 'Select' : 'Deselect'}`);
1026        updateStatusMessage(`${shouldSelect ? 'Selected' : 'Deselected'} ${changedCount} liked songs.`);
1027        updateSelectAllCheckboxState(selectAllSelector, listSelector);
1028        if (updateButtonTextFn) updateButtonTextFn();
1029    }
1030
1031    function clearSelectionGeneric(listSelector, selectionSet, selectAllSelector, updateButtonTextFn) {
1032        if (!uiElement || isMinimized || isDeleting || isDownloading || isChangingPrivacy) return;
1033        const checkboxes = uiElement.querySelectorAll(`${listSelector} input[type="checkbox"]:checked`);
1034        if (checkboxes.length === 0) { log('No songs selected to clear.', 'info'); return; }
1035
1036        checkboxes.forEach(cb => cb.checked = false);
1037        selectionSet.clear();
1038
1039        const selectAllCheckbox = uiElement.querySelector(selectAllSelector);
1040        if (selectAllCheckbox) selectAllCheckbox.checked = false;
1041        log(`Cleared selection for ${checkboxes.length} songs in ${listSelector}.`);
1042        updateStatusMessage('Selection cleared.');
1043        if (updateButtonTextFn) updateButtonTextFn();
1044    }
1045
1046
1047    function updateDownloadSelectLikedButtonText() {
1048        if (!uiElement || currentView !== 'download' || isMinimized) return;
1049        const button = uiElement.querySelector('#downloadSelectLiked');
1050        if (!button) return;
1051
1052        const checkboxes = uiElement.querySelectorAll('#downloadSongList input[type="checkbox"]:not(:disabled)');
1053        if (checkboxes.length === 0) {
1054            button.textContent = 'Select Liked';
1055            button.disabled = true; return;
1056        }
1057        button.disabled = false;
1058        let shouldOfferSelect = false;
1059        checkboxes.forEach(cb => {
1060            if (cb.dataset.isLiked === 'true' && !cb.checked) {
1061                shouldOfferSelect = true;
1062            }
1063        });
1064        button.textContent = shouldOfferSelect ? 'Select Liked' : 'Deselect Liked';
1065    }
1066
1067    function updatePrivacySelectLikedButtonText() {
1068        if (!uiElement || currentView !== 'privacy' || isMinimized) return;
1069        const button = uiElement.querySelector('#privacySelectLiked');
1070        if (!button) return;
1071
1072        const checkboxes = uiElement.querySelectorAll('#privacySongList input[type="checkbox"]:not(:disabled)');
1073        if (checkboxes.length === 0) {
1074            button.textContent = 'Select Liked';
1075            button.disabled = true; return;
1076        }
1077        button.disabled = false;
1078        let shouldOfferSelect = false;
1079        checkboxes.forEach(cb => {
1080            if (cb.dataset.isLiked === 'true' && !cb.checked) {
1081                shouldOfferSelect = true;
1082            }
1083        });
1084        button.textContent = shouldOfferSelect ? 'Select Liked' : 'Deselect Liked';
1085    }
1086
1087    function updateChangePrivacyButtonText() {
1088        if (!uiElement || currentView !== 'privacy' || isMinimized) return;
1089        const button = uiElement.querySelector('#changePrivacyStatusButton');
1090        const select = uiElement.querySelector('#privacyLevelSelect');
1091        if (button && select) {
1092            const selectedOption = select.value;
1093            button.textContent = privacyButtonTextMap[selectedOption] || 'Change Status';
1094        }
1095    }
1096
1097    function updateCounter(type, count, total) {
1098        if (!uiElement || isMinimized) return;
1099        let counterElementId = '';
1100        if (type === 'delete') counterElementId = 'deleteCounter';
1101        else if (type === 'download') counterElementId = 'downloadCounter';
1102        else if (type === 'privacy') counterElementId = 'privacyCounter';
1103        else return;
1104
1105        const counterElement = uiElement.querySelector(`#${counterElementId}`); // Scoped query
1106        if (counterElement) {
1107            const prefix = type === 'delete' ? 'Deleted' : (type === 'download' ? 'Downloaded' : 'Updated');
1108            counterElement.textContent = `${prefix}: ${count} / ${total}`;
1109        }
1110    }
1111
1112    // --- Auto Reload Logic ---
1113    function handleAutoReloadToggle(event) {
1114        autoReloadEnabled = event.target.checked;
1115        log(`Automatic list reload ${autoReloadEnabled ? 'enabled' : 'disabled'}.`);
1116        if (!uiElement) return;
1117        const reloadDelBtn = uiElement.querySelector('#reloadDeleteButton');
1118        if (reloadDelBtn) reloadDelBtn.style.display = autoReloadEnabled ? 'none' : 'block';
1119        const reloadDownBtn = uiElement.querySelector('#reloadDownloadButton');
1120        if (reloadDownBtn) reloadDownBtn.style.display = autoReloadEnabled ? 'none' : 'block';
1121        const reloadPrivBtn = uiElement.querySelector('#reloadPrivacyButton');
1122        if (reloadPrivBtn) reloadPrivBtn.style.display = autoReloadEnabled ? 'none' : 'block';
1123
1124
1125        if (autoReloadEnabled) {
1126            startAutoReloadPolling();
1127            checkAndReloadLists(true); // Force a check on toggle to immediately reflect state
1128        } else {
1129            stopAutoReloadPolling();
1130        }
1131    }
1132
1133    function startAutoReloadPolling() {
1134        if (autoReloadTimer) clearInterval(autoReloadTimer);
1135        if (autoReloadEnabled) {
1136            autoReloadTimer = setInterval(() => checkAndReloadLists(false), AUTO_RELOAD_INTERVAL);
1137            logDebug('Auto-reload polling started.');
1138        }
1139    }
1140
1141    function stopAutoReloadPolling() {
1142        if (autoReloadTimer) {
1143            clearInterval(autoReloadTimer);
1144            autoReloadTimer = null;
1145            logDebug('Auto-reload polling stopped.');
1146        }
1147    }
1148
1149
1150    function checkAndReloadLists(forceCheckCurrentView = false) {
1151        if ((!autoReloadEnabled && !forceCheckCurrentView) || !uiElement || isMinimized || isDeleting || isDownloading || isChangingPrivacy || uiElement.style.display === 'none') {
1152            return;
1153        }
1154
1155        const songsOnPage = getSongDataFromPage();
1156        const currentPageSongIds = songsOnPage.map(s => s.id);
1157        const currentPageSongIdsString = [...currentPageSongIds].sort().join(','); // Create a comparable string
1158
1159        let listNeedsRefresh = false;
1160        let currentListIsEmpty = false;
1161
1162        if (currentView === 'selective' || (forceCheckCurrentView && currentView === 'selective')) {
1163            const knownIdsString = [...lastKnownSongIdsDelete].sort().join(',');
1164            listNeedsRefresh = currentPageSongIdsString !== knownIdsString;
1165            currentListIsEmpty = uiElement.querySelector('#deleteSongList')?.children.length === 0;
1166            if (listNeedsRefresh || (forceCheckCurrentView && currentListIsEmpty && songsOnPage.length > 0)) {
1167                logDebug('Page song list changed for Selective Deletion. Reloading UI list.');
1168                populateDeleteSongList();
1169            }
1170        } else if (currentView === 'download' || (forceCheckCurrentView && currentView === 'download')) {
1171            const knownIdsString = [...lastKnownSongIdsDownload].sort().join(',');
1172            listNeedsRefresh = currentPageSongIdsString !== knownIdsString;
1173            currentListIsEmpty = uiElement.querySelector('#downloadSongList')?.children.length === 0;
1174            if (listNeedsRefresh || (forceCheckCurrentView && currentListIsEmpty && songsOnPage.length > 0)) {
1175                logDebug('Page song list changed for Download Queue. Reloading UI list.');
1176                populateDownloadSongList();
1177            }
1178        } else if (currentView === 'privacy' || (forceCheckCurrentView && currentView === 'privacy')) {
1179            const knownIdsString = [...lastKnownSongIdsPrivacy].sort().join(',');
1180            listNeedsRefresh = currentPageSongIdsString !== knownIdsString;
1181            currentListIsEmpty = uiElement.querySelector('#privacySongList')?.children.length === 0;
1182            if (listNeedsRefresh || (forceCheckCurrentView && currentListIsEmpty && songsOnPage.length > 0)) {
1183                logDebug('Page song list changed for Privacy Status. Reloading UI list.');
1184                populatePrivacySongList();
1185            }
1186        }
1187    }
1188
1189
1190    // --- Deletion Logic ---
1191    function getCurrentSongElements() { // Helper to get current song row elements on the page
1192        let listContainer = document.querySelector('div[data-sentry-component="InfiniteScroll"] > div.grow');
1193        if (!listContainer || listContainer.children.length === 0) {
1194            const allRiffRows = document.querySelectorAll('div[data-sentry-component="DraggableRiffRow"]');
1195            if (allRiffRows.length > 0 && allRiffRows[0].parentElement.childElementCount > 1) {
1196                if (Array.from(allRiffRows[0].parentElement.children).every(child => child.getAttribute('data-sentry-component') === 'DraggableRiffRow' || child.tagName === 'HR')) {
1197                    listContainer = allRiffRows[0].parentElement;
1198                }
1199            }
1200        }
1201        if(listContainer) {
1202            return listContainer.querySelectorAll(':scope > div[data-sentry-component="DraggableRiffRow"]');
1203        }
1204        return document.querySelectorAll('div[data-sentry-component="DraggableRiffRow"]');
1205    }
1206
1207
1208    async function deleteSelectedSongs() {
1209        if (isMinimized) { log('Restore UI to delete.', 'warn'); return; }
1210        if (currentView !== 'selective') { log('Selective delete only in Selective View.', 'warn'); return; }
1211        if (isDeleting || isDownloading || isChangingPrivacy) { log('Operation in progress.', 'warn'); return; }
1212
1213        const songIdsToDeleteArray = Array.from(selectedSongIdsDelete);
1214        const totalToDelete = songIdsToDeleteArray.length;
1215
1216        if (totalToDelete === 0) {
1217            updateCounter('delete', 0, 0); log('No songs selected for deletion.');
1218            updateStatusMessage('No songs selected or all selected are ignored.'); return;
1219        }
1220
1221        isDeleting = true; setAllButtonsDisabled(true);
1222        log(`Starting deletion for ${totalToDelete} selected: [${songIdsToDeleteArray.join(', ')}]`);
1223        updateCounter('delete', 0, totalToDelete); updateStatusMessage(`Deleting ${totalToDelete} selected...`);
1224
1225        let deletedCount = 0, criticalErrorOccurred = false;
1226        for (const songId of songIdsToDeleteArray) {
1227            if (criticalErrorOccurred || !isDeleting) { break; }
1228            const songElement = Array.from(getCurrentSongElements()).find(el => {
1229                const link = el.querySelector(`a[href="/song/${songId}"]`);
1230                return !!link;
1231            });
1232            if (!songElement) {
1233                log(`Song ID ${songId} not found (deleted?). Removing from selection.`, 'warn');
1234                selectedSongIdsDelete.delete(songId); continue;
1235            }
1236            const title = getSongDataFromPage().find(s => s.id === songId)?.title || `song ID ${songId}`;
1237            const success = await processSingleAction(songElement, 'delete', songId);
1238            if (success) {
1239                deletedCount++; selectedSongIdsDelete.delete(songId);
1240                updateCounter('delete', deletedCount, totalToDelete); updateStatusMessage(`Deleted ${deletedCount}/${totalToDelete}...`);
1241            } else {
1242                log(`Failed to delete ${title}. Stopping.`, 'error');
1243                updateStatusMessage(`Error deleting ${title}. Stopped.`); criticalErrorOccurred = true;
1244            }
1245            await delay(50);
1246        }
1247        log(`Selective deletion: ${deletedCount}/${totalToDelete} attempted.`);
1248        updateStatusMessage(criticalErrorOccurred ? `Deletion stopped. ${deletedCount} deleted.` : `Selected deletion complete. ${deletedCount} deleted.`);
1249        isDeleting = false; setAllButtonsDisabled(false); populateDeleteSongList();
1250    }
1251
1252    function stopBulkDeletionByUser() {
1253        if (!isDeleting || currentView !== 'bulk') { return; }
1254        log('Bulk deletion stop by user.'); updateStatusMessage('Stopping bulk deletion...');
1255        stopBulkDeletionSignal = true;
1256    }
1257
1258    async function deleteAllSongsInLibrary() {
1259        if (isMinimized) { log('Restore UI to delete.', 'warn'); return; }
1260        if (currentView !== 'bulk') { log('Bulk delete only in Bulk View.', 'warn'); return; }
1261        if (isDownloading || isChangingPrivacy) { log('Another operation is in progress.', 'warn'); return; }
1262
1263        stopBulkDeletionSignal = false; isDeleting = true; setAllButtonsDisabled(true);
1264        const deleteAllBtn = uiElement.querySelector('#deleteAllButton'); // Scoped query
1265        if (deleteAllBtn) deleteAllBtn.textContent = 'Stop Deletion';
1266        log('--- STARTING BULK LIBRARY DELETION ---'); updateStatusMessage('Starting full library deletion...');
1267        let totalDeleted = 0, emptyChecks = 0;
1268        while (isDeleting && !stopBulkDeletionSignal) {
1269            await delay(500);
1270            if (stopBulkDeletionSignal) { isDeleting = false; break; }
1271
1272            const currentElements = getCurrentSongElements();
1273            let currentSize = currentElements.length;
1274            if (currentSize === 0) {
1275                emptyChecks++;
1276                if (emptyChecks >= MAX_EMPTY_CHECKS) { log('No songs after retries. Assuming empty.'); isDeleting = false; break; }
1277                updateStatusMessage(`No songs. Re-checking (${emptyChecks}/${MAX_EMPTY_CHECKS})...`); await delay(EMPTY_RETRY_DELAY); continue;
1278            }
1279            emptyChecks = 0; let batchDeleted = 0;
1280
1281            for (const firstElement of currentElements) { // Iterate over a static list for this batch
1282                if (!isDeleting || stopBulkDeletionSignal) { isDeleting = false; break; }
1283
1284                if (!firstElement || !firstElement.parentNode) { continue; }
1285                const title = (firstElement.querySelector('a[href^="/song/"] h4, [data-sentry-element="RiffTitle"], div[class*="truncate"]') || {textContent: 'Top song'}).textContent.trim();
1286                updateStatusMessage(`Deleting ${title} (${totalDeleted + batchDeleted + 1} total...)`);
1287                const success = await processSingleAction(firstElement, 'delete', `Bulk ${totalDeleted + batchDeleted + 1}`);
1288                if (success) {
1289                    batchDeleted++;
1290                    await delay(DELETION_DELAY / 2);
1291                } else {
1292                    log(`Failed to delete ${title}. Stopping.`, 'error'); updateStatusMessage(`Error deleting ${title}.`); isDeleting = false; break;
1293                }
1294                await delay(50);
1295            }
1296            totalDeleted += batchDeleted;
1297            if (isDeleting && !stopBulkDeletionSignal) updateStatusMessage(`Batch done. Total: ${totalDeleted}. Checking more...`);
1298        }
1299        if (deleteAllBtn) deleteAllBtn.textContent = 'Delete Entire Library';
1300        let finalMsg = stopBulkDeletionSignal ? `Stopped by user. Total: ${totalDeleted}.`
1301            : (emptyChecks >= MAX_EMPTY_CHECKS ? `Complete. No more songs. Total: ${totalDeleted}.`
1302                : `Finished. Total: ${totalDeleted}.`);
1303        log(`--- BULK DELETION FINISHED --- ${finalMsg}`); updateStatusMessage(finalMsg);
1304        isDeleting = false; stopBulkDeletionSignal = false; setAllButtonsDisabled(false);
1305    }
1306
1307    // --- Download Logic ---
1308    async function startDownloadQueue() {
1309        if (!uiElement || isMinimized) { log('Restore UI to download.', 'warn'); return; }
1310        if (currentView !== 'download') { log('Download only in Download View.', 'warn'); return; }
1311        if (isDeleting || isDownloading || isChangingPrivacy) { log('Operation in progress.', 'warn'); return; }
1312
1313        const songIdsToDownloadArray = Array.from(selectedSongIdsDownload);
1314        const totalSongsToDownload = songIdsToDownloadArray.length;
1315        if (totalSongsToDownload === 0) { updateCounter('download', 0, 0); log('No songs selected.'); updateStatusMessage('No songs selected.'); return; }
1316        const selectedFormats = [];
1317        if (uiElement.querySelector('#formatMP3')?.checked) selectedFormats.push('MP3'); // Scoped query
1318        if (uiElement.querySelector('#formatM4A')?.checked) selectedFormats.push('M4A'); // Scoped query
1319        if (uiElement.querySelector('#formatWAV')?.checked) selectedFormats.push('WAV'); // Scoped query
1320        if (selectedFormats.length === 0) { log('No formats selected.', 'error'); updateStatusMessage('Select download format(s).'); return; }
1321
1322        isDownloading = true; setAllButtonsDisabled(true);
1323        const interSongDelayMs = downloadInterSongDelaySeconds * 1000, intraFormatDelayMs = downloadIntraFormatDelaySeconds * 1000;
1324        log(`Starting download: ${totalSongsToDownload} songs. Formats: [${selectedFormats.join(', ')}]`);
1325        updateCounter('download', 0, totalSongsToDownload); updateStatusMessage(`Downloading ${totalSongsToDownload} (${selectedFormats.join('/')})...`);
1326        let songsProcessedCount = 0, criticalErrorOccurred = false;
1327
1328        for (const songId of songIdsToDownloadArray) {
1329            if (criticalErrorOccurred || !isDownloading) { break; }
1330            const songElement = Array.from(getCurrentSongElements()).find(el => {
1331                const link = el.querySelector(`a[href="/song/${songId}"]`);
1332                return !!link;
1333            });
1334            if (!songElement) { log(`Song ID ${songId} not found. Skipping.`, 'warn'); selectedSongIdsDownload.delete(songId); continue; }
1335            const title = getSongDataFromPage().find(s => s.id === songId)?.title || `song ID ${songId}`;
1336            let songDownloadSuccessThisSong = false, formatIndex = 0;
1337            for (const format of selectedFormats) {
1338                if (!isDownloading) { criticalErrorOccurred = true; break; }
1339                const currentSongElCheck = Array.from(getCurrentSongElements()).find(el => {
1340                    const link = el.querySelector(`a[href="/song/${songId}"]`);
1341                    return !!link;
1342                });
1343                if (!currentSongElCheck) { logWarn(`${title} disappeared. Skipping formats.`); criticalErrorOccurred = true; break; }
1344                updateStatusMessage(`DL ${songsProcessedCount + 1}/${totalSongsToDownload}: ${title} (${format})...`);
1345                const success = await processSingleAction(currentSongElCheck, 'download', `${songId}-${format}`, format);
1346                if (success) {
1347                    songDownloadSuccessThisSong = true; formatIndex++;
1348                    if (formatIndex < selectedFormats.length && isDownloading && intraFormatDelayMs > 0) await delay(intraFormatDelayMs);
1349                    else if (isDownloading && intraFormatDelayMs <= 0 && formatIndex < selectedFormats.length) await delay(50);
1350                } else {
1351                    log(`Failed DL ${title} (${format}). Stopping.`, 'error'); updateStatusMessage(`Error DL ${title} (${format}).`); criticalErrorOccurred = true; break;
1352                }
1353            }
1354            if (songDownloadSuccessThisSong) songsProcessedCount++;
1355            if (criticalErrorOccurred || !isDownloading) break;
1356            if (songsProcessedCount < totalSongsToDownload && isDownloading && (songIdsToDownloadArray.indexOf(songId) < songIdsToDownloadArray.length -1) ) {
1357                updateStatusMessage(`Wait ${downloadInterSongDelaySeconds}s for next song...`); await delay(interSongDelayMs);
1358            }
1359        }
1360        log(`DL queue finished. Processed ${songsProcessedCount}/${totalSongsToDownload}.`);
1361        updateStatusMessage(criticalErrorOccurred ? `DL stopped. ${songsProcessedCount} processed.` : `DL queue complete. ${songsProcessedCount} processed.`);
1362        isDownloading = false; setAllButtonsDisabled(false); populateDownloadSongList();
1363    }
1364
1365    async function changeSelectedSongsPrivacy() {
1366        if (!uiElement || isMinimized) { log('Restore UI to change privacy.', 'warn'); return; }
1367        if (currentView !== 'privacy') { log('Privacy change only in Privacy View.', 'warn'); return; }
1368        if (isDeleting || isDownloading || isChangingPrivacy) { log('Operation in progress.', 'warn'); return; }
1369
1370        const songIdsToChangeArray = Array.from(selectedSongIdsPrivacy);
1371        const totalToChange = songIdsToChangeArray.length;
1372        if (totalToChange === 0) {
1373            updateCounter('privacy', 0, 0); log('No songs selected for privacy change.');
1374            updateStatusMessage('No songs selected.'); return;
1375        }
1376
1377        const privacyLevelSelect = uiElement.querySelector('#privacyLevelSelect'); // Scoped query
1378        const targetPrivacyKey = privacyLevelSelect ? privacyLevelSelect.value : 'Only Me';
1379        const targetSubMenuText = privacySubmenuTextMap[targetPrivacyKey]; // Get exact submenu text
1380
1381        isChangingPrivacy = true; stopPrivacyChangeSignal = false; setAllButtonsDisabled(true);
1382        const interSongDelayMs = privacyInterSongDelaySeconds * 1000;
1383        log(`Starting privacy change for ${totalToChange} songs to '${targetPrivacyKey}'.`);
1384        updateCounter('privacy', 0, totalToChange); updateStatusMessage(`Updating ${totalToChange} songs to ${targetPrivacyKey}...`);
1385
1386        let changedCount = 0, criticalErrorOccurred = false;
1387        for (const songId of songIdsToChangeArray) {
1388            if (criticalErrorOccurred || !isChangingPrivacy || stopPrivacyChangeSignal) { break; }
1389            const songElement = Array.from(getCurrentSongElements()).find(el => {
1390                const link = el.querySelector(`a[href="/song/${songId}"]`);
1391                return !!link;
1392            });
1393            if (!songElement) {
1394                log(`Song ID ${songId} not found (possibly removed?). Skipping.`, 'warn');
1395                selectedSongIdsPrivacy.delete(songId);
1396                continue;
1397            }
1398            const title = getSongDataFromPage().find(s => s.id === songId)?.title || `song ID ${songId}`;
1399            updateStatusMessage(`Updating ${changedCount + 1}/${totalToChange}: ${title} to ${targetPrivacyKey}...`);
1400
1401            const success = await processSinglePrivacyChange(songElement, targetSubMenuText, songId);
1402            if (success) {
1403                changedCount++;
1404                updateCounter('privacy', changedCount, totalToChange);
1405            } else {
1406                log(`Failed to change privacy for ${title}. Stopping.`, 'error');
1407                updateStatusMessage(`Error for ${title}. Stopped.`);
1408                criticalErrorOccurred = true;
1409            }
1410            if (isChangingPrivacy && !stopPrivacyChangeSignal && !criticalErrorOccurred && (songIdsToChangeArray.indexOf(songId) < songIdsToChangeArray.length - 1)) {
1411                if (interSongDelayMs > 0) {
1412                    updateStatusMessage(`Waiting ${privacyInterSongDelaySeconds}s for next song...`);
1413                    await delay(interSongDelayMs);
1414                } else {
1415                    await delay(50);
1416                }
1417            }
1418        }
1419        const finalMessage = criticalErrorOccurred ? `Privacy change stopped. ${changedCount} updated.`
1420            : stopPrivacyChangeSignal ? `Privacy change stopped by user. ${changedCount} updated.`
1421                : `Privacy change complete. ${changedCount} updated.`;
1422        log(finalMessage);
1423        updateStatusMessage(finalMessage);
1424        isChangingPrivacy = false; stopPrivacyChangeSignal = false; setAllButtonsDisabled(false);
1425        populatePrivacySongList();
1426    }
1427
1428
1429    async function processSinglePrivacyChange(songElement, targetSubMenuText, identifier, retryCount = 0) {
1430        const logPrefix = `(Privacy - ${identifier}) -`;
1431        if (!isChangingPrivacy || stopPrivacyChangeSignal) { log(`${logPrefix} Op cancelled.`, 'warn'); return false; }
1432        if (!songElement || !songElement.parentNode) { log(`${logPrefix} Element gone.`, 'warn'); return false; }
1433
1434        const menuButton = songElement.querySelector('button[data-sentry-element="MenuTrigger"]');
1435        if (!menuButton) { log(`${logPrefix} No MenuTrigger.`, 'error'); return false; }
1436
1437        logDebug(`${logPrefix} Clicking 'More options' button:`, menuButton);
1438        if (!simulateClick(menuButton)) { log(`${logPrefix} Fail click MenuTrigger.`, 'error'); return false; }
1439        await delay(PRIVACY_MENU_OPEN_DELAY);
1440
1441        let menuContentElement = null;
1442        let popperWrapper = document.querySelector('div[data-radix-popper-content-wrapper][style*="transform: translate"]');
1443        if (popperWrapper) menuContentElement = popperWrapper.querySelector('div[data-radix-menu-content][data-state="open"]');
1444        if (!menuContentElement) {
1445            const openMenus = document.querySelectorAll('div[data-radix-menu-content][data-state="open"]');
1446            if (openMenus.length > 0) menuContentElement = openMenus[openMenus.length - 1];
1447        }
1448        if (!menuContentElement) {
1449            if (retryCount < MAX_RETRIES) {
1450                logWarn(`${logPrefix} No open menu content. Retrying (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
1451                try { document.body.click(); await delay(150); } catch(_e){}
1452                return processSinglePrivacyChange(songElement, targetSubMenuText, identifier, retryCount + 1);
1453            }
1454            log(`${logPrefix} No open menu content after retries.`, 'error');
1455            try { document.body.click(); await delay(50); } catch(_e){} return false;
1456        }
1457
1458        let privacyMainMenuItem = Array.from(menuContentElement.querySelectorAll('div[role="menuitem"]')).find(el => {
1459            const mainDiv = el.querySelector('div.flex.items-center.gap-4');
1460            if (!mainDiv) return false;
1461            const hasEyeIcon = mainDiv.querySelector('svg[data-icon="eye"]');
1462            // Extract text directly from mainDiv, excluding sub-texts like current privacy status
1463            let mainText = '';
1464            mainDiv.childNodes.forEach(node => {
1465                if (node.nodeType === Node.TEXT_NODE) mainText += node.textContent.trim() + ' ';
1466                else if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('overflow-hidden')) { // Common container for main text
1467                    mainText += node.textContent.trim() + ' ';
1468                }
1469            });
1470            mainText = mainText.trim();
1471            return hasEyeIcon && mainText === 'Privacy';
1472        });
1473        if (!privacyMainMenuItem) { // Fallback using data-sentry-source-file as it was specific
1474            privacyMainMenuItem = menuContentElement.querySelector('div[role="menuitem"][data-sentry-source-file="PrivacySubMenu.tsx"]');
1475            if (privacyMainMenuItem) logDebug(`${logPrefix} Found Privacy item by data-sentry-source-file`);
1476        }
1477
1478        if (!privacyMainMenuItem) {
1479            if (retryCount < MAX_RETRIES) {
1480                logWarn(`${logPrefix} 'Privacy' option not found in menu (Attempt ${retryCount + 1}/${MAX_RETRIES}). Retrying...`);
1481                try { document.body.click(); await delay(150); } catch(_e){}
1482                if (!songElement?.parentNode) { logWarn(`${logPrefix} Song element disappeared before retry for 'Privacy'.`); return false; }
1483                return processSinglePrivacyChange(songElement, targetSubMenuText, identifier, retryCount + 1);
1484            }
1485            log(`${logPrefix} 'Privacy' option not found after ${MAX_RETRIES} retries. Menu HTML:`, menuContentElement.innerHTML.substring(0, 700) + '...');
1486            try { document.body.click(); await delay(50); } catch(_e){} return false;
1487        }
1488
1489        const privacySubMenuId = privacyMainMenuItem.getAttribute('aria-controls');
1490        logDebug(`${logPrefix} Clicking 'Privacy' menu item (controls: ${privacySubMenuId || 'N/A'}):`, privacyMainMenuItem);
1491        if (!simulateClick(privacyMainMenuItem)) {
1492            log(`${logPrefix} Failed to simulate click on 'Privacy' option.`, 'error');
1493            try { document.body.click(); await delay(50); } catch(_e){} return false;
1494        }
1495        await delay(PRIVACY_SUBMENU_TRIGGER_CLICK_DELAY);
1496
1497        let subMenuContent = null, subMenuOpenRetries = 0;
1498        while ((isChangingPrivacy && !stopPrivacyChangeSignal) && subMenuOpenRetries < MAX_SUB_MENU_OPEN_RETRIES) {
1499            await delay(PRIVACY_SUBMENU_OPEN_DELAY);
1500            if (privacySubMenuId) subMenuContent = document.getElementById(privacySubMenuId);
1501
1502            if (subMenuContent?.getAttribute('data-state') === 'open') {
1503                logDebug(`${logPrefix} Privacy sub-menu (ID: ${privacySubMenuId}) found by ID and open.`);
1504                break;
1505            } else {
1506                const allPoppers = Array.from(document.querySelectorAll('div[data-radix-popper-content-wrapper][style*="transform: translate"]'));
1507                let foundSubMenu = null;
1508                allPoppers.some(p => {
1509                    const sm = p.querySelector('div[data-radix-menu-content][data-state="open"][data-sentry-component="MenuSubContent"]');
1510                    if (sm && (!privacySubMenuId || sm.id === privacySubMenuId)) {
1511                        foundSubMenu = sm;
1512                        logDebug(`${logPrefix} Found privacy sub-menu in popper (ID: ${sm.id}).`);
1513                        return true;
1514                    }
1515                    return false;
1516                });
1517                if (foundSubMenu) { subMenuContent = foundSubMenu; break; }
1518            }
1519            subMenuOpenRetries++;
1520            if (subMenuOpenRetries > 1 && privacyMainMenuItem?.offsetParent !== null && privacyMainMenuItem.getAttribute('data-state') === 'closed' && privacyMainMenuItem.getAttribute('aria-expanded') === 'false') {
1521                logWarn(`${logPrefix} Submenu not opening. Re-clicking 'Privacy'. Attempt ${subMenuOpenRetries}.`);
1522                if(!simulateClick(privacyMainMenuItem)) { logWarn('Re-click \'Privacy\' for submenu failed.'); break;}
1523                await delay(PRIVACY_SUBMENU_TRIGGER_CLICK_DELAY);
1524            }
1525        }
1526
1527        if (!isChangingPrivacy || stopPrivacyChangeSignal) { log(`${logPrefix} Op cancelled while waiting for sub-menu.`, 'warn'); return false; }
1528        if (!subMenuContent || subMenuContent.getAttribute('data-state') !== 'open') {
1529            log(`${logPrefix} Privacy sub-menu (ID: ${privacySubMenuId || 'unknown'}) did not open. Aborting.`, 'error');
1530            try { document.body.click(); await delay(50); } catch(_e){} return false;
1531        }
1532
1533        logDebug(`${logPrefix} Open sub-menu. Searching for option '${targetSubMenuText}'.`);
1534        const potentialItems = subMenuContent.querySelectorAll(':scope > div[role="menuitem"]');
1535        let targetItem = Array.from(potentialItems).find(el => {
1536            const textDiv = el.querySelector('.line-clamp-2');
1537            const itemText = textDiv ? textDiv.textContent.trim() : el.textContent.trim();
1538            let iconMatch = false;
1539            if (targetSubMenuText === 'Only me' && el.querySelector('svg[data-icon="lock"]')) iconMatch = true;
1540            else if (targetSubMenuText === 'Anyone with the link' && el.querySelector('svg[data-icon="link-simple"]')) iconMatch = true;
1541            else if (targetSubMenuText === 'Publish' && el.querySelector('svg[data-icon="earth-americas"]')) iconMatch = true;
1542            return itemText === targetSubMenuText && iconMatch && el.offsetParent !== null;
1543        });
1544
1545        if (!targetItem && retryCount < MAX_RETRIES) {
1546            logWarn(`${logPrefix} Target privacy option '${targetSubMenuText}' not in sub-menu (Attempt ${retryCount + 1}/${MAX_RETRIES}). Retrying sub-menu check...`);
1547            // This is a soft retry, just re-evaluating the existing submenu content after a small delay
1548            await delay(PRIVACY_SUBMENU_OPEN_DELAY / 2);
1549            if (subMenuContent?.getAttribute('data-state') === 'open') {
1550                const potentialItemsAgain = subMenuContent.querySelectorAll(':scope > div[role="menuitem"]');
1551                targetItem = Array.from(potentialItemsAgain).find(el => {
1552                    const textDiv = el.querySelector('.line-clamp-2');
1553                    const itemText = textDiv ? textDiv.textContent.trim() : el.textContent.trim();
1554                    let iconMatch = false;
1555                    if (targetSubMenuText === 'Only me' && el.querySelector('svg[data-icon="lock"]')) iconMatch = true;
1556                    else if (targetSubMenuText === 'Anyone with the link' && el.querySelector('svg[data-icon="link-simple"]')) iconMatch = true;
1557                    else if (targetSubMenuText === 'Publish' && el.querySelector('svg[data-icon="earth-americas"]')) iconMatch = true;
1558                    return itemText === targetSubMenuText && iconMatch && el.offsetParent !== null;
1559                });
1560                if (targetItem) logDebug(`${logPrefix} Found target privacy option '${targetSubMenuText}' after re-check.`);
1561            } else { log(`${logPrefix} Sub-menu closed unexpectedly during re-check.`, 'error'); return false; }
1562        }
1563
1564        if (!targetItem) {
1565            log(`${logPrefix} Target privacy option '${targetSubMenuText}' not found. Submenu HTML:`, subMenuContent.innerHTML.substring(0, 700) + '...');
1566            try { document.body.click(); await delay(50); } catch(_e){} return false;
1567        }
1568
1569        logDebug(`${logPrefix} Clicking target privacy option '${targetSubMenuText}':`, targetItem);
1570        if (!simulateClick(targetItem)) {
1571            log(`${logPrefix} Failed to click target privacy option '${targetSubMenuText}'.`, 'error');
1572            try { document.body.click(); await delay(50); } catch(_e){} return false;
1573        }
1574        await delay(PRIVACY_ACTION_CLICK_DELAY);
1575        logDebug(`--- Finished processing ${logPrefix} (Privacy change to '${targetSubMenuText}' assumed initiated) ---`);
1576        return true;
1577    }
1578
1579    async function processSingleAction(songElement, actionType, identifier, format = null, retryCount = 0) {
1580        const logPrefix = `(${actionType} - ${identifier}) -`;
1581        if ((actionType === 'delete' && (!isDeleting || stopBulkDeletionSignal)) ||
1582            (actionType === 'download' && !isDownloading)) {
1583            log(`${logPrefix} Op cancelled.`, 'warn'); return false;
1584        }
1585        if (isChangingPrivacy) { log(`${logPrefix} Privacy change in progress, ${actionType} op paused.`, 'warn'); return false;}
1586
1587
1588        if (!songElement || !songElement.parentNode) { log(`${logPrefix} Element gone.`, 'warn'); return actionType === 'delete'; }
1589
1590        const menuButton = songElement.querySelector('button[data-sentry-element="MenuTrigger"]');
1591        if (!menuButton) { log(`${logPrefix} No MenuTrigger.`, 'error'); return false; }
1592
1593        logDebug(`${logPrefix} Clicking 'More options' button:`, menuButton);
1594        if (!simulateClick(menuButton)) { log(`${logPrefix} Fail click MenuTrigger.`, 'error'); return false; }
1595        await delay(DROPDOWN_DELAY);
1596
1597        let primaryActionText = actionType === 'delete' ? 'delete' : 'download';
1598        let primaryActionItem = null, downloadMenuItemId = null, menuContentElement = null;
1599
1600        let popperWrapper = document.querySelector('div[data-radix-popper-content-wrapper][style*="transform: translate"]');
1601        if (popperWrapper) menuContentElement = popperWrapper.querySelector('div[data-radix-menu-content][data-state="open"]');
1602
1603        if (!menuContentElement) {
1604            const openMenus = document.querySelectorAll('div[data-radix-menu-content][data-state="open"]');
1605            if (openMenus.length > 0) menuContentElement = openMenus[openMenus.length - 1];
1606        }
1607
1608        if (!menuContentElement) {
1609            if (retryCount < MAX_RETRIES) {
1610                logWarn(`${logPrefix} No open menu content. Retrying (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
1611                try { document.body.click(); await delay(150); } catch(_e){}
1612                return processSingleAction(songElement, actionType, identifier, format, retryCount + 1);
1613            }
1614            log(`${logPrefix} No open menu content after retries.`, 'error');
1615            try { document.body.click(); await delay(50); } catch(_e){} return false;
1616        }
1617
1618        primaryActionItem = Array.from(menuContentElement.querySelectorAll(':scope > [role="menuitem"], :scope > [data-radix-collection-item]')).find(el => {
1619            const isVisible = el.offsetParent !== null;
1620            if (!isVisible) return false;
1621            let itemText = '';
1622            const lineClampDiv = el.querySelector('.line-clamp-2'); // Common for primary text
1623            if (lineClampDiv) {
1624                itemText = lineClampDiv.textContent.trim().toLowerCase();
1625            } else { // Fallback if no .line-clamp-2
1626                const mainContentContainer = el.querySelector('.flex.items-center.gap-4 > .overflow-hidden:not(:has(svg))'); // Div that holds text, not icon
1627                if (mainContentContainer) {
1628                    itemText = mainContentContainer.textContent.trim().toLowerCase();
1629                } else { // Broader fallback
1630                    itemText = el.textContent.trim().toLowerCase();
1631                    // Clean up potential extra text for items like Privacy that show current status
1632                    if (itemText.startsWith('privacy ')) itemText = 'privacy';
1633                    if (itemText.startsWith('download ')) itemText = 'download';
1634                }
1635            }
1636            logDebug(`${logPrefix} Checking menu item, text: '${itemText}' vs target: '${primaryActionText}'`);
1637            if (itemText === primaryActionText) {
1638                if (primaryActionText === 'download') {
1639                    downloadMenuItemId = el.getAttribute('aria-controls');
1640                    logDebug(`${logPrefix} Found '${primaryActionText}' item, controls submenu: ${downloadMenuItemId || 'N/A'}`);
1641                } else {
1642                    logDebug(`${logPrefix} Found '${primaryActionText}' item`);
1643                }
1644                return true;
1645            }
1646            return false;
1647        });
1648
1649
1650        if (!primaryActionItem && retryCount < MAX_RETRIES) {
1651            logWarn(`${logPrefix} '${primaryActionText}' option not found in menu (Attempt ${retryCount + 1}/${MAX_RETRIES}). Retrying...`);
1652            try { document.body.click(); await delay(150); } catch(_e){}
1653            if (!songElement?.parentNode) {
1654                logWarn(`${logPrefix} Song element disappeared before retry for '${primaryActionText}'.`);
1655                return (actionType === 'delete');
1656            }
1657            return processSingleAction(songElement, actionType, identifier, format, retryCount + 1);
1658        }
1659
1660        if (!primaryActionItem) {
1661            log(`${logPrefix} '${primaryActionText}' option not found after ${MAX_RETRIES} retries. Aborting. Menu HTML:`, menuContentElement.innerHTML.substring(0, 700) + '...');
1662            try { document.body.click(); await delay(50); } catch(_e){} return false;
1663        }
1664
1665        logDebug(`${logPrefix} Clicking primary action '${primaryActionText}' (controls: ${primaryActionItem.getAttribute('aria-controls') || 'N/A'}):`, primaryActionItem);
1666        if (!simulateClick(primaryActionItem)) {
1667            log(`${logPrefix} Failed to simulate click on '${primaryActionText}' option.`, 'error');
1668            try { document.body.click(); await delay(50); } catch(_e){} return false;
1669        }
1670
1671        if (actionType === 'delete') {
1672            await delay(DELETION_DELAY);
1673            logDebug(`--- Finished processing ${logPrefix} (Delete action assumed successful) ---`);
1674            return true;
1675        }
1676        else if (actionType === 'download') {
1677            if (!downloadMenuItemId) {
1678                log(`${logPrefix} No submenu ID for Download. Aborting format ${format}.`, 'error');
1679                try { document.body.click(); await delay(50); } catch(_e){} return false;
1680            }
1681            let subMenuContent = null, subMenuOpenRetries = 0;
1682            while (isDownloading && subMenuOpenRetries < MAX_SUB_MENU_OPEN_RETRIES) {
1683                await delay(DOWNLOAD_MENU_DELAY);
1684                if (downloadMenuItemId) subMenuContent = document.getElementById(downloadMenuItemId);
1685
1686                if (subMenuContent?.getAttribute('data-state') === 'open') {
1687                    logDebug(`${logPrefix} Download sub-menu (ID: ${downloadMenuItemId}) found by ID and open.`);
1688                    break;
1689                } else {
1690                    const allPoppers = Array.from(document.querySelectorAll('div[data-radix-popper-content-wrapper][style*="transform: translate"]'));
1691                    let foundInPopper = false;
1692                    allPoppers.some(p => {
1693                        const sm = p.querySelector('div[data-radix-menu-content][data-state="open"]' + (downloadMenuItemId ? `[id="${downloadMenuItemId}"]` : '[data-sentry-component="MenuSubContent"]'));
1694                        if (sm) {
1695                            subMenuContent = sm;
1696                            logDebug(`${logPrefix} Found download sub-menu in popper (ID: ${sm.id}).`);
1697                            foundInPopper = true;
1698                        }
1699                        return foundInPopper;
1700                    });
1701                    if (foundInPopper) break;
1702                }
1703                subMenuOpenRetries++;
1704                if (subMenuOpenRetries > 1 && primaryActionItem?.offsetParent !== null && primaryActionItem.getAttribute('data-state') === 'closed' && primaryActionItem.getAttribute('aria-expanded') === 'false') {
1705                    logWarn(`${logPrefix} Submenu not opening. Re-clicking 'Download'. Attempt ${subMenuOpenRetries}.`);
1706                    if(!simulateClick(primaryActionItem)) { logWarn('Re-click download for submenu failed.'); break;}
1707                    await delay(DOWNLOAD_MENU_DELAY / 2);
1708                }
1709            }
1710
1711            if (!isDownloading) { log(`${logPrefix} Download cancelled while waiting for sub-menu.`, 'warn'); return false; }
1712            if (!subMenuContent || subMenuContent.getAttribute('data-state') !== 'open') {
1713                log(`${logPrefix} Download sub-menu (ID: ${downloadMenuItemId || 'unknown'}) did not open. Aborting format ${format}.`, 'error');
1714                try { document.body.click(); await delay(50); } catch(_e){} return false;
1715            }
1716
1717            let formatItem = null; const formatTextUpper = format.toUpperCase();
1718            logDebug(`${logPrefix} Open sub-menu. Searching for format '${formatTextUpper}'.`);
1719            const potentialFormatItems = subMenuContent.querySelectorAll(':scope > [role="menuitem"], :scope > [data-radix-collection-item]');
1720            formatItem = Array.from(potentialFormatItems).find(el => {
1721                const textDiv = el.querySelector('.line-clamp-2');
1722                const itemText = textDiv ? textDiv.textContent.trim().toUpperCase() : el.textContent.trim().toUpperCase();
1723                return itemText === formatTextUpper && el.offsetParent !== null && !el.querySelector('svg[data-icon="angle-right"]');
1724            });
1725
1726            if (!formatItem && retryCount < MAX_RETRIES) { // Soft retry for format item
1727                logWarn(`${logPrefix} Format '${formatTextUpper}' not in sub-menu (Attempt ${retryCount +1}/${MAX_RETRIES}). Re-checking...`);
1728                await delay(DOWNLOAD_MENU_DELAY / 2);
1729                if (subMenuContent?.getAttribute('data-state') === 'open') {
1730                    const potentialFormatItemsAgain = subMenuContent.querySelectorAll(':scope > [role="menuitem"], :scope > [data-radix-collection-item]');
1731                    formatItem = Array.from(potentialFormatItemsAgain).find(el => {
1732                        const textDiv = el.querySelector('.line-clamp-2');
1733                        const itemText = textDiv ? textDiv.textContent.trim().toUpperCase() : el.textContent.trim().toUpperCase();
1734                        return itemText === formatTextUpper && el.offsetParent !== null && !el.querySelector('svg[data-icon="angle-right"]');
1735                    });
1736                    if (formatItem) logDebug(`${logPrefix} Found format '${formatTextUpper}' after re-check.`);
1737                } else { log(`${logPrefix} Sub-menu closed unexpectedly.`, 'error'); return false; }
1738            }
1739            if (!formatItem) { log(`${logPrefix} Format '${formatTextUpper}' not found. Submenu:`, subMenuContent.innerHTML.substring(0,700)); return false; }
1740
1741            logDebug(`${logPrefix} Clicking format '${formatTextUpper}' option:`, formatItem);
1742            if (!simulateClick(formatItem)) { log(`${logPrefix} Failed click format '${formatTextUpper}'.`, 'error'); return false; }
1743            await delay(DOWNLOAD_ACTION_DELAY);
1744            logDebug(`--- Finished processing ${logPrefix} (Format ${format} assumed initiated) ---`);
1745            return true;
1746        }
1747        return false;
1748    }
1749
1750    function setAllButtonsDisabled(disabled) {
1751        if (!uiElement) return;
1752        if (isMinimized && disabled) { return; }
1753
1754        const buttons = uiElement.querySelectorAll('#riffControlContent button');
1755        buttons.forEach(btn => {
1756            if (btn.id === 'minimizeButton') return;
1757            if (btn.id === 'deleteAllButton' && currentView === 'bulk' && isDeleting && disabled) {
1758                btn.disabled = false; // Keep stop button enabled
1759            }
1760            // Add similar logic for a stopPrivacyChangeButton if it exists
1761            else {
1762                btn.disabled = disabled;
1763            }
1764        });
1765        const inputs = uiElement.querySelectorAll('#riffControlContent input, #riffControlContent select');
1766        inputs.forEach(input => {
1767            if (input.type === 'checkbox' && (input.id === 'autoReloadToggle' || input.id === 'debugToggleCheckbox' || input.id.startsWith('format') || input.id === 'ignoreLikedToggleDelete')) {
1768                input.disabled = false; // Always enable these specific checkboxes
1769            } else if (input.id.includes('DelayInput') || input.id === 'privacyLevelSelect') {
1770                input.disabled = false; // Always enable delay inputs and privacy select
1771            }
1772            else input.disabled = disabled;
1773        });
1774
1775        const labels = uiElement.querySelectorAll('#riffControlContent label.settings-checkbox-label, #riffControlContent label.selectAllContainer, #riffControlContent .downloadFormatContainer > label.settings-checkbox-label, #riffControlContent .downloadDelayContainer label.settings-checkbox-label, #riffControlContent .privacySettingsContainer label.settings-checkbox-label');
1776        labels.forEach(label => {
1777            const forActiveStop = label.htmlFor === 'deleteAllButton' && currentView === 'bulk' && isDeleting && disabled;
1778            const isAlwaysInteractive = label.closest('#commonSettingsFooter') ||
1779                                      label.closest('.downloadFormatContainer') ||
1780                                      label.closest('.downloadDelayContainer') ||
1781                                      label.closest('.privacySettingsContainer') ||
1782                                      label.htmlFor === 'ignoreLikedToggleDelete';
1783
1784            if (forActiveStop || isAlwaysInteractive) {
1785                label.style.cursor = 'pointer'; label.style.opacity = '1';
1786            }
1787            else { label.style.cursor = disabled ? 'not-allowed' : 'pointer'; label.style.opacity = disabled ? '0.7' : '1'; }
1788        });
1789
1790        const songListCheckboxes = uiElement.querySelectorAll('.songListContainer input[type="checkbox"]');
1791        songListCheckboxes.forEach(cb => {
1792            if (cb.closest('label.ignored')) { // Ignored checkboxes in delete list
1793                cb.disabled = true;
1794            } else {
1795                cb.disabled = disabled;
1796            }
1797        });
1798
1799
1800        if (!disabled) {
1801            if(currentView === 'selective' && uiElement.querySelector('#selectiveModeControls')?.style.display === 'block') populateDeleteSongListIfNeeded();
1802            if(currentView === 'download' && uiElement.querySelector('#downloadQueueControls')?.style.display === 'block') populateDownloadSongListIfNeeded();
1803            if(currentView === 'privacy' && uiElement.querySelector('#privacyStatusControls')?.style.display === 'block') populatePrivacySongListIfNeeded();
1804        }
1805        const status = uiElement.querySelector('#statusMessage');
1806        if (status) {
1807            const allowPointerEvents = (!disabled) || (currentView === 'bulk' && isDeleting && disabled);
1808            status.style.pointerEvents = allowPointerEvents ? 'auto' : 'none';
1809        }
1810        logDebug(`Controls ${disabled ? 'mostly disabled' : 'enabled'}.`);
1811    }
1812
1813
1814    // --- Initialization ---
1815    function waitForAppReady(callback) {
1816        const checkInterval = 500, maxWaitTime = 20000; let elapsedTime = 0;
1817        const intervalId = setInterval(() => {
1818            elapsedTime += checkInterval;
1819            const appReadyElement = document.querySelector('div#__next main, div#__next nav, div#__next header'); // Check for main app structure
1820            if (appReadyElement) {
1821                clearInterval(intervalId); setTimeout(callback, 300); // Wait a bit more for full render
1822            } else if (elapsedTime >= maxWaitTime) {
1823                clearInterval(intervalId); logWarn('Riffusion app not fully detected. Initializing anyway.'); setTimeout(callback, 300);
1824            }
1825        }, checkInterval);
1826    }
1827
1828    function init() {
1829        try {
1830            log(`Riffusion Multitool Script Loaded (v${GM_info.script.version}).`);
1831            createMainUI();
1832            log(`Initialized. UI is ${isMinimized ? 'minimized' : 'visible'}. Auto-reload: ${autoReloadEnabled}. Debug: ${debugMode}.`);
1833            if (isMinimized) updateStatusMessage('Ready. Click icon to expand.');
1834            logDebug('Initial debug log test post-UI creation.');
1835        } catch (e) {
1836            console.error('[RiffTool] Initialization failed:', e); alert('RiffTool Init Error: ' + e.message);
1837        }
1838    }
1839
1840    waitForAppReady(init);
1841
1842})();
Riffusion Multitool | Robomonkey