|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Clone Hero Library Manager</title> |
|
<style> |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; |
|
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); |
|
min-height: 100vh; |
|
padding: 20px; |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
} |
|
|
|
header { |
|
background: white; |
|
padding: 30px; |
|
border-radius: 12px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 30px; |
|
} |
|
|
|
h1 { |
|
color: #1e3c72; |
|
font-size: 2em; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.subtitle { |
|
color: #666; |
|
font-size: 0.95em; |
|
} |
|
|
|
.controls { |
|
background: white; |
|
padding: 20px; |
|
border-radius: 12px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
margin-bottom: 20px; |
|
} |
|
|
|
.controls-row { |
|
display: flex; |
|
gap: 10px; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.btn { |
|
background: #2a5298; |
|
color: white; |
|
border: none; |
|
padding: 12px 24px; |
|
border-radius: 6px; |
|
cursor: pointer; |
|
font-size: 1em; |
|
transition: background 0.3s; |
|
} |
|
|
|
.btn:hover { |
|
background: #1e3c72; |
|
} |
|
|
|
.btn:disabled { |
|
background: #ccc; |
|
cursor: not-allowed; |
|
} |
|
|
|
.btn-danger { |
|
background: #dc3545; |
|
} |
|
|
|
.btn-danger:hover { |
|
background: #c82333; |
|
} |
|
|
|
.btn-success { |
|
background: #28a745; |
|
} |
|
|
|
.btn-success:hover { |
|
background: #218838; |
|
} |
|
|
|
.btn-small { |
|
padding: 6px 12px; |
|
font-size: 0.9em; |
|
} |
|
|
|
.library-info { |
|
background: #f8f9fa; |
|
padding: 15px; |
|
border-radius: 6px; |
|
margin-top: 15px; |
|
} |
|
|
|
.library-info p { |
|
color: #495057; |
|
margin: 5px 0; |
|
} |
|
|
|
.artist-list { |
|
display: grid; |
|
gap: 20px; |
|
} |
|
|
|
.artist-card { |
|
background: white; |
|
border-radius: 12px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
overflow: hidden; |
|
transition: transform 0.2s; |
|
} |
|
|
|
.artist-card:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); |
|
} |
|
|
|
.artist-header { |
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
color: white; |
|
padding: 20px; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
.artist-name { |
|
font-size: 1.5em; |
|
font-weight: 600; |
|
} |
|
|
|
.artist-count { |
|
background: rgba(255, 255, 255, 0.2); |
|
padding: 5px 12px; |
|
border-radius: 20px; |
|
font-size: 0.9em; |
|
} |
|
|
|
.artist-actions { |
|
display: flex; |
|
gap: 10px; |
|
margin-top: 10px; |
|
} |
|
|
|
.song-list { |
|
padding: 20px; |
|
} |
|
|
|
.song-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 12px; |
|
border-bottom: 1px solid #e9ecef; |
|
transition: background 0.2s; |
|
} |
|
|
|
.song-item:last-child { |
|
border-bottom: none; |
|
} |
|
|
|
.song-item:hover { |
|
background: #f8f9fa; |
|
} |
|
|
|
.song-name { |
|
color: #333; |
|
flex: 1; |
|
} |
|
|
|
.song-folder { |
|
color: #888; |
|
font-size: 0.85em; |
|
margin-top: 3px; |
|
} |
|
|
|
.modal { |
|
display: none; |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background: rgba(0, 0, 0, 0.5); |
|
justify-content: center; |
|
align-items: center; |
|
z-index: 1000; |
|
} |
|
|
|
.modal.active { |
|
display: flex; |
|
} |
|
|
|
.modal-content { |
|
background: white; |
|
padding: 30px; |
|
border-radius: 12px; |
|
max-width: 500px; |
|
width: 90%; |
|
} |
|
|
|
.modal-content h2 { |
|
color: #1e3c72; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.form-group { |
|
margin-bottom: 20px; |
|
} |
|
|
|
.form-group label { |
|
display: block; |
|
margin-bottom: 8px; |
|
color: #495057; |
|
font-weight: 500; |
|
} |
|
|
|
.form-group input { |
|
width: 100%; |
|
padding: 10px; |
|
border: 2px solid #dee2e6; |
|
border-radius: 6px; |
|
font-size: 1em; |
|
} |
|
|
|
.form-group input:focus { |
|
outline: none; |
|
border-color: #2a5298; |
|
} |
|
|
|
.modal-actions { |
|
display: flex; |
|
gap: 10px; |
|
justify-content: flex-end; |
|
} |
|
|
|
.empty-state { |
|
background: white; |
|
padding: 60px 20px; |
|
border-radius: 12px; |
|
text-align: center; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
.empty-state h2 { |
|
color: #495057; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.empty-state p { |
|
color: #6c757d; |
|
} |
|
|
|
.loading { |
|
text-align: center; |
|
padding: 40px; |
|
color: white; |
|
font-size: 1.2em; |
|
} |
|
|
|
.progress-bar { |
|
background: white; |
|
border-radius: 12px; |
|
padding: 20px; |
|
margin-top: 20px; |
|
} |
|
|
|
.progress-track { |
|
background: #e9ecef; |
|
height: 30px; |
|
border-radius: 15px; |
|
overflow: hidden; |
|
position: relative; |
|
} |
|
|
|
.progress-fill { |
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); |
|
height: 100%; |
|
transition: width 0.3s; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
color: white; |
|
font-weight: 600; |
|
} |
|
|
|
.warning-message { |
|
background: #fff3cd; |
|
border: 1px solid #ffc107; |
|
color: #856404; |
|
padding: 12px; |
|
border-radius: 6px; |
|
margin-top: 15px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<header> |
|
<h1>🎸 Clone Hero Library Manager</h1> |
|
<p class="subtitle">Manage your Clone Hero song library with ease</p> |
|
</header> |
|
|
|
<div class="controls"> |
|
<div class="controls-row"> |
|
<button id="selectFolder" class="btn">Select Library Folder</button> |
|
<button id="syncMetadata" class="btn btn-success" style="display: none;">Sync Folder Names from Metadata</button> |
|
</div> |
|
|
|
<div id="libraryInfo" class="library-info" style="display: none;"> |
|
<p><strong>Library:</strong> <span id="libraryPath"></span></p> |
|
<p><strong>Artists:</strong> <span id="artistCount"></span> | <strong>Songs:</strong> <span id="songCount"></span></p> |
|
</div> |
|
</div> |
|
|
|
<div id="content"> |
|
<div class="empty-state"> |
|
<h2>Welcome!</h2> |
|
<p>Select your Clone Hero library folder to get started</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- Rename Artist Modal --> |
|
<div id="renameModal" class="modal"> |
|
<div class="modal-content"> |
|
<h2>Rename Artist</h2> |
|
<div class="form-group"> |
|
<label for="newArtistName">New Artist Name</label> |
|
<input type="text" id="newArtistName" placeholder="Enter new artist name"> |
|
</div> |
|
<p class="warning-message">This will rename all song folders and update all song.ini files for this artist.</p> |
|
<div class="modal-actions"> |
|
<button class="btn" onclick="closeRenameModal()">Cancel</button> |
|
<button class="btn btn-success" onclick="performRename()">Rename</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
let libraryHandle = null; |
|
let libraryData = {}; |
|
let currentRenameArtist = null; |
|
|
|
document.getElementById('selectFolder').addEventListener('click', selectLibraryFolder); |
|
document.getElementById('syncMetadata').addEventListener('click', syncMetadataToFolders); |
|
|
|
async function selectLibraryFolder() { |
|
try { |
|
libraryHandle = await window.showDirectoryPicker(); |
|
await loadLibrary(); |
|
} catch (err) { |
|
if (err.name !== 'AbortError') { |
|
console.error('Error selecting folder:', err); |
|
alert('Error selecting folder: ' + err.message); |
|
} |
|
} |
|
} |
|
|
|
async function parseIniFile(fileContent) { |
|
const ini = {}; |
|
let currentSection = null; |
|
|
|
const lines = fileContent.split('\n'); |
|
for (const line of lines) { |
|
const trimmed = line.trim(); |
|
|
|
// Skip empty lines and comments |
|
if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) { |
|
continue; |
|
} |
|
|
|
// Check for section header (case-insensitive) |
|
const sectionMatch = trimmed.match(/^\[(.+)\]$/); |
|
if (sectionMatch) { |
|
currentSection = sectionMatch[1].toLowerCase(); |
|
ini[currentSection] = ini[currentSection] || {}; |
|
continue; |
|
} |
|
|
|
// Parse key-value pairs (case-insensitive keys) |
|
const kvMatch = trimmed.match(/^([^=]+)=(.*)$/); |
|
if (kvMatch && currentSection) { |
|
const key = kvMatch[1].trim().toLowerCase(); |
|
const value = kvMatch[2].trim(); |
|
ini[currentSection][key] = value; |
|
} |
|
} |
|
|
|
return ini; |
|
} |
|
|
|
async function readSongIni(dirHandle) { |
|
try { |
|
const fileHandle = await dirHandle.getFileHandle('song.ini'); |
|
const file = await fileHandle.getFile(); |
|
const content = await file.text(); |
|
const ini = await parseIniFile(content); |
|
|
|
return { |
|
artist: ini.song?.artist || 'Unknown Artist', |
|
name: ini.song?.name || 'Unknown Song' |
|
}; |
|
} catch (err) { |
|
// If song.ini doesn't exist, try to parse from folder name |
|
return null; |
|
} |
|
} |
|
|
|
async function loadLibrary() { |
|
document.getElementById('content').innerHTML = '<div class="loading">Loading library...</div>'; |
|
libraryData = {}; |
|
|
|
try { |
|
let processed = 0; |
|
let total = 0; |
|
|
|
// First count total entries |
|
for await (const entry of libraryHandle.values()) { |
|
if (entry.kind === 'directory') { |
|
total++; |
|
} |
|
} |
|
|
|
// Show progress |
|
document.getElementById('content').innerHTML = ` |
|
<div class="loading"> |
|
<p>Scanning library...</p> |
|
<div class="progress-bar"> |
|
<div class="progress-track"> |
|
<div class="progress-fill" id="progressFill" style="width: 0%">0%</div> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
|
|
// Process each directory |
|
for await (const entry of libraryHandle.values()) { |
|
if (entry.kind === 'directory') { |
|
const metadata = await readSongIni(entry); |
|
|
|
if (metadata) { |
|
const artist = metadata.artist; |
|
const songName = metadata.name; |
|
|
|
if (!libraryData[artist]) { |
|
libraryData[artist] = []; |
|
} |
|
|
|
libraryData[artist].push({ |
|
name: songName, |
|
folderName: entry.name, |
|
dirHandle: entry |
|
}); |
|
} |
|
|
|
processed++; |
|
const percent = Math.round((processed / total) * 100); |
|
const progressFill = document.getElementById('progressFill'); |
|
if (progressFill) { |
|
progressFill.style.width = percent + '%'; |
|
progressFill.textContent = percent + '%'; |
|
} |
|
} |
|
} |
|
|
|
// Sort artists alphabetically |
|
const sortedArtists = Object.keys(libraryData).sort((a, b) => |
|
a.toLowerCase().localeCompare(b.toLowerCase()) |
|
); |
|
|
|
displayLibrary(sortedArtists); |
|
updateLibraryInfo(); |
|
document.getElementById('syncMetadata').style.display = 'inline-block'; |
|
} catch (err) { |
|
alert('Error loading library: ' + err.message); |
|
document.getElementById('content').innerHTML = '<div class="empty-state"><h2>Error Loading Library</h2><p>' + err.message + '</p></div>'; |
|
} |
|
} |
|
|
|
function displayLibrary(artists) { |
|
if (artists.length === 0) { |
|
document.getElementById('content').innerHTML = '<div class="empty-state"><h2>No Songs Found</h2><p>No folders with valid song.ini files found in this directory</p></div>'; |
|
return; |
|
} |
|
|
|
let html = '<div class="artist-list">'; |
|
|
|
for (const artist of artists) { |
|
const songs = libraryData[artist]; |
|
songs.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())); |
|
|
|
html += ` |
|
<div class="artist-card"> |
|
<div class="artist-header"> |
|
<div> |
|
<div class="artist-name">${escapeHtml(artist)}</div> |
|
<div class="artist-actions"> |
|
<button class="btn btn-small btn-rename" data-artist="${escapeHtml(artist)}">Rename Artist</button> |
|
<button class="btn btn-danger btn-small btn-delete-artist" data-artist="${escapeHtml(artist)}">Delete All Songs</button> |
|
</div> |
|
</div> |
|
<div class="artist-count">${songs.length} song${songs.length !== 1 ? 's' : ''}</div> |
|
</div> |
|
<div class="song-list"> |
|
`; |
|
|
|
for (const song of songs) { |
|
html += ` |
|
<div class="song-item"> |
|
<div> |
|
<div class="song-name">${escapeHtml(song.name)}</div> |
|
<div class="song-folder">📁 ${escapeHtml(song.folderName)}</div> |
|
</div> |
|
<button class="btn btn-danger btn-small btn-delete-song" data-artist="${escapeHtml(artist)}" data-folder="${escapeHtml(song.folderName)}">Delete</button> |
|
</div> |
|
`; |
|
} |
|
|
|
html += ` |
|
</div> |
|
</div> |
|
`; |
|
} |
|
|
|
html += '</div>'; |
|
document.getElementById('content').innerHTML = html; |
|
|
|
// Add event listeners using event delegation |
|
document.getElementById('content').addEventListener('click', handleContentClick); |
|
} |
|
|
|
function handleContentClick(e) { |
|
const target = e.target; |
|
|
|
if (target.classList.contains('btn-rename')) { |
|
const artist = target.dataset.artist; |
|
renameArtist(artist); |
|
} else if (target.classList.contains('btn-delete-artist')) { |
|
const artist = target.dataset.artist; |
|
deleteArtist(artist); |
|
} else if (target.classList.contains('btn-delete-song')) { |
|
const artist = target.dataset.artist; |
|
const folderName = target.dataset.folder; |
|
deleteSong(artist, folderName); |
|
} |
|
} |
|
|
|
function updateLibraryInfo() { |
|
document.getElementById('libraryInfo').style.display = 'block'; |
|
document.getElementById('libraryPath').textContent = libraryHandle.name; |
|
document.getElementById('artistCount').textContent = Object.keys(libraryData).length; |
|
|
|
let totalSongs = 0; |
|
for (const artist in libraryData) { |
|
totalSongs += libraryData[artist].length; |
|
} |
|
document.getElementById('songCount').textContent = totalSongs; |
|
} |
|
|
|
async function deleteSong(artist, folderName) { |
|
if (!confirm(`Are you sure you want to delete "${folderName}"?`)) { |
|
return; |
|
} |
|
|
|
// Save scroll position |
|
const scrollPosition = window.scrollY; |
|
|
|
try { |
|
await libraryHandle.removeEntry(folderName, { recursive: true }); |
|
|
|
// Update local data |
|
libraryData[artist] = libraryData[artist].filter(s => s.folderName !== folderName); |
|
if (libraryData[artist].length === 0) { |
|
delete libraryData[artist]; |
|
} |
|
|
|
// Refresh display |
|
const sortedArtists = Object.keys(libraryData).sort((a, b) => |
|
a.toLowerCase().localeCompare(b.toLowerCase()) |
|
); |
|
displayLibrary(sortedArtists); |
|
updateLibraryInfo(); |
|
|
|
// Restore scroll position |
|
setTimeout(() => { |
|
window.scrollTo(0, scrollPosition); |
|
}, 50); |
|
} catch (err) { |
|
alert('Error deleting song: ' + err.message); |
|
} |
|
} |
|
|
|
async function deleteArtist(artist) { |
|
const songs = libraryData[artist]; |
|
if (!confirm(`Are you sure you want to delete all ${songs.length} song(s) by ${artist}?`)) { |
|
return; |
|
} |
|
|
|
// Save scroll position |
|
const scrollPosition = window.scrollY; |
|
|
|
try { |
|
for (const song of songs) { |
|
await libraryHandle.removeEntry(song.folderName, { recursive: true }); |
|
} |
|
|
|
// Update local data |
|
delete libraryData[artist]; |
|
|
|
// Refresh display |
|
const sortedArtists = Object.keys(libraryData).sort((a, b) => |
|
a.toLowerCase().localeCompare(b.toLowerCase()) |
|
); |
|
displayLibrary(sortedArtists); |
|
updateLibraryInfo(); |
|
|
|
// Restore scroll position |
|
setTimeout(() => { |
|
window.scrollTo(0, scrollPosition); |
|
}, 50); |
|
} catch (err) { |
|
alert('Error deleting artist: ' + err.message); |
|
} |
|
} |
|
|
|
function renameArtist(artist) { |
|
currentRenameArtist = artist; |
|
document.getElementById('newArtistName').value = artist; |
|
document.getElementById('renameModal').classList.add('active'); |
|
document.getElementById('newArtistName').focus(); |
|
document.getElementById('newArtistName').select(); |
|
} |
|
|
|
function closeRenameModal() { |
|
document.getElementById('renameModal').classList.remove('active'); |
|
currentRenameArtist = null; |
|
} |
|
|
|
async function updateSongIni(dirHandle, newArtist) { |
|
try { |
|
const fileHandle = await dirHandle.getFileHandle('song.ini', { create: false }); |
|
const file = await fileHandle.getFile(); |
|
const content = await file.text(); |
|
|
|
// Replace artist line in the INI file (case-insensitive) |
|
const lines = content.split('\n'); |
|
const updatedLines = lines.map(line => { |
|
const trimmed = line.trim(); |
|
const lowerTrimmed = trimmed.toLowerCase(); |
|
if (lowerTrimmed.startsWith('artist =') || lowerTrimmed.startsWith('artist=')) { |
|
// Preserve original spacing style |
|
const hasSpace = trimmed.includes('artist ='); |
|
return hasSpace ? `artist = ${newArtist}` : `artist=${newArtist}`; |
|
} |
|
return line; |
|
}); |
|
|
|
const updatedContent = updatedLines.join('\n'); |
|
|
|
// Write back to file |
|
const writable = await fileHandle.createWritable(); |
|
await writable.write(updatedContent); |
|
await writable.close(); |
|
} catch (err) { |
|
console.error('Error updating song.ini:', err); |
|
throw err; |
|
} |
|
} |
|
|
|
async function performRename() { |
|
const newArtistName = document.getElementById('newArtistName').value.trim(); |
|
|
|
if (!newArtistName) { |
|
alert('Please enter a new artist name'); |
|
return; |
|
} |
|
|
|
if (newArtistName === currentRenameArtist) { |
|
closeRenameModal(); |
|
return; |
|
} |
|
|
|
const songs = libraryData[currentRenameArtist]; |
|
const oldArtistName = currentRenameArtist; |
|
closeRenameModal(); |
|
|
|
// Save scroll position |
|
const scrollPosition = window.scrollY; |
|
|
|
document.getElementById('content').innerHTML = ` |
|
<div class="loading"> |
|
<p>Renaming artist...</p> |
|
<div class="progress-bar"> |
|
<div class="progress-track"> |
|
<div class="progress-fill" id="renameProgressFill" style="width: 0%">0%</div> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
|
|
try { |
|
// Update all song.ini files for this artist |
|
for (let i = 0; i < songs.length; i++) { |
|
const song = songs[i]; |
|
const dirHandle = await libraryHandle.getDirectoryHandle(song.folderName); |
|
await updateSongIni(dirHandle, newArtistName); |
|
|
|
const percent = Math.round(((i + 1) / songs.length) * 100); |
|
const progressFill = document.getElementById('renameProgressFill'); |
|
if (progressFill) { |
|
progressFill.style.width = percent + '%'; |
|
progressFill.textContent = percent + '%'; |
|
} |
|
} |
|
|
|
// Update local data - merge with existing artist if it exists |
|
if (libraryData[newArtistName]) { |
|
libraryData[newArtistName] = [...libraryData[newArtistName], ...songs]; |
|
} else { |
|
libraryData[newArtistName] = songs; |
|
} |
|
|
|
delete libraryData[oldArtistName]; |
|
|
|
// Refresh display |
|
const sortedArtists = Object.keys(libraryData).sort((a, b) => |
|
a.toLowerCase().localeCompare(b.toLowerCase()) |
|
); |
|
displayLibrary(sortedArtists); |
|
updateLibraryInfo(); |
|
|
|
// Restore scroll position after a brief delay to allow DOM to render |
|
setTimeout(() => { |
|
window.scrollTo(0, scrollPosition); |
|
}, 100); |
|
} catch (err) { |
|
alert('Error renaming artist: ' + err.message); |
|
// Reload library to ensure consistency |
|
await loadLibrary(); |
|
// Restore scroll position |
|
setTimeout(() => { |
|
window.scrollTo(0, scrollPosition); |
|
}, 100); |
|
} |
|
} |
|
|
|
async function syncMetadataToFolders() { |
|
if (!confirm('This will rename all song folders to match the metadata in song.ini files. This operation cannot be undone. Continue?')) { |
|
return; |
|
} |
|
|
|
let totalSongs = 0; |
|
for (const artist in libraryData) { |
|
totalSongs += libraryData[artist].length; |
|
} |
|
|
|
document.getElementById('content').innerHTML = ` |
|
<div class="loading"> |
|
<p>Syncing folder names from metadata...</p> |
|
<div class="progress-bar"> |
|
<div class="progress-track"> |
|
<div class="progress-fill" id="syncProgressFill" style="width: 0%">0%</div> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
|
|
try { |
|
let processed = 0; |
|
|
|
for (const artist in libraryData) { |
|
const songs = libraryData[artist]; |
|
|
|
for (const song of songs) { |
|
const expectedFolderName = `${artist} - ${song.name}`; |
|
|
|
// Only rename if the folder name doesn't match |
|
if (song.folderName !== expectedFolderName) { |
|
try { |
|
// Get the old directory handle |
|
const oldDirHandle = await libraryHandle.getDirectoryHandle(song.folderName); |
|
|
|
// Create new directory |
|
const newDirHandle = await libraryHandle.getDirectoryHandle(expectedFolderName, { create: true }); |
|
|
|
// Copy all files from old to new |
|
await copyDirectory(oldDirHandle, newDirHandle); |
|
|
|
// Delete old directory |
|
await libraryHandle.removeEntry(song.folderName, { recursive: true }); |
|
|
|
// Update local data |
|
song.folderName = expectedFolderName; |
|
} catch (err) { |
|
console.error(`Error renaming ${song.folderName}:`, err); |
|
// Continue with other songs even if one fails |
|
} |
|
} |
|
|
|
processed++; |
|
const percent = Math.round((processed / totalSongs) * 100); |
|
const progressFill = document.getElementById('syncProgressFill'); |
|
if (progressFill) { |
|
progressFill.style.width = percent + '%'; |
|
progressFill.textContent = percent + '%'; |
|
} |
|
} |
|
} |
|
|
|
// Refresh display |
|
const sortedArtists = Object.keys(libraryData).sort((a, b) => |
|
a.toLowerCase().localeCompare(b.toLowerCase()) |
|
); |
|
displayLibrary(sortedArtists); |
|
updateLibraryInfo(); |
|
|
|
alert('Folder names synced successfully!'); |
|
} catch (err) { |
|
alert('Error syncing folder names: ' + err.message); |
|
await loadLibrary(); |
|
} |
|
} |
|
|
|
async function copyDirectory(sourceHandle, destHandle) { |
|
for await (const entry of sourceHandle.values()) { |
|
if (entry.kind === 'file') { |
|
const file = await entry.getFile(); |
|
const newFileHandle = await destHandle.getFileHandle(entry.name, { create: true }); |
|
const writable = await newFileHandle.createWritable(); |
|
await writable.write(file); |
|
await writable.close(); |
|
} else if (entry.kind === 'directory') { |
|
const newDirHandle = await destHandle.getDirectoryHandle(entry.name, { create: true }); |
|
await copyDirectory(entry, newDirHandle); |
|
} |
|
} |
|
} |
|
|
|
function escapeHtml(text) { |
|
const div = document.createElement('div'); |
|
div.textContent = text; |
|
return div.innerHTML; |
|
} |
|
|
|
// Allow Enter key to confirm rename |
|
document.getElementById('newArtistName').addEventListener('keypress', (e) => { |
|
if (e.key === 'Enter') { |
|
performRename(); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |