-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
375 lines (313 loc) · 13.3 KB
/
script.js
File metadata and controls
375 lines (313 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
"use strict";
// --- DOM Elements ---
const selectRomFolderBtn = document.getElementById('select-rom-folder');
const selectImageFolderBtn = document.getElementById('select-image-folder');
const romFolderPathSpan = document.getElementById('rom-folder-path');
const imageFolderPathSpan = document.getElementById('image-folder-path');
const scanButton = document.getElementById('scan-button');
const deleteButton = document.getElementById('delete-button');
const resultsContainer = document.getElementById('results-container');
const statusBar = document.getElementById('status-bar');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
// --- Configuration ---
const MULTI_PART_PATTERNS = [
// Original patterns for content in brackets
/\(disc\s*\d+\)/i,
/\(disk\s*\d+\)/i,
/\(side\s*[AB\d]+\)/i,
/\(part\s*\d+\)/i,
/\(cart\s*\d+\)/i,
/\(tape\s*\d+\)/i,
/\(book\s*\d+\)/i,
/\(mission\s*disk\)/i,
// New, more flexible patterns for content NOT necessarily in brackets
// This covers "Mission Disk Vol. 1", "Scenery Disk 7", "Disk 2 of 4", etc.
/(?:disc|disk|part|vol|volume|side|scenery disk)\s*[\dIVX]+/i
];
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.bmp'];
// --- Application Status ---
let romDirHandle = null;
let imageDirHandle = null;
let duplicateGroups = {};
// --- Event Listeners ---
selectRomFolderBtn.addEventListener('click', async () => {
try {
romDirHandle = await window.showDirectoryPicker({
id: 'roms',
mode: 'readwrite'
});
romFolderPathSpan.textContent = romDirHandle.name;
updateButtonStates();
updateStatus(`ROM folder selected: ${romDirHandle.name}`);
} catch (err) {
if (err.name !== 'AbortError') {
updateStatus(`Error selecting ROM folder: ${err.message}`, true);
}
}
});
selectImageFolderBtn.addEventListener('click', async () => {
try {
imageDirHandle = await window.showDirectoryPicker({
id: 'images',
mode: 'readwrite'
});
imageFolderPathSpan.textContent = imageDirHandle.name;
updateButtonStates();
updateStatus(`Image folder selected: ${imageDirHandle.name}`);
} catch (err) {
if (err.name !== 'AbortError') {
updateStatus(`Error selecting image folder: ${err.message}`, true);
}
}
});
scanButton.addEventListener('click', scanForDuplicates);
deleteButton.addEventListener('click', deleteSelectedFiles);
// --- Core Logic ---
function isMultiPart(filename) {
return MULTI_PART_PATTERNS.some(pattern => pattern.test(filename));
}
function getBaseName(filename) {
// Start with the filename, remove extension as it's not relevant for the base name
let name = filename.lastIndexOf('.') > 0 ? filename.substring(0, filename.lastIndexOf('.')) : filename;
// Rule 1: Remove all content in brackets () and []
// This gets rid of (USA), (Europe), [b1], etc.
name = name.replace(/\s*[\(\[].*?[\)\]]/g, '');
// Rule 2: Take only text before the first '+' (for compilations)
name = name.split('+')[0];
// Rule 3: Normalize sequels (remove Roman/Arabic numerals at the end)
// This is only applied if the title is complex (contains a separator AND is long enough)
// to avoid false positives like 'Turrican 3' or 'R-Type II'.
if ((name.includes('-') || name.includes(':')) && name.length > 15) {
name = name.replace(/\s+(?:\d+|I|II|III|IV|V|VI|VII|VIII)$/i, '');
}
// Rule 4: Final cleanup of trailing characters and whitespace
name = name.replace(/[\s-_]+$/, '').trim();
// Return in lowercase for case-insensitive comparison
return name.toLowerCase();
}
async function scanForDuplicates() {
if (!romDirHandle) {
updateStatus("Error: No ROM folder selected.", true);
return;
}
updateStatus("Scan in progress... Please wait.");
scanButton.disabled = true;
deleteButton.disabled = true;
const potentialDuplicates = new Map();
try {
for await (const entry of romDirHandle.values()) {
if (entry.kind !== 'file') continue; // Ignore subdirectories
if (isMultiPart(entry.name)) {
console.log(`Ignoring multi-part set: ${entry.name}`);
continue;
}
const baseName = getBaseName(entry.name);
console.log(`File: ${entry.name} -> Base: ${baseName}`); // Diagnostic log
if (baseName) {
if (!potentialDuplicates.has(baseName)) {
potentialDuplicates.set(baseName, []);
}
potentialDuplicates.get(baseName).push(entry.name);
}
}
} catch (err) {
updateStatus(`Error scanning folder: ${err.message}`, true);
scanButton.disabled = false;
return;
}
duplicateGroups = {};
for (const [base, files] of potentialDuplicates.entries()) {
if (files.length > 1) {
duplicateGroups[base] = files.sort();
}
}
await displayResults();
updateButtonStates();
}
async function displayResults() {
resultsContainer.innerHTML = ''; // Clear previous results
const groupKeys = Object.keys(duplicateGroups);
if (groupKeys.length === 0) {
resultsContainer.innerHTML = '<p>No duplicates found according to the rules.</p>';
updateStatus("Scan complete. No duplicates found.");
return;
}
const groupCount = groupKeys.length;
const fileCount = Object.values(duplicateGroups).reduce((sum, files) => sum + files.length, 0);
updateStatus(`Scan complete. Found ${groupCount} duplicate groups with a total of ${fileCount} files.`);
const resultsList = document.createElement('ul');
// Sort groups by base name
for (const baseName of groupKeys.sort()) {
const files = duplicateGroups[baseName];
const groupHeader = document.createElement('li');
groupHeader.className = 'group-header';
const imagePreviewContainer = document.createElement('div');
imagePreviewContainer.className = 'image-preview-container';
groupHeader.appendChild(imagePreviewContainer);
const groupTitle = document.createElement('span');
groupTitle.textContent = `Group: ${baseName} (${files.length} files)`;
groupHeader.appendChild(groupTitle);
resultsList.appendChild(groupHeader);
// Find and display the first available image for the group
if (imageDirHandle) {
let imageFound = false;
for (const file of files) {
if (imageFound) break;
const baseFilename = file.substring(0, file.lastIndexOf('.'));
for (const ext of IMAGE_EXTENSIONS) {
const imgFilename = baseFilename + ext;
try {
const fileHandle = await imageDirHandle.getFileHandle(imgFilename);
const imgFile = await fileHandle.getFile();
const img = document.createElement('img');
img.src = URL.createObjectURL(imgFile);
img.onload = () => URL.revokeObjectURL(img.src); // Important: Free up memory
img.title = imgFilename;
imagePreviewContainer.appendChild(img);
imageFound = true;
break; // Only one image per group
} catch (e) {
if (e.name !== 'NotFoundError') {
console.warn(`Could not load image: ${imgFilename}`, e);
}
}
}
}
}
files.forEach((file, index) => {
const fileItem = document.createElement('li');
fileItem.className = 'file-item';
fileItem.dataset.group = baseName;
fileItem.dataset.filename = file;
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = `group-${baseName}`;
radio.value = file;
radio.id = `radio-${file}`;
if (index === 0) { // Select first file by default
radio.checked = true;
}
const label = document.createElement('label');
label.htmlFor = `radio-${file}`;
label.textContent = file;
fileItem.appendChild(radio);
fileItem.appendChild(label);
resultsList.appendChild(fileItem);
radio.addEventListener('change', () => {
updateVisualSelection(baseName);
});
});
}
resultsContainer.appendChild(resultsList);
// Apply initial styling for all groups AFTER the list is in the DOM
groupKeys.forEach(baseName => {
updateVisualSelection(baseName);
});
}
async function deleteSelectedFiles() {
if (!romDirHandle || !imageDirHandle) {
updateStatus("Error: ROM and Image folders must be selected.", true);
return;
}
const filesToDelete = [];
for (const baseName in duplicateGroups) {
const selectedFile = document.querySelector(`input[name="group-${baseName}"]:checked`).value;
duplicateGroups[baseName].forEach(file => {
if (file !== selectedFile) {
filesToDelete.push(file);
}
});
}
if (filesToDelete.length === 0) {
updateStatus("No files selected for deletion.", false);
return;
}
const confirmation = confirm(
`You are about to delete ${filesToDelete.length} files (and their images) marked as redundant. The files marked in green will be kept.\n\nContinue?\n\n${filesToDelete.slice(0, 10).join('\n')}${filesToDelete.length > 10 ? '\n...' : ''}`
);
if (!confirmation) {
updateStatus("Deletion process canceled.");
return;
}
updateStatus(`Deleting ${filesToDelete.length} files...`);
deleteButton.disabled = true;
scanButton.disabled = true; // Disable scan button during deletion
progressContainer.classList.remove('hidden');
progressBar.style.width = '0%';
let deletedCount = 0;
let errorCount = 0;
const totalFiles = filesToDelete.length;
for (let i = 0; i < totalFiles; i++) {
const filename = filesToDelete[i];
try {
// Delete ROM
await romDirHandle.removeEntry(filename);
console.log(`Deleted: ${filename} in ROM folder`);
deletedCount++;
// Delete associated image (try)
const baseFilename = filename.substring(0, filename.lastIndexOf('.'));
for (const ext of IMAGE_EXTENSIONS) {
try {
await imageDirHandle.removeEntry(baseFilename + ext);
console.log(`Deleted: ${baseFilename + ext} in image folder`);
} catch (imgErr) {
if (imgErr.name !== 'NotFoundError') throw imgErr;
}
}
} catch (err) {
console.error(`Error deleting ${filename}: ${err.message}`);
errorCount++;
}
// Update progress and force UI update
const progress = ((i + 1) / totalFiles) * 100;
progressBar.style.width = `${progress}%`;
// Short pause to allow the browser to redraw the UI
if (i % 10 === 0) { // Not for every file, to avoid slowing it down
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Short delay to show 100%
await new Promise(resolve => setTimeout(resolve, 200));
progressContainer.classList.add('hidden');
let finalMsg = `Deletion complete. ${deletedCount} ROM files deleted.`;
if (errorCount > 0) {
finalMsg += ` ${errorCount} errors occurred (see console for details).`;
}
updateStatus(finalMsg);
// Short pause before rescan so the status message is visible
await new Promise(resolve => setTimeout(resolve, 1500));
// Rescan to show the updated state
await scanForDuplicates();
// Final status message after successful deletion
if (errorCount === 0) {
updateStatus("Deletion successful. No more duplicates found. You can select new folders.");
}
}
// --- Helper Functions ---
function updateButtonStates() {
const bothFoldersSelected = romDirHandle && imageDirHandle;
scanButton.disabled = !bothFoldersSelected;
deleteButton.disabled = Object.keys(duplicateGroups).length === 0 || !bothFoldersSelected;
}
function updateStatus(message, isError = false) {
statusBar.textContent = message;
statusBar.style.color = isError ? '#c53030' : '#2d3748';
statusBar.style.backgroundColor = isError ? '#fed7d7' : '#f7fafc';
console.log(message);
}
function updateVisualSelection(baseName) {
const fileItems = document.querySelectorAll(`.file-item[data-group="${baseName}"]`);
fileItems.forEach(item => {
const radio = item.querySelector('input[type="radio"]');
if (radio.checked) {
item.classList.add('file-to-keep');
item.classList.remove('file-to-delete');
} else {
item.classList.add('file-to-delete');
item.classList.remove('file-to-keep');
}
});
}
// --- Initialization ---
updateStatus("Ready. Please select the ROM and Image folders.");