Initial commit

This commit is contained in:
Percy 2026-01-13 22:26:48 +00:00
commit c803de020e
52 changed files with 20439 additions and 0 deletions

6536
static/app.js Normal file

File diff suppressed because it is too large Load diff

BIN
static/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

10
static/icon.svg Normal file
View file

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#818cf8"/>
<stop offset="100%" style="stop-color:#6366f1"/>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="45" fill="url(#gradient)"/>
<path d="M35 25 L35 75 L75 50 Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 397 B

778
static/index.html Normal file
View file

@ -0,0 +1,778 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
<meta name="theme-color" content="#1a1a2e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>Freedify</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/static/icon.png" type="image/png">
<link rel="apple-touch-icon" href="/static/icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@400;700;900&family=Permanent+Marker&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/styles.css?t=1736053100">
<!-- Google API for Drive sync -->
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script src="https://apis.google.com/js/api.js" async defer></script>
<!-- Butterchurn (MilkDrop WebGL visualizer) -->
<script src="https://cdn.jsdelivr.net/npm/butterchurn@2.6.7/lib/butterchurn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/butterchurn-presets@2.4.7/lib/butterchurnPresets.min.js"></script>
</head>
<body>
<!-- Toast Container -->
<div id="toast-container" class="toast-container"></div>
<!-- Keyboard Shortcuts Help -->
<div id="shortcuts-help" class="shortcuts-help hidden">
<div class="shortcuts-content">
<div class="shortcuts-header">
<h3>⌨️ Keyboard Shortcuts</h3>
<button id="shortcuts-close" class="shortcuts-close">×</button>
</div>
<div class="shortcuts-grid">
<div class="shortcut"><kbd>Space</kbd><span>Play/Pause</span></div>
<div class="shortcut"><kbd></kbd><span>Next Track</span></div>
<div class="shortcut"><kbd></kbd><span>Previous Track</span></div>
<div class="shortcut"><kbd>Shift + →</kbd><span>Seek +10s</span></div>
<div class="shortcut"><kbd>Shift + ←</kbd><span>Seek -10s</span></div>
<div class="shortcut"><kbd></kbd><span>Volume Up</span></div>
<div class="shortcut"><kbd></kbd><span>Volume Down</span></div>
<div class="shortcut"><kbd>M</kbd><span>Mute/Unmute</span></div>
<div class="shortcut"><kbd>S</kbd><span>Shuffle Queue</span></div>
<div class="shortcut"><kbd>R</kbd><span>Toggle Repeat</span></div>
<div class="shortcut"><kbd>F</kbd><span>Fullscreen</span></div>
<div class="shortcut"><kbd>Q</kbd><span>Toggle Queue</span></div>
<div class="shortcut"><kbd>E</kbd><span>Toggle EQ</span></div>
<div class="shortcut"><kbd>P</kbd><span>Add to Playlist</span></div>
<div class="shortcut"><kbd>H</kbd><span>HiFi/Hi-Res</span></div>
<div class="shortcut"><kbd>D</kbd><span>Download Track</span></div>
<div class="shortcut"><kbd>A</kbd><span>AI Radio</span></div>
<div class="shortcut"><kbd>L</kbd><span>Lyrics</span></div>
<div class="shortcut"><kbd>V</kbd><span>Music Video</span></div>
<div class="shortcut"><kbd>Shift + S</kbd><span>Sync to Drive</span></div>
<div class="shortcut"><kbd>?</kbd><span>This Help</span></div>
</div>
</div>
</div>
<div class="app-container">
<!-- Header -->
<header class="header">
<h1 class="logo"><span class="brand-text">Freedify</span></h1>
<div class="header-actions">
<button id="hifi-btn" class="header-btn" title="HiFi Mode - Stream lossless FLAC (faster startup, more data)">HiFi</button>
<label for="file-input" class="header-btn" title="Add Local Songs (MP3/FLAC)" style="cursor: pointer; display: inline-flex; align-items: center; justify-content: center;">📂</label>
<button id="dj-mode-btn" class="header-btn" title="DJ Mode">🎧</button>
<button id="sync-btn" class="header-btn" title="Sync with Google Drive">☁️</button>
<button id="theme-btn" class="theme-btn" title="Change Theme">🎨</button>
</div>
</header>
<!-- Theme Picker Dropdown -->
<div id="theme-picker" class="theme-picker hidden">
<div class="theme-option" data-theme="">🌙 Default</div>
<div class="theme-option" data-theme="theme-purple">💜 Purple</div>
<div class="theme-option" data-theme="theme-blue">💙 Blue</div>
<div class="theme-option" data-theme="theme-green">💚 Green</div>
<div class="theme-option" data-theme="theme-pink">💕 Pink</div>
<div class="theme-option" data-theme="theme-orange">🧡 Orange</div>
</div>
<!-- Search Section -->
<section class="search-section">
<div class="search-container">
<input
type="text"
id="search-input"
class="search-input"
placeholder="Search music or paste ANY link (Spotify, Bandcamp, SoundCloud...)"
autocomplete="off"
title="Search or Import"
>
<button id="search-clear" class="search-clear" title="Clear search">×</button>
</div>
<!-- Search Type Selector -->
<div class="search-type-selector">
<button class="type-btn active" data-type="track">Songs</button>
<button class="type-btn" data-type="album">Albums</button>
<button class="type-btn" data-type="artist">Artists</button>
<button class="type-btn" data-type="podcast">Podcasts</button>
<button id="search-more-btn" class="type-btn more-btn">⋮ More</button>
</div>
<!-- Search More Menu -->
<div id="search-more-menu" class="search-more-menu hidden">
<button class="menu-item" id="ai-menu-btn">Smart Playlist</button>
<button class="menu-item" id="concert-search-menu-btn">Concert Search</button>
<div class="menu-divider"></div>
<button class="menu-item type-btn-menu" data-type="ytmusic">YT Music</button>
<button class="menu-item type-btn-menu" data-type="setlist">Setlists</button>
<button class="menu-item type-btn-menu" data-type="rec">For You</button>
<button class="menu-item type-btn-menu" data-type="favorites">Playlists</button>
</div>
</section>
<!-- Loading Overlay -->
<div id="loading-overlay" class="loading-overlay hidden">
<div class="loading-content">
<div class="spinner"></div>
<p id="loading-text">Loading...</p>
</div>
</div>
<!-- Error Message -->
<div id="error-message" class="error-message hidden">
<span class="error-icon">😕</span>
<p id="error-text">Something went wrong</p>
<button id="error-retry" class="btn-retry">Try Again</button>
</div>
<!-- Download Modal -->
<div id="download-modal" class="modal hidden">
<div class="modal-content">
<h3>Download Track</h3>
<p id="download-track-name">Track Name</p>
<p id="download-source-hint" class="download-hint hidden"></p>
<div class="format-selector">
<label>Select Format:</label>
<select id="download-format">
<optgroup label="Lossy">
<option value="mp3" data-min-quality="lossy">MP3 (320kbps)</option>
</optgroup>
<optgroup label="Lossless (16-bit)">
<option value="flac" data-min-quality="lossless">FLAC (16-bit)</option>
<option value="aiff" data-min-quality="lossless">AIFF (16-bit)</option>
<option value="wav" data-min-quality="lossless">WAV (16-bit)</option>
<option value="alac" data-min-quality="lossless">ALAC (Apple Lossless)</option>
</optgroup>
<optgroup id="hires-formats" label="Hi-Res (24-bit)">
<option value="flac_24" data-min-quality="hires">FLAC (24-bit Hi-Res)</option>
<option value="aiff_24" data-min-quality="hires">AIFF (24-bit)</option>
<option value="wav_24" data-min-quality="hires">WAV (24-bit)</option>
</optgroup>
</select>
</div>
<div class="modal-actions">
<button id="download-cancel-btn" class="btn-secondary">Cancel</button>
<button id="download-drive-btn" class="btn-secondary" title="Save to Google Drive">☁️ Save to Drive</button>
<button id="download-confirm-btn" class="btn-primary">Download</button>
</div>
</div>
</div>
<!-- Results Section -->
<section id="results-section" class="results-section">
<div id="results-container" class="results-container">
<!-- Search results will be inserted here -->
<div class="empty-state">
<span class="empty-icon">🔍</span>
<p>Search for your favorite music</p>
<p class="hint">Or paste a Spotify link to an album or playlist</p>
</div>
</div>
<button id="load-more-btn" class="load-more-btn hidden">Load More Results</button>
</section>
<!-- Album/Playlist Detail View -->
<section id="detail-view" class="detail-view hidden">
<div class="detail-header">
<button id="back-btn" class="back-btn">← Back</button>
<div class="detail-actions">
<button id="shuffle-btn" class="shuffle-btn" title="Shuffle & Play">Shuffle</button>
<button id="queue-all-btn" class="queue-all-btn">Add All to Queue</button>
<button id="download-all-btn" class="download-all-btn">Download ZIP</button>
</div>
</div>
<div id="detail-info" class="detail-info">
<!-- Album/playlist info will be inserted here -->
</div>
<div id="detail-tracks" class="detail-tracks">
<!-- Tracks will be inserted here -->
</div>
</section>
<!-- Queue Section -->
<section id="queue-section" class="queue-section hidden">
<div class="queue-header">
<h3>Queue <span id="queue-count">(0)</span></h3>
<div class="queue-controls">
<button id="queue-clear" class="queue-clear">Clear</button>
<button id="queue-close" class="queue-close-btn" title="Close Queue">×</button>
</div>
</div>
<!-- Crossfade Toggle -->
<div class="crossfade-toggle">
<span class="crossfade-icon"></span>
<div class="crossfade-info">
<span class="crossfade-title">1s Crossfade</span>
<span class="crossfade-desc">Smooth transition between tracks</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="crossfade-checkbox">
<span class="toggle-slider"></span>
</label>
</div>
<div id="queue-container" class="queue-container">
<!-- Queue items will be inserted here -->
</div>
</section>
<!-- Album Details Modal -->
<div id="album-modal" class="album-modal hidden">
<div class="album-modal-overlay"></div>
<div class="album-modal-content">
<div class="album-modal-header">
<button id="album-modal-close" class="album-modal-close" title="Close">×</button>
<h2>Album Details</h2>
</div>
<div class="album-modal-body">
<div class="album-modal-info">
<img id="album-modal-art" class="album-modal-art" src="" alt="Album Art">
<div class="album-modal-meta">
<h3 id="album-modal-title" class="album-modal-title">Album Title</h3>
<p id="album-modal-artist" class="album-modal-artist">Artist Name</p>
<!-- Metadata Pills -->
<div class="album-meta-pills">
<span id="album-modal-date" class="meta-pill">📅 2024-01-01</span>
<span id="album-modal-trackcount" class="meta-pill">🎵 12 tracks</span>
<span id="album-modal-duration" class="meta-pill">⏱️ 45 min</span>
</div>
<!-- Action Buttons -->
<div class="album-action-buttons">
<button id="album-play-btn" class="album-action-btn primary">▶ Play Album</button>
<button id="album-queue-btn" class="album-action-btn">+ Add to Queue</button>
<button id="album-download-btn" class="album-action-btn">⬇ Download Album</button>
<button id="album-playlist-btn" class="album-action-btn">♡ Add to Playlist</button>
</div>
<!-- Quality Badge -->
<div id="album-modal-quality" class="album-quality-badge">
🎵 FLAC • 16bit / 44.1kHz
</div>
</div>
</div>
<!-- Tabs -->
<div class="album-tabs">
<button class="album-tab active" data-tab="tracks">Tracks</button>
<button class="album-tab" data-tab="info">Album Info</button>
</div>
<!-- Track List -->
<div id="album-modal-tracks" class="album-modal-tracks">
<!-- Tracks will be inserted here -->
</div>
<!-- Album Info (hidden by default) -->
<div id="album-modal-info-tab" class="album-modal-info-tab hidden">
<p id="album-modal-description">Album description and additional info...</p>
</div>
</div>
</div>
</div>
<!-- AI Assistant Modal -->
<div id="ai-modal" class="ai-modal hidden">
<div class="ai-modal-overlay"></div>
<div class="ai-modal-content">
<div class="ai-modal-header">
<h2>🧠 Smart Playlist</h2>
<button id="ai-modal-close" class="ai-modal-close" title="Close">×</button>
</div>
<!-- Playlist Generator Content -->
<div class="ai-tab-content active">
<p class="ai-tab-desc">Describe your perfect playlist and I'll create it...</p>
<textarea id="ai-playlist-input" class="ai-input" placeholder="e.g., 'A 30-minute morning coffee playlist with jazz and bossa nova'" rows="3"></textarea>
<div class="ai-duration-row">
<label>Duration:</label>
<input type="range" id="ai-duration-slider" min="15" max="120" value="60" step="15">
<span id="ai-duration-label">60 min</span>
</div>
<button id="ai-playlist-gen-btn" class="ai-action-btn">🎵 Generate Playlist</button>
<div id="ai-playlist-results" class="ai-results"></div>
</div>
</div>
</div>
<!-- Bottom Player -->
<div id="player-bar" class="player-bar hidden">
<!-- Main visible row (Art + Info + Primary Controls) -->
<div class="player-main-row">
<div class="player-info">
<img id="player-art" class="player-art" src="" alt="Album art">
<div class="player-details">
<p id="player-title" class="player-title">No track playing</p>
<div class="player-meta-row">
<span id="player-artist" class="player-artist clickable" title="Search artist">-</span>
<span class="player-separator"></span>
<span id="player-album" class="player-album clickable" title="View album">-</span>
<span id="player-year" class="player-year"></span>
<span id="audio-format-badge" class="audio-format-badge hidden">MP3</span>
</div>
</div>
</div>
<div class="player-controls-primary">
<button id="prev-btn" class="control-btn" title="Previous"></button>
<button id="play-btn" class="control-btn play-btn" title="Play/Pause"></button>
<button id="next-btn" class="control-btn" title="Next"></button>
<button id="fs-toggle-btn" class="control-btn" title="Full Screen"></button>
<button id="more-controls-btn" class="control-btn" title="More Options"></button>
</div>
</div>
<!-- Progress Bar -->
<div class="player-progress">
<span id="current-time" class="time">0:00</span>
<input type="range" id="progress-bar" class="progress-bar" min="0" max="100" value="0" title="Seek">
<span id="duration" class="time">0:00</span>
</div>
<!-- More Menu (Popup) - 2x5 Grid -->
<div id="player-more-menu" class="player-more-menu hidden">
<div class="more-menu-grid four-col">
<!-- Row 1 -->
<button id="shuffle-queue-btn" class="control-btn" title="Shuffle Queue"></button>
<button id="repeat-btn" class="control-btn" title="Repeat: Off"></button>
<button id="download-current-btn" class="control-btn" title="Download"></button>
<button id="queue-btn" class="control-btn queue-toggle" title="Queue"></button>
<!-- Row 2 -->
<button id="mute-btn" class="control-btn volume-btn" title="Mute">🔊</button>
<button id="eq-toggle-btn" class="control-btn eq-btn" title="Equalizer">🎛️</button>
<button id="ai-radio-btn" class="control-btn ai-radio-btn" title="AI Radio">📻</button>
<button id="mini-player-btn" class="control-btn" title="Popout Winamp Player"></button>
<!-- Row 3 -->
<button id="add-to-playlist-btn" class="control-btn" title="Add to Playlist">🩷</button>
<button id="lyrics-btn" class="control-btn lyrics-btn" title="Lyrics">📝</button>
<button id="video-btn" class="control-btn" title="Music Video">🎬</button>
<button id="menu-visualizer-btn" class="control-btn" title="Visualizer">🌈</button>
</div>
<!-- Volume slider inside menu for mobile compactness -->
<div class="more-menu-volume">
<input type="range" id="volume-slider" class="volume-slider" min="0" max="100" value="100" title="Volume">
</div>
</div>
</div>
<!-- Equalizer Panel -->
<div id="eq-panel" class="eq-panel hidden">
<div class="eq-header">
<h3>🎛️ Equalizer</h3>
<button id="eq-close-btn" class="eq-close-btn">×</button>
</div>
<div class="eq-presets">
<button class="eq-preset active" data-preset="flat">Flat</button>
<button class="eq-preset" data-preset="bass">Bass Boost</button>
<button class="eq-preset" data-preset="treble">Treble</button>
<button class="eq-preset" data-preset="vocal">Vocal</button>
</div>
<div class="eq-sliders">
<div class="eq-band">
<input type="range" id="eq-60" class="eq-slider" min="-12" max="12" value="0" orient="vertical">
<span class="eq-label">60Hz</span>
</div>
<div class="eq-band">
<input type="range" id="eq-230" class="eq-slider" min="-12" max="12" value="0">
<span class="eq-label">230Hz</span>
</div>
<div class="eq-band">
<input type="range" id="eq-910" class="eq-slider" min="-12" max="12" value="0">
<span class="eq-label">910Hz</span>
</div>
<div class="eq-band">
<input type="range" id="eq-3600" class="eq-slider" min="-12" max="12" value="0">
<span class="eq-label">3.6kHz</span>
</div>
<div class="eq-band">
<input type="range" id="eq-7500" class="eq-slider" min="-12" max="12" value="0">
<span class="eq-label">7.5kHz</span>
</div>
</div>
<div class="eq-extras">
<div class="eq-extra">
<label for="bass-boost">Bass Boost</label>
<input type="range" id="bass-boost" class="eq-boost-slider" min="0" max="12" value="0">
<span id="bass-boost-val">0dB</span>
</div>
<div class="eq-extra">
<label for="volume-boost">Volume Boost</label>
<input type="range" id="volume-boost" class="eq-boost-slider" min="0" max="6" value="0">
<span id="volume-boost-val">0dB</span>
</div>
</div>
</div>
<!-- Audio Elements (dual for gapless/crossfade) -->
<audio id="audio-player" preload="auto"></audio>
<audio id="audio-player-2" preload="auto"></audio>
</div>
<!-- Fullscreen Player Overlay (outside .app for proper fixed positioning) -->
<div id="fullscreen-player" class="fullscreen-player hidden">
<div class="fs-backdrop"></div>
<div class="fs-content">
<div class="fs-header">
<button id="fs-close-btn" class="fs-close-btn">×</button>
</div>
<div class="fs-art-container">
<img id="fs-art" src="/static/icon.svg" alt="Album Art">
<button id="fs-lyrics-btn" class="fs-art-lyrics-btn" title="Lyrics">📝</button>
</div>
<div class="fs-info">
<h2 id="fs-title">No Track Playing</h2>
<p id="fs-artist">Select music to play</p>
<div id="fs-dj-info" class="fs-dj-info hidden"></div>
</div>
<div class="fs-progress-container">
<span id="fs-current-time">0:00</span>
<input type="range" id="fs-progress-bar" class="progress-bar" min="0" max="100" value="0" title="Seek">
<span id="fs-duration">0:00</span>
</div>
<div class="fs-controls">
<button id="fs-heart-btn" class="fs-control-btn" title="Add to Playlist">🩷</button>
<button id="fs-prev-btn" class="fs-control-btn"></button>
<button id="fs-play-btn" class="fs-control-btn play-btn"></button>
<button id="fs-next-btn" class="fs-control-btn"></button>
<button id="fs-download-btn" class="fs-control-btn" title="Download"></button>
<button id="fs-visualizer-btn" class="fs-control-btn" title="Visualizer">🌈</button>
</div>
</div>
</div>
<!-- Visualizer Overlay -->
<div id="visualizer-overlay" class="visualizer-overlay hidden">
<canvas id="visualizer-canvas"></canvas>
<canvas id="visualizer-canvas-webgl" class="hidden"></canvas>
<div class="visualizer-controls">
<div class="visualizer-track-info">
<span id="viz-track-name">No Track</span>
<span id="viz-track-artist"></span>
</div>
<div class="visualizer-mode-selector">
<button id="viz-prev-preset" class="viz-action-btn" title="Previous Preset (P)" style="display: none;">⏮ Prev</button>
<button class="viz-mode-btn" data-mode="milkdrop">MilkDrop</button>
<button id="viz-next-preset" class="viz-action-btn" title="Next Preset (N)" style="display: none;">Next ⏭</button>
<button class="viz-mode-btn active" data-mode="bars">Bars</button>
<button class="viz-mode-btn" data-mode="wave">Wave</button>
<button class="viz-mode-btn" data-mode="particles">Particles</button>
</div>
<button id="visualizer-close" class="visualizer-close-btn">✕ Exit</button>
</div>
<div class="visualizer-hint">Press ESC or click to exit</div>
</div>
<!-- Podcast Episode Details Modal -->
<div id="podcast-modal" class="podcast-modal hidden">
<div class="podcast-modal-content">
<button id="podcast-modal-close" class="podcast-modal-close">×</button>
<img id="podcast-modal-art" class="podcast-modal-art" src="" alt="Episode Art">
<h2 id="podcast-modal-title" class="podcast-modal-title"></h2>
<p id="podcast-modal-date" class="podcast-modal-date"></p>
<p id="podcast-modal-duration" class="podcast-modal-duration"></p>
<div id="podcast-modal-description" class="podcast-modal-description"></div>
<div class="podcast-modal-actions">
<button id="podcast-modal-play" class="podcast-modal-play">▶ Play Episode</button>
</div>
</div>
</div>
<!-- Lyrics Modal -->
<div id="lyrics-modal" class="lyrics-modal hidden">
<div class="lyrics-modal-content">
<button id="lyrics-modal-close" class="lyrics-modal-close">×</button>
<div class="lyrics-modal-header">
<img id="lyrics-modal-art" class="lyrics-modal-art" src="" alt="Album Art">
<div class="lyrics-modal-info">
<h2 id="lyrics-modal-title">Song Title</h2>
<p id="lyrics-modal-artist">Artist</p>
<p id="lyrics-modal-album" class="lyrics-modal-album"></p>
</div>
</div>
<div class="lyrics-tabs">
<button class="lyrics-tab active" data-tab="lyrics">Lyrics</button>
<button class="lyrics-tab" data-tab="about">About</button>
<button class="lyrics-tab" data-tab="annotations">Annotations</button>
</div>
<div class="lyrics-tab-content">
<div id="lyrics-panel" class="lyrics-panel active">
<div id="lyrics-loading" class="lyrics-loading hidden">
<div class="lyrics-spinner"></div>
<p>Fetching lyrics...</p>
</div>
<div id="lyrics-text" class="lyrics-text"></div>
<div id="lyrics-not-found" class="lyrics-not-found hidden">
<p>😢 Lyrics not found for this track</p>
<a id="lyrics-search-link" href="#" target="_blank" class="lyrics-search-link">Search on Genius →</a>
</div>
</div>
<div id="about-panel" class="lyrics-panel">
<div id="about-content" class="about-content">
<div id="about-description" class="about-description"></div>
<div id="about-credits" class="about-credits">
<p id="about-release"></p>
<p id="about-writers"></p>
<p id="about-producers"></p>
</div>
<a id="genius-link" href="#" target="_blank" class="genius-link">View on Genius →</a>
</div>
</div>
<div id="annotations-panel" class="lyrics-panel">
<div id="annotations-loading" class="lyrics-loading hidden">
<div class="lyrics-spinner"></div>
<p>Loading annotations...</p>
</div>
<div id="annotations-list" class="annotations-list"></div>
<div id="annotations-empty" class="lyrics-not-found hidden">
<p>No annotations available for this track</p>
</div>
</div>
</div>
</div>
</div>
<!-- Concerts Modal -->
<div id="concerts-modal" class="concerts-modal hidden">
<div class="concerts-modal-content">
<button id="concerts-modal-close" class="concerts-modal-close">×</button>
<div class="concerts-modal-header">
<h2>🎤 Upcoming Concerts</h2>
</div>
<div class="concerts-settings">
<label>My Cities (comma-separated):</label>
<input type="text" id="concerts-cities" class="concerts-cities-input" placeholder="San Francisco, Los Angeles, Seattle...">
<button id="concerts-save-cities" class="concerts-save-btn">Save</button>
</div>
<div class="concerts-tabs">
<button class="concerts-tab active" data-source="queue">From Queue</button>
<button class="concerts-tab" data-source="search">Search Artist</button>
</div>
<div id="concerts-search-section" class="concerts-search-section hidden">
<input type="text" id="concerts-artist-search" class="concerts-artist-input" placeholder="Search artist...">
<button id="concerts-search-btn" class="concerts-search-btn">🔍</button>
</div>
<div id="concerts-loading" class="lyrics-loading hidden">
<div class="lyrics-spinner"></div>
<p>Finding concerts...</p>
</div>
<div id="concerts-list" class="concerts-list"></div>
<div id="concerts-empty" class="concerts-empty hidden">
<p>No upcoming concerts found</p>
<p class="concerts-empty-hint">Try adding more artists to your queue or adjusting your cities</p>
</div>
</div>
</div>
<!-- DJ Setlist Modal -->
<div id="dj-setlist-modal" class="dj-modal hidden">
<div class="dj-modal-content">
<div class="dj-modal-header">
<h2>🎧 AI DJ Setlist</h2>
<button id="dj-modal-close" class="dj-modal-close">×</button>
</div>
<div class="dj-modal-body">
<div class="dj-style-selector">
<label>Set Style:</label>
<select id="dj-style-select">
<option value="progressive">Progressive (Build Energy)</option>
<option value="peak-time">Peak Time (High Energy)</option>
<option value="chill">Chill (Low-Med Energy)</option>
<option value="journey">Journey (Wave Pattern)</option>
</select>
</div>
<div id="dj-setlist-loading" class="dj-loading hidden">
<div class="spinner"></div>
<p>Analyzing tracks and generating setlist...</p>
</div>
<div id="dj-setlist-results" class="dj-results hidden">
<div id="dj-ordered-tracks" class="dj-ordered-tracks"></div>
</div>
</div>
<div class="dj-modal-actions">
<button id="dj-generate-btn" class="btn-primary">✨ Generate Setlist</button>
<button id="dj-apply-btn" class="btn-secondary hidden">Apply to Queue</button>
</div>
</div>
</div>
<!-- Hidden File Input (Moved to body end for reliability) -->
<input
type="file"
id="file-input"
multiple
accept="audio/*,.flac,.mp3,.wav,.aiff,.aac,.ogg,.m4a"
style="position: fixed; top: -100px; left: -100px; opacity: 0; pointer-events: none;"
onclick="this.value=null"
onchange="if(window.handleFiles) { window.handleFiles(this.files); } else { alert('Error: handleFiles not loaded'); }"
>
<!-- Playlist Selection Modal -->
<div id="playlist-modal" class="modal hidden">
<div class="modal-content playlist-modal-content">
<h3>Add to Playlist</h3>
<div id="playlist-list" class="playlist-list"></div>
<div class="create-playlist-row">
<input type="text" id="new-playlist-input" placeholder="New playlist name..." class="new-playlist-input">
<button id="create-playlist-btn" class="btn-primary">+</button>
</div>
<button id="playlist-modal-close" class="btn-secondary" style="width:100%; margin-top:12px;">Cancel</button>
</div>
</div>
<!-- Setlist Detail Modal -->
<div id="setlist-modal" class="modal hidden">
<div class="modal-content setlist-modal-content" style="max-height: 80vh; overflow-y: auto;">
<div class="modal-header">
<h3>Setlist</h3>
<button id="setlist-close-btn" class="modal-close-btn">×</button>
</div>
<div id="setlist-info" class="setlist-header-info">
<!-- Artist at Venue - Date inserted here -->
</div>
<div id="setlist-tracks" class="setlist-tracks-list">
<!-- Tracks inserted here -->
</div>
<div class="modal-actions" style="margin-top: 16px;">
<button id="setlist-play-btn" class="btn-primary" style="width: 100%;">
🎧 Listen to Show
</button>
</div>
</div>
</div>
<!-- Drive Sync Modal -->
<div id="drive-sync-modal" class="modal hidden">
<div class="modal-content drive-modal-content">
<div class="modal-header">
<h2>Google Drive Sync</h2>
<button id="drive-modal-close-top" class="modal-close-btn">×</button>
</div>
<!-- Auth Section -->
<div id="drive-auth-section">
<p style="margin-bottom: 20px; color: var(--text-secondary);">Sign in to sync your library across devices.</p>
<div class="drive-signin-container">
<button id="drive-signin-btn" class="drive-auth-btn">
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M21.35 11.1h-9.17v2.73h6.51c-.33 3.81-3.5 5.44-6.5 5.44C8.36 19.27 5 16.25 5 12c0-4.1 3.2-7.27 7.2-7.27 3.09 0 4.9 1.97 4.9 1.97L19 4.72S16.56 2 12.1 2C6.42 2 2.03 6.8 2.03 12c0 5.05 4.13 10 10.22 10 5.35 0 9.25-3.67 9.25-9.09 0-1.15-.15-1.81-.15-1.81z"/></svg>
Sign in with Google
</button>
<div id="drive-loading" class="hidden">Connecting...</div>
</div>
</div>
<!-- Options Section -->
<div id="drive-options-section" class="hidden">
<div class="drive-user-info">
<span id="drive-user-email">Connected</span>
<button id="drive-signout-btn" class="text-link">Sign Out</button>
</div>
<div class="drive-actions-grid">
<!-- Upload Group -->
<div class="sync-group upload-group">
<h3>☁️ Upload (Save)</h3>
<p class="sync-desc">Save your current library to the cloud.</p>
<div class="action-buttons">
<button id="drive-up-all" class="action-btn upload">
<span class="btn-icon">⬆️</span>
<span class="btn-text">Everything</span>
</button>
<button id="drive-up-playlists" class="action-btn upload secondary">
<span class="btn-text">Playlists Only</span>
</button>
<button id="drive-up-queue" class="action-btn upload secondary">
<span class="btn-text">Queue Only</span>
</button>
</div>
</div>
<!-- Download Group -->
<div class="sync-group download-group">
<h3>⬇️ Download (Load)</h3>
<p class="sync-desc">Restore your library from the cloud.</p>
<div class="action-buttons">
<button id="drive-down-all" class="action-btn download">
<span class="btn-icon">⬇️</span>
<span class="btn-text">Everything</span>
</button>
<button id="drive-down-playlists" class="action-btn download secondary">
<span class="btn-text">Playlists Only</span>
</button>
<button id="drive-down-queue" class="action-btn download secondary">
<span class="btn-text">Queue Only</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Concert Alerts Modal -->
<div id="concert-modal" class="modal hidden">
<div class="modal-content concert-modal-content">
<div class="modal-header">
<h3>🎫 Concert Alerts</h3>
<button class="modal-close" id="concert-modal-close">×</button>
</div>
<!-- Tabs: Recent Artists | Search -->
<div class="concert-tabs">
<button class="concert-tab active" data-tab="recent">Recent Artists</button>
<button class="concert-tab" data-tab="search">Search Artist</button>
</div>
<!-- Recent Artists Tab (default) -->
<div id="concert-recent-section" class="concert-tab-content">
<p class="concert-hint">Showing concerts for artists you've listened to recently</p>
</div>
<!-- Search Tab -->
<div id="concert-search-section" class="concert-tab-content hidden">
<div class="concert-search-wrapper">
<input type="text" id="concert-artist-search" placeholder="Search for an artist...">
<button id="concert-search-btn" class="btn-primary">Search</button>
</div>
</div>
<!-- Results -->
<div id="concert-results" class="concert-results"></div>
<div id="concert-loading" class="concert-loading hidden">
<span class="loading-spinner"></span> Finding concerts...
</div>
<div id="concert-empty" class="concert-empty hidden">
<span>🎵</span>
<p>No upcoming concerts found</p>
</div>
</div>
</div>
<script src="static/jsmediatags.min.js"></script>
<script src="https://apis.google.com/js/api.js"></script>
<script src="https://accounts.google.com/gsi/client"></script>
<script src="/static/app.js?t=1736053100"></script></script>
</body>
</html>

95
static/jsmediatags.min.js vendored Normal file
View file

@ -0,0 +1,95 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(v,h,p){v!=Array.prototype&&v!=Object.prototype&&(v[h]=p.value)};$jscomp.getGlobal=function(v){return"undefined"!=typeof window&&window===v?v:"undefined"!=typeof global&&null!=global?global:v};$jscomp.global=$jscomp.getGlobal(this);
$jscomp.polyfill=function(v,h,p,n){if(h){p=$jscomp.global;v=v.split(".");for(n=0;n<v.length-1;n++){var m=v[n];m in p||(p[m]={});p=p[m]}v=v[v.length-1];n=p[v];h=h(n);h!=n&&null!=h&&$jscomp.defineProperty(p,v,{configurable:!0,writable:!0,value:h})}};$jscomp.polyfill("Object.setPrototypeOf",function(v){return v?v:"object"!=typeof"".__proto__?null:function(h,p){h.__proto__=p;if(h.__proto__!==p)throw new TypeError(h+" is not extensible");return h}},"es6","es5");
(function(v){"object"===typeof exports&&"undefined"!==typeof module?module.exports=v():"function"===typeof define&&define.amd?define([],v):("undefined"!==typeof window?window:"undefined"!==typeof global?global:"undefined"!==typeof self?self:this).jsmediatags=v()})(function(){return function h(p,n,m){function q(c,b){if(!n[c]){if(!p[c]){var g="function"==typeof require&&require;if(!b&&g)return g(c,!0);if(t)return t(c,!0);b=Error("Cannot find module '"+c+"'");throw b.code="MODULE_NOT_FOUND",b;}b=n[c]=
{exports:{}};p[c][0].call(b.exports,function(b){var k=p[c][1][b];return q(k?k:b)},b,b.exports,h,p,n,m)}return n[c].exports}for(var t="function"==typeof require&&require,f=0;f<m.length;f++)q(m[f]);return q}({1:[function(h,p,n){},{}],2:[function(h,p,n){p.exports=XMLHttpRequest},{}],3:[function(h,p,n){function m(b){m="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"===typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?
"symbol":typeof a};return m(b)}function q(b,a){for(var e=0;e<a.length;e++){var d=a[e];d.enumerable=d.enumerable||!1;d.configurable=!0;"value"in d&&(d.writable=!0);Object.defineProperty(b,d.key,d)}}function t(b,a,e){a&&q(b.prototype,a);e&&q(b,e);return b}function f(b){f=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return f(b)}function c(b){if(void 0===b)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return b}
function b(b,a){if("function"!==typeof a&&null!==a)throw new TypeError("Super expression must either be null or a function");b.prototype=Object.create(a&&a.prototype,{constructor:{value:b,writable:!0,configurable:!0}});a&&g(b,a)}function g(b,a){g=Object.setPrototypeOf||function(a,d){a.__proto__=d;return a};return g(b,a)}function k(b,a,e){a in b?Object.defineProperty(b,a,{value:e,enumerable:!0,configurable:!0,writable:!0}):b[a]=e;return b}h=function(g){function a(e){if(!(this instanceof a))throw new TypeError("Cannot call a class as a function");
var d=f(a).call(this);d=!d||"object"!==m(d)&&"function"!==typeof d?c(this):d;k(c(d),"_array",void 0);k(c(d),"_size",void 0);d._array=e;d._size=e.length;d._isInitialized=!0;return d}b(a,g);t(a,[{key:"init",value:function(a){setTimeout(a.onSuccess,0)}},{key:"loadRange",value:function(a,d){setTimeout(d.onSuccess,0)}},{key:"getByteAt",value:function(a){if(a>=this._array.length)throw Error("Offset "+a+" hasn't been loaded yet.");return this._array[a]}}],[{key:"canReadFile",value:function(a){return Array.isArray(a)||
"function"===typeof Buffer&&Buffer.isBuffer(a)}}]);return a}(h("./MediaFileReader"));p.exports=h},{"./MediaFileReader":11}],4:[function(h,p,n){function m(a){m="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"===typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};return m(a)}function q(a,e){for(var d=0;d<e.length;d++){var l=e[d];l.enumerable=l.enumerable||!1;l.configurable=!0;"value"in l&&(l.writable=
!0);Object.defineProperty(a,l.key,l)}}function t(a,e,d){e&&q(a.prototype,e);d&&q(a,d);return a}function f(a){f=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return f(a)}function c(a){if(void 0===a)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return a}function b(a,e){if("function"!==typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");a.prototype=Object.create(e&&
e.prototype,{constructor:{value:a,writable:!0,configurable:!0}});e&&g(a,e)}function g(a,e){g=Object.setPrototypeOf||function(d,a){d.__proto__=a;return d};return g(a,e)}function k(a,e,d){e in a?Object.defineProperty(a,e,{value:d,enumerable:!0,configurable:!0,writable:!0}):a[e]=d;return a}var r=h("./ChunkedFileData");h=function(a){function e(d){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function");var a=f(e).call(this);a=!a||"object"!==m(a)&&"function"!==typeof a?c(this):
a;k(c(a),"_blob",void 0);k(c(a),"_fileData",void 0);a._blob=d;a._fileData=new r;return a}b(e,a);t(e,[{key:"_init",value:function(d){this._size=this._blob.size;setTimeout(d.onSuccess,1)}},{key:"loadRange",value:function(d,a){var e=this,l=(this._blob.slice||this._blob.mozSlice||this._blob.webkitSlice).call(this._blob,d[0],d[1]+1),b=new FileReader;b.onloadend=function(l){l=new Uint8Array(b.result);e._fileData.addData(d[0],l);a.onSuccess()};b.onerror=b.onabort=function(d){if(a.onError)a.onError({type:"blob",
info:b.error})};b.readAsArrayBuffer(l)}},{key:"getByteAt",value:function(a){return this._fileData.getByteAt(a)}}],[{key:"canReadFile",value:function(a){return"undefined"!==typeof Blob&&a instanceof Blob||"undefined"!==typeof File&&a instanceof File}}]);return e}(h("./MediaFileReader"));p.exports=h},{"./ChunkedFileData":5,"./MediaFileReader":11}],5:[function(h,p,n){function m(h,f){for(var c=0;c<f.length;c++){var b=f[c];b.enumerable=b.enumerable||!1;b.configurable=!0;"value"in b&&(b.writable=!0);Object.defineProperty(h,
b.key,b)}}function q(h,f,c){f&&m(h.prototype,f);c&&m(h,c);return h}h=function(){function h(){if(!(this instanceof h))throw new TypeError("Cannot call a class as a function");"_fileData"in this?Object.defineProperty(this,"_fileData",{value:void 0,enumerable:!0,configurable:!0,writable:!0}):this._fileData=void 0;this._fileData=[]}q(h,null,[{key:"NOT_FOUND",get:function(){return-1}}]);q(h,[{key:"addData",value:function(f,c){var b=f+c.length-1,g=this._getChunkRange(f,b);if(-1===g.startIx)this._fileData.splice(g.insertIx||
0,0,{offset:f,data:c});else{var k=this._fileData[g.startIx],r=this._fileData[g.endIx];b=b<r.offset+r.data.length-1;var a={offset:Math.min(f,k.offset),data:c};f>k.offset&&(f=this._sliceData(k.data,0,f-k.offset),a.data=this._concatData(f,c));b&&(f=this._sliceData(a.data,0,r.offset-a.offset),a.data=this._concatData(f,r.data));this._fileData.splice(g.startIx,g.endIx-g.startIx+1,a)}}},{key:"_concatData",value:function(f,c){if("undefined"!==typeof ArrayBuffer&&ArrayBuffer.isView&&ArrayBuffer.isView(f)){var b=
new f.constructor(f.length+c.length);b.set(f,0);b.set(c,f.length);return b}return f.concat(c)}},{key:"_sliceData",value:function(f,c,b){return f.slice?f.slice(c,b):f.subarray(c,b)}},{key:"_getChunkRange",value:function(f,c){for(var b,g,k=-1,r=-1,a=0,e=0;e<this._fileData.length;e++,a=e){g=this._fileData[e].offset;b=g+this._fileData[e].data.length;if(c<g-1)break;if(f<=b+1&&c>=g-1){k=e;break}}if(-1===k)return{startIx:-1,endIx:-1,insertIx:a};for(e=k;e<this._fileData.length&&!(g=this._fileData[e].offset,
b=g+this._fileData[e].data.length,c>=g-1&&(r=e),c<=b+1);e++);-1===r&&(r=k);return{startIx:k,endIx:r}}},{key:"hasDataRange",value:function(f,c){for(var b=0;b<this._fileData.length;b++){var g=this._fileData[b];if(c<g.offset)break;if(f>=g.offset&&c<g.offset+g.data.length)return!0}return!1}},{key:"getByteAt",value:function(f){for(var c,b=0;b<this._fileData.length;b++){var g=this._fileData[b].offset,k=g+this._fileData[b].data.length-1;if(f>=g&&f<=k){c=this._fileData[b];break}}if(c)return c.data[f-c.offset];
throw Error("Offset "+f+" hasn't been loaded yet.");}}]);return h}();p.exports=h},{}],6:[function(h,p,n){function m(a){m="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"===typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};return m(a)}function q(a,e){for(var d=0;d<e.length;d++){var b=e[d];b.enumerable=b.enumerable||!1;b.configurable=!0;"value"in b&&(b.writable=!0);Object.defineProperty(a,b.key,b)}}
function t(a,e,b){e&&q(a.prototype,e);b&&q(a,b);return a}function f(a){f=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return f(a)}function c(a){if(void 0===a)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return a}function b(a,e){if("function"!==typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");a.prototype=Object.create(e&&e.prototype,{constructor:{value:a,writable:!0,
configurable:!0}});e&&g(a,e)}function g(a,e){g=Object.setPrototypeOf||function(a,d){a.__proto__=d;return a};return g(a,e)}function k(a,e,b){e in a?Object.defineProperty(a,e,{value:b,enumerable:!0,configurable:!0,writable:!0}):a[e]=b;return a}var r=[4,132],a=[6,134],e="Other;32x32 pixels 'file icon' (PNG only);Other file icon;Cover (front);Cover (back);Leaflet page;Media (e.g. label side of CD);Lead artist/lead performer/soloist;Artist/performer;Conductor;Band/Orchestra;Composer;Lyricist/text writer;Recording Location;During recording;During performance;Movie/video screen capture;A bright coloured fish;Illustration;Band/artist logotype;Publisher/Studio logotype".split(";");
h=function(d){function l(){var a;if(!(this instanceof l))throw new TypeError("Cannot call a class as a function");for(var d=arguments.length,e=Array(d),b=0;b<d;b++)e[b]=arguments[b];d=(a=f(l)).call.apply(a,[this].concat(e));a=!d||"object"!==m(d)&&"function"!==typeof d?c(this):d;k(c(a),"_commentOffset",void 0);k(c(a),"_pictureOffset",void 0);return a}b(l,d);t(l,[{key:"_loadData",value:function(a,d){var e=this;a.loadRange([4,7],{onSuccess:function(){e._loadBlock(a,4,d)}})}},{key:"_loadBlock",value:function(d,
e,b){var l=this,c=d.getByteAt(e),k=d.getInteger24At(e+1,!0);if(-1!==r.indexOf(c)){var g=e+4;d.loadRange([g,g+k],{onSuccess:function(){l._commentOffset=g;l._nextBlock(d,e,c,k,b)}})}else-1!==a.indexOf(c)?(g=e+4,d.loadRange([g,g+k],{onSuccess:function(){l._pictureOffset=g;l._nextBlock(d,e,c,k,b)}})):l._nextBlock(d,e,c,k,b)}},{key:"_nextBlock",value:function(a,d,e,b,l){var c=this;if(127<e)if(c._commentOffset)l.onSuccess();else l.onError({type:"loadData",info:"Comment block could not be found."});else a.loadRange([d+
4+b,d+4+4+b],{onSuccess:function(){c._loadBlock(a,d+4+b,l)}})}},{key:"_parseData",value:function(a,d){var b=a.getLongAt(this._commentOffset,!1)+(this._commentOffset+4);d=a.getLongAt(b,!1);b+=4;for(var l,c,k,g,u,r,f=0;f<d;f++){var w=a.getLongAt(b,!1),h=a.getStringWithCharsetAt(b+4,w,"utf-8").toString(),m=h.indexOf("=");h=[h.slice(0,m),h.slice(m+1)];switch(h[0].toUpperCase()){case "TITLE":l=h[1];break;case "ARTIST":c=h[1];break;case "ALBUM":k=h[1];break;case "TRACKNUMBER":g=h[1];break;case "GENRE":u=
h[1]}b+=4+w}this._pictureOffset&&(r=a.getLongAt(this._pictureOffset,!0),d=this._pictureOffset+4,b=a.getLongAt(d,!0),f=d+4,d=a.getStringAt(f,b),b=f+b,f=a.getLongAt(b,!0),w=b+4,b=a.getStringWithCharsetAt(w,f,"utf-8").toString(),f=w+f+16,w=a.getLongAt(f,!0),a=a.getBytesAt(f+4,w,!0),r={format:d,type:e[r],description:b,data:a});return{type:"FLAC",version:"1",tags:{title:l,artist:c,album:k,track:g,genre:u,picture:r}}}}],[{key:"getTagIdentifierByteRange",value:function(){return{offset:0,length:4}}},{key:"canReadTagFormat",
value:function(a){return"fLaC"===String.fromCharCode.apply(String,a.slice(0,4))}}]);return l}(h("./MediaTagReader"));p.exports=h},{"./MediaTagReader":12}],7:[function(h,p,n){function m(b){m="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(b){return typeof b}:function(b){return b&&"function"===typeof Symbol&&b.constructor===Symbol&&b!==Symbol.prototype?"symbol":typeof b};return m(b)}function q(b,c){for(var a=0;a<c.length;a++){var e=c[a];e.enumerable=e.enumerable||!1;e.configurable=
!0;"value"in e&&(e.writable=!0);Object.defineProperty(b,e.key,e)}}function t(b,c,a){c&&q(b.prototype,c);a&&q(b,a);return b}function f(b){f=Object.setPrototypeOf?Object.getPrototypeOf:function(b){return b.__proto__||Object.getPrototypeOf(b)};return f(b)}function c(c,g){if("function"!==typeof g&&null!==g)throw new TypeError("Super expression must either be null or a function");c.prototype=Object.create(g&&g.prototype,{constructor:{value:c,writable:!0,configurable:!0}});g&&b(c,g)}function b(c,g){b=Object.setPrototypeOf||
function(a,e){a.__proto__=e;return a};return b(c,g)}n=h("./MediaTagReader");h("./MediaFileReader");h=function(b){function k(){if(!(this instanceof k))throw new TypeError("Cannot call a class as a function");var a=f(k).apply(this,arguments);if(!a||"object"!==m(a)&&"function"!==typeof a){if(void 0===this)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");a=this}return a}c(k,b);t(k,[{key:"_loadData",value:function(a,e){var d=a.getSize();a.loadRange([d-128,d-1],e)}},
{key:"_parseData",value:function(a,e){var d=a.getSize()-128,b=a.getStringWithCharsetAt(d+3,30).toString(),c=a.getStringWithCharsetAt(d+33,30).toString(),k=a.getStringWithCharsetAt(d+63,30).toString(),f=a.getStringWithCharsetAt(d+93,4).toString();var h=a.getByteAt(d+97+28);e=a.getByteAt(d+97+29);if(0==h&&0!=e){var r="1.1";h=a.getStringWithCharsetAt(d+97,28).toString()}else r="1.0",h=a.getStringWithCharsetAt(d+97,30).toString(),e=0;a=a.getByteAt(d+97+30);a={type:"ID3",version:r,tags:{title:b,artist:c,
album:k,year:f,comment:h,genre:255>a?g[a]:""}};e&&(a.tags.track=e);return a}}],[{key:"getTagIdentifierByteRange",value:function(){return{offset:-128,length:128}}},{key:"canReadTagFormat",value:function(a){return"TAG"===String.fromCharCode.apply(String,a.slice(0,3))}}]);return k}(n);var g="Blues;Classic Rock;Country;Dance;Disco;Funk;Grunge;Hip-Hop;Jazz;Metal;New Age;Oldies;Other;Pop;R&B;Rap;Reggae;Rock;Techno;Industrial;Alternative;Ska;Death Metal;Pranks;Soundtrack;Euro-Techno;Ambient;Trip-Hop;Vocal;Jazz+Funk;Fusion;Trance;Classical;Instrumental;Acid;House;Game;Sound Clip;Gospel;Noise;AlternRock;Bass;Soul;Punk;Space;Meditative;Instrumental Pop;Instrumental Rock;Ethnic;Gothic;Darkwave;Techno-Industrial;Electronic;Pop-Folk;Eurodance;Dream;Southern Rock;Comedy;Cult;Gangsta;Top 40;Christian Rap;Pop/Funk;Jungle;Native American;Cabaret;New Wave;Psychadelic;Rave;Showtunes;Trailer;Lo-Fi;Tribal;Acid Punk;Acid Jazz;Polka;Retro;Musical;Rock & Roll;Hard Rock;Folk;Folk-Rock;National Folk;Swing;Fast Fusion;Bebob;Latin;Revival;Celtic;Bluegrass;Avantgarde;Gothic Rock;Progressive Rock;Psychedelic Rock;Symphonic Rock;Slow Rock;Big Band;Chorus;Easy Listening;Acoustic;Humour;Speech;Chanson;Opera;Chamber Music;Sonata;Symphony;Booty Bass;Primus;Porn Groove;Satire;Slow Jam;Club;Tango;Samba;Folklore;Ballad;Power Ballad;Rhythmic Soul;Freestyle;Duet;Punk Rock;Drum Solo;Acapella;Euro-House;Dance Hall".split(";");
p.exports=h},{"./MediaFileReader":11,"./MediaTagReader":12}],8:[function(h,p,n){function m(a,e){for(var d=0;d<e.length;d++){var b=e[d];b.enumerable=b.enumerable||!1;b.configurable=!0;"value"in b&&(b.writable=!0);Object.defineProperty(a,b.key,b)}}function q(a,e,d){e&&m(a.prototype,e);d&&m(a,d);return a}function t(a){switch(a){case 0:a="iso-8859-1";break;case 1:a="utf-16";break;case 2:a="utf-16be";break;case 3:a="utf-8";break;default:a="iso-8859-1"}return a}function f(a,e,d,b){var l=d.getStringWithCharsetAt(a+
1,e-1,b);a=d.getStringWithCharsetAt(a+1+l.bytesReadCount,e-1-l.bytesReadCount,b);return{user_description:l.toString(),data:a.toString()}}h("./MediaFileReader");var c=h("./StringUtils"),b=h("./ArrayFileReader"),g={BUF:"Recommended buffer size",CNT:"Play counter",COM:"Comments",CRA:"Audio encryption",CRM:"Encrypted meta frame",ETC:"Event timing codes",EQU:"Equalization",GEO:"General encapsulated object",IPL:"Involved people list",LNK:"Linked information",MCI:"Music CD Identifier",MLL:"MPEG location lookup table",
PIC:"Attached picture",POP:"Popularimeter",REV:"Reverb",RVA:"Relative volume adjustment",SLT:"Synchronized lyric/text",STC:"Synced tempo codes",TAL:"Album/Movie/Show title",TBP:"BPM (Beats Per Minute)",TCM:"Composer",TCO:"Content type",TCR:"Copyright message",TDA:"Date",TDY:"Playlist delay",TEN:"Encoded by",TFT:"File type",TIM:"Time",TKE:"Initial key",TLA:"Language(s)",TLE:"Length",TMT:"Media type",TOA:"Original artist(s)/performer(s)",TOF:"Original filename",TOL:"Original Lyricist(s)/text writer(s)",
TOR:"Original release year",TOT:"Original album/Movie/Show title",TP1:"Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",TP2:"Band/Orchestra/Accompaniment",TP3:"Conductor/Performer refinement",TP4:"Interpreted, remixed, or otherwise modified by",TPA:"Part of a set",TPB:"Publisher",TRC:"ISRC (International Standard Recording Code)",TRD:"Recording dates",TRK:"Track number/Position in set",TSI:"Size",TSS:"Software/hardware and settings used for encoding",TT1:"Content group description",TT2:"Title/Songname/Content description",
TT3:"Subtitle/Description refinement",TXT:"Lyricist/text writer",TXX:"User defined text information frame",TYE:"Year",UFI:"Unique file identifier",ULT:"Unsychronized lyric/text transcription",WAF:"Official audio file webpage",WAR:"Official artist/performer webpage",WAS:"Official audio source webpage",WCM:"Commercial information",WCP:"Copyright/Legal information",WPB:"Publishers official webpage",WXX:"User defined URL link frame",AENC:"Audio encryption",APIC:"Attached picture",ASPI:"Audio seek point index",
CHAP:"Chapter",CTOC:"Table of contents",COMM:"Comments",COMR:"Commercial frame",ENCR:"Encryption method registration",EQU2:"Equalisation (2)",EQUA:"Equalization",ETCO:"Event timing codes",GEOB:"General encapsulated object",GRID:"Group identification registration",IPLS:"Involved people list",LINK:"Linked information",MCDI:"Music CD identifier",MLLT:"MPEG location lookup table",OWNE:"Ownership frame",PRIV:"Private frame",PCNT:"Play counter",POPM:"Popularimeter",POSS:"Position synchronisation frame",
RBUF:"Recommended buffer size",RVA2:"Relative volume adjustment (2)",RVAD:"Relative volume adjustment",RVRB:"Reverb",SEEK:"Seek frame",SYLT:"Synchronized lyric/text",SYTC:"Synchronized tempo codes",TALB:"Album/Movie/Show title",TBPM:"BPM (beats per minute)",TCOM:"Composer",TCON:"Content type",TCOP:"Copyright message",TDAT:"Date",TDLY:"Playlist delay",TDRC:"Recording time",TDRL:"Release time",TDTG:"Tagging time",TENC:"Encoded by",TEXT:"Lyricist/Text writer",TFLT:"File type",TIME:"Time",TIPL:"Involved people list",
TIT1:"Content group description",TIT2:"Title/songname/content description",TIT3:"Subtitle/Description refinement",TKEY:"Initial key",TLAN:"Language(s)",TLEN:"Length",TMCL:"Musician credits list",TMED:"Media type",TMOO:"Mood",TOAL:"Original album/movie/show title",TOFN:"Original filename",TOLY:"Original lyricist(s)/text writer(s)",TOPE:"Original artist(s)/performer(s)",TORY:"Original release year",TOWN:"File owner/licensee",TPE1:"Lead performer(s)/Soloist(s)",TPE2:"Band/orchestra/accompaniment",TPE3:"Conductor/performer refinement",
TPE4:"Interpreted, remixed, or otherwise modified by",TPOS:"Part of a set",TPRO:"Produced notice",TPUB:"Publisher",TRCK:"Track number/Position in set",TRDA:"Recording dates",TRSN:"Internet radio station name",TRSO:"Internet radio station owner",TSOA:"Album sort order",TSOP:"Performer sort order",TSOT:"Title sort order",TSIZ:"Size",TSRC:"ISRC (international standard recording code)",TSSE:"Software/Hardware and settings used for encoding",TSST:"Set subtitle",TYER:"Year",TXXX:"User defined text information frame",
UFID:"Unique file identifier",USER:"Terms of use",USLT:"Unsychronized lyric/text transcription",WCOM:"Commercial information",WCOP:"Copyright/Legal information",WOAF:"Official audio file webpage",WOAR:"Official artist/performer webpage",WOAS:"Official audio source webpage",WORS:"Official internet radio station homepage",WPAY:"Payment",WPUB:"Publishers official webpage",WXXX:"User defined URL link frame"};h=function(){function a(){if(!(this instanceof a))throw new TypeError("Cannot call a class as a function");
}q(a,null,[{key:"getFrameReaderFunction",value:function(a){return a in k?k[a]:"T"===a[0]?k["T*"]:"W"===a[0]?k["W*"]:null}},{key:"readFrames",value:function(e,d,b,c,g){for(var l={},k=this._getFrameHeaderSize(c);e<d-k;){var f=this._readFrameHeader(b,e,c),u=f.id;if(!u)break;var h=f.flags,w=f.size,r=e+f.headerSize,m=b;e+=f.headerSize+f.size;if(!g||-1!==g.indexOf(u)){if("MP3e"===u||"\x00MP3"===u||"\x00\x00MP"===u||" MP3"===u)break;h&&h.format.unsynchronisation&&!c.flags.unsynchronisation&&(m=this.getUnsyncFileReader(m,
r,w),r=0,w=m.getSize());h&&h.format.data_length_indicator&&(r+=4,w-=4);h=(f=a.getFrameReaderFunction(u))?f.apply(this,[r,w,m,h,c]):null;r=this._getFrameDescription(u);w={id:u,size:w,description:r,data:h};u in l?(l[u].id&&(l[u]=[l[u]]),l[u].push(w)):l[u]=w}}return l}},{key:"_getFrameHeaderSize",value:function(a){a=a.major;return 2==a?6:3==a||4==a?10:0}},{key:"_readFrameHeader",value:function(a,d,b){var e=b.major,l=null;b=this._getFrameHeaderSize(b);switch(e){case 2:var c=a.getStringAt(d,3);var g=a.getInteger24At(d+
3,!0);break;case 3:c=a.getStringAt(d,4);g=a.getLongAt(d+4,!0);break;case 4:c=a.getStringAt(d,4),g=a.getSynchsafeInteger32At(d+4)}if(c==String.fromCharCode(0,0,0)||c==String.fromCharCode(0,0,0,0))c="";c&&2<e&&(l=this._readFrameFlags(a,d+8));return{id:c||"",size:g||0,headerSize:b||0,flags:l}}},{key:"_readFrameFlags",value:function(a,d){return{message:{tag_alter_preservation:a.isBitSetAt(d,6),file_alter_preservation:a.isBitSetAt(d,5),read_only:a.isBitSetAt(d,4)},format:{grouping_identity:a.isBitSetAt(d+
1,7),compression:a.isBitSetAt(d+1,3),encryption:a.isBitSetAt(d+1,2),unsynchronisation:a.isBitSetAt(d+1,1),data_length_indicator:a.isBitSetAt(d+1,0)}}}},{key:"_getFrameDescription",value:function(a){return a in g?g[a]:"Unknown"}},{key:"getUnsyncFileReader",value:function(a,d,l){a=a.getBytesAt(d,l);for(d=0;d<a.length-1;d++)255===a[d]&&0===a[d+1]&&a.splice(d+1,1);return new b(a)}}]);return a}();var k={APIC:function(a,b,d,l,c){l=a;var e=t(d.getByteAt(a));switch(c&&c.major){case 2:c=d.getStringAt(a+1,
3);a+=4;break;case 3:case 4:c=d.getStringWithCharsetAt(a+1,b-1);a+=1+c.bytesReadCount;break;default:throw Error("Couldn't read ID3v2 major version.");}var g=d.getByteAt(a);g=r[g];e=d.getStringWithCharsetAt(a+1,b-(a-l)-1,e);a+=1+e.bytesReadCount;return{format:c.toString(),type:g,description:e.toString(),data:d.getBytesAt(a,l+b-a)}},CHAP:function(a,b,d,l,g){l=a;var e={},k=c.readNullTerminatedString(d.getBytesAt(a,b));e.id=k.toString();a+=k.bytesReadCount;e.startTime=d.getLongAt(a,!0);a+=4;e.endTime=
d.getLongAt(a,!0);a+=4;e.startOffset=d.getLongAt(a,!0);a+=4;e.endOffset=d.getLongAt(a,!0);a+=4;e.subFrames=this.readFrames(a,a+(b-(a-l)),d,g);return e},CTOC:function(a,b,d,l,g){l=a;var e={childElementIds:[],id:void 0,topLevel:void 0,ordered:void 0,entryCount:void 0,subFrames:void 0},k=c.readNullTerminatedString(d.getBytesAt(a,b));e.id=k.toString();a+=k.bytesReadCount;e.topLevel=d.isBitSetAt(a,1);e.ordered=d.isBitSetAt(a,0);a++;e.entryCount=d.getByteAt(a);a++;for(k=0;k<e.entryCount;k++){var f=c.readNullTerminatedString(d.getBytesAt(a,
b-(a-l)));e.childElementIds.push(f.toString());a+=f.bytesReadCount}e.subFrames=this.readFrames(a,a+(b-(a-l)),d,g);return e},COMM:function(a,b,d,l,c){var e=a,g=t(d.getByteAt(a));l=d.getStringAt(a+1,3);c=d.getStringWithCharsetAt(a+4,b-4,g);a+=4+c.bytesReadCount;a=d.getStringWithCharsetAt(a,e+b-a,g);return{language:l,short_description:c.toString(),text:a.toString()}}};k.COM=k.COMM;k.PIC=function(a,b,d,l,c){return k.APIC(a,b,d,l,c)};k.PCNT=function(a,b,d,l,c){return d.getLongAt(a,!1)};k.CNT=k.PCNT;k["T*"]=
function(a,b,d,l,c){l=t(d.getByteAt(a));return d.getStringWithCharsetAt(a+1,b-1,l).toString()};k.TXXX=function(a,b,d,l,c){l=t(d.getByteAt(a));return f(a,b,d,l)};k.WXXX=function(a,b,d,l,c){if(0===b)return null;l=t(d.getByteAt(a));return f(a,b,d,l)};k["W*"]=function(a,b,d,l,c){return 0===b?null:d.getStringWithCharsetAt(a,b,"iso-8859-1").toString()};k.TCON=function(a,b,d,l){return k["T*"].apply(this,arguments).replace(/^\(\d+\)/,"")};k.TCO=k.TCON;k.USLT=function(a,b,d,l,c){var e=a,g=t(d.getByteAt(a));
l=d.getStringAt(a+1,3);c=d.getStringWithCharsetAt(a+4,b-4,g);a+=4+c.bytesReadCount;a=d.getStringWithCharsetAt(a,e+b-a,g);return{language:l,descriptor:c.toString(),lyrics:a.toString()}};k.ULT=k.USLT;k.UFID=function(a,b,d,l,g){l=c.readNullTerminatedString(d.getBytesAt(a,b));a+=l.bytesReadCount;a=d.getBytesAt(a,b-l.bytesReadCount);return{ownerIdentifier:l.toString(),identifier:a}};var r="Other;32x32 pixels 'file icon' (PNG only);Other file icon;Cover (front);Cover (back);Leaflet page;Media (e.g. label side of CD);Lead artist/lead performer/soloist;Artist/performer;Conductor;Band/Orchestra;Composer;Lyricist/text writer;Recording Location;During recording;During performance;Movie/video screen capture;A bright coloured fish;Illustration;Band/artist logotype;Publisher/Studio logotype".split(";");
p.exports=h},{"./ArrayFileReader":3,"./MediaFileReader":11,"./StringUtils":13}],9:[function(h,p,n){function m(b){m="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"===typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};return m(b)}function q(b,a){for(var e=0;e<a.length;e++){var d=a[e];d.enumerable=d.enumerable||!1;d.configurable=!0;"value"in d&&(d.writable=!0);Object.defineProperty(b,d.key,d)}}function t(b,
a,e){a&&q(b.prototype,a);e&&q(b,e);return b}function f(b){f=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return f(b)}function c(c,a){if("function"!==typeof a&&null!==a)throw new TypeError("Super expression must either be null or a function");c.prototype=Object.create(a&&a.prototype,{constructor:{value:c,writable:!0,configurable:!0}});a&&b(c,a)}function b(c,a){b=Object.setPrototypeOf||function(a,d){a.__proto__=d;return a};return b(c,a)}n=h("./MediaTagReader");
h("./MediaFileReader");var g=h("./ID3v2FrameReader");h=function(b){function a(){if(!(this instanceof a))throw new TypeError("Cannot call a class as a function");var b=f(a).apply(this,arguments);if(!b||"object"!==m(b)&&"function"!==typeof b){if(void 0===this)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");b=this}return b}c(a,b);t(a,[{key:"_loadData",value:function(a,b){a.loadRange([6,9],{onSuccess:function(){a.loadRange([0,10+a.getSynchsafeInteger32At(6)-1],b)},
onError:b.onError})}},{key:"_parseData",value:function(a,b){var d,e=0,c=a.getByteAt(e+3);if(4<c)return{type:"ID3",version:">2.4",tags:{}};var f=a.getByteAt(e+4),h=a.isBitSetAt(e+5,7),r=a.isBitSetAt(e+5,6),m=a.isBitSetAt(e+5,5),p=a.getSynchsafeInteger32At(e+6);e+=10;if(r)if(4===c){var n=a.getSynchsafeInteger32At(e);e+=n}else n=a.getLongAt(e,!0),e+=n+4;n={type:"ID3",version:"2."+c+"."+f,major:c,revision:f,flags:{unsynchronisation:h,extended_header:r,experimental_indicator:m,footer_present:!1},size:p,
tags:{}};b&&(d=this._expandShortcutTags(b));b=p+10;n.flags.unsynchronisation&&(a=g.getUnsyncFileReader(a,e,p),e=0,b=a.getSize());a=g.readFrames(e,b,a,n,d);for(var q in k)k.hasOwnProperty(q)&&(d=this._getFrameData(a,k[q]))&&(n.tags[q]=d);for(var t in a)a.hasOwnProperty(t)&&(n.tags[t]=a[t]);return n}},{key:"_getFrameData",value:function(a,b){for(var d=0,e;e=b[d];d++)if(e in a)return a=a[e]instanceof Array?a[e][0]:a[e],a.data}},{key:"getShortcuts",value:function(){return k}}],[{key:"getTagIdentifierByteRange",
value:function(){return{offset:0,length:10}}},{key:"canReadTagFormat",value:function(a){return"ID3"===String.fromCharCode.apply(String,a.slice(0,3))}}]);return a}(n);var k={title:["TIT2","TT2"],artist:["TPE1","TP1"],album:["TALB","TAL"],year:["TYER","TYE"],comment:["COMM","COM"],track:["TRCK","TRK"],genre:["TCON","TCO"],picture:["APIC","PIC"],lyrics:["USLT","ULT"]};p.exports=h},{"./ID3v2FrameReader":8,"./MediaFileReader":11,"./MediaTagReader":12}],10:[function(h,p,n){function m(a){m="function"===
typeof Symbol&&"symbol"===typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"===typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};return m(a)}function q(a,b){for(var d=0;d<b.length;d++){var e=b[d];e.enumerable=e.enumerable||!1;e.configurable=!0;"value"in e&&(e.writable=!0);Object.defineProperty(a,e.key,e)}}function t(a,b,d){b&&q(a.prototype,b);d&&q(a,d);return a}function f(a){f=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||
Object.getPrototypeOf(a)};return f(a)}function c(a,e){if("function"!==typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");a.prototype=Object.create(e&&e.prototype,{constructor:{value:a,writable:!0,configurable:!0}});e&&b(a,e)}function b(a,e){b=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return b(a,e)}n=h("./MediaTagReader");h("./MediaFileReader");h=function(a){function b(){if(!(this instanceof b))throw new TypeError("Cannot call a class as a function");
var a=f(b).apply(this,arguments);if(!a||"object"!==m(a)&&"function"!==typeof a){if(void 0===this)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");a=this}return a}c(b,a);t(b,[{key:"_loadData",value:function(a,b){var d=this;a.loadRange([0,16],{onSuccess:function(){d._loadAtom(a,0,"",b)},onError:b.onError})}},{key:"_loadAtom",value:function(a,b,e,c){if(b>=a.getSize())c.onSuccess();else{var d=this,l=a.getLongAt(b,!0);if(0==l||isNaN(l))c.onSuccess();else{var g=a.getStringAt(b+
4,4);if(this._isContainerAtom(g)){"meta"==g&&(b+=4);var k=(e?e+".":"")+g;"moov.udta.meta.ilst"===k?a.loadRange([b,b+l],c):a.loadRange([b+8,b+8+8],{onSuccess:function(){d._loadAtom(a,b+8,k,c)},onError:c.onError})}else a.loadRange([b+l,b+l+8],{onSuccess:function(){d._loadAtom(a,b+l,e,c)},onError:c.onError})}}}},{key:"_isContainerAtom",value:function(a){return 0<=["moov","udta","meta","ilst"].indexOf(a)}},{key:"_canReadAtom",value:function(a){return"----"!==a}},{key:"_parseData",value:function(a,b){var d=
{};b=this._expandShortcutTags(b);this._readAtom(d,a,0,a.getSize(),b);for(var e in r)r.hasOwnProperty(e)&&(b=d[r[e]])&&(d[e]="track"===e?b.data.track:b.data);return{type:"MP4",ftyp:a.getStringAt(8,4),version:a.getLongAt(12,!0),tags:d}}},{key:"_readAtom",value:function(a,b,e,c,g,k,f){f=void 0===f?"":f+" ";for(var d=e;d<e+c;){var l=b.getLongAt(d,!0);if(0==l)break;var h=b.getStringAt(d+4,4);if(this._isContainerAtom(h)){"meta"==h&&(d+=4);this._readAtom(a,b,d+8,l-8,g,(k?k+".":"")+h,f);break}(!g||0<=g.indexOf(h))&&
"moov.udta.meta.ilst"===k&&this._canReadAtom(h)&&(a[h]=this._readMetadataAtom(b,d));d+=l}}},{key:"_readMetadataAtom",value:function(a,b){var d=a.getLongAt(b,!0),e=a.getStringAt(b+4,4),c=a.getInteger24At(b+16+1,!0);c=g[c];if("trkn"==e)var l={track:a.getByteAt(b+16+11),total:a.getByteAt(b+16+13)};else if("disk"==e)l={disk:a.getByteAt(b+16+11),total:a.getByteAt(b+16+13)};else{b+=24;var f=d-24;"covr"===e&&"uint8"===c&&(c="jpeg");switch(c){case "text":l=a.getStringWithCharsetAt(b,f,"utf-8").toString();
break;case "uint8":l=a.getShortAt(b,!1);break;case "int":case "uint":l=("int"==c?1==f?a.getSByteAt:2==f?a.getSShortAt:4==f?a.getSLongAt:a.getLongAt:1==f?a.getByteAt:2==f?a.getShortAt:a.getLongAt).call(a,b+(8==f?4:0),!0);break;case "jpeg":case "png":l={format:"image/"+c,data:a.getBytesAt(b,f)}}}return{id:e,size:d,description:k[e]||"Unknown",data:l}}},{key:"getShortcuts",value:function(){return r}}],[{key:"getTagIdentifierByteRange",value:function(){return{offset:0,length:16}}},{key:"canReadTagFormat",
value:function(a){return"ftyp"===String.fromCharCode.apply(String,a.slice(4,8))}}]);return b}(n);var g={0:"uint8",1:"text",13:"jpeg",14:"png",21:"int",22:"uint"},k={"\u00a9alb":"Album","\u00a9ART":"Artist",aART:"Album Artist","\u00a9day":"Release Date","\u00a9nam":"Title","\u00a9gen":"Genre",gnre:"Genre",trkn:"Track Number","\u00a9wrt":"Composer","\u00a9too":"Encoding Tool","\u00a9enc":"Encoded By",cprt:"Copyright",covr:"Cover Art","\u00a9grp":"Grouping",keyw:"Keywords","\u00a9lyr":"Lyrics","\u00a9cmt":"Comment",
tmpo:"Tempo",cpil:"Compilation",disk:"Disc Number",tvsh:"TV Show Name",tven:"TV Episode ID",tvsn:"TV Season",tves:"TV Episode",tvnn:"TV Network",desc:"Description",ldes:"Long Description",sonm:"Sort Name",soar:"Sort Artist",soaa:"Sort Album",soco:"Sort Composer",sosn:"Sort Show",purd:"Purchase Date",pcst:"Podcast",purl:"Podcast URL",catg:"Category",hdvd:"HD Video",stik:"Media Type",rtng:"Content Rating",pgap:"Gapless Playback",apID:"Purchase Account",sfID:"Country Code",atID:"Artist ID",cnID:"Catalog ID",
plID:"Collection ID",geID:"Genre ID","xid ":"Vendor Information",flvr:"Codec Flavor"},r={title:"\u00a9nam",artist:"\u00a9ART",album:"\u00a9alb",year:"\u00a9day",comment:"\u00a9cmt",track:"trkn",genre:"\u00a9gen",picture:"covr",lyrics:"\u00a9lyr"};p.exports=h},{"./MediaFileReader":11,"./MediaTagReader":12}],11:[function(h,p,n){function m(c,b){for(var g=0;g<b.length;g++){var k=b[g];k.enumerable=k.enumerable||!1;k.configurable=!0;"value"in k&&(k.writable=!0);Object.defineProperty(c,k.key,k)}}function q(c,
b,g){b&&m(c.prototype,b);g&&m(c,g);return c}function t(c,b,g){b in c?Object.defineProperty(c,b,{value:g,enumerable:!0,configurable:!0,writable:!0}):c[b]=g;return c}var f=h("./StringUtils");h=function(){function c(b){if(!(this instanceof c))throw new TypeError("Cannot call a class as a function");t(this,"_isInitialized",void 0);t(this,"_size",void 0);this._isInitialized=!1;this._size=0}q(c,[{key:"init",value:function(b){var c=this;if(this._isInitialized)setTimeout(b.onSuccess,1);else return this._init({onSuccess:function(){c._isInitialized=
!0;b.onSuccess()},onError:b.onError})}},{key:"_init",value:function(b){throw Error("Must implement init function");}},{key:"loadRange",value:function(b,c){throw Error("Must implement loadRange function");}},{key:"getSize",value:function(){if(!this._isInitialized)throw Error("init() must be called first.");return this._size}},{key:"getByteAt",value:function(b){throw Error("Must implement getByteAt function");}},{key:"getBytesAt",value:function(b,c){for(var g=Array(c),f=0;f<c;f++)g[f]=this.getByteAt(b+
f);return g}},{key:"isBitSetAt",value:function(b,c){return 0!=(this.getByteAt(b)&1<<c)}},{key:"getSByteAt",value:function(b){b=this.getByteAt(b);return 127<b?b-256:b}},{key:"getShortAt",value:function(b,c){b=c?(this.getByteAt(b)<<8)+this.getByteAt(b+1):(this.getByteAt(b+1)<<8)+this.getByteAt(b);0>b&&(b+=65536);return b}},{key:"getSShortAt",value:function(b,c){b=this.getShortAt(b,c);return 32767<b?b-65536:b}},{key:"getLongAt",value:function(b,c){var g=this.getByteAt(b),f=this.getByteAt(b+1),a=this.getByteAt(b+
2);b=this.getByteAt(b+3);c=c?(((g<<8)+f<<8)+a<<8)+b:(((b<<8)+a<<8)+f<<8)+g;0>c&&(c+=4294967296);return c}},{key:"getSLongAt",value:function(b,c){b=this.getLongAt(b,c);return 2147483647<b?b-4294967296:b}},{key:"getInteger24At",value:function(b,c){var g=this.getByteAt(b),f=this.getByteAt(b+1);b=this.getByteAt(b+2);c=c?((g<<8)+f<<8)+b:((b<<8)+f<<8)+g;0>c&&(c+=16777216);return c}},{key:"getStringAt",value:function(b,c){for(var g=[],f=b,a=0;f<b+c;f++,a++)g[a]=String.fromCharCode(this.getByteAt(f));return g.join("")}},
{key:"getStringWithCharsetAt",value:function(b,c,k){b=this.getBytesAt(b,c);switch((k||"").toLowerCase()){case "utf-16":case "utf-16le":case "utf-16be":k=f.readUTF16String(b,"utf-16be"===k);break;case "utf-8":k=f.readUTF8String(b);break;default:k=f.readNullTerminatedString(b)}return k}},{key:"getCharAt",value:function(b){return String.fromCharCode(this.getByteAt(b))}},{key:"getSynchsafeInteger32At",value:function(b){var c=this.getByteAt(b),f=this.getByteAt(b+1),h=this.getByteAt(b+2);return this.getByteAt(b+
3)&127|(h&127)<<7|(f&127)<<14|(c&127)<<21}}],[{key:"canReadFile",value:function(b){throw Error("Must implement canReadFile function");}}]);return c}();p.exports=h},{"./StringUtils":13}],12:[function(h,p,n){function m(f,c){for(var b=0;b<c.length;b++){var g=c[b];g.enumerable=g.enumerable||!1;g.configurable=!0;"value"in g&&(g.writable=!0);Object.defineProperty(f,g.key,g)}}function q(f,c,b){c&&m(f.prototype,c);b&&m(f,b);return f}function t(f,c,b){c in f?Object.defineProperty(f,c,{value:b,enumerable:!0,
configurable:!0,writable:!0}):f[c]=b;return f}h("./MediaFileReader");h=function(){function f(c){if(!(this instanceof f))throw new TypeError("Cannot call a class as a function");t(this,"_mediaFileReader",void 0);t(this,"_tags",void 0);this._mediaFileReader=c;this._tags=null}q(f,[{key:"setTagsToRead",value:function(c){this._tags=c;return this}},{key:"read",value:function(c){var b=this;this._mediaFileReader.init({onSuccess:function(){b._loadData(b._mediaFileReader,{onSuccess:function(){try{var g=b._parseData(b._mediaFileReader,
b._tags)}catch(k){if(c.onError){c.onError({type:"parseData",info:k.message});return}}c.onSuccess(g)},onError:c.onError})},onError:c.onError})}},{key:"getShortcuts",value:function(){return{}}},{key:"_loadData",value:function(c,b){throw Error("Must implement _loadData function");}},{key:"_parseData",value:function(c,b){throw Error("Must implement _parseData function");}},{key:"_expandShortcutTags",value:function(c){if(!c)return null;for(var b=[],g=this.getShortcuts(),f=0,h;h=c[f];f++)b=b.concat(g[h]||
[h]);return b}}],[{key:"getTagIdentifierByteRange",value:function(){throw Error("Must implement");}},{key:"canReadTagFormat",value:function(c){throw Error("Must implement");}}]);return f}();p.exports=h},{"./MediaFileReader":11}],13:[function(h,p,n){function m(c,b){for(var g=0;g<b.length;g++){var f=b[g];f.enumerable=f.enumerable||!1;f.configurable=!0;"value"in f&&(f.writable=!0);Object.defineProperty(c,f.key,f)}}function q(c,b,f){b&&m(c.prototype,b);f&&m(c,f);return c}function t(c,b,f){b in c?Object.defineProperty(c,
b,{value:f,enumerable:!0,configurable:!0,writable:!0}):c[b]=f;return c}var f=function(){function c(b,f){if(!(this instanceof c))throw new TypeError("Cannot call a class as a function");t(this,"_value",void 0);t(this,"bytesReadCount",void 0);t(this,"length",void 0);this._value=b;this.bytesReadCount=f;this.length=b.length}q(c,[{key:"toString",value:function(){return this._value}}]);return c}();p.exports={readUTF16String:function(c,b,g){var k=0,h=1,a=0;g=Math.min(g||c.length,c.length);254==c[0]&&255==
c[1]?(b=!0,k=2):255==c[0]&&254==c[1]&&(b=!1,k=2);b&&(h=0,a=1);b=[];for(var e=0;k<g;e++){var d=c[k+h],l=(d<<8)+c[k+a];k+=2;if(0==l)break;else 216>d||224<=d?b[e]=String.fromCharCode(l):(d=(c[k+h]<<8)+c[k+a],k+=2,b[e]=String.fromCharCode(l,d))}return new f(b.join(""),k)},readUTF8String:function(c,b){var g=0;b=Math.min(b||c.length,c.length);239==c[0]&&187==c[1]&&191==c[2]&&(g=3);for(var h=[],m=0;g<b;m++){var a=c[g++];if(0==a)break;else if(128>a)h[m]=String.fromCharCode(a);else if(194<=a&&224>a){var e=
c[g++];h[m]=String.fromCharCode(((a&31)<<6)+(e&63))}else if(224<=a&&240>a){e=c[g++];var d=c[g++];h[m]=String.fromCharCode(((a&255)<<12)+((e&63)<<6)+(d&63))}else if(240<=a&&245>a){e=c[g++];d=c[g++];var l=c[g++];d=((a&7)<<18)+((e&63)<<12)+((d&63)<<6)+(l&63)-65536;h[m]=String.fromCharCode((d>>10)+55296,(d&1023)+56320)}}return new f(h.join(""),g)},readNullTerminatedString:function(c,b){var g=[];b=b||c.length;for(var h=0;h<b;){var m=c[h++];if(0==m)break;g[h-1]=String.fromCharCode(m)}return new f(g.join(""),
h)}}},{}],14:[function(h,p,n){function m(a){m="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"===typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a};return m(a)}function q(a,b){for(var d=0;d<b.length;d++){var c=b[d];c.enumerable=c.enumerable||!1;c.configurable=!0;"value"in c&&(c.writable=!0);Object.defineProperty(a,c.key,c)}}function t(a,b,d){b&&q(a.prototype,b);d&&q(a,d);return a}function f(a){f=
Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return f(a)}function c(a){if(void 0===a)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return a}function b(a,b){if("function"!==typeof b&&null!==b)throw new TypeError("Super expression must either be null or a function");a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});b&&g(a,b)}function g(a,b){g=Object.setPrototypeOf||
function(a,b){a.__proto__=b;return a};return g(a,b)}function k(a,b,d){b in a?Object.defineProperty(a,b,{value:d,enumerable:!0,configurable:!0,writable:!0}):a[b]=d;return a}var r=h("./ChunkedFileData");n=function(a){function e(a){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function");var b=f(e).call(this);b=!b||"object"!==m(b)&&"function"!==typeof b?c(this):b;k(c(b),"_url",void 0);k(c(b),"_fileData",void 0);b._url=a;b._fileData=new r;return b}b(e,a);t(e,[{key:"_init",value:function(a){e._config.avoidHeadRequests?
this._fetchSizeWithGetRequest(a):this._fetchSizeWithHeadRequest(a)}},{key:"_fetchSizeWithHeadRequest",value:function(a){var b=this;this._makeXHRRequest("HEAD",null,{onSuccess:function(d){(d=b._parseContentLength(d))?(b._size=d,a.onSuccess()):b._fetchSizeWithGetRequest(a)},onError:a.onError})}},{key:"_fetchSizeWithGetRequest",value:function(a){var b=this,d=this._roundRangeToChunkMultiple([0,0]);this._makeXHRRequest("GET",d,{onSuccess:function(d){var c=b._parseContentRange(d);d=b._getXhrResponseContent(d);
if(c){if(null==c.instanceLength){b._fetchEntireFile(a);return}b._size=c.instanceLength}else b._size=d.length;b._fileData.addData(0,d);a.onSuccess()},onError:a.onError})}},{key:"_fetchEntireFile",value:function(a){var b=this;this._makeXHRRequest("GET",null,{onSuccess:function(d){d=b._getXhrResponseContent(d);b._size=d.length;b._fileData.addData(0,d);a.onSuccess()},onError:a.onError})}},{key:"_getXhrResponseContent",value:function(a){return a.responseBody||a.responseText||""}},{key:"_parseContentLength",
value:function(a){a=this._getResponseHeader(a,"Content-Length");return null==a?a:parseInt(a,10)}},{key:"_parseContentRange",value:function(a){if(a=this._getResponseHeader(a,"Content-Range")){var b=a.match(/bytes (\d+)-(\d+)\/(?:(\d+)|\*)/i);if(!b)throw Error("FIXME: Unknown Content-Range syntax: "+a);return{firstBytePosition:parseInt(b[1],10),lastBytePosition:parseInt(b[2],10),instanceLength:b[3]?parseInt(b[3],10):null}}return null}},{key:"loadRange",value:function(a,b){var d=this;d._fileData.hasDataRange(a[0],
Math.min(d._size,a[1]))?setTimeout(b.onSuccess,1):(a=this._roundRangeToChunkMultiple(a),a[1]=Math.min(d._size,a[1]),this._makeXHRRequest("GET",a,{onSuccess:function(c){c=d._getXhrResponseContent(c);d._fileData.addData(a[0],c);b.onSuccess()},onError:b.onError}))}},{key:"_roundRangeToChunkMultiple",value:function(a){return[a[0],a[0]+1024*Math.ceil((a[1]-a[0]+1)/1024)-1]}},{key:"_makeXHRRequest",value:function(a,b,c){var d=this._createXHRObject();d.open(a,this._url);var f=function(){if(200===d.status||
206===d.status)c.onSuccess(d);else if(c.onError)c.onError({type:"xhr",info:"Unexpected HTTP status "+d.status+".",xhr:d});d=null};"undefined"!==typeof d.onload?(d.onload=f,d.onerror=function(){if(c.onError)c.onError({type:"xhr",info:"Generic XHR error, check xhr object.",xhr:d})}):d.onreadystatechange=function(){4===d.readyState&&f()};e._config.timeoutInSec&&(d.timeout=1E3*e._config.timeoutInSec,d.ontimeout=function(){if(c.onError)c.onError({type:"xhr",info:"Timeout after "+d.timeout/1E3+"s. Use jsmediatags.Config.setXhrTimeout to override.",
xhr:d})});d.overrideMimeType("text/plain; charset=x-user-defined");b&&this._setRequestHeader(d,"Range","bytes="+b[0]+"-"+b[1]);this._setRequestHeader(d,"If-Modified-Since","Sat, 01 Jan 1970 00:00:00 GMT");d.send(null)}},{key:"_setRequestHeader",value:function(a,b,c){0>e._config.disallowedXhrHeaders.indexOf(b.toLowerCase())&&a.setRequestHeader(b,c)}},{key:"_hasResponseHeader",value:function(a,b){a=a.getAllResponseHeaders();if(!a)return!1;a=a.split("\r\n");for(var d=[],c=0;c<a.length;c++)d[c]=a[c].split(":")[0].toLowerCase();
return 0<=d.indexOf(b.toLowerCase())}},{key:"_getResponseHeader",value:function(a,b){return this._hasResponseHeader(a,b)?a.getResponseHeader(b):null}},{key:"getByteAt",value:function(a){return this._fileData.getByteAt(a).charCodeAt(0)&255}},{key:"_isWebWorker",value:function(){return"undefined"!==typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope}},{key:"_createXHRObject",value:function(){if("undefined"===typeof window&&!this._isWebWorker())return new (h("xhr2").XMLHttpRequest);if("undefined"!==
typeof XMLHttpRequest)return new XMLHttpRequest;throw Error("XMLHttpRequest is not supported");}}],[{key:"canReadFile",value:function(a){return"string"===typeof a&&/^[a-z]+:\/\//i.test(a)}},{key:"setConfig",value:function(a){for(var b in a)a.hasOwnProperty(b)&&(this._config[b]=a[b]);a=this._config.disallowedXhrHeaders;for(b=0;b<a.length;b++)a[b]=a[b].toLowerCase()}}]);return e}(h("./MediaFileReader"));k(n,"_config",void 0);n._config={avoidHeadRequests:!1,disallowedXhrHeaders:[],timeoutInSec:30};p.exports=
n},{"./ChunkedFileData":5,"./MediaFileReader":11,xhr2:2}],15:[function(h,p,n){function m(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function");}function q(a,b){for(var c=0;c<b.length;c++){var d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;"value"in d&&(d.writable=!0);Object.defineProperty(a,d.key,d)}}function t(a,b,c){b&&q(a.prototype,b);c&&q(a,c);return a}function f(a,b,c){b in a?Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0}):a[b]=
c;return a}function c(a,b){var c=0>a.offset&&(-a.offset>b||0<a.offset+a.length);return!(0<=a.offset&&a.offset+a.length>=b||c)}h("./MediaFileReader");var b=h("./XhrFileReader"),g=h("./BlobFileReader"),k=h("./ArrayFileReader");h("./MediaTagReader");var r=h("./ID3v1TagReader"),a=h("./ID3v2TagReader"),e=h("./MP4TagReader"),d=h("./FLACTagReader"),l=[],u=[],w=function(){function a(b){m(this,a);f(this,"_file",void 0);f(this,"_tagsToRead",void 0);f(this,"_fileReader",void 0);f(this,"_tagReader",void 0);this._file=
b}t(a,[{key:"setTagsToRead",value:function(a){this._tagsToRead=a;return this}},{key:"setFileReader",value:function(a){this._fileReader=a;return this}},{key:"setTagReader",value:function(a){this._tagReader=a;return this}},{key:"read",value:function(a){var b=new (this._getFileReader())(this._file),c=this;b.init({onSuccess:function(){c._getTagReader(b,{onSuccess:function(d){(new d(b)).setTagsToRead(c._tagsToRead).read(a)},onError:a.onError})},onError:a.onError})}},{key:"_getFileReader",value:function(){return this._fileReader?
this._fileReader:this._findFileReader()}},{key:"_findFileReader",value:function(){for(var a=0;a<l.length;a++)if(l[a].canReadFile(this._file))return l[a];throw Error("No suitable file reader found for "+this._file);}},{key:"_getTagReader",value:function(a,b){if(this._tagReader){var c=this._tagReader;setTimeout(function(){b.onSuccess(c)},1)}else this._findTagReader(a,b)}},{key:"_findTagReader",value:function(a,b){for(var d=[],e=[],f=a.getSize(),g=0;g<u.length;g++){var h=u[g].getTagIdentifierByteRange();
c(h,f)&&(0<=h.offset&&h.offset<f/2||0>h.offset&&h.offset<-f/2?d.push(u[g]):e.push(u[g]))}var k=!1;g={onSuccess:function(){if(k){for(var d=0;d<u.length;d++){var e=u[d].getTagIdentifierByteRange();if(c(e,f)){try{var g=a.getBytesAt(0<=e.offset?e.offset:e.offset+f,e.length)}catch(x){if(b.onError)b.onError({type:"fileReader",info:x.message});return}if(u[d].canReadTagFormat(g)){b.onSuccess(u[d]);return}}}if(b.onError)b.onError({type:"tagFormat",info:"No suitable tag reader found"})}else k=!0},onError:b.onError};
this._loadTagIdentifierRanges(a,d,g);this._loadTagIdentifierRanges(a,e,g)}},{key:"_loadTagIdentifierRanges",value:function(a,b,c){if(0===b.length)setTimeout(c.onSuccess,1);else{for(var d=[Number.MAX_VALUE,0],e=a.getSize(),f=0;f<b.length;f++){var g=b[f].getTagIdentifierByteRange(),h=0<=g.offset?g.offset:g.offset+e;g=h+g.length-1;d[0]=Math.min(h,d[0]);d[1]=Math.max(g,d[1])}a.loadRange(d,c)}}}]);return a}();n=function(){function a(){m(this,a)}t(a,null,[{key:"addFileReader",value:function(b){l.push(b);
return a}},{key:"addTagReader",value:function(b){u.push(b);return a}},{key:"removeTagReader",value:function(b){b=u.indexOf(b);0<=b&&u.splice(b,1);return a}},{key:"EXPERIMENTAL_avoidHeadRequests",value:function(){b.setConfig({avoidHeadRequests:!0})}},{key:"setDisallowedXhrHeaders",value:function(a){b.setConfig({disallowedXhrHeaders:a})}},{key:"setXhrTimeoutInSec",value:function(a){b.setConfig({timeoutInSec:a})}}]);return a}();n.addFileReader(b).addFileReader(g).addFileReader(k).addTagReader(a).addTagReader(r).addTagReader(e).addTagReader(d);
"undefined"===typeof process||process.browser||(h="undefined"!==typeof navigator&&"ReactNative"===navigator.product?h("./ReactNativeFileReader"):h("./NodeFileReader"),n.addFileReader(h));p.exports={read:function(a,b){(new w(a)).read(b)},Reader:w,Config:n}},{"./ArrayFileReader":3,"./BlobFileReader":4,"./FLACTagReader":6,"./ID3v1TagReader":7,"./ID3v2TagReader":9,"./MP4TagReader":10,"./MediaFileReader":11,"./MediaTagReader":12,"./NodeFileReader":1,"./ReactNativeFileReader":1,"./XhrFileReader":14}]},
{},[15])(15)});

18
static/manifest.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "Freedify",
"short_name": "Freedify",
"description": "Stream music from anywhere",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0f",
"theme_color": "#6366f1",
"orientation": "portrait",
"icons": [
{
"src": "/static/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}

5718
static/styles.css Normal file

File diff suppressed because it is too large Load diff

66
static/sw.js Normal file
View file

@ -0,0 +1,66 @@
/**
* SpotiFLAC Service Worker
* Caches app shell for offline access
*/
const CACHE_NAME = 'spotiflac-v6';
const STATIC_ASSETS = [
'/',
'/static/styles.css',
'/static/app.js',
'/static/manifest.json',
'/static/icon.svg'
];
// Install - cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate - clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch - network first, fallback to cache
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip API and audio streaming requests
if (url.pathname.startsWith('/api/')) {
return;
}
event.respondWith(
fetch(request)
.then((response) => {
// Cache successful responses for static assets
if (response.ok && STATIC_ASSETS.includes(url.pathname)) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
}
return response;
})
.catch(() => {
// Fallback to cache
return caches.match(request);
})
);
});