# NAS Music Player - A Spotify Clone

#This project consists of several components:
#1. SQLite database for music library information
#2. Python backend for scanning and indexing music
#3. PHP web application for streaming and playback
#4. Authentication system for secure remote access
#5. Responsive web frontend that mimics Spotify's design

## Project Structure
#```
#nas-spotify-clone/
#├── database/
#│   ├── schema.sql           # Database schema
#│   └── db_operations.py     # Database operations 
#├── scanner/
#│   ├── music_scanner.py     # Music library scanner
#│   └── metadata.py          # Music metadata extraction
#├── backend/
#│   ├── api.py               # Python API endpoints
#│   ├── stream.php           # PHP streaming endpoints
#│   └── auth.php             # Authentication system
#├── frontend/
#│   ├── index.html           # Main application page
#│   ├── styles/              # CSS files
#│   ├── scripts/             # JavaScript files
#│   └── assets/              # Images and other assets
#├── config.ini               # Configuration file
#└── run.py                   # Main application runner
#```

## 1. Database Schema (schema.sql)

## 2. Database Operations (db_operations.py)

```python
import sqlite3
import os
import hashlib
import datetime
import json

class DatabaseManager:
    def __init__(self, db_path):
        self.db_path = db_path
        self.conn = None
        self.ensure_db_exists()
        
    def ensure_db_exists(self):
        """Create the database and schema if it doesn't exist"""
        db_dir = os.path.dirname(self.db_path)
        if not os.path.exists(db_dir):
            os.makedirs(db_dir)
            
        if not os.path.exists(self.db_path):
            self.connect()
            with open('database/schema.sql', 'r') as f:
                self.conn.executescript(f.read())
            self.conn.commit()
            self.create_admin_user('admin', 'password')  # Default admin user
    
    def connect(self):
        """Connect to the SQLite database"""
        if self.conn is None:
            self.conn = sqlite3.connect(self.db_path)
            self.conn.row_factory = sqlite3.Row
        return self.conn
    
    def close(self):
        """Close the database connection"""
        if self.conn:
            self.conn.close()
            self.conn = None
    
    def create_admin_user(self, username, password):
        """Create an admin user with the given credentials"""
        password_hash = hashlib.sha256(password.encode()).hexdigest()
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT INTO users (username, password_hash) VALUES (?, ?)",
            (username, password_hash)
        )
        self.conn.commit()
    
    def authenticate_user(self, username, password):
        """Authenticate a user and return user data if successful"""
        password_hash = hashlib.sha256(password.encode()).hexdigest()
        cursor = self.conn.cursor()
        cursor.execute(
            "SELECT id, username FROM users WHERE username = ? AND password_hash = ?",
            (username, password_hash)
        )
        user = cursor.fetchone()
        
        if user:
            # Update last login time
            cursor.execute(
                "UPDATE users SET last_login = ? WHERE id = ?",
                (datetime.datetime.now().isoformat(), user['id'])
            )
            self.conn.commit()
            return dict(user)
        return None
    
    def add_artist(self, name):
        """Add an artist to the database or get existing one"""
        cursor = self.conn.cursor()
        cursor.execute("SELECT id FROM artists WHERE name = ?", (name,))
        artist = cursor.fetchone()
        
        if artist:
            return artist['id']
        
        cursor.execute("INSERT INTO artists (name) VALUES (?)", (name,))
        self.conn.commit()
        return cursor.lastrowid
    
    def add_album(self, title, artist_id, release_year=None, cover_path=None):
        """Add an album to the database or get existing one"""
        cursor = self.conn.cursor()
        cursor.execute(
            "SELECT id FROM albums WHERE title = ? AND artist_id = ?", 
            (title, artist_id)
        )
        album = cursor.fetchone()
        
        if album:
            return album['id']
        
        cursor.execute(
            "INSERT INTO albums (title, artist_id, release_year, cover_path) VALUES (?, ?, ?, ?)",
            (title, artist_id, release_year, cover_path)
        )
        self.conn.commit()
        return cursor.lastrowid
    
    def add_track(self, track_data):
        """Add a track to the database"""
        cursor = self.conn.cursor()
        
        # Check if track already exists
        cursor.execute("SELECT id FROM tracks WHERE file_path = ?", (track_data['file_path'],))
        track = cursor.fetchone()
        
        if track:
            return track['id']
        
        # Add the track
        cursor.execute("""
            INSERT INTO tracks (
                title, artist_id, album_id, genre, duration, 
                track_number, file_path, file_format, bitrate
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            track_data['title'],
            track_data['artist_id'],
            track_data['album_id'],
            track_data.get('genre'),
            track_data.get('duration'),
            track_data.get('track_number'),
            track_data['file_path'],
            track_data.get('file_format'),
            track_data.get('bitrate')
        ))
        self.conn.commit()
        return cursor.lastrowid
    
    def search_tracks(self, query, limit=50):
        """Search for tracks by title, artist, or album"""
        search_term = f"%{query}%"
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT t.id, t.title, t.file_path, t.duration, t.track_number, 
                   a.name as artist_name, al.title as album_title, al.cover_path
            FROM tracks t
            LEFT JOIN artists a ON t.artist_id = a.id
            LEFT JOIN albums al ON t.album_id = al.id
            WHERE t.title LIKE ? OR a.name LIKE ? OR al.title LIKE ?
            LIMIT ?
        """, (search_term, search_term, search_term, limit))
        
        tracks = [dict(row) for row in cursor.fetchall()]
        return tracks
    
    def get_albums(self, limit=50, offset=0):
        """Get albums with their cover art"""
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT al.id, al.title, al.release_year, al.cover_path, a.name as artist_name,
                   COUNT(t.id) as track_count
            FROM albums al
            LEFT JOIN artists a ON al.artist_id = a.id
            LEFT JOIN tracks t ON t.album_id = al.id
            GROUP BY al.id
            ORDER BY al.title
            LIMIT ? OFFSET ?
        """, (limit, offset))
        
        albums = [dict(row) for row in cursor.fetchall()]
        return albums
    
    def get_album_tracks(self, album_id):
        """Get all tracks for a specific album"""
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT t.id, t.title, t.file_path, t.duration, t.track_number, 
                   a.name as artist_name
            FROM tracks t
            LEFT JOIN artists a ON t.artist_id = a.id
            WHERE t.album_id = ?
            ORDER BY t.track_number, t.title
        """, (album_id,))
        
        tracks = [dict(row) for row in cursor.fetchall()]
        return tracks
    
    def get_playlists(self, user_id):
        """Get all playlists for a user"""
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT p.id, p.name, p.created_at, COUNT(pt.track_id) as track_count
            FROM playlists p
            LEFT JOIN playlist_tracks pt ON p.id = pt.playlist_id
            WHERE p.user_id = ?
            GROUP BY p.id
            ORDER BY p.updated_at DESC
        """, (user_id,))
        
        playlists = [dict(row) for row in cursor.fetchall()]
        return playlists
    
    def create_playlist(self, user_id, name):
        """Create a new playlist"""
        cursor = self.conn.cursor()
        now = datetime.datetime.now().isoformat()
        cursor.execute(
            "INSERT INTO playlists (name, user_id, created_at, updated_at) VALUES (?, ?, ?, ?)",
            (name, user_id, now, now)
        )
        self.conn.commit()
        return cursor.lastrowid
    
    def add_track_to_playlist(self, playlist_id, track_id, position=None):
        """Add a track to a playlist"""
        cursor = self.conn.cursor()
        
        # Get the next position if not specified
        if position is None:
            cursor.execute(
                "SELECT COALESCE(MAX(position), 0) + 1 FROM playlist_tracks WHERE playlist_id = ?",
                (playlist_id,)
            )
            position = cursor.fetchone()[0]
        
        cursor.execute(
            "INSERT OR REPLACE INTO playlist_tracks (playlist_id, track_id, position) VALUES (?, ?, ?)",
            (playlist_id, track_id, position)
        )
        
        # Update the playlist's updated_at timestamp
        cursor.execute(
            "UPDATE playlists SET updated_at = ? WHERE id = ?",
            (datetime.datetime.now().isoformat(), playlist_id)
        )
        
        self.conn.commit()
    
    def get_playlist_tracks(self, playlist_id):
        """Get all tracks in a playlist"""
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT t.id, t.title, t.file_path, t.duration, 
                   a.name as artist_name, al.title as album_title, al.cover_path,
                   pt.position
            FROM playlist_tracks pt
            JOIN tracks t ON pt.track_id = t.id
            LEFT JOIN artists a ON t.artist_id = a.id
            LEFT JOIN albums al ON t.album_id = al.id
            WHERE pt.playlist_id = ?
            ORDER BY pt.position
        """, (playlist_id,))
        
        tracks = [dict(row) for row in cursor.fetchall()]
        return tracks
    
    def add_to_favorites(self, user_id, track_id):
        """Add a track to user's favorites"""
        cursor = self.conn.cursor()
        cursor.execute(
            "INSERT OR IGNORE INTO favorites (user_id, track_id) VALUES (?, ?)",
            (user_id, track_id)
        )
        self.conn.commit()
    
    def remove_from_favorites(self, user_id, track_id):
        """Remove a track from user's favorites"""
        cursor = self.conn.cursor()
        cursor.execute(
            "DELETE FROM favorites WHERE user_id = ? AND track_id = ?",
            (user_id, track_id)
        )
        self.conn.commit()
    
    def get_favorites(self, user_id):
        """Get all favorite tracks for a user"""
        cursor = self.conn.cursor()
        cursor.execute("""
            SELECT t.id, t.title, t.file_path, t.duration, 
                   a.name as artist_name, al.title as album_title, al.cover_path
            FROM favorites f
            JOIN tracks t ON f.track_id = t.id
            LEFT JOIN artists a ON t.artist_id = a.id
            LEFT JOIN albums al ON t.album_id = al.id
            WHERE f.user_id = ?
            ORDER BY f.added_at DESC
        """, (user_id,))
        
        tracks = [dict(row) for row in cursor.fetchall()]
        return tracks
    
    def update_play_count(self, track_id):
        """Increment the play count for a track"""
        cursor = self.conn.cursor()
        cursor.execute(
            "UPDATE tracks SET play_count = play_count + 1 WHERE id = ?",
            (track_id,)
        )
        self.conn.commit()
    
    def get_stats(self):
        """Get database statistics"""
        cursor = self.conn.cursor()
        cursor.execute("SELECT COUNT(*) FROM tracks")
        track_count = cursor.fetchone()[0]
        
        cursor.execute("SELECT COUNT(*) FROM albums")
        album_count = cursor.fetchone()[0]
        
        cursor.execute("SELECT COUNT(*) FROM artists")
        artist_count = cursor.fetchone()[0]
        
        cursor.execute("SELECT COUNT(*) FROM playlists")
        playlist_count = cursor.fetchone()[0]
        
        return {
            'tracks': track_count,
            'albums': album_count,
            'artists': artist_count,
            'playlists': playlist_count
        }
```

## 3. Music Scanner (music_scanner.py)

```python
import os
import time
import json
import logging
from metadata import MetadataExtractor
from db_operations import DatabaseManager

class MusicScanner:
    def __init__(self, config):
        self.nas_paths = config['music_paths']
        self.supported_formats = config['supported_formats']
        self.db_manager = DatabaseManager(config['database_path'])
        self.metadata_extractor = MetadataExtractor()
        self.logger = self._setup_logger()
    
    def _setup_logger(self):
        logger = logging.getLogger('music_scanner')
        logger.setLevel(logging.INFO)
        
        handler = logging.FileHandler('scanner.log')
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        handler.setFormatter(formatter)
        
        logger.addHandler(handler)
        return logger
    
    def scan_library(self):
        """Scan all configured music directories and update the database"""
        start_time = time.time()
        self.logger.info("Starting music library scan")
        
        for music_path in self.nas_paths:
            if not os.path.exists(music_path):
                self.logger.error(f"Music path does not exist: {music_path}")
                continue
                
            self.logger.info(f"Scanning directory: {music_path}")
            self._scan_directory(music_path)
        
        elapsed_time = time.time() - start_time
        self.logger.info(f"Scan completed in {elapsed_time:.2f} seconds")
        
        stats = self.db_manager.get_stats()
        self.logger.info(f"Library stats: {json.dumps(stats)}")
        
        return stats
    
    def _scan_directory(self, directory):
        """Recursively scan a directory for music files"""
        for root, dirs, files in os.walk(directory):
            for file in files:
                if self._is_supported_format(file):
                    file_path = os.path.join(root, file)
                    self._process_file(file_path)
    
    def _is_supported_format(self, filename):
        """Check if the file has a supported music format"""
        ext = os.path.splitext(filename)[1].lower().lstrip('.')
        return ext in self.supported_formats
    
    def _process_file(self, file_path):
        """Extract metadata and add the file to the database"""
        try:
            metadata = self.metadata_extractor.extract(file_path)
            
            if not metadata:
                self.logger.warning(f"Could not extract metadata from: {file_path}")
                return
            
            # Add artist
            artist_id = self.db_manager.add_artist(metadata.get('artist', 'Unknown Artist'))
            
            # Add album
            album_id = self.db_manager.add_album(
                metadata.get('album', 'Unknown Album'),
                artist_id,
                metadata.get('year'),
                metadata.get('cover_path')
            )
            
            # Add track
            track_data = {
                'title': metadata.get('title', os.path.basename(file_path)),
                'artist_id': artist_id,
                'album_id': album_id,
                'genre': metadata.get('genre'),
                'duration': metadata.get('duration'),
                'track_number': metadata.get('track_number'),
                'file_path': file_path,
                'file_format': os.path.splitext(file_path)[1].lower().lstrip('.'),
                'bitrate': metadata.get('bitrate')
            }
            
            track_id = self.db_manager.add_track(track_data)
            
        except Exception as e:
            self.logger.error(f"Error processing file {file_path}: {str(e)}")

    def cleanup(self):
        """Close database connection"""
        self.db_manager.close()
```

## 4. Music Metadata Extractor (metadata.py)

```python
import os
import json
import tempfile
import subprocess
from PIL import Image

class MetadataExtractor:
    def __init__(self):
        # Check for dependencies
        self._check_dependencies()
        
    def _check_dependencies(self):
        """Check if required command-line tools are available"""
        try:
            # Check for ffprobe (part of ffmpeg)
            subprocess.run(['ffprobe', '-version'], 
                          stdout=subprocess.PIPE, 
                          stderr=subprocess.PIPE)
        except FileNotFoundError:
            print("Warning: ffprobe not found. Install ffmpeg for better metadata extraction.")
    
    def extract(self, file_path):
        """Extract metadata from a music file"""
        if not os.path.exists(file_path):
            print(f"File not found: {file_path}")
            return None
        
        # Try to extract metadata using ffprobe
        metadata = self._extract_with_ffprobe(file_path)
        
        # If ffprobe failed, try to get basic info from file
        if not metadata:
            metadata = self._extract_basic_info(file_path)
        
        # Extract cover art if available
        cover_path = self._extract_cover_art(file_path)
        if cover_path:
            metadata['cover_path'] = cover_path
            
        return metadata
    
    def _extract_with_ffprobe(self, file_path):
        """Extract metadata using ffprobe"""
        try:
            cmd = [
                'ffprobe', 
                '-v', 'quiet',
                '-print_format', 'json',
                '-show_format',
                '-show_streams',
                file_path
            ]
            
            result = subprocess.run(cmd, 
                                   stdout=subprocess.PIPE, 
                                   stderr=subprocess.PIPE,
                                   text=True)
            
            if result.returncode != 0:
                return None
                
            data = json.loads(result.stdout)
            
            # Extract relevant metadata
            metadata = {}
            
            if 'format' in data:
                fmt = data['format']
                
                # Get duration in seconds
                if 'duration' in fmt:
                    metadata['duration'] = int(float(fmt['duration']))
                
                # Get bitrate
                if 'bit_rate' in fmt:
                    metadata['bitrate'] = int(int(fmt['bit_rate']) / 1000)  # Convert to kbps
                
                # Get tags
                if 'tags' in fmt:
                    tags = fmt['tags']
                    
                    # Map common tag names to our metadata keys
                    tag_mapping = {
                        'title': ['title', 'TITLE'],
                        'artist': ['artist', 'ARTIST', 'album_artist', 'ALBUM_ARTIST'],
                        'album': ['album', 'ALBUM'],
                        'genre': ['genre', 'GENRE'],
                        'year': ['date', 'year', 'YEAR', 'DATE'],
                        'track_number': ['track', 'TRACK']
                    }
                    
                    for meta_key, tag_keys in tag_mapping.items():
                        for tag_key in tag_keys:
                            if tag_key in tags:
                                value = tags[tag_key]
                                
                                # Convert track number (e.g. "1/12" to 1)
                                if meta_key == 'track_number' and '/' in value:
                                    value = value.split('/')[0]
                                
                                # Try to convert year to int
                                if meta_key == 'year' and value:
                                    try:
                                        # Extract first 4 digits if it's a date
                                        if len(value) > 4:
                                            value = value[:4]
                                        value = int(value)
                                    except ValueError:
                                        pass
                                
                                metadata[meta_key] = value
                                break
            
            return metadata
            
        except Exception as e:
            print(f"Error extracting metadata with ffprobe: {str(e)}")
            return None
    
    def _extract_basic_info(self, file_path):
        """Extract basic info from filename when metadata extraction fails"""
        filename = os.path.basename(file_path)
        name, _ = os.path.splitext(filename)
        
        # Try to parse "Artist - Title" format
        parts = name.split(' - ', 1)
        
        metadata = {
            'title': name,
            'file_path': file_path
        }
        
        if len(parts) == 2:
            metadata['artist'] = parts[0].strip()
            metadata['title'] = parts[1].strip()
        
        # Get file size and estimate duration (very rough)
        try:
            file_size = os.path.getsize(file_path) / (1024 * 1024)  # Size in MB
            # Rough estimate: 1MB ≈ 1 minute of music at moderate quality
            estimated_duration = int(file_size * 60)
            metadata['duration'] = estimated_duration
        except:
            pass
            
        return metadata
    
    def _extract_cover_art(self, file_path):
        """Extract cover art from audio file"""
        try:
            # Create a temporary directory for the cover art
            covers_dir = os.path.join('frontend', 'assets', 'covers')
            if not os.path.exists(covers_dir):
                os.makedirs(covers_dir)
            
            # Create a filename based on the audio file path
            file_hash = str(hash(file_path))
            cover_filename = f"cover_{file_hash}.jpg"
            cover_path = os.path.join(covers_dir, cover_filename)
            
            # Check if we already extracted this cover
            if os.path.exists(cover_path):
                return cover_path
                
            # Extract cover art using ffmpeg
            cmd = [
                'ffmpeg',
                '-i', file_path,
                '-an', '-vcodec', 'copy',
                cover_path
            ]
            
            result = subprocess.run(cmd, 
                                  stdout=subprocess.PIPE, 
                                  stderr=subprocess.PIPE)
            
            # If successful, return the path to the cover art
            if result.returncode == 0 and os.path.exists(cover_path):
                # Verify it's a valid image
                try:
                    with Image.open(cover_path) as img:
                        # If it's too small, it might not be a real cover
                        if img.width < 50 or img.height < 50:
                            os.remove(cover_path)
                            return None
                    return cover_path
                except:
                    # Not a valid image
                    if os.path.exists(cover_path):
                        os.remove(cover_path)
                    return None
            else:
                return None
                
        except Exception as e:
            print(f"Error extracting cover art: {str(e)}")
            return None
```

## 5. PHP Streaming Backend (stream.php)

```php
<?php
/**
 * Music Streaming Script
 * 
 * This script handles secure streaming of music files from the NAS to the client
 * with authentication, range requests (for seeking), and bandwidth control.
 */

// Start session for authentication
session_start();

// Configuration
$config = parse_ini_file('config.ini', true);
$dbPath = $config['database']['path'];
$tokenSecret = $config['app']['secret_key'];
$streamRate = isset($config['streaming']['rate_limit']) ? 
    (int)$config['streaming']['rate_limit'] : 0; // KB/s (0 = unlimited)

// Connect to SQLite database
try {
    $db = new PDO('sqlite:' . $dbPath);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    header('HTTP/1.1 500 Internal Server Error');
    exit('Database connection failed: ' . $e->getMessage());
}

// Check if user is authenticated with token
function isAuthenticated($token) {
    global $tokenSecret, $db;
    
    // Verify token format (should be "userId:timestamp:hash")
    $parts = explode(':', $token);
    if (count($parts) !== 3) {
        return false;
    }
    
    list($userId, $timestamp, $hash) = $parts;
    
    // Check if token is expired (24 hour validity)
    if (time() - (int)$timestamp > 86400) {
        return false;
    }
    
    // Verify token hash
    $expectedHash = hash_hmac('sha256', "$userId:$timestamp", $tokenSecret);
    if (!hash_equals($expectedHash, $hash)) {
        return false;
    }
    
    // Check if user exists
    $stmt = $db->prepare('SELECT id FROM users WHERE id = ?');
    $stmt->execute([$userId]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
    
    return $user ? true : false;
}

// Function to get track information from database
function getTrack($trackId) {
    global $db;
    
    $stmt = $db->prepare('
        SELECT t.id, t.title, t.file_path, t.file_format,
               a.name as artist_name, al.title as album_title
        FROM tracks t
        LEFT JOIN artists a ON t.artist_id = a.id
        LEFT JOIN albums al ON t.album_id = al.id
        WHERE t.id = ?
    ');
    $stmt->execute([$trackId]);
    
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

// Check required parameters
if (!isset($_GET['track']) || !isset($_GET['token'])) {
    header('HTTP/1.1 400 Bad Request');
    exit('Missing parameters');
}

$trackId = (int)$_GET['track'];
$token = $_GET['token'];

// Authenticate user
if (!isAuthenticated($token)) {
    header('HTTP/1.1 401 Unauthorized');
    exit('Invalid or expired token');
}

// Get track information
$track = getTrack($trackId);
if (!$track) {
    header('HTTP/1.1 404 Not Found');
    exit('Track not found');
}

$filePath = $track['file_path'];

// Check if file exists
if (!file_exists($filePath)) {
    header('HTTP/1.1 404 Not Found');
    exit('File not found');
}

// Get file information
$fileSize = filesize($filePath);
$fileTime = filemtime($filePath);

// Set content type based on file format
$contentTypes = [
    'mp3' => 'audio/mpeg',
    'flac' => 'audio/flac',
    'ogg' => 'audio/ogg',
    'aac' => 'audio/aac',
    'm4a' => 'audio/mp4',
    'wav' => 'audio/wav'
];

$contentType = isset($contentTypes[$track['file_format']]) ? 
    $contentTypes[$track['file_format']] : 'application/octet-stream';

// Set headers
header("Content-Type: $contentType");
header('Accept-Ranges: bytes');
header('Cache-Control: max-age=86400, public'); // Cache for 24 hours
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $fileTime) . ' GMT');
header('Etag: "' . md5($filePath . $fileTime) . '"');
header('Content-Disposition: inline; filename="' . basename($filePath) . '"');

// Handle range requests (for seeking)
$start = 0;
$end = $fileSize - 1;

if (isset($_SERVER['HTTP_RANGE'])) {
    // Parse range header
    $ranges = explode('=', $_SERVER['HTTP_RANGE']);
    if ($ranges[0] === 'bytes') {
        $range = explode('-', $ranges[1]);
        $start = isset($range[0]) && $range[0] !== '' ? intval($range[0]) : 0;
        $end = isset($range[1]) && $range[1] !== '' ? intval($range[1]) : $fileSize - 1;
        
        // Validate range
        if ($start > $end || $start >= $fileSize || $end >= $fileSize) {
            header('HTTP/1.1 416 Requested Range Not Satisfiable');
            header("Content-Range: bytes */$fileSize");
            exit;
        }
        
        // Set partial content response
        header('HTTP/1.1 206 Partial Content');
        header("Content-Range: bytes $start-$end/$fileSize");
        header('Content-Length: ' . ($end - $start + 1));
    } else {
        header('HTTP/1.1 416 Requested Range Not Satisfiable');
        exit;
    }
} else {
    header('Content-Length: ' . $fileSize);
}

// Check if this is a HEAD request
if ($_SERVER['REQUEST_METHOD'] === 'HEAD') {
    exit;
}

// Log play count if full file is requested
if ($start === 0 && $end === $fileSize - 1) {
    $stmt = $db->prepare('UPDATE tracks SET play_count = play_count + 1 WHERE id = ?');
    $stmt->execute([$trackId]);
}

// Stream the file with rate limiting
$handle = fopen($filePath, 'rb');
if ($handle === false) {
    header('HTTP/1.1 500 Internal Server Error');
    exit('Could not open file for reading');
}

// Seek to start position
fseek($handle, $start);

// Disable output buffering
if (ob_get_level()) {
    ob_end_clean();
}

// Set time limit to 0 for large files
set_time_limit(0);

// Calculate chunk size - smaller for rate limiting, larger for faster transfer
$chunkSize = $streamRate > 0 ? min(1024 * $streamRate, 8192) : 8192;
$bytesSent = 0;

// Stream the file in chunks
while (!feof($handle) && ($bytesSent < ($end - $start + 1))) {
    // Calculate bytes left to send
    $bytesLeft = ($end - $start + 1) - $bytesSent;
    
    // Read and output chunk
    $buffer = fread($handle, min($chunkSize, $bytesLeft));
    echo $buffer;
    
    // Flush the output buffer
    flush();
    
    // Update bytes sent
    $bytesSent += strlen($buffer);
    
    // Apply rate limiting
    if ($streamRate > 0) {
        // Sleep to maintain rate limit
        usleep(1000000 * (strlen($buffer) / ($streamRate * 1024)));
    }
}

// Close file handle
fclose($handle);
exit;
```

## 6. PHP Authentication System (auth.php)

```php
<?php
/**
 * Authentication System
 * 
 * This script handles user authentication and token generation for secure streaming.
 */

// Start session
session_start();

// Load configuration
$config = parse_ini_file('config.ini', true);
$dbPath = $config['database']['path'];
$tokenSecret = $config['app']['secret_key'];

// Connect to SQLite database
try {
    $db = new PDO('sqlite:' . $dbPath);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]);
    exit;
}

// Function to generate a secure streaming token
function generateStreamToken($userId, $tokenSecret) {
    $timestamp = time();
    $hash = hash_hmac('sha256', "$userId:$timestamp", $tokenSecret);
    return "$userId:$timestamp:$hash";
}

// Handle API requests
$action = isset($_GET['action']) ? $_GET['action'] : '';

switch ($action) {
    case 'login':
        // Process login
        $data = json_decode(file_get_contents('php://input'), true);
        $username = isset($data['username']) ? $data['username'] : '';
        $password = isset($data['password']) ? $data['password'] : '';
        
        // Validate input
        if (empty($username) || empty($password)) {
            header('Content-Type: application/json');
            echo json_encode(['error' => 'Username and password are required']);
            exit;
        }
        
        // Hash the password (using SHA-256 for simplicity - in production, use better methods)
        $passwordHash = hash('sha256', $password);
        
        // Query the database
        $stmt = $db->prepare('
            SELECT id, username 
            FROM users 
            WHERE username = ? AND password_hash = ?
        ');
        $stmt->execute([$username, $passwordHash]);
        
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($user) {
            // Update last login time
            $stmt = $db->prepare('UPDATE users SET last_login = ? WHERE id = ?');
            $stmt->execute([date('Y-m-d H:i:s'), $user['id']]);
            
            // Create session
            $_SESSION['user_id'] = $user['id'];
            $_SESSION['username'] = $user['username'];
            
            // Generate streaming token
            $token = generateStreamToken($user['id'], $tokenSecret);
            
            header('Content-Type: application/json');
            echo json_encode([
                'success' => true,
                'user' => $user,
                'token' => $token
            ]);
        } else {
            header('Content-Type: application/json');
            echo json_encode([
                'success' => false,
                'error' => 'Invalid username or password'
            ]);
        }
        break;
        
    case 'logout':
        // Clear session
        session_unset();
        session_destroy();
        
        header('Content-Type: application/json');
        echo json_encode(['success' => true]);
        break;
        
    case 'check':
        // Check if user is logged in
        if (isset($_SESSION['user_id'])) {
            $token = generateStreamToken($_SESSION['user_id'], $tokenSecret);
            
            header('Content-Type: application/json');
            echo json_encode([
                'authenticated' => true,
                'user' => [
                    'id' => $_SESSION['user_id'],
                    'username' => $_SESSION['username']
                ],
                'token' => $token
            ]);
        } else {
            header('Content-Type: application/json');
            echo json_encode(['authenticated' => false]);
        }
        break;
        
    case 'token':
        // Generate a new streaming token (requires authenticated session)
        if (isset($_SESSION['user_id'])) {
            $token = generateStreamToken($_SESSION['user_id'], $tokenSecret);
            
            header('Content-Type: application/json');
            echo json_encode([
                'success' => true,
                'token' => $token
            ]);
        } else {
            header('Content-Type: application/json');
            echo json_encode([
                'success' => false,
                'error' => 'Not authenticated'
            ]);
        }
        break;
        
    default:
        header('Content-Type: application/json');
        echo json_encode(['error' => 'Unknown action']);
        break;
}
```

## 7. Frontend - Main HTML (index.html)

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>NAS Music Player</title>
    <link rel="stylesheet" href="styles/reset.css">
    <link rel="stylesheet" href="styles/main.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css">
    <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
    <!-- Login Screen -->
    <div id="login-container" class="screen">
        <div class="login-box">
            <div class="logo">
                <i class="fas fa-music"></i>
                <h1>NAS Music Player</h1>
            </div>
            <form id="login-form">
                <div class="input-group">
                    <label for="username">Username</label>
                    <input type="text" id="username" name="username" required>
                </div>
                <div class="input-group">
                    <label for="password">Password</label>
                    <input type="password" id="password" name="password" required>
                </div>
                <button type="submit" class="btn primary-btn">Log In</button>
                <div id="login-error" class="error-message"></div>
            </form>
        </div>
    </div>

    <!-- App Container -->
    <div id="app-container" class="screen hidden">
        <!-- Sidebar -->
        
}

```python
from flask import Flask, request, jsonify, send_file, session, redirect, url_for
from flask_cors import CORS
import os
import time
import json
from db_operations import DatabaseManager
from music_scanner import MusicScanner
import configparser

# Load configuration
config = configparser.ConfigParser()
config.read('config.ini')

app = Flask(__name__)
app.secret_key = config['app']['secret_key']
CORS(app)

# Initialize database manager
db_manager = DatabaseManager(config['database']['path'])

@app.route('/api/login', methods=['POST'])
def login():
    """Authenticate user"""
    data = request.json
    username = data.get('username')
    password = data.get('password')
    
    user = db_manager.authenticate_user(username, password)
    
    if user:
        session['user_id'] = user['id']
        session['username'] = user['username']
        return jsonify({
            'success': True,
            'user': user
        })
    
    return jsonify({
        'success': False,
        'message': 'Invalid username or password'
    }), 401

@app.route('/api/logout', methods=['POST'])
def logout():
    """Log out user"""
    session.clear()
    return jsonify({'success': True})

@app.route('/api/user', methods=['GET'])
def get_user():
    """Get current user info"""
    if 'user_id' not in session:
        return jsonify({'authenticated': False}), 401
    
    return jsonify({
        'authenticated': True,
        'user': {
            'id': session['user_id'],
            'username': session['username']
        }
    })

@app.route('/api/scan', methods=['POST'])
def scan_library():
    """Scan music library"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    music_config = {
        'music_paths': json.loads(config['music']['paths']),
        'supported_formats': json.loads(config['music']['formats']),
        'database_path': config['database']['path']
    }
    
    scanner = MusicScanner(music_config)
    stats = scanner.scan_library()
    
    return jsonify({
        'success': True,
        'stats': stats
    })

@app.route('/api/search', methods=['GET'])
def search():
    """Search for music"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    query = request.args.get('q', '')
    limit = int(request.args.get('limit', 50))
    
    if not query:
        return jsonify({'results': []})
    
    results = db_manager.search_tracks(query, limit)
    
    return jsonify({
        'results': results
    })

@app.route('/api/albums', methods=['GET'])
def get_albums():
    """Get album list"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    limit = int(request.args.get('limit', 50))
    offset = int(request.args.get('offset', 0))
    
    albums = db_manager.get_albums(limit, offset)
    
    return jsonify({
        'albums': albums
    })

@app.route('/api/albums/<int:album_id>/tracks', methods=['GET'])
def get_album_tracks(album_id):
    """Get tracks for an album"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    tracks = db_manager.get_album_tracks(album_id)
    
    return jsonify({
        'tracks': tracks
    })

@app.route('/api/playlists', methods=['GET'])
def get_playlists():
    """Get user's playlists"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    playlists = db_manager.get_playlists(session['user_id'])
    
    return jsonify({
        'playlists': playlists
    })

@app.route('/api/playlists', methods=['POST'])
def create_playlist():
    """Create a new playlist"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    data = request.json
    name = data.get('name')
    
    if not name:
        return jsonify({'error': 'Playlist name is required'}), 400
    
    playlist_id = db_manager.create_playlist(session['user_id'], name)
    
    return jsonify({
        'success': True,
        'playlist_id': playlist_id
    })

@app.route('/api/playlists/<int:playlist_id>/tracks', methods=['GET'])
def get_playlist_tracks(playlist_id):
    """Get tracks in a playlist"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    tracks = db_manager.get_playlist_tracks(playlist_id)
    
    return jsonify({
        'tracks': tracks
    })

@app.route('/api/playlists/<int:playlist_id>/tracks', methods=['POST'])
def add_track_to_playlist(playlist_id):
    """Add a track to a playlist"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    data = request.json
    track_id = data.get('track_id')
    position = data.get('position')
    
    if not track_id:
        return jsonify({'error': 'Track ID is required'}), 400
    
    db_manager.add_track_to_playlist(playlist_id, track_id, position)
    
    return jsonify({
        'success': True
    })

@app.route('/api/favorites', methods=['GET'])
def get_favorites():
    """Get user's favorite tracks"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    favorites = db_manager.get_favorites(session['user_id'])
    
    return jsonify({
        'favorites': favorites
    })

@app.route('/api/favorites/<int:track_id>', methods=['POST'])
def add_favorite(track_id):
    """Add a track to favorites"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    db_manager.add_to_favorites(session['user_id'], track_id)
    
    return jsonify({
        'success': True
    })

@app.route('/api/favorites/<int:track_id>', methods=['DELETE'])
def remove_favorite(track_id):
    """Remove a track from favorites"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    db_manager.remove_from_favorites(session['user_id'], track_id)
    
    return jsonify({
        'success': True
    })

@app.route('/api/tracks/<int:track_id>/play', methods=['POST'])
def record_play(track_id):
    """Record that a track was played"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    db_manager.update_play_count(track_id)
    
    return jsonify({
        'success': True
    })

@app.route('/api/stats', methods=['GET'])
def get_stats():
    """Get library statistics"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    stats = db_manager.get_stats()
    
    return jsonify({
        'stats': stats
    })

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)
```

## 6. Python API Backend (api.py)

```python
from flask import Flask, request, jsonify, send_file, session, redirect, url_for
from flask_cors import CORS
import os
import time
import json
from db_operations import DatabaseManager
from music_scanner import MusicScanner
import configparser

# Load configuration
config = configparser.ConfigParser()
config.read('config.ini')

app = Flask(__name__)
app.secret_key = config['app']['secret_key']
CORS(app)

# Initialize database manager
db_manager = DatabaseManager(config['database']['path'])

@app.route('/api/login', methods=['POST'])
def login():
    """Authenticate user"""
    data = request.json
    username = data.get('username')
    password = data.get('password')
    
    user = db_manager.authenticate_user(username, password)
    
    if user:
        session['user_id'] = user['id']
        session['username'] = user['username']
        return jsonify({
            'success': True,
            'user': user
        })
    
    return jsonify({
        'success': False,
        'message': 'Invalid username or password'
    }), 401

@app.route('/api/logout', methods=['POST'])
def logout():
    """Log out user"""
    session.clear()
    return jsonify({'success': True})

@app.route('/api/user', methods=['GET'])
def get_user():
    """Get current user info"""
    if 'user_id' not in session:
        return jsonify({'authenticated': False}), 401
    
    return jsonify({
        'authenticated': True,
        'user': {
            'id': session['user_id'],
            'username': session['username']
        }
    })

@app.route('/api/scan', methods=['POST'])
def scan_library():
    """Scan music library"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    music_config = {
        'music_paths': json.loads(config['music']['paths']),
        'supported_formats': json.loads(config['music']['formats']),
        'database_path': config['database']['path']
    }
    
    scanner = MusicScanner(music_config)
    stats = scanner.scan_library()
    
    return jsonify({
        'success': True,
        'stats': stats
    })

@app.route('/api/search', methods=['GET'])
def search():
    """Search for music"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    query = request.args.get('q', '')
    limit = int(request.args.get('limit', 50))
    
    if not query:
        return jsonify({'results': []})
    
    results = db_manager.search_tracks(query, limit)
    
    return jsonify({
        'results': results
    })

@app.route('/api/albums', methods=['GET'])
def get_albums():
    """Get album list"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    limit = int(request.args.get('limit', 50))
    offset = int(request.args.get('offset', 0))
    
    albums = db_manager.get_albums(limit, offset)
    
    return jsonify({
        'albums': albums
    })

@app.route('/api/albums/<int:album_id>/tracks', methods=['GET'])
def get_album_tracks(album_id):
    """Get tracks for an album"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    tracks = db_manager.get_album_tracks(album_id)
    
    return jsonify({
        'tracks': tracks
    })

@app.route('/api/playlists', methods=['GET'])
def get_playlists():
    """Get user's playlists"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    playlists = db_manager.get_playlists(session['user_id'])
    
    return jsonify({
        'playlists': playlists
    })

@app.route('/api/playlists', methods=['POST'])
def create_playlist():
    """Create a new playlist"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    data = request.json
    name = data.get('name')
    
    if not name:
        return jsonify({'error': 'Playlist name is required'}), 400
    
    playlist_id = db_manager.create_playlist(session['user_id'], name)
    
    return jsonify({
        'success': True,
        'playlist_id': playlist_id
    })

@app.route('/api/playlists/<int:playlist_id>/tracks', methods=['GET'])
def get_playlist_tracks(playlist_id):
    """Get tracks in a playlist"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    tracks = db_manager.get_playlist_tracks(playlist_id)
    
    return jsonify({
        'tracks': tracks
    })

@app.route('/api/playlists/<int:playlist_id>/tracks', methods=['POST'])
def add_track_to_playlist(playlist_id):
    """Add a track to a playlist"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    data = request.json
    track_id = data.get('track_id')
    position = data.get('position')
    
    if not track_id:
        return jsonify({'error': 'Track ID is required'}), 400
    
    db_manager.add_track_to_playlist(playlist_id, track_id, position)
    
    return jsonify({
        'success': True
    })

@app.route('/api/favorites', methods=['GET'])
def get_favorites():
    """Get user's favorite tracks"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    favorites = db_manager.get_favorites(session['user_id'])
    
    return jsonify({
        'favorites': favorites
    })

@app.route('/api/favorites/<int:track_id>', methods=['POST'])
def add_favorite(track_id):
    """Add a track to favorites"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    db_manager.add_to_favorites(session['user_id'], track_id)
    
    return jsonify({
        'success': True
    })

@app.route('/api/favorites/<int:track_id>', methods=['DELETE'])
def remove_favorite(track_id):
    """Remove a track from favorites"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    db_manager.remove_from_favorites(session['user_id'], track_id)
    
    return jsonify({
        'success': True
    })

@app.route('/api/tracks/<int:track_id>/play', methods=['POST'])
def record_play(track_id):
    """Record that a track was played"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    db_manager.update_play_count(track_id)
    
    return jsonify({
        'success': True
    })

@app.route('/api/stats', methods=['GET'])
def get_stats():
    """Get library statistics"""
    if 'user_id' not in session:
        return jsonify({'error': 'Authentication required'}), 401
    
    stats = db_manager.get_stats()
    
    return jsonify({
        'stats': stats
    })

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)