i’ve recently tried organizing my music library and ended up going through metedata/id3 tag hell, i got claude to make me a simple mp3 in browser (kinda fits my work flow to have it in the browser) i named it “webp3”
the idea is pretty simple, choose a working dir and folders within are artists, any folders nestled further are albums ( i browse my music by artist)
currently all i have is a very rough version.....
also its a react/vite project and needs lucid-react,
(install with npm install lucide-react)
the claude part without vite scaffold
import React, { useState, useRef, useEffect } from 'react';
import { Play, Pause, SkipForward, SkipBack, Volume2, Folder, Moon, Sun, ChevronLeft, ChevronRight, Music } from 'lucide-react';
const MusicPlayer = () => {
const [theme, setTheme] = useState('light');
const [directory, setDirectory] = useState(null);
const [artists, setArtists] = useState([]);
const [selectedArtist, setSelectedArtist] = useState(null);
const [albums, setAlbums] = useState([]);
const [selectedAlbum, setSelectedAlbum] = useState(null);
const [tracks, setTracks] = useState([]);
const [currentTrack, setCurrentTrack] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(1);
const [sidebarOpen, setSidebarOpen] = useState(true);
const audioRef = useRef(null);
const themes = {
light: {
bg: '#f9fafb',
card: '#ffffff',
text: '#111827',
textSecondary: '#6b7280',
border: '#e5e7eb',
hover: '#f3f4f6',
active: '#2563eb',
activeText: '#ffffff'
},
dark: {
bg: '#111827',
card: '#1f2937',
text: '#f3f4f6',
textSecondary: '#9ca3af',
border: '#374151',
hover: '#374151',
active: '#2563eb',
activeText: '#ffffff'
},
amoled: {
bg: '#000000',
card: '#030712',
text: '#f3f4f6',
textSecondary: '#6b7280',
border: '#111827',
hover: '#111827',
active: '#2563eb',
activeText: '#ffffff'
}
};
const t = themes[theme];
const selectDirectory = async () => {
try {
const dirHandle = await window.showDirectoryPicker();
setDirectory(dirHandle);
await scanDirectory(dirHandle);
} catch (err) {
console.error('Error selecting directory:', err);
}
};
const scanDirectory = async (dirHandle) => {
const artistList = [];
for await (const entry of dirHandle.values()) {
if (entry.kind === 'directory') {
artistList.push({
name: entry.name,
handle: entry
});
}
}
setArtists(artistList.sort((a, b) => a.name.localeCompare(b.name)));
};
const loadArtistAlbums = async (artistHandle) => {
const albumList = [];
for await (const entry of artistHandle.values()) {
if (entry.kind === 'directory') {
albumList.push({
name: entry.name,
handle: entry
});
}
}
setAlbums(albumList.sort((a, b) => a.name.localeCompare(b.name)));
};
const loadAlbumTracks = async (albumHandle) => {
const trackList = [];
const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.flac'];
for await (const entry of albumHandle.values()) {
if (entry.kind === 'file') {
const ext = entry.name.substring(entry.name.lastIndexOf('.')).toLowerCase();
if (audioExtensions.includes(ext)) {
trackList.push({
name: entry.name,
handle: entry
});
}
}
}
setTracks(trackList.sort((a, b) => a.name.localeCompare(b.name)));
};
const playTrack = async (track) => {
try {
const file = await track.handle.getFile();
const url = URL.createObjectURL(file);
if (audioRef.current) {
audioRef.current.src = url;
audioRef.current.load();
await audioRef.current.play();
setCurrentTrack(track);
setIsPlaying(true);
}
} catch (err) {
console.error('Error playing track:', err);
}
};
const togglePlayPause = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const skipTrack = (direction) => {
const currentIndex = tracks.findIndex(t => t.name === currentTrack?.name);
const nextIndex = direction === 'next' ? currentIndex + 1 : currentIndex - 1;
if (nextIndex >= 0 && nextIndex < tracks.length) {
playTrack(tracks[nextIndex]);
}
};
const cycleTheme = () => {
const themeOrder = ['light', 'dark', 'amoled'];
const currentIndex = themeOrder.indexOf(theme);
const nextIndex = (currentIndex + 1) % themeOrder.length;
setTheme(themeOrder[nextIndex]);
};
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const updateTime = () => setCurrentTime(audio.currentTime);
const updateDuration = () => setDuration(audio.duration);
const handleEnded = () => skipTrack('next');
audio.addEventListener('timeupdate', updateTime);
audio.addEventListener('loadedmetadata', updateDuration);
audio.addEventListener('ended', handleEnded);
return () => {
audio.removeEventListener('timeupdate', updateTime);
audio.removeEventListener('loadedmetadata', updateDuration);
audio.removeEventListener('ended', handleEnded);
};
}, [tracks, currentTrack]);
const formatTime = (seconds) => {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div style={{ minHeight: '100vh', backgroundColor: t.bg, color: t.text, display: 'flex', flexDirection: 'column', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }}>
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar */}
<div
onMouseEnter={() => setSidebarOpen(true)}
onMouseLeave={() => setSidebarOpen(false)}
style={{
width: sidebarOpen ? '320px' : '60px',
backgroundColor: t.card,
borderRight: `1px solid ${t.border}`,
transition: 'width 0.3s',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}
>
{sidebarOpen && (
<div style={{ padding: '1rem', flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<button
onClick={cycleTheme}
style={{
padding: '0.75rem',
borderRadius: '0.5rem',
border: 'none',
cursor: 'pointer',
backgroundColor: 'transparent',
color: t.text,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
title="Toggle Theme"
>
{theme === 'light' ? <Sun size={24} /> : <Moon size={24} />}
</button>
<button
onClick={selectDirectory}
style={{
padding: '0.75rem',
borderRadius: '0.5rem',
border: 'none',
cursor: 'pointer',
backgroundColor: 'transparent',
color: t.text,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
title="Select Music Folder"
>
<Folder size={24} />
</button>
</div>
</div>
)}
</div>
{/* Main Content */}
<div style={{ flex: 1, overflowY: 'auto' }}>
<div style={{ padding: '1.5rem' }}>
<div style={{ backgroundColor: t.card, borderRadius: '0.5rem', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', padding: '1.5rem', marginBottom: '1.5rem', textAlign: 'center' }}>
<h1 style={{ fontSize: '1.875rem', fontWeight: 'bold' }}>webp3</h1>
</div>
{artists.length === 0 ? (
<div style={{ backgroundColor: t.card, borderRadius: '0.5rem', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', padding: '3rem', textAlign: 'center' }}>
<Music size={64} style={{ margin: '0 auto 1rem', color: t.textSecondary }} />
<h2 style={{ fontSize: '1.5rem', fontWeight: 600, marginBottom: '0.5rem' }}>No Music Folder Selected</h2>
<p style={{ color: t.textSecondary }}>Click the folder icon in the sidebar to browse your music library</p>
</div>
) : selectedAlbum ? (
<div>
<button
onClick={() => {
setSelectedAlbum(null);
setTracks([]);
}}
style={{
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '1rem',
background: 'none',
border: 'none',
color: t.text,
cursor: 'pointer',
padding: 0,
textDecoration: 'underline'
}}
>
← Back to {selectedArtist.name}
</button>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>{selectedAlbum.name}</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '1rem'
}}>
{tracks.map((track, idx) => (
<div
key={idx}
onClick={() => playTrack(track)}
style={{
backgroundColor: t.card,
borderRadius: '0.5rem',
padding: '1rem',
cursor: 'pointer',
transition: 'all 0.2s',
border: currentTrack?.name === track.name ? `2px solid ${t.active}` : '2px solid transparent'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = t.card}
>
<div style={{
aspectRatio: '1',
backgroundColor: t.card,
border: `1px solid ${t.border}`,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '0.75rem'
}}>
<Music size={48} style={{ color: t.textSecondary }} />
</div>
<div style={{ fontWeight: 600, fontSize: '0.875rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'center' }}>
{track.name.replace(/\.(mp3|wav|ogg|m4a|flac)$/i, '')}
</div>
</div>
))}
</div>
</div>
) : selectedArtist ? (
<div>
<button
onClick={() => {
setSelectedArtist(null);
setAlbums([]);
setSelectedAlbum(null);
setTracks([]);
}}
style={{
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '1rem',
background: 'none',
border: 'none',
color: t.text,
cursor: 'pointer',
padding: 0,
textDecoration: 'underline'
}}
>
← Back to Artists
</button>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>{selectedArtist.name}</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '1rem'
}}>
{albums.map((album, idx) => (
<div
key={idx}
onClick={() => {
setSelectedAlbum(album);
loadAlbumTracks(album.handle);
}}
style={{
backgroundColor: t.card,
borderRadius: '0.5rem',
padding: '1rem',
cursor: 'pointer',
transition: 'all 0.2s',
border: '2px solid transparent'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = t.card}
>
<div style={{
aspectRatio: '1',
backgroundColor: t.card,
border: `1px solid ${t.border}`,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '0.75rem'
}}>
<Music size={48} style={{ color: t.textSecondary }} />
</div>
<div style={{ fontWeight: 600, fontSize: '0.875rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'center' }}>
{album.name}
</div>
</div>
))}
</div>
</div>
) : (
<div>
<h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>Artists</h2>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
gap: '1rem'
}}>
{artists.map((artist, idx) => (
<div
key={idx}
onClick={() => {
setSelectedArtist(artist);
loadArtistAlbums(artist.handle);
setSelectedAlbum(null);
setTracks([]);
}}
style={{
backgroundColor: t.card,
borderRadius: '0.5rem',
padding: '1rem',
cursor: 'pointer',
transition: 'all 0.2s',
border: '2px solid transparent'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = t.card}
>
<div style={{
aspectRatio: '1',
backgroundColor: t.card,
border: `1px solid ${t.border}`,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '0.75rem'
}}>
<Music size={48} style={{ color: t.textSecondary }} />
</div>
<div style={{ fontWeight: 600, fontSize: '0.875rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'center' }}>
{artist.name}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
{/* Player Bar */}
{currentTrack && (
<div style={{ backgroundColor: t.card, borderTop: `1px solid ${t.border}`, padding: '1rem' }}>
<div style={{ maxWidth: '1280px', margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<div style={{ flex: 1, minWidth: 0, marginRight: '1rem' }}>
<div style={{ fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{currentTrack.name}
</div>
<div style={{ fontSize: '0.875rem', color: t.textSecondary, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{selectedArtist?.name} • {selectedAlbum?.name}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
<button
onClick={() => skipTrack('prev')}
style={{
padding: '0.5rem',
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
backgroundColor: 'transparent',
color: t.text,
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.target.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
>
<SkipBack size={20} />
</button>
<button
onClick={togglePlayPause}
style={{
padding: '0.75rem',
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
backgroundColor: 'transparent',
color: t.text,
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.target.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
>
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
</button>
<button
onClick={() => skipTrack('next')}
style={{
padding: '0.5rem',
borderRadius: '50%',
border: 'none',
cursor: 'pointer',
backgroundColor: 'transparent',
color: t.text,
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.target.style.backgroundColor = t.hover}
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
>
<SkipForward size={20} />
</button>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginLeft: '1rem' }}>
<Volume2 size={20} />
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => {
setVolume(e.target.value);
audioRef.current.volume = e.target.value;
}}
style={{ width: '96px' }}
/>
</div>
</div>
<div>
<input
type="range"
min="0"
max={duration || 0}
value={currentTime}
onChange={(e) => {
audioRef.current.currentTime = e.target.value;
setCurrentTime(e.target.value);
}}
style={{ width: '100%' }}
/>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem', color: t.textSecondary, marginTop: '0.25rem' }}>
<span>{formatTime(currentTime)}</span>
<span>{formatTime(duration)}</span>
</div>
</div>
</div>
</div>
)}
<audio ref={audioRef} />
</div>
);
};
export default MusicPlayer;
(drive link has both/everything)




