Tag editing local music files vs. music streaming

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)

1 Like

Great job. If your interested, there is a great free app that probably does all of what you need (albeit not in the browser)

1 Like

thanks!
I did some looking around and missed this, but…

the site looks like its from dial up internet and its for windows only,(mac version costs)

go near tag editing and your world crumbles, I’m traumatized…

I love tag editing, ever since I found this tool.

So do the sites for many GREAT free programs (Everything, 7-Zip, Notepad++, VLC, you get my point)

2 Likes

true true

13 posts were split to a new topic: What do you do while coding?

Or you just use Claude Cowork to name everything for you…

Saw an ad for this. Is this not just Claude code with a “less scary” ui instead of using the terminal, and some extra system prompts?

1 Like

Thats good but I very much prefer MusicBrainz Picard as it tags a lot of songs automatically

You can use MusicBrainz Picard as a data source for Mp3 tag… or choose from a bunch of others. Its flexible

1 Like

Point is it worked :face_with_tongue:

1 Like

If I wasted money on a subscription just to organize music, then I would need to spend another few thousand dollars a month on therapy for dealing with the guilt for wasting all that money.

I’d rather waste that money on “saving” on something didn’t need because it’s on sale.

1 Like

Claude isn’t only used for organizing music lol

I’m talking about paying for 24six, not Claude.

Good AI is one of the few things I would pay a subscription for, and if I’m paying for AI it would probably be Claude.

3 Likes

okay i took this a drop further

its now also a public repo :grinning_face:

https://github.com/lubabs770/Musicy



5 Likes

https://github.com/lubabs770/Musicy

bump. mild improvments, preferences cleaned up a bit.
added grid sizing slider color buttons still not functional

2 Likes