Initial commit - SoundWave v1.0

- Full PWA support with offline capabilities
- Comprehensive search across songs, playlists, and channels
- Offline playlist manager with download tracking
- Pre-built frontend for zero-build deployment
- Docker-based deployment with docker compose
- Material-UI dark theme interface
- YouTube audio download and management
- Multi-user authentication support
This commit is contained in:
Iulian 2025-12-16 23:43:07 +00:00
commit 51679d1943
254 changed files with 37281 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

179
frontend/dist/assets/mui-DW1KyNMb.js vendored Normal file

File diff suppressed because one or more lines are too long

59
frontend/dist/assets/vendor-Bv7lQTk9.js vendored Normal file

File diff suppressed because one or more lines are too long

11
frontend/dist/avatars/preset_1.svg vendored Normal file
View file

@ -0,0 +1,11 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="100" cy="100" r="100" fill="#6366F1"/>
<!-- Music note -->
<g fill="white">
<circle cx="90" cy="140" r="15"/>
<rect x="102" y="90" width="8" height="65" rx="4"/>
<path d="M 110 90 Q 130 85 135 75 Q 138 68 135 65 Q 132 62 125 65 L 110 72 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 406 B

11
frontend/dist/avatars/preset_2.svg vendored Normal file
View file

@ -0,0 +1,11 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="100" cy="100" r="100" fill="#EC4899"/>
<!-- Headphones -->
<g fill="white" stroke="white" stroke-width="4">
<path d="M 60 100 Q 60 50 100 50 Q 140 50 140 100" fill="none"/>
<rect x="50" y="95" width="20" height="35" rx="5"/>
<rect x="130" y="95" width="20" height="35" rx="5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 442 B

12
frontend/dist/avatars/preset_3.svg vendored Normal file
View file

@ -0,0 +1,12 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="100" cy="100" r="100" fill="#10B981"/>
<!-- Microphone -->
<g fill="white">
<rect x="85" y="60" width="30" height="50" rx="15"/>
<path d="M 70 100 Q 70 130 100 130 Q 130 130 130 100" fill="none" stroke="white" stroke-width="6"/>
<rect x="95" y="130" width="10" height="30"/>
<rect x="75" y="155" width="50" height="8" rx="4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 495 B

12
frontend/dist/avatars/preset_4.svg vendored Normal file
View file

@ -0,0 +1,12 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="100" cy="100" r="100" fill="#F59E0B"/>
<!-- Vinyl record -->
<g>
<circle cx="100" cy="100" r="60" fill="white"/>
<circle cx="100" cy="100" r="50" fill="#1F2937"/>
<circle cx="100" cy="100" r="15" fill="white"/>
<circle cx="100" cy="100" r="8" fill="#1F2937"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 428 B

15
frontend/dist/avatars/preset_5.svg vendored Normal file
View file

@ -0,0 +1,15 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="100" cy="100" r="100" fill="#8B5CF6"/>
<!-- Waveform -->
<g fill="white">
<rect x="50" y="90" width="8" height="20" rx="4"/>
<rect x="65" y="70" width="8" height="60" rx="4"/>
<rect x="80" y="50" width="8" height="100" rx="4"/>
<rect x="95" y="80" width="8" height="40" rx="4"/>
<rect x="110" y="60" width="8" height="80" rx="4"/>
<rect x="125" y="75" width="8" height="50" rx="4"/>
<rect x="140" y="85" width="8" height="30" rx="4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 615 B

BIN
frontend/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

271
frontend/dist/icon-preview.html vendored Normal file
View file

@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SoundWave PWA Icons Preview</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 20px;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 2.5em;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 40px;
font-size: 1.1em;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
margin-bottom: 50px;
}
.icon-card {
text-align: center;
padding: 20px;
border: 2px solid #e0e0e0;
border-radius: 15px;
transition: all 0.3s ease;
background: #f9f9f9;
}
.icon-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
border-color: #1976d2;
}
.icon-wrapper {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 15px;
display: inline-block;
}
.icon-card img {
display: block;
margin: 0 auto;
image-rendering: -webkit-optimize-contrast;
image-rendering: crisp-edges;
}
.icon-label {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 1.1em;
}
.icon-size {
color: #666;
font-size: 0.9em;
}
.section-title {
font-size: 1.8em;
color: #333;
margin: 40px 0 20px;
padding-bottom: 10px;
border-bottom: 3px solid #1976d2;
}
.special-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
}
.badge {
display: inline-block;
background: #4caf50;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75em;
margin-top: 5px;
font-weight: 600;
}
.maskable {
background: #ff9800;
}
.apple {
background: #000;
}
.stats {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 15px;
margin-top: 40px;
text-align: center;
}
.stats h3 {
margin-bottom: 20px;
font-size: 1.5em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.stat-item {
background: rgba(255,255,255,0.2);
padding: 20px;
border-radius: 10px;
}
.stat-value {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
</style>
</head>
<body>
<div class="container">
<h1>🎵 SoundWave</h1>
<p class="subtitle">PWA Icon Set - All Sizes Generated</p>
<h2 class="section-title">Standard Icons (Any Purpose)</h2>
<div class="grid">
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-72x72.png" alt="72x72" width="72" height="72">
</div>
<div class="icon-label">Small</div>
<div class="icon-size">72×72 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-96x96.png" alt="96x96" width="96" height="96">
</div>
<div class="icon-label">Medium</div>
<div class="icon-size">96×96 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-128x128.png" alt="128x128" width="128" height="128">
</div>
<div class="icon-label">Large</div>
<div class="icon-size">128×128 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-144x144.png" alt="144x144" width="144" height="144">
</div>
<div class="icon-label">X-Large</div>
<div class="icon-size">144×144 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-152x152.png" alt="152x152" width="152" height="152">
</div>
<div class="icon-label">iOS</div>
<div class="icon-size">152×152 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-192x192.png" alt="192x192" width="192" height="192">
</div>
<div class="icon-label">Standard HD</div>
<div class="icon-size">192×192 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-384x384.png" alt="384x384" width="192" height="192">
</div>
<div class="icon-label">Retina</div>
<div class="icon-size">384×384 pixels</div>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-512x512.png" alt="512x512" width="192" height="192">
</div>
<div class="icon-label">Ultra HD</div>
<div class="icon-size">512×512 pixels</div>
</div>
</div>
<h2 class="section-title">Platform-Specific Icons</h2>
<div class="special-grid">
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-192x192-maskable.png" alt="192x192 Maskable" width="192" height="192">
</div>
<div class="icon-label">Android Maskable</div>
<div class="icon-size">192×192 pixels</div>
<span class="badge maskable">Maskable</span>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/icon-512x512-maskable.png" alt="512x512 Maskable" width="192" height="192">
</div>
<div class="icon-label">Android HD Maskable</div>
<div class="icon-size">512×512 pixels</div>
<span class="badge maskable">Maskable</span>
</div>
<div class="icon-card">
<div class="icon-wrapper">
<img src="/img/icons/apple-touch-icon.png" alt="Apple Touch Icon" width="180" height="180">
</div>
<div class="icon-label">Apple Touch Icon</div>
<div class="icon-size">180×180 pixels</div>
<span class="badge apple">iOS</span>
</div>
</div>
<div class="stats">
<h3>📊 Icon Set Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">11</div>
<div class="stat-label">Total Icons</div>
</div>
<div class="stat-item">
<div class="stat-value">8</div>
<div class="stat-label">Standard Sizes</div>
</div>
<div class="stat-item">
<div class="stat-value">2</div>
<div class="stat-label">Maskable Icons</div>
</div>
<div class="stat-item">
<div class="stat-value">1</div>
<div class="stat-label">Apple Icon</div>
</div>
<div class="stat-item">
<div class="stat-value">~500KB</div>
<div class="stat-label">Total Size</div>
</div>
<div class="stat-item">
<div class="stat-value"></div>
<div class="stat-label">PWA Ready</div>
</div>
</div>
</div>
<div style="text-align: center; margin-top: 40px; color: #666;">
<p>✨ All icons generated from official SoundWave logo</p>
<p style="margin-top: 10px;">🎯 Optimized for all platforms: Android, iOS, Desktop</p>
</div>
</div>
</body>
</html>

23
frontend/dist/img/GENERATE_ICONS.md vendored Normal file
View file

@ -0,0 +1,23 @@
# PWA Icon Generation Script
# This creates placeholder icons - replace with actual app icons
# For production, generate proper icons using:
# https://www.pwabuilder.com/imageGenerator
# or
# https://realfavicongenerator.net/
echo "To generate PWA icons, use one of these tools:"
echo "1. PWA Builder Image Generator: https://www.pwabuilder.com/imageGenerator"
echo "2. Real Favicon Generator: https://realfavicongenerator.net/"
echo ""
echo "Required icon sizes:"
echo "- 72x72"
echo "- 96x96"
echo "- 128x128"
echo "- 144x144"
echo "- 152x152"
echo "- 192x192"
echo "- 384x384"
echo "- 512x512"
echo ""
echo "Place generated icons in: frontend/public/img/"

BIN
frontend/dist/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
frontend/dist/img/icons/icon-128x128.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
frontend/dist/img/icons/icon-144x144.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
frontend/dist/img/icons/icon-152x152.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
frontend/dist/img/icons/icon-192x192.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
frontend/dist/img/icons/icon-384x384.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
frontend/dist/img/icons/icon-512x512.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
frontend/dist/img/icons/icon-72x72.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
frontend/dist/img/icons/icon-96x96.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

17
frontend/dist/img/icons/logo-source.svg vendored Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768">
<rect width="768" height="768" fill="#A8D5D8"/>
<circle cx="384" cy="330" r="135" fill="none" stroke="#0F4C75" stroke-width="12"/>
<circle cx="384" cy="330" r="110" fill="none" stroke="#0F4C75" stroke-width="12"/>
<path d="M 385 230 Q 405 240 385 250 Q 405 255 385 270 Q 410 275 385 290" fill="none" stroke="#00C8C8" stroke-width="8" stroke-linecap="round"/>
<circle cx="384" cy="330" r="80" fill="none" stroke="#00C8C8" stroke-width="10"/>
<path fill="#0F4C75" d="M 350 305 L 350 355 L 410 330 Z"/>
<rect x="220" y="310" width="12" height="40" rx="6" fill="#00C8C8"/>
<rect x="245" y="295" width="12" height="70" rx="6" fill="#00C8C8"/>
<rect x="270" y="285" width="12" height="90" rx="6" fill="#0F4C75"/>
<rect x="510" y="310" width="12" height="40" rx="6" fill="#00C8C8"/>
<rect x="535" y="295" width="12" height="70" rx="6" fill="#00C8C8"/>
<rect x="560" y="285" width="12" height="90" rx="6" fill="#0F4C75"/>
<text x="384" y="550" font-family="Arial, sans-serif" font-size="80" font-weight="bold" text-anchor="middle" fill="#0F4C75">sound</text>
<text x="384" y="550" font-family="Arial, sans-serif" font-size="80" font-weight="bold" text-anchor="middle" fill="#00C8C8" dx="160">wave</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

39
frontend/dist/img/logo-app.svg vendored Normal file
View file

@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="768" height="768">
<!-- Background circle -->
<circle cx="384" cy="384" r="384" fill="#A8DADC"/>
<!-- Sound wave lines - left -->
<rect x="190" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
<rect x="210" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
<rect x="230" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
<!-- Outer circle - cyan -->
<circle cx="384" cy="320" r="110" fill="none" stroke="#4ECDC4" stroke-width="16"/>
<!-- Middle circle - dark blue -->
<circle cx="384" cy="320" r="85" fill="none" stroke="#1D3557" stroke-width="12"/>
<!-- Inner circle - cyan -->
<circle cx="384" cy="320" r="65" fill="#A8DADC" stroke="#4ECDC4" stroke-width="10"/>
<!-- Play button -->
<path d="M 370 300 L 370 340 L 405 320 Z" fill="#1D3557"/>
<!-- Sound wave lines - right -->
<rect x="526" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
<rect x="546" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
<rect x="566" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
<!-- Text "soundwave" curved -->
<path id="curve" d="M 180 420 Q 384 500 588 420" fill="none"/>
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#1D3557">
<textPath href="#curve" startOffset="12%">
sound
</textPath>
</text>
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#4ECDC4">
<textPath href="#curve" startOffset="50%">
wave
</textPath>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
frontend/dist/img/logo-new.png vendored Normal file
View file

@ -0,0 +1 @@
# This will be replaced with the actual image file

BIN
frontend/dist/img/logo-temp.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
frontend/dist/img/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

39
frontend/dist/img/logo.svg vendored Normal file
View file

@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 768 768" width="768" height="768">
<!-- Background circle -->
<circle cx="384" cy="384" r="384" fill="#A8DADC"/>
<!-- Sound wave lines - left -->
<rect x="190" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
<rect x="210" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
<rect x="230" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
<!-- Outer circle - cyan -->
<circle cx="384" cy="320" r="110" fill="none" stroke="#4ECDC4" stroke-width="16"/>
<!-- Middle circle - dark blue -->
<circle cx="384" cy="320" r="85" fill="none" stroke="#1D3557" stroke-width="12"/>
<!-- Inner circle - cyan -->
<circle cx="384" cy="320" r="65" fill="#A8DADC" stroke="#4ECDC4" stroke-width="10"/>
<!-- Play button -->
<path d="M 370 300 L 370 340 L 405 320 Z" fill="#1D3557"/>
<!-- Sound wave lines - right -->
<rect x="526" y="345" width="12" height="50" rx="6" fill="#4ECDC4"/>
<rect x="546" y="355" width="12" height="30" rx="6" fill="#4ECDC4"/>
<rect x="566" y="340" width="12" height="60" rx="6" fill="#4ECDC4"/>
<!-- Text "soundwave" curved -->
<path id="curve" d="M 180 420 Q 384 500 588 420" fill="none"/>
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#1D3557">
<textPath href="#curve" startOffset="12%">
sound
</textPath>
</text>
<text font-family="Arial, sans-serif" font-size="80" font-weight="700" fill="#4ECDC4">
<textPath href="#curve" startOffset="50%">
wave
</textPath>
</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

71
frontend/dist/index.html vendored Normal file
View file

@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Primary Meta Tags -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no" />
<meta name="title" content="SoundWave - Music Streaming & YouTube Archive" />
<meta name="description" content="Multi-tenant YouTube music streaming platform with local file support, offline playback, and advanced audio features" />
<meta name="keywords" content="music, streaming, youtube, audio, offline, pwa, soundwave" />
<meta name="author" content="SoundWave" />
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Soundwave" />
<meta name="application-name" content="Soundwave" />
<meta name="theme-color" content="#1976d2" />
<meta name="msapplication-TileColor" content="#1976d2" />
<meta name="msapplication-tap-highlight" content="no" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- ID3 Tag Reader -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
<!-- Icons -->
<link rel="icon" type="image/x-icon" href="/img/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/img/icons/icon-72x72.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/img/icons/icon-192x192.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/icons/apple-touch-icon.png" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/img/icon-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/img/icon-512x512.png" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://soundwave.app/" />
<meta property="og:title" content="SoundWave - Music Streaming & YouTube Archive" />
<meta property="og:description" content="Multi-tenant YouTube music streaming platform with local file support, offline playback, and advanced audio features" />
<meta property="og:image" content="/img/icon-512x512.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://soundwave.app/" />
<meta property="twitter:title" content="SoundWave - Music Streaming & YouTube Archive" />
<meta property="twitter:description" content="Multi-tenant YouTube music streaming platform with local file support, offline playback, and advanced audio features" />
<meta property="twitter:image" content="/img/icon-512x512.png" />
<title>SoundWave - Music Streaming & YouTube Archive</title>
<!-- Preconnect to API -->
<link rel="preconnect" href="/api" />
<script type="module" crossorigin src="/assets/index-ChIfYXgy.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-Bv7lQTk9.js">
<link rel="modulepreload" crossorigin href="/assets/mui-DW1KyNMb.js">
<link rel="stylesheet" crossorigin href="/assets/index-BeXoqz9j.css">
</head>
<body>
<noscript>
<div style="text-align: center; padding: 50px; font-family: Arial, sans-serif;">
<h1>JavaScript Required</h1>
<p>SoundWave requires JavaScript to be enabled. Please enable JavaScript in your browser settings.</p>
</div>
</noscript>
<div id="root"></div>
</body>
</html>

135
frontend/dist/manifest.json vendored Normal file
View file

@ -0,0 +1,135 @@
{
"name": "SoundWave",
"short_name": "SoundWave",
"description": "Music streaming platform with local file support and offline playback",
"start_url": "/",
"display": "standalone",
"background_color": "#121212",
"theme_color": "#1976d2",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"src": "/img/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/icons/icon-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/img/icons/icon-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/img/screenshot-wide.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "/img/screenshot-narrow.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
}
],
"categories": ["music", "entertainment"],
"shortcuts": [
{
"name": "Home",
"short_name": "Home",
"description": "Open home page",
"url": "/",
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
},
{
"name": "Search",
"short_name": "Search",
"description": "Search for music",
"url": "/search",
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
},
{
"name": "Library",
"short_name": "Library",
"description": "View your library",
"url": "/library",
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
},
{
"name": "Local Files",
"short_name": "Local Files",
"description": "View local files",
"url": "/local-files",
"icons": [{ "src": "/img/icons/icon-192x192.png", "sizes": "192x192" }]
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "audio",
"accept": ["audio/*"]
}
]
}
},
"prefer_related_applications": false
}

9
frontend/dist/robots.txt vendored Normal file
View file

@ -0,0 +1,9 @@
User-agent: *
Allow: /
# Disallow admin and API endpoints
Disallow: /api/
Disallow: /admin/
# Sitemap
Sitemap: https://soundwave.app/sitemap.xml

377
frontend/dist/service-worker.js vendored Normal file
View file

@ -0,0 +1,377 @@
/* eslint-disable no-restricted-globals */
const CACHE_NAME = 'soundwave-v1';
const API_CACHE_NAME = 'soundwave-api-v1';
const AUDIO_CACHE_NAME = 'soundwave-audio-v1';
const IMAGE_CACHE_NAME = 'soundwave-images-v1';
// Assets to cache on install
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json',
'/favicon.ico',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[Service Worker] Caching static assets');
return cache.addAll(STATIC_ASSETS).catch((err) => {
console.error('[Service Worker] Failed to cache static assets:', err);
});
}).then(() => {
console.log('[Service Worker] Installed');
return self.skipWaiting(); // Activate immediately
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (
cacheName !== CACHE_NAME &&
cacheName !== API_CACHE_NAME &&
cacheName !== AUDIO_CACHE_NAME &&
cacheName !== IMAGE_CACHE_NAME
) {
console.log('[Service Worker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
console.log('[Service Worker] Activated');
return self.clients.claim(); // Take control immediately
})
);
});
// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip chrome extensions and non-http(s) requests
if (!url.protocol.startsWith('http')) {
return;
}
// API requests - Network first, fallback to cache
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstStrategy(request, API_CACHE_NAME));
return;
}
// Audio files - Cache first, fallback to network
if (
url.pathname.includes('/audio/') ||
url.pathname.includes('/media/local_audio/') ||
request.destination === 'audio'
) {
event.respondWith(cacheFirstStrategy(request, AUDIO_CACHE_NAME));
return;
}
// Images - Cache first, fallback to network
if (
url.pathname.includes('/img/') ||
url.pathname.includes('/media/') ||
url.pathname.includes('thumbnail') ||
url.pathname.includes('cover') ||
request.destination === 'image'
) {
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE_NAME));
return;
}
// Static assets (JS, CSS) - Stale while revalidate
if (
request.destination === 'script' ||
request.destination === 'style' ||
url.pathname.endsWith('.js') ||
url.pathname.endsWith('.css')
) {
event.respondWith(staleWhileRevalidateStrategy(request, CACHE_NAME));
return;
}
// HTML pages - Network first, fallback to cache
if (request.mode === 'navigate' || request.destination === 'document') {
event.respondWith(networkFirstStrategy(request, CACHE_NAME));
return;
}
// Default - Network first
event.respondWith(networkFirstStrategy(request, CACHE_NAME));
});
// Network first strategy - try network, fallback to cache
async function networkFirstStrategy(request, cacheName) {
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse && networkResponse.status === 200) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('[Service Worker] Network request failed, trying cache:', request.url);
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for navigation requests
if (request.mode === 'navigate') {
const cache = await caches.open(CACHE_NAME);
const offlinePage = await cache.match('/index.html');
if (offlinePage) {
return offlinePage;
}
}
throw error;
}
}
// Cache first strategy - try cache, fallback to network
async function cacheFirstStrategy(request, cacheName) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse && networkResponse.status === 200) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[Service Worker] Cache and network failed:', error);
throw error;
}
}
// Stale while revalidate - return cache immediately, update in background
async function staleWhileRevalidateStrategy(request, cacheName) {
const cachedResponse = await caches.match(request);
const fetchPromise = fetch(request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200) {
const cache = caches.open(cacheName);
cache.then((c) => c.put(request, networkResponse.clone()));
}
return networkResponse;
}).catch((error) => {
console.log('[Service Worker] Background fetch failed:', error);
});
return cachedResponse || fetchPromise;
}
// Background sync for offline uploads
self.addEventListener('sync', (event) => {
console.log('[Service Worker] Background sync:', event.tag);
if (event.tag === 'sync-audio-uploads') {
event.waitUntil(syncAudioUploads());
}
if (event.tag === 'sync-favorites') {
event.waitUntil(syncFavorites());
}
});
async function syncAudioUploads() {
console.log('[Service Worker] Syncing audio uploads...');
// Implementation for syncing pending uploads when back online
// This would read from IndexedDB and upload pending files
}
async function syncFavorites() {
console.log('[Service Worker] Syncing favorites...');
// Implementation for syncing favorite changes when back online
}
// Push notifications
self.addEventListener('push', (event) => {
console.log('[Service Worker] Push notification received');
const data = event.data ? event.data.json() : {};
const title = data.title || 'SoundWave';
const options = {
body: data.body || 'New content available',
icon: '/img/icon-192x192.png',
badge: '/img/icon-72x72.png',
vibrate: [200, 100, 200],
data: data.url || '/',
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Close' },
],
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] Notification clicked');
event.notification.close();
if (event.action === 'open' || !event.action) {
const urlToOpen = event.notification.data || '/';
event.waitUntil(
clients.openWindow(urlToOpen)
);
}
});
// Message handling for cache management
self.addEventListener('message', (event) => {
console.log('[Service Worker] Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => caches.delete(cacheName))
);
}).then(() => {
event.ports[0].postMessage({ success: true });
})
);
}
if (event.data && event.data.type === 'CACHE_AUDIO') {
const { url } = event.data;
event.waitUntil(
caches.open(AUDIO_CACHE_NAME).then((cache) => {
return cache.add(url);
}).then(() => {
event.ports[0].postMessage({ success: true });
}).catch((error) => {
event.ports[0].postMessage({ success: false, error: error.message });
})
);
}
// Cache playlist for offline access with authentication
if (event.data && event.data.type === 'CACHE_PLAYLIST') {
const { playlistId, audioUrls } = event.data;
event.waitUntil(
(async () => {
try {
console.log('[Service Worker] Caching playlist:', playlistId, 'with', audioUrls.length, 'tracks');
const results = {
metadata: false,
audioFiles: [],
failed: []
};
// Cache playlist metadata API response (includes items)
try {
const apiCache = await caches.open(API_CACHE_NAME);
const metadataUrl = `/api/playlist/${playlistId}/?include_items=true`;
await apiCache.add(metadataUrl);
results.metadata = true;
console.log('[Service Worker] Cached playlist metadata');
} catch (err) {
console.warn('[Service Worker] Failed to cache playlist metadata:', err);
}
// Cache all audio files in playlist with authentication
const audioCache = await caches.open(AUDIO_CACHE_NAME);
for (const url of audioUrls) {
try {
// Create authenticated request
const authRequest = new Request(url, {
credentials: 'include',
headers: {
'Accept': 'audio/*',
}
});
const response = await fetch(authRequest);
if (response.ok) {
// Clone and cache the response
await audioCache.put(url, response.clone());
results.audioFiles.push(url);
console.log('[Service Worker] Cached audio:', url);
} else {
results.failed.push(url);
console.warn('[Service Worker] Failed to cache audio (status ' + response.status + '):', url);
}
} catch (err) {
results.failed.push(url);
console.warn('[Service Worker] Failed to cache audio:', url, err);
}
}
console.log('[Service Worker] Playlist caching complete:', results);
event.ports[0].postMessage({
success: results.audioFiles.length > 0,
metadata: results.metadata,
cached: results.audioFiles.length,
failed: results.failed.length,
details: results
});
} catch (error) {
console.error('[Service Worker] Playlist caching error:', error);
event.ports[0].postMessage({ success: false, error: error.message });
}
})()
);
}
// Remove cached playlist
if (event.data && event.data.type === 'REMOVE_PLAYLIST_CACHE') {
const { playlistId, audioUrls } = event.data;
event.waitUntil(
Promise.all([
// Remove playlist metadata from cache
caches.open(API_CACHE_NAME).then((cache) => {
return cache.delete(`/api/playlist/${playlistId}/`);
}),
// Remove audio files from cache (only if not used by other playlists)
caches.open(AUDIO_CACHE_NAME).then((cache) => {
return Promise.all(
audioUrls.map(url => cache.delete(url))
);
})
]).then(() => {
event.ports[0].postMessage({ success: true });
}).catch((error) => {
event.ports[0].postMessage({ success: false, error: error.message });
})
);
}
});
console.log('[Service Worker] Loaded');

33
frontend/dist/sitemap.xml vendored Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://soundwave.app/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://soundwave.app/search</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://soundwave.app/library</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://soundwave.app/favorites</loc>
<changefreq>daily</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://soundwave.app/local-files</loc>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://soundwave.app/settings</loc>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
</urlset>