<#
.SYNOPSIS
    PSMediaLibrary - Advanced Media Library Manager with Web Interface
.DESCRIPTION
    A PowerShell-based media library management system with SQLite database,
    web interface, metadata fetching, and intelligent media analysis.
.PARAMETER AutoScan
    Automatically scan media folders on startup without prompting (default: $false)
.PARAMETER FetchPosters
    Automatically fetch movie posters from TMDB without prompting (default: $false)
.PARAMETER ExtractAlbumArt
    Automatically extract album artwork from music files without prompting (default: $false)
.PARAMETER GeneratePDFThumbnails
    Automatically generate PDF preview thumbnails without prompting (default: $false)
.PARAMETER StartServer
    Automatically start the web server without prompting (default: $false)
.PARAMETER Silent
    Run in silent mode - no prompts, uses default values (same as -AutoScan:$false -FetchPosters:$false -ExtractAlbumArt:$false -GeneratePDFThumbnails:$false -StartServer:$true)
.EXAMPLE
    .\PSMediaLibrary_v0.7.ps1
    Interactive mode - prompts for all options
.EXAMPLE
    .\PSMediaLibrary_v0.7.ps1 -StartServer
    Skip all setup, just start the server
.EXAMPLE
    .\PSMediaLibrary_v0.7.ps1 -AutoScan -FetchPosters -StartServer
    Scan media, fetch posters, then start server
.EXAMPLE
    .\PSMediaLibrary_v0.7.ps1 -AutoScan -GeneratePDFThumbnails -StartServer
    Scan media, generate PDF thumbnails, then start server
.EXAMPLE
    .\PSMediaLibrary_v0.7.ps1 -Silent
    Silent mode - just start the server with existing data
.NOTES
    Requires: PowerShell 7.x, PSSQLite module, Pode module (Not used yet), Ghostscript, ffmpeg, epub.min.js, jszip.min.js, video.min.js, video-js.min.css
    Author: Michael DALLA RIVA / Blog : https://lafrenchaieti.com/
    Official NexusStack Website : https://nexusstack.org/
    Version: Beta v0.7 - Public Preview
    Release date : January 1, 2026
    License : See https://nexusstack.org/ -- Commercial use prohibited!
#>

#Requires -Version 7.0

[CmdletBinding()]
param(
    [Parameter(HelpMessage="Automatically scan media folders on startup")]
    [switch]$AutoScan,
    
    [Parameter(HelpMessage="Automatically fetch movie posters from TMDB")]
    [switch]$FetchPosters,
    
    [Parameter(HelpMessage="Automatically extract album artwork from music files")]
    [switch]$ExtractAlbumArt,
    
    [Parameter(HelpMessage="Automatically generate PDF/EPUB preview thumbnails")]
    [switch]$GeneratePDFThumbnails,
    
    [Parameter(HelpMessage="Automatically start the web server")]
    [switch]$StartServer,
    
    [Parameter(HelpMessage="Run in silent mode (no prompts, start server only)")]
    [switch]$Silent
)

# ============================================================================
# USER MANAGEMENT MODULE (v1.0)
# ============================================================================

# Global session storage
$Global:UserSessions = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
$Global:CurrentUser = $null

<#
.SYNOPSIS
    Encrypts a PIN code using SHA256 hashing with salt
#>
function Get-EncryptedPIN {
    param(
        [Parameter(Mandatory=$true)]
        [string]$PIN,
        
        [Parameter(Mandatory=$false)]
        [string]$Salt
    )
    
    if ([string]::IsNullOrWhiteSpace($Salt)) {
        # Generate random salt
        $saltBytes = New-Object byte[] 32
        $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
        $rng.GetBytes($saltBytes)
        $Salt = [Convert]::ToBase64String($saltBytes)
    }
    
    $saltedPIN = $PIN + $Salt
    $sha256 = [System.Security.Cryptography.SHA256]::Create()
    $hashBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($saltedPIN))
    $hash = [Convert]::ToBase64String($hashBytes)
    
    return @{
        Hash = $hash
        Salt = $Salt
    }
}

<#
.SYNOPSIS
    Validates a PIN code against stored hash
#>
function Test-PINCode {
    param(
        [Parameter(Mandatory=$true)]
        [string]$PIN,
        
        [Parameter(Mandatory=$true)]
        [string]$Hash,
        
        [Parameter(Mandatory=$true)]
        [string]$Salt
    )
    
    $encrypted = Get-EncryptedPIN -PIN $PIN -Salt $Salt
    return $encrypted.Hash -eq $Hash
}

<#
.SYNOPSIS
    Initializes the main users database
#>
function Initialize-UsersDatabase {
    param(
        [Parameter(Mandatory=$true)]
        [string]$DatabasePath
    )
    
    try {
        # Create users directory if it doesn't exist
        $usersDir = Split-Path -Parent $DatabasePath
        if (-not (Test-Path $usersDir)) {
            New-Item -Path $usersDir -ItemType Directory -Force | Out-Null
        }
        
        # Create users table
        $createTableQuery = @"
CREATE TABLE IF NOT EXISTS users (
    user_id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL UNIQUE,
    pin_hash TEXT NOT NULL,
    pin_salt TEXT NOT NULL,
    user_type TEXT NOT NULL CHECK(user_type IN ('admin', 'guest')),
    created_date TEXT NOT NULL,
    last_login TEXT,
    is_active INTEGER DEFAULT 1,
    display_name TEXT,
    avatar_path TEXT
);
"@
        
        Invoke-SqliteQuery -DataSource $DatabasePath -Query $createTableQuery
        
        # Check if admin user exists
        $adminCheck = Invoke-SqliteQuery -DataSource $DatabasePath -Query "SELECT COUNT(*) as count FROM users WHERE user_type = 'admin'"
        
        if ($adminCheck.count -eq 0) {
            Write-Host "[i] No admin user found. Creating default admin user..." -ForegroundColor Yellow
            
            # Create default admin user with PIN: 000000
            $adminPIN = "000000"
            $encrypted = Get-EncryptedPIN -PIN $adminPIN
            
            $insertQuery = @"
INSERT INTO users (username, pin_hash, pin_salt, user_type, created_date, display_name)
VALUES ('admin', @pin_hash, @pin_salt, 'admin', @created_date, 'Administrator');
"@
            
            $params = @{
                pin_hash = $encrypted.Hash
                pin_salt = $encrypted.Salt
                created_date = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
            }
            
            Invoke-SqliteQuery -DataSource $DatabasePath -Query $insertQuery -SqlParameters $params
            
            Write-Host "[✓] Default admin user created" -ForegroundColor Green
            Write-Host "    Username: admin" -ForegroundColor Cyan
            Write-Host "    PIN: 000000" -ForegroundColor Cyan
            Write-Host "    ⚠️  Please change this PIN immediately!" -ForegroundColor Yellow
            
            # Initialize admin's user database
            Initialize-UserDatabase -Username "admin"
        }
        
        Write-Host "[✓] Users database initialized: $DatabasePath" -ForegroundColor Green
        
    } catch {
        Write-Host "[✗] Failed to initialize users database: $_" -ForegroundColor Red
        throw
    }
}

<#
.SYNOPSIS
    Initializes a per-user database for tracking watch progress and preferences
#>
function Initialize-UserDatabase {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username
    )
    
    try {
        $userDbPath = Join-Path $CONFIG.UsersDBPath "$Username.db"
        
        # Create watch progress table for videos
        $createVideoProgressQuery = @"
CREATE TABLE IF NOT EXISTS video_progress (
    progress_id INTEGER PRIMARY KEY AUTOINCREMENT,
    video_id INTEGER,
    video_path TEXT NOT NULL UNIQUE,
    video_title TEXT,
    poster_url TEXT,
    media_type TEXT DEFAULT 'movie',
    last_position_seconds REAL DEFAULT 0,
    duration_seconds REAL,
    percent_watched REAL DEFAULT 0,
    watch_count INTEGER DEFAULT 0,
    last_watched TEXT,
    completed INTEGER DEFAULT 0,
    manually_marked INTEGER DEFAULT 0
);
"@
        
        # Create music listening history
        $createMusicHistoryQuery = @"
CREATE TABLE IF NOT EXISTS music_history (
    history_id INTEGER PRIMARY KEY AUTOINCREMENT,
    music_path TEXT NOT NULL UNIQUE,
    artist TEXT,
    title TEXT,
    album TEXT,
    play_count INTEGER DEFAULT 0,
    last_played TEXT,
    total_play_time_seconds REAL DEFAULT 0,
    FOREIGN KEY (music_path) REFERENCES music(path)
);
"@
        
        # Create radio listening history
        $createRadioHistoryQuery = @"
CREATE TABLE IF NOT EXISTS radio_history (
    history_id INTEGER PRIMARY KEY AUTOINCREMENT,
    radio_url TEXT NOT NULL UNIQUE,
    radio_name TEXT,
    listen_count INTEGER DEFAULT 0,
    last_listened TEXT,
    total_listen_time_seconds REAL DEFAULT 0
);
"@
        
        # Create favorites table
        $createFavoritesQuery = @"
CREATE TABLE IF NOT EXISTS favorites (
    favorite_id INTEGER PRIMARY KEY AUTOINCREMENT,
    item_type TEXT NOT NULL CHECK(item_type IN ('video', 'music', 'picture', 'pdf', 'radio')),
    item_path TEXT NOT NULL,
    item_title TEXT,
    added_date TEXT NOT NULL,
    UNIQUE(item_type, item_path)
);
"@
        
        # Create user preferences
        $createPreferencesQuery = @"
CREATE TABLE IF NOT EXISTS preferences (
    pref_key TEXT PRIMARY KEY,
    pref_value TEXT
);
"@
        
        # Create playlists table
        $createPlaylistsQuery = @"
CREATE TABLE IF NOT EXISTS playlists (
    playlist_id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    description TEXT,
    created_date TEXT NOT NULL,
    modified_date TEXT NOT NULL,
    track_count INTEGER DEFAULT 0
);
"@
        
        # Create playlist tracks table
        $createPlaylistTracksQuery = @"
CREATE TABLE IF NOT EXISTS playlist_tracks (
    track_id INTEGER PRIMARY KEY AUTOINCREMENT,
    playlist_id INTEGER NOT NULL,
    music_id INTEGER NOT NULL,
    music_path TEXT NOT NULL,
    title TEXT,
    artist TEXT,
    album TEXT,
    track_order INTEGER NOT NULL,
    added_date TEXT NOT NULL,
    FOREIGN KEY (playlist_id) REFERENCES playlists(playlist_id) ON DELETE CASCADE
);
"@
        
        # Execute all table creations
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createVideoProgressQuery
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createMusicHistoryQuery
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createRadioHistoryQuery
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createFavoritesQuery
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createPreferencesQuery
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createPlaylistsQuery
        Invoke-SqliteQuery -DataSource $userDbPath -Query $createPlaylistTracksQuery
        
        # Migrate existing video_progress table if needed (add new columns for older databases)
        try {
            $columns = Invoke-SqliteQuery -DataSource $userDbPath -Query "PRAGMA table_info(video_progress)"
            $columnNames = $columns | ForEach-Object { $_.name }
            
            if ($columnNames -notcontains 'video_id') {
                Invoke-SqliteQuery -DataSource $userDbPath -Query "ALTER TABLE video_progress ADD COLUMN video_id INTEGER"
                Write-Host "[i] Added video_id column to video_progress" -ForegroundColor Yellow
            }
            if ($columnNames -notcontains 'poster_url') {
                Invoke-SqliteQuery -DataSource $userDbPath -Query "ALTER TABLE video_progress ADD COLUMN poster_url TEXT"
                Write-Host "[i] Added poster_url column to video_progress" -ForegroundColor Yellow
            }
            if ($columnNames -notcontains 'media_type') {
                Invoke-SqliteQuery -DataSource $userDbPath -Query "ALTER TABLE video_progress ADD COLUMN media_type TEXT DEFAULT 'movie'"
                Write-Host "[i] Added media_type column to video_progress" -ForegroundColor Yellow
            }
            if ($columnNames -notcontains 'manually_marked') {
                Invoke-SqliteQuery -DataSource $userDbPath -Query "ALTER TABLE video_progress ADD COLUMN manually_marked INTEGER DEFAULT 0"
                Write-Host "[i] Added manually_marked column to video_progress" -ForegroundColor Yellow
            }
        } catch {
            # Migration errors are non-fatal
        }
        
        Write-Host "[✓] User database initialized for: $Username" -ForegroundColor Green
        
    } catch {
        Write-Host "[✗] Failed to initialize user database for $Username : $_" -ForegroundColor Red
        throw
    }
}

<#
.SYNOPSIS
    Creates a new user account
#>
function New-UserAccount {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username,
        
        [Parameter(Mandatory=$true)]
        [string]$PIN,
        
        [Parameter(Mandatory=$true)]
        [ValidateSet('admin', 'guest')]
        [string]$UserType,
        
        [Parameter(Mandatory=$false)]
        [string]$DisplayName
    )
    
    try {
        # Validate PIN format (6 digits, numbers only)
        if ($PIN -notmatch '^\d{6}$') {
            throw "PIN must be exactly 6 digits (numbers only)"
        }
        
        # Check if username already exists
        $existingUser = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT COUNT(*) as count FROM users WHERE username = @username" -SqlParameters @{ username = $Username }
        
        if ($existingUser.count -gt 0) {
            throw "Username '$Username' already exists"
        }
        
        # Encrypt PIN
        $encrypted = Get-EncryptedPIN -PIN $PIN
        
        # Use username as display name if not provided
        if ([string]::IsNullOrWhiteSpace($DisplayName)) {
            $DisplayName = $Username
        }
        
        # Insert new user
        $insertQuery = @"
INSERT INTO users (username, pin_hash, pin_salt, user_type, created_date, display_name)
VALUES (@username, @pin_hash, @pin_salt, @user_type, @created_date, @display_name);
"@
        
        $params = @{
            username = $Username
            pin_hash = $encrypted.Hash
            pin_salt = $encrypted.Salt
            user_type = $UserType
            created_date = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
            display_name = $DisplayName
        }
        
        Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query $insertQuery -SqlParameters $params
        
        # Initialize user's database
        Initialize-UserDatabase -Username $Username
        
        Write-Host "[✓] User '$Username' created successfully" -ForegroundColor Green
        return $true
        
    } catch {
        Write-Host "[✗] Failed to create user: $_" -ForegroundColor Red
        return $false
    }
}

<#
.SYNOPSIS
    Authenticates a user with username and PIN
#>
function Invoke-UserAuthentication {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username,
        
        [Parameter(Mandatory=$true)]
        [string]$PIN
    )
    
    try {
        # Get user from database
        $user = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
SELECT user_id, username, pin_hash, pin_salt, user_type, display_name, is_active
FROM users
WHERE username = @username AND is_active = 1
"@ -SqlParameters @{ username = $Username }
        
        if (-not $user) {
            return $null
        }
        
        # Validate PIN
        if (Test-PINCode -PIN $PIN -Hash $user.pin_hash -Salt $user.pin_salt) {
            # Update last login
            Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
UPDATE users SET last_login = @last_login WHERE user_id = @user_id
"@ -SqlParameters @{
                user_id = $user.user_id
                last_login = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
            }
            
            # Create session object
            $session = @{
                UserId = $user.user_id
                Username = $user.username
                UserType = $user.user_type
                DisplayName = $user.display_name
                AvatarPath = $user.avatar_path
                LoginTime = Get-Date
                SessionId = [Guid]::NewGuid().ToString()
                UserDbPath = Join-Path $CONFIG.UsersDBPath "$($user.username).db"
            }
            
            return $session
        }
        
        return $null
        
    } catch {
        Write-Host "[✗] Authentication error: $_" -ForegroundColor Red
        return $null
    }
}

<#
.SYNOPSIS
    Updates video watch progress for current user
#>
function Update-VideoProgress {
    param(
        [Parameter(Mandatory=$true)]
        [string]$VideoPath,
        
        [Parameter(Mandatory=$true)]
        [double]$PositionSeconds,
        
        [Parameter(Mandatory=$false)]
        [double]$DurationSeconds,
        
        [Parameter(Mandatory=$false)]
        [string]$VideoTitle,
        
        [Parameter(Mandatory=$false)]
        $VideoId,
        
        [Parameter(Mandatory=$false)]
        [string]$PosterUrl,
        
        [Parameter(Mandatory=$false)]
        [string]$MediaType = 'movie',
        
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    if (-not $UserSession) {
        Write-Host "[!] No user session for video progress tracking" -ForegroundColor Yellow
        return
    }
    
    try {
        $percentWatched = 0
        if ($DurationSeconds -gt 0) {
            $percentWatched = [math]::Min(100, ($PositionSeconds / $DurationSeconds) * 100)
        }
        
        $completed = if ($percentWatched -ge 90) { 1 } else { 0 }
        
        # Convert VideoId to int if it's a string
        $videoIdInt = if ($VideoId) { [int]$VideoId } else { $null }
        
        Write-Host "[i] Tracking: $VideoTitle - Position: $([math]::Round($PositionSeconds,0))s / $([math]::Round($DurationSeconds,0))s ($([math]::Round($percentWatched,1))%)" -ForegroundColor Gray
        
        $upsertQuery = @"
INSERT INTO video_progress (video_id, video_path, video_title, poster_url, media_type, last_position_seconds, duration_seconds, percent_watched, watch_count, last_watched, completed)
VALUES (@video_id, @video_path, @video_title, @poster_url, @media_type, @position, @duration, @percent, 1, @timestamp, @completed)
ON CONFLICT(video_path) DO UPDATE SET
    video_id = COALESCE(@video_id, video_id),
    last_position_seconds = @position,
    duration_seconds = @duration,
    percent_watched = @percent,
    watch_count = CASE WHEN @completed = 1 AND completed = 0 THEN watch_count + 1 ELSE watch_count END,
    last_watched = @timestamp,
    completed = CASE WHEN manually_marked = 1 THEN completed ELSE @completed END,
    video_title = COALESCE(@video_title, video_title),
    poster_url = COALESCE(@poster_url, poster_url),
    media_type = COALESCE(@media_type, media_type);
"@
        
        $params = @{
            video_id = $videoIdInt
            video_path = $VideoPath
            video_title = $VideoTitle
            poster_url = $PosterUrl
            media_type = $MediaType
            position = $PositionSeconds
            duration = $DurationSeconds
            percent = [math]::Round($percentWatched, 2)
            timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
            completed = $completed
        }
        
        Invoke-SqliteQuery -DataSource $UserSession.UserDbPath -Query $upsertQuery -SqlParameters $params
        
    } catch {
        Write-Host "[✗] Failed to update video progress: $_" -ForegroundColor Red
    }
}

<#
.SYNOPSIS
    Updates music listening history for current user
#>
function Update-MusicHistory {
    param(
        [Parameter(Mandatory=$true)]
        [string]$MusicPath,
        
        [Parameter(Mandatory=$false)]
        [string]$Artist,
        
        [Parameter(Mandatory=$false)]
        [string]$Title,
        
        [Parameter(Mandatory=$false)]
        [string]$Album,
        
        [Parameter(Mandatory=$false)]
        [double]$PlayTimeSeconds = 0,
        
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    if (-not $UserSession) {
        return
    }
    
    try {
        $upsertQuery = @"
INSERT INTO music_history (music_path, artist, title, album, play_count, last_played, total_play_time_seconds)
VALUES (@music_path, @artist, @title, @album, 1, @timestamp, @play_time)
ON CONFLICT(music_path) DO UPDATE SET
    play_count = play_count + 1,
    last_played = @timestamp,
    total_play_time_seconds = total_play_time_seconds + @play_time,
    artist = COALESCE(@artist, artist),
    title = COALESCE(@title, title),
    album = COALESCE(@album, album);
"@
        
        $params = @{
            music_path = $MusicPath
            artist = $Artist
            title = $Title
            album = $Album
            timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
            play_time = $PlayTimeSeconds
        }
        
        Invoke-SqliteQuery -DataSource $UserSession.UserDbPath -Query $upsertQuery -SqlParameters $params
        
    } catch {
        Write-Host "[✗] Failed to update music history: $_" -ForegroundColor Red
    }
}

<#
.SYNOPSIS
    Updates radio listening history for current user
#>
function Update-RadioHistory {
    param(
        [Parameter(Mandatory=$true)]
        [string]$RadioUrl,
        
        [Parameter(Mandatory=$false)]
        [string]$RadioName,
        
        [Parameter(Mandatory=$false)]
        [double]$ListenTimeSeconds = 0,
        
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    if (-not $UserSession) {
        return
    }
    
    try {
        $upsertQuery = @"
INSERT INTO radio_history (radio_url, radio_name, listen_count, last_listened, total_listen_time_seconds)
VALUES (@radio_url, @radio_name, 1, @timestamp, @listen_time)
ON CONFLICT(radio_url) DO UPDATE SET
    listen_count = listen_count + 1,
    last_listened = @timestamp,
    total_listen_time_seconds = total_listen_time_seconds + @listen_time,
    radio_name = COALESCE(@radio_name, radio_name);
"@
        
        $params = @{
            radio_url = $RadioUrl
            radio_name = $RadioName
            timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
            listen_time = $ListenTimeSeconds
        }
        
        Invoke-SqliteQuery -DataSource $UserSession.UserDbPath -Query $upsertQuery -SqlParameters $params
        
    } catch {
        Write-Host "[✗] Failed to update radio history: $_" -ForegroundColor Red
    }
}

<#
.SYNOPSIS
    Gets top 10 most played music tracks for current user
#>
function Get-UserTopMusic {
    param(
        [Parameter(Mandatory=$false)]
        [int]$Limit = 10,
        
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    if (-not $UserSession) {
        return @()
    }
    
    try {
        $query = @"
SELECT 
    music_path,
    artist,
    title,
    album,
    play_count,
    last_played,
    total_play_time_seconds
FROM music_history
ORDER BY play_count DESC, last_played DESC
LIMIT @limit;
"@
        
        $result = Invoke-SqliteQuery -DataSource $UserSession.UserDbPath -Query $query -SqlParameters @{ limit = $Limit }
        return $result
        
    } catch {
        Write-Host "[✗] Failed to get top music: $_" -ForegroundColor Red
        return @()
    }
}

<#
.SYNOPSIS
    Gets most listened radio stations for current user
#>
function Get-UserTopRadios {
    param(
        [Parameter(Mandatory=$false)]
        [int]$Limit = 10,
        
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    if (-not $UserSession) {
        return @()
    }
    
    try {
        $query = @"
SELECT 
    radio_url,
    radio_name,
    listen_count,
    last_listened,
    total_listen_time_seconds
FROM radio_history
ORDER BY listen_count DESC, last_listened DESC
LIMIT @limit;
"@
        
        $result = Invoke-SqliteQuery -DataSource $UserSession.UserDbPath -Query $query -SqlParameters @{ limit = $Limit }
        return $result
        
    } catch {
        Write-Host "[✗] Failed to get top radios: $_" -ForegroundColor Red
        return @()
    }
}

<#
.SYNOPSIS
    Gets watched movies for current user
#>
function Get-UserWatchedMovies {
    param(
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    if (-not $UserSession) {
        return @()
    }
    
    try {
        $query = @"
SELECT 
    video_path,
    video_title,
    last_position_seconds,
    duration_seconds,
    percent_watched,
    watch_count,
    last_watched,
    completed
FROM video_progress
ORDER BY last_watched DESC;
"@
        
        $result = Invoke-SqliteQuery -DataSource $UserSession.UserDbPath -Query $query
        return $result
        
    } catch {
        Write-Host "[✗] Failed to get watched movies: $_" -ForegroundColor Red
        return @()
    }
}

<#
.SYNOPSIS
    User management menu (admin only)
#>
function Show-UserManagementMenu {
    if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
        Write-Host "`n[✗] Access denied. Admin privileges required." -ForegroundColor Red
        return
    }
    
    while ($true) {
        Write-Host "`n╔════════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "║      USER MANAGEMENT MENU              ║" -ForegroundColor Cyan
        Write-Host "╠════════════════════════════════════════╣" -ForegroundColor Cyan
        Write-Host "║  1. List All Users                     ║" -ForegroundColor White
        Write-Host "║  2. Create New User                    ║" -ForegroundColor White
        Write-Host "║  3. Delete User                        ║" -ForegroundColor White
        Write-Host "║  4. Change User PIN                    ║" -ForegroundColor White
        Write-Host "║  5. Change User Type (Admin/Guest)     ║" -ForegroundColor White
        Write-Host "║  6. Disable/Enable User                ║" -ForegroundColor White
        Write-Host "║  7. View User Statistics               ║" -ForegroundColor White
        Write-Host "║  0. Back to Main Menu                  ║" -ForegroundColor Yellow
        Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Cyan
        
        $choice = Read-Host "`nSelect option"
        
        switch ($choice) {
            '1' {
                # List all users
                $users = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
SELECT user_id, username, display_name, user_type, created_date, last_login, is_active
FROM users
ORDER BY user_type DESC, username;
"@
                
                Write-Host "`n=== All Users ===" -ForegroundColor Cyan
                foreach ($user in $users) {
                    $status = if ($user.is_active -eq 1) { "Active" } else { "Disabled" }
                    $statusColor = if ($user.is_active -eq 1) { "Green" } else { "Red" }
                    
                    Write-Host "`n  ID: $($user.user_id)" -ForegroundColor Gray
                    Write-Host "  Username: $($user.username)" -ForegroundColor White
                    Write-Host "  Display Name: $($user.display_name)" -ForegroundColor White
                    Write-Host "  Type: $($user.user_type)" -ForegroundColor $(if ($user.user_type -eq 'admin') { 'Yellow' } else { 'Cyan' })
                    Write-Host "  Status: $status" -ForegroundColor $statusColor
                    Write-Host "  Created: $($user.created_date)" -ForegroundColor Gray
                    Write-Host "  Last Login: $($user.last_login)" -ForegroundColor Gray
                }
            }
            
            '2' {
                # Create new user
                Write-Host "`n=== Create New User ===" -ForegroundColor Cyan
                $newUsername = Read-Host "Enter username"
                
                if ([string]::IsNullOrWhiteSpace($newUsername)) {
                    Write-Host "[✗] Username cannot be empty" -ForegroundColor Red
                    continue
                }
                
                $newDisplayName = Read-Host "Enter display name (press Enter to use username)"
                if ([string]::IsNullOrWhiteSpace($newDisplayName)) {
                    $newDisplayName = $newUsername
                }
                
                Write-Host "`nUser Types:" -ForegroundColor Yellow
                Write-Host "  1. Admin (full access)" -ForegroundColor Cyan
                Write-Host "  2. Guest (read-only settings)" -ForegroundColor Cyan
                $typeChoice = Read-Host "Select user type (1/2)"
                
                $newUserType = switch ($typeChoice) {
                    '1' { 'admin' }
                    '2' { 'guest' }
                    default { 'guest' }
                }
                
                $newPIN = Read-Host "Enter 6-digit PIN (numbers only)"
                
                if ($newPIN -notmatch '^\d{6}$') {
                    Write-Host "[✗] PIN must be exactly 6 digits" -ForegroundColor Red
                    continue
                }
                
                $confirmPIN = Read-Host "Confirm PIN"
                
                if ($newPIN -ne $confirmPIN) {
                    Write-Host "[✗] PINs do not match" -ForegroundColor Red
                    continue
                }
                
                New-UserAccount -Username $newUsername -PIN $newPIN -UserType $newUserType -DisplayName $newDisplayName
            }
            
            '3' {
                # Delete user
                Write-Host "`n=== Delete User ===" -ForegroundColor Cyan
                $deleteUsername = Read-Host "Enter username to delete"
                
                # Prevent deleting current user
                if ($deleteUsername -eq $Global:CurrentUser.Username) {
                    Write-Host "[✗] Cannot delete currently logged in user" -ForegroundColor Red
                    continue
                }
                
                # Prevent deleting last admin
                $adminCount = (Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT COUNT(*) as count FROM users WHERE user_type = 'admin' AND is_active = 1").count
                $targetUser = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT user_type FROM users WHERE username = @username" -SqlParameters @{ username = $deleteUsername }
                
                if ($targetUser.user_type -eq 'admin' -and $adminCount -le 1) {
                    Write-Host "[✗] Cannot delete the last admin user" -ForegroundColor Red
                    continue
                }
                
                $confirm = Read-Host "Are you sure you want to delete user '$deleteUsername'? (yes/no)"
                
                if ($confirm -eq 'yes') {
                    try {
                        # Delete user from database
                        Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "DELETE FROM users WHERE username = @username" -SqlParameters @{ username = $deleteUsername }
                        
                        # Delete user's database file
                        $userDbPath = Join-Path $CONFIG.UsersDBPath "$deleteUsername.db"
                        if (Test-Path $userDbPath) {
                            Remove-Item -Path $userDbPath -Force
                        }
                        
                        Write-Host "[✓] User '$deleteUsername' deleted successfully" -ForegroundColor Green
                        
                    } catch {
                        Write-Host "[✗] Failed to delete user: $_" -ForegroundColor Red
                    }
                }
            }
            
            '4' {
                # Change user PIN
                Write-Host "`n=== Change User PIN ===" -ForegroundColor Cyan
                $targetUsername = Read-Host "Enter username"
                
                $newPIN = Read-Host "Enter new 6-digit PIN (numbers only)"
                
                if ($newPIN -notmatch '^\d{6}$') {
                    Write-Host "[✗] PIN must be exactly 6 digits" -ForegroundColor Red
                    continue
                }
                
                $confirmPIN = Read-Host "Confirm new PIN"
                
                if ($newPIN -ne $confirmPIN) {
                    Write-Host "[✗] PINs do not match" -ForegroundColor Red
                    continue
                }
                
                try {
                    $encrypted = Get-EncryptedPIN -PIN $newPIN
                    
                    Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
UPDATE users SET pin_hash = @pin_hash, pin_salt = @pin_salt
WHERE username = @username
"@ -SqlParameters @{
                        username = $targetUsername
                        pin_hash = $encrypted.Hash
                        pin_salt = $encrypted.Salt
                    }
                    
                    Write-Host "[✓] PIN updated successfully for user '$targetUsername'" -ForegroundColor Green
                    
                } catch {
                    Write-Host "[✗] Failed to update PIN: $_" -ForegroundColor Red
                }
            }
            
            '5' {
                # Change user type
                Write-Host "`n=== Change User Type ===" -ForegroundColor Cyan
                $targetUsername = Read-Host "Enter username"
                
                # Prevent changing current user's type
                if ($targetUsername -eq $Global:CurrentUser.Username) {
                    Write-Host "[✗] Cannot change your own user type" -ForegroundColor Red
                    continue
                }
                
                Write-Host "`nUser Types:" -ForegroundColor Yellow
                Write-Host "  1. Admin (full access)" -ForegroundColor Cyan
                Write-Host "  2. Guest (read-only settings)" -ForegroundColor Cyan
                $typeChoice = Read-Host "Select new user type (1/2)"
                
                $newType = switch ($typeChoice) {
                    '1' { 'admin' }
                    '2' { 'guest' }
                    default { $null }
                }
                
                if ($newType) {
                    # Check if changing last admin to guest
                    $targetUser = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT user_type FROM users WHERE username = @username" -SqlParameters @{ username = $targetUsername }
                    $adminCount = (Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT COUNT(*) as count FROM users WHERE user_type = 'admin' AND is_active = 1").count
                    
                    if ($targetUser.user_type -eq 'admin' -and $newType -eq 'guest' -and $adminCount -le 1) {
                        Write-Host "[✗] Cannot change the last admin to guest" -ForegroundColor Red
                        continue
                    }
                    
                    try {
                        Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
UPDATE users SET user_type = @user_type WHERE username = @username
"@ -SqlParameters @{
                            username = $targetUsername
                            user_type = $newType
                        }
                        
                        Write-Host "[✓] User type updated successfully for '$targetUsername'" -ForegroundColor Green
                        
                    } catch {
                        Write-Host "[✗] Failed to update user type: $_" -ForegroundColor Red
                    }
                }
            }
            
            '6' {
                # Disable/Enable user
                Write-Host "`n=== Disable/Enable User ===" -ForegroundColor Cyan
                $targetUsername = Read-Host "Enter username"
                
                # Prevent disabling current user
                if ($targetUsername -eq $Global:CurrentUser.Username) {
                    Write-Host "[✗] Cannot disable currently logged in user" -ForegroundColor Red
                    continue
                }
                
                $user = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT is_active, user_type FROM users WHERE username = @username" -SqlParameters @{ username = $targetUsername }
                
                if ($user) {
                    # Prevent disabling last admin
                    $adminCount = (Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT COUNT(*) as count FROM users WHERE user_type = 'admin' AND is_active = 1").count
                    
                    if ($user.user_type -eq 'admin' -and $user.is_active -eq 1 -and $adminCount -le 1) {
                        Write-Host "[✗] Cannot disable the last active admin user" -ForegroundColor Red
                        continue
                    }
                    
                    $newStatus = if ($user.is_active -eq 1) { 0 } else { 1 }
                    $action = if ($newStatus -eq 1) { "enable" } else { "disable" }
                    
                    $confirm = Read-Host "Are you sure you want to $action user '$targetUsername'? (yes/no)"
                    
                    if ($confirm -eq 'yes') {
                        try {
                            Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
UPDATE users SET is_active = @is_active WHERE username = @username
"@ -SqlParameters @{
                                username = $targetUsername
                                is_active = $newStatus
                            }
                            
                            Write-Host "[✓] User '$targetUsername' ${action}d successfully" -ForegroundColor Green
                            
                        } catch {
                            Write-Host "[✗] Failed to $action user: $_" -ForegroundColor Red
                        }
                    }
                }
                else {
                    Write-Host "[✗] User not found" -ForegroundColor Red
                }
            }
            
            '7' {
                # View user statistics
                Write-Host "`n=== User Statistics ===" -ForegroundColor Cyan
                $targetUsername = Read-Host "Enter username (or press Enter for all users)"
                
                if ([string]::IsNullOrWhiteSpace($targetUsername)) {
                    # Show stats for all users
                    $users = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT username FROM users WHERE is_active = 1 ORDER BY username"
                    
                    foreach ($user in $users) {
                        Show-UserStats -Username $user.username
                    }
                }
                else {
                    Show-UserStats -Username $targetUsername
                }
            }
            
            '0' {
                return
            }
            
            default {
                Write-Host "`n[✗] Invalid option" -ForegroundColor Red
            }
        }
        
        Read-Host "`nPress Enter to continue"
    }
}

<#
.SYNOPSIS
    Shows statistics for a specific user
#>
function Show-UserStats {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username
    )
    
    $userDbPath = Join-Path $CONFIG.UsersDBPath "$Username.db"
    
    if (-not (Test-Path $userDbPath)) {
        Write-Host "`n[!] No statistics available for user '$Username'" -ForegroundColor Yellow
        return
    }
    
    try {
        Write-Host "`n┌─────────────────────────────────────────┐" -ForegroundColor Cyan
        Write-Host "│ Statistics for: $($Username.PadRight(24)) │" -ForegroundColor Cyan
        Write-Host "└─────────────────────────────────────────┘" -ForegroundColor Cyan
        
        # Video stats
        $videoStats = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as total_watched,
    SUM(CASE WHEN completed = 1 THEN 1 ELSE 0 END) as completed_count,
    SUM(watch_count) as total_plays,
    AVG(percent_watched) as avg_completion
FROM video_progress
"@
        
        Write-Host "`n📹 Video Statistics:" -ForegroundColor Yellow
        Write-Host "   Videos Watched: $($videoStats.total_watched)" -ForegroundColor White
        Write-Host "   Completed: $($videoStats.completed_count)" -ForegroundColor White
        Write-Host "   Total Plays: $($videoStats.total_plays)" -ForegroundColor White
        Write-Host "   Avg Completion: $([math]::Round($videoStats.avg_completion, 1))%" -ForegroundColor White
        
        # Music stats
        $musicStats = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as unique_tracks,
    SUM(play_count) as total_plays,
    SUM(total_play_time_seconds) as total_time
FROM music_history
"@
        
        Write-Host "`n🎵 Music Statistics:" -ForegroundColor Yellow
        Write-Host "   Unique Tracks: $($musicStats.unique_tracks)" -ForegroundColor White
        Write-Host "   Total Plays: $($musicStats.total_plays)" -ForegroundColor White
        $musicHours = [math]::Round($musicStats.total_time / 3600, 1)
        Write-Host "   Total Listen Time: $musicHours hours" -ForegroundColor White
        
        # Radio stats
        $radioStats = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as unique_stations,
    SUM(listen_count) as total_listens,
    SUM(total_listen_time_seconds) as total_time
FROM radio_history
"@
        
        Write-Host "`n📻 Radio Statistics:" -ForegroundColor Yellow
        Write-Host "   Unique Stations: $($radioStats.unique_stations)" -ForegroundColor White
        Write-Host "   Total Listens: $($radioStats.total_listens)" -ForegroundColor White
        $radioHours = [math]::Round($radioStats.total_time / 3600, 1)
        Write-Host "   Total Listen Time: $radioHours hours" -ForegroundColor White
        
        # Top 5 music tracks
        $topMusic = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT artist, title, play_count
FROM music_history
ORDER BY play_count DESC
LIMIT 5
"@
        
        if ($topMusic) {
            Write-Host "`n🎼 Top 5 Music Tracks:" -ForegroundColor Yellow
            $rank = 1
            foreach ($track in $topMusic) {
                $artistTitle = if ($track.artist -and $track.title) {
                    "$($track.artist) - $($track.title)"
                } elseif ($track.title) {
                    $track.title
                } else {
                    "(Unknown)"
                }
                Write-Host "   $rank. $artistTitle ($($track.play_count) plays)" -ForegroundColor White
                $rank++
            }
        }
        
    } catch {
        Write-Host "`n[✗] Error retrieving stats: $_" -ForegroundColor Red
    }
}

# ============================================================================
# ASYNC STREAMING MODULE (v2.9)
# ============================================================================

# Global variables for async streaming
$Global:RunspacePool = $null
$Global:ActiveStreams = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
$Global:StreamCleanupTimer = $null

function Initialize-AsyncStreaming {
    if ($null -eq $Global:RunspacePool) {
        $Global:RunspacePool = [runspacefactory]::CreateRunspacePool(1, 10)
        $Global:RunspacePool.ApartmentState = "MTA"
        $Global:RunspacePool.Open()
        
        # Start cleanup timer for orphaned streams
        if ($null -eq $Global:StreamCleanupTimer) {
            $Global:StreamCleanupTimer = [System.Timers.Timer]::new(30000)
            
            Register-ObjectEvent -InputObject $Global:StreamCleanupTimer -EventName Elapsed -Action {
                try {
                    $now = Get-Date
                    $toRemove = @()
                    
                    foreach ($kvp in $Global:ActiveStreams.GetEnumerator()) {
                        $stream = $kvp.Value
                        
                        if ($stream.PSObject.Properties.Name -contains 'StartTime') {
                            $age = ($now - $stream.StartTime).TotalMinutes
                            if ($age -gt 5) {
                                $toRemove += $kvp.Key
                            }
                        }
                        
                        if ($stream.PSObject.Properties.Name -contains 'PowerShell') {
                            if ($stream.PowerShell.InvocationStateInfo.State -eq 'Completed' -or 
                                $stream.PowerShell.InvocationStateInfo.State -eq 'Failed') {
                                $toRemove += $kvp.Key
                            }
                        }
                    }
                    
                    foreach ($key in $toRemove) {
                        $removed = $null
                        if ($Global:ActiveStreams.TryRemove($key, [ref]$removed)) {
                            try {
                                if ($removed.PowerShell) {
                                    $removed.PowerShell.Stop()
                                    $removed.PowerShell.Dispose()
                                }
                                
                                if ($removed.TempDir -and (Test-Path $removed.TempDir)) {
                                    Remove-Item -Path $removed.TempDir -Recurse -Force -ErrorAction SilentlyContinue
                                }
                            } catch {}
                        }
                    }
                    
                    $tempBase = Join-Path $env:TEMP "PSMediaLibrary_Stream_*"
                    Get-ChildItem -Path $tempBase -Directory -ErrorAction SilentlyContinue | ForEach-Object {
                        if ((Get-Date) - $_.CreationTime -gt [TimeSpan]::FromMinutes(10)) {
                            Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
                        }
                    }
                    
                    # Clean up old HLS transcoding directories
                    $hlsBase = Join-Path $PSScriptRoot "hls_temp"
                    if (Test-Path $hlsBase) {
                        Get-ChildItem -Path $hlsBase -Directory -ErrorAction SilentlyContinue | ForEach-Object {
                            # Check if transcode is still active
                            $dirName = $_.Name
                            $isActive = $Global:ActiveTranscodes.ContainsKey($dirName)
                            
                            if (-not $isActive) {
                                # Check age - remove if older than 10 minutes
                                if ((Get-Date) - $_.CreationTime -gt [TimeSpan]::FromMinutes(10)) {
                                    Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
                                }
                            }
                            # NOTE: Removed segment cleanup for active transcodes
                            # We keep ALL segments for EVENT-type HLS to allow seeking
                        }
                    }
                    
                } catch {}
            } | Out-Null
            
            $Global:StreamCleanupTimer.Start()
        }
        
        Write-Host "[✓] Async streaming: Ready (max 10 concurrent)" -ForegroundColor Green
    }
}

function Cleanup-AsyncStreaming {
    # Stop cleanup timer
    if ($null -ne $Global:StreamCleanupTimer) {
        $Global:StreamCleanupTimer.Stop()
        $Global:StreamCleanupTimer.Dispose()
        $Global:StreamCleanupTimer = $null
    }
    
    if ($null -ne $Global:RunspacePool) {
        Write-Host "[i] Stopping async streams..." -ForegroundColor Gray
        foreach ($kvp in $Global:ActiveStreams.GetEnumerator()) {
            try {
                $kvp.Value.PowerShell.Stop()
                $kvp.Value.PowerShell.Dispose()
                
                # Clean up temp files
                if ($kvp.Value.TempDir -and (Test-Path $kvp.Value.TempDir)) {
                    Remove-Item -Path $kvp.Value.TempDir -Recurse -Force -ErrorAction SilentlyContinue
                }
            } catch {}
        }
        
        $Global:RunspacePool.Close()
        $Global:RunspacePool.Dispose()
        $Global:RunspacePool = $null
        $Global:ActiveStreams.Clear()
    }
}

# ============================================================================
# FFMPEG TRANSCODING MODULE (v1.0)
# ============================================================================

# Global transcoding configuration - ADD TO YOUR CONFIG SECTION
$Global:TranscodingConfig = @{
    # FFmpeg path
    FFmpegPath = Join-Path $PSScriptRoot "tools\ffmpeg\bin\ffmpeg.exe"
    FFprobePath = Join-Path $PSScriptRoot "tools\ffmpeg\bin\ffprobe.exe"
    
    # Hardware acceleration settings
    EnableHardwareAccel = $true
    PreferredEncoder = "software"  # Options: "auto", "nvenc", "qsv", "amf", "software"
    # NOTE: Using "software" as default for maximum compatibility
    #       Change to "auto" or specific encoder ("nvenc", "qsv", "amf") if you have working GPU encoding
    # NOTE: "software" is recommended for AMD integrated GPUs (Radeon Graphics)
    #       AMD AMF can be unreliable. Software encoding is slower but works on all systems.
    #       Change to "auto" to try hardware encoding, but be prepared to fall back to "software"
    
    # Transcoding quality settings
    VideoCodec = "h264"  # Output codec
    VideoPreset = "veryfast"  # For software: ultrafast, superfast, veryfast, faster, fast, medium, slow
    # Using "veryfast" for better real-time performance
    VideoCRF = 23  # Constant Rate Factor (18-28, lower = better quality, 23 is default)
    VideoMaxrate = "5M"  # Max bitrate for hardware encoding
    VideoBufsize = "10M"  # Buffer size for hardware encoding
    
    # Audio settings
    AudioCodec = "aac"
    AudioBitrate = "128k"
    AudioChannels = 2
    
    # Performance settings
    ThreadCount = 0  # 0 = auto-detect optimal thread count
    MaxConcurrentTranscodes = 2  # Max simultaneous transcoding sessions
    
    # HTS (HTTP Live Streaming) settings - OPTIMIZED
    HTSSegmentDuration = 4      # Seconds per segment (reduced for faster startup)
    HTSMaxSegments = 5          # Maximum segments to keep (prevents temp file buildup!)
    HTSBufferSize = 8192        # Buffer size for file I/O (8KB chunks for smooth streaming)
    HTSPlaylistMaxEntries = 10  # Max entries in m3u8 playlist
    
    # Formats that need transcoding (MP4/M4V are handled via codec checking)
    TranscodeFormats = @('.mkv', '.avi', '.wmv', '.flv', '.mov', '.mpg', '.mpeg', '.vob', '.ts', '.webm', '.divx', '.3gp')
}

# Active transcoding processes
$Global:ActiveTranscodes = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
$Global:FFmpegCapabilities = $null

# ============================================================================
# TRANSCODING FUNCTIONS
# ============================================================================

function Initialize-FFmpegTranscoding {
    <#
    .SYNOPSIS
        Initializes FFmpeg and detects hardware encoding capabilities
    #>
    
    Write-Host "`n=== FFmpeg Transcoding Setup ===" -ForegroundColor Cyan
    
    $ffmpegPath = $Global:TranscodingConfig.FFmpegPath
    $ffprobePath = $Global:TranscodingConfig.FFprobePath
    
    # Check FFmpeg exists
    if (-not (Test-Path $ffmpegPath)) {
        Write-Host "[!] FFmpeg not found at: $ffmpegPath" -ForegroundColor Yellow
        Write-Host "    Place ffmpeg.exe in: tools\ffmpeg\bin\" -ForegroundColor Yellow
        Write-Host "    Download from: https://ffmpeg.org/download.html" -ForegroundColor Yellow
        return $false
    }
    
    Write-Host "[✓] FFmpeg found" -ForegroundColor Green
    
    # Get FFmpeg version
    try {
        $versionOutput = & $ffmpegPath -version 2>&1 | Select-Object -First 1
        Write-Host "[i] $versionOutput" -ForegroundColor Gray
    } catch {}
    
    # If software encoding is preferred, skip hardware detection entirely
    if ($Global:TranscodingConfig.PreferredEncoder -eq "software") {
        Write-Host "[i] Software encoding selected - skipping hardware detection" -ForegroundColor Cyan
        
        $Global:FFmpegCapabilities = @{
            FFmpegPath = $ffmpegPath
            FFprobePath = $ffprobePath
            HWEncoders = @{
                NVENC = $false
                QSV = $false
                AMF = $false
                VAAPI = $false
                VideoToolbox = $false
            }
        }
        
        Write-Host "[✓] Using software encoder: libx264 (CPU)" -ForegroundColor Green
        return $true
    }
    
    # Detect hardware encoders (only if not in software mode)
    try {
        $encodersOutput = & $ffmpegPath -hide_banner -encoders 2>&1 | Out-String
        
        $hwEncoders = @{
            NVENC = ($encodersOutput -match 'h264_nvenc')
            QSV = ($encodersOutput -match 'h264_qsv')
            AMF = ($encodersOutput -match 'h264_amf')
            VAAPI = ($encodersOutput -match 'h264_vaapi')
            VideoToolbox = ($encodersOutput -match 'h264_videotoolbox')
        }
        
        Write-Host "`n[i] Hardware Encoder Support:" -ForegroundColor Cyan
        Write-Host "    NVIDIA NVENC:  $(if ($hwEncoders.NVENC) { '✓ Available' } else { '✗ Not Available' })" -ForegroundColor $(if ($hwEncoders.NVENC) { 'Green' } else { 'Gray' })
        Write-Host "    Intel QSV:     $(if ($hwEncoders.QSV) { '✓ Available' } else { '✗ Not Available' })" -ForegroundColor $(if ($hwEncoders.QSV) { 'Green' } else { 'Gray' })
        Write-Host "    AMD AMF:       $(if ($hwEncoders.AMF) { '✓ Available' } else { '✗ Not Available' })" -ForegroundColor $(if ($hwEncoders.AMF) { 'Green' } else { 'Gray' })
        Write-Host "    VAAPI (Linux): $(if ($hwEncoders.VAAPI) { '✓ Available' } else { '✗ Not Available' })" -ForegroundColor $(if ($hwEncoders.VAAPI) { 'Green' } else { 'Gray' })
        Write-Host "    VideoToolbox:  $(if ($hwEncoders.VideoToolbox) { '✓ Available' } else { '✗ Not Available' })" -ForegroundColor $(if ($hwEncoders.VideoToolbox) { 'Green' } else { 'Gray' })
        
        $Global:FFmpegCapabilities = @{
            FFmpegPath = $ffmpegPath
            FFprobePath = $ffprobePath
            HWEncoders = $hwEncoders
        }
        
        # Determine which encoder will be used
        $selectedEncoder = Get-OptimalFFmpegEncoder
        Write-Host "`n[✓] Selected encoder: $($selectedEncoder.Name) ($($selectedEncoder.Type))" -ForegroundColor Green
        
        return $true
    }
    catch {
        Write-Host "[!] Error detecting FFmpeg capabilities: $_" -ForegroundColor Yellow
        return $false
    }
}

function Get-OptimalFFmpegEncoder {
    <#
    .SYNOPSIS
        Determines the best available encoder based on hardware and configuration
    #>
    
    $preferred = $Global:TranscodingConfig.PreferredEncoder
    $hwEncoders = $Global:FFmpegCapabilities.HWEncoders
    
    # ALWAYS use software if that's the preference - don't even check hardware
    if ($preferred -eq "software") {
        return @{
            Name = "libx264"
            Type = "Software (CPU)"
            Encoder = "libx264"
            HWAccel = $null
            Preset = $Global:TranscodingConfig.VideoPreset
            UsesCRF = $true
        }
    }
    
    # Encoder preference mapping
    $encoderMap = @{
        "nvenc" = @{
            Name = "NVIDIA NVENC"
            Type = "Hardware (NVIDIA GPU)"
            Encoder = "h264_nvenc"
            HWAccel = "cuda"
            Preset = "p4"
            Available = $hwEncoders.NVENC
        }
        "qsv" = @{
            Name = "Intel QuickSync"
            Type = "Hardware (Intel GPU)"
            Encoder = "h264_qsv"
            HWAccel = "qsv"
            Preset = "medium"
            Available = $hwEncoders.QSV
        }
        "amf" = @{
            Name = "AMD AMF"
            Type = "Hardware (AMD GPU)"
            Encoder = "h264_amf"
            HWAccel = "d3d11va"
            Preset = "balanced"
            Available = $hwEncoders.AMF
        }
        "vaapi" = @{
            Name = "VAAPI"
            Type = "Hardware (Linux)"
            Encoder = "h264_vaapi"
            HWAccel = "vaapi"
            Preset = "medium"
            Available = $hwEncoders.VAAPI
        }
        "videotoolbox" = @{
            Name = "VideoToolbox"
            Type = "Hardware (macOS)"
            Encoder = "h264_videotoolbox"
            HWAccel = "videotoolbox"
            Preset = "medium"
            Available = $hwEncoders.VideoToolbox
        }
    }
    
    # If specific encoder requested, try to use it
    if ($preferred -ne "auto" -and $encoderMap.ContainsKey($preferred)) {
        if ($encoderMap[$preferred].Available) {
            return $encoderMap[$preferred] + @{ UsesCRF = $false }
        }
    }
    
    # Auto-detect: Try in order of preference
    # For maximum compatibility, prioritize working encoders
    # NVENC is most reliable, then QSV, then software (AMF can be problematic)
    $autoOrder = @("nvenc", "qsv", "amf")
    
    foreach ($encoderType in $autoOrder) {
        if ($encoderMap[$encoderType].Available) {
            Write-Host "[i] Auto-selected hardware encoder: $encoderType" -ForegroundColor Cyan
            return $encoderMap[$encoderType] + @{ UsesCRF = $false }
        }
    }
    
    # Fallback to software encoding (most compatible)
    Write-Host "[i] No hardware encoders available or all failed, using software encoding" -ForegroundColor Yellow
    return @{
        Name = "libx264"
        Type = "Software (CPU)"
        Encoder = "libx264"
        HWAccel = $null
        Preset = $Global:TranscodingConfig.VideoPreset
        UsesCRF = $true
    }
}

function Test-VideoNeedsTranscoding {
    <#
    .SYNOPSIS
        Checks if a video file needs transcoding for browser compatibility
    .DESCRIPTION
        Checks file extension and for MP4/M4V files, also checks codec compatibility
        Browser-compatible: H.264 video + AAC audio
        Needs transcoding: HEVC/H.265, AV1, VP9, or incompatible audio codecs
    #>
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath
    )
    
    $extension = [System.IO.Path]::GetExtension($FilePath).ToLower()
    
    # Check if extension is in transcode list (MKV, AVI, etc.)
    if ($Global:TranscodingConfig.TranscodeFormats -contains $extension) {
        # For MKV files, still check if they have compatible codecs
        # Some MKV files with H.264+AAC might not need transcoding
        if ($extension -eq '.mkv') {
            $videoInfo = Get-VideoInfo -FilePath $FilePath
            if ($videoInfo) {
                $videoCodec = $videoInfo.VideoCodec
                $audioCodec = $videoInfo.AudioCodec
                
                # Log the codec info for MKV
                Write-Host "[i] MKV detected - Video: $videoCodec, Audio: $audioCodec" -ForegroundColor Gray
                
                # MKV container always needs transcoding for browser compatibility
                # Even if codecs are compatible, browsers don't support MKV container
                return $true
            }
        }
        return $true
    }
    
    # For MP4/M4V files, check codec compatibility
    if ($extension -eq '.mp4' -or $extension -eq '.m4v') {
        # Get video codec info using FFprobe
        $videoInfo = Get-VideoInfo -FilePath $FilePath
        
        if ($videoInfo) {
            $videoCodec = $videoInfo.VideoCodec
            $audioCodec = $videoInfo.AudioCodec
            
            # Browser-compatible video codecs: h264, mpeg4
            # Incompatible: hevc (h265), av1, vp8, vp9, etc.
            $compatibleVideoCodecs = @('h264', 'mpeg4', 'avc')
            
            # Browser-compatible audio codecs: AAC and MP3 only
            # INCOMPATIBLE: EAC3, AC3, DTS, TrueHD, FLAC, PCM, Opus, Vorbis, etc.
            $compatibleAudioCodecs = @('aac', 'mp3')
            
            # List of known incompatible audio codecs that MUST be transcoded
            $incompatibleAudioCodecs = @('eac3', 'ac3', 'dts', 'truehd', 'flac', 'pcm_s16le', 'pcm_s24le', 'opus', 'vorbis', 'wmav2', 'alac')
            
            $videoNeedsTranscode = $videoCodec -and ($compatibleVideoCodecs -notcontains $videoCodec.ToLower())
            $audioNeedsTranscode = $audioCodec -and (
                ($compatibleAudioCodecs -notcontains $audioCodec.ToLower()) -or
                ($incompatibleAudioCodecs -contains $audioCodec.ToLower())
            )
            
            if ($videoNeedsTranscode -or $audioNeedsTranscode) {
                if ($videoNeedsTranscode) {
                    Write-Host "[i] MP4 needs transcoding - Incompatible video codec: $videoCodec" -ForegroundColor Yellow
                }
                if ($audioNeedsTranscode) {
                    Write-Host "[i] MP4 needs transcoding - Incompatible audio codec: $audioCodec (browsers only support AAC/MP3)" -ForegroundColor Yellow
                }
                return $true
            }
        }
    }
    
    # MP4 with compatible codecs or unknown codec info - assume compatible
    return $false
}

function Get-VideoInfo {
    <#
    .SYNOPSIS
        Gets video information using FFprobe
    #>
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath
    )
    
    if (-not $Global:FFmpegCapabilities) {
        return $null
    }
    
    $ffprobePath = $Global:FFmpegCapabilities.FFprobePath
    
    if (-not (Test-Path $ffprobePath)) {
        return $null
    }
    
    try {
        $args = @(
            "-v", "quiet"
            "-print_format", "json"
            "-show_format"
            "-show_streams"
            $FilePath
        )
        
        $jsonOutput = & $ffprobePath @args 2>&1 | Out-String
        $info = $jsonOutput | ConvertFrom-Json
        
        $videoStream = $info.streams | Where-Object { $_.codec_type -eq "video" } | Select-Object -First 1
        $audioStreams = $info.streams | Where-Object { $_.codec_type -eq "audio" }
        
        # Get first audio stream for backwards compatibility
        $audioStream = $audioStreams | Select-Object -First 1
        
        # Extract audio track information (language, codec, channels)
        $audioTracksInfo = @()
        $trackIndex = 0
        foreach ($audio in $audioStreams) {
            # Try to get language from tags
            $language = if ($audio.tags.language) { $audio.tags.language } else { $null }
            
            # If no language tag, try to parse from title
            if (-not $language -or $language -eq "und") {
                $title = if ($audio.tags.title) { $audio.tags.title } else { "" }
                
                # Check if title contains language info
                if ($title -match "english|eng") { $language = "eng" }
                elseif ($title -match "french|fra|fre") { $language = "fra" }
                elseif ($title -match "spanish|spa|esp") { $language = "spa" }
                elseif ($title -match "german|ger|deu") { $language = "deu" }
                elseif ($title -match "italian|ita") { $language = "ita" }
                elseif ($title -match "portuguese|por") { $language = "por" }
                elseif ($title -match "japanese|jpn") { $language = "jpn" }
                elseif ($title -match "chinese|chi|zho") { $language = "zho" }
                elseif ($title -match "korean|kor") { $language = "kor" }
                elseif ($title -match "russian|rus") { $language = "rus" }
                # If still unknown and this is the first audio track, assume English
                elseif ($trackIndex -eq 0) { $language = "eng" }
                else { $language = "und" }
            }
            
            $title = if ($audio.tags.title) { $audio.tags.title } else { "" }
            $channels = if ($audio.channels) { $audio.channels } else { 2 }
            $codec = if ($audio.codec_name) { $audio.codec_name } else { "unknown" }
            
            $audioTracksInfo += @{
                index = $trackIndex
                language = $language
                title = $title
                codec = $codec
                channels = $channels
            }
            $trackIndex++
        }
        
        # Convert audio tracks to JSON string for storage
        # Force array output even for single track using @() wrapper
        $audioTracksJson = if ($audioTracksInfo.Count -gt 0) { 
            if ($audioTracksInfo.Count -eq 1) {
                # Single track - wrap in array to ensure consistent JSON format
                ConvertTo-Json -InputObject @($audioTracksInfo) -Compress
            } else {
                # Multiple tracks
                $audioTracksInfo | ConvertTo-Json -Compress
            }
        } else { 
            $null 
        }
        
        return @{
            Duration = [double]$info.format.duration
            Size = [long]$info.format.size
            VideoCodec = $videoStream.codec_name
            VideoWidth = $videoStream.width
            VideoHeight = $videoStream.height
            AudioCodec = $audioStream.codec_name
            AudioTracks = $audioTracksJson
            Bitrate = [long]$info.format.bit_rate
        }
    }
    catch {
        Write-Host "[!] Error getting video info: $_" -ForegroundColor Yellow
        return $null
    }
}

function Start-VideoTranscode {
    <#
    .SYNOPSIS
        Starts FFmpeg transcoding to HLS format for browser-compatible streaming
    .DESCRIPTION
        Uses HLS (HTTP Live Streaming) which creates small segments that browsers
        can easily decode. This solves the MEDIA_ERR_DECODE issues.
    #>
    param(
        [Parameter(Mandatory=$true)]
        [string]$InputFile,
        [Parameter(Mandatory=$true)]
        [int]$VideoId,
        [Parameter(Mandatory=$false)]
        [int]$AudioTrackIndex = 0
    )
    
    $transcodeId = [guid]::NewGuid().ToString()
    $fileName = Split-Path -Leaf $InputFile
    
    Write-Host "[→] Transcode $transcodeId START (HLS): $fileName" -ForegroundColor Magenta
    
    # Create HLS output directory
    $hlsDir = Join-Path $PSScriptRoot "hls_temp\$transcodeId"
    New-Item -ItemType Directory -Path $hlsDir -Force | Out-Null
    
    # Get encoder configuration
    $encoderConfig = Get-OptimalFFmpegEncoder
    
    # Build FFmpeg arguments for HLS output
    $ffmpegArgs = @()
    
    # Input file
    $ffmpegArgs += @("-i", $InputFile)
    
    # Video encoding
    $ffmpegArgs += @(
        "-c:v", $encoderConfig.Encoder
        "-preset", $encoderConfig.Preset
        "-pix_fmt", "yuv420p"  # Force 8-bit YUV420 for browser compatibility
    )
    
    # Quality settings
    if ($encoderConfig.UsesCRF) {
        $ffmpegArgs += @("-crf", $Global:TranscodingConfig.VideoCRF.ToString())
    } else {
        $ffmpegArgs += @(
            "-b:v", $Global:TranscodingConfig.VideoMaxrate
            "-maxrate", $Global:TranscodingConfig.VideoMaxrate
            "-bufsize", $Global:TranscodingConfig.VideoBufsize
        )
        
        if ($encoderConfig.Encoder -eq "h264_nvenc") {
            $ffmpegArgs += @("-rc", "vbr", "-cq", "23")
        }
        elseif ($encoderConfig.Encoder -eq "h264_qsv") {
            $ffmpegArgs += @("-global_quality", "23")
        }
        elseif ($encoderConfig.Encoder -eq "h264_amf") {
            $ffmpegArgs += @("-rc", "cqp", "-qp_i", "23", "-qp_p", "23", "-qp_b", "23")
        }
    }
    
    # Map video and selected audio track only
    $ffmpegArgs += @("-map", "0:v:0")  # Map first video stream
    $ffmpegArgs += @("-map", "0:a:$AudioTrackIndex")  # Map ONLY the selected audio track
    
    # Audio encoding
    $ffmpegArgs += @(
        "-c:a", $Global:TranscodingConfig.AudioCodec
        "-b:a", $Global:TranscodingConfig.AudioBitrate
        "-ac", $Global:TranscodingConfig.AudioChannels.ToString()
    )
    
    # Preserve metadata including language tags
    $ffmpegArgs += @("-map_metadata", "0")
    
    # HLS-specific settings with MPEG-TS segments (proven stable)
    $segmentDuration = if ($Global:TranscodingConfig.HTSSegmentDuration) { $Global:TranscodingConfig.HTSSegmentDuration } else { 4 }
    
    $ffmpegArgs += @(
        "-f", "hls"
        "-hls_time", $segmentDuration.ToString()
        "-hls_list_size", "0"
        "-hls_playlist_type", "event"
        "-hls_segment_type", "mpegts"        # MPEG-TS is proven stable
        "-hls_flags", "independent_segments+append_list"
        "-hls_segment_filename", "$hlsDir\segment_%03d.ts"
        "$hlsDir\playlist.m3u8"
    )
    
    # Build arguments string with proper quoting for paths with spaces
    $argList = @()
    foreach ($arg in $ffmpegArgs) {
        if ($arg -match '\s' -or $arg -match '\\') {
            # Path or argument with spaces - wrap in quotes
            $argList += "`"$arg`""
        } else {
            $argList += $arg
        }
    }
    
    # Log command
    $cmdLine = $Global:FFmpegCapabilities.FFmpegPath + " " + ($argList -join ' ')
    Write-Host "[i] Full Command:" -ForegroundColor Gray
    Write-Host "    $cmdLine" -ForegroundColor DarkGray
    Write-Host "[i] HLS directory: $hlsDir" -ForegroundColor Gray
    
    # Store transcode info
    $transcodeInfo = @{
        TranscodeId = $transcodeId
        VideoId = $VideoId
        HLSDirectory = $hlsDir
        StartTime = Get-Date
        Process = $null
        ErrorOutput = [System.Collections.Concurrent.ConcurrentBag[string]]::new()
    }
    
    try {
        # Start FFmpeg process
        $psi = New-Object System.Diagnostics.ProcessStartInfo
        $psi.FileName = $Global:FFmpegCapabilities.FFmpegPath
        $psi.Arguments = $argList -join ' '
        
        $psi.UseShellExecute = $false
        $psi.RedirectStandardOutput = $true
        $psi.RedirectStandardError = $true
        $psi.CreateNoWindow = $true
        
        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $psi
        
        # Register event handler BEFORE starting process
        $stderrHandler = Register-ObjectEvent -InputObject $process -EventName ErrorDataReceived -Action {
            param($sender, $e)
            if (-not [string]::IsNullOrEmpty($e.Data)) {
                Write-Host "    FFmpeg: $($e.Data)" -ForegroundColor Gray
            }
        }
        
        # Start process
        $process.Start() | Out-Null
        
        # Begin async reads
        $process.BeginErrorReadLine()
        $process.BeginOutputReadLine()
        
        $transcodeInfo.Process = $process
        
        Write-Host "[✓] Transcoding process started (PID: $($process.Id))" -ForegroundColor Green
        Write-Host "[i] HLS segments will be available at: /hls/$transcodeId/playlist.m3u8" -ForegroundColor Cyan
        Write-Host "[i] EVENT→VOD conversion enabled for proper seeking (v6.9)" -ForegroundColor Green
        
        # Background job: Convert EVENT playlist to VOD when transcoding completes
        Start-Job -ScriptBlock {
            param($ProcessId, $PlaylistPath)
            
            # Wait for process to complete
            $process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
            if ($process) {
                $process.WaitForExit()
            }
            
            # Give FFmpeg time to finalize
            Start-Sleep -Seconds 2
            
            # Convert EVENT to VOD and add ENDLIST
            if (Test-Path $PlaylistPath) {
                try {
                    $content = Get-Content -Path $PlaylistPath -Raw
                    
                    # Replace EVENT with VOD
                    $content = $content -replace '#EXT-X-PLAYLIST-TYPE:EVENT', '#EXT-X-PLAYLIST-TYPE:VOD'
                    
                    # Add ENDLIST if not present
                    if ($content -notmatch '#EXT-X-ENDLIST') {
                        $content = $content.TrimEnd() + "`n#EXT-X-ENDLIST`n"
                    }
                    
                    Set-Content -Path $PlaylistPath -Value $content -NoNewline
                } catch {}
            }
        } -ArgumentList $process.Id, (Join-Path $hlsDir "playlist.m3u8") | Out-Null
        
        # Wait a moment for FFmpeg to start writing files
        Start-Sleep -Milliseconds 1500
        
        # Check if process already exited (error)
        if ($process.HasExited) {
            Write-Host "[✗] FFmpeg exited immediately (exit code: $($process.ExitCode))" -ForegroundColor Red
            Write-Host "[!] FFmpeg errors:" -ForegroundColor Yellow
            $transcodeInfo.ErrorOutput | Select-Object -Last 10 | ForEach-Object {
                Write-Host "    $_" -ForegroundColor Gray
            }
            
            # Cleanup
            if (Test-Path $hlsDir) {
                Remove-Item -Path $hlsDir -Recurse -Force -ErrorAction SilentlyContinue
            }
            return $null
        }
        
        # Store in active transcodes
        $Global:ActiveTranscodes.TryAdd($transcodeId, $transcodeInfo) | Out-Null
        
        # Return transcode info
        return @{
            TranscodeId = $transcodeId
            HLSDirectory = $hlsDir
            PlaylistPath = "$hlsDir\playlist.m3u8"
        }
    }
    catch {
        Write-Host "[✗] Failed to start transcode: $_" -ForegroundColor Red
        
        # Cleanup
        if (Test-Path $hlsDir) {
            Remove-Item -Path $hlsDir -Recurse -Force -ErrorAction SilentlyContinue
        }
        
        return $null
    }
}

function Stop-AllTranscodes {
    <#
    .SYNOPSIS
        Stops all active transcoding processes and cleans up temp files
    #>
    
    if ($Global:ActiveTranscodes.Count -eq 0) {
        return
    }
    
    Write-Host "[i] Stopping $($Global:ActiveTranscodes.Count) active transcode(s)..." -ForegroundColor Gray
    
    foreach ($kvp in $Global:ActiveTranscodes.GetEnumerator()) {
        try {
            $transcodeInfo = $kvp.Value
            if ($transcodeInfo.Process -and -not $transcodeInfo.Process.HasExited) {
                Write-Host "    Stopping: $($kvp.Key)" -ForegroundColor Gray
                $transcodeInfo.Process.Kill()
                $transcodeInfo.Process.WaitForExit(2000) | Out-Null
                $transcodeInfo.Process.Dispose()
            }
            
            # Clean up HLS directory - FIXED: Added cleanup
            if ($transcodeInfo.HLSDirectory -and (Test-Path $transcodeInfo.HLSDirectory)) {
                Start-Sleep -Milliseconds 500  # Give FFmpeg time to release files
                Remove-Item -Path $transcodeInfo.HLSDirectory -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
        catch {
            Write-Host "    [!] Error: $_" -ForegroundColor Yellow
        }
    }
    
    $Global:ActiveTranscodes.Clear()
    
    # Clean up orphaned HLS directories
    $hlsBase = Join-Path $PSScriptRoot "hls_temp"
    if (Test-Path $hlsBase) {
        Get-ChildItem -Path $hlsBase -Directory -ErrorAction SilentlyContinue | ForEach-Object {
            Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
    
    Write-Host "[✓] All transcodes stopped and temp files cleaned" -ForegroundColor Green
}

function Get-TranscodingStatus {
    <#
    .SYNOPSIS
        Returns status of active transcoding sessions
    #>
    
    $status = @()
    
    foreach ($kvp in $Global:ActiveTranscodes.GetEnumerator()) {
        $info = $kvp.Value
        $duration = ((Get-Date) - $info.StartTime).TotalSeconds
        
        $status += @{
            TranscodeId = $kvp.Key
            InputFile = Split-Path -Leaf $info.InputFile
            Encoder = $info.Encoder
            Duration = [math]::Round($duration, 1)
            PID = $info.Process.Id
        }
    }
    
    return $status
}

# ============================================================================
# INTEGRATION WITH EXISTING MEDIA SERVER
# ============================================================================
# These functions modify the existing Start-MediaServer routing

function Get-SmartVideoStream {
    <#
    .SYNOPSIS
        Smart video streaming - returns HLS playlist URL for transcoded videos
    .DESCRIPTION
        For MKV/AVI/etc: Starts HLS transcoding and returns playlist URL
        For MP4: Streams directly
    #>
    param(
        [Parameter(Mandatory=$true)]
        [string]$FilePath,
        [Parameter(Mandatory=$true)]
        [int]$VideoId,
        [Parameter(Mandatory=$false)]
        [int]$AudioTrackIndex = 0
    )
    
    # Check if file exists
    if (-not (Test-Path -LiteralPath $FilePath)) {
        Write-Host "[✗] Video file not found: $FilePath" -ForegroundColor Red
        return $null
    }
    
    $needsTranscoding = Test-VideoNeedsTranscoding -FilePath $FilePath
    $fileName = Split-Path -Leaf $FilePath
    $extension = [System.IO.Path]::GetExtension($FilePath).ToLower()
    
    Write-Host "[i] Smart Stream Check: $fileName" -ForegroundColor Cyan
    Write-Host "    Extension: $extension" -ForegroundColor Gray
    Write-Host "    Needs Transcoding: $needsTranscoding" -ForegroundColor Gray
    Write-Host "    FFmpeg Available: $(if ($Global:FFmpegCapabilities) { 'YES' } else { 'NO' })" -ForegroundColor $(if ($Global:FFmpegCapabilities) { 'Green' } else { 'Red' })
    
    if ($needsTranscoding -and $Global:FFmpegCapabilities) {
        # Check if there's already an active transcode for this video
        $existingTranscode = $null
        foreach ($kvp in $Global:ActiveTranscodes.GetEnumerator()) {
            if ($kvp.Value.VideoId -eq $VideoId) {
                $existingTranscode = $kvp
                break
            }
        }
        
        if ($existingTranscode) {
            Write-Host "[i] Using existing transcode for video $VideoId : $($existingTranscode.Key)" -ForegroundColor Cyan
            return @{
                Type = "HLS"
                PlaylistURL = "/hls/$($existingTranscode.Key)/playlist.m3u8"
                TranscodeId = $existingTranscode.Key
            }
        }
        
        Write-Host "[→] Starting HLS transcode for: $fileName (audio track: $AudioTrackIndex)" -ForegroundColor Magenta
        
        # Start HLS transcoding with selected audio track
        $transcodeInfo = Start-VideoTranscode -InputFile $FilePath -VideoId $VideoId -AudioTrackIndex $AudioTrackIndex
        
        if ($transcodeInfo) {
            # Return HLS playlist URL
            return @{
                Type = "HLS"
                PlaylistURL = "/hls/$($transcodeInfo.TranscodeId)/playlist.m3u8"
                TranscodeId = $transcodeInfo.TranscodeId
            }
        }
        else {
            Write-Host "[✗] Failed to start HLS transcode" -ForegroundColor Red
            return $null
        }
    }
    else {
        # Direct streaming for MP4
        Write-Host "[✓] DIRECT STREAMING: $fileName (compatible format)" -ForegroundColor Green
        return @{
            Type = "Direct"
            StreamURL = "/stream/$VideoId"
        }
    }
}

# ============================================================================
# CONFIGURATION UI ADDITIONS
# ============================================================================

function Show-TranscodingConfig {
    <#
    .SYNOPSIS
        Displays current transcoding configuration
    #>
    
    Write-Host "`n=== Transcoding Configuration ===" -ForegroundColor Cyan
    Write-Host "Hardware Acceleration: $(if ($Global:TranscodingConfig.EnableHardwareAccel) { 'Enabled' } else { 'Disabled' })" -ForegroundColor $(if ($Global:TranscodingConfig.EnableHardwareAccel) { 'Green' } else { 'Yellow' })
    Write-Host "Preferred Encoder: $($Global:TranscodingConfig.PreferredEncoder)" -ForegroundColor Cyan
    Write-Host "Video Quality (CRF): $($Global:TranscodingConfig.VideoCRF)" -ForegroundColor Cyan
    Write-Host "Max Bitrate: $($Global:TranscodingConfig.VideoMaxrate)" -ForegroundColor Cyan
    Write-Host "Audio Bitrate: $($Global:TranscodingConfig.AudioBitrate)" -ForegroundColor Cyan
    Write-Host "Max Concurrent Transcodes: $($Global:TranscodingConfig.MaxConcurrentTranscodes)" -ForegroundColor Cyan
    
    if ($Global:FFmpegCapabilities) {
        $encoder = Get-OptimalFFmpegEncoder
        Write-Host "`nActive Encoder: $($encoder.Name) ($($encoder.Type))" -ForegroundColor Green
    }
}

function Set-TranscodingConfig {
    <#
    .SYNOPSIS
        Interactive configuration of transcoding settings
    #>
    
    Write-Host "`n=== Configure Transcoding ===" -ForegroundColor Cyan
    
    # Hardware acceleration
    $hwChoice = Read-Host "Enable hardware acceleration? (Y/N) [Current: $(if ($Global:TranscodingConfig.EnableHardwareAccel) { 'Y' } else { 'N' })]"
    if ($hwChoice -match '^[YyNn]$') {
        $Global:TranscodingConfig.EnableHardwareAccel = ($hwChoice -match '^[Yy]$')
    }
    
    # Encoder preference
    Write-Host "`nEncoder options: auto, nvenc (NVIDIA), qsv (Intel), amf (AMD), software"
    $encoderChoice = Read-Host "Preferred encoder? [Current: $($Global:TranscodingConfig.PreferredEncoder)]"
    if ($encoderChoice) {
        $validEncoders = @('auto', 'nvenc', 'qsv', 'amf', 'vaapi', 'videotoolbox', 'software')
        if ($validEncoders -contains $encoderChoice.ToLower()) {
            $Global:TranscodingConfig.PreferredEncoder = $encoderChoice.ToLower()
        }
    }
    
    # Quality
    $crfChoice = Read-Host "Video quality CRF (18-28, lower=better) [Current: $($Global:TranscodingConfig.VideoCRF)]"
    if ($crfChoice -match '^\d+$' -and [int]$crfChoice -ge 18 -and [int]$crfChoice -le 28) {
        $Global:TranscodingConfig.VideoCRF = [int]$crfChoice
    }
    
    Write-Host "`n[✓] Configuration updated" -ForegroundColor Green
    Show-TranscodingConfig
}

# ============================================================================
# EXAMPLE: Modify /stream-video route
# ============================================================================
<#
REPLACE this section in your route handler:

# OLD CODE (around line 5800-5850):
"/stream-video" {
    # ... existing code ...
    Start-AsyncFileStream -Response $response -Request $request -FilePath $videoPath -ContentType $contentType
}

# NEW CODE:
"/stream-video" {
    $videoId = $request.QueryString["id"]
    if (-not $videoId) {
        $response.StatusCode = 400
        $response.Close()
        continue
    }
    
    $videoQuery = @"
SELECT file_path FROM videos WHERE id = '$videoId'
"@
    $videoResult = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query $videoQuery
    
    if ($videoResult) {
        $videoPath = $videoResult.file_path
        
        # Use smart streaming (auto-transcodes if needed)
        Get-SmartVideoStream -FilePath $videoPath -Response $response -Request $request
    }
    else {
        $response.StatusCode = 404
        $response.Close()
    }
}
#>

Write-Host "[✓] FFmpeg Transcoding Module Loaded" -ForegroundColor Green

function Start-AsyncFileStream {
    param(
        [Parameter(Mandatory=$true)]
        [System.Net.HttpListenerResponse]$Response,
        [Parameter(Mandatory=$true)]
        [System.Net.HttpListenerRequest]$Request,
        [Parameter(Mandatory=$true)]
        [string]$FilePath,
        [Parameter(Mandatory=$true)]
        [string]$ContentType,
        [Parameter(Mandatory=$false)]
        [hashtable]$AdditionalHeaders = @{}
    )
    
    $streamId = [guid]::NewGuid().ToString()
    
    # Get file size
    try {
        $fileInfo = Get-Item -LiteralPath $FilePath -ErrorAction Stop
        $fileSize = $fileInfo.Length
    } catch {
        Write-Host "[✗] Error: Could not get file size for $FilePath" -ForegroundColor Red
        $Response.StatusCode = 500
        $Response.Close()
        return $null
    }
    
    # Parse Range header (e.g., "bytes=0-1023" or "bytes=1024-")
    $rangeHeader = $Request.Headers["Range"]
    $startByte = 0
    $endByte = $fileSize - 1
    $isRangeRequest = $false
    
    if ($rangeHeader) {
        $isRangeRequest = $true
        # Parse "bytes=START-END" format
        if ($rangeHeader -match 'bytes=(\d+)-(\d*)') {
            $startByte = [long]$matches[1]
            if ($matches[2]) {
                $endByte = [long]$matches[2]
            } else {
                # "bytes=1024-" means from 1024 to end of file
                # But limit to reasonable chunk size (50MB) to avoid overwhelming the system
                $maxChunkSize = 50MB
                $endByte = [Math]::Min($startByte + $maxChunkSize - 1, $fileSize - 1)
            }
            
            Write-Host "[DEBUG] Range parsed: start=$startByte, end=$endByte, fileSize=$fileSize" -ForegroundColor Gray
        } else {
            Write-Host "[!] Could not parse range header: $rangeHeader" -ForegroundColor Yellow
        }
        
        # Validate range
        if ($startByte -gt $endByte -or $startByte -ge $fileSize) {
            Write-Host "[!] Invalid range request: $rangeHeader" -ForegroundColor Yellow
            Write-Host "[!] Validation failed: startByte=$startByte, endByte=$endByte, fileSize=$fileSize" -ForegroundColor Yellow
            $Response.StatusCode = 416  # Range Not Satisfiable
            $Response.Headers.Add("Content-Range", "bytes */$fileSize")
            $Response.Close()
            return $null
        }
        
        # Adjust end byte if it exceeds file size
        if ($endByte -ge $fileSize) {
            $endByte = $fileSize - 1
        }
    }
    
    $contentLength = $endByte - $startByte + 1
    
    # Log the streaming details
    $fileName = Split-Path -Leaf $FilePath
    if ($isRangeRequest) {
        $startMB = [math]::Round($startByte / 1MB, 2)
        $endMB = [math]::Round($endByte / 1MB, 2)
        $sizeMB = [math]::Round($contentLength / 1MB, 2)
        Write-Host "[→] Stream $streamId RANGE: $fileName" -ForegroundColor Cyan
        Write-Host "    Range: $startMB MB - $endMB MB (sending $sizeMB MB)" -ForegroundColor Gray
    } else {
        $sizeMB = [math]::Round($fileSize / 1MB, 2)
        Write-Host "[→] Stream $streamId FULL: $fileName ($sizeMB MB)" -ForegroundColor Cyan
    }
    
    # Set response headers
    try {
        # Check if response has already been sent/closed
        if ($Response.OutputStream -eq $null -or -not $Response.OutputStream.CanWrite) {
            Write-Host "[!] Response already closed, cannot send data" -ForegroundColor Yellow
            return $null
        }
        
        $Response.ContentType = $ContentType
        $Response.ContentLength64 = $contentLength
        
        # Essential headers for video streaming
        $Response.Headers.Add("Accept-Ranges", "bytes")
        $Response.Headers.Add("Cache-Control", "public, max-age=3600")
        
        if ($isRangeRequest) {
            $Response.StatusCode = 206  # Partial Content
            $Response.Headers.Add("Content-Range", "bytes $startByte-$endByte/$fileSize")
        } else {
            $Response.StatusCode = 200  # OK
        }
        
        # Add additional headers
        foreach ($key in $AdditionalHeaders.Keys) {
            $Response.Headers.Add($key, $AdditionalHeaders[$key])
        }
    }
    catch {
        Write-Host "[!] Error setting response headers: $_" -ForegroundColor Yellow
        Write-Host "    Response may already be in use - this is normal if client cancelled request" -ForegroundColor Gray
        return $null
    }
    
    # Create async streaming job
    $ps = [powershell]::Create()
    $ps.RunspacePool = $Global:RunspacePool
    
    [void]$ps.AddScript({
        param($OutputStream, $FilePath, $StartByte, $EndByte, $StreamId)
        $fileStream = $null
        try {
            # Open file and seek to start position
            $fileStream = [System.IO.File]::OpenRead($FilePath)
            if ($StartByte -gt 0) {
                $fileStream.Seek($StartByte, [System.IO.SeekOrigin]::Begin) | Out-Null
            }
            
            # Use larger buffer for better performance (512KB)
            $bufferSize = 524288  # 512KB buffer
            $buffer = New-Object byte[] $bufferSize
            $totalBytes = 0
            $remainingBytes = $EndByte - $StartByte + 1
            
            while ($remainingBytes -gt 0) {
                # Read up to buffer size or remaining bytes, whichever is smaller
                $bytesToRead = [Math]::Min($bufferSize, $remainingBytes)
                $bytesRead = $fileStream.Read($buffer, 0, $bytesToRead)
                
                if ($bytesRead -le 0) { break }
                
                # Write to output stream
                $OutputStream.Write($buffer, 0, $bytesRead)
                $OutputStream.Flush()
                
                $totalBytes += $bytesRead
                $remainingBytes -= $bytesRead
            }
            
            $sizeMB = [math]::Round($totalBytes / 1MB, 2)
            Write-Host "[✓] Stream $StreamId completed: $sizeMB MB sent" -ForegroundColor Green
        }
        catch {
            Write-Host "[✗] Stream $StreamId error: $_" -ForegroundColor Red
        }
        finally {
            if ($null -ne $fileStream) {
                $fileStream.Close()
                $fileStream.Dispose()
            }
            if ($null -ne $OutputStream) {
                try { $OutputStream.Close() } catch { }
            }
        }
    })
    
    [void]$ps.AddParameter('OutputStream', $Response.OutputStream)
    [void]$ps.AddParameter('FilePath', $FilePath)
    [void]$ps.AddParameter('StartByte', $startByte)
    [void]$ps.AddParameter('EndByte', $endByte)
    [void]$ps.AddParameter('StreamId', $streamId)
    
    # Before starting new stream, cleanup old streams for the same file (keep only most recent 2)
    $streamsForThisFile = @()
    foreach ($kvp in $Global:ActiveStreams.GetEnumerator()) {
        if ($kvp.Value.FilePath -eq $FilePath) {
            $streamsForThisFile += $kvp
        }
    }
    
    # If we have more than 2 streams for this file, kill the oldest ones
    if ($streamsForThisFile.Count -ge 2) {
        $toKill = $streamsForThisFile | Sort-Object { $_.Value.StartTime } | Select-Object -First ($streamsForThisFile.Count - 1)
        foreach ($kvp in $toKill) {
            Write-Host "[i] Stopping old stream for same file: $($kvp.Key)" -ForegroundColor Gray
            try {
                if ($kvp.Value.PowerShell) {
                    $kvp.Value.PowerShell.Stop()
                    $kvp.Value.PowerShell.Dispose()
                }
                $Global:ActiveStreams.TryRemove($kvp.Key, [ref]$null) | Out-Null
            } catch {}
        }
    }
    
    $handle = $ps.BeginInvoke()
    $streamJob = @{
        PowerShell = $ps
        Handle = $handle
        StreamId = $streamId
        FilePath = $FilePath
        StartTime = Get-Date
    }
    $Global:ActiveStreams.TryAdd($streamId, $streamJob) | Out-Null
    Write-Host "[i] Active streams: $($Global:ActiveStreams.Count)" -ForegroundColor Gray
    
    # Cleanup completed streams
    $completedStreams = @()
    foreach ($kvp in $Global:ActiveStreams.GetEnumerator()) {
        $job = $kvp.Value
        if ($null -ne $job.PowerShell) {
            $state = $job.PowerShell.InvocationStateInfo.State
            if ($state -in @('Completed', 'Failed', 'Stopped')) {
                $completedStreams += $kvp.Key
                try { $job.PowerShell.Dispose() } catch {}
            }
        }
    }
    foreach ($id in $completedStreams) {
        $removed = $null
        $Global:ActiveStreams.TryRemove($id, [ref]$removed) | Out-Null
    }
    if ($completedStreams.Count -gt 0) {
        Write-Host "[i] Cleaned up $($completedStreams.Count) completed stream(s)" -ForegroundColor Gray
    }
    
    $Response | Add-Member -NotePropertyName '_AsyncHandled' -NotePropertyValue $true -Force
    return $streamId
}

# ============================================================================
# CONFIGURATION VARIABLES - Edit these to match your setup
# ============================================================================

# ============================================================================
# CONFIGURATION FILE MANAGEMENT
# ============================================================================

# ============================================================================
# CONFIGURATION FILE MANAGEMENT
# ============================================================================

# Initialize debug log with rotation (keep logs for 7 days)
function Initialize-DebugLog {
    $logPath = Join-Path $PSScriptRoot "debug.log"
    
    # Check if log file exists and is older than 7 days
    if (Test-Path $logPath) {
        $logAge = (Get-Date) - (Get-Item $logPath).LastWriteTime
        if ($logAge.Days -ge 7) {
            # Archive old log
            $archiveName = "debug_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
            $archivePath = Join-Path $PSScriptRoot $archiveName
            Move-Item -Path $logPath -Destination $archivePath -Force
            
            # Delete archives older than 30 days
            Get-ChildItem -Path $PSScriptRoot -Filter "debug_*.log" | 
                Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } | 
                Remove-Item -Force
        }
    }
    
    # Create/clear log for new session
    "=== PSMediaLibrary Debug Log ===" | Out-File -FilePath $logPath
    "Session started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" | Out-File -FilePath $logPath -Append
    "" | Out-File -FilePath $logPath -Append
}

# Initialize log
Initialize-DebugLog

function Initialize-ConfigFile {
    <#
    .SYNOPSIS
        Creates or loads PSMediaLib.conf configuration file
    .DESCRIPTION
        Creates a new configuration file with default values if it doesn't exist,
        or loads existing configuration from the file. Uses CSV format for easy editing.
    #>
    
    $configPath = Join-Path $PSScriptRoot "PSMediaLib.conf"
    
    
    "[DEBUG] Config file path: $configPath" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    "[DEBUG] Config file exists: $(Test-Path $configPath)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    
    if (Test-Path $configPath) {
        Write-Host "[i] Loading configuration from PSMediaLib.conf..." -ForegroundColor Cyan
        "[i] Loading configuration from PSMediaLib.conf..." | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
        
        try {
            # Load configuration from CSV file
            $configData = Import-Csv -Path $configPath
            
            
            # Create hashtable from CSV data
            $loadedConfig = @{}
            
            foreach ($item in $configData) {
                $key = $item.Setting
                $value = $item.Value
                
                # Debug output for our specific settings
                if ($key -in @('GhostscriptInstalled', 'FFmpegInstalled')) {
                }
                
                # Parse different value types
                switch ($key) {
                    # Arrays (comma-separated values)
                    { $_ -in @('IPWhitelist', 'VideoExtensions', 'AudioExtensions', 'ImageExtensions', 'PDFExtensions') } {
                        if ([string]::IsNullOrWhiteSpace($value) -or $value -eq '()') {
                            $loadedConfig[$key] = @()
                        } else {
                            $loadedConfig[$key] = $value -split ',' | ForEach-Object { $_.Trim() }
                        }
                    }
                    # Integers
                    { $_ -in @('ServerPort', 'LowQualityVideoThreshold', 'LowQualityAudioThreshold', 'VideoCRF', 'MaxConcurrentTranscodes') } {
                        $loadedConfig[$key] = [int]$value
                    }
                    # Booleans (Yes/No or True/False)
                    { $_ -in @('GhostscriptInstalled', 'FFmpegInstalled', 'TranscodingEnabled', 'SecurityByPin') } {
                        $loadedConfig[$key] = ($value -eq 'Yes' -or $value -eq 'yes' -or $value -eq 'Y' -or $value -eq 'y' -or $value -eq 'True' -or $value -eq 'true')
                    }
                    # Strings (default)
                    default {
                        $loadedConfig[$key] = $value
                    }
                }
            }
            
            Write-Host "[✓] Configuration loaded successfully from PSMediaLib.conf" -ForegroundColor Green
            
            "[✓] Configuration loaded successfully" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
            
            # Migration: Add SecurityByPin if not present in existing config
            if (-not $loadedConfig.ContainsKey('SecurityByPin')) {
                Write-Host "[i] Adding SecurityByPin setting to config file..." -ForegroundColor Yellow
                try {
                    # Read existing config
                    $existingConfig = Import-Csv -Path $configPath
                    
                    # Add new setting
                    $newSetting = [PSCustomObject]@{ 
                        Setting = "SecurityByPin"
                        Value = "True"
                        Description = "Require PIN authentication (True/False). Set to False to auto-login as admin."
                    }
                    
                    # Combine and save
                    $updatedConfig = @($existingConfig) + @($newSetting)
                    $updatedConfig | Export-Csv -Path $configPath -NoTypeInformation -Encoding UTF8
                    
                    # Add to loaded config
                    $loadedConfig['SecurityByPin'] = $true
                    
                    Write-Host "[✓] SecurityByPin setting added to PSMediaLib.conf" -ForegroundColor Green
                }
                catch {
                    Write-Host "[!] Could not add SecurityByPin to config: $_" -ForegroundColor Red
                    # Default to true if migration fails
                    $loadedConfig['SecurityByPin'] = $true
                }
            }
            
            # Debug: Show if manual installation flags are set
            if ($loadedConfig.ContainsKey('GhostscriptInstalled')) {
                Write-Host "[i] GhostscriptInstalled value from config: '$($loadedConfig.GhostscriptInstalled)'" -ForegroundColor Gray
                "[i] GhostscriptInstalled = $($loadedConfig.GhostscriptInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
            } else {
                "[DEBUG] GhostscriptInstalled NOT in hashtable" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
                "[DEBUG] Keys: $($loadedConfig.Keys -join ', ')" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
            }
            if ($loadedConfig.ContainsKey('FFmpegInstalled')) {
                Write-Host "[i] FFmpegInstalled value from config: '$($loadedConfig.FFmpegInstalled)'" -ForegroundColor Gray
                "[i] FFmpegInstalled = $($loadedConfig.FFmpegInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
            }
            
            return $loadedConfig
        }
        catch {
            Write-Host "[!] Error loading configuration file: $_" -ForegroundColor Red
            Write-Host "[!] Using default configuration values" -ForegroundColor Yellow
            return $null
        }
    }
    else {
        Write-Host "[i] PSMediaLib.conf not found. Creating new configuration file..." -ForegroundColor Cyan
        
        # Create default configuration data for CSV
        $defaultConfig = @(
            [PSCustomObject]@{ Setting = "PicturesFolder"; Value = "D:\Share"; Description = "Folder containing pictures/images" }
            [PSCustomObject]@{ Setting = "MoviesFolder"; Value = "D:\Share"; Description = "Folder containing video files" }
            [PSCustomObject]@{ Setting = "MusicFolder"; Value = "D:\Share"; Description = "Folder containing music files" }
            [PSCustomObject]@{ Setting = "PDFFolder"; Value = "D:\Share"; Description = "Folder containing PDF documents" }
            [PSCustomObject]@{ Setting = "ServerPort"; Value = "8182"; Description = "Web server port number" }
            [PSCustomObject]@{ Setting = "ServerHost"; Value = "+"; Description = "Server host binding (+ for all interfaces)" }
            [PSCustomObject]@{ Setting = "IPWhitelist"; Value = ""; Description = "Allowed IP addresses (comma-separated, empty for all)" }
            [PSCustomObject]@{ Setting = "SecurityByPin"; Value = "True"; Description = "Require PIN authentication (True/False). Set to False to auto-login as admin." }
            [PSCustomObject]@{ Setting = "TMDB_APIKey"; Value = ""; Description = "TheMovieDB API key for movie posters" }
            [PSCustomObject]@{ Setting = "LastFM_APIKey"; Value = ""; Description = "Last.fm API key for music album art" }
            [PSCustomObject]@{ Setting = "LowQualityVideoThreshold"; Value = "720"; Description = "Video resolution threshold (height in pixels)" }
            [PSCustomObject]@{ Setting = "LowQualityAudioThreshold"; Value = "128"; Description = "Audio bitrate threshold (kbps)" }
            [PSCustomObject]@{ Setting = "VideoExtensions"; Value = ".mp4,.mkv,.avi,.mov,.wmv,.flv,.webm,.m4v"; Description = "Supported video file extensions" }
            [PSCustomObject]@{ Setting = "AudioExtensions"; Value = ".mp3,.flac,.wav,.m4a,.aac,.ogg,.wma,.opus"; Description = "Supported audio file extensions" }
            [PSCustomObject]@{ Setting = "ImageExtensions"; Value = ".jpg,.jpeg,.png,.gif,.bmp,.webp,.tiff"; Description = "Supported image file extensions" }
            [PSCustomObject]@{ Setting = "PDFExtensions"; Value = ".pdf"; Description = "Supported PDF file extensions" }
            [PSCustomObject]@{ Setting = "GhostscriptInstalled"; Value = "No"; Description = "Set to 'Yes' if Ghostscript is manually installed (for PDF previews)" }
            [PSCustomObject]@{ Setting = "FFmpegInstalled"; Value = "No"; Description = "Set to 'Yes' if FFmpeg is manually installed (for album artwork extraction)" }
            
            # Transcoding settings (v4.9)
            [PSCustomObject]@{ Setting = "TranscodingEnabled"; Value = "Yes"; Description = "Enable FFmpeg transcoding for MKV/AVI/etc (Yes/No)" }
            [PSCustomObject]@{ Setting = "PreferredEncoder"; Value = "auto"; Description = "Encoder: auto, nvenc (NVIDIA), qsv (Intel), amf (AMD), software (CPU)" }
            [PSCustomObject]@{ Setting = "VideoPreset"; Value = "veryfast"; Description = "Software encoding speed: ultrafast, veryfast, fast, medium, slow" }
            [PSCustomObject]@{ Setting = "VideoCRF"; Value = "23"; Description = "Video quality (18-28, lower=better, 23=default)" }
            [PSCustomObject]@{ Setting = "VideoMaxrate"; Value = "5M"; Description = "Max video bitrate for hardware encoding" }
            [PSCustomObject]@{ Setting = "AudioBitrate"; Value = "128k"; Description = "Audio bitrate for transcoding" }
            [PSCustomObject]@{ Setting = "MaxConcurrentTranscodes"; Value = "2"; Description = "Max simultaneous transcoding sessions" }
        )
        
        try {
            # Export to CSV
            $defaultConfig | Export-Csv -Path $configPath -NoTypeInformation -Encoding UTF8
            
            Write-Host "[✓] Created PSMediaLib.conf with default settings" -ForegroundColor Green
            Write-Host "[i] You can edit this file to customize your configuration" -ForegroundColor Gray
            
            return $null  # Return null to use hardcoded defaults for first run
        }
        catch {
            Write-Host "[!] Error creating configuration file: $_" -ForegroundColor Red
            Write-Host "[!] Using default configuration values" -ForegroundColor Yellow
            return $null
        }
    }
}


# Load configuration from file or create default
"[DEBUG] About to call Initialize-ConfigFile" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
$LoadedConfig = Initialize-ConfigFile
"[DEBUG] LoadedConfig is null: $($null -eq $LoadedConfig)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
if ($LoadedConfig) {
    "[DEBUG] LoadedConfig type: $($LoadedConfig.GetType().Name)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    "[DEBUG] LoadedConfig has $($LoadedConfig.Count) items" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
}


# Define Radio Stations array
$RadioStationsArray = @(
    # ========== NORTH AMERICA - USA ==========
    @{ Name = "NPR News"; Country = "USA"; Genre = "News"; URL = "https://npr-ice.streamguys1.com/live.mp3"; Description = "National Public Radio - News & Talk"; Logo = "radiologos/USA/npr.png" }
    @{ Name = "KEXP Seattle"; Country = "USA"; Genre = "Alternative Rock"; URL = "https://kexp-mp3-128.streamguys1.com/kexp128.mp3"; Description = "Independent alternative music from Seattle"; Logo = "radiologos/USA/kexp.png" }
    @{ Name = "181.FM Fusion Jazz"; Country = "USA"; Genre = "Jazz"; URL = "https://listen.181fm.com/181-fusionjazz_128k.mp3"; Description = "Fusion jazz hits" ; Logo = "radiologos/USA/14344.v3.png"}
    @{ Name = "WNYC New York"; Country = "USA"; Genre = "News/Talk"; URL = "https://fm939.wnyc.org/wnycfm"; Description = "New York Public Radio"; Logo = "radiologos/USA/wnyc.png" }
    @{ Name = "Radio Paradise"; Country = "USA"; Genre = "Eclectic Rock"; URL = "https://stream.radioparadise.com/aac-320"; Description = "Commercial-free eclectic music"; Logo = "radiologos/USA/radioparadise.png" }
    @{ Name = "SomaFM Groove Salad"; Country = "USA"; Genre = "Ambient/Downtempo"; URL = "https://ice1.somafm.com/groovesalad-128-mp3"; Description = "Ambient/downtempo grooves"; Logo = "radiologos/USA/groovesalad-400.png" }
    @{ Name = "SomaFM Defcon"; Country = "USA"; Genre = "Hacker/Electronic"; URL = "https://ice1.somafm.com/defcon-128-mp3"; Description = "Music for hacking"; Logo = "radiologos/USA/defcon400.png" }
    @{ Name = "181.FM The Rock"; Country = "USA"; Genre = "Classic Rock"; URL = "https://listen.181fm.com/181-rock_128k.mp3"; Description = "Classic rock hits"; Logo = "radiologos/USA/181_logo_300.webp" }
    @{ Name = "NPR National Live"; Country = "USA"; Genre = "News/Talk"; URL = "https://npr-ice.streamguys1.com/live.mp3"; Description = "National Public Radio live stream"; Logo = "radiologos/USA/npr.png" }
    @{ Name = "181.FM Energy 93"; Country = "USA"; Genre = "Top 40/Dance"; URL = "https://listen.181fm.com/181-energy93_128k.mp3"; Description = "Top 40 & dance hits"; Logo = "radiologos/USA/14344.v3.png" }
    @{ Name = "181.FM Classical Guitar"; Country = "USA"; Genre = "Classical"; URL = "https://listen.181fm.com/181-classicalguitar_128k.mp3"; Description = "Relaxing classical guitar"; Logo = "radiologos/USA/14344.v3.png" }
    @{ Name = "SomaFM Secret Agent"; Country = "USA"; Genre = "Lounge/Chill"; URL = "https://ice2.somafm.com/secretagent-128-mp3"; Description = "Spy-themed downtempo grooves"; Logo = "radiologos/USA/secret.png" }
    @{ Name = "SomaFM Indie Pop Rocks"; Country = "USA"; Genre = "Indie Pop"; URL = "https://ice2.somafm.com/indiepop-128-mp3"; Description = "Indie pop & alt rock"; Logo = "radiologos/USA/somaindie.png" }
   
    # ========== EUROPE - UK ==========
    # NOTE: Most BBC stations are geo-restricted to UK only (except World Service)
    @{ Name = "BBC Radio 1"; Country = "UK"; Genre = "Pop/Dance"; URL = "https://stream.live.vc.bbcmedia.co.uk/bbc_radio_one"; Description = "BBC's flagship pop music station"; Logo = "radiologos/Uk/bbc-radio-1-logo-png_seeklogo-314493.png" }
    @{ Name = "BBC Radio 2"; Country = "UK"; Genre = "Pop/Rock"; URL = "https://stream.live.vc.bbcmedia.co.uk/bbc_radio_two"; Description = "Popular music and culture"; Logo = "radiologos/Uk/bbc-radio-2-logo-png_seeklogo-314524.png" }
    @{ Name = "BBC Radio 3"; Country = "UK"; Genre = "Classical"; URL = "https://stream.live.vc.bbcmedia.co.uk/bbc_radio_three"; Description = "Classical music and culture"; Logo = "radiologos/Uk/bbc-radio-3-logo-png_seeklogo-314577.png" }
    @{ Name = "BBC Radio 4"; Country = "UK"; Genre = "News/Talk"; URL = "https://stream.live.vc.bbcmedia.co.uk/bbc_radio_fourfm"; Description = "News, drama and documentaries"; Logo = "radiologos/Uk/bbc-radio-4-logo-png_seeklogo-314491.png" }
    @{ Name = "BBC Radio 6 Music"; Country = "UK"; Genre = "Alternative"; URL = "https://stream.live.vc.bbcmedia.co.uk/bbc_6music"; Description = "Alternative music"; Logo = "radiologos/Uk/bbc-radio-6-music-logo-png_seeklogo-314590.png" }
    @{ Name = "BBC World Service"; Country = "UK"; Genre = "World News"; URL = "https://stream.live.vc.bbcmedia.co.uk/bbc_world_service"; Description = "International news and analysis"; Logo = "radiologos/Uk/BBC_World_Service.png" }
    @{ Name = "Heart UK"; Country = "UK"; Genre = "Pop"; URL = "https://media-ice.musicradio.com/HeartLondonMP3"; Description = "Feel good music from London"; Logo = "radiologos/Uk/heart.webp" }
    @{ Name = "Smooth Chill UK"; Country = "UK"; Genre = "Chill"; URL = "https://media-ice.musicradio.com/SmoothChillMP3"; Description = "Relaxing smooth chill vibes"; Logo = "radiologos/Uk/SMOOTH-CHILL.png" }
    
    
    # ========== EUROPE - FRANCE ==========
    @{ Name = "FIP Radio"; Country = "France"; Genre = "Eclectic"; URL = "https://direct.fipradio.fr/live/fip-midfi.mp3"; Description = "Eclectic French music discovery"; Logo = "radiologos/France/fip.webp" }
    @{ Name = "France Musique"; Country = "France"; Genre = "Classical"; URL = "https://direct.francemusique.fr/live/francemusique-midfi.mp3"; Description = "French classical music"; Logo = "radiologos/France/france-musique.webp" }
    @{ Name = "France Info"; Country = "France"; Genre = "News"; URL = "https://direct.franceinfo.fr/live/franceinfo-midfi.mp3"; Description = "24/7 French news"; Logo = "radiologos/France/france-info.png" }
    @{ Name = "France Bleu"; Country = "France"; Genre = "Local/Pop"; URL = "https://direct.francebleu.fr/live/fb1071-midfi.mp3"; Description = "French local radio"; Logo = "radiologos/France/france-bleu.webp" }
    @{ Name = "Europe 1"; Country = "France"; Genre = "News/Talk"; URL = "http://stream.europe1.fr/europe1.mp3"; Description = "French news and information"; Logo = "radiologos/France/europe.webp" }
    @{ Name = "RTL France"; Country = "France"; Genre = "News/Talk"; URL = "http://streaming.radio.rtl.fr/rtl-1-44-128"; Description = "French news and talk"; Logo = "radiologos/France/rtl.webp" }
    @{ Name = "RTL2 France"; Country = "France"; Genre = "Pop/Rock"; URL = "http://streaming.radio.rtl2.fr/rtl2-1-44-128"; Description = "French pop and rock"; Logo = "radiologos/France/rtl2.webp" }
    @{ Name = "Fun Radio"; Country = "France"; Genre = "Dance/Electronic"; URL = "http://streaming.radio.funradio.fr/fun-1-44-128"; Description = "French dance music"; Logo = "radiologos/France/fun.webp" }
    @{ Name = "Skyrock"; Country = "France"; Genre = "Hip-Hop/R&B"; URL = "https://icecast.skyrock.net/s/natio_mp3_128k"; Description = "French hip-hop and R&B"; Logo = "radiologos/France/skyrock.webp" }
    @{ Name = "TSF Jazz"; Country = "France"; Genre = "Jazz"; URL = "http://tsfjazz.ice.infomaniak.ch/tsfjazz-high.mp3"; Description = "French jazz radio"; Logo = "radiologos/France/jazz.webp" }
    @{ Name = "MFM Radio"; Country = "France"; Genre = "Pop"; URL = "http://mfm.ice.infomaniak.ch/mfm-128.mp3"; Description = "French pop hits"; Logo = "radiologos/France/mfm.webp" }
    @{ Name = "Generations FM"; Country = "France"; Genre = "Urban"; URL = "http://generationfm.ice.infomaniak.ch/generationfm-high.mp3"; Description = "French urban music"; Logo = "radiologos/France/generations.webp" }
    
    # ========== EUROPE - GERMANY ==========
    @{ Name = "1Live"; Country = "Germany"; Genre = "Pop/Rock"; URL = "https://wdr-1live-live.icecastssl.wdr.de/wdr/1live/live/mp3/128/stream.mp3"; Description = "German contemporary music"; Logo = "radiologos/Germany/eins-live.png" }
    @{ Name = "WDR 2"; Country = "Germany"; Genre = "Pop"; URL = "https://wdr-wdr2-ruhrgebiet.icecastssl.wdr.de/wdr/wdr2/ruhrgebiet/mp3/128/stream.mp3"; Description = "German popular radio"; Logo = "radiologos/Germany/wdr2.webp" }
    @{ Name = "WDR 5"; Country = "Germany"; Genre = "News/Culture"; URL = "https://wdr-wdr5-live.icecastssl.wdr.de/wdr/wdr5/live/mp3/128/stream.mp3"; Description = "German news and culture"; Logo = "radiologos/Germany/wdr5.png" }
    @{ Name = "Antenne Bayern"; Country = "Germany"; Genre = "Pop/Rock"; URL = "https://mp3channels.webradio.antenne.de/antenne"; Description = "Bavarian hit radio"; Logo = "radiologos/Germany/antenne.png" }
    @{ Name = "Radio BOB!"; Country = "Germany"; Genre = "Rock"; URL = "https://streams.radiobob.de/bob-national/mp3-192/streams.radiobob.de/"; Description = "German rock station"; Logo = "radiologos/Germany/bob.webp" }
    @{ Name = "FFH"; Country = "Germany"; Genre = "Pop"; URL = "https://mp3.ffh.de/radioffh/hqlivestream.mp3"; Description = "Hit Radio FFH"; Logo = "radiologos/Germany/ffh.webp" }
    @{ Name = "JAM FM"; Country = "Germany"; Genre = "R&B/Hip-Hop"; URL = "https://stream.jam.fm/jamfm-live/mp3-128"; Description = "Berlin urban music"; Logo = "radiologos/Germany/jam-fm.webp" }
    @{ Name = "Kiss FM Berlin"; Country = "Germany"; Genre = "Dance/Electronic"; URL = "https://stream.kissfm.de/kissfm/mp3-128"; Description = "Berlin dance music"; Logo = "radiologos/Germany/kiss-berlin.webp" }
    @{ Name = "Sunshine Live"; Country = "Germany"; Genre = "Dance/Electronic"; URL = "https://stream.sunshine-live.de/live/mp3-192"; Description = "Mannheim dance radio"; Logo = "radiologos/Germany/sunshine-mannheim.webp" }
    @{ Name = "SAW Magdeburg"; Country = "Germany"; Genre = "Pop/Rock"; URL = "https://stream.saw-musikwelt.de/saw/mp3-128"; Description = "Saxony-Anhalt hits"; Logo = "radiologos/Germany/saw-magdeburg.webp" }
    @{ Name = "Musik Club"; Country = "Germany"; Genre = "Dance"; URL = "https://streams.deltaradio.de/musik-club/mp3-192"; Description = "German dance club hits"; Logo = "radiologos/Germany/musik-club.webp" }
    @{ Name = "Blackbeats FM"; Country = "Germany"; Genre = "Urban"; URL = "https://stream.blackbeats.fm/live"; Description = "German urban station"; Logo = "radiologos/Germany/blackbeats.png" }
    
    # ========== EUROPE - NETHERLANDS ==========
    @{ Name = "NPO Radio 1"; Country = "Netherlands"; Genre = "News/Talk"; URL = "https://icecast.omroep.nl/radio1-bb-mp3"; Description = "Dutch news and talk"; Logo = "radiologos/Netherlands/npo-1.webp" }
    @{ Name = "NPO Radio 2"; Country = "Netherlands"; Genre = "Pop"; URL = "https://icecast.omroep.nl/radio2-bb-mp3"; Description = "Dutch popular music"; Logo = "radiologos/Netherlands/npo-2.webp" }
    @{ Name = "Radio 538"; Country = "Netherlands"; Genre = "Pop/Dance"; URL = "https://22723.live.streamtheworld.com/RADIO538.mp3"; Description = "Dutch hit music"; Logo = "radiologos/Netherlands/538.webp" }
    @{ Name = "100% NL"; Country = "Netherlands"; Genre = "Dutch Pop"; URL = "https://stream.100p.nl/100pctnl.mp3"; Description = "Only Dutch music"; Logo = "radiologos/Netherlands/100-nl.webp" }
    @{ Name = "Radio Veronica"; Country = "Netherlands"; Genre = "Rock/Pop"; URL = "https://25293.live.streamtheworld.com/VERONICA.mp3"; Description = "Dutch rock and pop"; Logo = "radiologos/Netherlands/veronica.webp" }
    @{ Name = "Sky Radio"; Country = "Netherlands"; Genre = "Love Songs"; URL = "https://25293.live.streamtheworld.com/SKYRADIO.mp3"; Description = "Dutch love songs"; Logo = "radiologos/Netherlands/sky-lovesongs.png" }
    @{ Name = "Radio NL"; Country = "Netherlands"; Genre = "Dutch Hits"; URL = "https://stream.radionl.fm/radionl"; Description = "Dutch hits station"; Logo = "radiologos/Netherlands/radionl.webp" }
    @{ Name = "Classic FM Netherlands"; Country = "Netherlands"; Genre = "Classical"; URL = "https://icecast.omroep.nl/radio4-bb-mp3"; Description = "Dutch classical music"; Logo = "radiologos/Netherlands/classic-fm.webp" }
    @{ Name = "Arrow Classic Rock"; Country = "Netherlands"; Genre = "Classic Rock"; URL = "https://stream.gal.io/arrow"; Description = "Dutch classic rock"; Logo = "radiologos/Netherlands/arrow-classic-rock.png" }
    @{ Name = "FunX"; Country = "Netherlands"; Genre = "Hip-Hop"; URL = "https://icecast.omroep.nl/funx-bb-mp3"; Description = "Dutch hip-hop station"; Logo = "radiologos/Netherlands/funx-hiphop.png" }
    
    # ========== EUROPE - ITALY ==========
    @{ Name = "RAI Radio 2"; Country = "Italy"; Genre = "Pop/Rock"; URL = "https://icestreaming.rai.it/2.mp3"; Description = "Italian contemporary music"; Logo = "radiologos/Italy/rai-2.webp" }
    @{ Name = "Radio Kiss Kiss"; Country = "Italy"; Genre = "Pop"; URL = "https://ice07.fluidstream.net/KissKiss.mp3"; Description = "Italian pop hits"; Logo = "radiologos/Italy/kiss-kiss.webp" }
    @{ Name = "Virgin Radio Italy"; Country = "Italy"; Genre = "Rock"; URL = "https://icy.unitedradio.it/Virgin.mp3"; Description = "Italian rock station"; Logo = "radiologos/Italy/virgin.webp" }
    @{ Name = "Radio Monte Carlo"; Country = "Italy"; Genre = "Pop/Rock"; URL = "https://icy.unitedradio.it/RMC.mp3"; Description = "Italian entertainment radio"; Logo = "radiologos/Italy/monte-carlo.webp" }
    @{ Name = "Radio 105"; Country = "Italy"; Genre = "Pop/Rock"; URL = "https://icy.unitedradio.it/Radio105.mp3"; Description = "Italian contemporary hits"; Logo = "radiologos/Italy/piu-brescia.webp" }
    
    # ========== EUROPE - BELGIUM ==========
    @{ Name = "VRT Radio 1"; Country = "Belgium"; Genre = "News/Talk"; URL = "http://icecast.vrtcdn.be/radio1-high.mp3"; Description = "Flemish news and talk"; Logo = "radiologos/Belgium/vrt-1.webp" }
    @{ Name = "Studio Brussel"; Country = "Belgium"; Genre = "Alternative"; URL = "http://icecast.vrtcdn.be/stubru-high.mp3"; Description = "Belgian alternative music"; Logo = "radiologos/Belgium/studio-brussel.webp" }
    @{ Name = "Klara Continuo"; Country = "Belgium"; Genre = "Classical"; URL = "http://icecast.vrtcdn.be/klara-high.mp3"; Description = "Flemish classical music"; Logo = "radiologos/Belgium/klara-continuo.webp" }
    @{ Name = "Classic 21"; Country = "Belgium"; Genre = "Rock"; URL = "https://radios.rtbf.be/classic21-128.mp3"; Description = "Belgian rock station"; Logo = "radiologos/Belgium/rtbf-classic-21.webp" }
    @{ Name = "Bel RTL"; Country = "Belgium"; Genre = "News/Talk"; URL = "https://belrtl.ice.infomaniak.ch/belrtl-mp3-128.mp3"; Description = "Belgian news and talk"; Logo = "radiologos/Belgium/bel-rtl.png" }
    @{ Name = "Joe FM"; Country = "Belgium"; Genre = "Pop/Rock"; URL = "https://playerservices.streamtheworld.com/api/livestream-redirect/JOE.mp3"; Description = "Belgian popular music"; Logo = "radiologos/Belgium/joefm.webp" }
    @{ Name = "NRJ Belgium"; Country = "Belgium"; Genre = "Dance/Pop"; URL = "https://playerservices.streamtheworld.com/api/livestream-redirect/NRJBELGIE.mp3"; Description = "Belgian dance and pop"; Logo = "radiologos/Belgium/nrj.webp" }
    
    # ========== EUROPE - IRELAND ==========
    @{ Name = "RTÉ 2FM"; Country = "Ireland"; Genre = "Pop/Rock"; URL = "https://icecast.rte.ie/2fm"; Description = "Irish contemporary music"; Logo = "radiologos/Ireland/rte-2fm.webp" }
    @{ Name = "RTÉ Gold"; Country = "Ireland"; Genre = "Oldies"; URL = "https://icecast.rte.ie/gold"; Description = "Irish classic hits"; Logo = "radiologos/Ireland/rte-gold.webp" }
    @{ Name = "Today FM"; Country = "Ireland"; Genre = "Music/Talk"; URL = "https://edge.audioxi.com/TDAAC"; Description = "Irish music and talk"; Logo = "radiologos/Ireland/today-fm.webp" }
    
    # ========== EUROPE - SWITZERLAND ==========
    @{ Name = "SRF 1"; Country = "Switzerland"; Genre = "Pop/News"; URL = "http://stream.srg-ssr.ch/m/rsp/mp3_128"; Description = "Swiss German radio"; Logo = "radiologos/Schweiz/srf-1-zurich-schaffhausen.png" }
    @{ Name = "SRF 2 Kultur"; Country = "Switzerland"; Genre = "Culture/Classical"; URL = "http://stream.srg-ssr.ch/m/rsc_de/mp3_128"; Description = "Swiss culture radio"; Logo = "radiologos/Schweiz/srf-2-kultur.png" }
    @{ Name = "SRF Musikwelle"; Country = "Switzerland"; Genre = "Folk/Oldies"; URL = "http://stream.srg-ssr.ch/m/regi_ag_so/mp3_128"; Description = "Swiss folk music"; Logo = "radiologos/Schweiz/srf-musikwelle.webp" }
    @{ Name = "Radio Swiss Jazz"; Country = "Switzerland"; Genre = "Jazz"; URL = "http://stream.srg-ssr.ch/m/rsj/mp3_128"; Description = "24/7 Swiss jazz"; Logo = "radiologos/Schweiz/swiss-pop.webp" }
    @{ Name = "Radio Swiss Classic"; Country = "Switzerland"; Genre = "Classical"; URL = "http://stream.srg-ssr.ch/m/rsc_de/mp3_128"; Description = "24/7 Swiss classical"; Logo = "radiologos/Schweiz/1.webp" }
    @{ Name = "Radio Swiss Pop"; Country = "Switzerland"; Genre = "Pop"; URL = "http://stream.srg-ssr.ch/m/rsp/mp3_128"; Description = "24/7 Swiss pop hits"; Logo = "radiologos/Schweiz/swiss-pop.webp" }
    @{ Name = "Energy Zürich"; Country = "Switzerland"; Genre = "Dance/Pop"; URL = "https://energyzuerich.ice.infomaniak.ch/energyzuerich-high.mp3"; Description = "Zurich dance hits"; Logo = "radiologos/Schweiz/energy-zurich.png" }
    @{ Name = "RTS Couleur 3"; Country = "Switzerland"; Genre = "Alternative"; URL = "http://stream.srg-ssr.ch/m/couleur3/mp3_128"; Description = "Swiss French alternative"; Logo = "radiologos/Schweiz/rts-couleur-3.webp" }
)

# ============================================================================
# INTERNET TV CHANNELS LOADER (from CSV config file + M3U playlists)
# ============================================================================

function Import-M3UPlaylist {
    <#
    .SYNOPSIS
        Parses an M3U/M3U8 playlist file and extracts TV channels
    .DESCRIPTION
        Reads M3U format files and converts them to channel objects.
        Supports EXTINF tags with tvg-id, tvg-logo, tvg-country, group-title attributes.
    .PARAMETER Path
        Path to the M3U file
    .PARAMETER DefaultCountry
        Default country to assign if not detected (default: "International")
    #>
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,
        
        [string]$DefaultCountry = "International"
    )
    
    $channels = @()
    
    if (-not (Test-Path $Path)) {
        Write-Host "[!] M3U file not found: $Path" -ForegroundColor Yellow
        return $channels
    }
    
    try {
        $lines = Get-Content $Path -Encoding UTF8
        $currentInfo = $null
        
        foreach ($line in $lines) {
            $line = $line.Trim()
            
            # Skip empty lines and M3U header
            if ([string]::IsNullOrWhiteSpace($line) -or $line -eq '#EXTM3U') {
                continue
            }
            
            # Parse EXTINF line
            if ($line.StartsWith('#EXTINF:')) {
                # Extract attributes and channel name
                # Format: #EXTINF:-1 tvg-id="..." tvg-logo="..." group-title="...",Channel Name
                
                $currentInfo = @{
                    Name = ""
                    Country = $DefaultCountry
                    Genre = "TV"
                    Logo = ""
                    Description = ""
                }
                
                # Extract channel name (after the last comma)
                if ($line -match ',([^,]+)$') {
                    $currentInfo.Name = $matches[1].Trim()
                    # Clean up resolution info from name for description
                    if ($currentInfo.Name -match '\((\d+p)\)') {
                        $currentInfo.Description = "Quality: $($matches[1])"
                    }
                }
                
                # Extract tvg-logo
                if ($line -match 'tvg-logo="([^"]*)"') {
                    $currentInfo.Logo = $matches[1]
                }
                
                # Extract group-title (often contains country or category)
                if ($line -match 'group-title="([^"]*)"') {
                    $groupTitle = $matches[1]
                    # Try to detect if it's a country or genre
                    $knownCountries = @('France', 'USA', 'UK', 'Germany', 'Spain', 'Italy', 'Netherlands', 'Belgium', 'Switzerland', 'Canada', 'Australia', 'Japan', 'China', 'India', 'Brazil', 'Mexico', 'Russia', 'Poland', 'Turkey', 'Arabic', 'Qatar', 'UAE')
                    
                    $matchedCountry = $knownCountries | Where-Object { $groupTitle -like "*$_*" } | Select-Object -First 1
                    if ($matchedCountry) {
                        $currentInfo.Country = $matchedCountry
                    } else {
                        # Use group-title as genre if not a country
                        $currentInfo.Genre = $groupTitle
                    }
                }
                
                # Extract tvg-country if present
                if ($line -match 'tvg-country="([^"]*)"') {
                    $currentInfo.Country = $matches[1]
                }
                
                # Try to detect country from tvg-id (e.g., "BFMTV.fr@SD" -> France)
                if ($line -match 'tvg-id="[^"]*\.([a-z]{2})[@"]' -and $currentInfo.Country -eq $DefaultCountry) {
                    $countryCode = $matches[1].ToUpper()
                    $countryMap = @{
                        'FR' = 'France'
                        'US' = 'USA'
                        'GB' = 'UK'
                        'UK' = 'UK'
                        'DE' = 'Germany'
                        'ES' = 'Spain'
                        'IT' = 'Italy'
                        'NL' = 'Netherlands'
                        'BE' = 'Belgium'
                        'CH' = 'Switzerland'
                        'CA' = 'Canada'
                        'AU' = 'Australia'
                        'JP' = 'Japan'
                        'CN' = 'China'
                        'IN' = 'India'
                        'BR' = 'Brazil'
                        'MX' = 'Mexico'
                        'RU' = 'Russia'
                        'PL' = 'Poland'
                        'TR' = 'Turkey'
                        'QA' = 'Qatar'
                        'AE' = 'UAE'
                    }
                    if ($countryMap.ContainsKey($countryCode)) {
                        $currentInfo.Country = $countryMap[$countryCode]
                    }
                }
                
                continue
            }
            
            # URL line (not starting with #)
            if (-not $line.StartsWith('#') -and $currentInfo) {
                # This is the stream URL
                if ($line -match '^https?://') {
                    $channel = @{
                        Name        = $currentInfo.Name
                        Country     = $currentInfo.Country
                        Genre       = $currentInfo.Genre
                        URL         = $line
                        Description = $currentInfo.Description
                        Logo        = $currentInfo.Logo
                        Source      = "M3U"
                    }
                    $channels += $channel
                }
                $currentInfo = $null
            }
        }
        
        Write-Host "[✓] Parsed $($channels.Count) channels from M3U: $(Split-Path -Leaf $Path)" -ForegroundColor Green
    }
    catch {
        Write-Host "[!] Error parsing M3U file: $_" -ForegroundColor Yellow
    }
    
    return $channels
}

function Get-TVChannelsFromConfig {
    <#
    .SYNOPSIS
        Loads TV channels from tvchannels.conf CSV file and M3U playlists
    .DESCRIPTION
        Reads the CSV configuration file and any M3U files in the tvplaylists folder.
        Returns an array of TV channel objects.
        Creates a default config file if none exists.
    #>
    
    $tvConfigPath = Join-Path $PSScriptRoot "tvchannels.conf"
    $tvPlaylistsFolder = Join-Path $PSScriptRoot "tvplaylists"
    
    # If config file doesn't exist, create default
    if (-not (Test-Path $tvConfigPath)) {
        Write-Host "[i] Creating default tvchannels.conf..." -ForegroundColor Yellow
        
        $defaultContent = @"
# NexusStack Internet TV Channels Configuration
# Format: Name,Country,Genre,URL,Description,Logo
# Maximum: 10 countries, 30 channels per country displayed per page
# Logo: Optional - can be local path (tvlogos/country/file.png) or URL
# Lines starting with # are comments
#
# TIP: You can also drop .m3u/.m3u8 playlist files into the 'tvplaylists' folder
#      and they will be automatically imported!
#
# ========== FRANCE ==========
France 24 French,France,News,https://live.france24.com/hls/live/2037179/F24_FR_HI_HLS/master_5000.m3u8,French international news,tvlogos/France/france24.png
France 24 English,France,News,https://live.france24.com/hls/live/2037218/F24_EN_HI_HLS/master_5000.m3u8,International news in English,tvlogos/France/france24.png
Euronews French,France,News,https://rakuten-euronews-2-fr.samsung.wurl.tv/manifest/playlist.m3u8,European news coverage,tvlogos/France/euronews.png
TV5Monde Info,France,News,https://ott.tv5monde.com/Content/HLS/Live/channel(info)/variant.m3u8,Francophone world news,tvlogos/France/tv5monde.png
#
# ========== USA ==========
ABC News Live,USA,News,https://content.uplynk.com/channel/3324f2467c414329b3b0cc5cd987b6be.m3u8,ABC News 24/7 coverage,tvlogos/USA/abc.png
CBS News,USA,News,https://cbsnews.akamaized.net/hls/live/2020607/cbsnlineup_8/master.m3u8,CBS News live stream,tvlogos/USA/cbs.png
NBC News NOW,USA,News,https://nbcnews.akamaized.net/hls/live/2028003/nbcnews/master.m3u8,NBC News streaming,tvlogos/USA/nbc.png
Bloomberg TV,USA,Business,https://www.bloomberg.com/media-manifest/streams/us.m3u8,Business and financial news,tvlogos/USA/bloomberg.png
Newsy,USA,News,https://content.uplynk.com/channel/5fc43140a4fc4e2087b4f9db70dbb5bd.m3u8,Independent news coverage,tvlogos/USA/newsy.png
#
# ========== UK ==========
Sky News,UK,News,https://linear418-gb-hls1-prd-ak.cdn.skycdp.com/100e/Content/HLS_001_1080_30/Live/channel(skynews)/index.m3u8,UK and world news,tvlogos/UK/skynews.png
GB News,UK,News,https://live-manifest.tubi.io/live/gbnews/playlist.m3u8,British news channel,tvlogos/UK/gbnews.png
#
# ========== GERMANY ==========
DW News,Germany,News,https://dwamdstream104.akamaized.net/hls/live/2015530/dwstream104/index.m3u8,German international news,tvlogos/Germany/dw.png
DW Deutsch,Germany,News,https://dwamdstream102.akamaized.net/hls/live/2015525/dwstream102/index.m3u8,Deutsche Welle German,tvlogos/Germany/dw.png
Tagesschau24,Germany,News,https://tagesschau.akamaized.net/hls/live/2020115/tagesschau/tagesschau_1/master.m3u8,German daily news,tvlogos/Germany/tagesschau.png
#
# ========== SPAIN ==========
RTVE 24h,Spain,News,https://rtvelivestream.akamaized.net/24h_dvr/24h_dvr_720.m3u8,Spanish 24h news,tvlogos/Spain/rtve.png
Euronews Spanish,Spain,News,https://rakuten-euronews-2-es.samsung.wurl.tv/manifest/playlist.m3u8,European news in Spanish,tvlogos/Spain/euronews.png
#
# ========== ITALY ==========
Rai News 24,Italy,News,https://mediapolis.rai.it/relinker/relinkerServlet.htm?cont=1,Italian 24h news,tvlogos/Italy/rainews.png
#
# ========== MIDDLE EAST ==========
Al Jazeera English,Qatar,News,https://live-hls-web-aje.getaj.net/AJE/index.m3u8,International news from Qatar,tvlogos/MiddleEast/aljazeera.png
Al Jazeera Arabic,Qatar,News,https://live-hls-web-aja.getaj.net/AJA/index.m3u8,Arabic news channel,tvlogos/MiddleEast/aljazeera.png
#
# ========== ASIA ==========
CGTN,China,News,https://live.cgtn.com/1000/prog_index.m3u8,Chinese global television,tvlogos/Asia/cgtn.png
NHK World Japan,Japan,News,https://nhkworld.webcdn.stream.ne.jp/www11/nhkworld-tv/domestic/263942/live.m3u8,Japanese international news,tvlogos/Asia/nhk.png
Arirang TV,South Korea,News,https://amdlive-ch01-ctnd-com.akamaized.net/arirang_1ch/smil:arirang_1ch.smil/playlist.m3u8,Korean international channel,tvlogos/Asia/arirang.png
#
# ========== INTERNATIONAL ==========
Euronews English,International,News,https://rakuten-euronews-2-gb.samsung.wurl.tv/manifest/playlist.m3u8,European news English,tvlogos/International/euronews.png
TRT World,International,News,https://tv-trtworld.medya.trt.com.tr/master.m3u8,Turkish international news,tvlogos/International/trt.png
"@
        
        $defaultContent | Out-File -FilePath $tvConfigPath -Encoding UTF8
        Write-Host "[✓] Default tvchannels.conf created with legal free channels" -ForegroundColor Green
    }
    
    # Create tvplaylists folder if it doesn't exist
    if (-not (Test-Path $tvPlaylistsFolder)) {
        New-Item -ItemType Directory -Path $tvPlaylistsFolder -Force | Out-Null
        Write-Host "[i] Created tvplaylists folder - drop .m3u files here to import channels" -ForegroundColor Cyan
    }
    
    # Read and parse the CSV file
    $tvChannels = @()
    
    try {
        $lines = Get-Content $tvConfigPath -Encoding UTF8
        
        foreach ($line in $lines) {
            # Skip comments and empty lines
            if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) {
                continue
            }
            
            # Parse CSV line (Name,Country,Genre,URL,Description,Logo)
            $parts = $line.Split(',')
            
            if ($parts.Count -ge 5) {
                $channel = @{
                    Name        = $parts[0].Trim()
                    Country     = $parts[1].Trim()
                    Genre       = $parts[2].Trim()
                    URL         = $parts[3].Trim()
                    Description = $parts[4].Trim()
                    Logo        = if ($parts.Count -ge 6) { $parts[5].Trim() } else { "" }
                    Source      = "CSV"
                }
                $tvChannels += $channel
            }
        }
        
        Write-Host "[✓] Loaded $($tvChannels.Count) TV channels from tvchannels.conf" -ForegroundColor Green
    }
    catch {
        Write-Host "[!] Error reading tvchannels.conf: $_" -ForegroundColor Yellow
    }
    
    # Load M3U playlists from tvplaylists folder
    $m3uFiles = Get-ChildItem -Path $tvPlaylistsFolder -Filter "*.m3u*" -ErrorAction SilentlyContinue
    
    foreach ($m3uFile in $m3uFiles) {
        Write-Host "[i] Importing M3U playlist: $($m3uFile.Name)..." -ForegroundColor Cyan
        
        # Try to detect country from filename (e.g., "fr.m3u" -> France)
        $defaultCountry = "International"
        $fileBaseName = [System.IO.Path]::GetFileNameWithoutExtension($m3uFile.Name).ToLower()
        
        $filenameCountryMap = @{
            'fr' = 'France'
            'france' = 'France'
            'us' = 'USA'
            'usa' = 'USA'
            'uk' = 'UK'
            'gb' = 'UK'
            'de' = 'Germany'
            'germany' = 'Germany'
            'es' = 'Spain'
            'spain' = 'Spain'
            'it' = 'Italy'
            'italy' = 'Italy'
            'nl' = 'Netherlands'
            'be' = 'Belgium'
            'ch' = 'Switzerland'
            'ca' = 'Canada'
            'au' = 'Australia'
            'jp' = 'Japan'
            'cn' = 'China'
            'in' = 'India'
            'br' = 'Brazil'
            'mx' = 'Mexico'
            'ru' = 'Russia'
            'pl' = 'Poland'
            'tr' = 'Turkey'
        }
        
        foreach ($key in $filenameCountryMap.Keys) {
            if ($fileBaseName -like "*$key*" -or $fileBaseName -like "*_$key*" -or $fileBaseName -like "*-$key*") {
                $defaultCountry = $filenameCountryMap[$key]
                break
            }
        }
        
        $m3uChannels = Import-M3UPlaylist -Path $m3uFile.FullName -DefaultCountry $defaultCountry
        $tvChannels += $m3uChannels
    }
    
    # Remove duplicates based on URL
    $uniqueChannels = @()
    $seenUrls = @{}
    
    foreach ($channel in $tvChannels) {
        if (-not $seenUrls.ContainsKey($channel.URL)) {
            $seenUrls[$channel.URL] = $true
            $uniqueChannels += $channel
        }
    }
    
    if ($tvChannels.Count -ne $uniqueChannels.Count) {
        Write-Host "[i] Removed $($tvChannels.Count - $uniqueChannels.Count) duplicate channels" -ForegroundColor Gray
    }
    
    Write-Host "[✓] Total TV channels available: $($uniqueChannels.Count)" -ForegroundColor Green
    
    return $uniqueChannels
}

# Load TV channels
$Global:TVChannels = Get-TVChannelsFromConfig

# Build CONFIG based on whether config file was loaded
"[DEBUG] About to build CONFIG. LoadedConfig is null: $($null -eq $LoadedConfig)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append

if ($LoadedConfig) {
    "*** USING LOADED CONFIG FROM FILE ***" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    
    # Debug: Check what's in LoadedConfig before transferring
    "[DEBUG] LoadedConfig.ContainsKey('GhostscriptInstalled'): $($LoadedConfig.ContainsKey('GhostscriptInstalled'))" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    if ($LoadedConfig.ContainsKey('GhostscriptInstalled')) {
        "[DEBUG] LoadedConfig.GhostscriptInstalled value: $($LoadedConfig.GhostscriptInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
        "[DEBUG] LoadedConfig.GhostscriptInstalled type: $($LoadedConfig.GhostscriptInstalled.GetType().Name)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    }
    
    # Use loaded configuration from file
    $CONFIG = @{
        # Media Folders (from config file)
        PicturesFolder = $LoadedConfig.PicturesFolder
        MoviesFolder   = $LoadedConfig.MoviesFolder
        MusicFolder    = $LoadedConfig.MusicFolder
        PDFFolder      = $LoadedConfig.PDFFolder
        
        # Server Settings (from config file)
        ServerPort     = $LoadedConfig.ServerPort
        ServerHost     = $LoadedConfig.ServerHost
        
        # IP Whitelist (from config file)
        IPWhitelist    = $LoadedConfig.IPWhitelist
        
        # Security Settings (from config file)
        SecurityByPin  = if ($LoadedConfig.ContainsKey('SecurityByPin')) { $LoadedConfig.SecurityByPin } else { $true }
        
        # Metadata API Keys (from config file)
        TMDB_APIKey    = $LoadedConfig.TMDB_APIKey
        LastFM_APIKey  = $LoadedConfig.LastFM_APIKey
        
        # Media Quality Thresholds (from config file)
        LowQualityVideoThreshold = $LoadedConfig.LowQualityVideoThreshold
        LowQualityAudioThreshold = $LoadedConfig.LowQualityAudioThreshold
        
        # File Extensions (from config file)
        VideoExtensions = $LoadedConfig.VideoExtensions
        AudioExtensions = $LoadedConfig.AudioExtensions
        ImageExtensions = $LoadedConfig.ImageExtensions
        PDFExtensions   = $LoadedConfig.PDFExtensions
        
        # Manual installation flags (from config file)
        GhostscriptInstalled = if ($LoadedConfig.ContainsKey('GhostscriptInstalled')) { $LoadedConfig.GhostscriptInstalled } else { $false }
        FFmpegInstalled = if ($LoadedConfig.ContainsKey('FFmpegInstalled')) { $LoadedConfig.FFmpegInstalled } else { $false }
        
        # Database Files (always relative to script directory)
        MoviesDB       = "$PSScriptRoot\movies.db"
        MusicDB        = "$PSScriptRoot\music.db"
        PicturesDB     = "$PSScriptRoot\pictures.db"
        PDFDB          = "$PSScriptRoot\pdfs.db"
        
        # User Management Databases
        UsersDB        = "$PSScriptRoot\users\users.db"
        UsersDBPath    = "$PSScriptRoot\users"
        AvatarFolder   = "$PSScriptRoot\avatars"
        
        # Poster Cache (always relative to script directory)
        PosterCacheFolder = "$PSScriptRoot\posters"
        AlbumArtCacheFolder = "$PSScriptRoot\albumart"
        PDFPreviewCacheFolder = "$PSScriptRoot\pdfpreviews"
        EPUBCoverCacheFolder = "$PSScriptRoot\epubcovers"
        RadioLogoCacheFolder = "$PSScriptRoot\radiologos"
        
        # Tools Folder (always relative to script directory)
        ToolsFolder = "$PSScriptRoot\tools"
        
        # Internet Radio Stations (hardcoded - not in config file)
        RadioStations = $RadioStationsArray
        
        # Internet TV Channels (loaded from tvchannels.conf CSV file)
        TVChannels = $Global:TVChannels
        TVLogoCacheFolder = "$PSScriptRoot\tvlogos"
    }
    
    Write-Host "[✓] Configuration loaded from PSMediaLib.conf" -ForegroundColor Green
    
    "[✓] CONFIG object created from loaded config" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    "[DEBUG] CONFIG.GhostscriptInstalled = $($CONFIG.GhostscriptInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    "[DEBUG] CONFIG.GhostscriptInstalled type = $($CONFIG.GhostscriptInstalled.GetType().Name)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    "[DEBUG] CONFIG.FFmpegInstalled = $($CONFIG.FFmpegInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    "[DEBUG] CONFIG has GhostscriptInstalled key: $($CONFIG.ContainsKey('GhostscriptInstalled'))" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    
    # Apply transcoding settings from config file (v4.9)
    if ($LoadedConfig.ContainsKey('PreferredEncoder')) {
        $Global:TranscodingConfig.PreferredEncoder = $LoadedConfig.PreferredEncoder
    }
    
    # SAFETY: Force software encoding if config was "auto" or hardware encoder
    # This prevents NVENC/AMF/QSV failures on systems without proper drivers
    if ($Global:TranscodingConfig.PreferredEncoder -ne "software") {
        Write-Host "[!] WARNING: Config has hardware encoding enabled ($($Global:TranscodingConfig.PreferredEncoder))" -ForegroundColor Yellow
        Write-Host "[!] Forcing software encoding for stability. Edit PSMediaLib.conf to change." -ForegroundColor Yellow
        $Global:TranscodingConfig.PreferredEncoder = "software"
    }
    if ($LoadedConfig.ContainsKey('VideoPreset')) {
        $Global:TranscodingConfig.VideoPreset = $LoadedConfig.VideoPreset
    }
    if ($LoadedConfig.ContainsKey('VideoCRF')) {
        $Global:TranscodingConfig.VideoCRF = $LoadedConfig.VideoCRF
    }
    if ($LoadedConfig.ContainsKey('VideoMaxrate')) {
        $Global:TranscodingConfig.VideoMaxrate = $LoadedConfig.VideoMaxrate
    }
    if ($LoadedConfig.ContainsKey('AudioBitrate')) {
        $Global:TranscodingConfig.AudioBitrate = $LoadedConfig.AudioBitrate
    }
    if ($LoadedConfig.ContainsKey('MaxConcurrentTranscodes')) {
        $Global:TranscodingConfig.MaxConcurrentTranscodes = $LoadedConfig.MaxConcurrentTranscodes
    }
    if ($LoadedConfig.ContainsKey('TranscodingEnabled') -and -not $LoadedConfig.TranscodingEnabled) {
        Write-Host "[i] Transcoding disabled in PSMediaLib.conf" -ForegroundColor Yellow
    }
    
}
else {
    
    # Use hardcoded defaults (first run or error loading config)
    $CONFIG = @{
        # Media Folders
        PicturesFolder = "D:\Share"
        MoviesFolder   = "D:\Share"
        MusicFolder    = "D:\Share"
        PDFFolder      = "D:\Share"
        
        # Server Settings
        ServerPort     = 8182
        ServerHost     = "+"
        
        # IP Whitelist - Only these IPs can access the server
        # Use @() for empty array to allow all IPs
        IPWhitelist    = @(
            "127.0.0.1",           # Localhost IPv4
            "::1",                 # Localhost IPv6
            "192.168.0.163",       # Example: Your computer
            "192.168.1.101"        # Example: Another trusted device
        )
        
        # Security Settings
        SecurityByPin  = $true  # Require PIN authentication (set to $false to auto-login as admin)
        
        # Database Files (relative to script directory)
        MoviesDB       = "$PSScriptRoot\movies.db"
        MusicDB        = "$PSScriptRoot\music.db"
        PicturesDB     = "$PSScriptRoot\pictures.db"
        PDFDB          = "$PSScriptRoot\pdfs.db"
        
        # User Management Databases
        UsersDB        = "$PSScriptRoot\users\users.db"
        UsersDBPath    = "$PSScriptRoot\users"
        
        # Poster Cache (relative to script directory)
        PosterCacheFolder = "$PSScriptRoot\posters"
        AlbumArtCacheFolder = "$PSScriptRoot\albumart"
        PDFPreviewCacheFolder = "$PSScriptRoot\pdfpreviews"
        RadioLogoCacheFolder = "$PSScriptRoot\radiologos"
        
        # Tools Folder (for downloaded utilities like ffmpeg)
        ToolsFolder = "$PSScriptRoot\tools"
        
        # Metadata API Keys (Optional - add your own API keys)
        TMDB_APIKey = "66a84d3b5d34cd3dfa65cf4b0469eea3"  # For movie posters
        LastFM_APIKey  = ""  # For music album art
        
        # Media Quality Thresholds
        LowQualityVideoThreshold = 720  # Resolution height (720p)
        LowQualityAudioThreshold = 128  # Bitrate in kbps
        
        # File Extensions
        VideoExtensions = @('.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm', '.m4v')
        AudioExtensions = @('.mp3', '.flac', '.wav', '.m4a', '.aac', '.ogg', '.wma', '.opus')
        ImageExtensions = @('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff')
        PDFExtensions = @('.pdf')
        
        # Manual installation flags (not installed by default)
        GhostscriptInstalled = $false
        FFmpegInstalled = $false
        
        # Internet Radio Stations
        RadioStations = $RadioStationsArray
        
        # Internet TV Channels (loaded from tvchannels.conf CSV file)
        TVChannels = $Global:TVChannels
        TVLogoCacheFolder = "$PSScriptRoot\tvlogos"
    }
    
    Write-Host "[i] Using default configuration" -ForegroundColor Cyan
}

# ============================================================================
# GLOBAL VARIABLES
# ============================================================================

$Global:ServerRunning = $false
$Global:FFmpegPath = $null  # NEW: Path to ffmpeg executable
$Global:MediaStats = @{
    TotalVideos = 0
    TotalMusic = 0
    TotalPictures = 0
    TotalPDFs = 0
    TotalSizeGB = 0
    VideoSizeGB = 0
    MusicSizeGB = 0
    PicturesSizeGB = 0
    PDFSizeGB = 0
    LastScanDate = $null
}

# ============================================================================
# MODULE DEPENDENCY CHECK
# ============================================================================

function Test-PSSQLiteModule {
    Write-Host "`n=== Checking Dependencies ===" -ForegroundColor Cyan
    
    if (-not (Get-Module -ListAvailable -Name PSSQLite)) {
        Write-Host "`n[!] PSSQLite module is not installed." -ForegroundColor Yellow
        Write-Host "    This module is required for database operations." -ForegroundColor Gray
        
        $install = Read-Host "`nWould you like to install it now? (Y/N)"
        
        if ($install -eq 'Y' -or $install -eq 'y') {
            try {
                Write-Host "`nInstalling PSSQLite module..." -ForegroundColor Cyan
                Install-Module -Name PSSQLite -Scope CurrentUser -Force -AllowClobber
                Write-Host "[✓] PSSQLite installed successfully!" -ForegroundColor Green
                Import-Module PSSQLite
                return $true
            }
            catch {
                Write-Host "[✗] Failed to install PSSQLite: $_" -ForegroundColor Red
                return $false
            }
        }
        else {
            Write-Host "`n[✗] Cannot continue without PSSQLite module." -ForegroundColor Red
            Write-Host "    Install manually with: Install-Module PSSQLite -Scope CurrentUser" -ForegroundColor Gray
            return $false
        }
    }
    else {
        Import-Module PSSQLite -ErrorAction SilentlyContinue
        Write-Host "[✓] PSSQLite module is available" -ForegroundColor Green
        return $true
    }
}

function Test-PodeModule {
    Write-Host "`n=== Checking Pode Module ===" -ForegroundColor Cyan
    
    if (-not (Get-Module -ListAvailable -Name Pode)) {
        Write-Host "`n[!] Pode module is not installed." -ForegroundColor Yellow
        Write-Host "    Pode is required for the web server with HTTPS support." -ForegroundColor Gray
        Write-Host "    More info: https://badgerati.github.io/Pode/" -ForegroundColor Gray
        
        $install = Read-Host "`nWould you like to install it now? (Y/N)"
        
        if ($install -eq 'Y' -or $install -eq 'y') {
            try {
                Write-Host "`nInstalling Pode module..." -ForegroundColor Cyan
                Install-Module -Name Pode -Scope CurrentUser -Force -AllowClobber
                Write-Host "[✓] Pode installed successfully!" -ForegroundColor Green
                Import-Module Pode
                return $true
            }
            catch {
                Write-Host "[✗] Failed to install Pode: $_" -ForegroundColor Red
                return $false
            }
        }
        else {
            Write-Host "`n[✗] Cannot continue without Pode module." -ForegroundColor Red
            Write-Host "    Install manually with: Install-Module Pode -Scope CurrentUser" -ForegroundColor Gray
            return $false
        }
    }
    else {
        $podeVersion = (Get-Module -ListAvailable -Name Pode | Select-Object -First 1).Version
        Import-Module Pode -ErrorAction SilentlyContinue
        Write-Host "[✓] Pode module is available (v$podeVersion)" -ForegroundColor Green
        return $true
    }
}

# ============================================================================
# FFMPEG DEPENDENCY CHECK (NEW - FOR ALBUM ARTWORK)
# ============================================================================

function Test-FFmpegAvailability {
    Write-Host "`n=== Checking FFmpeg (for Album Artwork) ===" -ForegroundColor Cyan
    
    # First check if user has marked it as installed in config file
    if ($CONFIG.ContainsKey('FFmpegInstalled') -and $CONFIG.FFmpegInstalled -eq $true) {
        Write-Host "[✓] FFmpeg marked as installed in PSMediaLib.conf" -ForegroundColor Green
        Write-Host "    Skipping automatic detection as per configuration file" -ForegroundColor Gray
        
        # Try to find it anyway for the global path
        $ffmpegInPath = Get-Command ffmpeg -ErrorAction SilentlyContinue
        if ($ffmpegInPath) {
            $Global:FFmpegPath = $ffmpegInPath.Source
        } else {
            $localFFmpegExe = Join-Path $CONFIG.ToolsFolder "ffmpeg\bin\ffmpeg.exe"
            if (Test-Path $localFFmpegExe) {
                $Global:FFmpegPath = $localFFmpegExe
            } else {
                # Assume it's installed but we can't find it - user will need to add to PATH
                $Global:FFmpegPath = "ffmpeg"  # Will use system PATH
            }
        }
        return $true
    }
    
    # Check if ffmpeg is in PATH
    $ffmpegInPath = Get-Command ffmpeg -ErrorAction SilentlyContinue
    if ($ffmpegInPath) {
        Write-Host "[✓] ffmpeg found in system PATH" -ForegroundColor Green
        $Global:FFmpegPath = $ffmpegInPath.Source
        return $true
    }
    
    # Check if we have it in local tools folder
    $localFFmpegExe = Join-Path $CONFIG.ToolsFolder "ffmpeg\bin\ffmpeg.exe"
    if (Test-Path $localFFmpegExe) {
        Write-Host "[✓] ffmpeg found in local tools folder" -ForegroundColor Green
        $Global:FFmpegPath = $localFFmpegExe
        return $true
    }
    
    # ffmpeg not found
    Write-Host "`n[!] ffmpeg is not installed (or not in PATH)." -ForegroundColor Yellow
    Write-Host "    ffmpeg is OPTIONAL but required for album artwork extraction." -ForegroundColor Gray
    Write-Host "    Without it, music library works but won't show album art." -ForegroundColor Gray
    Write-Host "    Size: ~70 MB download" -ForegroundColor Gray
    Write-Host "`n    If you have installed FFmpeg manually:" -ForegroundColor Cyan
    Write-Host "    Edit PSMediaLib.conf and set: FFmpegInstalled,Yes" -ForegroundColor Cyan
    
    $install = Read-Host "`nWould you like to download ffmpeg now? (Y/N)"
    
    if ($install -eq 'Y' -or $install -eq 'y') {
        return Install-FFmpeg
    }
    else {
        Write-Host "`n[i] Continuing without ffmpeg. Album artwork will be disabled." -ForegroundColor Gray
        Write-Host "    You can download it later from: https://ffmpeg.org/download.html" -ForegroundColor Gray
        return $false
    }
}

function Install-FFmpeg {
    Write-Host "`n=== Downloading FFmpeg ===" -ForegroundColor Cyan
    
    try {
        # Create tools folder
        $ffmpegFolder = Join-Path $CONFIG.ToolsFolder "ffmpeg"
        if (-not (Test-Path $ffmpegFolder)) {
            New-Item -ItemType Directory -Path $ffmpegFolder -Force | Out-Null
        }
        
        # Detect OS - Use runtime check instead of assignment
        $onWindows = ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows) -or ($PSVersionTable.PSVersion.Major -lt 6)
        $onLinux = $PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux
        $onMacOS = $PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS
        
        if ($onWindows) {
            Write-Host "Detected Windows OS" -ForegroundColor Cyan
            
            # Download ffmpeg essentials build for Windows
            $downloadUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
            $zipPath = Join-Path $env:TEMP "ffmpeg.zip"
            
            Write-Host "Downloading from: $downloadUrl" -ForegroundColor Gray
            Write-Host "This may take a few minutes (~70 MB)..." -ForegroundColor Gray
            
            # Download with progress
            $ProgressPreference = 'SilentlyContinue'  # Faster download
            Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing
            $ProgressPreference = 'Continue'
            
            Write-Host "[✓] Download complete!" -ForegroundColor Green
            
            # Extract
            Write-Host "Extracting ffmpeg..." -ForegroundColor Cyan
            Expand-Archive -Path $zipPath -DestinationPath $env:TEMP -Force
            
            # Find the extracted folder (it has a version number in the name)
            $extractedFolder = Get-ChildItem -Path $env:TEMP -Directory | 
                Where-Object { $_.Name -like "ffmpeg-*-essentials_build" } | 
                Select-Object -First 1
            
            if ($extractedFolder) {
                # Copy to our tools folder
                Copy-Item -Path "$($extractedFolder.FullName)\*" -Destination $ffmpegFolder -Recurse -Force
                
                # Clean up
                Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
                Remove-Item $extractedFolder.FullName -Recurse -Force -ErrorAction SilentlyContinue
                
                # Verify installation
                $ffmpegExe = Join-Path $ffmpegFolder "bin\ffmpeg.exe"
                if (Test-Path $ffmpegExe) {
                    $Global:FFmpegPath = $ffmpegExe
                    Write-Host "[✓] ffmpeg installed successfully!" -ForegroundColor Green
                    Write-Host "    Location: $ffmpegExe" -ForegroundColor Gray
                    return $true
                }
            }
            
            Write-Host "[✗] Failed to extract ffmpeg" -ForegroundColor Red
            return $false
        }
        elseif ($onLinux) {
            Write-Host "Detected Linux OS" -ForegroundColor Cyan
            Write-Host "[!] Please install ffmpeg using your package manager:" -ForegroundColor Yellow
            Write-Host "    Ubuntu/Debian: sudo apt-get install ffmpeg" -ForegroundColor Gray
            Write-Host "    Fedora: sudo dnf install ffmpeg" -ForegroundColor Gray
            Write-Host "    Arch: sudo pacman -S ffmpeg" -ForegroundColor Gray
            return $false
        }
        elseif ($onMacOS) {
            Write-Host "Detected macOS" -ForegroundColor Cyan
            Write-Host "[!] Please install ffmpeg using Homebrew:" -ForegroundColor Yellow
            Write-Host "    brew install ffmpeg" -ForegroundColor Gray
            return $false
        }
        else {
            Write-Host "[!] Could not detect OS. Please install ffmpeg manually." -ForegroundColor Yellow
            Write-Host "    Download from: https://ffmpeg.org/download.html" -ForegroundColor Gray
            return $false
        }
    }
    catch {
        Write-Host "[✗] Failed to download/install ffmpeg: $_" -ForegroundColor Red
        Write-Host "    You can download it manually from: https://ffmpeg.org/download.html" -ForegroundColor Gray
        return $false
    }
}

# ============================================================================
# GHOSTSCRIPT DEPENDENCY CHECK (FOR PDF PREVIEW GENERATION)
# ============================================================================

function Test-GhostscriptAvailability {
    Write-Host "`n=== Checking Ghostscript (for PDF Preview Generation) ===" -ForegroundColor Cyan
    
    "[Test-GhostscriptAvailability] Function called" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    
    # Debug: Show what we have in CONFIG
    if ($CONFIG.PSObject.Properties.Name -contains 'GhostscriptInstalled') {
        "[DEBUG] CONFIG has GhostscriptInstalled via PSObject: $($CONFIG.GhostscriptInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    } else {
        "[DEBUG] CONFIG does NOT have GhostscriptInstalled via PSObject" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    }
    
    # Also check with ContainsKey (for hashtables)
    if ($CONFIG.ContainsKey('GhostscriptInstalled')) {
        "[DEBUG] CONFIG.ContainsKey('GhostscriptInstalled') = True" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
        "[DEBUG] CONFIG.GhostscriptInstalled = $($CONFIG.GhostscriptInstalled)" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    } else {
        "[DEBUG] CONFIG.ContainsKey('GhostscriptInstalled') = False" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
    }
    
    # First check if user has marked it as installed in config file
    if ($CONFIG.ContainsKey('GhostscriptInstalled') -and $CONFIG.GhostscriptInstalled -eq $true) {
        Write-Host "[✓] Ghostscript marked as installed in PSMediaLib.conf" -ForegroundColor Green
        Write-Host "    Skipping automatic detection as per configuration file" -ForegroundColor Gray
        "[✓] Ghostscript marked as installed - skipping detection" | Out-File -FilePath "$PSScriptRoot\debug.log" -Append
        return $true
    }
    
    # Check if Ghostscript is in PATH
    $gsCmd = Get-Command gs -ErrorAction SilentlyContinue
    if (-not $gsCmd) {
        $gsCmd = Get-Command gswin64c -ErrorAction SilentlyContinue
    }
    if (-not $gsCmd) {
        $gsCmd = Get-Command gswin32c -ErrorAction SilentlyContinue
    }
    
    if ($gsCmd) {
        Write-Host "[✓] Ghostscript found in system PATH" -ForegroundColor Green
        try {
            $version = & $gsCmd.Source --version 2>&1
            Write-Host "    Version: Ghostscript $version" -ForegroundColor Gray
        } catch {
            Write-Host "    Version: Ghostscript (installed)" -ForegroundColor Gray
        }
        return $true
    }
    
    # Ghostscript not found
    Write-Host "`n[!] Ghostscript is not installed (or not in PATH)." -ForegroundColor Yellow
    Write-Host "    Ghostscript is OPTIONAL but required for PDF preview thumbnails." -ForegroundColor Gray
    Write-Host "    Without it, PDFs will display with placeholder icons." -ForegroundColor Gray
    Write-Host "    An interpreter for the PostScript language and for PDF." -ForegroundColor Gray
    Write-Host "`n    If you have installed Ghostscript manually:" -ForegroundColor Cyan
    Write-Host "    Edit PSMediaLib.conf and set: GhostscriptInstalled,Yes" -ForegroundColor Cyan
    
    $install = Read-Host "`nWould you like to install Ghostscript now? (Y/N)"
    
    if ($install -eq 'Y' -or $install -eq 'y') {
        return Install-Ghostscript
    }
    else {
        Write-Host "`n[i] Continuing without Ghostscript. PDF thumbnails will be disabled." -ForegroundColor Gray
        Write-Host "    You can install it later with: winget install Ghostscript.Ghostscript" -ForegroundColor Gray
        return $false
    }
}

function Install-Ghostscript {
    Write-Host "`n=== Installing Ghostscript ===" -ForegroundColor Cyan
    
    try {
        # Detect OS
        $onWindows = ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows) -or ($PSVersionTable.PSVersion.Major -lt 6)
        $onLinux = $PSVersionTable.PSVersion.Major -ge 6 -and $IsLinux
        $onMacOS = $PSVersionTable.PSVersion.Major -ge 6 -and $IsMacOS
        
        if ($onWindows) {
            Write-Host "Detected Windows OS" -ForegroundColor Cyan
            
            # Try winget first (recommended)
            $wingetAvailable = Get-Command winget -ErrorAction SilentlyContinue
            
            if ($wingetAvailable) {
                Write-Host "`nUsing Windows Package Manager (winget)..." -ForegroundColor Cyan
                Write-Host "Running: winget install Ghostscript.Ghostscript" -ForegroundColor Gray
                Write-Host ""
                
                try {
                    $wingetResult = winget install Ghostscript.Ghostscript --accept-package-agreements --accept-source-agreements 2>&1
                    
                    if ($LASTEXITCODE -eq 0 -or $wingetResult -match "Successfully installed") {
                        Write-Host "`n[✓] Ghostscript installed successfully via winget!" -ForegroundColor Green
                        Write-Host "    You may need to restart PowerShell for PATH changes to take effect." -ForegroundColor Yellow
                        Write-Host "    PDF previews will work after restart." -ForegroundColor Green
                        
                        # Try to refresh PATH in current session
                        $env:PATH = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
                        
                        # Check if gs command is now available
                        $gsTest = Get-Command gs -ErrorAction SilentlyContinue
                        if (-not $gsTest) { $gsTest = Get-Command gswin64c -ErrorAction SilentlyContinue }
                        if (-not $gsTest) { $gsTest = Get-Command gswin32c -ErrorAction SilentlyContinue }
                        
                        if ($gsTest) {
                            Write-Host "    ✓ Ghostscript command is now available!" -ForegroundColor Green
                        }
                        else {
                            Write-Host "    Note: Please restart PowerShell to use Ghostscript" -ForegroundColor Yellow
                        }
                        
                        return $true
                    }
                    else {
                        Write-Host "[!] winget installation may have failed" -ForegroundColor Yellow
                        Write-Host "    Please install manually from: https://www.ghostscript.com/releases/gsdnld.html" -ForegroundColor Gray
                        return $false
                    }
                }
                catch {
                    Write-Host "[!] winget installation failed: $_" -ForegroundColor Yellow
                    Write-Host "    Please install manually from: https://www.ghostscript.com/releases/gsdnld.html" -ForegroundColor Gray
                    return $false
                }
            }
            else {
                Write-Host "[i] winget not available" -ForegroundColor Gray
                Write-Host "    Please install Ghostscript manually:" -ForegroundColor Yellow
                Write-Host "    1. Go to: https://www.ghostscript.com/releases/gsdnld.html" -ForegroundColor Gray
                Write-Host "    2. Download 'Ghostscript GPL Release' for Windows (64-bit)" -ForegroundColor Gray
                Write-Host "    3. Run the installer" -ForegroundColor Gray
                Write-Host "    4. Restart PowerShell" -ForegroundColor Gray
                return $false
            }
        }
        elseif ($onLinux) {
            Write-Host "Detected Linux OS" -ForegroundColor Cyan
            Write-Host "[!] Please install Ghostscript using your package manager:" -ForegroundColor Yellow
            Write-Host "    Ubuntu/Debian: sudo apt-get install ghostscript" -ForegroundColor Gray
            Write-Host "    Fedora: sudo dnf install ghostscript" -ForegroundColor Gray
            Write-Host "    Arch: sudo pacman -S ghostscript" -ForegroundColor Gray
            return $false
        }
        elseif ($onMacOS) {
            Write-Host "Detected macOS" -ForegroundColor Cyan
            Write-Host "[!] Please install Ghostscript using Homebrew:" -ForegroundColor Yellow
            Write-Host "    brew install ghostscript" -ForegroundColor Gray
            return $false
        }
        else {
            Write-Host "[!] Could not detect OS. Please install Ghostscript manually." -ForegroundColor Yellow
            Write-Host "    Windows: winget install Ghostscript.Ghostscript" -ForegroundColor Gray
            Write-Host "    Or download from: https://www.ghostscript.com/releases/gsdnld.html" -ForegroundColor Gray
            return $false
        }
    }
    catch {
        Write-Host "[✗] Failed to install Ghostscript: $_" -ForegroundColor Red
        Write-Host "    Please install manually: winget install Ghostscript.Ghostscript" -ForegroundColor Gray
        Write-Host "    Or download from: https://www.ghostscript.com/releases/gsdnld.html" -ForegroundColor Gray
        return $false
    }
}

# ============================================================================
# VIDEO.JS DEPENDENCY CHECK (FOR OFFLINE VIDEO PLAYER)
# ============================================================================

function Test-VideoJSAvailability {
    Write-Host "`n=== Checking Video.js (for Enhanced Video Player) ===" -ForegroundColor Cyan
    
    # Check if Video.js files exist in local folder
    $videojsFolder = Join-Path $CONFIG.ToolsFolder "videojs"
    $videojsCss = Join-Path $videojsFolder "video-js.min.css"
    $videojsJs = Join-Path $videojsFolder "video.min.js"
    
    if ((Test-Path $videojsCss) -and (Test-Path $videojsJs)) {
        Write-Host "[✓] Video.js found in local tools folder" -ForegroundColor Green
        return $true
    }
    
    # Video.js not found
    Write-Host "`n[!] Video.js is not installed locally." -ForegroundColor Yellow
    Write-Host "    Video.js provides enhanced video playback features." -ForegroundColor Gray
    Write-Host "    Without it, basic HTML5 player will be used (limited features)." -ForegroundColor Gray
    Write-Host "    Size: ~1 MB download" -ForegroundColor Gray
    
    $install = Read-Host "`nWould you like to download Video.js now for offline use? (Y/N)"
    
    if ($install -eq 'Y' -or $install -eq 'y') {
        return Install-VideoJS
    }
    else {
        Write-Host "`n[i] Continuing without Video.js. Basic HTML5 player will be used." -ForegroundColor Gray
        Write-Host "    Enhanced features (playback speed, better controls) will be unavailable." -ForegroundColor Gray
        return $false
    }
}

function Install-VideoJS {
    Write-Host "`n=== Downloading Video.js ===" -ForegroundColor Cyan
    
    try {
        # Create videojs folder
        $videojsFolder = Join-Path $CONFIG.ToolsFolder "videojs"
        if (-not (Test-Path $videojsFolder)) {
            New-Item -ItemType Directory -Path $videojsFolder -Force | Out-Null
        }
        
        # Download Video.js CSS
        $cssUrl = "https://vjs.zencdn.net/8.10.0/video-js.min.css"
        $cssPath = Join-Path $videojsFolder "video-js.min.css"
        
        Write-Host "Downloading Video.js CSS..." -ForegroundColor Cyan
        $ProgressPreference = 'SilentlyContinue'
        Invoke-WebRequest -Uri $cssUrl -OutFile $cssPath -UseBasicParsing
        
        # Download Video.js JavaScript
        $jsUrl = "https://vjs.zencdn.net/8.10.0/video.min.js"
        $jsPath = Join-Path $videojsFolder "video.min.js"
        
        Write-Host "Downloading Video.js JavaScript..." -ForegroundColor Cyan
        Invoke-WebRequest -Uri $jsUrl -OutFile $jsPath -UseBasicParsing
        $ProgressPreference = 'Continue'
        
        # Verify installation
        if ((Test-Path $cssPath) -and (Test-Path $jsPath)) {
            Write-Host "[✓] Video.js installed successfully!" -ForegroundColor Green
            Write-Host "    Location: $videojsFolder" -ForegroundColor Gray
            Write-Host "    Your media library will now work 100% offline!" -ForegroundColor Green
            return $true
        }
        else {
            Write-Host "[✗] Failed to download Video.js files" -ForegroundColor Red
            return $false
        }
    }
    catch {
        Write-Host "[✗] Failed to download Video.js: $_" -ForegroundColor Red
        Write-Host "    Will fall back to basic HTML5 player." -ForegroundColor Gray
        return $false
    }
}

# ============================================================================
# DATABASE INITIALIZATION
# ============================================================================

function Initialize-Database {
    param(
        [string]$DatabasePath,
        [string]$Type  # "movies", "music", "pictures", or "pdfs"
    )
    
    Write-Host "Initializing $Type database: $DatabasePath" -ForegroundColor Cyan
    
    if ($Type -eq "movies") {
        Invoke-SqliteQuery -DataSource $DatabasePath -Query @"
CREATE TABLE IF NOT EXISTS videos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    filename TEXT NOT NULL,
    filepath TEXT NOT NULL UNIQUE,
    title TEXT,
    year INTEGER,
    duration INTEGER,
    size_bytes INTEGER,
    format TEXT,
    resolution TEXT,
    width INTEGER,
    height INTEGER,
    codec TEXT,
    bitrate INTEGER,
    poster_url TEXT,
    backdrop_path TEXT,
    backdrop_url TEXT,
    tmdb_id INTEGER,
    media_type TEXT DEFAULT 'movie',
    genre TEXT,
    rating REAL,
    overview TEXT,
    director TEXT,
    cast TEXT,
    runtime INTEGER,
    audio_tracks TEXT,
    quality_score INTEGER,
    is_low_quality BOOLEAN DEFAULT 0,
    needs_metadata BOOLEAN DEFAULT 1,
    manually_edited BOOLEAN DEFAULT 0,
    date_added DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_modified DATETIME,
    last_played DATETIME
);

CREATE INDEX IF NOT EXISTS idx_videos_title ON videos(title);
CREATE INDEX IF NOT EXISTS idx_videos_year ON videos(year);
CREATE INDEX IF NOT EXISTS idx_videos_filepath ON videos(filepath);
CREATE INDEX IF NOT EXISTS idx_videos_quality ON videos(is_low_quality);
CREATE INDEX IF NOT EXISTS idx_videos_metadata ON videos(needs_metadata);
CREATE INDEX IF NOT EXISTS idx_videos_media_type ON videos(media_type);
"@
        
        # Add new backdrop columns to existing database if they don't exist
        try {
            Invoke-SqliteQuery -DataSource $DatabasePath -Query "ALTER TABLE videos ADD COLUMN backdrop_path TEXT" -ErrorAction SilentlyContinue
            Invoke-SqliteQuery -DataSource $DatabasePath -Query "ALTER TABLE videos ADD COLUMN backdrop_url TEXT" -ErrorAction SilentlyContinue
        } catch {
            # Columns already exist, ignore
        }
    }
    elseif ($Type -eq "music") {
        Invoke-SqliteQuery -DataSource $DatabasePath -Query @"
CREATE TABLE IF NOT EXISTS music (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    filename TEXT NOT NULL,
    filepath TEXT NOT NULL UNIQUE,
    title TEXT,
    artist TEXT,
    album TEXT,
    year INTEGER,
    duration INTEGER,
    size_bytes INTEGER,
    format TEXT,
    bitrate INTEGER,
    sample_rate INTEGER,
    channels INTEGER,
    album_art_url TEXT,
    album_art_cached TEXT,
    has_embedded_art BOOLEAN DEFAULT 0,
    musicbrainz_id TEXT,
    genre TEXT,
    track_number INTEGER,
    is_low_quality BOOLEAN DEFAULT 0,
    needs_metadata BOOLEAN DEFAULT 1,
    date_added DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_modified DATETIME,
    last_played DATETIME
);

CREATE INDEX IF NOT EXISTS idx_music_title ON music(title);
CREATE INDEX IF NOT EXISTS idx_music_artist ON music(artist);
CREATE INDEX IF NOT EXISTS idx_music_album ON music(album);
CREATE INDEX IF NOT EXISTS idx_music_filepath ON music(filepath);
CREATE INDEX IF NOT EXISTS idx_music_quality ON music(is_low_quality);
CREATE INDEX IF NOT EXISTS idx_music_metadata ON music(needs_metadata);
"@
        
        # Add new columns to existing database if they don't exist
        try {
            Invoke-SqliteQuery -DataSource $DatabasePath -Query "ALTER TABLE music ADD COLUMN album_art_cached TEXT" -ErrorAction SilentlyContinue
            Invoke-SqliteQuery -DataSource $DatabasePath -Query "ALTER TABLE music ADD COLUMN has_embedded_art BOOLEAN DEFAULT 0" -ErrorAction SilentlyContinue
        } catch {
            # Columns already exist, ignore
        }
    }
    elseif ($Type -eq "pictures") {
        Invoke-SqliteQuery -DataSource $DatabasePath -Query @"
CREATE TABLE IF NOT EXISTS images (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    filename TEXT NOT NULL,
    filepath TEXT NOT NULL UNIQUE,
    width INTEGER,
    height INTEGER,
    size_bytes INTEGER,
    format TEXT,
    date_taken DATETIME,
    camera_make TEXT,
    camera_model TEXT,
    date_added DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_modified DATETIME
);

CREATE INDEX IF NOT EXISTS idx_images_filepath ON images(filepath);
CREATE INDEX IF NOT EXISTS idx_images_date_taken ON images(date_taken);
"@
    }
    elseif ($Type -eq "pdfs") {
        Invoke-SqliteQuery -DataSource $DatabasePath -Query @"
CREATE TABLE IF NOT EXISTS pdfs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    filename TEXT NOT NULL,
    filepath TEXT NOT NULL UNIQUE,
    title TEXT,
    author TEXT,
    subject TEXT,
    keywords TEXT,
    creator TEXT,
    producer TEXT,
    page_count INTEGER,
    size_bytes INTEGER,
    preview_image TEXT,
    date_created DATETIME,
    date_modified DATETIME,
    date_added DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_pdfs_filepath ON pdfs(filepath);
CREATE INDEX IF NOT EXISTS idx_pdfs_filename ON pdfs(filename);
CREATE INDEX IF NOT EXISTS idx_pdfs_title ON pdfs(title);
"@
    }
    
    Write-Host "[✓] Database initialized successfully" -ForegroundColor Green
}

# ============================================================================
# TMDB API FUNCTIONS
# ============================================================================

function Get-TMDBMoviePoster {
    param(
        [string]$MovieTitle,
        [int]$Year = $null,
        [int]$VideoId,
        [switch]$Silent = $false
    )
    
    # Skip if no API key configured
    if ([string]::IsNullOrWhiteSpace($CONFIG.TMDB_APIKey)) {
        return $null
    }
    
    # Create poster cache folder if it doesn't exist
    if (-not (Test-Path $CONFIG.PosterCacheFolder)) {
        New-Item -ItemType Directory -Path $CONFIG.PosterCacheFolder -Force | Out-Null
    }
    
    # Aggressive title cleaning
    $cleanTitle = $MovieTitle
    
    # Remove everything after season/episode markers (S01E01, etc.)
    $cleanTitle = $cleanTitle -replace 'S\d{2}E\d{2}.*$', ''
    $cleanTitle = $cleanTitle -replace 'Season\s*\d+.*$', ''
    $cleanTitle = $cleanTitle -replace 'Episode\s*\d+.*$', ''
    
    # Remove year in parentheses if it exists (we'll use the year parameter instead)
    $cleanTitle = $cleanTitle -replace '\(\d{4}\)', ''
    $cleanTitle = $cleanTitle -replace '\[\d{4}\]', ''
    
    # Remove common release group tags first [xxx] or (xxx)
    $cleanTitle = $cleanTitle -replace '\[[\w\.\-]+\]', ''
    $cleanTitle = $cleanTitle -replace '\([\w\s\.\-]+\)$', ''
    
    # Remove quality indicators and everything after them (more comprehensive)
    $cleanTitle = $cleanTitle -replace '(1080p|720p|480p|2160p|4K|HDR|HDR10|HDR10Plus|DV|Dolby|HEVC|x264|x265|h264|h265|BluRay|BRRip|WEB-DL|WEBRip|WEB|HDTV|DVDRip|BRRip|AMZN|NF|DSNP|iP|DDP5|AAC|H\.264|H\.265|REPACK|PROPER|RERIP|10Bit|8Bit|IMAX|EXTENDED|UNRATED|DC).*$', ''
    
    # Remove streaming service tags
    $cleanTitle = $cleanTitle -replace '(AMZN|Netflix|NF|DSNP|HMAX|ATVP|PCOK|PMTP).*$', ''
    
    # Remove encoder/release group patterns
    $cleanTitle = $cleanTitle -replace '(ETHEL|RGB|PSA|WADU|ViTO|BYNDR|Ghost|RAWR|EZTVx).*$', ''
    
    # Remove file extensions
    $cleanTitle = $cleanTitle -replace '\.(mkv|mp4|avi|m4v|wmv|flv|webm)$', ''
    
    # Replace dots, underscores, and dashes with spaces BEFORE removing years
    $cleanTitle = $cleanTitle -replace '[\.\-_]+', ' '
    
    # Remove resolution numbers in parentheses like (1080) or (720)
    $cleanTitle = $cleanTitle -replace '\(1080\)', ''
    $cleanTitle = $cleanTitle -replace '\(720\)', ''
    $cleanTitle = $cleanTitle -replace '\(480\)', ''
    $cleanTitle = $cleanTitle -replace '\(2160\)', ''
    $cleanTitle = $cleanTitle -replace '\(4K\)', ''
    
    # Remove standalone resolution numbers like " 1080 ", " 720 ", " 2160 ", " 480 "
    $cleanTitle = $cleanTitle -replace '\s+(1080|720|480|2160|4K)\s*', ' '
    
    # Remove standalone year numbers (2024, 2025, etc.)
    $cleanTitle = $cleanTitle -replace '\s+20\d{2}\s*', ' '
    $cleanTitle = $cleanTitle -replace '\s+19\d{2}\s*', ' '
    
    # Remove "to" at the end (from torrents like "EZTVx.to")
    $cleanTitle = $cleanTitle -replace '\s+to\s*$', ''
    
    # Remove multiple spaces
    $cleanTitle = $cleanTitle -replace '\s{2,}', ' '
    
    # Trim whitespace
    $cleanTitle = $cleanTitle.Trim()
    
    try {
        # Search for the movie/TV show
        $searchUrl = "https://api.themoviedb.org/3/search/multi?api_key=$($CONFIG.TMDB_APIKey)&query=$([System.Web.HttpUtility]::UrlEncode($cleanTitle))"
        
        if ($Year) {
            # Try movie search first with year
            $movieSearchUrl = "https://api.themoviedb.org/3/search/movie?api_key=$($CONFIG.TMDB_APIKey)&query=$([System.Web.HttpUtility]::UrlEncode($cleanTitle))&year=$Year"
            $searchResponse = Invoke-RestMethod -Uri $movieSearchUrl -Method Get -ErrorAction Stop
            
            # If no movie results, try TV search
            if (-not $searchResponse.results -or $searchResponse.results.Count -eq 0) {
                $tvSearchUrl = "https://api.themoviedb.org/3/search/tv?api_key=$($CONFIG.TMDB_APIKey)&query=$([System.Web.HttpUtility]::UrlEncode($cleanTitle))&first_air_date_year=$Year"
                $searchResponse = Invoke-RestMethod -Uri $tvSearchUrl -Method Get -ErrorAction Stop
            }
        }
        else {
            # No year provided, use multi search
            $searchResponse = Invoke-RestMethod -Uri $searchUrl -Method Get -ErrorAction Stop
        }
        
        if (-not $Silent) {
            Write-Host "Searching TMDB for: $cleanTitle $(if($Year){"($Year)"})" -ForegroundColor Gray
        }
        
        if ($searchResponse.results -and $searchResponse.results.Count -gt 0) {
            $item = $searchResponse.results[0]
            
            # Get poster path (works for both movies and TV shows)
            $posterPath = $item.poster_path
            $backdropPath = $item.backdrop_path
            $itemTitle = if ($item.title) { $item.title } else { $item.name }
            $itemRating = if ($item.vote_average) { $item.vote_average } else { 0 }
            $tmdbId = $item.id
            $mediaType = if ($item.media_type) { $item.media_type } else { if ($item.title) { "movie" } else { "tv" } }
            
            # Fetch full details to get genres
            $genreString = $null
            $overview = $null
            $director = $null
            $castString = $null
            $runtime = $null
            try {
                # Add delay before detail fetch to avoid rate limiting
                Start-Sleep -Milliseconds 500
                
                # Build URL with explicit variable expansion to avoid substitution bugs
                $apiKey = $CONFIG.TMDB_APIKey
                if ($mediaType -eq "movie") {
                    $detailsUrl = "https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${apiKey}&append_to_response=credits"
                } else {
                    $detailsUrl = "https://api.themoviedb.org/3/tv/${tmdbId}?api_key=${apiKey}&append_to_response=credits"
                }
                
                if (-not $Silent) {
                    Write-Host "  Fetching $mediaType details..." -ForegroundColor Gray
                }
                $details = Invoke-RestMethod -Uri $detailsUrl -Method Get -ErrorAction Stop
                
                # Extract overview
                if ($details.overview) {
                    $overview = $details.overview
                    if (-not $Silent) {
                        Write-Host "  ✓ Overview retrieved" -ForegroundColor Green
                    }
                }
                
                # Extract runtime
                if ($details.runtime) {
                    $runtime = $details.runtime
                    if (-not $Silent) {
                        Write-Host "  ✓ Runtime: $runtime minutes" -ForegroundColor Green
                    }
                } elseif ($details.episode_run_time -and $details.episode_run_time.Count -gt 0) {
                    $runtime = $details.episode_run_time[0]
                    if (-not $Silent) {
                        Write-Host "  ✓ Runtime: $runtime minutes (TV episode)" -ForegroundColor Green
                    }
                }
                
                # Extract director (for movies)
                if ($mediaType -eq "movie" -and $details.credits -and $details.credits.crew) {
                    $directorObj = $details.credits.crew | Where-Object { $_.job -eq "Director" } | Select-Object -First 1
                    if ($directorObj) {
                        $director = $directorObj.name
                        if (-not $Silent) {
                            Write-Host "  ✓ Director: $director" -ForegroundColor Green
                        }
                    }
                } elseif ($mediaType -eq "tv" -and $details.created_by -and $details.created_by.Count -gt 0) {
                    $director = $details.created_by[0].name
                    if (-not $Silent) {
                        Write-Host "  ✓ Creator: $director" -ForegroundColor Green
                    }
                }
                
                # Extract cast (top 10 actors)
                $castString = $null
                if ($details.credits -and $details.credits.cast) {
                    $topCast = $details.credits.cast | Select-Object -First 10
                    if ($topCast) {
                        $castNames = $topCast | ForEach-Object { $_.name }
                        $castString = $castNames -join ", "
                        $castCount = $castNames.Count
                        if (-not $Silent) {
                            Write-Host "  ✓ Cast: $castCount actors retrieved" -ForegroundColor Green
                        }
                    }
                }
                
                # Extract genre names and join with commas
                if ($details.genres -and $details.genres.Count -gt 0) {
                    $genreNames = $details.genres | ForEach-Object { $_.name }
                    $genreString = $genreNames -join ", "
                    if (-not $Silent) {
                        Write-Host "  ✓ Genres: $genreString" -ForegroundColor Green
                    }
                } else {
                    if (-not $Silent) {
                        Write-Host "  ⚠ No genres found in TMDB data" -ForegroundColor Yellow
                    }
                }
            }
            catch {
                if (-not $Silent) {
                    Write-Host "  ⚠ Could not fetch genre details: $($_.Exception.Message)" -ForegroundColor Yellow
                }
            }
            
            if ($posterPath) {
                $posterUrl = "https://image.tmdb.org/t/p/w500$posterPath"
                # Use TMDB ID and media type for filename to avoid duplicates for same show/movie
                $posterFileName = "${mediaType}_${tmdbId}.jpg"
                $posterFullPath = Join-Path $CONFIG.PosterCacheFolder $posterFileName
                
                # Download poster if not already cached
                if (-not (Test-Path $posterFullPath)) {
                    if (-not $Silent) {
                        Write-Host "  ✓ Downloading poster: $itemTitle" -ForegroundColor Green
                    }
                    Invoke-WebRequest -Uri $posterUrl -OutFile $posterFullPath -ErrorAction Stop
                }
                else {
                    if (-not $Silent) {
                        Write-Host "  ✓ Poster already cached: $itemTitle" -ForegroundColor Cyan
                    }
                }
                
                # Download backdrop if available
                $backdropFileName = $null
                if ($backdropPath) {
                    $backdropUrl = "https://image.tmdb.org/t/p/w1280$backdropPath"
                    # Use TMDB ID and media type for filename to avoid duplicates for same show/movie
                    $backdropFileName = "backdrop_${mediaType}_${tmdbId}.jpg"
                    $backdropFullPath = Join-Path $CONFIG.PosterCacheFolder $backdropFileName
                    
                    # Download backdrop if not already cached
                    if (-not (Test-Path $backdropFullPath)) {
                        if (-not $Silent) {
                            Write-Host "  ✓ Downloading backdrop: $itemTitle" -ForegroundColor Green
                        }
                        Invoke-WebRequest -Uri $backdropUrl -OutFile $backdropFullPath -ErrorAction Stop
                    }
                    else {
                        if (-not $Silent) {
                            Write-Host "  ✓ Backdrop already cached: $itemTitle" -ForegroundColor Cyan
                        }
                    }
                }
                else {
                    if (-not $Silent) {
                        Write-Host "  ⚠ No backdrop available for: $itemTitle" -ForegroundColor Yellow
                    }
                }
                
                # Update database with poster info AND genres - only if not manually edited
                Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
UPDATE videos 
SET poster_url = CASE WHEN manually_edited = 1 THEN poster_url ELSE @url END, 
    backdrop_path = CASE WHEN manually_edited = 1 THEN backdrop_path ELSE @backdrop_path END,
    backdrop_url = CASE WHEN manually_edited = 1 THEN backdrop_url ELSE @backdrop_url END,
    tmdb_id = CASE WHEN manually_edited = 1 THEN tmdb_id ELSE @tmdb_id END,
    media_type = CASE WHEN manually_edited = 1 THEN media_type ELSE @media_type END,
    rating = CASE WHEN manually_edited = 1 THEN rating ELSE @rating END,
    genre = CASE WHEN manually_edited = 1 THEN genre ELSE @genre END,
    overview = CASE WHEN manually_edited = 1 THEN overview ELSE @overview END,
    director = CASE WHEN manually_edited = 1 THEN director ELSE @director END,
    "cast" = CASE WHEN manually_edited = 1 THEN "cast" ELSE @cast END,
    runtime = CASE WHEN manually_edited = 1 THEN runtime ELSE @runtime END,
    needs_metadata = CASE WHEN manually_edited = 1 THEN needs_metadata ELSE 0 END
WHERE id = @id
"@ -SqlParameters @{
                    url = $posterFileName
                    backdrop_path = $backdropPath
                    backdrop_url = $backdropFileName
                    tmdb_id = $tmdbId
                    media_type = $mediaType
                    rating = $itemRating
                    genre = $genreString
                    overview = $overview
                    director = $director
                    cast = $castString
                    runtime = $runtime
                    id = $VideoId
                }
                
                return $posterFileName
            }
            else {
                if (-not $Silent) {
                    Write-Host "  ⚠ No poster available for: $itemTitle" -ForegroundColor Yellow
                }
            }
        }
        else {
            if (-not $Silent) {
                Write-Host "  ✗ No results found for: $cleanTitle $(if($Year){"($Year)"})" -ForegroundColor Yellow
            }
        }
    }
    catch {
        if (-not $Silent) {
            Write-Host "  ✗ Error fetching poster: $_" -ForegroundColor Red
        }
    }
    
    return $null
}

function Update-AllMoviePosters {
    param(
        [switch]$Silent = $false
    )
    
    if (-not $Silent) {
        Write-Host "`n=== Fetching Movie Posters from TMDB ===" -ForegroundColor Cyan
    }
    
    if ([string]::IsNullOrWhiteSpace($CONFIG.TMDB_APIKey)) {
        if (-not $Silent) {
            Write-Host "`n[!] TMDB API key not configured!" -ForegroundColor Yellow
            Write-Host "    Add your API key to the config to enable poster downloads." -ForegroundColor Gray
            Write-Host "    Get free API key at: https://www.themoviedb.org/settings/api" -ForegroundColor Gray
        }
        return
    }
    
    # Get UNIQUE titles without posters - GROUP BY title to avoid duplicates for TV series
    # For each unique title, get one representative video ID to use for poster download
    $videosNeedingPosters = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
SELECT 
    MIN(id) as id,
    title, 
    year 
FROM videos 
WHERE poster_url IS NULL OR poster_url = ''
GROUP BY title, year
LIMIT 50
"@
    
    if (-not $videosNeedingPosters) {
        if (-not $Silent) {
            Write-Host "All videos already have posters!" -ForegroundColor Green
        }
        return
    }
    
    if (-not $Silent) {
        Write-Host "Fetching posters for $($videosNeedingPosters.Count) unique titles..." -ForegroundColor Cyan
    }
    
    $count = 0
    foreach ($video in $videosNeedingPosters) {
        $count++
        if (-not $Silent) {
            Write-Host "[$count/$($videosNeedingPosters.Count)] " -NoNewline -ForegroundColor Gray
        }
        $posterFileName = Get-TMDBMoviePoster -MovieTitle $video.title -Year $video.year -VideoId $video.id -Silent:$Silent
        
        # If poster was downloaded successfully, update ALL videos with the same title
        if ($posterFileName) {
            # Update all videos with the same title to use this poster
            $allVideosWithTitle = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
SELECT id FROM videos 
WHERE title = @title AND year = @year AND (poster_url IS NULL OR poster_url = '')
"@ -SqlParameters @{
                title = $video.title
                year = $video.year
            }
            
            foreach ($relatedVideo in $allVideosWithTitle) {
                if ($relatedVideo.id -ne $video.id) {
                    # Update related videos to use the same poster
                    Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
UPDATE videos 
SET poster_url = @poster_url,
    needs_metadata = 0
WHERE id = @id
"@ -SqlParameters @{
                        poster_url = $posterFileName
                        id = $relatedVideo.id
                    }
                }
            }
        }
        
        # Rate limiting - TMDB allows 40 requests per 10 seconds
        Start-Sleep -Milliseconds 300
    }
    
    if (-not $Silent) {
        Write-Host "`n[✓] Poster fetch complete!" -ForegroundColor Green
    }
}

function Update-AllGenres {
    Write-Host "`n=== Refreshing Genres from TMDB ===" -ForegroundColor Cyan
    
    if ([string]::IsNullOrWhiteSpace($CONFIG.TMDB_APIKey)) {
        Write-Host "`n[!] TMDB API key not configured!" -ForegroundColor Yellow
        Write-Host "    Add your API key to the config to enable genre fetching." -ForegroundColor Gray
        Write-Host "    Get free API key at: https://www.themoviedb.org/settings/api" -ForegroundColor Gray
        return
    }
    
    # Get ALL videos (or videos without genres)
    $allVideos = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT id, title, year, genre FROM videos ORDER BY date_added DESC"
    
    if (-not $allVideos) {
        Write-Host "No videos found in database!" -ForegroundColor Yellow
        return
    }
    
    # Count how many need genre updates
    $videosNeedingGenres = $allVideos | Where-Object { [string]::IsNullOrWhiteSpace($_.genre) }
    $totalToUpdate = $allVideos.Count
    
    if ($videosNeedingGenres.Count -eq 0) {
        Write-Host "`nAll videos already have genres!" -ForegroundColor Green
        $refreshAll = Read-Host "Would you like to refresh all genres anyway? (Y/N)"
        if ($refreshAll -ne 'Y' -and $refreshAll -ne 'y') {
            return
        }
    }
    
    Write-Host "Fetching genres for $totalToUpdate videos..." -ForegroundColor Cyan
    Write-Host "(This will also update posters if missing)`n" -ForegroundColor Gray
    
    $count = 0
    $updated = 0
    foreach ($video in $allVideos) {
        $count++
        Write-Host "[$count/$totalToUpdate] " -NoNewline -ForegroundColor Gray
        
        # Use existing poster fetch function which now also gets genres
        $result = Get-TMDBMoviePoster -MovieTitle $video.title -Year $video.year -VideoId $video.id
        
        if ($result) {
            $updated++
        }
        
        # Rate limiting - TMDB allows 40 requests per 10 seconds
        # With 2 API calls per movie (search + details), we need more delay
        Start-Sleep -Milliseconds 800
    }
    
    Write-Host "`n[✓] Genre refresh complete! Updated $updated movies." -ForegroundColor Green
}

# ============================================================================
# MEDIA SCANNING FUNCTIONS
# ============================================================================

function Get-MediaFiles {
    param(
        [string]$Path,
        [string[]]$Extensions
    )
    
    if (-not (Test-Path $Path)) {
        Write-Host "[!] Path not found: $Path" -ForegroundColor Yellow
        return @()
    }
    
    Write-Host "Scanning: $Path" -ForegroundColor Gray
    Get-ChildItem -Path $Path -File -Recurse -ErrorAction SilentlyContinue |
        Where-Object { $_.Extension -in $Extensions }
}

function Get-VideoMetadata {
    param([System.IO.FileInfo]$File)
    
    $metadata = @{
        filename = $File.Name
        filepath = $File.FullName
        title = [System.IO.Path]::GetFileNameWithoutExtension($File.Name)
        size_bytes = $File.Length
        format = $File.Extension.TrimStart('.')
        last_modified = $File.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
        needs_metadata = 1
        year = $null
        resolution = $null
        height = $null
        is_low_quality = 0
    }
    
    # Try to extract basic info from filename - only valid years (19xx or 20xx)
    if ($File.Name -match '(19\d{2}|20\d{2})') {
        $possibleYear = [int]$matches[1]
        # Exclude common resolution numbers
        if ($possibleYear -notin @(1080, 2160, 720, 480)) {
            $metadata.year = $possibleYear
        }
    }
    
    # Determine quality based on filename patterns
    if ($File.Name -match '(\d{3,4})p') {
        $height = [int]$matches[1]
        $metadata.height = $height
        $metadata.resolution = "${height}p"
        $metadata.is_low_quality = if ($height -lt $CONFIG.LowQualityVideoThreshold) { 1 } else { 0 }
    }
    
    return $metadata
}

function Get-AudioMetadata {
    param([System.IO.FileInfo]$File)
    
    # Initialize default metadata
    $metadata = @{
        filename = $File.Name
        filepath = $File.FullName
        title = $null
        size_bytes = $File.Length
        format = $File.Extension.TrimStart('.')
        last_modified = $File.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
        needs_metadata = 1
        artist = $null
        album = $null
        album_artist = $null
        year = $null
        genre = $null
        track_number = $null
        duration = $null
        bitrate = $null
        sample_rate = $null
        channels = $null
        composer = $null
    }
    
    try {
        # Use Shell.Application to read metadata
        $shell = New-Object -ComObject Shell.Application
        $folder = $shell.Namespace($File.DirectoryName)
        $item = $folder.ParseName($File.Name)
        
        if ($item) {
            # Extract metadata using Shell property indices
            # Note: These indices are consistent across Windows systems
            
            # Title (index 21)
            $title = $folder.GetDetailsOf($item, 21)
            if (-not [string]::IsNullOrWhiteSpace($title)) {
                $metadata.title = $title
                $metadata.needs_metadata = 0
            } else {
                $metadata.title = [System.IO.Path]::GetFileNameWithoutExtension($File.Name)
            }
            
            # Contributing artists (index 13)
            $artist = $folder.GetDetailsOf($item, 13)
            if (-not [string]::IsNullOrWhiteSpace($artist)) {
                $metadata.artist = $artist
            }
            
            # Album artist (index 20) - Falls back to contributing artists if not set
            $albumArtist = $folder.GetDetailsOf($item, 20)
            if (-not [string]::IsNullOrWhiteSpace($albumArtist)) {
                $metadata.album_artist = $albumArtist
            } elseif (-not [string]::IsNullOrWhiteSpace($artist)) {
                $metadata.album_artist = $artist
            }
            
            # Album (index 14)
            $album = $folder.GetDetailsOf($item, 14)
            if (-not [string]::IsNullOrWhiteSpace($album)) {
                $metadata.album = $album
            }
            
            # Year (index 15)
            $year = $folder.GetDetailsOf($item, 15)
            if ($year -match '(\d{4})') {
                $metadata.year = [int]$matches[1]
            }
            
            # Genre (index 16)
            $genre = $folder.GetDetailsOf($item, 16)
            if (-not [string]::IsNullOrWhiteSpace($genre)) {
                $metadata.genre = $genre
            }
            
            # Track number (index 26)
            $track = $folder.GetDetailsOf($item, 26)
            if ($track -match '(\d+)') {
                $metadata.track_number = [int]$matches[1]
            }
            
            # Duration (index 27) - format: HH:MM:SS or MM:SS
            $duration = $folder.GetDetailsOf($item, 27)
            if ($duration -match '(\d+):(\d+):(\d+)') {
                # HH:MM:SS format
                $metadata.duration = ([int]$matches[1] * 3600) + ([int]$matches[2] * 60) + [int]$matches[3]
            } elseif ($duration -match '(\d+):(\d+)') {
                # MM:SS format
                $metadata.duration = ([int]$matches[1] * 60) + [int]$matches[2]
            }
            
            # Bit rate (index 28)
            $bitrate = $folder.GetDetailsOf($item, 28)
            if ($bitrate -match '(\d+)') {
                $metadata.bitrate = [int]$matches[1]
            }
            
            # Sample rate (index 318) - may vary by system, try common indices
            $sampleRate = $folder.GetDetailsOf($item, 318)
            if ([string]::IsNullOrWhiteSpace($sampleRate)) {
                $sampleRate = $folder.GetDetailsOf($item, 43)
            }
            if ($sampleRate -match '(\d+)') {
                $metadata.sample_rate = [int]$matches[1]
            }
            
            # Channels (index 319) - may vary, look for pattern
            $channels = $folder.GetDetailsOf($item, 319)
            if ([string]::IsNullOrWhiteSpace($channels)) {
                $channels = $folder.GetDetailsOf($item, 44)
            }
            if ($channels -match '(\d+)') {
                $metadata.channels = [int]$matches[1]
            } elseif ($channels -match 'stereo') {
                $metadata.channels = 2
            } elseif ($channels -match 'mono') {
                $metadata.channels = 1
            }
            
            # Composers (index 19)
            $composer = $folder.GetDetailsOf($item, 19)
            if (-not [string]::IsNullOrWhiteSpace($composer)) {
                $metadata.composer = $composer
            }
        }
        
        # Release COM object
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()
    }
    catch {
        Write-Verbose "Could not extract metadata from $($File.Name): $_"
        # Fall back to filename as title
        if (-not $metadata.title) {
            $metadata.title = [System.IO.Path]::GetFileNameWithoutExtension($File.Name)
        }
    }
    
    # DON'T use folder path as fallback for artist/album
    # Leave them as $null if not found in ID3 tags
    
    # Check for year in filename if not found in tags
    if (-not $metadata.year -and $File.Name -match '(\d{4})') {
        $metadata.year = [int]$matches[1]
    }
    
    return $metadata
}

function Get-ImageMetadata {
    param([System.IO.FileInfo]$File)
    
    return @{
        filename = $File.Name
        filepath = $File.FullName
        size_bytes = $File.Length
        format = $File.Extension.TrimStart('.')
        last_modified = $File.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
    }
}

function Get-PDFMetadata {
    param([System.IO.FileInfo]$File)
    
    # Basic metadata - we'll extract more detailed PDF info if needed
    $metadata = @{
        filename = $File.Name
        filepath = $File.FullName
        size_bytes = $File.Length
        title = $File.BaseName  # Use filename without extension as title
        date_modified = $File.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
        date_created = $File.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")
    }
    
    # Try to extract PDF properties using .NET if available
    try {
        # We could add more sophisticated PDF parsing here if needed
        # For now, just using file properties
    }
    catch {
        # Silently continue with basic metadata
    }
    
    return $metadata
}

# ============================================================================
# ALBUM ARTWORK EXTRACTION (NEW)
# ============================================================================

function Extract-AlbumArtwork {
    param(
        [string]$MusicFilePath,
        [int]$MusicId
    )
    
    # Check if ffmpeg is available
    if (-not $Global:FFmpegPath -or -not (Test-Path $Global:FFmpegPath)) {
        return $null
    }
    
    # Create album art cache folder
    if (-not (Test-Path $CONFIG.AlbumArtCacheFolder)) {
        New-Item -ItemType Directory -Path $CONFIG.AlbumArtCacheFolder -Force | Out-Null
    }
    
    # Generate cache filename
    $artFileName = "albumart_$MusicId.jpg"
    $artFilePath = Join-Path $CONFIG.AlbumArtCacheFolder $artFileName
    
    # Check if already extracted
    if (Test-Path $artFilePath) {
        return $artFileName
    }
    
    # Extract embedded artwork using ffmpeg
    try {
        $process = Start-Process -FilePath $Global:FFmpegPath `
            -ArgumentList @(
                "-i", "`"$MusicFilePath`""
                "-an"
                "-vcodec", "copy"
                "`"$artFilePath`""
                "-y"
            ) `
            -NoNewWindow -Wait -PassThru `
            -RedirectStandardError "$env:TEMP\ffmpeg_err_$MusicId.txt" `
            -RedirectStandardOutput "$env:TEMP\ffmpeg_out_$MusicId.txt"
        
        if ($process.ExitCode -eq 0 -and (Test-Path $artFilePath)) {
            # Verify it's a valid image
            try {
                Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue
                $img = [System.Drawing.Image]::FromFile($artFilePath)
                $img.Dispose()
                
                # Update database
                Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
UPDATE music 
SET album_art_cached = @artFileName, has_embedded_art = 1
WHERE id = @musicId
"@ -SqlParameters @{ artFileName = $artFileName; musicId = $MusicId }
                
                Write-Verbose "  ✓ Extracted album art for ID $MusicId"
                return $artFileName
            }
            catch {
                # Not a valid image, delete it
                Remove-Item $artFilePath -Force -ErrorAction SilentlyContinue
                return $null
            }
        }
        return $null
    }
    catch {
        Write-Verbose "Failed to extract artwork: $_"
        return $null
    }
    finally {
        # Clean up temp files
        Remove-Item "$env:TEMP\ffmpeg_err_$MusicId.txt" -Force -ErrorAction SilentlyContinue
        Remove-Item "$env:TEMP\ffmpeg_out_$MusicId.txt" -Force -ErrorAction SilentlyContinue
    }
}

function Extract-AllAlbumArtwork {
    param(
        [switch]$Force
    )
    
    Write-Host "`n=== Extracting Album Artwork ===" -ForegroundColor Cyan
    
    if ($Force) {
        Write-Host "[!] Force mode enabled - will re-extract all artwork" -ForegroundColor Yellow
    }
    
    # Check if ffmpeg is available
    if (-not $Global:FFmpegPath) {
        Write-Host "[!] ffmpeg is not available. Album artwork extraction requires ffmpeg." -ForegroundColor Yellow
        
        $install = Read-Host "`nWould you like to download ffmpeg now? (Y/N)"
        if ($install -eq 'Y' -or $install -eq 'y') {
            if (-not (Install-FFmpeg)) {
                Write-Host "[✗] Cannot extract album artwork without ffmpeg" -ForegroundColor Red
                return
            }
        }
        else {
            Write-Host "[i] Skipping album artwork extraction" -ForegroundColor Gray
            return
        }
    }
    
    # Check if album_artist column exists in the database
    $hasAlbumArtist = $false
    try {
        $tableInfo = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "PRAGMA table_info(music);"
        $hasAlbumArtist = ($tableInfo | Where-Object { $_.name -eq 'album_artist' }).Count -gt 0
    }
    catch {
        Write-Host "[!] Warning: Could not check database schema" -ForegroundColor Yellow
    }
    
    # Get all music files - if Force, get all files; otherwise only get files without artwork
    $musicFiles = $null
    $whereClause = if ($Force) { "" } else { "WHERE album_art_cached IS NULL OR album_art_cached = ''" }
    
    try {
        if ($hasAlbumArtist) {
            $musicFiles = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
SELECT id, filepath, filename, artist, album, album_artist 
FROM music 
$whereClause
ORDER BY album, artist
"@
        }
        else {
            Write-Host "[i] Using database schema without album_artist column" -ForegroundColor Gray
            $musicFiles = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
SELECT id, filepath, filename, artist, album 
FROM music 
$whereClause
ORDER BY album, artist
"@
        }
    }
    catch {
        Write-Host "[✗] Error querying database: $_" -ForegroundColor Red
        Write-Host "[!] Please ensure the music database exists and has been scanned" -ForegroundColor Yellow
        return
    }
    
    if (-not $musicFiles -or $musicFiles.Count -eq 0) {
        Write-Host "[✓] All music files already have artwork extracted (or none available)" -ForegroundColor Green
        return
    }
    
    Write-Host "Found $($musicFiles.Count) music files to process..." -ForegroundColor Cyan
    
    # Group files by album to avoid duplicate extractions
    # BUT: Only group if we have valid artist AND album metadata
    $albumGroups = @{}
    $ungroupedFiles = @()
    
    foreach ($music in $musicFiles) {
        # Check if we have valid artist and album metadata
        $hasValidArtist = -not [string]::IsNullOrWhiteSpace($music.artist)
        $hasValidAlbum = -not [string]::IsNullOrWhiteSpace($music.album)
        
        if ($hasValidArtist -and $hasValidAlbum) {
            # We have metadata - group by album
            $albumArtist = if ($music.PSObject.Properties.Name -contains 'album_artist') { 
                $music.album_artist 
            } else { 
                $null 
            }
            
            $keyArtist = if ($albumArtist) { $albumArtist } else { $music.artist }
            $albumKey = "${keyArtist}::$($music.album)"
            
            if (-not $albumGroups.ContainsKey($albumKey)) {
                $albumGroups[$albumKey] = @()
            }
            $albumGroups[$albumKey] += $music
        }
        else {
            # No metadata - extract individually
            $ungroupedFiles += $music
        }
    }
    
    $totalToProcess = $albumGroups.Count + $ungroupedFiles.Count
    Write-Host "Processing $($albumGroups.Count) albums and $($ungroupedFiles.Count) individual files..." -ForegroundColor Cyan
    Write-Host "Using parallel processing (10 concurrent extractions)..." -ForegroundColor Green
    
    if ($albumGroups.Count -gt 0) {
        Write-Host "Grouped files will save time by avoiding duplicate artwork extraction!" -ForegroundColor Green
    }
    
    # Use thread-safe concurrent collections for counters
    $extractedBag = [System.Collections.Concurrent.ConcurrentBag[int]]::new()
    $sharedBag = [System.Collections.Concurrent.ConcurrentBag[int]]::new()
    $skippedBag = [System.Collections.Concurrent.ConcurrentBag[int]]::new()
    $processedCounter = [System.Threading.Interlocked]::Exchange([ref]$null, 0)
    
    # Process grouped albums first (in parallel)
    if ($albumGroups.Count -gt 0) {
        $albumGroups.GetEnumerator() | ForEach-Object -Parallel {
            # Import required variables into parallel scope
            $CONFIG = $using:CONFIG
            $Global:FFmpegPath = $using:Global:FFmpegPath
            $extractedBag = $using:extractedBag
            $sharedBag = $using:sharedBag
            $skippedBag = $using:skippedBag
            $totalToProcess = $using:totalToProcess
            
            # Load the Extract-AlbumArtwork function in parallel scope
            function Extract-AlbumArtwork {
                param(
                    [string]$MusicFilePath,
                    [int]$MusicId
                )
                
                if (-not $Global:FFmpegPath -or -not (Test-Path $Global:FFmpegPath)) {
                    return $null
                }
                
                if (-not (Test-Path $CONFIG.AlbumArtCacheFolder)) {
                    New-Item -ItemType Directory -Path $CONFIG.AlbumArtCacheFolder -Force | Out-Null
                }
                
                $artFileName = "albumart_$MusicId.jpg"
                $artFilePath = Join-Path $CONFIG.AlbumArtCacheFolder $artFileName
                
                if (Test-Path $artFilePath) {
                    return $artFileName
                }
                
                try {
                    $process = Start-Process -FilePath $Global:FFmpegPath `
                        -ArgumentList @(
                            "-i", "`"$MusicFilePath`""
                            "-an"
                            "-vcodec", "copy"
                            "`"$artFilePath`""
                            "-y"
                        ) `
                        -NoNewWindow -Wait -PassThru `
                        -RedirectStandardError "$env:TEMP\ffmpeg_err_$MusicId.txt" `
                        -RedirectStandardOutput "$env:TEMP\ffmpeg_out_$MusicId.txt"
                    
                    if ($process.ExitCode -eq 0 -and (Test-Path $artFilePath)) {
                        try {
                            Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue
                            $img = [System.Drawing.Image]::FromFile($artFilePath)
                            $img.Dispose()
                            
                            Import-Module PSSQLite -ErrorAction SilentlyContinue
                            Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
UPDATE music 
SET album_art_cached = @artFileName, has_embedded_art = 1
WHERE id = @musicId
"@ -SqlParameters @{ artFileName = $artFileName; musicId = $MusicId }
                            
                            return $artFileName
                        }
                        catch {
                            Remove-Item $artFilePath -Force -ErrorAction SilentlyContinue
                            return $null
                        }
                    }
                    return $null
                }
                catch {
                    return $null
                }
                finally {
                    Remove-Item "$env:TEMP\ffmpeg_err_$MusicId.txt" -Force -ErrorAction SilentlyContinue
                    Remove-Item "$env:TEMP\ffmpeg_out_$MusicId.txt" -Force -ErrorAction SilentlyContinue
                }
            }
            
            # Process this album
            $albumKey = $_.Key
            $albumTracks = $_.Value
            $firstTrack = $albumTracks[0]
            
            $albumName = if ($firstTrack.album) { $firstTrack.album } else { "Unknown Album" }
            $artistName = if ($firstTrack.PSObject.Properties.Name -contains 'album_artist' -and $firstTrack.album_artist) { 
                $firstTrack.album_artist 
            } elseif ($firstTrack.artist) { 
                $firstTrack.artist 
            } else { 
                "Unknown Artist" 
            }
            
            $currentCount = [System.Threading.Interlocked]::Increment([ref]$using:processedCounter)
            Write-Host "[$currentCount/$totalToProcess] $artistName - $albumName ($($albumTracks.Count) tracks)" -ForegroundColor Gray
            
            if (Test-Path -LiteralPath $firstTrack.filepath) {
                $artFileName = Extract-AlbumArtwork -MusicFilePath $firstTrack.filepath -MusicId $firstTrack.id
                
                if ($artFileName) {
                    $extractedBag.Add(1)
                    Write-Host "  ✓ Extracted artwork" -ForegroundColor Green
                    
                    if ($albumTracks.Count -gt 1) {
                        foreach ($track in $albumTracks[1..($albumTracks.Count - 1)]) {
                            try {
                                Import-Module PSSQLite -ErrorAction SilentlyContinue
                                Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
UPDATE music 
SET album_art_cached = @artFileName, has_embedded_art = 1
WHERE id = @musicId
"@ -SqlParameters @{ artFileName = $artFileName; musicId = $track.id }
                                $sharedBag.Add(1)
                            }
                            catch {
                                Write-Host "  ! Failed to share artwork for track ID $($track.id)" -ForegroundColor Yellow
                            }
                        }
                        Write-Host "  ✓ Shared with $($albumTracks.Count - 1) other tracks" -ForegroundColor Cyan
                    }
                }
                else {
                    $skippedBag.Add(1)
                    Write-Host "  ✗ No embedded artwork found" -ForegroundColor Yellow
                }
            }
            else {
                $skippedBag.Add(1)
                Write-Host "  ✗ File not found: $($firstTrack.filepath)" -ForegroundColor Red
            }
        } -ThrottleLimit 10
    }
    
    # Process ungrouped files (in parallel)
    if ($ungroupedFiles.Count -gt 0) {
        $ungroupedFiles | ForEach-Object -Parallel {
            $CONFIG = $using:CONFIG
            $Global:FFmpegPath = $using:Global:FFmpegPath
            $extractedBag = $using:extractedBag
            $skippedBag = $using:skippedBag
            $totalToProcess = $using:totalToProcess
            
            function Extract-AlbumArtwork {
                param(
                    [string]$MusicFilePath,
                    [int]$MusicId
                )
                
                if (-not $Global:FFmpegPath -or -not (Test-Path $Global:FFmpegPath)) {
                    return $null
                }
                
                if (-not (Test-Path $CONFIG.AlbumArtCacheFolder)) {
                    New-Item -ItemType Directory -Path $CONFIG.AlbumArtCacheFolder -Force | Out-Null
                }
                
                $artFileName = "albumart_$MusicId.jpg"
                $artFilePath = Join-Path $CONFIG.AlbumArtCacheFolder $artFileName
                
                if (Test-Path $artFilePath) {
                    return $artFileName
                }
                
                try {
                    $process = Start-Process -FilePath $Global:FFmpegPath `
                        -ArgumentList @(
                            "-i", "`"$MusicFilePath`""
                            "-an"
                            "-vcodec", "copy"
                            "`"$artFilePath`""
                            "-y"
                        ) `
                        -NoNewWindow -Wait -PassThru `
                        -RedirectStandardError "$env:TEMP\ffmpeg_err_$MusicId.txt" `
                        -RedirectStandardOutput "$env:TEMP\ffmpeg_out_$MusicId.txt"
                    
                    if ($process.ExitCode -eq 0 -and (Test-Path $artFilePath)) {
                        try {
                            Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue
                            $img = [System.Drawing.Image]::FromFile($artFilePath)
                            $img.Dispose()
                            
                            Import-Module PSSQLite -ErrorAction SilentlyContinue
                            Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
UPDATE music 
SET album_art_cached = @artFileName, has_embedded_art = 1
WHERE id = @musicId
"@ -SqlParameters @{ artFileName = $artFileName; musicId = $MusicId }
                            
                            return $artFileName
                        }
                        catch {
                            Remove-Item $artFilePath -Force -ErrorAction SilentlyContinue
                            return $null
                        }
                    }
                    return $null
                }
                catch {
                    return $null
                }
                finally {
                    Remove-Item "$env:TEMP\ffmpeg_err_$MusicId.txt" -Force -ErrorAction SilentlyContinue
                    Remove-Item "$env:TEMP\ffmpeg_out_$MusicId.txt" -Force -ErrorAction SilentlyContinue
                }
            }
            
            $music = $_
            $currentCount = [System.Threading.Interlocked]::Increment([ref]$using:processedCounter)
            
            Write-Host "[$currentCount/$totalToProcess] $($music.filename) (no metadata)" -ForegroundColor Gray
            
            if (Test-Path -LiteralPath $music.filepath) {
                $artFileName = Extract-AlbumArtwork -MusicFilePath $music.filepath -MusicId $music.id
                
                if ($artFileName) {
                    $extractedBag.Add(1)
                    Write-Host "  ✓ Extracted artwork" -ForegroundColor Green
                }
                else {
                    $skippedBag.Add(1)
                    Write-Host "  ✗ No embedded artwork found" -ForegroundColor Yellow
                }
            }
            else {
                $skippedBag.Add(1)
                Write-Host "  ✗ File not found: $($music.filepath)" -ForegroundColor Red
            }
        } -ThrottleLimit 10
    }
    
    # Calculate final counts from concurrent bags
    $totalExtracted = $extractedBag.Count
    $totalShared = $sharedBag.Count
    $totalSkipped = $skippedBag.Count
    
    Write-Host "`n[✓] Album artwork extraction complete!" -ForegroundColor Green
    Write-Host "  Albums processed: $($albumGroups.Count)" -ForegroundColor Cyan
    Write-Host "  Individual files processed: $($ungroupedFiles.Count)" -ForegroundColor Cyan
    Write-Host "  Artwork extracted: $totalExtracted" -ForegroundColor Cyan
    Write-Host "  Artwork shared across tracks: $totalShared" -ForegroundColor Cyan
    Write-Host "  Files with no artwork: $totalSkipped" -ForegroundColor Yellow
    Write-Host "  Total files processed: $($musicFiles.Count)" -ForegroundColor Cyan
    
    $savedExtractions = $totalShared
    if ($savedExtractions -gt 0) {
        Write-Host "`n  💾 Saved $savedExtractions duplicate extractions!" -ForegroundColor Green
    }
}

# ============================================================================
# EPUB COVER EXTRACTION
# ============================================================================

function Generate-EPUBCoverThumbnail {
    param(
        [string]$EPUBFilePath,
        [int]$EPUBId
    )
    
    # Create EPUB cover cache folder
    if (-not (Test-Path $CONFIG.EPUBCoverCacheFolder)) {
        New-Item -ItemType Directory -Path $CONFIG.EPUBCoverCacheFolder -Force | Out-Null
    }
    
    # Generate cache filename
    $coverFileName = "epub_cover_$EPUBId.jpg"
    $coverFilePath = Join-Path $CONFIG.EPUBCoverCacheFolder $coverFileName
    
    # Check if already generated
    if (Test-Path $coverFilePath) {
        Write-Verbose "  → EPUB cover already exists: $coverFileName"
        return $coverFileName
    }
    
    try {
        # EPUB files are ZIP archives - extract cover image
        $tempDir = Join-Path $env:TEMP "epub_extract_$(Get-Random)"
        New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
        
        try {
            Add-Type -AssemblyName System.IO.Compression.FileSystem
            [System.IO.Compression.ZipFile]::ExtractToDirectory($EPUBFilePath, $tempDir)
            
            # Common cover image locations in EPUB files
            $coverPatterns = @(
                "cover.jpg", "cover.jpeg", "cover.png",
                "Cover.jpg", "Cover.jpeg", "Cover.png",
                "OEBPS/cover.jpg", "OEBPS/cover.jpeg", "OEBPS/cover.png",
                "OEBPS/images/cover.jpg", "OEBPS/images/cover.jpeg", "OEBPS/images/cover.png",
                "EPUB/cover.jpg", "EPUB/cover.jpeg", "EPUB/cover.png",
                "Images/cover.jpg", "Images/cover.jpeg", "Images/cover.png",
                "images/cover.jpg", "images/cover.jpeg", "images/cover.png"
            )
            
            # Try to find cover image using patterns
            $coverFound = $false
            foreach ($pattern in $coverPatterns) {
                $coverPath = Join-Path $tempDir $pattern
                if (Test-Path $coverPath) {
                    Copy-Item -Path $coverPath -Destination $coverFilePath -Force
                    $coverFound = $true
                    Write-Verbose "  ✓ Cover found: $pattern"
                    break
                }
            }
            
            # If no cover found using patterns, search for any image file
            if (-not $coverFound) {
                $imageFiles = Get-ChildItem -Path $tempDir -Recurse -Include "*.jpg", "*.jpeg", "*.png" -File | 
                    Where-Object { $_.Length -gt 10KB } |
                    Sort-Object -Property Length -Descending |
                    Select-Object -First 1
                
                if ($imageFiles) {
                    Copy-Item -Path $imageFiles.FullName -Destination $coverFilePath -Force
                    $coverFound = $true
                    Write-Verbose "  ✓ Cover found via search: $($imageFiles.Name)"
                }
            }
            
            if ($coverFound) {
                # Update database
                Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @coverFileName
WHERE id = @epubId
"@ -SqlParameters @{ coverFileName = $coverFileName; epubId = $EPUBId }
                
                Write-Verbose "  ✓ Extracted EPUB cover successfully"
                return $coverFileName
            }
            else {
                Write-Verbose "  ✗ No cover image found in EPUB"
                return $null
            }
        }
        finally {
            # Clean up temp directory
            if (Test-Path $tempDir) {
                Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
    }
    catch {
        Write-Verbose "  ✗ Error extracting EPUB cover: $_"
        return $null
    }
}

# ============================================================================
# PDF THUMBNAIL GENERATION
# ============================================================================

function Generate-PDFThumbnail {
    param(
        [string]$PDFFilePath,
        [int]$PDFId
    )
    
    # Create PDF preview cache folder
    if (-not (Test-Path $CONFIG.PDFPreviewCacheFolder)) {
        New-Item -ItemType Directory -Path $CONFIG.PDFPreviewCacheFolder -Force | Out-Null
    }
    
    # Generate cache filename
    $previewFileName = "pdf_preview_$PDFId.jpg"
    $previewFilePath = Join-Path $CONFIG.PDFPreviewCacheFolder $previewFileName
    
    # Check if already generated
    if (Test-Path $previewFilePath) {
        return $previewFileName
    }
    
    # METHOD 1: Try using Windows.Data.Pdf (Windows 10+ native)
    try {
        # Load Windows Runtime assemblies
        [Windows.Data.Pdf.PdfDocument,Windows.Data.Pdf,ContentType=WindowsRuntime] | Out-Null
        [Windows.Storage.StorageFile,Windows.Storage,ContentType=WindowsRuntime] | Out-Null
        [Windows.Storage.Streams.RandomAccessStreamReference,Windows.Storage.Streams,ContentType=WindowsRuntime] | Out-Null
        
        # Open the PDF
        $pdfFile = [System.IO.FileInfo]::new($PDFFilePath)
        $storageFile = [Windows.Storage.StorageFile]::GetFileFromPathAsync($pdfFile.FullName).GetAwaiter().GetResult()
        $pdfDocument = [Windows.Data.Pdf.PdfDocument]::LoadFromFileAsync($storageFile).GetAwaiter().GetResult()
        
        if ($pdfDocument.PageCount -gt 0) {
            # Get first page
            $pdfPage = $pdfDocument.GetPage(0)
            
            # Create a temporary file for the rendered page
            $tempPngPath = [System.IO.Path]::Combine($env:TEMP, "pdf_temp_$PDFId.png")
            
            # Render to PNG first
            Add-Type -AssemblyName System.Drawing
            
            # Calculate dimensions (maintain aspect ratio, max 400x600)
            $maxWidth = 400
            $maxHeight = 600
            $pageWidth = $pdfPage.Size.Width
            $pageHeight = $pdfPage.Size.Height
            
            $scale = [Math]::Min($maxWidth / $pageWidth, $maxHeight / $pageHeight)
            $renderWidth = [int]($pageWidth * $scale)
            $renderHeight = [int]($pageHeight * $scale)
            
            # Create render options
            $renderOptions = [Windows.Data.Pdf.PdfPageRenderOptions]::new()
            $renderOptions.DestinationWidth = $renderWidth
            $renderOptions.DestinationHeight = $renderHeight
            
            # Create a temp file to render to
            $storageFolder = [Windows.Storage.ApplicationData]::Current.TemporaryFolder
            $outputFile = $storageFolder.CreateFileAsync("pdf_render_$PDFId.png", [Windows.Storage.CreationCollisionOption]::ReplaceExisting).GetAwaiter().GetResult()
            
            # Render the page
            $stream = $outputFile.OpenAsync([Windows.Storage.FileAccessMode]::ReadWrite).GetAwaiter().GetResult()
            $pdfPage.RenderToStreamAsync($stream, $renderOptions).GetAwaiter().GetResult()
            $stream.Dispose()
            
            # Copy the rendered file to our cache location as JPEG
            $renderedPath = $outputFile.Path
            
            # Convert PNG to JPEG using .NET
            $img = [System.Drawing.Image]::FromFile($renderedPath)
            
            # Create JPEG encoder
            $jpegEncoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
            $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
            $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 85)
            
            # Save as JPEG
            $img.Save($previewFilePath, $jpegEncoder, $encoderParams)
            $img.Dispose()
            
            # Cleanup temp file
            Remove-Item $renderedPath -Force -ErrorAction SilentlyContinue
            
            # Cleanup PDF objects
            $pdfPage.Dispose()
            $pdfDocument.Dispose()
            
            # Update database
            Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @previewFileName
WHERE id = @pdfId
"@ -SqlParameters @{ previewFileName = $previewFileName; pdfId = $PDFId }
            
            Write-Verbose "  ✓ Generated PDF thumbnail using Windows native renderer"
            return $previewFileName
        }
    }
    catch {
        Write-Verbose "Windows PDF rendering failed: $_"
        # Fall through to other methods
    }
    
    # METHOD 2: Try using COM/Shell.Application (Windows Explorer thumbnails)
    try {
        Add-Type -AssemblyName System.Drawing
        
        # Use Windows Shell to get the thumbnail that Explorer would show
        $shell = New-Object -ComObject Shell.Application
        $folder = $shell.NameSpace([System.IO.Path]::GetDirectoryName($PDFFilePath))
        $file = $folder.ParseName([System.IO.Path]::GetFileName($PDFFilePath))
        
        # Get thumbnail (this uses the same system Windows Explorer uses)
        $thumbnail = $file.GetThumbnail(400, 1) # 400px width, format = bitmap
        
        if ($thumbnail) {
            # Save the thumbnail
            $thumbnail.Save($previewFilePath, [System.Drawing.Imaging.ImageFormat]::Jpeg)
            $thumbnail.Dispose()
            
            # Update database
            Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @previewFileName
WHERE id = @pdfId
"@ -SqlParameters @{ previewFileName = $previewFileName; pdfId = $PDFId }
            
            Write-Verbose "  ✓ Generated PDF thumbnail using Windows Shell"
            return $previewFileName
        }
    }
    catch {
        Write-Verbose "Windows Shell thumbnail failed: $_"
        # Fall through to other methods
    }
    
    # METHOD 1: Try using Ghostscript directly (BEST - standalone, no dependencies)
    $gsPath = Get-Command gs -ErrorAction SilentlyContinue
    if (-not $gsPath) {
        $gsPath = Get-Command gswin64c -ErrorAction SilentlyContinue
    }
    if (-not $gsPath) {
        $gsPath = Get-Command gswin32c -ErrorAction SilentlyContinue
    }
    
    if ($gsPath) {
        try {
            $arguments = @(
                "-dNOPAUSE"
                "-dBATCH"
                "-dSAFER"
                "-sDEVICE=jpeg"
                "-r150"
                "-dJPEGQ=85"
                "-dFirstPage=1"
                "-dLastPage=1"
                "-dTextAlphaBits=4"
                "-dGraphicsAlphaBits=4"
                "-sOutputFile=`"$previewFilePath`""
                "`"$PDFFilePath`""
            )
            
            $process = Start-Process -FilePath $gsPath.Source `
                -ArgumentList $arguments `
                -NoNewWindow -Wait -PassThru `
                -RedirectStandardError "$env:TEMP\gs_err_$PDFId.txt"
            
            if ($process.ExitCode -eq 0 -and (Test-Path $previewFilePath)) {
                Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @previewFileName
WHERE id = @pdfId
"@ -SqlParameters @{ previewFileName = $previewFileName; pdfId = $PDFId }
                
                Remove-Item "$env:TEMP\gs_err_$PDFId.txt" -Force -ErrorAction SilentlyContinue
                Write-Verbose "  ✓ Generated PDF thumbnail using Ghostscript"
                return $previewFileName
            }
        }
        catch {
            Write-Verbose "Ghostscript conversion failed: $_"
        }
    }
    
    # If all methods fail, return null (will show placeholder)
    Write-Verbose "  ✗ Could not generate PDF thumbnail - Ghostscript conversion failed"
    return $null
}

function Generate-AllPDFThumbnails {
    param(
        [switch]$Force
    )
    
    Write-Host "`n=== Generating PDF Thumbnails ===" -ForegroundColor Cyan
    
    if ($Force) {
        Write-Host "[!] Force mode enabled - will re-generate all thumbnails" -ForegroundColor Yellow
    }
    
    # Check for rendering tools
    $windowsNative = $PSVersionTable.PSVersion.Major -ge 5 -and [Environment]::OSVersion.Version.Major -ge 10
    $gsAvailable = (Get-Command gs -ErrorAction SilentlyContinue) -or (Get-Command gswin64c -ErrorAction SilentlyContinue) -or (Get-Command gswin32c -ErrorAction SilentlyContinue)
    
    Write-Host "`nDetected PDF rendering capabilities:" -ForegroundColor Cyan
    
    if ($gsAvailable) {
        Write-Host "[✓] Ghostscript available (best option)" -ForegroundColor Green
    }
    
    if ($windowsNative) {
        Write-Host "[✓] Windows 10+ native PDF rendering (fallback option)" -ForegroundColor Green
    }
    else {
        Write-Host "[!] Windows native PDF rendering not available (requires Windows 10+)" -ForegroundColor Yellow
    }
    
    if (-not $windowsNative -and -not $gsAvailable) {
        Write-Host "`n[!] No PDF rendering capability detected." -ForegroundColor Yellow
        Write-Host "    For best results, install Ghostscript:" -ForegroundColor Gray
        Write-Host "    • winget install Ghostscript.Ghostscript" -ForegroundColor Gray
        Write-Host "    • Or download from: https://www.ghostscript.com/releases/gsdnld.html" -ForegroundColor Gray
        Write-Host "`n    PDFs will display with placeholder icons instead." -ForegroundColor Gray
        return
    }
    
    # Get all PDFs - if Force, get all files; otherwise only get files without previews
    $whereClause = if ($Force) { "" } else { "WHERE preview_image IS NULL OR preview_image = ''" }
    
    $pdfsNeedingPreviews = Invoke-SqliteQuery -DataSource $CONFIG.PDFDB `
        -Query "SELECT * FROM pdfs $whereClause"
    
    if ($pdfsNeedingPreviews.Count -eq 0) {
        Write-Host "`n[i] All PDFs already have thumbnails!" -ForegroundColor Cyan
        return
    }
    
    Write-Host "`nGenerating thumbnails for $($pdfsNeedingPreviews.Count) PDF(s)..." -ForegroundColor Cyan
    Write-Host "Using parallel processing (10 concurrent generations)..." -ForegroundColor Green
    
    # Use thread-safe concurrent collections for counters
    $generatedBag = [System.Collections.Concurrent.ConcurrentBag[int]]::new()
    $failedBag = [System.Collections.Concurrent.ConcurrentBag[int]]::new()
    $processedCounter = [System.Threading.Interlocked]::Exchange([ref]$null, 0)
    
    # Process PDFs in parallel
    $pdfsNeedingPreviews | ForEach-Object -Parallel {
        # Import required variables into parallel scope
        $CONFIG = $using:CONFIG
        $generatedBag = $using:generatedBag
        $failedBag = $using:failedBag
        $totalToProcess = $using:pdfsNeedingPreviews.Count
        
        # Load the Generate-EPUBCoverThumbnail function in parallel scope
        function Generate-EPUBCoverThumbnail {
            param(
                [string]$EPUBFilePath,
                [int]$EPUBId
            )
            
            # Create EPUB cover cache folder
            if (-not (Test-Path $CONFIG.EPUBCoverCacheFolder)) {
                New-Item -ItemType Directory -Path $CONFIG.EPUBCoverCacheFolder -Force | Out-Null
            }
            
            # Generate cache filename
            $coverFileName = "epub_cover_$EPUBId.jpg"
            $coverFilePath = Join-Path $CONFIG.EPUBCoverCacheFolder $coverFileName
            
            # Check if already generated
            if (Test-Path $coverFilePath) {
                return $coverFileName
            }
            
            try {
                # EPUB files are ZIP archives - extract cover image
                $tempDir = Join-Path $env:TEMP "epub_extract_$(Get-Random)"
                New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
                
                try {
                    Add-Type -AssemblyName System.IO.Compression.FileSystem
                    [System.IO.Compression.ZipFile]::ExtractToDirectory($EPUBFilePath, $tempDir)
                    
                    # Common cover image locations
                    $coverPatterns = @(
                        "cover.jpg", "cover.jpeg", "cover.png",
                        "Cover.jpg", "Cover.jpeg", "Cover.png",
                        "OEBPS/cover.jpg", "OEBPS/cover.jpeg", "OEBPS/cover.png",
                        "OEBPS/images/cover.jpg", "OEBPS/images/cover.jpeg", "OEBPS/images/cover.png",
                        "Images/cover.jpg", "images/cover.jpg"
                    )
                    
                    $coverFound = $false
                    foreach ($pattern in $coverPatterns) {
                        $coverPath = Join-Path $tempDir $pattern
                        if (Test-Path $coverPath) {
                            Copy-Item -Path $coverPath -Destination $coverFilePath -Force
                            $coverFound = $true
                            break
                        }
                    }
                    
                    # Fallback: find any large image
                    if (-not $coverFound) {
                        $imageFiles = Get-ChildItem -Path $tempDir -Recurse -Include "*.jpg", "*.jpeg", "*.png" -File | 
                            Where-Object { $_.Length -gt 10KB } |
                            Sort-Object -Property Length -Descending |
                            Select-Object -First 1
                        
                        if ($imageFiles) {
                            Copy-Item -Path $imageFiles.FullName -Destination $coverFilePath -Force
                            $coverFound = $true
                        }
                    }
                    
                    if ($coverFound) {
                        # Update database
                        Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @coverFileName
WHERE id = @epubId
"@ -SqlParameters @{ coverFileName = $coverFileName; epubId = $EPUBId }
                        
                        return $coverFileName
                    }
                }
                finally {
                    if (Test-Path $tempDir) {
                        Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
                    }
                }
            }
            catch {
            }
            
            return $null
        }
        
        # Load the Generate-PDFThumbnail function in parallel scope
        function Generate-PDFThumbnail {
            param(
                [string]$PDFFilePath,
                [int]$PDFId
            )
            
            # Create PDF preview cache folder
            if (-not (Test-Path $CONFIG.PDFPreviewCacheFolder)) {
                New-Item -ItemType Directory -Path $CONFIG.PDFPreviewCacheFolder -Force | Out-Null
            }
            
            # Generate cache filename
            $previewFileName = "pdf_preview_$PDFId.jpg"
            $previewFilePath = Join-Path $CONFIG.PDFPreviewCacheFolder $previewFileName
            
            # Check if already generated
            if (Test-Path $previewFilePath) {
                return $previewFileName
            }
            
            # METHOD 1: Try using Windows.Data.Pdf (Windows 10+ native)
            try {
                # Load Windows Runtime assemblies
                [Windows.Data.Pdf.PdfDocument,Windows.Data.Pdf,ContentType=WindowsRuntime] | Out-Null
                [Windows.Storage.StorageFile,Windows.Storage,ContentType=WindowsRuntime] | Out-Null
                [Windows.Storage.Streams.RandomAccessStreamReference,Windows.Storage.Streams,ContentType=WindowsRuntime] | Out-Null
                
                # Open the PDF
                $pdfFile = [System.IO.FileInfo]::new($PDFFilePath)
                $storageFile = [Windows.Storage.StorageFile]::GetFileFromPathAsync($pdfFile.FullName).GetAwaiter().GetResult()
                $pdfDocument = [Windows.Data.Pdf.PdfDocument]::LoadFromFileAsync($storageFile).GetAwaiter().GetResult()
                
                if ($pdfDocument.PageCount -gt 0) {
                    # Get first page
                    $pdfPage = $pdfDocument.GetPage(0)
                    
                    # Create a temporary file for the rendered page
                    $tempPngPath = [System.IO.Path]::Combine($env:TEMP, "pdf_temp_$PDFId.png")
                    
                    # Render to PNG first
                    Add-Type -AssemblyName System.Drawing
                    
                    # Calculate dimensions (maintain aspect ratio, max 400x600)
                    $maxWidth = 400
                    $maxHeight = 600
                    $pageWidth = $pdfPage.Size.Width
                    $pageHeight = $pdfPage.Size.Height
                    
                    $scale = [Math]::Min($maxWidth / $pageWidth, $maxHeight / $pageHeight)
                    $renderWidth = [int]($pageWidth * $scale)
                    $renderHeight = [int]($pageHeight * $scale)
                    
                    # Create render options
                    $renderOptions = [Windows.Data.Pdf.PdfPageRenderOptions]::new()
                    $renderOptions.DestinationWidth = $renderWidth
                    $renderOptions.DestinationHeight = $renderHeight
                    
                    # Create a temp file to render to
                    $storageFolder = [Windows.Storage.ApplicationData]::Current.TemporaryFolder
                    $outputFile = $storageFolder.CreateFileAsync("pdf_render_$PDFId.png", [Windows.Storage.CreationCollisionOption]::ReplaceExisting).GetAwaiter().GetResult()
                    
                    # Render the page
                    $stream = $outputFile.OpenAsync([Windows.Storage.FileAccessMode]::ReadWrite).GetAwaiter().GetResult()
                    $pdfPage.RenderToStreamAsync($stream, $renderOptions).GetAwaiter().GetResult()
                    $stream.Dispose()
                    
                    # Copy the rendered file to our cache location as JPEG
                    $renderedPath = $outputFile.Path
                    
                    # Convert PNG to JPEG using .NET
                    $img = [System.Drawing.Image]::FromFile($renderedPath)
                    
                    # Create JPEG encoder
                    $jpegEncoder = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
                    $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
                    $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 85)
                    
                    # Save as JPEG
                    $img.Save($previewFilePath, $jpegEncoder, $encoderParams)
                    $img.Dispose()
                    
                    # Cleanup temp file
                    Remove-Item $renderedPath -Force -ErrorAction SilentlyContinue
                    
                    # Cleanup PDF objects
                    $pdfPage.Dispose()
                    $pdfDocument.Dispose()
                    
                    # Update database
                    Import-Module PSSQLite -ErrorAction SilentlyContinue
                    Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @previewFileName
WHERE id = @pdfId
"@ -SqlParameters @{ previewFileName = $previewFileName; pdfId = $PDFId }
                    
                    return $previewFileName
                }
            }
            catch {
                # Fall through to other methods
            }
            
            # METHOD 2: Try using COM/Shell.Application (Windows Explorer thumbnails)
            try {
                Add-Type -AssemblyName System.Drawing
                
                # Use Windows Shell to get the thumbnail that Explorer would show
                $shell = New-Object -ComObject Shell.Application
                $folder = $shell.NameSpace([System.IO.Path]::GetDirectoryName($PDFFilePath))
                $file = $folder.ParseName([System.IO.Path]::GetFileName($PDFFilePath))
                
                # Get thumbnail (this uses the same system Windows Explorer uses)
                $thumbnail = $file.GetThumbnail(400, 1) # 400px width, format = bitmap
                
                if ($thumbnail) {
                    # Save the thumbnail
                    $thumbnail.Save($previewFilePath, [System.Drawing.Imaging.ImageFormat]::Jpeg)
                    $thumbnail.Dispose()
                    
                    # Update database
                    Import-Module PSSQLite -ErrorAction SilentlyContinue
                    Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @previewFileName
WHERE id = @pdfId
"@ -SqlParameters @{ previewFileName = $previewFileName; pdfId = $PDFId }
                    
                    return $previewFileName
                }
            }
            catch {
                # Fall through to other methods
            }
            
            # METHOD 3: Try using Ghostscript directly (BEST - standalone, no dependencies)
            $gsPath = Get-Command gs -ErrorAction SilentlyContinue
            if (-not $gsPath) {
                $gsPath = Get-Command gswin64c -ErrorAction SilentlyContinue
            }
            if (-not $gsPath) {
                $gsPath = Get-Command gswin32c -ErrorAction SilentlyContinue
            }
            
            if ($gsPath) {
                try {
                    $arguments = @(
                        "-dNOPAUSE"
                        "-dBATCH"
                        "-dSAFER"
                        "-sDEVICE=jpeg"
                        "-r150"
                        "-dJPEGQ=85"
                        "-dFirstPage=1"
                        "-dLastPage=1"
                        "-dTextAlphaBits=4"
                        "-dGraphicsAlphaBits=4"
                        "-sOutputFile=`"$previewFilePath`""
                        "`"$PDFFilePath`""
                    )
                    
                    $process = Start-Process -FilePath $gsPath.Source `
                        -ArgumentList $arguments `
                        -NoNewWindow -Wait -PassThru `
                        -RedirectStandardError "$env:TEMP\gs_err_$PDFId.txt"
                    
                    if ($process.ExitCode -eq 0 -and (Test-Path $previewFilePath)) {
                        Import-Module PSSQLite -ErrorAction SilentlyContinue
                        Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query @"
UPDATE pdfs 
SET preview_image = @previewFileName
WHERE id = @pdfId
"@ -SqlParameters @{ previewFileName = $previewFileName; pdfId = $PDFId }
                        
                        Remove-Item "$env:TEMP\gs_err_$PDFId.txt" -Force -ErrorAction SilentlyContinue
                        return $previewFileName
                    }
                }
                catch {
                    # Error in Ghostscript conversion
                }
            }
            
            # If all methods fail, return null (will show placeholder)
            return $null
        }
        
        # Process this document (PDF or EPUB)
        $pdf = $_
        $currentCount = [System.Threading.Interlocked]::Increment([ref]$using:processedCounter)
        
        Write-Host "[$currentCount/$totalToProcess] Processing: $($pdf.filename)" -ForegroundColor Gray
        
        if (Test-Path -LiteralPath $pdf.filepath) {
            # Check file extension to determine processing method
            $fileExt = [System.IO.Path]::GetExtension($pdf.filepath).ToLower()
            
            if ($fileExt -eq '.epub') {
                # Handle EPUB files - extract cover
                $previewFileName = Generate-EPUBCoverThumbnail -EPUBFilePath $pdf.filepath -EPUBId $pdf.id
            }
            else {
                # Handle PDF files - generate thumbnail
                $previewFileName = Generate-PDFThumbnail -PDFFilePath $pdf.filepath -PDFId $pdf.id
            }
            
            if ($previewFileName) {
                $generatedBag.Add(1)
                Write-Host "  ✓ Generated preview" -ForegroundColor Green
            }
            else {
                $failedBag.Add(1)
                Write-Host "  ✗ Failed to generate preview" -ForegroundColor Yellow
            }
        }
        else {
            $failedBag.Add(1)
            Write-Host "  ✗ File not found: $($pdf.filepath)" -ForegroundColor Red
        }
    } -ThrottleLimit 10
    
    # Calculate final counts from concurrent bags
    $totalGenerated = $generatedBag.Count
    $totalFailed = $failedBag.Count
    
    Write-Host "`n[✓] PDF thumbnail generation complete!" -ForegroundColor Green
    Write-Host "  Generated: $totalGenerated" -ForegroundColor Cyan
    Write-Host "  Failed: $totalFailed" -ForegroundColor Yellow
    Write-Host "  Total processed: $($pdfsNeedingPreviews.Count)" -ForegroundColor Cyan
}

# ============================================================================
# SCAN AND INDEX FUNCTIONS
# ============================================================================

function Invoke-MediaScan {
    Write-Host "`n=== Starting Media Scan ===" -ForegroundColor Cyan
    $scanStart = Get-Date
    
    # Scan Videos
    Write-Host "`n[1/4] Scanning videos..." -ForegroundColor Yellow
    $videoFiles = Get-MediaFiles -Path $CONFIG.MoviesFolder -Extensions $CONFIG.VideoExtensions
    $videoCount = 0
    $videoSize = 0
    
    foreach ($file in $videoFiles) {
        try {
            $metadata = Get-VideoMetadata -File $file
            
            # Check if already exists
            $exists = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                -Query "SELECT id FROM videos WHERE filepath = @path" `
                -SqlParameters @{ path = $file.FullName }
            
            if (-not $exists) {
                # Try to get detailed video info including audio tracks (only if FFmpeg is available)
                $videoInfo = $null
                if ($Global:FFmpegPath) {
                    try {
                        $videoInfo = Get-VideoInfo -FilePath $file.FullName
                    }
                    catch {
                        Write-Verbose "Could not extract detailed video info: $_"
                    }
                }
                
                # Build the INSERT query dynamically based on available metadata
                $fields = @('filename', 'filepath', 'title', 'size_bytes', 'format', 'last_modified', 'needs_metadata', 'is_low_quality')
                $params = @{
                    filename = $metadata.filename
                    filepath = $metadata.filepath
                    title = $metadata.title
                    size_bytes = $metadata.size_bytes
                    format = $metadata.format
                    last_modified = $metadata.last_modified
                    needs_metadata = $metadata.needs_metadata
                    is_low_quality = $metadata.is_low_quality
                }
                
                # Add optional fields if they exist
                if ($metadata.year) {
                    $fields += 'year'
                    $params.year = $metadata.year
                }
                if ($metadata.resolution) {
                    $fields += 'resolution'
                    $params.resolution = $metadata.resolution
                }
                if ($metadata.height) {
                    $fields += 'height'
                    $params.height = $metadata.height
                }
                
                # Add audio tracks info if available from FFprobe
                if ($videoInfo -and $videoInfo.AudioTracks) {
                    $fields += 'audio_tracks'
                    $params.audio_tracks = $videoInfo.AudioTracks
                }
                
                $fieldList = $fields -join ', '
                $paramList = ($fields | ForEach-Object { "@$_" }) -join ', '
                
                $query = "INSERT INTO videos ($fieldList) VALUES ($paramList)"
                Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query $query -SqlParameters $params
                $videoCount++
            }
            
            $videoSize += $file.Length
        }
        catch {
            Write-Host "[!] Error processing $($file.Name): $_" -ForegroundColor Red
        }
    }
    
    # Scan Music
    Write-Host "`n[2/4] Scanning music..." -ForegroundColor Yellow
    $musicFiles = Get-MediaFiles -Path $CONFIG.MusicFolder -Extensions $CONFIG.AudioExtensions
    $musicCount = 0
    $musicSize = 0
    
    # Use thread-safe counters for parallel processing
    $addedCount = [System.Collections.Concurrent.ConcurrentBag[int]]::new()
    $totalSize = [System.Collections.Concurrent.ConcurrentBag[long]]::new()
    
    $musicFiles | ForEach-Object -Parallel {
        $file = $_
        $CONFIG = $using:CONFIG
        $addedBag = $using:addedCount
        $sizeBag = $using:totalSize
        
        try {
            # Extract metadata inline (simplified version using Shell.Application)
            $metadata = @{
                filename = $file.Name
                filepath = $file.FullName
                title = $null
                size_bytes = $file.Length
                format = $file.Extension.TrimStart('.')
                last_modified = $file.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")
                needs_metadata = 1
                artist = $null
                album = $null
                year = $null
                duration = $null
                bitrate = $null
            }
            
            try {
                $shell = New-Object -ComObject Shell.Application
                $folder = $shell.Namespace($file.DirectoryName)
                $item = $folder.ParseName($file.Name)
                
                if ($item) {
                    # Title (index 21)
                    $title = $folder.GetDetailsOf($item, 21)
                    if (-not [string]::IsNullOrWhiteSpace($title)) {
                        $metadata.title = $title
                        $metadata.needs_metadata = 0
                    } else {
                        $metadata.title = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
                    }
                    
                    # Artist (index 13)
                    $artist = $folder.GetDetailsOf($item, 13)
                    if (-not [string]::IsNullOrWhiteSpace($artist)) {
                        $metadata.artist = $artist
                    }
                    
                    # Album (index 14)
                    $album = $folder.GetDetailsOf($item, 14)
                    if (-not [string]::IsNullOrWhiteSpace($album)) {
                        $metadata.album = $album
                    }
                    
                    # Year (index 15)
                    $year = $folder.GetDetailsOf($item, 15)
                    if ($year -match '(\d{4})') {
                        $metadata.year = [int]$matches[1]
                    }
                    
                    # Duration (index 27) - Format: "00:03:45" or "03:45"
                    $durationStr = $folder.GetDetailsOf($item, 27)
                    if (-not [string]::IsNullOrWhiteSpace($durationStr)) {
                        # Parse duration string to seconds
                        if ($durationStr -match '(\d+):(\d+):(\d+)') {
                            # Format: HH:MM:SS
                            $hours = [int]$matches[1]
                            $minutes = [int]$matches[2]
                            $seconds = [int]$matches[3]
                            $metadata.duration = ($hours * 3600) + ($minutes * 60) + $seconds
                        }
                        elseif ($durationStr -match '(\d+):(\d+)') {
                            # Format: MM:SS
                            $minutes = [int]$matches[1]
                            $seconds = [int]$matches[2]
                            $metadata.duration = ($minutes * 60) + $seconds
                        }
                    }
                    
                    # Bitrate (index 28) - Format: "320kbps" or "128 kbps"
                    $bitrateStr = $folder.GetDetailsOf($item, 28)
                    if (-not [string]::IsNullOrWhiteSpace($bitrateStr)) {
                        # Extract numeric value from string like "320kbps"
                        if ($bitrateStr -match '(\d+)') {
                            $metadata.bitrate = [int]$matches[1]
                        }
                    }
                }
                
                [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell) | Out-Null
            }
            catch {
                if (-not $metadata.title) {
                    $metadata.title = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
                }
            }
            
            $exists = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB `
                -Query "SELECT id FROM music WHERE filepath = @path" `
                -SqlParameters @{ path = $file.FullName }
            
            if (-not $exists) {
                # Build the INSERT query dynamically
                $fields = @('filename', 'filepath', 'title', 'size_bytes', 'format', 'last_modified', 'needs_metadata')
                $params = @{
                    filename = $metadata.filename
                    filepath = $metadata.filepath
                    title = $metadata.title
                    size_bytes = $metadata.size_bytes
                    format = $metadata.format
                    last_modified = $metadata.last_modified
                    needs_metadata = $metadata.needs_metadata
                }
                
                # Add optional fields if they exist
                if ($metadata.artist) {
                    $fields += 'artist'
                    $params.artist = $metadata.artist
                }
                if ($metadata.album) {
                    $fields += 'album'
                    $params.album = $metadata.album
                }
                if ($metadata.year) {
                    $fields += 'year'
                    $params.year = $metadata.year
                }
                if ($metadata.duration) {
                    $fields += 'duration'
                    $params.duration = $metadata.duration
                }
                if ($metadata.bitrate) {
                    $fields += 'bitrate'
                    $params.bitrate = $metadata.bitrate
                }
                
                $fieldList = $fields -join ', '
                $paramList = ($fields | ForEach-Object { "@$_" }) -join ', '
                
                $query = "INSERT INTO music ($fieldList) VALUES ($paramList)"
                Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query $query -SqlParameters $params
                $addedBag.Add(1)
            }
            
            $sizeBag.Add($file.Length)
        }
        catch {
            Write-Host "[!] Error processing $($file.Name): $_" -ForegroundColor Red
        }
    } -ThrottleLimit 50
    
    # Calculate totals from concurrent bags
    $musicCount = $addedCount.Count
    $musicSize = ($totalSize | Measure-Object -Sum).Sum
    
    # Scan Pictures
    Write-Host "`n[3/4] Scanning pictures..." -ForegroundColor Yellow
    $imageFiles = Get-MediaFiles -Path $CONFIG.PicturesFolder -Extensions $CONFIG.ImageExtensions
    $imageCount = 0
    $imageSize = 0
    
    foreach ($file in $imageFiles) {
        try {
            $metadata = Get-ImageMetadata -File $file
            
            $exists = Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB `
                -Query "SELECT id FROM images WHERE filepath = @path" `
                -SqlParameters @{ path = $file.FullName }
            
            if (-not $exists) {
                $query = @"
INSERT INTO images (filename, filepath, size_bytes, format, last_modified)
VALUES (@filename, @filepath, @size_bytes, @format, @last_modified)
"@
                Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query $query -SqlParameters $metadata
                $imageCount++
            }
            
            $imageSize += $file.Length
        }
        catch {
            Write-Host "[!] Error processing $($file.Name): $_" -ForegroundColor Red
        }
    }
    
    # Scan PDFs and EPUBs
    Write-Host "`n[4/4] Scanning PDFs/ePUBS..." -ForegroundColor Yellow
    # Get both PDF and EPUB files - build array of extensions
    $pdfExtensions = $CONFIG.PDFExtensions -split ','
    $allExtensions = $pdfExtensions + @('.epub')
    $documentFiles = Get-MediaFiles -Path $CONFIG.PDFFolder -Extensions $allExtensions
    $pdfCount = 0
    $pdfSize = 0
    
    foreach ($file in $documentFiles) {
        try {
            $metadata = Get-PDFMetadata -File $file
            
            $exists = Invoke-SqliteQuery -DataSource $CONFIG.PDFDB `
                -Query "SELECT id FROM pdfs WHERE filepath = @path" `
                -SqlParameters @{ path = $file.FullName }
            
            if (-not $exists) {
                $query = @"
INSERT INTO pdfs (filename, filepath, title, size_bytes, date_modified, date_created)
VALUES (@filename, @filepath, @title, @size_bytes, @date_modified, @date_created)
"@
                Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query $query -SqlParameters $metadata
                $pdfCount++
            }
            
            $pdfSize += $file.Length
        }
        catch {
            Write-Host "[!] Error processing $($file.Name): $_" -ForegroundColor Red
        }
    }
    
    # Update global stats
    $Global:MediaStats.TotalVideos = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos").count
    $Global:MediaStats.TotalMusic = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(*) as count FROM music").count
    $Global:MediaStats.TotalPictures = (Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query "SELECT COUNT(*) as count FROM images").count
    $Global:MediaStats.TotalPDFs = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COUNT(*) as count FROM pdfs").count
    
    $Global:MediaStats.VideoSizeGB = [math]::Round($videoSize / 1GB, 2)
    $Global:MediaStats.MusicSizeGB = [math]::Round($musicSize / 1GB, 2)
    $Global:MediaStats.PicturesSizeGB = [math]::Round($imageSize / 1GB, 2)
    $Global:MediaStats.PDFSizeGB = [math]::Round($pdfSize / 1GB, 2)
    $Global:MediaStats.TotalSizeGB = [math]::Round(($videoSize + $musicSize + $imageSize + $pdfSize) / 1GB, 2)
    $Global:MediaStats.LastScanDate = Get-Date
    
    $scanDuration = (Get-Date) - $scanStart
    
    Write-Host "`n=== Scan Complete ===" -ForegroundColor Green
    Write-Host "Videos:   $videoCount new files indexed ($($Global:MediaStats.TotalVideos) total)" -ForegroundColor Gray
    Write-Host "Music:    $musicCount new files indexed ($($Global:MediaStats.TotalMusic) total)" -ForegroundColor Gray
    Write-Host "Pictures: $imageCount new files indexed ($($Global:MediaStats.TotalPictures) total)" -ForegroundColor Gray
    Write-Host "PDFs:     $pdfCount new files indexed ($($Global:MediaStats.TotalPDFs) total)" -ForegroundColor Gray
    Write-Host "Duration: $([math]::Round($scanDuration.TotalSeconds, 2)) seconds" -ForegroundColor Gray
    Write-Host "Total Size: $($Global:MediaStats.TotalSizeGB) GB" -ForegroundColor Cyan
}

# ============================================================================
# INTELLIGENT ANALYSIS FUNCTIONS
# ============================================================================

function Get-LowQualityMedia {
    Write-Host "`n=== Low Quality Media Analysis ===" -ForegroundColor Cyan
    
    $lowQualityVideos = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT title, resolution, filepath FROM videos WHERE is_low_quality = 1 ORDER BY title"
    
    if ($lowQualityVideos) {
        Write-Host "`nLow Quality Videos (< $($CONFIG.LowQualityVideoThreshold)p):" -ForegroundColor Yellow
        foreach ($video in $lowQualityVideos) {
            Write-Host "  • $($video.title) [$($video.resolution)]" -ForegroundColor Gray
        }
    }
    else {
        Write-Host "✓ No low quality videos detected" -ForegroundColor Green
    }
}

function Get-IncompleteMetadata {
    Write-Host "`n=== Incomplete Metadata Analysis ===" -ForegroundColor Cyan
    
    $videosNeedingMetadata = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT COUNT(*) as count FROM videos WHERE needs_metadata = 1"
    
    $musicNeedingMetadata = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB `
        -Query "SELECT COUNT(*) as count FROM music WHERE needs_metadata = 1"
    
    Write-Host "`nItems needing metadata:" -ForegroundColor Yellow
    Write-Host "  Videos: $($videosNeedingMetadata.count)" -ForegroundColor Gray
    Write-Host "  Music:  $($musicNeedingMetadata.count)" -ForegroundColor Gray
    
    if ($videosNeedingMetadata.count -gt 0 -or $musicNeedingMetadata.count -gt 0) {
        Write-Host "`n💡 Tip: Add TMDB and Last.FM API keys to auto-fetch metadata" -ForegroundColor Cyan
    }
}

function Get-WatchRecommendations {
    Write-Host "`n=== Smart Recommendations ===" -ForegroundColor Cyan
    
    # Get unwatched recent additions
    $recentUnwatched = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT title, date_added FROM videos WHERE last_played IS NULL ORDER BY date_added DESC LIMIT 5"
    
    if ($recentUnwatched) {
        Write-Host "`nRecent additions you haven't watched:" -ForegroundColor Yellow
        foreach ($item in $recentUnwatched) {
            $daysAgo = [math]::Round(((Get-Date) - [datetime]$item.date_added).TotalDays)
            Write-Host "  • $($item.title) (added $daysAgo days ago)" -ForegroundColor Gray
        }
    }
    
    # Get highly rated content
    $topRated = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT title, rating FROM videos WHERE rating >= 7.5 AND last_played IS NULL ORDER BY rating DESC LIMIT 3"
    
    if ($topRated) {
        Write-Host "`nHighly rated unwatched content:" -ForegroundColor Yellow
        foreach ($item in $topRated) {
            Write-Host "  • $($item.title) ⭐ $($item.rating)/10" -ForegroundColor Gray
        }
    }
}

function Get-CleanupSuggestions {
    Write-Host "`n=== Cleanup & Improvement Suggestions ===" -ForegroundColor Cyan
    
    # Find duplicate filenames
    $duplicates = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query @"
SELECT filename, COUNT(*) as count 
FROM videos 
GROUP BY filename 
HAVING count > 1
"@
    
    if ($duplicates) {
        Write-Host "`nPossible duplicates found:" -ForegroundColor Yellow
        foreach ($dup in $duplicates) {
            Write-Host "  • $($dup.filename) ($($dup.count) copies)" -ForegroundColor Gray
        }
    }
    
    # Find very small files (might be samples or corrupted)
    $smallFiles = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT title, size_bytes FROM videos WHERE size_bytes < 10485760 ORDER BY size_bytes"  # < 10MB
    
    if ($smallFiles) {
        Write-Host "`nSuspiciously small video files (< 10MB):" -ForegroundColor Yellow
        foreach ($file in $smallFiles) {
            $sizeMB = [math]::Round($file.size_bytes / 1MB, 2)
            Write-Host "  • $($file.title) (${sizeMB}MB)" -ForegroundColor Gray
        }
    }
}

# ============================================================================
# WEB SERVER HTML GENERATION
# ============================================================================

function Get-HTMLPage {
    param(
        [string]$Content,
        [string]$Title = "PSMediaLibrary",
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    # Get statistics for the page
    $stats = $Global:MediaStats
    
    # Generate Settings link for admin users
    $settingsLink = ""
    if ($UserSession -and $UserSession.UserType -eq 'admin') {
        $settingsLink = @"
<a href="/settings">
  <div class="left"><span class="ico">⚙️</span><span>Settings</span></div>
  <span class="pill" style="background: rgba(251,191,36,0.2); color: #fbbf24; border-color: #fbbf24;">ADMIN</span>
</a>
"@
    }
    
    return @"
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>$Title</title>
  <style>
    :root{
      --bg:#0d1410;
      --panel:#141f18;
      --panel2:#0f1612;
      --stroke: rgba(255,255,255,.10);
      --text: rgba(255,255,255,.92);
      --muted: rgba(255,255,255,.62);
      --accent:#34d399;
      --accent2:#34d399;
      --accent3:#fbbf24;
      --glow1: rgba(52,211,153,.18);
      --glow2: rgba(34,197,94,.12);
      --radius: 18px;
      --shadow: 0 18px 55px rgba(0,0,0,.45);
    }
    /* Blue Theme */
    body.theme-blue{
      --bg:#0a0f1a;
      --panel:#131b2e;
      --panel2:#0e1420;
        --accent:#3b82f6;
      --accent2:#60a5fa;
      --accent3:#fbbf24;
      --glow1: rgba(59,130,246,.25);
      --glow2: rgba(37,99,235,.18);
    }
    /* Purple Theme */
    body.theme-purple{
      --bg:#0f0a1a;
      --panel:#1a1329;
      --panel2:#130e20;
        --accent:#8b5cf6;
      --accent2:#a78bfa;
      --accent3:#fbbf24;
      --glow1: rgba(139,92,246,.25);
      --glow2: rgba(124,58,237,.18);
    }
    /* Red Theme */
    body.theme-red{
      --bg:#1a0a0a;
      --panel:#2e1313;
      --panel2:#200e0e;
      --accent:#ef4444;
      --accent2:#f87171;
      --accent3:#fbbf24;
      --glow1: rgba(239,68,68,.25);
      --glow2: rgba(220,38,38,.18);
    }
    *{box-sizing:border-box}
    body{
      margin:0;
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
      background:
      radial-gradient(950px 600px at 10% 15%, var(--glow1), transparent 55%),
      radial-gradient(800px 520px at 90% 85%, var(--glow2), transparent 55%),
      linear-gradient(180deg, #0a1410, var(--bg));
      color: var(--text);
      min-height:100vh;
      transition: background 0.4s ease;
    }
    .app{
      display:grid;
      grid-template-columns: 280px 1fr;
      min-height:100vh;
    }
    @media (max-width: 980px){
      .app{grid-template-columns: 1fr}
    }
    .sidebar{
      padding:18px;
      border-right: 1px solid rgba(255,255,255,.12);
      background: rgba(255,255,255,.08);
      /* backdrop-filter removed for performance */
      /* transform removed */
    }
    .brand{
      display:flex; gap:12px; align-items:center;
      padding:14px;
      border:1px solid rgba(255,255,255,.15);
      border-radius: var(--radius);
      background: rgba(255,255,255,.08);
      /* backdrop-filter removed for performance */
      box-shadow: 0 8px 32px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.1);
    }
    .logo{
      width:40px; height:40px; border-radius: 14px;
      background: linear-gradient(135deg, rgba(96,165,250,.9), rgba(52,211,153,.7));
      box-shadow: 0 4px 12px rgba(96,165,250,.3);
    }
    .brand strong{display:block; font-size:16px}
    .brand span{display:block; font-size:12px; color:var(--muted); margin-top:2px}
    .nav{
      margin-top:14px;
      display:flex; flex-direction:column; gap:10px;
    }
    .nav a{
      display:flex; align-items:center; justify-content:space-between;
      gap:12px;
      padding:14px 14px;
      border-radius: 16px;
      border:1px solid rgba(255,255,255,.15);
      background: rgba(255,255,255,.10);
      /* backdrop-filter removed for performance */
      text-decoration:none;
      color: var(--text);
      transition: transform .08s ease, background .2s ease, border-color .2s ease;
      box-shadow: 0 4px 16px rgba(0,0,0,.2);
    }
    .nav a:hover{
      transform: translateY(-1px); 
      background: rgba(255,255,255,.10); 
      border-color: rgba(255,255,255,.22);
    }
    .nav .left{display:flex; align-items:center; gap:12px}
    .nav .ico{font-size:20px}
    .nav .ico img{width:24px; height:24px; display:block}
    .btn .ico img{width:20px; height:20px; display:block}
    .nav .pill{
      font-size:12px;
      padding:6px 10px;
      border-radius: 999px;
      border:1px solid rgba(255,255,255,.18);
      color: var(--muted);
      background: rgba(0,0,0,.35);
    }

    .sidebar .cta{
      margin-top:14px;
      display:grid;
      gap:10px;
    }
    .btn{
      border:1px solid rgba(255,255,255,.15);
      background: rgba(255,255,255,.08);
      color: var(--text);
      padding:14px 14px;
      border-radius: 16px;
      cursor:pointer;
      display:flex; align-items:center; justify-content:space-between;
      text-decoration:none;
      transition: transform .08s ease, background .2s ease, border-color .2s ease;
      box-shadow: 0 4px 16px rgba(0,0,0,.2);
    }
    .btn:hover{
      transform: translateY(-1px); 
      background: rgba(255,255,255,.12); 
      border-color: rgba(255,255,255,.22);
    }
    .btn.primary{
      background: linear-gradient(135deg, rgba(96,165,250,.25), rgba(52,211,153,.18));
      border-color: rgba(96,165,250,.3);
      box-shadow: 0 4px 16px rgba(96,165,250,.2);
    }
    .btn .ico{font-size:18px}

    .main{
      padding:18px;
    }
    .top{
      display:flex; gap:12px; align-items:center;
      padding:14px;
      border-radius: var(--radius);
      border:1px solid rgba(255,255,255,.15);
      background: rgba(255,255,255,.08);
      /* backdrop-filter removed for performance */
      /* transform removed */
      box-shadow: 0 8px 32px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.1);
    }
    .search{
      flex:1;
      display:flex; gap:10px; align-items:center;
      padding:12px 12px;
      border-radius: 16px;
      border:1px solid rgba(255,255,255,.15);
      background: rgba(0,0,0,.35);
      /* backdrop-filter removed for performance */
      box-shadow: inset 0 2px 8px rgba(0,0,0,.2);
    }
    .search input{
      width:100%; border:0; outline:none;
      background: transparent; color: var(--text);
      font-size:14px;
    }
    .seg{
      display:flex; gap:8px; flex-wrap:wrap;
    }
    .chip{
      padding:10px 12px;
      border-radius: 999px;
      border:1px solid rgba(255,255,255,.15);
      background: rgba(255,255,255,.10);
      color: rgba(255,255,255,.80);
      cursor:pointer;
      user-select:none;
      transition: transform .08s ease, background .15s ease, border-color .15s ease;
      box-shadow: 0 2px 8px rgba(0,0,0,.15);
    }
    .chip:hover{
      transform: translateY(-1px);
      background: rgba(255,255,255,.12);
      border-color: rgba(255,255,255,.25);
    }
    .chip.on{
      border-color: rgba(96,165,250,.4); 
      background: rgba(96,165,250,.15);
      box-shadow: 0 4px 12px rgba(96,165,250,.25);
    }
    .content{
      margin-top:14px;
      display:grid;
      grid-template-columns: 1fr;
      gap:14px;
    }

    .card{
      border:1px solid rgba(255,255,255,.15);
      border-radius: var(--radius);
      background: rgba(255,255,255,.08);
      /* backdrop-filter removed for performance */
      /* transform removed */
      box-shadow: 0 8px 32px rgba(0,0,0,.3), inset 0 1px 0 rgba(255,255,255,.1);
      overflow:hidden;
    }
    .card .head{
      padding:14px 14px;
      display:flex; justify-content:space-between; align-items:center;
      border-bottom: 1px solid rgba(255,255,255,.12);
      background: rgba(255,255,255,.03);
    }
    .title{
      display:flex; align-items:baseline; gap:10px;
    }
    .title strong{font-size:15px}
    .title span{font-size:12px; color:var(--muted)}
    .actions{
      display:flex; gap:10px; flex-wrap:wrap;
    }
    .small{
      padding:10px 12px;
      border-radius: 14px;
      border:1px solid rgba(255,255,255,.15);
      background: rgba(0,0,0,.35);
      color: rgba(255,255,255,.86);
      cursor:pointer;
      user-select:none;
      box-shadow: 0 2px 8px rgba(0,0,0,.15);
      transition: transform .08s ease, background .15s ease;
    }
    .small:hover{
      transform: translateY(-1px);
      background: rgba(0,0,0,.35);
      border-color: rgba(255,255,255,.22);
    }
    .grid{
      padding:14px;
      display:grid;
      grid-template-columns: repeat(7, 1fr);
      gap:10px;
    }
    @media (max-width: 1200px){ .grid{grid-template-columns: repeat(6, 1fr)} }
    @media (max-width: 980px){ .grid{grid-template-columns: repeat(4, 1fr)} }
    @media (max-width: 560px){ .grid{grid-template-columns: repeat(2, 1fr)} }

    .item{
      border-radius: 16px;
      border:1px solid rgba(255,255,255,.15);
      background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04));
      /* backdrop-filter removed for performance */
      overflow:hidden;
      cursor:pointer;
      transition: transform .08s ease, border-color .15s ease;
      position:relative;
      min-height: 170px;
      text-decoration: none;
      color: var(--text);
      box-shadow: 0 4px 16px rgba(0,0,0,.2);
    }
    .item:hover{
      transform: translateY(-2px); 
      border-color: rgba(255,255,255,.25);
      box-shadow: 0 8px 24px rgba(0,0,0,.3);
    }
    .thumb{
      height: 380px;
      background:
        radial-gradient(280px 160px at 30% 20%, rgba(255,255,255,.18), transparent 55%),
        linear-gradient(135deg, rgba(96,165,250,.22), rgba(251,191,36,.10));
      background-size: cover;
      background-position: center;
    }
    .meta{
      padding:10px 10px 12px;
      background: rgba(0,0,0,.35);
      /* backdrop-filter removed for performance */
    }
    .meta strong{
      display:block;
      font-size:13px;
      white-space:nowrap;
      overflow:hidden;
      text-overflow: ellipsis;
    }
    .meta span{
      display:block;
      font-size:12px;
      color: var(--muted);
      margin-top:3px;
      white-space:nowrap;
      overflow:hidden;
      text-overflow: ellipsis;
    }
    .flag{
      position:absolute;
      top:10px; left:10px;
      padding:6px 10px;
      border-radius:999px;
      border:1px solid rgba(255,255,255,.18);
      background: rgba(0,0,0,.55);
      /* backdrop-filter removed for performance */
      font-size:11px;
      color: rgba(255,255,255,.90);
      box-shadow: 0 2px 8px rgba(0,0,0,.25);
    }
    .flag.video{border-color: rgba(96,165,250,.35); background: rgba(96,165,250,.2)}
    .flag.audio{border-color: rgba(52,211,153,.35); background: rgba(52,211,153,.2)}
    .flag.pdf{border-color: rgba(220,38,38,.35); background: rgba(220,38,38,.2)}
    .flag.epub{border-color: rgba(99,102,241,.35); background: rgba(99,102,241,.2)}
    .flag.picture{border-color: rgba(251,191,36,.35); background: rgba(251,191,36,.2)}
    .flag.other{border-color: rgba(251,191,36,.35); background: rgba(251,191,36,.2)}
    .corner{
      position:absolute;
      top:10px; right:10px;
      width:36px; height:36px;
      border-radius: 14px;
      border:1px solid rgba(255,255,255,.18);
      background: rgba(0,0,0,.55);
      /* backdrop-filter removed for performance */
      display:flex; align-items:center; justify-content:center;
      font-size:16px;
      box-shadow: 0 2px 8px rgba(0,0,0,.25);
    }
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }
    .footerbar{
      margin-top:14px;
      display:grid;
      grid-template-columns: repeat(5, 1fr);
      gap:12px;
    }
    @media (max-width: 1400px){ .footerbar{grid-template-columns: repeat(3, 1fr)} }
    @media (max-width: 980px){ .footerbar{grid-template-columns: 1fr} }
    .mini{
      border:1px solid rgba(255,255,255,.10);
      border-radius: var(--radius);
      background: rgba(255,255,255,.08);
      box-shadow: var(--shadow);
      padding:14px;
    }
    .mini .row{display:flex; align-items:center; justify-content:space-between}
    .mini .k{font-size:12px; color:var(--muted)}
    .mini .v{font-size:18px}
    .progress-bar {
      width: 100%;
      height: 8px;
      background: rgba(255,255,255,.08);
      border-radius: 999px;
      margin-top: 8px;
      overflow: hidden;
    }
    .progress-fill {
      height: 100%;
      background: linear-gradient(90deg, var(--accent), var(--accent2));
      border-radius: 999px;
      transition: width 0.3s ease;
    }
    .status-panel {
      margin-top: 14px;
      border:1px solid rgba(255,255,255,.10);
      border-radius: var(--radius);
      background: rgba(255,255,255,.08);
      box-shadow: var(--shadow);
      padding:18px;
    }
    .status-header {
      display:flex;
      justify-content:space-between;
      align-items:center;
      margin-bottom:12px;
    }
    .status-title {
      font-size:14px;
      font-weight:600;
      color: var(--text);
    }
    .status-ok {
      font-size:12px;
      padding:6px 12px;
      border-radius:8px;
      background: rgba(52,211,153,.15);
      color: var(--accent2);
      border:1px solid rgba(52,211,153,.25);
    }
    .theme-switcher {
      margin-top: 14px;
      border:1px solid rgba(255,255,255,.10);
      border-radius: var(--radius);
      background: rgba(255,255,255,.08);
      box-shadow: var(--shadow);
      padding:18px;
    }
    .theme-title {
      font-size:14px;
      font-weight:600;
      color: var(--text);
      margin-bottom:12px;
    }
    .theme-buttons {
      display:grid;
      grid-template-columns: repeat(2, 1fr);
      gap:8px;
    }
    .theme-btn {
      padding:10px;
      border-radius:12px;
      border:1px solid rgba(255,255,255,.12);
      background: rgba(255,255,255,.04);
      cursor:pointer;
      text-align:center;
      font-size:12px;
      transition: all 0.2s ease;
      display:flex;
      flex-direction:column;
      align-items:center;
      gap:6px;
    }
    .theme-btn:hover {
      background: rgba(255,255,255,.08);
      border-color: rgba(255,255,255,.20);
      transform: translateY(-1px);
    }
    .theme-btn.active {
      border-color: var(--accent);
      background: rgba(255,255,255,.12);
    }
    .theme-color {
      width:32px;
      height:32px;
      border-radius:8px;
      border:2px solid rgba(255,255,255,.20);
    }
    .theme-color.green { background: linear-gradient(135deg, #34d399, #22c55e); }
    .theme-color.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
    .theme-color.purple { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
    .theme-color.red { background: linear-gradient(135deg, #ef4444, #dc2626); }
    
    /* User Panel */
    .user-panel {
      margin-top: auto;
      padding: 16px;
      border-top: 1px solid rgba(255,255,255,0.1);
    }
    .user-info {
      display: flex;
      align-items: center;
      gap: 12px;
      margin-bottom: 12px;
    }
    .user-avatar {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background: linear-gradient(135deg, var(--accent), #22c55e);
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: 700;
      font-size: 18px;
      color: #0d1410;
    }
    .user-details {
      flex: 1;
      min-width: 0;
    }
    .user-name {
      font-weight: 600;
      font-size: 14px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .user-role {
      font-size: 12px;
      color: rgba(255,255,255,0.5);
      text-transform: capitalize;
    }
    .logout-btn {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      width: 100%;
      padding: 10px;
      background: rgba(239,68,68,0.15);
      border: 1px solid rgba(239,68,68,0.3);
      border-radius: 8px;
      color: #ef4444;
      text-decoration: none;
      font-weight: 600;
      font-size: 14px;
      transition: all 0.2s;
    }
    .logout-btn:hover {
      background: rgba(239,68,68,0.25);
      border-color: rgba(239,68,68,0.5);
      transform: translateY(-1px);
    }
  </style>
</head>
<body>
  <div class="app">
    <aside class="sidebar">
      <div class="brand">
        <div>
          <strong>NexusStack BETA v0.7</strong>
          <span>PowerShell Media Manager</span>
        </div>
      </div>

      <nav class="nav">
        <a href="/">
          <div class="left"><span class="ico"><img src="/menu/home.png" alt="Home"></span><span>Home</span></div>
          <span class="pill">Now</span>
        </a>
        <a href="/videos">
          <div class="left"><span class="ico"><img src="/menu/movie.png" alt="Videos"></span><span>Videos</span></div>
          <span class="pill">$($stats.TotalVideos)</span>
        </a>
        <a href="/music">
          <div class="left"><span class="ico"><img src="/menu/music.png" alt="Music"></span><span>Music</span></div>
          <span class="pill">$($stats.TotalMusic)</span>
        </a>
        <a href="/radio">
          <div class="left"><span class="ico"><img src="/menu/radio.png" alt="Radio"></span><span>Radio</span></div>
          <span class="pill" style="background: rgba(52,211,153,0.2); color: var(--accent); border-color: var(--accent);">LIVE</span>
        </a>
        <a href="/tv">
          <div class="left"><span class="ico"><img src="/menu/tv.png" alt="TV"></span><span>Internet TV</span></div>
          <span class="pill" style="background: rgba(96,165,250,0.2); color: #60a5fa; border-color: #60a5fa;">LIVE</span>
        </a>
        <a href="/pictures">
          <div class="left"><span class="ico"><img src="/menu/pictures.png" alt="Pictures"></span><span>Pictures</span></div>
          <span class="pill">$($stats.TotalPictures)</span>
        </a>
        <a href="/pdfs">
          <div class="left"><span class="ico"><img src="/menu/pdf.png" alt="Documents"></span><span>PDFs/ePUBS</span></div>
          <span class="pill">$($stats.TotalPDFs)</span>
        </a>
        <a href="/analysis">
          <div class="left"><span class="ico"><img src="/menu/analysis.png" alt="Analysis"></span><span>Analysis</span></div>
          <span class="pill">Smart</span>
        </a>
        $settingsLink
      </nav>

      <div class="cta">
        <a class="btn" href="/rescan"><span class="ico"><img src="/menu/scan.png" alt="Rescan"></span><span>Rescan Folders</span><span>➜</span></a>
        <a class="btn primary" href="/fetch-posters"><span class="ico"><img src="/menu/fetch.png" alt="Fetch"></span><span>Fetch Posters</span><span>➜</span></a>
      </div>
      
      <div class="status-panel">
        <div class="status-header">
          <div class="status-title">Status</div>
          <div class="status-ok">OK</div>
        </div>
        <div class="k">Storage used</div>
        <div class="v" style="margin-top:4px;">$($stats.TotalSizeGB) GB</div>
        <div class="k" style="margin-top:12px; line-height:1.6;">
          Video $($stats.VideoSizeGB) GB<br/>
          Music $($stats.MusicSizeGB) GB<br/>
          Pictures $($stats.PicturesSizeGB) GB<br/>
          PDFs $($stats.PDFSizeGB) GB
        </div>
      </div>
      
      <div class="theme-switcher">
        <div class="theme-title">Color Theme</div>
        <div class="theme-buttons">
          <div class="theme-btn active" onclick="setTheme('green')" id="theme-green">
            <div class="theme-color green"></div>
            <span>Green</span>
          </div>
          <div class="theme-btn" onclick="setTheme('blue')" id="theme-blue">
            <div class="theme-color blue"></div>
            <span>Blue</span>
          </div>
          <div class="theme-btn" onclick="setTheme('purple')" id="theme-purple">
            <div class="theme-color purple"></div>
            <span>Purple</span>
          </div>
          <div class="theme-btn" onclick="setTheme('red')" id="theme-red">
            <div class="theme-color red"></div>
            <span>Red</span>
          </div>
        </div>
      </div>
      
      <!-- User Info & Logout -->
      $(if ($UserSession) {
        $avatarContent = if ($UserSession.AvatarPath) {
            "<img src='/avatar/$($UserSession.AvatarPath)' style='width:100%; height:100%; object-fit:cover; border-radius:50%;'>"
        } else {
            $UserSession.DisplayName.Substring(0,1).ToUpper()
        }
        @"
      <div class="user-panel">
        <div class="user-info">
          <div class="user-avatar">$avatarContent</div>
          <div class="user-details">
            <div class="user-name">$($UserSession.DisplayName)</div>
            <div class="user-role">$($UserSession.UserType)</div>
          </div>
        </div>
        <a href="/logout" class="logout-btn" onclick="return confirm('Are you sure you want to logout?')">
          <span>🚪</span> Logout
        </a>
      </div>
"@
      })
    </aside>

    <main class="main">
      $Content
    </main>
  </div>
  <script>
    // Load saved theme on page load
    (function() {
      const savedTheme = localStorage.getItem('psmedia-theme') || 'green';
      setTheme(savedTheme);
    })();
    
    function setTheme(theme) {
      // Remove all theme classes
      document.body.classList.remove('theme-blue', 'theme-purple', 'theme-red');
      
      // Add new theme class (green is default, no class needed)
      if (theme !== 'green') {
        document.body.classList.add('theme-' + theme);
      }
      
      // Update active button
      document.querySelectorAll('.theme-btn').forEach(btn => {
        btn.classList.remove('active');
      });
      const btn = document.getElementById('theme-' + theme);
      if (btn) btn.classList.add('active');
      
      // Save to localStorage
      localStorage.setItem('psmedia-theme', theme);
    }
  </script>
</body>
</html>
"@
}

function Get-HomePage {
    param(
        [string[]]$SortParams = @('rating', 'date')
    )
    
    # Build ORDER BY clause based on selected sort options
    $orderByParts = @()
    if ($SortParams -contains 'rating') {
        $orderByParts += 'rating DESC'
    }
    if ($SortParams -contains 'date') {
        $orderByParts += 'date_added DESC'
    }
    
    # Default to rating if nothing selected
    if ($orderByParts.Count -eq 0) {
        $orderByParts += 'date_added DESC'
    }
    
    $orderByClause = $orderByParts -join ', '
    
    # Set checkbox states based on parameters
    $bestRatedChecked = if ($SortParams -contains 'rating') { 'checked' } else { '' }
    $newestChecked = if ($SortParams -contains 'date') { 'checked' } else { '' }
    
    # Get all available genres from the database
    $genreQuery = @"
SELECT DISTINCT genre 
FROM videos 
WHERE genre IS NOT NULL AND genre != ''
ORDER BY genre
"@
    
    $genreResults = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query $genreQuery
    
    # Build an array of unique FIRST genres only (handle comma-separated genres)
    $allGenres = @()
    foreach ($result in $genreResults) {
        if ($result.genre) {
            # Split by comma and take ONLY the first genre
            $firstGenre = ($result.genre -split ',')[0].Trim()
            if ($firstGenre -and $allGenres -notcontains $firstGenre) {
                $allGenres += $firstGenre
            }
        }
    }
    
    # Sort genres alphabetically
    $allGenres = $allGenres | Sort-Object
    
    # Build genre rows HTML
    $genreRowsHtml = ""
    foreach ($genre in $allGenres) {
        # Get videos where this genre is the FIRST listed genre (limit 30)
        # Using LIKE to match the beginning of the genre field
        $genreVideos = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
SELECT * FROM videos 
WHERE genre LIKE @genreStart OR genre = @genreExact
ORDER BY $orderByClause
LIMIT 30
"@ -SqlParameters @{ 
    genreStart = "$genre,%"  # Matches "Action, Drama" but not "Drama, Action"
    genreExact = $genre      # Matches exact "Action"
}
        
        if ($genreVideos) {
            $genreItemsHtml = ""
            $videoCount = @($genreVideos).Count
            
            foreach ($video in $genreVideos) {
                $posterUrl = if ($video.poster_url) { "/poster/$($video.poster_url)" } else { "" }
                $backdropUrl = if ($video.backdrop_url) { "/poster/$($video.backdrop_url)" } else { $posterUrl }
                
                $imageUrl = if ($backdropUrl) { $backdropUrl } elseif ($posterUrl) { $posterUrl } else { "" }
                $imageStyle = if ($imageUrl) { "background-image: url('$imageUrl');" } else { "background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(52,211,153,0.2));" }
                
                $mediaTypeBadge = if ($video.media_type -eq 'tv') { "📺" } else { "🎬" }
                
                $genreItemsHtml += @"
<a href="/movie/$($video.id)" class="genre-item">
    <div class="genre-poster" style="$imageStyle">
        <div class="genre-overlay">
            <div class="genre-title">$($video.title)</div>
        </div>
        <div class="genre-badge">$mediaTypeBadge</div>
    </div>
</a>
"@
            }
            
            # Show arrows only if more than 5 items
            $showArrows = $videoCount -gt 5
            $arrowsHtml = if ($showArrows) {
                @"
        <button class="scroll-btn scroll-left" onclick="scrollGenre(this, -1)">‹</button>
        <button class="scroll-btn scroll-right" onclick="scrollGenre(this, 1)">›</button>
"@
            } else {
                ""
            }
            
            $genreRowsHtml += @"
<section class="genre-row">
    <div class="genre-header">
        <h2 class="genre-name">$genre</h2>
        <span class="genre-count">$videoCount titles</span>
    </div>
    <div class="genre-scroll-container">
        $arrowsHtml
        <div class="genre-items">
            $genreItemsHtml
        </div>
    </div>
</section>
"@
        }
    }
    
    $content = @"
<style>
.home-header {
    padding: 20px 40px;
    background: rgba(0,0,0,0.3);
    border-bottom: 1px solid rgba(255,255,255,0.1);
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
}

.search-container {
    flex: 1;
    max-width: 600px;
}

.search-input {
    width: 100%;
    padding: 12px 20px 12px 45px;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 25px;
    color: var(--text);
    font-size: 15px;
    transition: all 0.3s ease;
}

.search-input:focus {
    outline: none;
    background: rgba(255,255,255,0.12);
    border-color: var(--accent);
}

.search-icon {
    position: absolute;
    left: 18px;
    top: 50%;
    transform: translateY(-50%);
    opacity: 0.6;
    font-size: 18px;
}

.filter-buttons {
    display: flex;
    gap: 10px;
}

.filter-btn {
    padding: 10px 20px;
    background: rgba(255,255,255,0.08);
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 20px;
    color: var(--text);
    cursor: pointer;
    transition: all 0.3s ease;
    font-size: 14px;
    font-weight: 600;
}

.filter-btn:hover {
    background: rgba(255,255,255,0.15);
    transform: translateY(-2px);
}

.filter-btn.active {
    background: var(--accent);
    color: #000;
    border-color: var(--accent);
}

.sort-toggles {
    display: flex;
    gap: 15px;
    padding: 8px 16px;
    background: rgba(255,255,255,0.05);
    border-radius: 25px;
    border: 1px solid rgba(255,255,255,0.1);
}

.toggle-label {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    user-select: none;
}

.toggle-label input[type="checkbox"] {
    width: 18px;
    height: 18px;
    cursor: pointer;
    accent-color: var(--accent);
}

.toggle-text {
    color: var(--text);
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
}

.genre-row {
    margin-bottom: 50px;
    padding: 0 40px;
}

.genre-header {
    margin-bottom: 20px;
    display: flex;
    align-items: baseline;
    gap: 15px;
}

.genre-name {
    font-size: 22px;
    font-weight: 700;
    color: var(--text);
    margin: 0;
}

.genre-count {
    font-size: 14px;
    color: rgba(255,255,255,0.5);
}

.genre-scroll-container {
    position: relative;
    display: flex;
    align-items: center;
    overflow: hidden;
    max-width: calc(100vw - 80px); /* Account for padding */
}

.genre-items {
    display: flex;
    gap: 12px;
    overflow-x: scroll;
    overflow-y: hidden;
    scroll-behavior: smooth;
    scrollbar-width: none;
    -ms-overflow-style: none;
    padding: 10px 0;
    /* Show exactly 5 items: (280px * 5) + (12px gap * 4) = 1448px */
    max-width: 1448px;
}

.genre-items::-webkit-scrollbar {
    display: none;
}

.genre-item {
    flex: 0 0 280px;
    text-decoration: none;
    transition: transform 0.3s ease;
}

.genre-item:hover {
    transform: scale(1.05);
    z-index: 10;
}

.genre-poster {
    width: 280px;
    height: 158px;
    background-size: cover;
    background-position: center;
    border-radius: 8px;
    position: relative;
    overflow: hidden;
    box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}

.genre-overlay {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 40px 15px 15px;
    background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.7) 50%, transparent 100%);
    opacity: 1;
    transition: opacity 0.3s ease;
}

.genre-item:hover .genre-overlay {
    opacity: 1;
}

.genre-title {
    color: white;
    font-size: 15px;
    font-weight: 700;
    text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
    line-height: 1.3;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.genre-badge {
    position: absolute;
    top: 10px;
    right: 10px;
    font-size: 20px;
    filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.8));
}

.scroll-btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 50px;
    height: 100px;
    background: rgba(0,0,0,0.8);
    border: none;
    color: white;
    font-size: 40px;
    cursor: pointer;
    z-index: 100;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.7;
}

.scroll-btn:hover {
    background: rgba(0,0,0,0.95);
    opacity: 1;
}

.scroll-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.scroll-left {
    left: 0;
    border-radius: 0 8px 8px 0;
}

.scroll-right {
    /* Position over the right side of 5th (last visible) item */
    /* 5 items width + 4 gaps - button width = (280*5) + (12*4) - 50 = 1398px */
    left: 1398px;
    border-radius: 8px 0 0 8px;
}

.genres-container {
    padding: 40px 0;
    min-height: calc(100vh - 200px);
}

@media (max-width: 1024px) {
    .genre-item {
        flex: 0 0 220px;
    }
    
    .genre-poster {
        width: 220px;
        height: 124px;
    }
    
    .genre-items {
        /* Show 3 items on tablets: (220px * 3) + (12px gap * 2) = 684px */
        max-width: 684px;
    }
    
    .scroll-right {
        /* Position over right side of 3rd item on tablets: (220*3) + (12*2) - 50 = 634px */
        left: 634px;
    }
    
    .genre-row {
        padding: 0 20px;
    }
    
    .home-header {
        padding: 15px 20px;
        flex-direction: column;
    }
    
    .search-container {
        max-width: 100%;
    }
}
</style>

<div class="home-header">
    <div class="search-container" style="position: relative;">
        <span class="search-icon">🔎</span>
        <input class="search-input" id="searchInput" placeholder="Search movies, TV shows, music..." />
    </div>
    <div style="display: flex; gap: 20px; align-items: center;">
        <div class="sort-toggles">
            <label class="toggle-label">
                <input type="checkbox" id="sortBestRated" $bestRatedChecked onchange="updateSort()">
                <span class="toggle-text">⭐ Best Rated</span>
            </label>
            <label class="toggle-label">
                <input type="checkbox" id="sortNewest" $newestChecked onchange="updateSort()">
                <span class="toggle-text">🕒 Newest</span>
            </label>
        </div>
        <div class="filter-buttons">
            <button class="filter-btn active" onclick="filterContent('all')">All</button>
            <button class="filter-btn" onclick="filterContent('video')">Videos</button>
            <button class="filter-btn" onclick="filterContent('audio')">Music</button>
        </div>
    </div>
</div>

<div class="genres-container" id="genresContainer">
    $genreRowsHtml
</div>

<script>
function scrollGenre(button, direction) {
    const container = button.parentElement.querySelector('.genre-items');
    // Scroll by 5 items: (280px per item + 12px gap) * 5 = 1460px
    const scrollAmount = (280 + 12) * 5;
    container.scrollBy({
        left: direction * scrollAmount,
        behavior: 'smooth'
    });
}

function updateSort() {
    const bestRated = document.getElementById('sortBestRated').checked;
    const newest = document.getElementById('sortNewest').checked;
    
    // Build query parameters
    let params = [];
    if (bestRated) params.push('sort=rating');
    if (newest) params.push('sort=date');
    
    // Reload page with sort parameters
    const queryString = params.length > 0 ? '?' + params.join('&') : '';
    window.location.href = '/' + queryString;
}

function filterContent(type) {
    // Update active button
    document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
    event.target.classList.add('active');
    
    if (type === 'video') {
        window.location.href = '/videos';
    } else if (type === 'audio') {
        window.location.href = '/music';
    }
    // 'all' stays on home page
}

document.getElementById('searchInput').addEventListener('keyup', function(e) {
    if (e.key === 'Enter') {
        window.location.href = '/search?q=' + encodeURIComponent(this.value);
    }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Home"
}

function Get-VideosPage {
    param(
        [string]$Filter = "all"
    )
    
    # Get continue watching items from user database
    $continueWatchingHtml = ""
    $continueWatchingCount = 0
    if ($Global:CurrentUser -and $Global:CurrentUser.Username) {
        $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
        if (Test-Path $userDbPath) {
            try {
                # Get videos that are in progress (not completed, have position > 30 seconds, < 90% watched)
                $inProgress = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT * FROM video_progress 
WHERE completed = 0 
  AND last_position_seconds > 30 
  AND percent_watched < 90
  AND percent_watched > 0
ORDER BY last_watched DESC 
LIMIT 10
"@
                if ($inProgress) {
                    $continueWatchingCount = @($inProgress).Count
                    Write-Host "[i] Found $continueWatchingCount videos in progress" -ForegroundColor Cyan
                    foreach ($prog in $inProgress) {
                        # Get video details from main database
                        $videoDetails = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT * FROM videos WHERE id = @id OR filepath = @path LIMIT 1" -SqlParameters @{ id = $prog.video_id; path = $prog.video_path }
                        
                        if ($videoDetails) {
                            $posterStyle = if ($videoDetails.poster_url) { "background-image: url('/poster/$($videoDetails.poster_url)');" } else { "" }
                            $progressPercent = [math]::Round($prog.percent_watched, 0)
                            $remainingMins = if ($prog.duration_seconds -and $prog.last_position_seconds) {
                                [math]::Round(($prog.duration_seconds - $prog.last_position_seconds) / 60, 0)
                            } else { "?" }
                            
                            $continueWatchingHtml += @"
<a class="continue-item" href="/movie/$($videoDetails.id)?resume=1">
    <div class="continue-poster" style="$posterStyle">
        <div class="continue-progress-bar">
            <div class="continue-progress-fill" style="width: ${progressPercent}%"></div>
        </div>
        <div class="continue-play-icon">▶</div>
    </div>
    <div class="continue-info">
        <div class="continue-title">$($videoDetails.title)</div>
        <div class="continue-meta">${remainingMins} min left</div>
    </div>
</a>
"@
                        }
                    }
                }
            } catch {
                Write-Host "[!] Continue Watching error: $_" -ForegroundColor Yellow
            }
        }
    }
    
    # Get videos based on filter
    $query = switch ($Filter) {
        "movies" {
            "SELECT * FROM videos WHERE media_type = 'movie' OR media_type IS NULL ORDER BY date_added DESC"
        }
        "tvshows" {
            "SELECT * FROM videos WHERE media_type = 'tv' ORDER BY date_added DESC"
        }
        "recent" {
            "SELECT * FROM videos ORDER BY date_added DESC LIMIT 50"
        }
        default {  # "all"
            "SELECT * FROM videos ORDER BY date_added DESC"
        }
    }
    
    $allVideos = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query $query
    
    # Count movies and TV shows
    $movieCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE media_type = 'movie' OR media_type IS NULL").count
    $tvCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE media_type = 'tv'").count
    
    $videoItems = ""
    foreach ($video in $allVideos) {
        $size = [math]::Round($video.size_bytes / 1GB, 2)
        $encodedPath = [System.Web.HttpUtility]::UrlEncode($video.filepath)
        
        # Check if poster exists
        $posterStyle = if ($video.poster_url -and -not [string]::IsNullOrWhiteSpace($video.poster_url)) {
            "background-image: url('/poster/$($video.poster_url)');"
        } else {
            ""
        }
        
        # Media type badge
        $mediaTypeBadge = if ($video.media_type -eq 'tv') {
            "<div class='flag tv'>TV Show</div>"
        } else {
            "<div class='flag video'>Movie</div>"
        }
        
        $videoItems += @"
<a class="item" href="/movie/$($video.id)">
  <div class="thumb" style="$posterStyle"></div>
  $mediaTypeBadge
  <div class="corner">▶</div>
  <div class="meta">
    <strong>$($video.title)</strong>
    <span>$($video.format) • ${size}GB</span>
  </div>
</a>
"@
    }
    
    # Determine which chip is active
    $allActive = if ($Filter -eq "all") { "on" } else { "" }
    $moviesActive = if ($Filter -eq "movies") { "on" } else { "" }
    $tvActive = if ($Filter -eq "tvshows") { "on" } else { "" }
    
    # Determine title based on filter
    $titleText = switch ($Filter) {
        "movies" { "Movies" }
        "tvshows" { "TV Shows" }
        "recent" { "Recently Added" }
        default { "All Videos" }
    }
    
    # Continue Watching section HTML
    $continueWatchingSection = ""
    if ($continueWatchingHtml) {
        $continueWatchingSection = @"
<section class="card continue-watching-section">
    <div class="head">
        <div class="title">
            <strong>▶ Continue Watching</strong>
            <span>$continueWatchingCount items</span>
        </div>
    </div>
    <div class="continue-watching-grid">
        $continueWatchingHtml
    </div>
</section>
"@
    }
    
    $content = @"
<style>
/* Continue Watching Styles */
.continue-watching-section {
    margin-bottom: 24px;
}
.continue-watching-grid {
    display: flex;
    gap: 16px;
    overflow-x: auto;
    padding: 16px 0;
    scrollbar-width: thin;
}
.continue-watching-grid::-webkit-scrollbar {
    height: 6px;
}
.continue-watching-grid::-webkit-scrollbar-thumb {
    background: rgba(255,255,255,0.2);
    border-radius: 3px;
}
.continue-item {
    flex: 0 0 auto;
    width: 180px;
    text-decoration: none;
    color: inherit;
    transition: transform 0.2s;
}
.continue-item:hover {
    transform: scale(1.05);
}
.continue-poster {
    width: 180px;
    height: 100px;
    background: rgba(255,255,255,0.1);
    background-size: cover;
    background-position: center top;
    border-radius: 8px;
    position: relative;
    overflow: hidden;
}
.continue-progress-bar {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 4px;
    background: rgba(0,0,0,0.5);
}
.continue-progress-fill {
    height: 100%;
    background: linear-gradient(90deg, #ef4444, #f97316);
    transition: width 0.3s;
}
.continue-play-icon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 40px;
    height: 40px;
    background: rgba(0,0,0,0.7);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.2s;
}
.continue-item:hover .continue-play-icon {
    opacity: 1;
}
.continue-info {
    padding: 8px 4px;
}
.continue-title {
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.continue-meta {
    font-size: 11px;
    color: rgba(255,255,255,0.5);
    margin-top: 2px;
}

/* TV Show flag color */
.flag.tv {
    background: #8b5cf6;
}

/* Type toggle button */
.type-toggle {
    display: inline-flex;
    background: rgba(255,255,255,0.05);
    border-radius: 8px;
    overflow: hidden;
    border: 1px solid rgba(255,255,255,0.1);
}
.type-toggle .toggle-btn {
    padding: 8px 16px;
    font-size: 13px;
    cursor: pointer;
    transition: all 0.2s;
    border: none;
    background: transparent;
    color: rgba(255,255,255,0.6);
}
.type-toggle .toggle-btn:hover {
    background: rgba(255,255,255,0.05);
    color: rgba(255,255,255,0.9);
}
.type-toggle .toggle-btn.active {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    color: white;
}
.type-toggle .toggle-btn:first-child {
    border-radius: 7px 0 0 7px;
}
.type-toggle .toggle-btn:last-child {
    border-radius: 0 7px 7px 0;
}
</style>

<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search videos…" id="searchInput" />
  </div>
  <div class="seg">
    <div class="type-toggle">
        <button class="toggle-btn $allActive" onclick="window.location.href='/videos?filter=all'">All ($($movieCount + $tvCount))</button>
        <button class="toggle-btn $moviesActive" onclick="window.location.href='/videos?filter=movies'">🎬 Movies ($movieCount)</button>
        <button class="toggle-btn $tvActive" onclick="window.location.href='/videos?filter=tvshows'">📺 TV Shows ($tvCount)</button>
    </div>
    <div class="chip" onclick="window.location.href='/videos/genres'" style="margin-left: 12px;">🎭 By Genre</div>
  </div>
</div>

<div class="content">
  $continueWatchingSection
  
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$titleText</strong>
        <span>$($allVideos.Count) items • $($Global:MediaStats.VideoSizeGB) GB</span>
      </div>
      <div class="actions">
        <a href="/refresh-genres" class="small" style="text-decoration:none; color:inherit;">🎭 Refresh Genres</a>
        <a href="/fetch-posters" class="small" style="text-decoration:none; color:inherit;">🎨 Fetch Posters</a>
        <div class="small">🔄 Refresh</div>
      </div>
    </div>
    <div class="grid">
      $videoItems
    </div>
  </section>
</div>

<script>
document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=videos';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Videos"
}

function Get-GenreListPage {
    # Get all unique genres from database
    $allVideos = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT genre FROM videos WHERE genre IS NOT NULL AND genre != ''"
    
    # Extract and count all genres
    $genreCount = @{}
    foreach ($video in $allVideos) {
        if ($video.genre) {
            # Split by comma since movies can have multiple genres
            $genres = $video.genre -split ',' | ForEach-Object { $_.Trim() }
            foreach ($genre in $genres) {
                if ($genre) {
                    if ($genreCount.ContainsKey($genre)) {
                        $genreCount[$genre]++
                    } else {
                        $genreCount[$genre] = 1
                    }
                }
            }
        }
    }
    
    # Sort genres by count (most popular first)
    $sortedGenres = $genreCount.GetEnumerator() | Sort-Object -Property Value -Descending
    
    # Create genre cards
    $genreCards = ""
    foreach ($genreEntry in $sortedGenres) {
        $genreName = $genreEntry.Key
        $count = $genreEntry.Value
        $encodedGenre = [System.Web.HttpUtility]::UrlEncode($genreName)
        
        # Pick an emoji based on genre
        $emoji = switch -Regex ($genreName) {
            "Action"      { "💥" }
            "Adventure"   { "🗺️" }
            "Animation"   { "🎨" }
            "Comedy"      { "😂" }
            "Crime"       { "🔫" }
            "Documentary" { "📹" }
            "Drama"       { "🎭" }
            "Fantasy"     { "🧙" }
            "Horror"      { "👻" }
            "Mystery"     { "🔍" }
            "Romance"     { "💕" }
            "Sci-Fi"      { "🚀" }
            "Thriller"    { "😱" }
            "War"         { "⚔️" }
            "Western"     { "🤠" }
            "Family"      { "👨‍👩‍👧‍👦" }
            "History"     { "📜" }
            "Music"       { "🎵" }
            default       { "🎬" }
        }
        
        $genreCards += @"
<a class="genre-card" href="/videos/genre/$encodedGenre" style="text-decoration:none;">
  <div class="genre-icon">$emoji</div>
  <div class="genre-info">
    <strong style="font-size:16px; color:var(--text);">$genreName</strong>
    <span style="font-size:13px; color:var(--muted); margin-top:4px; display:block;">$count movies</span>
  </div>
</a>
"@
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search videos…" id="searchInput" />
  </div>
  <div class="seg">
    <div class="chip" onclick="window.location.href='/videos?filter=all'">All Videos</div>
    <div class="chip" onclick="window.location.href='/videos?filter=recent'">Recently Added</div>
    <div class="chip" onclick="window.location.href='/videos?filter=unwatched'">Unwatched</div>
    <div class="chip on">🎭 By Genre</div>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>Browse by Genre</strong>
        <span>$($sortedGenres.Count) genres available</span>
      </div>
      <div class="actions">
        <a href="/videos" class="small" style="text-decoration:none; color:inherit;">← Back to Videos</a>
      </div>
    </div>
    <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:14px; padding:20px;">
      $genreCards
    </div>
  </section>
</div>

<style>
.genre-card {
  padding: 20px;
  border-radius: 16px;
  border: 1px solid rgba(255,255,255,.10);
  background: rgba(255,255,255,.04);
  display: flex;
  align-items: center;
  gap: 14px;
  transition: transform .08s ease, background .2s ease, border-color .2s ease;
}
.genre-card:hover {
  transform: translateY(-2px);
  background: rgba(255,255,255,.08);
  border-color: rgba(255,255,255,.18);
}
.genre-icon {
  font-size: 32px;
  width: 50px;
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(96,165,250,.12);
  border-radius: 12px;
}
.genre-info {
  display: flex;
  flex-direction: column;
}
</style>

<script>
document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=videos';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Browse by Genre"
}

function Get-GenreVideosPage {
    param(
        [string]$Genre
    )
    
    # Get all videos with this genre
    $allVideos = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
        -Query "SELECT * FROM videos WHERE genre LIKE @genre ORDER BY title" `
        -SqlParameters @{ genre = "%$Genre%" }
    
    $videoItems = ""
    foreach ($video in $allVideos) {
        $size = [math]::Round($video.size_bytes / 1GB, 2)
        
        # Check if poster exists
        $posterStyle = if ($video.poster_url -and -not [string]::IsNullOrWhiteSpace($video.poster_url)) {
            "background-image: url('/poster/$($video.poster_url)');"
        } else {
            ""
        }
        
        # Show genre badges
        $genreBadges = ""
        if ($video.genre) {
            $genres = $video.genre -split ',' | ForEach-Object { $_.Trim() } | Select-Object -First 3
            foreach ($g in $genres) {
                $genreBadges += "<span style='display:inline-block; padding:4px 8px; background:rgba(96,165,250,.15); border:1px solid rgba(96,165,250,.3); border-radius:6px; font-size:11px; margin-right:4px; margin-top:4px;'>$g</span>"
            }
        }
        
        $videoItems += @"
<a class="item" href="/movie/$($video.id)">
  <div class="thumb" style="$posterStyle"></div>
  <div class="flag video">Video</div>
  <div class="corner">▶</div>
  <div class="meta">
    <strong>$($video.title)</strong>
    <span>$($video.format) • ${size}GB</span>
    <div style="margin-top:6px;">$genreBadges</div>
  </div>
</a>
"@
    }
    
    # Pick emoji for this genre
    $emoji = switch -Regex ($Genre) {
        "Action"      { "💥" }
        "Adventure"   { "🗺️" }
        "Animation"   { "🎨" }
        "Comedy"      { "😂" }
        "Crime"       { "🔫" }
        "Documentary" { "📹" }
        "Drama"       { "🎭" }
        "Fantasy"     { "🧙" }
        "Horror"      { "👻" }
        "Mystery"     { "🔍" }
        "Romance"     { "💕" }
        "Sci-Fi"      { "🚀" }
        "Thriller"    { "😱" }
        "War"         { "⚔️" }
        "Western"     { "🤠" }
        "Family"      { "👨‍👩‍👧‍👦" }
        "History"     { "📜" }
        "Music"       { "🎵" }
        default       { "🎬" }
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search videos…" id="searchInput" />
  </div>
  <div class="seg">
    <div class="chip" onclick="window.location.href='/videos?filter=all'">All Videos</div>
    <div class="chip" onclick="window.location.href='/videos/genres'">🎭 By Genre</div>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$emoji $Genre</strong>
        <span>$($allVideos.Count) movies</span>
      </div>
      <div class="actions">
        <a href="/videos/genres" class="small" style="text-decoration:none; color:inherit;">← All Genres</a>
        <a href="/videos" class="small" style="text-decoration:none; color:inherit;">← Videos</a>
      </div>
    </div>
    <div class="grid">
      $videoItems
    </div>
  </section>
</div>

<script>
document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=videos';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - $Genre Movies"
}

function Get-MusicPage {
    param(
        [string]$Filter = "all"
    )
    
    # Get music based on filter
    $query = switch ($Filter) {
        "artist" {
            "SELECT * FROM music ORDER BY artist, album, track_number"
        }
        "album" {
            "SELECT * FROM music ORDER BY album, track_number"
        }
        default {  # "all"
            "SELECT * FROM music ORDER BY title"
        }
    }
    
    $allMusic = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query $query
    
    # Get user's playlists count for display
    $playlistCount = 0
    if ($Global:CurrentUser -and $Global:CurrentUser.Username) {
        $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
        if (Test-Path $userDbPath) {
            try {
                $playlistCount = (Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT COUNT(*) as count FROM playlists").count
            } catch { $playlistCount = 0 }
        }
    }
    
    $musicItems = ""
    foreach ($music in $allMusic) {
        $size = [math]::Round($music.size_bytes / 1MB, 1)
        $encodedPath = [System.Web.HttpUtility]::UrlEncode($music.filepath)
        $artist = if ($music.artist) { $music.artist } else { "Unknown Artist" }
        $album = if ($music.album) { $music.album } else { "Unknown Album" }
        
        # Check for cached album art first
        $artStyle = ""
        if ($music.album_art_cached -and -not [string]::IsNullOrWhiteSpace($music.album_art_cached)) {
            $artStyle = "background-image: url('/albumart/$($music.album_art_cached)');"
        }
        elseif ($music.album_art_url -and -not [string]::IsNullOrWhiteSpace($music.album_art_url)) {
            $artStyle = "background-image: url('/poster/$($music.album_art_url)');"
        }
        
        $musicItems += @"
<a class="item music-item" href="/play-audio/$($music.id)" draggable="true" data-music-id="$($music.id)" data-title="$($music.title -replace '"', '&quot;')" data-artist="$($artist -replace '"', '&quot;')" data-album="$($album -replace '"', '&quot;')" data-path="$($music.filepath -replace '"', '&quot;')">
  <div class="thumb" style="$artStyle"></div>
  <div class="flag audio">Audio</div>
  <div class="corner">♫</div>
  <div class="meta">
    <strong>$($music.title)</strong>
    <span>$artist</span>
    <span style="font-size:11px; opacity:0.7;">$album</span>
  </div>
</a>
"@
    }
    
    # Determine which chip is active
    $allActive = if ($Filter -eq "all") { "on" } else { "" }
    $artistActive = if ($Filter -eq "artist") { "on" } else { "" }
    $albumActive = if ($Filter -eq "album") { "on" } else { "" }
    
    # Determine title based on filter
    $titleText = switch ($Filter) {
        "artist" { "Music - Sorted by Artist" }
        "album" { "Music - Sorted by Album" }
        default { "All Music" }
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search music…" id="searchInput" />
  </div>
  <div class="seg">
    <div class="chip $allActive" onclick="window.location.href='/music?filter=all'">All Music</div>
    <div class="chip $artistActive" onclick="window.location.href='/music?filter=artist'">By Artist</div>
    <div class="chip $albumActive" onclick="window.location.href='/music?filter=album'">By Album</div>
    <div class="chip" onclick="window.location.href='/playlists'" style="background: linear-gradient(135deg, rgba(139,92,246,0.3), rgba(99,102,241,0.3)); border-color: rgba(139,92,246,0.5);">📋 Playlists ($playlistCount)</div>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$titleText</strong>
        <span>$($allMusic.Count) tracks • $($Global:MediaStats.MusicSizeGB) GB</span>
      </div>
      <div class="actions">
        <a href="/play-audio/shuffle" class="small" style="text-decoration:none; color:inherit;">🔀 Shuffle Play</a>
        <a href="/playlists" class="small" style="text-decoration:none; color:inherit;">📋 My Playlists</a>
        <a href="/extract-artwork" class="small" style="text-decoration:none; color:inherit;">🎨 Extract Album Art</a>
        <div class="small">🔄 Refresh</div>
      </div>
    </div>
    <div class="grid" id="musicGrid">
      $musicItems
    </div>
  </section>
</div>

<script>
document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=music';
  }
});

// Enable drag functionality for music items
document.querySelectorAll('.music-item').forEach(function(item) {
    item.addEventListener('dragstart', function(e) {
        e.dataTransfer.setData('application/json', JSON.stringify({
            musicId: this.dataset.musicId,
            title: this.dataset.title,
            artist: this.dataset.artist,
            album: this.dataset.album,
            path: this.dataset.path
        }));
        e.dataTransfer.effectAllowed = 'copy';
        this.style.opacity = '0.5';
    });
    item.addEventListener('dragend', function(e) {
        this.style.opacity = '1';
    });
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Music"
}

function Get-PlaylistsPage {
    # Check if user is logged in
    if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
        return Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>Please log in to manage playlists</h1><p><a href='/login'>← Login</a></p></div>" -Title "Playlists"
    }
    
    $username = $Global:CurrentUser.Username
    $userDbPath = Join-Path $CONFIG.UsersDBPath "$username.db"
    
    # Ensure playlist tables exist
    try {
        Invoke-SqliteQuery -DataSource $userDbPath -Query @"
CREATE TABLE IF NOT EXISTS playlists (
    playlist_id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    description TEXT,
    created_date TEXT NOT NULL,
    modified_date TEXT NOT NULL,
    track_count INTEGER DEFAULT 0
);
"@
        Invoke-SqliteQuery -DataSource $userDbPath -Query @"
CREATE TABLE IF NOT EXISTS playlist_tracks (
    track_id INTEGER PRIMARY KEY AUTOINCREMENT,
    playlist_id INTEGER NOT NULL,
    music_id INTEGER NOT NULL,
    music_path TEXT NOT NULL,
    title TEXT,
    artist TEXT,
    album TEXT,
    track_order INTEGER NOT NULL,
    added_date TEXT NOT NULL,
    FOREIGN KEY (playlist_id) REFERENCES playlists(playlist_id) ON DELETE CASCADE
);
"@
    } catch { }
    
    # Get all playlists
    $playlists = Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT * FROM playlists ORDER BY modified_date DESC"
    $playlistCount = if ($playlists) { @($playlists).Count } else { 0 }
    
    # Build playlist cards
    $playlistItems = ""
    if ($playlists) {
        foreach ($pl in $playlists) {
            $trackCount = $pl.track_count
            $createdDate = if ($pl.created_date) { 
                try { ([datetime]$pl.created_date).ToString("MMM d, yyyy") } catch { "Unknown" }
            } else { "Unknown" }
            
            $playlistItems += @"
<a href="/playlist/$($pl.playlist_id)" class="playlist-card" data-playlist-id="$($pl.playlist_id)" ondrop="dropOnPlaylist(event, $($pl.playlist_id)); return false;" ondragover="allowDrop(event)" ondragleave="dragLeave(event)">
    <div class="playlist-icon">📋</div>
    <div class="playlist-info">
        <h3 class="playlist-name">$($pl.name)</h3>
        <span class="playlist-meta">$trackCount tracks • Created $createdDate</span>
        <span class="playlist-hint">Click to view & add music</span>
    </div>
    <div class="playlist-actions" onclick="event.stopPropagation(); event.preventDefault();">
        <button class="playlist-btn edit" onclick="editPlaylist($($pl.playlist_id), '$($pl.name -replace "'", "\'")')" title="Rename">✏️</button>
        <button class="playlist-btn delete" onclick="deletePlaylist($($pl.playlist_id), '$($pl.name -replace "'", "\'")')" title="Delete">🗑️</button>
    </div>
</a>
"@
        }
    }
    
    if (-not $playlistItems) {
        $playlistItems = @"
<div class="no-playlists">
    <div style="font-size: 64px; margin-bottom: 20px;">📋</div>
    <h3>No Playlists Yet</h3>
    <p>Create your first playlist to organize your favorite music!</p>
</div>
"@
    }
    
    $content = @"
<style>
.playlists-container { padding: 24px; max-width: 1200px; margin: 0 auto; }
.playlists-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 32px;
    padding-bottom: 20px;
    border-bottom: 1px solid rgba(255,255,255,0.1);
}
.playlists-header h1 {
    font-size: 28px;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 12px;
}
.playlists-header .count {
    font-size: 14px;
    color: rgba(255,255,255,0.5);
    font-weight: normal;
}

.create-playlist-btn {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 12px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    transition: all 0.3s ease;
}
.create-playlist-btn:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 20px rgba(99,102,241,0.4);
}
.create-playlist-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
}

.playlists-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
    gap: 20px;
}

.playlist-card {
    background: rgba(255,255,255,0.03);
    border: 2px solid rgba(255,255,255,0.08);
    border-radius: 16px;
    padding: 20px;
    display: flex;
    align-items: center;
    gap: 16px;
    transition: all 0.3s ease;
    cursor: pointer;
    text-decoration: none;
    color: inherit;
}
.playlist-card:hover {
    background: rgba(255,255,255,0.06);
    border-color: rgba(99,102,241,0.3);
    transform: translateY(-2px);
}
.playlist-card:hover .playlist-hint {
    opacity: 1;
}
.playlist-card.drag-over {
    border-color: #22c55e;
    background: rgba(34,197,94,0.1);
    box-shadow: 0 0 20px rgba(34,197,94,0.3);
}

.playlist-icon {
    font-size: 40px;
    width: 60px;
    height: 60px;
    background: linear-gradient(135deg, rgba(99,102,241,0.2), rgba(139,92,246,0.2));
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.playlist-info { flex: 1; }
.playlist-name {
    font-size: 18px;
    font-weight: 600;
    margin: 0 0 6px 0;
    color: #fff;
}
.playlist-meta {
    font-size: 13px;
    color: rgba(255,255,255,0.5);
    display: block;
}
.playlist-hint {
    font-size: 12px;
    color: #6366f1;
    display: block;
    margin-top: 4px;
    opacity: 0;
    transition: opacity 0.2s;
}

.playlist-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
}
.playlist-btn {
    width: 36px;
    height: 36px;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
    text-decoration: none;
}
.playlist-btn.play {
    background: rgba(34,197,94,0.2);
    color: #22c55e;
}
.playlist-btn.play:hover { background: rgba(34,197,94,0.3); }
.playlist-btn.edit {
    background: rgba(99,102,241,0.2);
    color: #6366f1;
}
.playlist-btn.edit:hover { background: rgba(99,102,241,0.3); }
.playlist-btn.delete {
    background: rgba(239,68,68,0.2);
    color: #ef4444;
}
.playlist-btn.delete:hover { background: rgba(239,68,68,0.3); }

.no-playlists {
    text-align: center;
    padding: 60px 20px;
    color: rgba(255,255,255,0.5);
}
.no-playlists h3 { color: rgba(255,255,255,0.7); margin-bottom: 10px; }

.back-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: rgba(255,255,255,0.7);
    text-decoration: none;
    margin-bottom: 24px;
    font-size: 14px;
    transition: color 0.2s;
}
.back-link:hover { color: #fff; }

.drag-hint {
    background: rgba(99,102,241,0.1);
    border: 1px dashed rgba(99,102,241,0.3);
    border-radius: 12px;
    padding: 16px;
    text-align: center;
    margin-bottom: 24px;
    color: rgba(255,255,255,0.6);
    font-size: 14px;
}
.drag-hint strong { color: #6366f1; }

/* Modal styles */
.modal-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0,0,0,0.8);
    z-index: 9999;
    align-items: center;
    justify-content: center;
}
.modal-overlay.show { display: flex; }
.modal {
    background: #1a1a2e;
    border-radius: 20px;
    padding: 32px;
    width: 90%;
    max-width: 450px;
    border: 1px solid rgba(255,255,255,0.1);
}
.modal h2 {
    margin: 0 0 24px 0;
    font-size: 24px;
    display: flex;
    align-items: center;
    gap: 12px;
}
.modal input {
    width: 100%;
    padding: 14px 16px;
    background: rgba(255,255,255,0.05);
    border: 1px solid rgba(255,255,255,0.1);
    border-radius: 10px;
    color: #fff;
    font-size: 16px;
    margin-bottom: 8px;
}
.modal input:focus {
    outline: none;
    border-color: #6366f1;
}
.modal .char-count {
    font-size: 12px;
    color: rgba(255,255,255,0.4);
    text-align: right;
    margin-bottom: 24px;
}
.modal-buttons {
    display: flex;
    gap: 12px;
    justify-content: flex-end;
}
.modal-btn {
    padding: 12px 24px;
    border-radius: 10px;
    border: none;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
}
.modal-btn.cancel {
    background: rgba(255,255,255,0.1);
    color: #fff;
}
.modal-btn.cancel:hover { background: rgba(255,255,255,0.15); }
.modal-btn.confirm {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    color: #fff;
}
.modal-btn.confirm:hover { transform: translateY(-1px); }
.modal-btn.delete {
    background: linear-gradient(135deg, #ef4444, #dc2626);
    color: #fff;
}

@media (max-width: 768px) {
    .playlists-header { flex-direction: column; gap: 16px; align-items: flex-start; }
    .playlists-grid { grid-template-columns: 1fr; }
}
</style>

<div class="playlists-container">
    <a href="/music" class="back-link">← Back to Music</a>
    
    <div class="playlists-header">
        <h1>📋 My Playlists <span class="count">($playlistCount/20)</span></h1>
        <button class="create-playlist-btn" onclick="showCreateModal()" $(if ($playlistCount -ge 20) { "disabled title='Maximum 20 playlists reached'" })>
            <span>+</span> Create Playlist
        </button>
    </div>
    
    <div class="drag-hint">
        💡 <strong>Tip:</strong> Drag music tracks from the Music page and drop them onto a playlist to add them!
    </div>
    
    <div class="playlists-grid">
        $playlistItems
    </div>
</div>

<!-- Create/Edit Modal -->
<div class="modal-overlay" id="playlistModal">
    <div class="modal">
        <h2 id="modalTitle">📋 Create Playlist</h2>
        <input type="text" id="playlistName" placeholder="Enter playlist name..." maxlength="30" oninput="updateCharCount()">
        <div class="char-count"><span id="charCount">0</span>/30 characters</div>
        <div class="modal-buttons">
            <button class="modal-btn cancel" onclick="hideModal()">Cancel</button>
            <button class="modal-btn confirm" id="modalConfirm" onclick="confirmAction()">Create</button>
        </div>
    </div>
</div>

<!-- Delete Confirm Modal -->
<div class="modal-overlay" id="deleteModal">
    <div class="modal">
        <h2>🗑️ Delete Playlist</h2>
        <p id="deleteMessage" style="color: rgba(255,255,255,0.7); margin-bottom: 24px;">Are you sure you want to delete this playlist?</p>
        <div class="modal-buttons">
            <button class="modal-btn cancel" onclick="hideDeleteModal()">Cancel</button>
            <button class="modal-btn delete" onclick="confirmDelete()">Delete</button>
        </div>
    </div>
</div>

<script>
var modalMode = 'create';
var editingPlaylistId = null;
var deletingPlaylistId = null;

function showCreateModal() {
    modalMode = 'create';
    document.getElementById('modalTitle').textContent = '📋 Create Playlist';
    document.getElementById('playlistName').value = '';
    document.getElementById('modalConfirm').textContent = 'Create';
    document.getElementById('charCount').textContent = '0';
    document.getElementById('playlistModal').classList.add('show');
    document.getElementById('playlistName').focus();
}

function editPlaylist(id, name) {
    modalMode = 'edit';
    editingPlaylistId = id;
    document.getElementById('modalTitle').textContent = '✏️ Rename Playlist';
    document.getElementById('playlistName').value = name;
    document.getElementById('modalConfirm').textContent = 'Save';
    document.getElementById('charCount').textContent = name.length;
    document.getElementById('playlistModal').classList.add('show');
    document.getElementById('playlistName').focus();
}

function hideModal() {
    document.getElementById('playlistModal').classList.remove('show');
}

function updateCharCount() {
    var len = document.getElementById('playlistName').value.length;
    document.getElementById('charCount').textContent = len;
}

function confirmAction() {
    var name = document.getElementById('playlistName').value.trim();
    if (!name || name.length < 1 || name.length > 30) {
        alert('Playlist name must be between 1 and 30 characters');
        return;
    }
    
    if (modalMode === 'create') {
        fetch('/api/playlist/create', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name: name })
        })
        .then(r => r.json())
        .then(data => {
            if (data.success) {
                window.location.reload();
            } else {
                alert(data.error || 'Failed to create playlist');
            }
        });
    } else {
        fetch('/api/playlist/rename', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ playlistId: editingPlaylistId, name: name })
        })
        .then(r => r.json())
        .then(data => {
            if (data.success) {
                window.location.reload();
            } else {
                alert(data.error || 'Failed to rename playlist');
            }
        });
    }
}

function deletePlaylist(id, name) {
    deletingPlaylistId = id;
    document.getElementById('deleteMessage').textContent = 'Are you sure you want to delete "' + name + '"? This cannot be undone.';
    document.getElementById('deleteModal').classList.add('show');
}

function hideDeleteModal() {
    document.getElementById('deleteModal').classList.remove('show');
}

function confirmDelete() {
    fetch('/api/playlist/delete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ playlistId: deletingPlaylistId })
    })
    .then(r => r.json())
    .then(data => {
        if (data.success) {
            window.location.reload();
        } else {
            alert(data.error || 'Failed to delete playlist');
        }
    });
}

// Drag and drop handlers
function allowDrop(e) {
    e.preventDefault();
    e.currentTarget.classList.add('drag-over');
}

function dragLeave(e) {
    e.currentTarget.classList.remove('drag-over');
}

function dropOnPlaylist(e, playlistId) {
    e.preventDefault();
    e.currentTarget.classList.remove('drag-over');
    
    try {
        var data = JSON.parse(e.dataTransfer.getData('application/json'));
        if (data && data.musicId) {
            fetch('/api/playlist/add-track', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    playlistId: playlistId,
                    musicId: data.musicId,
                    title: data.title,
                    artist: data.artist,
                    album: data.album,
                    path: data.path
                })
            })
            .then(r => r.json())
            .then(result => {
                if (result.success) {
                    // Show success feedback
                    var card = e.currentTarget;
                    card.style.borderColor = '#22c55e';
                    card.style.boxShadow = '0 0 20px rgba(34,197,94,0.5)';
                    setTimeout(function() {
                        card.style.borderColor = '';
                        card.style.boxShadow = '';
                        window.location.reload();
                    }, 500);
                } else {
                    alert(result.error || 'Failed to add track');
                }
            });
        }
    } catch (err) {
        console.error('Drop error:', err);
    }
}

// Close modal on Escape key
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        hideModal();
        hideDeleteModal();
    }
    if (e.key === 'Enter' && document.getElementById('playlistModal').classList.contains('show')) {
        confirmAction();
    }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "My Playlists - NexusStack"
}

function Get-PlaylistDetailPage {
    param([int]$PlaylistId)
    
    # Check if user is logged in
    if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
        return Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>Please log in</h1><p><a href='/login'>← Login</a></p></div>" -Title "Playlist"
    }
    
    $username = $Global:CurrentUser.Username
    $userDbPath = Join-Path $CONFIG.UsersDBPath "$username.db"
    
    # Get playlist info
    $playlist = Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT * FROM playlists WHERE playlist_id = @id" -SqlParameters @{ id = $PlaylistId }
    
    if (-not $playlist) {
        return Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>Playlist not found</h1><p><a href='/playlists'>← Back to Playlists</a></p></div>" -Title "Not Found"
    }
    
    # Get tracks in playlist
    $tracks = Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT * FROM playlist_tracks WHERE playlist_id = @id ORDER BY track_order" -SqlParameters @{ id = $PlaylistId }
    
    # Get track IDs already in playlist for filtering
    $existingTrackIds = @()
    if ($tracks) {
        $existingTrackIds = @($tracks | ForEach-Object { $_.music_id })
    }
    
    $trackItems = ""
    $trackIndex = 0
    if ($tracks) {
        foreach ($track in $tracks) {
            $trackIndex++
            # Get album art from main music database
            $musicInfo = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT album_art_cached, album_art_url FROM music WHERE id = @id" -SqlParameters @{ id = $track.music_id }
            
            $artStyle = ""
            if ($musicInfo) {
                if ($musicInfo.album_art_cached -and -not [string]::IsNullOrWhiteSpace($musicInfo.album_art_cached)) {
                    $artStyle = "background-image: url('/albumart/$($musicInfo.album_art_cached)');"
                } elseif ($musicInfo.album_art_url -and -not [string]::IsNullOrWhiteSpace($musicInfo.album_art_url)) {
                    $artStyle = "background-image: url('/poster/$($musicInfo.album_art_url)');"
                }
            }
            
            $trackItems += @"
<div class="track-item" data-track-id="$($track.track_id)" data-order="$($track.track_order)">
    <div class="track-num">$trackIndex</div>
    <div class="track-art" style="$artStyle"></div>
    <div class="track-info">
        <div class="track-title">$($track.title)</div>
        <div class="track-artist">$($track.artist) $(if ($track.album) { "• $($track.album)" })</div>
    </div>
    <div class="track-actions">
        <a href="/play-audio/$($track.music_id)" class="track-btn play" title="Play">▶</a>
        <button class="track-btn remove" onclick="removeTrack($($track.track_id))" title="Remove">✕</button>
    </div>
</div>
"@
        }
    }
    
    if (-not $trackItems) {
        $trackItems = @"
<div class="no-tracks">
    <div style="font-size: 48px; margin-bottom: 16px;">🎵</div>
    <h3>No tracks yet</h3>
    <p>Click <strong>"+ Add Music"</strong> above to browse and add tracks!</p>
</div>
"@
    }
    
    $trackCount = if ($tracks) { @($tracks).Count } else { 0 }
    
    # Get all music for the browser panel
    $allMusic = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT id, title, artist, album, album_art_cached, album_art_url FROM music ORDER BY title LIMIT 500"
    
    $musicBrowserItems = ""
    foreach ($music in $allMusic) {
        $artist = if ($music.artist) { $music.artist } else { "Unknown Artist" }
        $album = if ($music.album) { $music.album } else { "" }
        $isInPlaylist = $existingTrackIds -contains $music.id
        $disabledClass = if ($isInPlaylist) { "in-playlist" } else { "" }
        $disabledAttr = if ($isInPlaylist) { "disabled" } else { "" }
        
        $artStyle = ""
        if ($music.album_art_cached -and -not [string]::IsNullOrWhiteSpace($music.album_art_cached)) {
            $artStyle = "background-image: url('/albumart/$($music.album_art_cached)');"
        } elseif ($music.album_art_url -and -not [string]::IsNullOrWhiteSpace($music.album_art_url)) {
            $artStyle = "background-image: url('/poster/$($music.album_art_url)');"
        }
        
        $musicBrowserItems += @"
<div class="browser-item $disabledClass" data-music-id="$($music.id)" data-title="$($music.title -replace '"', '&quot;')" data-artist="$($artist -replace '"', '&quot;')" data-album="$($album -replace '"', '&quot;')">
    <div class="browser-art" style="$artStyle"></div>
    <div class="browser-info">
        <div class="browser-title">$($music.title)</div>
        <div class="browser-artist">$artist</div>
    </div>
    <button class="browser-add-btn" onclick="addTrackToPlaylist(this, $($music.id), '$($music.title -replace "'", "\'")', '$($artist -replace "'", "\'")', '$($album -replace "'", "\'")')" $disabledAttr>
        $(if ($isInPlaylist) { "✓" } else { "+" })
    </button>
</div>
"@
    }
    
    $content = @"
<style>
.playlist-page { display: flex; height: calc(100vh - 60px); }
.playlist-main { flex: 1; padding: 24px; overflow-y: auto; }
.music-browser { 
    width: 400px; 
    background: rgba(0,0,0,0.3); 
    border-left: 1px solid rgba(255,255,255,0.1);
    display: none;
    flex-direction: column;
}
.music-browser.open { display: flex; }

.playlist-header {
    display: flex;
    align-items: center;
    gap: 24px;
    margin-bottom: 32px;
    padding-bottom: 24px;
    border-bottom: 1px solid rgba(255,255,255,0.1);
}
.playlist-cover {
    width: 140px;
    height: 140px;
    background: linear-gradient(135deg, rgba(99,102,241,0.3), rgba(139,92,246,0.3));
    border-radius: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 56px;
    flex-shrink: 0;
}
.playlist-details { flex: 1; }
.playlist-details h1 {
    font-size: 28px;
    margin: 0 0 8px 0;
}
.playlist-details .meta {
    color: rgba(255,255,255,0.5);
    font-size: 14px;
    margin-bottom: 16px;
}
.playlist-controls {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
}
.control-btn {
    padding: 10px 20px;
    border-radius: 10px;
    border: none;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    transition: all 0.2s;
    text-decoration: none;
    color: white;
}
.control-btn.play {
    background: linear-gradient(135deg, #22c55e, #16a34a);
}
.control-btn.play:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(34,197,94,0.4); }
.control-btn.shuffle {
    background: rgba(255,255,255,0.1);
}
.control-btn.shuffle:hover { background: rgba(255,255,255,0.15); }
.control-btn.add-music {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.control-btn.add-music:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99,102,241,0.4); }
.control-btn.add-music.active {
    background: linear-gradient(135deg, #8b5cf6, #6366f1);
    box-shadow: 0 0 20px rgba(99,102,241,0.5);
}

.back-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: rgba(255,255,255,0.7);
    text-decoration: none;
    margin-bottom: 24px;
    font-size: 14px;
}
.back-link:hover { color: #fff; }

.tracks-list {
    background: rgba(255,255,255,0.02);
    border-radius: 16px;
    overflow: hidden;
}

.track-item {
    display: flex;
    align-items: center;
    padding: 12px 16px;
    gap: 16px;
    border-bottom: 1px solid rgba(255,255,255,0.05);
    transition: background 0.2s;
}
.track-item:hover { background: rgba(255,255,255,0.03); }
.track-item:last-child { border-bottom: none; }

.track-num {
    width: 32px;
    text-align: center;
    color: rgba(255,255,255,0.4);
    font-size: 14px;
}
.track-art {
    width: 48px;
    height: 48px;
    background: rgba(255,255,255,0.1);
    background-size: cover;
    background-position: center;
    border-radius: 8px;
    flex-shrink: 0;
}
.track-info { flex: 1; min-width: 0; }
.track-title {
    font-weight: 500;
    margin-bottom: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.track-artist {
    font-size: 13px;
    color: rgba(255,255,255,0.5);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.track-actions {
    display: flex;
    gap: 8px;
    opacity: 0;
    transition: opacity 0.2s;
    flex-shrink: 0;
}
.track-item:hover .track-actions { opacity: 1; }
.track-btn {
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
    text-decoration: none;
    color: inherit;
}
.track-btn.play {
    background: rgba(34,197,94,0.2);
    color: #22c55e;
}
.track-btn.play:hover { background: rgba(34,197,94,0.3); }
.track-btn.remove {
    background: rgba(239,68,68,0.2);
    color: #ef4444;
}
.track-btn.remove:hover { background: rgba(239,68,68,0.3); }

.track-item.new-track {
    animation: trackAdded 0.5s ease;
    background: rgba(34,197,94,0.1);
}
@keyframes trackAdded {
    0% { opacity: 0; transform: translateX(20px); }
    100% { opacity: 1; transform: translateX(0); }
}

.no-tracks {
    text-align: center;
    padding: 60px 20px;
    color: rgba(255,255,255,0.5);
}
.no-tracks h3 { color: rgba(255,255,255,0.7); margin-bottom: 8px; }

/* Music Browser Panel */
.browser-header {
    padding: 20px;
    border-bottom: 1px solid rgba(255,255,255,0.1);
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.browser-header h2 {
    margin: 0;
    font-size: 18px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.browser-close {
    background: none;
    border: none;
    color: rgba(255,255,255,0.6);
    font-size: 24px;
    cursor: pointer;
    padding: 4px 8px;
    border-radius: 6px;
}
.browser-close:hover { background: rgba(255,255,255,0.1); color: #fff; }

.browser-search {
    padding: 16px 20px;
    border-bottom: 1px solid rgba(255,255,255,0.1);
}
.browser-search input {
    width: 100%;
    padding: 12px 16px;
    background: rgba(255,255,255,0.05);
    border: 1px solid rgba(255,255,255,0.1);
    border-radius: 10px;
    color: #fff;
    font-size: 14px;
}
.browser-search input:focus {
    outline: none;
    border-color: #6366f1;
}
.browser-search input::placeholder { color: rgba(255,255,255,0.4); }

.browser-list {
    flex: 1;
    overflow-y: auto;
    padding: 12px;
}

.browser-item {
    display: flex;
    align-items: center;
    padding: 10px 12px;
    gap: 12px;
    border-radius: 10px;
    cursor: pointer;
    transition: all 0.2s;
    margin-bottom: 4px;
}
.browser-item:hover { background: rgba(255,255,255,0.05); }
.browser-item.in-playlist {
    opacity: 0.5;
    cursor: default;
}

.browser-art {
    width: 44px;
    height: 44px;
    background: rgba(255,255,255,0.1);
    background-size: cover;
    background-position: center;
    border-radius: 8px;
    flex-shrink: 0;
}
.browser-info { flex: 1; min-width: 0; }
.browser-title {
    font-size: 14px;
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 2px;
}
.browser-artist {
    font-size: 12px;
    color: rgba(255,255,255,0.5);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.browser-add-btn {
    width: 32px;
    height: 32px;
    border: none;
    border-radius: 50%;
    background: rgba(99,102,241,0.2);
    color: #6366f1;
    font-size: 18px;
    font-weight: 600;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
    flex-shrink: 0;
}
.browser-add-btn:hover:not(:disabled) { 
    background: rgba(99,102,241,0.4); 
    transform: scale(1.1);
}
.browser-add-btn:disabled {
    background: rgba(34,197,94,0.2);
    color: #22c55e;
    cursor: default;
}
.browser-add-btn.adding {
    background: rgba(234,179,8,0.3);
    color: #eab308;
}

.browser-count {
    padding: 12px 20px;
    border-top: 1px solid rgba(255,255,255,0.1);
    font-size: 13px;
    color: rgba(255,255,255,0.5);
    text-align: center;
}

@media (max-width: 900px) {
    .playlist-page { flex-direction: column; }
    .music-browser { 
        width: 100%; 
        height: 50vh;
        border-left: none;
        border-top: 1px solid rgba(255,255,255,0.1);
    }
    .playlist-header { flex-direction: column; text-align: center; }
    .playlist-controls { justify-content: center; }
}
</style>

<div class="playlist-page">
    <div class="playlist-main">
        <a href="/playlists" class="back-link">← Back to Playlists</a>
        
        <div class="playlist-header">
            <div class="playlist-cover">📋</div>
            <div class="playlist-details">
                <h1>$($playlist.name)</h1>
                <div class="meta">$trackCount tracks • Created $(try { ([datetime]$playlist.created_date).ToString("MMMM d, yyyy") } catch { "Unknown" })</div>
                <div class="playlist-controls">
                    $(if ($trackCount -gt 0) {
                        "<a href='/play-playlist/$PlaylistId' class='control-btn play'>▶ Play All</a>
                        <a href='/play-playlist/$PlaylistId/shuffle' class='control-btn shuffle'>🔀 Shuffle</a>"
                    })
                    <button class="control-btn add-music" id="addMusicBtn" onclick="toggleMusicBrowser()">
                        <span>+</span> Add Music
                    </button>
                </div>
            </div>
        </div>
        
        <div class="tracks-list" id="tracksList">
            $trackItems
        </div>
    </div>
    
    <div class="music-browser" id="musicBrowser">
        <div class="browser-header">
            <h2>🎵 Add Music</h2>
            <button class="browser-close" onclick="toggleMusicBrowser()">×</button>
        </div>
        <div class="browser-search">
            <input type="text" id="browserSearch" placeholder="Search music..." oninput="filterMusic(this.value)">
        </div>
        <div class="browser-list" id="browserList">
            $musicBrowserItems
        </div>
        <div class="browser-count" id="browserCount">
            Showing $($allMusic.Count) tracks
        </div>
    </div>
</div>

<script>
var playlistId = $PlaylistId;

function toggleMusicBrowser() {
    var browser = document.getElementById('musicBrowser');
    var btn = document.getElementById('addMusicBtn');
    browser.classList.toggle('open');
    btn.classList.toggle('active');
    
    if (browser.classList.contains('open')) {
        document.getElementById('browserSearch').focus();
    }
}

function filterMusic(query) {
    var items = document.querySelectorAll('.browser-item');
    var visibleCount = 0;
    query = query.toLowerCase();
    
    items.forEach(function(item) {
        var title = item.querySelector('.browser-title').textContent.toLowerCase();
        var artist = item.querySelector('.browser-artist').textContent.toLowerCase();
        
        if (title.includes(query) || artist.includes(query)) {
            item.style.display = 'flex';
            visibleCount++;
        } else {
            item.style.display = 'none';
        }
    });
    
    document.getElementById('browserCount').textContent = 'Showing ' + visibleCount + ' tracks';
}

var addedTracksCount = 0; // Track newly added tracks

function addTrackToPlaylist(btn, musicId, title, artist, album) {
    if (btn.disabled) return;
    
    btn.classList.add('adding');
    btn.textContent = '...';
    
    fetch('/api/playlist/add-track', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            playlistId: playlistId,
            musicId: musicId,
            title: title,
            artist: artist,
            album: album,
            path: ''
        })
    })
    .then(r => r.json())
    .then(result => {
        if (result.success) {
            btn.classList.remove('adding');
            btn.textContent = '✓';
            btn.disabled = true;
            btn.parentElement.classList.add('in-playlist');
            
            // Add track to the playlist display without reloading
            addedTracksCount++;
            var tracksList = document.getElementById('tracksList');
            var noTracks = tracksList.querySelector('.no-tracks');
            if (noTracks) {
                noTracks.remove();
            }
            
            // Get current track count
            var currentCount = tracksList.querySelectorAll('.track-item').length;
            var newTrackNum = currentCount + 1;
            
            // Get album art from the browser item
            var browserItem = btn.parentElement;
            var artStyle = browserItem.querySelector('.browser-art').getAttribute('style') || '';
            
            // Create new track element
            var newTrack = document.createElement('div');
            newTrack.className = 'track-item new-track';
            newTrack.innerHTML = 
                '<div class="track-num">' + newTrackNum + '</div>' +
                '<div class="track-art" style="' + artStyle + '"></div>' +
                '<div class="track-info">' +
                    '<div class="track-title">' + escapeHtml(title) + '</div>' +
                    '<div class="track-artist">' + escapeHtml(artist) + (album ? ' • ' + escapeHtml(album) : '') + '</div>' +
                '</div>' +
                '<div class="track-actions">' +
                    '<a href="/play-audio/' + musicId + '" class="track-btn play" title="Play">▶</a>' +
                    '<span class="track-btn" style="background:rgba(34,197,94,0.2);color:#22c55e;">✓</span>' +
                '</div>';
            
            tracksList.appendChild(newTrack);
            
            // Update header track count
            var metaEl = document.querySelector('.playlist-details .meta');
            if (metaEl) {
                var text = metaEl.textContent;
                var match = text.match(/(\d+) tracks/);
                if (match) {
                    var newCount = parseInt(match[1]) + 1;
                    metaEl.textContent = text.replace(/\d+ tracks/, newCount + ' tracks');
                }
            }
            
            // Show Play All and Shuffle buttons if this is the first track
            if (newTrackNum === 1) {
                var controls = document.querySelector('.playlist-controls');
                var addMusicBtn = controls.querySelector('.add-music');
                if (addMusicBtn && !controls.querySelector('.play')) {
                    var playBtn = document.createElement('a');
                    playBtn.href = '/play-playlist/' + playlistId;
                    playBtn.className = 'control-btn play';
                    playBtn.innerHTML = '▶ Play All';
                    controls.insertBefore(playBtn, addMusicBtn);
                    
                    var shuffleBtn = document.createElement('a');
                    shuffleBtn.href = '/play-playlist/' + playlistId + '/shuffle';
                    shuffleBtn.className = 'control-btn shuffle';
                    shuffleBtn.innerHTML = '🔀 Shuffle';
                    controls.insertBefore(shuffleBtn, addMusicBtn);
                }
            }
            
            // Flash animation on new track
            setTimeout(function() {
                newTrack.classList.remove('new-track');
            }, 1000);
            
        } else {
            btn.classList.remove('adding');
            btn.textContent = '+';
            alert(result.error || 'Failed to add track');
        }
    })
    .catch(function(err) {
        btn.classList.remove('adding');
        btn.textContent = '+';
        alert('Error adding track');
    });
}

function escapeHtml(text) {
    var div = document.createElement('div');
    div.textContent = text || '';
    return div.innerHTML;
}

function removeTrack(trackId) {
    if (!confirm('Remove this track from the playlist?')) return;
    
    fetch('/api/playlist/remove-track', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ trackId: trackId })
    })
    .then(r => r.json())
    .then(data => {
        if (data.success) {
            window.location.reload();
        } else {
            alert(data.error || 'Failed to remove track');
        }
    });
}

// Keyboard shortcut: Escape to close browser
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        var browser = document.getElementById('musicBrowser');
        if (browser.classList.contains('open')) {
            toggleMusicBrowser();
        }
    }
});

// Auto-open music browser if playlist is empty
document.addEventListener('DOMContentLoaded', function() {
    var tracksList = document.getElementById('tracksList');
    var noTracks = tracksList.querySelector('.no-tracks');
    if (noTracks) {
        // Playlist is empty, auto-open the music browser
        setTimeout(function() {
            toggleMusicBrowser();
        }, 300);
    }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "$($playlist.name) - NexusStack"
}

function Get-PicturesPage {
    param(
        [string]$Filter = "all"
    )
    
    # Get pictures based on filter
    $query = switch ($Filter) {
        "recent" {
            "SELECT * FROM images ORDER BY date_added DESC LIMIT 100"
        }
        "bydate" {
            "SELECT * FROM images ORDER BY date_added DESC"
        }
        default {  # "all"
            "SELECT * FROM images ORDER BY filename"
        }
    }
    
    $allPictures = Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query $query
    
    $pictureItems = ""
    foreach ($picture in $allPictures) {
        $size = [math]::Round($picture.size_bytes / 1MB, 1)
        $encodedPath = [System.Web.HttpUtility]::UrlEncode($picture.filepath)
        
        # Use the actual image as thumbnail
        $thumbStyle = "background-image: url('/image/$($encodedPath)');"
        
        $pictureItems += @"
<a class="item" href="/view-image?path=$encodedPath" target="_blank">
  <div class="thumb" style="$thumbStyle background-size: cover; background-position: center;"></div>
  <div class="flag picture">Picture</div>
  <div class="corner">🖼</div>
  <div class="meta">
    <strong>$($picture.filename)</strong>
    <span>$($picture.format) • ${size}MB</span>
  </div>
</a>
"@
    }
    
    # Determine which chip is active
    $allActive = if ($Filter -eq "all") { "on" } else { "" }
    $recentActive = if ($Filter -eq "recent") { "on" } else { "" }
    $byDateActive = if ($Filter -eq "bydate") { "on" } else { "" }
    
    # Determine title based on filter
    $titleText = switch ($Filter) {
        "recent" { "Recent Pictures (Last 100)" }
        "bydate" { "Pictures - Sorted by Date" }
        default { "All Pictures" }
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search pictures…" id="searchInput" />
  </div>
  <div class="seg">
    <div class="chip $allActive" onclick="window.location.href='/pictures?filter=all'">All Pictures</div>
    <div class="chip $recentActive" onclick="window.location.href='/pictures?filter=recent'">Recent</div>
    <div class="chip $byDateActive" onclick="window.location.href='/pictures?filter=bydate'">By Date</div>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$titleText</strong>
        <span>$($allPictures.Count) items • $($Global:MediaStats.PicturesSizeGB) GB</span>
      </div>
      <div class="actions">
        <div class="small">🔄 Refresh</div>
      </div>
    </div>
    <div class="grid">
      $pictureItems
    </div>
  </section>
</div>

<script>
document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=pictures';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Pictures"
}

function Get-PDFsPage {
    param(
        [string]$Filter = "all"
    )
    
    # Get PDFs based on filter
    $query = switch ($Filter) {
        "recent" {
            "SELECT * FROM pdfs ORDER BY date_added DESC LIMIT 50"
        }
        "byname" {
            "SELECT * FROM pdfs ORDER BY title"
        }
        default {  # "all"
            "SELECT * FROM pdfs ORDER BY date_added DESC"
        }
    }
    
    $allPDFs = Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query $query
    
    $pdfItems = ""
    foreach ($pdf in $allPDFs) {
        $sizeMB = [math]::Round($pdf.size_bytes / 1MB, 1)
        $encodedPath = [System.Web.HttpUtility]::UrlEncode($pdf.filepath)
        
        # Detect file type - check database field or file extension as fallback
        $fileExtension = [System.IO.Path]::GetExtension($pdf.filepath).ToLower()
        $isEpub = ($pdf.PSObject.Properties.Name -contains 'file_type' -and $pdf.file_type -eq 'epub') -or $fileExtension -eq '.epub'
        
        # Set badge text and icon based on file type
        $badgeText = if ($isEpub) { "EPUB" } else { "PDF" }
        $badgeClass = if ($isEpub) { "epub" } else { "pdf" }
        $fileTypeLabel = if ($isEpub) { "EPUB" } else { "PDF" }
        
        # Check if document has generated preview/cover
        $thumbStyle = ""
        if ($pdf.preview_image -and -not [string]::IsNullOrWhiteSpace($pdf.preview_image)) {
            # Use actual preview/cover image
            $thumbStyle = "background-image: url('/pdfpreview/$($pdf.preview_image)'); background-size: cover; background-position: center;"
        }
        else {
            # Use placeholder icon with different colors for EPUB
            if ($isEpub) {
                $thumbStyle = "background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(168,85,247,0.15)); display:flex; align-items:center; justify-content:center; font-size:48px;"
            }
            else {
                $thumbStyle = "background: linear-gradient(135deg, rgba(220,38,38,0.15), rgba(251,191,36,0.15)); display:flex; align-items:center; justify-content:center; font-size:48px;"
            }
        }
        
        # Choose icon based on file type
        $iconPath = if ($isEpub) { "/menu/pdf.png" } else { "/menu/pdf.png" }  # You can create a separate epub.png icon if desired
        $iconHtml = if (-not $pdf.preview_image -or [string]::IsNullOrWhiteSpace($pdf.preview_image)) { "<img src='$iconPath' style='width:48px; height:48px;'>" } else { "" }
        
        # For EPUB files, use reader instead of download
        $clickAction = if ($isEpub) {
            "href='/read-epub?path=$encodedPath'"
        } else {
            "href='/view-pdf?path=$encodedPath' target='_blank'"
        }
        
        $pdfItems += @"
<a class="item" $clickAction>
  <div class="thumb" style="$thumbStyle">$iconHtml</div>
  <div class="flag $badgeClass">$badgeText</div>
  <div class="corner">📖</div>
  <div class="meta">
    <strong>$($pdf.title)</strong>
    <span>$fileTypeLabel • ${sizeMB}MB</span>
  </div>
</a>
"@
    }
    
    # Determine which chip is active
    $allActive = if ($Filter -eq "all") { "on" } else { "" }
    $recentActive = if ($Filter -eq "recent") { "on" } else { "" }
    $byNameActive = if ($Filter -eq "byname") { "on" } else { "" }
    
    # Determine title based on filter
    $titleText = switch ($Filter) {
        "recent" { "Recent PDFs (Last 50)" }
        "byname" { "PDFs - Sorted by Name" }
        default { "All PDFs" }
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search PDFs…" id="searchInput" />
  </div>
  <div class="seg">
    <div class="chip $allActive" onclick="window.location.href='/pdfs?filter=all'">All PDFs</div>
    <div class="chip $recentActive" onclick="window.location.href='/pdfs?filter=recent'">Recent</div>
    <div class="chip $byNameActive" onclick="window.location.href='/pdfs?filter=byname'">By Name</div>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$titleText</strong>
        <span>$($allPDFs.Count) documents • $($Global:MediaStats.PDFSizeGB) GB</span>
      </div>
      <div class="actions">
        <a href="/generate-pdf-thumbnails" class="small" style="text-decoration:none; color:inherit;">🎨 Generate Thumbnails</a>
        <div class="small">🔄 Refresh</div>
      </div>
    </div>
    <div class="grid">
      $pdfItems
    </div>
  </section>
</div>

<script>
document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=pdfs';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - PDFs/ePUBS"
}

function Get-RadioPage {
    param(
        [string]$Filter = "all"
    )
    
    # Get radio stations based on filter
    $stations = switch ($Filter) {
        "usa" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "USA" }
        }
        "uk" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "UK" }
        }
        "france" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "France" }
        }
        "germany" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "Germany" }
        }
        "netherlands" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "Netherlands" }
        }
        "italy" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "Italy" }
        }
        "switzerland" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "Switzerland" }
        }
        "belgium" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "Belgium" }
        }
        "ireland" {
            $CONFIG.RadioStations | Where-Object { $_.Country -eq "Ireland" }
        }
        "europe" {
            $CONFIG.RadioStations | Where-Object { $_.Country -notin @("USA", "UK") }
        }
        "news" {
            $CONFIG.RadioStations | Where-Object { $_.Genre -like "*News*" -or $_.Genre -like "*Talk*" }
        }
        "music" {
            $CONFIG.RadioStations | Where-Object { $_.Genre -notlike "*News*" -and $_.Genre -notlike "*Talk*" }
        }
        "jazz" {
            $CONFIG.RadioStations | Where-Object { $_.Genre -like "*Jazz*" }
        }
        "classical" {
            $CONFIG.RadioStations | Where-Object { $_.Genre -like "*Classical*" }
        }
        default {
            $CONFIG.RadioStations
        }
    }
    
    $radioItems = ""
    $index = 0
    foreach ($station in $stations) {
        $index++
        
        # Generate a gradient background based on genre
        $gradientStyle = switch -Regex ($station.Genre) {
            "News|Talk" { "background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(220,38,38,0.1));" }
            "Jazz" { "background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(147,51,234,0.1));" }
            "Classical" { "background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(37,99,235,0.1));" }
            "Rock|Alternative" { "background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(251,146,60,0.1));" }
            "Pop|Dance" { "background: linear-gradient(135deg, rgba(236,72,153,0.2), rgba(219,39,119,0.1));" }
            "Electronic|Ambient" { "background: linear-gradient(135deg, rgba(34,211,238,0.2), rgba(6,182,212,0.1));" }
            default { "background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(16,185,129,0.1));" }
        }
        
        # Emoji based on genre
        $emoji = switch -Regex ($station.Genre) {
            "News|Talk" { "📻" }
            "Jazz" { "🎷" }
            "Classical" { "🎻" }
            "Rock" { "🎸" }
            "Pop" { "🎤" }
            "Electronic" { "🎛️" }
            default { "📡" }
        }
        
        $encodedURL = [System.Web.HttpUtility]::UrlEncode($station.URL)
        
        # Use logo if available, otherwise use emoji with gradient
        $thumbContent = ""
        if ($station.Logo -and -not [string]::IsNullOrWhiteSpace($station.Logo)) {
            # Check if logo is a local file (no http/https) or external URL
            if ($station.Logo -like "http*") {
                # External URL - use directly (fallback for any we haven't localized)
                $logoURL = $station.Logo
            } else {
                # Local file - serve from root with proper path
                $logoURL = "/$($station.Logo)"
            }
            $thumbContent = "<div class='thumb' style='$gradientStyle background-image: url(""$logoURL""); background-size: 60%; background-repeat: no-repeat; background-position: center;'></div>"
        } else {
            # No logo - use emoji with gradient background
            $thumbContent = "<div class='thumb' style='$gradientStyle display:flex; align-items:center; justify-content:center; font-size:48px;'>$emoji</div>"
        }
        
        $radioItems += @"
<div class="item" onclick="playRadio('$encodedURL', '$($station.Name)', '$($station.Description)')">
  $thumbContent
  <div class="flag radio" style="background: rgba(52,211,153,0.9);">LIVE</div>
  <div class="corner">$($station.Country)</div>
  <div class="meta">
    <strong>$($station.Name)</strong>
    <span>$($station.Genre)</span>
    <span style="font-size:11px; opacity:0.7;">$($station.Description)</span>
  </div>
</div>
"@
    }
    
    # Determine which chip is active
    $allActive = if ($Filter -eq "all") { "on" } else { "" }
    $usaActive = if ($Filter -eq "usa") { "on" } else { "" }
    $ukActive = if ($Filter -eq "uk") { "on" } else { "" }
    $franceActive = if ($Filter -eq "france") { "on" } else { "" }
    $germanyActive = if ($Filter -eq "germany") { "on" } else { "" }
    $netherlandsActive = if ($Filter -eq "netherlands") { "on" } else { "" }
    $italyActive = if ($Filter -eq "italy") { "on" } else { "" }
    $switzerlandActive = if ($Filter -eq "switzerland") { "on" } else { "" }
    $belgiumActive = if ($Filter -eq "belgium") { "on" } else { "" }
    $irelandActive = if ($Filter -eq "ireland") { "on" } else { "" }
    $europeActive = if ($Filter -eq "europe") { "on" } else { "" }
    $newsActive = if ($Filter -eq "news") { "on" } else { "" }
    $musicActive = if ($Filter -eq "music") { "on" } else { "" }
    $jazzActive = if ($Filter -eq "jazz") { "on" } else { "" }
    $classicalActive = if ($Filter -eq "classical") { "on" } else { "" }
    
    # Determine title based on filter
    $titleText = switch ($Filter) {
        "usa" { "USA Radio Stations" }
        "uk" { "UK Radio Stations" }
        "france" { "French Radio Stations" }
        "germany" { "German Radio Stations" }
        "netherlands" { "Dutch Radio Stations" }
        "italy" { "Italian Radio Stations" }
        "switzerland" { "Swiss Radio Stations" }
        "belgium" { "Belgian Radio Stations" }
        "ireland" { "Irish Radio Stations" }
        "europe" { "European Radio Stations" }
        "news" { "News & Talk Radio" }
        "music" { "Music Radio Stations" }
        "jazz" { "Jazz Radio" }
        "classical" { "Classical Radio" }
        default { "Internet Radio - All Stations" }
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search radio stations…" id="searchInput" />
  </div>
  <div class="seg" style="flex-wrap: wrap; gap: 8px;">
    <div class="chip $allActive" onclick="window.location.href='/radio?filter=all'">All</div>
<div class="chip $usaActive" onclick="window.location.href='/radio?filter=usa'"><img src="/menu/us.png" alt="USA" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> USA</div>
    <div class="chip $ukActive" onclick="window.location.href='/radio?filter=uk'"><img src="/menu/gb.png" alt="UK" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> UK</div>
    <div class="chip $franceActive" onclick="window.location.href='/radio?filter=france'"><img src="/menu/fr.png" alt="France" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> France</div>
    <div class="chip $germanyActive" onclick="window.location.href='/radio?filter=germany'"><img src="/menu/de.png" alt="Germany" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> Germany</div>
    <div class="chip $netherlandsActive" onclick="window.location.href='/radio?filter=netherlands'"><img src="/menu/nl.png" alt="Netherlands" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> Netherlands</div>
    <div class="chip $italyActive" onclick="window.location.href='/radio?filter=italy'"><img src="/menu/it.png" alt="Italy" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> Italy</div>
    <div class="chip $switzerlandActive" onclick="window.location.href='/radio?filter=switzerland'"><img src="/menu/ch.png" alt="Switzerland" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> Switzerland</div>
    <div class="chip $belgiumActive" onclick="window.location.href='/radio?filter=belgium'"><img src="/menu/be.png" alt="Belgium" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> Belgium</div>
    <div class="chip $irelandActive" onclick="window.location.href='/radio?filter=ireland'"><img src="/menu/ie.png" alt="Ireland" style="width:16px; height:12px; vertical-align:middle; margin-right:4px;"> Ireland</div>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$titleText</strong>
        <span>$($stations.Count) stations • 100% Legal & Free</span>
      </div>
      <div class="actions">
        <div class="small" onclick="stopRadio()" style="cursor:pointer;">⏹️ Stop</div>
      </div>
    </div>
    <div class="grid">
      $radioItems
    </div>
  </section>
  
  <!-- Radio Player -->
  <div id="radioPlayer" style="position: fixed; bottom: 20px; right: 20px; background: var(--panel); border: 1px solid var(--stroke); border-radius: 16px; padding: 20px; box-shadow: var(--shadow); display: none; min-width: 300px; z-index: 1000;">
    <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
      <div style="font-size: 24px;">📻</div>
      <div style="flex: 1;">
        <div id="radioNowPlaying" style="font-weight: 600; margin-bottom: 4px;">Select a station</div>
        <div id="radioDescription" style="font-size: 12px; color: var(--muted);">Click a station to start</div>
      </div>
      <div onclick="stopRadio()" style="cursor: pointer; font-size: 24px; opacity: 0.7; transition: opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">✕</div>
    </div>
    <audio id="radioAudio" controls style="width: 100%; border-radius: 8px;"></audio>
  </div>
  
  <!-- Error Modal -->
  <div id="errorModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 2000; align-items: center; justify-content: center;">
    <div style="background: var(--panel); border: 1px solid var(--stroke); border-radius: 20px; padding: 32px; max-width: 440px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.5); animation: modalSlideIn 0.3s ease;">
      <div style="text-align: center; margin-bottom: 24px;">
        <div style="width: 80px; height: 80px; margin: 0 auto 16px auto; background-image: url('/menu/radio.png'); background-size: contain; background-repeat: no-repeat; background-position: center;"></div>
        <h2 style="margin: 0 0 12px 0; font-size: 22px; font-weight: 600; color: var(--text);">Unable to Play Station</h2>
        <p style="margin: 0; color: var(--muted); font-size: 15px; line-height: 1.6;">
          This radio stream couldn't be played. This may happen if:
        </p>
      </div>
      <div style="background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.3); border-radius: 12px; padding: 16px; margin-bottom: 24px;">
        <div style="display: flex; align-items: start; gap: 12px; margin-bottom: 10px;">
          <div style="font-size: 20px;">🌍</div>
          <div style="flex: 1;">
            <div style="font-weight: 600; margin-bottom: 4px; font-size: 14px;">Geographic Restriction</div>
            <div style="font-size: 13px; color: var(--muted); line-height: 1.5;">The station may be geo-locked to a specific country or region</div>
          </div>
        </div>
        <div style="display: flex; align-items: start; gap: 12px;">
          <div style="font-size: 20px;">🔗</div>
          <div style="flex: 1;">
            <div style="font-weight: 600; margin-bottom: 4px; font-size: 14px;">Stream URL Changed</div>
            <div style="font-size: 13px; color: var(--muted); line-height: 1.5;">The direct streaming link may have been updated by the broadcaster</div>
          </div>
        </div>
      </div>
      <button onclick="closeErrorModal()" style="width: 100%; padding: 14px; background: linear-gradient(135deg, rgba(96,165,250,0.9), rgba(59,130,246,0.9)); border: none; border-radius: 12px; color: white; font-weight: 600; font-size: 15px; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 12px rgba(96,165,250,0.3);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(96,165,250,0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(96,165,250,0.3)'">
        OK, Got It
      </button>
    </div>
  </div>
  
  <style>
  @keyframes modalSlideIn {
    from {
      opacity: 0;
      transform: translateY(-20px) scale(0.95);
    }
    to {
      opacity: 1;
      transform: translateY(0) scale(1);
    }
  }
  </style>
</div>

<script>
let currentRadio = null;

function showErrorModal() {
  const modal = document.getElementById('errorModal');
  modal.style.display = 'flex';
}

function closeErrorModal() {
  const modal = document.getElementById('errorModal');
  modal.style.display = 'none';
}

// Close modal when clicking outside
document.getElementById('errorModal')?.addEventListener('click', function(e) {
  if (e.target === this) {
    closeErrorModal();
  }
});

function playRadio(url, name, description) {
  const player = document.getElementById('radioPlayer');
  const audio = document.getElementById('radioAudio');
  const nowPlaying = document.getElementById('radioNowPlaying');
  const desc = document.getElementById('radioDescription');
  
  // Decode URL
  url = decodeURIComponent(url);
  
  // Show player
  player.style.display = 'block';
  
  // Update UI
  nowPlaying.textContent = name;
  desc.textContent = description;
  
  // Stop current stream if playing (and track it)
  if (currentRadio) {
    trackRadioStop();
    audio.pause();
    audio.src = '';
  }
  
  // Start new stream
  audio.src = url;
  audio.load();
  audio.play().catch(err => {
    console.error('Playback error:', err);
    showErrorModal();
  });
  
  currentRadio = { url, name, description };
  radioStartTime = Date.now();
}

function stopRadio() {
  const player = document.getElementById('radioPlayer');
  const audio = document.getElementById('radioAudio');
  
  // Track before stopping
  trackRadioStop();
  
  audio.pause();
  audio.src = '';
  player.style.display = 'none';
  currentRadio = null;
}

// ============= RADIO TRACKING =============
var radioStartTime = null;

function trackRadioStop() {
  if (currentRadio && radioStartTime) {
    var listenDuration = (Date.now() - radioStartTime) / 1000; // seconds
    
    // Only track if listened for at least 30 seconds
    if (listenDuration >= 30) {
      fetch('/api/track/radio', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          url: currentRadio.url,
          name: currentRadio.name,
          listenTime: listenDuration
        })
      }).catch(err => console.error('Radio tracking error:', err));
    }
    
    radioStartTime = null;
  }
}

// Track radio on page unload
window.addEventListener('beforeunload', function() {
  trackRadioStop();
});

document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=radio';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Radio"
}

# ============================================================================
# INTERNET TV PAGE
# ============================================================================

function Get-TVPage {
    param(
        [string]$Filter = "all",
        [int]$Page = 1
    )
    
    # Channels per page
    $channelsPerPage = 50
    
    # Get unique countries from TV channels (up to 10)
    $allCountries = ($CONFIG.TVChannels | Select-Object -ExpandProperty Country -Unique | Sort-Object) | Select-Object -First 10
    
    # Get TV channels based on filter
    $allFilteredChannels = switch ($Filter) {
        default {
            if ($Filter -eq "all") {
                $CONFIG.TVChannels
            } else {
                # Filter by country (case-insensitive match)
                $CONFIG.TVChannels | Where-Object { $_.Country -ieq $Filter }
            }
        }
    }
    
    # Calculate pagination
    $totalChannels = @($allFilteredChannels).Count
    $totalPages = [math]::Ceiling($totalChannels / $channelsPerPage)
    if ($Page -lt 1) { $Page = 1 }
    if ($Page -gt $totalPages -and $totalPages -gt 0) { $Page = $totalPages }
    
    $skipCount = ($Page - 1) * $channelsPerPage
    $channels = $allFilteredChannels | Select-Object -Skip $skipCount -First $channelsPerPage
    
    $tvItems = ""
    $index = 0
    foreach ($channel in $channels) {
        $index++
        
        # Generate a gradient background based on genre
        $gradientStyle = switch -Regex ($channel.Genre) {
            "News" { "background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(220,38,38,0.1));" }
            "Business" { "background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(16,185,129,0.1));" }
            "Sports" { "background: linear-gradient(135deg, rgba(251,146,60,0.2), rgba(234,88,12,0.1));" }
            "Entertainment" { "background: linear-gradient(135deg, rgba(236,72,153,0.2), rgba(219,39,119,0.1));" }
            "Documentary" { "background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(147,51,234,0.1));" }
            "Kids" { "background: linear-gradient(135deg, rgba(251,191,36,0.2), rgba(245,158,11,0.1));" }
            "Music" { "background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(37,99,235,0.1));" }
            default { "background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(59,130,246,0.1));" }
        }
        
        # Emoji based on genre
        $emoji = switch -Regex ($channel.Genre) {
            "News" { "📺" }
            "Business" { "💼" }
            "Sports" { "⚽" }
            "Entertainment" { "🎬" }
            "Documentary" { "🎥" }
            "Kids" { "🧸" }
            "Music" { "🎵" }
            default { "📡" }
        }
        
        $encodedURL = [System.Web.HttpUtility]::UrlEncode($channel.URL)
        $encodedName = [System.Web.HttpUtility]::HtmlEncode($channel.Name)
        $encodedDesc = [System.Web.HttpUtility]::HtmlEncode($channel.Description)
        
        # Use logo if available, otherwise use emoji with gradient
        $thumbContent = ""
        if ($channel.Logo -and -not [string]::IsNullOrWhiteSpace($channel.Logo)) {
            # Check if logo is a local file (no http/https) or external URL
            if ($channel.Logo -like "http*") {
                $logoURL = $channel.Logo
            } else {
                $logoURL = "/$($channel.Logo)"
            }
            $thumbContent = "<div class='thumb' style='$gradientStyle background-image: url(""$logoURL""); background-size: 60%; background-repeat: no-repeat; background-position: center;'></div>"
        } else {
            $thumbContent = "<div class='thumb' style='$gradientStyle display:flex; align-items:center; justify-content:center; font-size:48px;'>$emoji</div>"
        }
        
        $tvItems += @"
<div class="item" onclick="playTV('$encodedURL', '$encodedName', '$encodedDesc')">
  $thumbContent
  <div class="flag tv" style="background: rgba(96,165,250,0.9);">LIVE</div>
  <div class="corner">$($channel.Country)</div>
  <div class="meta">
    <strong>$($channel.Name)</strong>
    <span>$($channel.Genre)</span>
    <span style="font-size:11px; opacity:0.7;">$($channel.Description)</span>
  </div>
</div>
"@
    }
    
    # Build country filter chips dynamically
    $countryChips = ""
    $allActive = if ($Filter -eq "all") { "on" } else { "" }
    
    # Country flag mappings
    $countryFlags = @{
        "France" = "fr"
        "USA" = "us"
        "UK" = "gb"
        "Germany" = "de"
        "Spain" = "es"
        "Italy" = "it"
        "Qatar" = "qa"
        "China" = "cn"
        "Japan" = "jp"
        "South Korea" = "kr"
        "International" = "un"
    }
    
    foreach ($country in $allCountries) {
        $countryActive = if ($Filter -ieq $country) { "on" } else { "" }
        $countryLower = $country.ToLower().Replace(" ", "")
        $flagCode = if ($countryFlags.ContainsKey($country)) { $countryFlags[$country] } else { "un" }
        $countryChips += "<div class='chip $countryActive' onclick=""window.location.href='/tv?filter=$countryLower'""><img src='/menu/$flagCode.png' alt='$country' style='width:16px; height:12px; vertical-align:middle; margin-right:4px;'> $country</div>`n"
    }
    
    # Determine title based on filter
    $titleText = if ($Filter -eq "all") { 
        "Internet TV - All Channels" 
    } else { 
        "$Filter TV Channels"
    }
    
    # Build pagination controls
    $paginationHtml = ""
    if ($totalPages -gt 1) {
        $prevDisabled = if ($Page -le 1) { "opacity: 0.5; pointer-events: none;" } else { "" }
        $nextDisabled = if ($Page -ge $totalPages) { "opacity: 0.5; pointer-events: none;" } else { "" }
        
        $paginationHtml = @"
<div style="display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 20px; padding: 16px;">
  <a href="/tv?filter=$Filter&page=$($Page - 1)" class="chip" style="$prevDisabled">← Previous</a>
  <span style="color: var(--muted);">Page $Page of $totalPages ($totalChannels channels)</span>
  <a href="/tv?filter=$Filter&page=$($Page + 1)" class="chip" style="$nextDisabled">Next →</a>
</div>
"@
    }
    
    $channelCountText = if ($totalPages -gt 1) {
        "Showing $($skipCount + 1)-$($skipCount + @($channels).Count) of $totalChannels channels"
    } else {
        "$totalChannels channels"
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search TV channels…" id="searchInput" />
  </div>
  <div class="seg" style="flex-wrap: wrap; gap: 8px;">
    <div class="chip $allActive" onclick="window.location.href='/tv?filter=all'">All</div>
    $countryChips
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>$titleText</strong>
        <span>$channelCountText • 100% Legal & Free</span>
      </div>
      <div class="actions">
        <div class="small" onclick="stopTV()" style="cursor:pointer;">⏹️ Stop</div>
      </div>
    </div>
    <div class="grid">
      $tvItems
    </div>
    $paginationHtml
  </section>
  
  <!-- TV Player Modal -->
  <div id="tvPlayerModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); backdrop-filter: blur(8px); z-index: 2000; align-items: center; justify-content: center;">
    <div style="width: 90%; max-width: 1200px; background: var(--panel); border-radius: 20px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.5);">
      <div style="display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: rgba(0,0,0,0.3); border-bottom: 1px solid var(--stroke);">
        <div style="display: flex; align-items: center; gap: 12px;">
          <div style="font-size: 24px;">📺</div>
          <div>
            <div id="tvNowPlaying" style="font-weight: 600; font-size: 16px;">Select a channel</div>
            <div id="tvDescription" style="font-size: 12px; color: var(--muted);">Click a channel to start watching</div>
          </div>
        </div>
        <div onclick="stopTV()" style="cursor: pointer; font-size: 28px; opacity: 0.7; transition: opacity 0.2s; padding: 8px;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">✕</div>
      </div>
      <div style="position: relative; padding-top: 56.25%; background: #000;">
        <video id="tvVideo" controls autoplay style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" playsinline></video>
      </div>
    </div>
  </div>
  
  <!-- Error Modal -->
  <div id="tvErrorModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 3000; align-items: center; justify-content: center;">
    <div style="background: var(--panel); border: 1px solid var(--stroke); border-radius: 20px; padding: 32px; max-width: 440px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.5); animation: modalSlideIn 0.3s ease;">
      <div style="text-align: center; margin-bottom: 24px;">
        <div style="font-size: 64px; margin-bottom: 16px;">📺</div>
        <h2 style="margin: 0 0 12px 0; font-size: 22px; font-weight: 600; color: var(--text);">Unable to Play Channel</h2>
        <p style="margin: 0; color: var(--muted); font-size: 15px; line-height: 1.6;">
          This TV stream couldn't be played. This may happen if:
        </p>
      </div>
      <div style="background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.3); border-radius: 12px; padding: 16px; margin-bottom: 24px;">
        <div style="display: flex; align-items: start; gap: 12px; margin-bottom: 10px;">
          <div style="font-size: 20px;">🌍</div>
          <div style="flex: 1;">
            <div style="font-weight: 600; margin-bottom: 4px; font-size: 14px;">Geographic Restriction</div>
            <div style="font-size: 13px; color: var(--muted); line-height: 1.5;">The channel may be geo-locked to a specific country or region</div>
          </div>
        </div>
        <div style="display: flex; align-items: start; gap: 12px;">
          <div style="font-size: 20px;">🔗</div>
          <div style="flex: 1;">
            <div style="font-weight: 600; margin-bottom: 4px; font-size: 14px;">Stream URL Changed</div>
            <div style="font-size: 13px; color: var(--muted); line-height: 1.5;">The streaming link may have been updated by the broadcaster</div>
          </div>
        </div>
      </div>
      <button onclick="closeTVErrorModal()" style="width: 100%; padding: 14px; background: linear-gradient(135deg, rgba(96,165,250,0.9), rgba(59,130,246,0.9)); border: none; border-radius: 12px; color: white; font-weight: 600; font-size: 15px; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 12px rgba(96,165,250,0.3);" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
        OK, Got It
      </button>
    </div>
  </div>
  
  <style>
  @keyframes modalSlideIn {
    from { opacity: 0; transform: translateY(-20px) scale(0.95); }
    to { opacity: 1; transform: translateY(0) scale(1); }
  }
  </style>
</div>

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
let currentTV = null;
let hlsPlayer = null;

function showTVErrorModal() {
  document.getElementById('tvErrorModal').style.display = 'flex';
}

function closeTVErrorModal() {
  document.getElementById('tvErrorModal').style.display = 'none';
}

document.getElementById('tvErrorModal')?.addEventListener('click', function(e) {
  if (e.target === this) closeTVErrorModal();
});

function playTV(url, name, description) {
  const modal = document.getElementById('tvPlayerModal');
  const video = document.getElementById('tvVideo');
  const nowPlaying = document.getElementById('tvNowPlaying');
  const desc = document.getElementById('tvDescription');
  
  // Decode URL
  url = decodeURIComponent(url);
  
  // Show modal
  modal.style.display = 'flex';
  
  // Update UI
  nowPlaying.textContent = name;
  desc.textContent = description;
  
  // Stop current stream if playing
  if (hlsPlayer) {
    hlsPlayer.destroy();
    hlsPlayer = null;
  }
  video.pause();
  video.src = '';
  
  // Check if it's an HLS stream
  if (url.includes('.m3u8')) {
    if (Hls.isSupported()) {
      hlsPlayer = new Hls({
        enableWorker: true,
        lowLatencyMode: true
      });
      hlsPlayer.loadSource(url);
      hlsPlayer.attachMedia(video);
      hlsPlayer.on(Hls.Events.MANIFEST_PARSED, function() {
        video.play().catch(err => {
          console.error('Playback error:', err);
          showTVErrorModal();
        });
      });
      hlsPlayer.on(Hls.Events.ERROR, function(event, data) {
        if (data.fatal) {
          console.error('HLS error:', data);
          showTVErrorModal();
        }
      });
    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      // Safari native HLS support
      video.src = url;
      video.addEventListener('loadedmetadata', function() {
        video.play().catch(err => {
          console.error('Playback error:', err);
          showTVErrorModal();
        });
      });
    } else {
      showTVErrorModal();
    }
  } else {
    // Direct video URL
    video.src = url;
    video.load();
    video.play().catch(err => {
      console.error('Playback error:', err);
      showTVErrorModal();
    });
  }
  
  currentTV = { url, name, description };
}

function stopTV() {
  const modal = document.getElementById('tvPlayerModal');
  const video = document.getElementById('tvVideo');
  
  if (hlsPlayer) {
    hlsPlayer.destroy();
    hlsPlayer = null;
  }
  video.pause();
  video.src = '';
  modal.style.display = 'none';
  currentTV = null;
}

// Close modal when clicking outside video
document.getElementById('tvPlayerModal')?.addEventListener('click', function(e) {
  if (e.target === this) stopTV();
});

// ESC key to close
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape' && currentTV) stopTV();
});

document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + '&type=tv';
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Internet TV"
}

function Get-AnalysisPage {
    # Get current logged in user - MUST access .Username property from the session object
    $currentUsername = "guest"
    if ($Global:CurrentUser -and $Global:CurrentUser.Username) {
        $currentUsername = $Global:CurrentUser.Username
    }
    $userDbPath = Join-Path $CONFIG.UsersDBPath "$currentUsername.db"
    
    # Initialize user stats with defaults
    $videoStats = @{ total_watched = 0; completed_count = 0; total_plays = 0; avg_completion = 0 }
    $musicStats = @{ unique_tracks = 0; total_plays = 0; total_time = 0 }
    $radioStats = @{ unique_stations = 0; total_listens = 0; total_time = 0 }
    $topMusic = @()
    
    # Get user stats if database exists
    if (Test-Path $userDbPath) {
        try {
            $videoStatsResult = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as total_watched,
    SUM(CASE WHEN completed = 1 THEN 1 ELSE 0 END) as completed_count,
    SUM(watch_count) as total_plays,
    AVG(percent_watched) as avg_completion
FROM video_progress
"@
            if ($videoStatsResult) { $videoStats = $videoStatsResult }
            
            $musicStatsResult = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as unique_tracks,
    SUM(play_count) as total_plays,
    SUM(total_play_time_seconds) as total_time
FROM music_history
"@
            if ($musicStatsResult) { $musicStats = $musicStatsResult }
            
            $radioStatsResult = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as unique_stations,
    SUM(listen_count) as total_listens,
    SUM(total_listen_time_seconds) as total_time
FROM radio_history
"@
            if ($radioStatsResult) { $radioStats = $radioStatsResult }
            
            $topMusic = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT artist, title, play_count
FROM music_history
ORDER BY play_count DESC
LIMIT 5
"@
        } catch {
            Write-Host "  [!] Error reading user stats: $_" -ForegroundColor Yellow
        }
    }
    
    # Safely get values with null handling
    $videoWatched = if ($videoStats.total_watched) { $videoStats.total_watched } else { 0 }
    $videoCompleted = if ($videoStats.completed_count) { $videoStats.completed_count } else { 0 }
    $videoPlays = if ($videoStats.total_plays) { $videoStats.total_plays } else { 0 }
    $videoAvgCompletion = if ($videoStats.avg_completion) { [math]::Round($videoStats.avg_completion, 1) } else { 0 }
    
    $musicTracks = if ($musicStats.unique_tracks) { $musicStats.unique_tracks } else { 0 }
    $musicPlays = if ($musicStats.total_plays) { $musicStats.total_plays } else { 0 }
    $musicTime = if ($musicStats.total_time) { $musicStats.total_time } else { 0 }
    $musicHours = [math]::Round(($musicTime / 3600), 1)
    
    $radioStations = if ($radioStats.unique_stations) { $radioStats.unique_stations } else { 0 }
    $radioListens = if ($radioStats.total_listens) { $radioStats.total_listens } else { 0 }
    $radioTime = if ($radioStats.total_time) { $radioStats.total_time } else { 0 }
    $radioHours = [math]::Round(($radioTime / 3600), 1)
    
    # ========== LIBRARY STATISTICS ==========
    
    # Video Stats
    $totalVideos = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos").count
    $totalVideoSize = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM videos").total
    $videoSizeGB = [math]::Round($totalVideoSize / 1GB, 2)
    
    # Low quality video breakdown
    $lowQualityCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE is_low_quality = 1").count
    $hdCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE height >= 720 AND height < 1080").count
    $fullHDCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE height >= 1080 AND height < 2160").count
    $fourKCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE height >= 2160").count
    $unknownResCount = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos WHERE height IS NULL OR height = 0").count
    
    # Video formats breakdown
    $videoFormats = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
SELECT UPPER(format) as format, COUNT(*) as count 
FROM videos 
WHERE format IS NOT NULL AND format != ''
GROUP BY UPPER(format) 
ORDER BY count DESC 
LIMIT 5
"@
    
    # Music Stats
    $totalMusic = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(*) as count FROM music").count
    $totalMusicSize = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM music").total
    $musicSizeGB = [math]::Round($totalMusicSize / 1GB, 2)
    $uniqueArtists = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(DISTINCT artist) as count FROM music WHERE artist IS NOT NULL AND artist != ''").count
    $uniqueAlbums = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(DISTINCT album) as count FROM music WHERE album IS NOT NULL AND album != ''").count
    
    # Music formats breakdown
    $musicFormats = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query @"
SELECT UPPER(format) as format, COUNT(*) as count 
FROM music 
WHERE format IS NOT NULL AND format != ''
GROUP BY UPPER(format) 
ORDER BY count DESC 
LIMIT 5
"@
    
    # Pictures Stats
    $totalPictures = (Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query "SELECT COUNT(*) as count FROM images").count
    $totalPicturesSize = (Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM images").total
    $picturesSizeGB = [math]::Round($totalPicturesSize / 1GB, 2)
    
    # PDF/EPUB Stats - count by file extension since there's no format column
    $totalPDFs = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COUNT(*) as count FROM pdfs WHERE LOWER(filepath) LIKE '%.pdf'").count
    $totalEPUBs = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COUNT(*) as count FROM pdfs WHERE LOWER(filepath) LIKE '%.epub'").count
    $totalDocsSize = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM pdfs").total
    $docsSizeGB = [math]::Round($totalDocsSize / 1GB, 2)
    
    # Total library stats
    $totalItems = $totalVideos + $totalMusic + $totalPictures + $totalPDFs + $totalEPUBs
    $totalSizeGB = $videoSizeGB + $musicSizeGB + $picturesSizeGB + $docsSizeGB
    
    # Build top music HTML
    $topMusicHtml = ""
    $rank = 1
    foreach ($track in $topMusic) {
        if ($track) {
            $artistTitle = if ($track.artist -and $track.title) {
                "$($track.artist) - $($track.title)"
            } elseif ($track.title) {
                $track.title
            } else {
                "(Unknown)"
            }
            $topMusicHtml += "<div class='top-item'><span class='rank'>$rank</span><span class='track-name'>$artistTitle</span><span class='plays'>$($track.play_count) plays</span></div>"
            $rank++
        }
    }
    if (-not $topMusicHtml) {
        $topMusicHtml = "<div class='no-data'>No music history yet</div>"
    }
    
    # Build video formats HTML
    $videoFormatsHtml = ""
    foreach ($fmt in $videoFormats) {
        if ($fmt -and $fmt.format) {
            $videoFormatsHtml += "<span class='format-tag'>$($fmt.format) ($($fmt.count))</span>"
        }
    }
    if (-not $videoFormatsHtml) { $videoFormatsHtml = "<span class='format-tag'>N/A</span>" }
    
    # Build music formats HTML
    $musicFormatsHtml = ""
    foreach ($fmt in $musicFormats) {
        if ($fmt -and $fmt.format) {
            $musicFormatsHtml += "<span class='format-tag'>$($fmt.format) ($($fmt.count))</span>"
        }
    }
    if (-not $musicFormatsHtml) { $musicFormatsHtml = "<span class='format-tag'>N/A</span>" }
    
    $content = @"
<style>
.analysis-container { padding: 24px; max-width: 1400px; margin: 0 auto; }
.analysis-header { 
    display: flex; 
    align-items: center; 
    gap: 16px; 
    margin-bottom: 32px;
    padding-bottom: 20px;
    border-bottom: 1px solid rgba(255,255,255,0.1);
}
.analysis-header h1 { 
    font-size: 28px; 
    margin: 0; 
    display: flex; 
    align-items: center; 
    gap: 12px;
}
.user-badge {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    padding: 6px 16px;
    border-radius: 20px;
    font-size: 14px;
    font-weight: 600;
}

.stats-section { margin-bottom: 32px; }
.section-title {
    font-size: 18px;
    font-weight: 600;
    margin-bottom: 16px;
    display: flex;
    align-items: center;
    gap: 10px;
    color: rgba(255,255,255,0.9);
}
.section-title img { width: 24px; height: 24px; }

.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 20px;
}

.stat-card {
    background: rgba(255,255,255,0.03);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 16px;
    padding: 24px;
    transition: all 0.3s ease;
}
.stat-card:hover {
    background: rgba(255,255,255,0.05);
    border-color: rgba(255,255,255,0.15);
    transform: translateY(-2px);
}
.stat-card.highlight {
    background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.1));
    border-color: rgba(99,102,241,0.3);
}
.stat-card-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
}
.stat-card-header img { width: 40px; height: 40px; opacity: 0.9; }
.stat-card-header h3 { 
    font-size: 16px; 
    margin: 0; 
    color: rgba(255,255,255,0.7);
    font-weight: 500;
}
.stat-value {
    font-size: 36px;
    font-weight: 700;
    color: #fff;
    margin-bottom: 4px;
    line-height: 1;
}
.stat-value.accent { color: #6366f1; }
.stat-value.green { color: #22c55e; }
.stat-value.yellow { color: #eab308; }
.stat-value.red { color: #ef4444; }
.stat-label {
    font-size: 13px;
    color: rgba(255,255,255,0.5);
    margin-bottom: 16px;
}
.stat-details {
    font-size: 13px;
    color: rgba(255,255,255,0.6);
    line-height: 1.8;
}
.stat-details div { display: flex; justify-content: space-between; }
.stat-details .value { color: rgba(255,255,255,0.9); font-weight: 500; }

.quality-breakdown {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
    margin-top: 12px;
}
.quality-item {
    background: rgba(255,255,255,0.03);
    padding: 12px;
    border-radius: 8px;
    text-align: center;
}
.quality-item .q-value {
    font-size: 20px;
    font-weight: 700;
    color: #fff;
}
.quality-item .q-label {
    font-size: 11px;
    color: rgba(255,255,255,0.5);
    text-transform: uppercase;
    margin-top: 4px;
}
.quality-item.low .q-value { color: #ef4444; }
.quality-item.hd .q-value { color: #eab308; }
.quality-item.fhd .q-value { color: #22c55e; }
.quality-item.uhd .q-value { color: #6366f1; }

.format-tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.format-tag {
    background: rgba(255,255,255,0.08);
    padding: 4px 10px;
    border-radius: 12px;
    font-size: 11px;
    color: rgba(255,255,255,0.7);
}

.top-tracks-card {
    background: rgba(255,255,255,0.03);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 16px;
    padding: 24px;
}
.top-item {
    display: flex;
    align-items: center;
    padding: 12px 0;
    border-bottom: 1px solid rgba(255,255,255,0.05);
}
.top-item:last-child { border-bottom: none; }
.top-item .rank {
    width: 28px;
    height: 28px;
    background: rgba(99,102,241,0.2);
    color: #6366f1;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    font-size: 12px;
    margin-right: 12px;
}
.top-item .track-name { flex: 1; font-size: 14px; }
.top-item .plays { color: rgba(255,255,255,0.5); font-size: 13px; }
.no-data { color: rgba(255,255,255,0.4); font-style: italic; padding: 20px; text-align: center; }

.library-summary {
    background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(139,92,246,0.05));
    border: 1px solid rgba(99,102,241,0.2);
    border-radius: 20px;
    padding: 32px;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 24px;
    text-align: center;
    margin-bottom: 32px;
}
.summary-item .sum-value {
    font-size: 32px;
    font-weight: 700;
    color: #fff;
}
.summary-item .sum-label {
    font-size: 12px;
    color: rgba(255,255,255,0.5);
    text-transform: uppercase;
    margin-top: 4px;
}

@media (max-width: 768px) {
    .stats-grid { grid-template-columns: 1fr; }
    .quality-breakdown { grid-template-columns: 1fr 1fr; }
    .library-summary { grid-template-columns: repeat(2, 1fr); }
}
</style>

<div class="analysis-container">
    <div class="analysis-header">
        <h1>
            <img src="/menu/analysis.png" style="width:36px; height:36px;">
            Statistics & Analysis
        </h1>
        <span class="user-badge">👤 $currentUsername</span>
    </div>

    <!-- Library Summary -->
    <div class="library-summary">
        <div class="summary-item">
            <div class="sum-value">$totalItems</div>
            <div class="sum-label">Total Items</div>
        </div>
        <div class="summary-item">
            <div class="sum-value">$([math]::Round($totalSizeGB, 1)) GB</div>
            <div class="sum-label">Total Size</div>
        </div>
        <div class="summary-item">
            <div class="sum-value">$totalVideos</div>
            <div class="sum-label">Videos</div>
        </div>
        <div class="summary-item">
            <div class="sum-value">$totalMusic</div>
            <div class="sum-label">Music</div>
        </div>
        <div class="summary-item">
            <div class="sum-value">$totalPictures</div>
            <div class="sum-label">Pictures</div>
        </div>
        <div class="summary-item">
            <div class="sum-value">$($totalPDFs + $totalEPUBs)</div>
            <div class="sum-label">Documents</div>
        </div>
    </div>

    <!-- User Activity Section -->
    <div class="stats-section">
        <div class="section-title">👤 Your Activity</div>
        <div class="stats-grid">
            <div class="stat-card highlight">
                <div class="stat-card-header">
                    <img src="/menu/movie.png">
                    <h3>Videos Watched</h3>
                </div>
                <div class="stat-value accent">$videoWatched</div>
                <div class="stat-label">unique videos</div>
                <div class="stat-details">
                    <div><span>Completed</span><span class="value">$videoCompleted</span></div>
                    <div><span>Total Plays</span><span class="value">$videoPlays</span></div>
                    <div><span>Avg. Completion</span><span class="value">$videoAvgCompletion%</span></div>
                </div>
            </div>
            
            <div class="stat-card highlight">
                <div class="stat-card-header">
                    <img src="/menu/music.png">
                    <h3>Music Listened</h3>
                </div>
                <div class="stat-value accent">$musicTracks</div>
                <div class="stat-label">unique tracks</div>
                <div class="stat-details">
                    <div><span>Total Plays</span><span class="value">$musicPlays</span></div>
                    <div><span>Time Listened</span><span class="value">$musicHours hrs</span></div>
                </div>
            </div>
            
            <div class="stat-card highlight">
                <div class="stat-card-header">
                    <img src="/menu/radio.png">
                    <h3>Radio Stations</h3>
                </div>
                <div class="stat-value accent">$radioStations</div>
                <div class="stat-label">stations tuned</div>
                <div class="stat-details">
                    <div><span>Total Listens</span><span class="value">$radioListens</span></div>
                    <div><span>Time Listened</span><span class="value">$radioHours hrs</span></div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Top Music -->
    <div class="stats-section">
        <div class="section-title">🎵 Your Top 5 Tracks</div>
        <div class="top-tracks-card">
            $topMusicHtml
        </div>
    </div>

    <!-- Library Statistics Section -->
    <div class="stats-section">
        <div class="section-title">📚 Library Statistics</div>
        <div class="stats-grid">
            <div class="stat-card">
                <div class="stat-card-header">
                    <img src="/menu/movie.png">
                    <h3>Video Library</h3>
                </div>
                <div class="stat-value">$totalVideos</div>
                <div class="stat-label">$videoSizeGB GB total</div>
                <div class="quality-breakdown">
                    <div class="quality-item low">
                        <div class="q-value">$lowQualityCount</div>
                        <div class="q-label">&lt; 720p</div>
                    </div>
                    <div class="quality-item hd">
                        <div class="q-value">$hdCount</div>
                        <div class="q-label">720p HD</div>
                    </div>
                    <div class="quality-item fhd">
                        <div class="q-value">$fullHDCount</div>
                        <div class="q-label">1080p FHD</div>
                    </div>
                    <div class="quality-item uhd">
                        <div class="q-value">$fourKCount</div>
                        <div class="q-label">4K UHD</div>
                    </div>
                </div>
                <div style="margin-top:8px; font-size:12px; color:rgba(255,255,255,0.4);">Unknown resolution: $unknownResCount</div>
                <div class="format-tags">$videoFormatsHtml</div>
            </div>
            
            <div class="stat-card">
                <div class="stat-card-header">
                    <img src="/menu/music.png">
                    <h3>Music Library</h3>
                </div>
                <div class="stat-value">$totalMusic</div>
                <div class="stat-label">$musicSizeGB GB total</div>
                <div class="stat-details">
                    <div><span>Artists</span><span class="value">$uniqueArtists</span></div>
                    <div><span>Albums</span><span class="value">$uniqueAlbums</span></div>
                </div>
                <div class="format-tags">$musicFormatsHtml</div>
            </div>
            
            <div class="stat-card">
                <div class="stat-card-header">
                    <img src="/menu/pictures.png">
                    <h3>Pictures Library</h3>
                </div>
                <div class="stat-value">$totalPictures</div>
                <div class="stat-label">$picturesSizeGB GB total</div>
            </div>
            
            <div class="stat-card">
                <div class="stat-card-header">
                    <img src="/menu/pdf.png">
                    <h3>Documents Library</h3>
                </div>
                <div class="stat-value">$($totalPDFs + $totalEPUBs)</div>
                <div class="stat-label">$docsSizeGB GB total</div>
                <div class="stat-details">
                    <div><span>PDF Files</span><span class="value">$totalPDFs</span></div>
                    <div><span>EPUB Books</span><span class="value">$totalEPUBs</span></div>
                </div>
            </div>
        </div>
    </div>

    <!-- Quality Analysis Section -->
    <div class="stats-section">
        <div class="section-title">📈 Quality Analysis</div>
        <div class="stats-grid">
            <div class="stat-card">
                <div class="stat-card-header">
                    <h3>⚠️ Low Quality Videos</h3>
                </div>
                <div class="stat-value $(if ($lowQualityCount -gt 0) { 'yellow' } else { 'green' })">$lowQualityCount</div>
                <div class="stat-label">videos below 720p resolution</div>
                <div class="stat-details">
                    $(if ($lowQualityCount -gt 0) {
                        "<div style='color:rgba(234,179,8,0.8);'>💡 Consider upgrading to higher quality versions</div>"
                    } else {
                        "<div style='color:rgba(34,197,94,0.8);'>✓ All videos are HD quality or better!</div>"
                    })
                </div>
            </div>
            
            <div class="stat-card">
                <div class="stat-card-header">
                    <h3>🎬 HD Content Ratio</h3>
                </div>
                <div class="stat-value green">$([math]::Round((($hdCount + $fullHDCount + $fourKCount) / [math]::Max(($totalVideos - $unknownResCount), 1)) * 100, 1))%</div>
                <div class="stat-label">of videos with known resolution are 720p or better</div>
                <div class="stat-details">
                    <div><span>4K UHD</span><span class="value" style="color:#6366f1;">$fourKCount videos</span></div>
                    <div><span>1080p Full HD</span><span class="value" style="color:#22c55e;">$fullHDCount videos</span></div>
                    <div><span>720p HD</span><span class="value" style="color:#eab308;">$hdCount videos</span></div>
                </div>
            </div>
        </div>
    </div>
</div>
"@
    
    return Get-HTMLPage -Content $content -Title "NexusStack - Statistics & Analysis"
}

function Get-SearchPage {
    param(
        [string]$Query,
        [string]$Type = ""
    )
    
    if ([string]::IsNullOrWhiteSpace($Query)) {
        return Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>No search query provided</h1><p><a href='/'>← Back to Home</a></p></div>" -Title "Search"
    }
    
    $escapedQuery = [System.Web.HttpUtility]::HtmlEncode($Query)
    $resultItems = ""
    $totalResults = 0
    
    # Search based on type or search all if no type specified
    $searchTypes = if ($Type) { @($Type) } else { @('videos', 'music', 'pictures', 'pdfs', 'radio', 'tv') }
    
    foreach ($searchType in $searchTypes) {
        switch ($searchType) {
            'videos' {
                # ENHANCED: Search in ALL video metadata fields including cast, director, genre, overview
                # Note: "cast" is a reserved SQL keyword, so we use [cast] or "cast" to escape it
                $results = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                    -Query "SELECT * FROM videos WHERE title LIKE @query OR filename LIKE @query OR genre LIKE @query OR director LIKE @query OR [cast] LIKE @query OR overview LIKE @query ORDER BY CASE WHEN title LIKE @query THEN 1 WHEN [cast] LIKE @query THEN 2 WHEN director LIKE @query THEN 3 WHEN genre LIKE @query THEN 4 ELSE 5 END, title" `
                    -SqlParameters @{ query = "%$Query%" }
                
                foreach ($video in $results) {
                    $totalResults++
                    $size = [math]::Round($video.size_bytes / 1GB, 2)
                    $encodedPath = [System.Web.HttpUtility]::UrlEncode($video.filepath)
                    
                    $posterStyle = ""
                    if ($video.poster_url -and -not [string]::IsNullOrWhiteSpace($video.poster_url)) {
                        $posterStyle = "background-image: url('/poster/$($video.poster_url)');"
                    }
                    
                    # Build match context (show what matched)
                    $matchContext = @()
                    if ($video.cast -and $video.cast -like "*$Query*") { $matchContext += "Cast: $($video.cast.Substring(0, [Math]::Min(50, $video.cast.Length)))..." }
                    if ($video.director -and $video.director -like "*$Query*") { $matchContext += "Director: $($video.director)" }
                    if ($video.genre -and $video.genre -like "*$Query*") { $matchContext += "Genre: $($video.genre)" }
                    $matchInfo = if ($matchContext.Count -gt 0) { "<span style='font-size:10px; opacity:0.6; display:block; margin-top:2px;'>$($matchContext[0])</span>" } else { "" }
                    
                    $yearInfo = if ($video.year) { " ($($video.year))" } else { "" }
                    
                    $resultItems += @"
<a class="item" href="/play/$($video.id)">
  <div class="thumb" style="$posterStyle"></div>
  <div class="flag video">Video</div>
  <div class="corner">▶</div>
  <div class="meta">
    <strong>$($video.title)$yearInfo</strong>
    <span>$($video.format) • ${size}GB</span>
    $matchInfo
  </div>
</a>
"@
                }
            }
            'music' {
                # ENHANCED: Search in ALL music metadata fields including genre
                $results = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB `
                    -Query "SELECT * FROM music WHERE title LIKE @query OR artist LIKE @query OR album LIKE @query OR genre LIKE @query OR filename LIKE @query ORDER BY CASE WHEN title LIKE @query THEN 1 WHEN artist LIKE @query THEN 2 WHEN album LIKE @query THEN 3 WHEN genre LIKE @query THEN 4 ELSE 5 END, title" `
                    -SqlParameters @{ query = "%$Query%" }
                
                foreach ($music in $results) {
                    $totalResults++
                    $size = [math]::Round($music.size_bytes / 1MB, 1)
                    $artist = if ($music.artist) { $music.artist } else { "Unknown Artist" }
                    $album = if ($music.album) { $music.album } else { "Unknown Album" }
                    
                    $artStyle = ""
                    if ($music.album_art_cached -and -not [string]::IsNullOrWhiteSpace($music.album_art_cached)) {
                        $artStyle = "background-image: url('/albumart/$($music.album_art_cached)');"
                    }
                    elseif ($music.album_art_url -and -not [string]::IsNullOrWhiteSpace($music.album_art_url)) {
                        $artStyle = "background-image: url('/poster/$($music.album_art_url)');"
                    }
                    
                    # Show genre if it matched
                    $genreInfo = if ($music.genre -and $music.genre -like "*$Query*") { "<span style='font-size:10px; opacity:0.6;'>Genre: $($music.genre)</span>" } else { "" }
                    
                    $resultItems += @"
<a class="item" href="/play-audio/$($music.id)">
  <div class="thumb" style="$artStyle"></div>
  <div class="flag audio">Audio</div>
  <div class="corner">♫</div>
  <div class="meta">
    <strong>$($music.title)</strong>
    <span>$artist</span>
    <span style="font-size:11px; opacity:0.7;">$album</span>
    $genreInfo
  </div>
</a>
"@
                }
            }
            'pictures' {
                # ENHANCED: Search in pictures including camera info and filepath
                $results = Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB `
                    -Query "SELECT * FROM images WHERE filename LIKE @query OR filepath LIKE @query OR camera_make LIKE @query OR camera_model LIKE @query ORDER BY filename" `
                    -SqlParameters @{ query = "%$Query%" }
                
                foreach ($picture in $results) {
                    $totalResults++
                    $size = [math]::Round($picture.size_bytes / 1MB, 1)
                    $encodedPath = [System.Web.HttpUtility]::UrlEncode($picture.filepath)
                    $thumbStyle = "background-image: url('/image/$($encodedPath)');"
                    
                    $resultItems += @"
<a class="item" href="/view-image?path=$encodedPath" target="_blank">
  <div class="thumb" style="$thumbStyle background-size: cover; background-position: center;"></div>
  <div class="flag picture">Picture</div>
  <div class="corner">🖼</div>
  <div class="meta">
    <strong>$($picture.filename)</strong>
    <span>$($picture.format) • ${size}MB</span>
  </div>
</a>
"@
                }
            }
            'pdfs' {
                # ENHANCED: Search in ALL PDF metadata fields including author, subject, keywords
                $results = Invoke-SqliteQuery -DataSource $CONFIG.PDFDB `
                    -Query "SELECT * FROM pdfs WHERE title LIKE @query OR filename LIKE @query OR author LIKE @query OR subject LIKE @query OR keywords LIKE @query ORDER BY CASE WHEN title LIKE @query THEN 1 WHEN author LIKE @query THEN 2 ELSE 3 END, title" `
                    -SqlParameters @{ query = "%$Query%" }
                
                foreach ($pdf in $results) {
                    $totalResults++
                    $sizeMB = [math]::Round($pdf.size_bytes / 1MB, 1)
                    $encodedPath = [System.Web.HttpUtility]::UrlEncode($pdf.filepath)
                    
                    # Detect file type
                    $fileExtension = [System.IO.Path]::GetExtension($pdf.filepath).ToLower()
                    $isEpub = ($pdf.PSObject.Properties.Name -contains 'file_type' -and $pdf.file_type -eq 'epub') -or $fileExtension -eq '.epub'
                    
                    # Set badge text and styling
                    $badgeText = if ($isEpub) { "EPUB" } else { "PDF" }
                    $badgeClass = if ($isEpub) { "epub" } else { "pdf" }
                    $fileTypeLabel = if ($isEpub) { "EPUB" } else { "PDF" }
                    
                    $thumbStyle = ""
                    if ($pdf.preview_image -and -not [string]::IsNullOrWhiteSpace($pdf.preview_image)) {
                        $thumbStyle = "background-image: url('/pdfpreview/$($pdf.preview_image)'); background-size: cover; background-position: center;"
                    }
                    else {
                        if ($isEpub) {
                            $thumbStyle = "background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(168,85,247,0.15)); display:flex; align-items:center; justify-content:center; font-size:48px;"
                        }
                        else {
                            $thumbStyle = "background: linear-gradient(135deg, rgba(220,38,38,0.15), rgba(251,191,36,0.15)); display:flex; align-items:center; justify-content:center; font-size:48px;"
                        }
                    }
                    
                    $iconHtml = if (-not $pdf.preview_image -or [string]::IsNullOrWhiteSpace($pdf.preview_image)) { "<img src='/menu/pdf.png' style='width:48px; height:48px;'>" } else { "" }
                    
                    # Different click action for EPUB vs PDF
                    $clickAction = if ($isEpub) {
                        "href='/read-epub?path=$encodedPath'"
                    } else {
                        "href='/view-pdf?path=$encodedPath' target='_blank'"
                    }
                    
                    # Show author if matched
                    $authorInfo = if ($pdf.author -and $pdf.author -like "*$Query*") { "<span style='font-size:10px; opacity:0.6;'>by $($pdf.author)</span>" } else { "" }
                    
                    $resultItems += @"
<a class="item" $clickAction>
  <div class="thumb" style="$thumbStyle">$iconHtml</div>
  <div class="flag $badgeClass">$badgeText</div>
  <div class="corner">📖</div>
  <div class="meta">
    <strong>$($pdf.title)</strong>
    <span>$fileTypeLabel • ${sizeMB}MB</span>
    $authorInfo
  </div>
</a>
"@
                }
            }
            'radio' {
                # Search in radio stations: name, country, genre, description
                $results = $CONFIG.RadioStations | Where-Object {
                    $_.Name -like "*$Query*" -or 
                    $_.Country -like "*$Query*" -or 
                    $_.Genre -like "*$Query*" -or 
                    $_.Description -like "*$Query*"
                }
                
                foreach ($station in $results) {
                    $totalResults++
                    
                    # Generate a gradient background based on genre
                    $gradientStyle = switch -Regex ($station.Genre) {
                        "News|Talk" { "background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(220,38,38,0.1));" }
                        "Jazz" { "background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(147,51,234,0.1));" }
                        "Classical" { "background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(37,99,235,0.1));" }
                        "Rock|Alternative" { "background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(251,146,60,0.1));" }
                        "Pop|Dance" { "background: linear-gradient(135deg, rgba(236,72,153,0.2), rgba(219,39,119,0.1));" }
                        "Electronic|Ambient" { "background: linear-gradient(135deg, rgba(34,211,238,0.2), rgba(6,182,212,0.1));" }
                        default { "background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(16,185,129,0.1));" }
                    }
                    
                    # Emoji based on genre
                    $emoji = switch -Regex ($station.Genre) {
                        "News|Talk" { "📻" }
                        "Jazz" { "🎷" }
                        "Classical" { "🎻" }
                        "Rock" { "🎸" }
                        "Pop" { "🎤" }
                        "Electronic" { "🎛️" }
                        default { "📡" }
                    }
                    
                    $encodedURL = [System.Web.HttpUtility]::UrlEncode($station.URL)
                    
                    # Use logo if available, otherwise use emoji with gradient
                    $thumbStyle = ""
                    $thumbContent = ""
                    
                    if ($station.Logo -and -not [string]::IsNullOrWhiteSpace($station.Logo)) {
                        # Has logo - use as background image
                        if ($station.Logo -like "http*") {
                            $logoURL = $station.Logo
                        } else {
                            $logoURL = "/$($station.Logo)"
                        }
                        $thumbStyle = "$gradientStyle background-image: url('$logoURL'); background-size: 60%; background-repeat: no-repeat; background-position: center;"
                        $thumbContent = ""  # Empty when using background image
                    } else {
                        # No logo - use emoji with gradient
                        $thumbStyle = "$gradientStyle display:flex; align-items:center; justify-content:center; font-size:48px;"
                        $thumbContent = $emoji
                    }
                    
                    $resultItems += @"
<div class="item" onclick="playRadio('$encodedURL', '$($station.Name)', '$($station.Description)')">
  <div class="thumb" style="$thumbStyle">$thumbContent</div>
  <div class="flag radio" style="background: rgba(52,211,153,0.9);">LIVE</div>
  <div class="corner">$($station.Country)</div>
  <div class="meta">
    <strong>$($station.Name)</strong>
    <span>$($station.Genre)</span>
    <span style="font-size:11px; opacity:0.7;">$($station.Description)</span>
  </div>
</div>
"@
                }
            }
            'tv' {
                # Search in TV channels: name, country, genre, description
                $results = $CONFIG.TVChannels | Where-Object {
                    $_.Name -like "*$Query*" -or 
                    $_.Country -like "*$Query*" -or 
                    $_.Genre -like "*$Query*" -or 
                    $_.Description -like "*$Query*"
                }
                
                foreach ($channel in $results) {
                    $totalResults++
                    
                    # Generate a gradient background based on genre
                    $gradientStyle = switch -Regex ($channel.Genre) {
                        "News" { "background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(220,38,38,0.1));" }
                        "Business" { "background: linear-gradient(135deg, rgba(52,211,153,0.2), rgba(16,185,129,0.1));" }
                        "Sports" { "background: linear-gradient(135deg, rgba(251,146,60,0.2), rgba(234,88,12,0.1));" }
                        default { "background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(59,130,246,0.1));" }
                    }
                    
                    $emoji = switch -Regex ($channel.Genre) {
                        "News" { "📺" }
                        "Business" { "💼" }
                        "Sports" { "⚽" }
                        default { "📡" }
                    }
                    
                    $encodedURL = [System.Web.HttpUtility]::UrlEncode($channel.URL)
                    
                    $thumbStyle = ""
                    $thumbContent = ""
                    
                    if ($channel.Logo -and -not [string]::IsNullOrWhiteSpace($channel.Logo)) {
                        if ($channel.Logo -like "http*") {
                            $logoURL = $channel.Logo
                        } else {
                            $logoURL = "/$($channel.Logo)"
                        }
                        $thumbStyle = "$gradientStyle background-image: url('$logoURL'); background-size: 60%; background-repeat: no-repeat; background-position: center;"
                        $thumbContent = ""
                    } else {
                        $thumbStyle = "$gradientStyle display:flex; align-items:center; justify-content:center; font-size:48px;"
                        $thumbContent = $emoji
                    }
                    
                    $resultItems += @"
<div class="item" onclick="playTV('$encodedURL', '$($channel.Name)', '$($channel.Description)')">
  <div class="thumb" style="$thumbStyle">$thumbContent</div>
  <div class="flag tv" style="background: rgba(96,165,250,0.9);">LIVE</div>
  <div class="corner">$($channel.Country)</div>
  <div class="meta">
    <strong>$($channel.Name)</strong>
    <span>$($channel.Genre)</span>
    <span style="font-size:11px; opacity:0.7;">$($channel.Description)</span>
  </div>
</div>
"@
                }
            }
        }
    }
    
    # Determine what was searched
    $searchScope = if ($Type) {
        switch ($Type) {
            'videos' { "Videos" }
            'music' { "Music" }
            'pictures' { "Pictures" }
            'pdfs' { "PDFs/ePUBS" }
            'radio' { "Radio Stations" }
            'tv' { "TV Channels" }
            default { "All Media" }
        }
    } else {
        "All Media"
    }
    
    $noResultsHtml = if ($totalResults -eq 0) {
        @"
<div style="text-align:center; padding:60px 20px;">
  <div style="font-size:64px; margin-bottom:20px;">🔍</div>
  <h2 style="margin:0 0 10px 0;">No results found</h2>
  <p style="color:var(--muted); margin:0;">Try searching with different keywords</p>
</div>
"@
    } else {
        ""
    }
    
    $content = @"
<div class="top">
  <div class="search">
    <span style="opacity:.85">🔎</span>
    <input placeholder="Search…" id="searchInput" value="$escapedQuery" />
  </div>
  <div style="display:flex; gap:8px; align-items:center;">
    <span style="font-size:14px; color:var(--muted);">in $searchScope</span>
  </div>
</div>

<div class="content">
  <section class="card">
    <div class="head">
      <div class="title">
        <strong>Search Results</strong>
        <span>$totalResults result$(if($totalResults -ne 1){'s'}) for "$escapedQuery"</span>
      </div>
      <div class="actions">
        <a href="/" class="small" style="text-decoration:none; color:inherit;">← Back to Home</a>
      </div>
    </div>
    <div class="grid">
      $resultItems
    </div>
    $noResultsHtml
  </section>
  
  <!-- Radio Player (for radio search results) -->
  <div id="radioPlayer" style="position: fixed; bottom: 20px; right: 20px; background: var(--panel); border: 1px solid var(--stroke); border-radius: 16px; padding: 20px; box-shadow: var(--shadow); display: none; min-width: 300px; z-index: 1000;">
    <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
      <div style="font-size: 24px;">📻</div>
      <div style="flex: 1;">
        <div id="radioNowPlaying" style="font-weight: 600; margin-bottom: 4px;">Select a station</div>
        <div id="radioDescription" style="font-size: 12px; color: var(--muted);">Click a station to start</div>
      </div>
      <div onclick="stopRadio()" style="cursor: pointer; font-size: 24px; opacity: 0.7; transition: opacity 0.2s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">✕</div>
    </div>
    <audio id="radioAudio" controls style="width: 100%; border-radius: 8px;"></audio>
  </div>
  
  <!-- Error Modal -->
  <div id="errorModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 2000; align-items: center; justify-content: center;">
    <div style="background: var(--panel); border: 1px solid var(--stroke); border-radius: 20px; padding: 32px; max-width: 440px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,0.5); animation: modalSlideIn 0.3s ease;">
      <div style="text-align: center; margin-bottom: 24px;">
        <div style="font-size: 64px; margin-bottom: 16px;">📻</div>
        <h2 style="margin: 0 0 12px 0; font-size: 22px; font-weight: 600; color: var(--text);">Unable to Play Station</h2>
        <p style="margin: 0; color: var(--muted); font-size: 15px; line-height: 1.6;">
          This radio stream couldn't be played. This may happen if:
        </p>
      </div>
      <div style="background: rgba(251,191,36,0.1); border: 1px solid rgba(251,191,36,0.3); border-radius: 12px; padding: 16px; margin-bottom: 24px;">
        <div style="display: flex; align-items: start; gap: 12px; margin-bottom: 10px;">
          <div style="font-size: 20px;">🌍</div>
          <div style="flex: 1;">
            <div style="font-weight: 600; margin-bottom: 4px; font-size: 14px;">Geographic Restriction</div>
            <div style="font-size: 13px; color: var(--muted); line-height: 1.5;">The station may be geo-locked to a specific country or region</div>
          </div>
        </div>
        <div style="display: flex; align-items: start; gap: 12px;">
          <div style="font-size: 20px;">🔗</div>
          <div style="flex: 1;">
            <div style="font-weight: 600; margin-bottom: 4px; font-size: 14px;">Stream URL Changed</div>
            <div style="font-size: 13px; color: var(--muted); line-height: 1.5;">The direct streaming link may have been updated by the broadcaster</div>
          </div>
        </div>
      </div>
      <button onclick="closeErrorModal()" style="width: 100%; padding: 14px; background: linear-gradient(135deg, rgba(96,165,250,0.9), rgba(59,130,246,0.9)); border: none; border-radius: 12px; color: white; font-weight: 600; font-size: 15px; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 12px rgba(96,165,250,0.3);" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(96,165,250,0.4)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(96,165,250,0.3)'">
        OK, Got It
      </button>
    </div>
  </div>
  
  <style>
  @keyframes modalSlideIn {
    from {
      opacity: 0;
      transform: translateY(-20px) scale(0.95);
    }
    to {
      opacity: 1;
      transform: translateY(0) scale(1);
    }
  }
  </style>
</div>

<script>
let currentRadio = null;

function showErrorModal() {
  const modal = document.getElementById('errorModal');
  modal.style.display = 'flex';
}

function closeErrorModal() {
  const modal = document.getElementById('errorModal');
  modal.style.display = 'none';
}

// Close modal when clicking outside
document.getElementById('errorModal')?.addEventListener('click', function(e) {
  if (e.target === this) {
    closeErrorModal();
  }
});

function playRadio(url, name, description) {
  const player = document.getElementById('radioPlayer');
  const audio = document.getElementById('radioAudio');
  const nowPlaying = document.getElementById('radioNowPlaying');
  const desc = document.getElementById('radioDescription');
  
  // Decode URL
  url = decodeURIComponent(url);
  
  // Show player
  player.style.display = 'block';
  
  // Update UI
  nowPlaying.textContent = name;
  desc.textContent = description;
  
  // Stop current stream if playing
  if (currentRadio) {
    audio.pause();
    audio.src = '';
  }
  
  // Start new stream
  audio.src = url;
  audio.load();
  audio.play().catch(err => {
    console.error('Playback error:', err);
    showErrorModal();
  });
  
  currentRadio = { url, name, description };
}

function stopRadio() {
  const player = document.getElementById('radioPlayer');
  const audio = document.getElementById('radioAudio');
  
  audio.pause();
  audio.src = '';
  player.style.display = 'none';
  currentRadio = null;
}

document.getElementById('searchInput').addEventListener('keyup', function(e) {
  if (e.key === 'Enter') {
    var typeParam = '$Type' ? '&type=$Type' : '';
    window.location.href = '/search?q=' + encodeURIComponent(this.value) + typeParam;
  }
});
</script>
"@
    
    return Get-HTMLPage -Content $content -Title "Search: $Query"
}

# ============================================================================
# IP WHITELIST SECURITY
# ============================================================================

function Test-IPWhitelist {
    <#
    .SYNOPSIS
        Tests if an IP address is in the whitelist
    .DESCRIPTION
        Validates the remote IP address against the configured whitelist.
        If whitelist is empty, all IPs are allowed.
    .PARAMETER RemoteIP
        The IP address to check
    .RETURNS
        $true if IP is allowed, $false otherwise
    #>
    param(
        [string]$RemoteIP
    )
    
    # If whitelist is empty, allow all IPs
    if ($CONFIG.IPWhitelist.Count -eq 0) {
        return $true
    }
    
    # Remove port if present (e.g., "192.168.1.1:12345" -> "192.168.1.1")
    if ($RemoteIP -match '^(.+):(\d+)$') {
        $RemoteIP = $matches[1]
    }
    
    # Remove IPv6 brackets if present (e.g., "[::1]" -> "::1")
    $RemoteIP = $RemoteIP.Trim('[', ']')
    
    # Check if IP is in whitelist
    foreach ($allowedIP in $CONFIG.IPWhitelist) {
        if ($RemoteIP -eq $allowedIP) {
            Write-Host "  ✓ IP allowed: $RemoteIP" -ForegroundColor Green
            return $true
        }
    }
    
    # IP not in whitelist
    Write-Host "  ✗ IP blocked: $RemoteIP (not in whitelist)" -ForegroundColor Red
    return $false
}

function Send-AccessDeniedResponse {
    <#
    .SYNOPSIS
        Sends a 403 Forbidden response to the client
    .DESCRIPTION
        Sends an HTML error page informing the user their IP is not authorized
    .PARAMETER Response
        The HttpListenerResponse object
    .PARAMETER RemoteIP
        The blocked IP address
    #>
    param(
        [System.Net.HttpListenerResponse]$Response,
        [string]$RemoteIP
    )
    
    $errorHtml = @"
<!DOCTYPE html>
<html>
<head>
    <title>Access Denied - NexusStack</title>
    <style>
        body {
            font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
            background:
                radial-gradient(950px 600px at 10% 15%, rgba(52,211,153,.18), transparent 55%),
                radial-gradient(800px 520px at 90% 85%, rgba(34,197,94,.12), transparent 55%),
                linear-gradient(180deg, #0a1410, #0d1410);
            margin: 0;
            padding: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            color: rgba(255,255,255,.92);
        }
        .container {
            text-align: center;
            padding: 40px;
            background: rgba(20,31,24,0.8);
            border: 1px solid rgba(255,255,255,.10);
            border-radius: 18px;
            /* backdrop-filter removed for performance */
            box-shadow: 0 18px 55px rgba(0,0,0,.45);
            max-width: 500px;
        }
        h1 {
            font-size: 72px;
            margin: 0;
            font-weight: 800;
        }
        h2 {
            font-size: 24px;
            margin: 10px 0;
            font-weight: 600;
            color: rgba(255,255,255,.92);
        }
        p {
            font-size: 16px;
            color: rgba(255,255,255,.62);
            margin: 20px 0;
            line-height: 1.6;
        }
        .ip {
            background: rgba(52,211,153,.14);
            border: 1px solid rgba(52,211,153,.22);
            padding: 12px 20px;
            border-radius: 12px;
            display: inline-block;
            margin-top: 10px;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            color: #34d399;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚫</h1>
        <h2>Access Denied</h2>
        <p>Your IP address is not authorized to access this media library.</p>
        <div class="ip">Your IP: $RemoteIP</div>
        <p style="margin-top:30px; font-size:14px; opacity:0.7;">
            If you believe this is an error, please contact the administrator.
        </p>
    </div>
</body>
</html>
"@
    
    $Response.StatusCode = 403
    $Response.ContentType = "text/html; charset=utf-8"
    $buffer = [System.Text.Encoding]::UTF8.GetBytes($errorHtml)
    $Response.ContentLength64 = $buffer.Length
    $Response.OutputStream.Write($buffer, 0, $buffer.Length)
    $Response.Close()
}

# ============================================================================
# WEB SERVER
# ============================================================================

# ============================================================================
# WEB UI - AUTHENTICATION & SETTINGS (v1.0)
# ============================================================================

<#
.SYNOPSIS
    Get login page HTML
#>
function Get-LoginPage {
    param(
        [string]$Error = "",
        [string]$SelectedUser = ""
    )
    
    # Get all active users (max 5)
    $users = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT username, display_name, avatar_path FROM users WHERE is_active = 1 ORDER BY user_id LIMIT 5"
    
    # Build user cards HTML
    $userCardsHtml = ""
    foreach ($user in $users) {
        $displayName = if ($user.display_name) { $user.display_name } else { $user.username }
        
        # Avatar HTML
        $avatarHtml = if ($user.avatar_path) {
            "<img src='/avatar/$($user.avatar_path)' style='width:100%; height:100%; object-fit:cover;'>"
        } else {
            "<div style='width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:64px; font-weight:700; color:white; background:linear-gradient(135deg, #34d399, #22c55e);'>$($displayName.Substring(0,1).ToUpper())</div>"
        }
        
        $userCardsHtml += @"
        <div class="user-card" onclick="selectUser('$($user.username)', '$displayName')">
          <div class="user-avatar-large">
            $avatarHtml
          </div>
          <div class="user-name">$displayName</div>
        </div>
"@
    }
    
    $errorHtml = ""
    if (-not [string]::IsNullOrWhiteSpace($Error)) {
        $errorHtml = "<div class='error-message'>$Error</div>"
    }
    
    # Show user selection or PIN entry based on SelectedUser
    $showPinEntry = if ($SelectedUser) { 'block' } else { 'none' }
    $showUserSelection = if ($SelectedUser) { 'none' } else { 'block' }
    
    # Get selected user's display name
    $selectedDisplayName = ""
    if ($SelectedUser) {
        $selectedUserData = $users | Where-Object { $_.username -eq $SelectedUser } | Select-Object -First 1
        $selectedDisplayName = if ($selectedUserData.display_name) { $selectedUserData.display_name } else { $SelectedUser }
    }
    
    return @"
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Login - NexusStack</title>
  <style>
    * { margin:0; padding:0; box-sizing:border-box; }
    body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: linear-gradient(135deg, rgba(13,20,16,0.85) 0%, rgba(26,44,36,0.85) 100%);
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    background-attachment: fixed;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    color: #fff;
    padding: 20px;
    }
    .login-wrapper {
      width: 100%;
      max-width: 900px;
    }
    .logo {
      text-align: center;
      margin-bottom: 48px;
    }
    .logo h1 {
      font-size: 48px;
      font-weight: 700;
      color: #34d399;
      margin-bottom: 8px;
    }
    .logo p {
      color: rgba(255,255,255,0.6);
      font-size: 18px;
    }
    
    /* User Selection Screen */
    #user-selection {
      text-align: center;
    }
    #user-selection h2 {
      font-size: 32px;
      margin-bottom: 40px;
      font-weight: 400;
      color: rgba(255,255,255,0.9);
    }
    .user-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: 32px;
      max-width: 800px;
      margin: 0 auto;
      justify-items: center;
    }
    .user-card {
      cursor: pointer;
      transition: transform 0.2s;
      text-align: center;
    }
    .user-card:hover {
      transform: scale(1.1);
    }
    .user-avatar-large {
      width: 150px;
      height: 150px;
      border-radius: 12px;
      overflow: hidden;
      border: 3px solid transparent;
      transition: border-color 0.2s;
      margin-bottom: 16px;
    }
    .user-card:hover .user-avatar-large {
      border-color: #34d399;
    }
    .user-name {
      font-size: 18px;
      font-weight: 500;
      color: rgba(255,255,255,0.8);
    }
    
    /* PIN Entry Screen */
    #pin-entry {
      max-width: 420px;
      margin: 0 auto;
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 16px;
      padding: 48px;
      backdrop-filter: blur(10px);
    }
    #pin-entry h2 {
      font-size: 24px;
      text-align: center;
      margin-bottom: 32px;
      font-weight: 400;
    }
    .form-group {
      margin-bottom: 24px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      font-size: 14px;
      font-weight: 500;
      color: rgba(255,255,255,0.8);
    }
    input {
      width: 100%;
      padding: 14px 16px;
      background: rgba(255,255,255,0.08);
      border: 1px solid rgba(255,255,255,0.15);
      border-radius: 8px;
      color: #fff;
      font-size: 16px;
      transition: all 0.2s;
    }
    input:focus {
      outline: none;
      border-color: #34d399;
      background: rgba(255,255,255,0.12);
    }
    .pin-input {
      text-align: center;
      letter-spacing: 8px;
      font-size: 24px;
      font-weight: 600;
    }
    .btn {
      width: 100%;
      padding: 14px;
      background: linear-gradient(135deg, #34d399, #22c55e);
      border: none;
      border-radius: 8px;
      color: #0d1410;
      font-size: 16px;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.2s, box-shadow 0.2s;
    }
    .btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 8px 16px rgba(52,211,153,0.3);
    }
    .btn:active {
      transform: translateY(0);
    }
    .btn-secondary {
      background: rgba(255,255,255,0.1);
      color: rgba(255,255,255,0.8);
      margin-top: 12px;
    }
    .btn-secondary:hover {
      background: rgba(255,255,255,0.15);
      box-shadow: none;
    }
    .error-message {
      background: rgba(239,68,68,0.15);
      border: 1px solid rgba(239,68,68,0.3);
      color: #fca5a5;
      padding: 12px;
      border-radius: 8px;
      margin-bottom: 24px;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <div class="login-wrapper">
    <div class="logo">
    <img src="/menu/nslogo.png" alt="NexusStack" style="width:120px; height:120px; margin:0 auto 16px;">
    <h1>NexusStack</h1>
    <p>MEDIA LIBRARY MANAGER</p>
    </div>
    
    <!-- User Selection Screen -->
    <div id="user-selection" style="display: $showUserSelection;">
      <h2>Who’s jumping in?</h2>
      <div class="user-grid">
        $userCardsHtml
      </div>
    </div>
    
    <!-- PIN Entry Screen -->
    <div id="pin-entry" style="display: $showPinEntry;">
      <h2>Enter PIN for $selectedDisplayName</h2>
      
      $errorHtml
      
      <form method="POST" action="/login">
        <input type="hidden" id="username" name="username" value="$SelectedUser">
        
        <div class="form-group">
          <label for="pin">PIN Code (6 digits)</label>
          <input type="password" id="pin" name="pin" class="pin-input" maxlength="6" pattern="[0-9]{6}" required autofocus autocomplete="current-password">
        </div>
        
        <button type="submit" class="btn">Login</button>
        <button type="button" class="btn btn-secondary" onclick="window.location.href='/login'">← Back to Users</button>
      </form>
    </div>
  </div>
  
  <script>
    var randomBg = Math.floor(Math.random() * 6) + 1;
    document.body.style.backgroundImage = 
        "linear-gradient(135deg, rgba(13,20,16,0.85) 0%, rgba(26,44,36,0.85) 100%), url('/menu/bg-" + randomBg + ".jpg')";
    
    function selectUser(username, displayName) {
      window.location.href = '/login?user=' + encodeURIComponent(username);
    }
  </script>
</body>
</html>
"@
}

<#
.SYNOPSIS
    Get Settings page HTML (Admin only)
#>
function Get-SettingsPage {
    param(
        [Parameter(Mandatory=$false)]
        [object]$UserSession = $Global:CurrentUser
    )
    
    # Check if user is admin
    if (-not $UserSession -or $UserSession.UserType -ne 'admin') {
        return Get-AccessDeniedPage
    }
    
    # Load current configuration
    $configPath = Join-Path $PSScriptRoot "PSMediaLib.conf"
    $configContent = ""
    if (Test-Path $configPath) {
        $configContent = Get-Content $configPath -Raw
        # Escape HTML special characters
        $configContent = [System.Web.HttpUtility]::HtmlEncode($configContent)
    }
    
    # Get list of users
    $users = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query @"
SELECT user_id, username, display_name, user_type, created_date, last_login, is_active
FROM users
ORDER BY user_type DESC, username
"@
    
    $usersHtml = ""
    foreach ($user in $users) {
        $statusBadge = if ($user.is_active -eq 1) {
            "<span class='badge badge-success'>Active</span>"
        } else {
            "<span class='badge badge-danger'>Disabled</span>"
        }
        
        $typeBadge = if ($user.user_type -eq 'admin') {
            "<span class='badge badge-warning'>Admin</span>"
        } else {
            "<span class='badge badge-info'>Guest</span>"
        }
        
        $lastLogin = if ($user.last_login) { $user.last_login } else { "Never" }
        
        $usersHtml += @"
        <tr>
          <td>$($user.user_id)</td>
          <td>$($user.username)</td>
          <td>$($user.display_name)</td>
          <td>$typeBadge</td>
          <td>$statusBadge</td>
          <td style="font-size: 12px; color: rgba(255,255,255,0.6);">$($user.created_date)</td>
          <td style="font-size: 12px; color: rgba(255,255,255,0.6);">$lastLogin</td>
          <td>
            <button class='btn-small' onclick="viewUserStats('$($user.username)')">📊 Stats</button>
            <button class='btn-small' onclick="editUser('$($user.username)')">✏️ Edit</button>
            <button class='btn-small btn-danger' onclick="deleteUser('$($user.username)')">🗑️ Delete</button>
          </td>
        </tr>
"@
    }
    
    $content = @"
    <div class="settings-container">
      <h1 class="page-title">⚙️ Settings</h1>
      
      <!-- Tabs -->
      <div class="tabs">
        <button class="tab-btn active" onclick="showTab('users')">👥 User Management</button>
        <button class="tab-btn" onclick="showTab('config')">🔧 Application Settings</button>
        <button class="tab-btn" onclick="showTab('about')">ℹ️ About</button>
      </div>
      
      <!-- Users Tab -->
      <div id="tab-users" class="tab-content active">
        <div class="section-header">
          <h2>User Management</h2>
          <button class="btn-primary" onclick="showCreateUserModal()">➕ Create New User</button>
        </div>
        
        <div class="table-container">
          <table class="users-table">
            <thead>
              <tr>
                <th>ID</th>
                <th>Username</th>
                <th>Display Name</th>
                <th>Type</th>
                <th>Status</th>
                <th>Created</th>
                <th>Last Login</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              $usersHtml
            </tbody>
          </table>
        </div>
      </div>
      
      <!-- Config Tab -->
      <div id="tab-config" class="tab-content">
        <div class="section-header">
          <h2>Application Configuration</h2>
          <button class="btn-primary" onclick="saveConfig()">💾 Save Configuration</button>
        </div>
        
        <div class="config-editor">
          <p class="help-text">⚠️ Warning: Incorrect configuration may break the application. Restart required after changes.</p>
          <textarea id="config-editor" rows="30" spellcheck="false">$configContent</textarea>
        </div>
      </div>
      
      <!-- About Tab -->
      <div id="tab-about" class="tab-content">
        <div class="about-section">
          <h2>📚 NexusStack Media Library</h2>
          <p><strong>Version:</strong> BETA 0.7 (January 2026)</p>
          <p><strong>Author:</strong> Michael DALLA RIVA</p>
          <p><strong>Description:</strong> Advanced PowerShell-based media library management system with multi-user support.</p>
          
          <h3 style="margin-top: 32px;">Features</h3>
          <ul class="feature-list">
            <li>Video Library Management</li>
            <li>Music Collection with Album Art</li>
            <li>Internet Radio Streaming</li>
            <li>Picture Gallery</li>
            <li>PDF/EPUB Document Library</li>
            <li>Multi-user Support with Encrypted Authentication</li>
            <li>Per-user Watch Progress & Statistics</li>
            <li>Role-based Access Control (Admin/Guest)</li>
            <li>Customizable Themes</li>
            <li>Advanced Search & Analysis</li>
            <li>Built-in ePUB viewer</li>
          </ul>
          
          <h3 style="margin-top: 32px;">Current User</h3>
          <div class="user-info-card">
            <p><strong>Username:</strong> $($UserSession.Username)</p>
            <p><strong>Display Name:</strong> $($UserSession.DisplayName)</p>
            <p><strong>Role:</strong> $($UserSession.UserType)</p>
            <p><strong>Session ID:</strong> $($UserSession.SessionId)</p>
          </div>
        </div>
      </div>
    </div>
    
    <!-- Create User Modal -->
    <div id="createUserModal" class="modal">
      <div class="modal-content">
        <div class="modal-header">
          <h3>Create New User</h3>
          <button class="close-btn" onclick="closeCreateUserModal()">✕</button>
        </div>
        <form id="createUserForm" onsubmit="createUser(event)">
          <div class="form-group">
            <label>Username</label>
            <input type="text" id="new_username" required pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, underscore, and hyphen only">
          </div>
          <div class="form-group">
            <label>Display Name</label>
            <input type="text" id="new_displayname" required>
          </div>
          <div class="form-group">
            <label>User Type</label>
            <select id="new_usertype" required>
              <option value="guest">Guest (Read-only settings)</option>
              <option value="admin">Admin (Full access)</option>
            </select>
          </div>
          <div class="form-group">
            <label>PIN Code (6 digits)</label>
            <input type="password" id="new_pin" maxlength="6" pattern="[0-9]{6}" required>
          </div>
          <div class="form-group">
            <label>Confirm PIN</label>
            <input type="password" id="new_pin_confirm" maxlength="6" pattern="[0-9]{6}" required>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn-secondary" onclick="closeCreateUserModal()">Cancel</button>
            <button type="submit" class="btn-primary">Create User</button>
          </div>
        </form>
      </div>
    </div>
    
    <style>
    .settings-container { padding: 20px; max-width: 1400px; margin: 0 auto; }
    .page-title { font-size: 32px; margin-bottom: 32px; color: var(--accent); }
    
    .tabs {
      display: flex;
      gap: 8px;
      margin-bottom: 32px;
      border-bottom: 2px solid rgba(255,255,255,0.1);
    }
    .tab-btn {
      background: none;
      border: none;
      color: rgba(255,255,255,0.6);
      padding: 12px 24px;
      font-size: 16px;
      cursor: pointer;
      border-bottom: 3px solid transparent;
      transition: all 0.2s;
    }
    .tab-btn:hover { color: rgba(255,255,255,0.9); }
    .tab-btn.active {
      color: var(--accent);
      border-bottom-color: var(--accent);
    }
    
    .tab-content { display: none; }
    .tab-content.active { display: block; }
    
    .section-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 24px;
    }
    .section-header h2 { font-size: 24px; margin: 0; }
    
    .table-container {
      background: rgba(255,255,255,0.03);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 12px;
      overflow: hidden;
    }
    .users-table {
      width: 100%;
      border-collapse: collapse;
    }
    .users-table th {
      background: rgba(255,255,255,0.05);
      padding: 16px;
      text-align: left;
      font-weight: 600;
      border-bottom: 2px solid rgba(255,255,255,0.1);
    }
    .users-table td {
      padding: 16px;
      border-bottom: 1px solid rgba(255,255,255,0.05);
    }
    .users-table tr:hover {
      background: rgba(255,255,255,0.02);
    }
    
    .badge {
      display: inline-block;
      padding: 4px 12px;
      border-radius: 12px;
      font-size: 12px;
      font-weight: 600;
    }
    .badge-success { background: rgba(34,197,94,0.2); color: #22c55e; }
    .badge-danger { background: rgba(239,68,68,0.2); color: #ef4444; }
    .badge-warning { background: rgba(251,191,36,0.2); color: #fbbf24; }
    .badge-info { background: rgba(96,165,250,0.2); color: #60a5fa; }
    
    .btn-primary, .btn-secondary, .btn-small, .btn-danger {
      padding: 8px 16px;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s;
    }
    .btn-primary {
      background: linear-gradient(135deg, #34d399, #22c55e);
      color: #0d1410;
    }
    .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(52,211,153,0.3); }
    
    .btn-secondary {
      background: rgba(255,255,255,0.1);
      color: #fff;
      border: 1px solid rgba(255,255,255,0.2);
    }
    .btn-secondary:hover { background: rgba(255,255,255,0.15); }
    
    .btn-small {
      padding: 6px 12px;
      font-size: 12px;
      background: rgba(255,255,255,0.08);
      color: #fff;
      margin-right: 4px;
    }
    .btn-small:hover { background: rgba(255,255,255,0.15); }
    
    .btn-danger {
      background: rgba(239,68,68,0.15);
      color: #ef4444;
      padding: 6px 12px;
      font-size: 12px;
    }
    .btn-danger:hover { background: rgba(239,68,68,0.25); }
    
    .config-editor {
      background: rgba(255,255,255,0.03);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 12px;
      padding: 20px;
    }
    .config-editor textarea {
      width: 100%;
      background: rgba(0,0,0,0.3);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 8px;
      color: #fff;
      font-family: 'Courier New', monospace;
      font-size: 13px;
      padding: 16px;
      resize: vertical;
    }
    .help-text {
      color: rgba(251,191,36,0.9);
      margin-bottom: 16px;
      padding: 12px;
      background: rgba(251,191,36,0.1);
      border-left: 4px solid #fbbf24;
      border-radius: 4px;
    }
    
    .about-section {
      background: rgba(255,255,255,0.03);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 12px;
      padding: 32px;
    }
    .about-section h2 { margin-bottom: 16px; }
    .about-section h3 { margin-top: 24px; margin-bottom: 12px; color: var(--accent); }
    .about-section p { margin-bottom: 8px; line-height: 1.6; }
    
    .feature-list {
      list-style: none;
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 12px;
      margin-top: 16px;
    }
    .feature-list li {
      padding: 12px;
      background: rgba(255,255,255,0.05);
      border-radius: 8px;
      border-left: 3px solid var(--accent);
    }
    
    .user-info-card {
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 8px;
      padding: 20px;
      margin-top: 16px;
    }
    .user-info-card p { margin-bottom: 8px; }
    
    .modal {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0,0,0,0.7);
      z-index: 1000;
      align-items: center;
      justify-content: center;
    }
    .modal.active { display: flex; }
    
    .modal-content {
      background: #0d1410;
      border: 1px solid rgba(255,255,255,0.2);
      border-radius: 16px;
      width: 90%;
      max-width: 500px;
      max-height: 90vh;
      overflow-y: auto;
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 24px;
      border-bottom: 1px solid rgba(255,255,255,0.1);
    }
    .modal-header h3 { margin: 0; font-size: 20px; }
    .close-btn {
      background: none;
      border: none;
      color: rgba(255,255,255,0.6);
      font-size: 24px;
      cursor: pointer;
      padding: 0;
      width: 32px;
      height: 32px;
    }
    .close-btn:hover { color: #fff; }
    
    .modal form { padding: 24px; }
    .form-group {
      margin-bottom: 20px;
    }
    .form-group label {
      display: block;
      margin-bottom: 8px;
      font-weight: 500;
      color: rgba(255,255,255,0.8);
    }
    .form-group input, .form-group select {
      width: 100%;
      padding: 12px;
      background: rgba(255,255,255,0.08);
      border: 1px solid rgba(255,255,255,0.15);
      border-radius: 8px;
      color: #fff;
      font-size: 14px;
    }
    .form-group input:focus, .form-group select:focus {
      outline: none;
      border-color: var(--accent);
      background: rgba(255,255,255,0.12);
    }
    
    .modal-footer {
      display: flex;
      gap: 12px;
      justify-content: flex-end;
      padding-top: 24px;
      border-top: 1px solid rgba(255,255,255,0.1);
    }
    </style>
    
    <script>
    function showTab(tabName) {
      // Hide all tabs
      document.querySelectorAll('.tab-content').forEach(tab => {
        tab.classList.remove('active');
      });
      document.querySelectorAll('.tab-btn').forEach(btn => {
        btn.classList.remove('active');
      });
      
      // Show selected tab
      document.getElementById('tab-' + tabName).classList.add('active');
      event.target.classList.add('active');
    }
    
    function showCreateUserModal() {
      document.getElementById('createUserModal').classList.add('active');
    }
    
    function closeCreateUserModal() {
      document.getElementById('createUserModal').classList.remove('active');
      document.getElementById('createUserForm').reset();
    }
    
    async function createUser(event) {
      event.preventDefault();
      
      const username = document.getElementById('new_username').value;
      const displayname = document.getElementById('new_displayname').value;
      const usertype = document.getElementById('new_usertype').value;
      const pin = document.getElementById('new_pin').value;
      const pinConfirm = document.getElementById('new_pin_confirm').value;
      
      if (pin !== pinConfirm) {
        alert('PINs do not match!');
        return;
      }
      
      if (pin.length !== 6 || !/^[0-9]+$/.test(pin)) {
        alert('PIN must be exactly 6 digits!');
        return;
      }
      
      try {
        const response = await fetch('/api/users/create', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ username, displayname, usertype, pin })
        });
        
        const result = await response.json();
        
        if (result.success) {
          alert('User created successfully!');
          location.reload();
        } else {
          alert('Error: ' + result.error);
        }
      } catch (error) {
        alert('Error creating user: ' + error);
      }
    }
    
    async function deleteUser(username) {
      if (!confirm('Are you sure you want to delete user "' + username + '"?')) {
        return;
      }
      
      try {
        const response = await fetch('/api/users/delete', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ username })
        });
        
        const result = await response.json();
        
        if (result.success) {
          alert('User deleted successfully!');
          location.reload();
        } else {
          alert('Error: ' + result.error);
        }
      } catch (error) {
        alert('Error deleting user: ' + error);
      }
    }
    
    function editUser(username) {
      // Fetch current user data
      fetch('/api/users/get?username=' + encodeURIComponent(username))
        .then(response => response.json())
        .then(user => {
          if (!user.success) {
            alert('Error: ' + user.error);
            return;
          }
          
          // Build avatar HTML
          var avatarHtml = user.data.avatar_path ? 
            '<img src="/avatar/' + user.data.avatar_path + '" style="width:64px; height:64px; border-radius:50%; object-fit:cover; border:2px solid var(--accent);">' : 
            '<div style="width:64px; height:64px; border-radius:50%; background:var(--accent); display:flex; align-items:center; justify-content:center; font-size:24px; font-weight:bold; color:white;">' + (user.data.display_name || user.data.username).substring(0,1).toUpperCase() + '</div>';
          
          // Create modal
          var modal = document.createElement('div');
          modal.id = 'editUserModal';
          modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); display:flex; align-items:center; justify-content:center; z-index:10000;';
          
          var guestSelected = user.data.user_type === 'guest' ? 'selected' : '';
          var adminSelected = user.data.user_type === 'admin' ? 'selected' : '';
          
          modal.innerHTML = '<div style="background:var(--panel); border:1px solid var(--stroke); border-radius:16px; padding:32px; max-width:500px; width:90%; max-height:90vh; overflow-y:auto; box-shadow:0 20px 60px rgba(0,0,0,0.5);">' +
            '<h2 style="margin:0 0 24px 0; color:var(--text);">✏️ Edit User: ' + user.data.username + '</h2>' +
            
            '<div style="margin-bottom:20px;">' +
              '<label style="display:block; margin-bottom:8px; color:var(--text); font-weight:600;">Display Name</label>' +
              '<input type="text" id="editDisplayName" value="' + (user.data.display_name || '') + '" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--stroke); border-radius:8px; color:var(--text); font-size:14px;">' +
            '</div>' +
            
            '<div style="margin-bottom:20px;">' +
              '<label style="display:block; margin-bottom:8px; color:var(--text); font-weight:600;">New PIN (leave empty to keep current)</label>' +
              '<input type="password" id="editPIN" placeholder="Enter new PIN or leave blank" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--stroke); border-radius:8px; color:var(--text); font-size:14px;">' +
            '</div>' +
            
            '<div style="margin-bottom:20px;">' +
              '<label style="display:block; margin-bottom:8px; color:var(--text); font-weight:600;">User Type</label>' +
              '<select id="editUserType" style="width:100%; padding:12px; background:var(--bg); border:1px solid var(--stroke); border-radius:8px; color:var(--text); font-size:14px;">' +
                '<option value="guest" ' + guestSelected + '>Guest</option>' +
                '<option value="admin" ' + adminSelected + '>Admin</option>' +
              '</select>' +
            '</div>' +
            
            '<div style="margin-bottom:20px;">' +
              '<label style="display:block; margin-bottom:8px; color:var(--text); font-weight:600;">Avatar</label>' +
              '<div style="display:flex; align-items:center; gap:16px;">' +
                avatarHtml +
                '<div style="flex:1;">' +
                  '<input type="file" id="editAvatar" accept="image/*" style="width:100%; padding:8px; background:var(--bg); border:1px solid var(--stroke); border-radius:8px; color:var(--text); font-size:12px;">' +
                  '<div style="font-size:11px; color:var(--muted); margin-top:4px;">Max 2MB, JPG/PNG recommended</div>' +
                '</div>' +
              '</div>' +
            '</div>' +
            
            '<div style="display:flex; gap:12px; margin-top:24px;">' +
              '<button onclick="saveUserEdit(\'' + user.data.username + '\')" style="flex:1; padding:12px 24px; background:linear-gradient(135deg, var(--accent), var(--accent2)); border:none; border-radius:8px; color:white; font-weight:600; cursor:pointer;">Save Changes</button>' +
              '<button onclick="closeEditModal()" style="flex:1; padding:12px 24px; background:var(--bg); border:1px solid var(--stroke); border-radius:8px; color:var(--text); font-weight:600; cursor:pointer;">Cancel</button>' +
            '</div>' +
          '</div>';
          
          document.body.appendChild(modal);
        })
        .catch(error => {
          alert('Error loading user data: ' + error);
        });
    }
    
    function closeEditModal() {
      var modal = document.getElementById('editUserModal');
      if (modal) modal.remove();
    }
    
    async function saveUserEdit(username) {
      var displayName = document.getElementById('editDisplayName').value;
      var pin = document.getElementById('editPIN').value;
      var userType = document.getElementById('editUserType').value;
      var avatarFile = document.getElementById('editAvatar').files[0];
      
      try {
        // Handle avatar upload if provided
        var avatarPath = null;
        if (avatarFile) {
          if (avatarFile.size > 2 * 1024 * 1024) {
            alert('Avatar file must be less than 2MB');
            return;
          }
          
          var formData = new FormData();
          formData.append('avatar', avatarFile);
          formData.append('username', username);
          
          var uploadResponse = await fetch('/api/users/upload-avatar', {
            method: 'POST',
            body: formData
          });
          
          var uploadResult = await uploadResponse.json();
          if (!uploadResult.success) {
            alert('Error uploading avatar: ' + uploadResult.error);
            return;
          }
          avatarPath = uploadResult.filename;
        }
        
        // Update user data
        var updateData = {
          username: username,
          displayName: displayName,
          userType: userType
        };
        
        if (pin) {
          updateData.pin = pin;
        }
        
        if (avatarPath) {
          updateData.avatarPath = avatarPath;
        }
        
        var response = await fetch('/api/users/edit', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(updateData)
        });
        
        var result = await response.json();
        if (result.success) {
          alert('User updated successfully!');
          closeEditModal();
          location.reload();
        } else {
          alert('Error: ' + result.error);
        }
      } catch (error) {
        alert('Error updating user: ' + error);
      }
    }
    
    function viewUserStats(username) {
      window.location.href = '/user-stats?username=' + encodeURIComponent(username);
    }
    
    async function saveConfig() {
      const config = document.getElementById('config-editor').value;
      
      if (!confirm('Are you sure you want to save these configuration changes? The application will need to be restarted.')) {
        return;
      }
      
      try {
        const response = await fetch('/api/config/save', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ config })
        });
        
        const result = await response.json();
        
        if (result.success) {
          alert('Configuration saved successfully! Please restart the application for changes to take effect.');
        } else {
          alert('Error: ' + result.error);
        }
      } catch (error) {
        alert('Error saving configuration: ' + error);
      }
    }
    
    // Ensure PIN inputs only accept numbers
    document.querySelectorAll('input[type="password"][maxlength="6"]').forEach(input => {
      input.addEventListener('input', function() {
        this.value = this.value.replace(/[^0-9]/g, '');
      });
    });
    </script>
"@
    
    return Get-HTMLPage -Content $content -Title "Settings - NexusStack"
}

<#
.SYNOPSIS
    Get access denied page
#>
function Get-AccessDeniedPage {
    $content = @"
    <div style="text-align: center; padding: 100px 20px;">
      <h1 style="font-size: 72px; margin-bottom: 24px;">🔒</h1>
      <h2 style="font-size: 32px; margin-bottom: 16px;">Access Denied</h2>
      <p style="font-size: 18px; color: rgba(255,255,255,0.6); margin-bottom: 32px;">
        You don't have permission to access this page.
      </p>
      <a href="/" style="display: inline-block; padding: 12px 24px; background: linear-gradient(135deg, #34d399, #22c55e); color: #0d1410; text-decoration: none; border-radius: 8px; font-weight: 600;">
        Go to Home
      </a>
    </div>
"@
    
    return Get-HTMLPage -Content $content -Title "Access Denied"
}

<#
.SYNOPSIS
    Get user statistics page
#>
function Get-UserStatsPage {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username
    )
    
    $userDbPath = Join-Path $CONFIG.UsersDBPath "$Username.db"
    
    if (-not (Test-Path $userDbPath)) {
        $content = "<div style='text-align:center; padding:100px;'><h2>No statistics available for this user</h2></div>"
        return Get-HTMLPage -Content $content -Title "User Stats"
    }
    
    # Get stats
    $videoStats = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as total_watched,
    SUM(CASE WHEN completed = 1 THEN 1 ELSE 0 END) as completed_count,
    SUM(watch_count) as total_plays,
    AVG(percent_watched) as avg_completion
FROM video_progress
"@
    
    $musicStats = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as unique_tracks,
    SUM(play_count) as total_plays,
    SUM(total_play_time_seconds) as total_time
FROM music_history
"@
    
    $radioStats = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT 
    COUNT(*) as unique_stations,
    SUM(listen_count) as total_listens,
    SUM(total_listen_time_seconds) as total_time
FROM radio_history
"@
    
    $topMusic = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT artist, title, play_count
FROM music_history
ORDER BY play_count DESC
LIMIT 10
"@
    
    $topMusicHtml = ""
    $rank = 1
    foreach ($track in $topMusic) {
        $artistTitle = if ($track.artist -and $track.title) {
            "$($track.artist) - $($track.title)"
        } elseif ($track.title) {
            $track.title
        } else {
            "(Unknown)"
        }
        $topMusicHtml += "<div class='stat-item'><span class='rank'>$rank.</span> $artistTitle <span class='plays'>$($track.play_count) plays</span></div>"
        $rank++
    }
    
    $musicHours = [math]::Round($musicStats.total_time / 3600, 1)
    $radioHours = [math]::Round($radioStats.total_time / 3600, 1)
    
    $content = @"
    <div class="stats-container">
      <h1 class="page-title"><img src="/menu/analysis.png" style="width:32px; height:32px; vertical-align:middle;"> Statistics for: $Username</h1>
      
      <div class="stats-grid">
        <div class="stat-card">
          <div class="stat-icon"><img src="/menu/movie.png" style="width:48px; height:48px;"></div>
          <div class="stat-content">
            <h3>Videos</h3>
            <div class="stat-value">$($videoStats.total_watched)</div>
            <div class="stat-label">Videos Watched</div>
            <div class="stat-details">
              <div>✓ $($videoStats.completed_count) completed</div>
              <div>▶ $($videoStats.total_plays) total plays</div>
              <div>📈 $([math]::Round($videoStats.avg_completion, 1))% avg completion</div>
            </div>
          </div>
        </div>
        
        <div class="stat-card">
          <div class="stat-icon"><img src="/menu/music.png" style="width:48px; height:48px;"></div>
          <div class="stat-content">
            <h3>Music</h3>
            <div class="stat-value">$($musicStats.unique_tracks)</div>
            <div class="stat-label">Unique Tracks</div>
            <div class="stat-details">
              <div>▶ $($musicStats.total_plays) total plays</div>
              <div>⏱ $musicHours hours listened</div>
            </div>
          </div>
        </div>
        
        <div class="stat-card">
          <div class="stat-icon"><img src="/menu/radio.png" style="width:48px; height:48px;"></div>
          <div class="stat-content">
            <h3>Radio</h3>
            <div class="stat-value">$($radioStats.unique_stations)</div>
            <div class="stat-label">Stations</div>
            <div class="stat-details">
              <div>▶ $($radioStats.total_listens) total listens</div>
              <div>⏱ $radioHours hours listened</div>
            </div>
          </div>
        </div>
      </div>
      
      <div class="top-music-section">
        <h2><img src="/menu/music.png" style="width:28px; height:28px; vertical-align:middle;"> Top 10 Music Tracks</h2>
        <div class="top-music-list">
          $topMusicHtml
        </div>
      </div>
      
      <div style="text-align: center; margin-top: 40px;">
        <a href="/settings" class="btn-back">← Back to Settings</a>
      </div>
    </div>
    
    <style>
    .stats-container { padding: 20px; max-width: 1200px; margin: 0 auto; }
    .page-title { font-size: 32px; margin-bottom: 32px; text-align: center; }
    
    .stats-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: 24px;
      margin-bottom: 40px;
    }
    
    .stat-card {
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 16px;
      padding: 24px;
      display: flex;
      gap: 20px;
    }
    .stat-icon {
      font-size: 48px;
      opacity: 0.8;
    }
    .stat-content { flex: 1; }
    .stat-content h3 {
      margin: 0 0 8px 0;
      font-size: 18px;
      color: rgba(255,255,255,0.6);
    }
    .stat-value {
      font-size: 36px;
      font-weight: 700;
      color: var(--accent);
      margin-bottom: 4px;
    }
    .stat-label {
      font-size: 14px;
      color: rgba(255,255,255,0.5);
      margin-bottom: 16px;
    }
    .stat-details {
      font-size: 14px;
      color: rgba(255,255,255,0.7);
      line-height: 1.8;
    }
    
    .top-music-section {
      background: rgba(255,255,255,0.05);
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 16px;
      padding: 32px;
    }
    .top-music-section h2 {
      margin: 0 0 24px 0;
      font-size: 24px;
    }
    .top-music-list {
      display: grid;
      gap: 12px;
    }
    .stat-item {
      padding: 16px;
      background: rgba(255,255,255,0.03);
      border-radius: 8px;
      display: flex;
      align-items: center;
      gap: 12px;
    }
    .stat-item .rank {
      font-weight: 700;
      color: var(--accent);
      min-width: 30px;
    }
    .stat-item .plays {
      margin-left: auto;
      color: rgba(255,255,255,0.5);
      font-size: 14px;
    }
    
    .btn-back {
      display: inline-block;
      padding: 12px 24px;
      background: rgba(255,255,255,0.1);
      color: #fff;
      text-decoration: none;
      border-radius: 8px;
      border: 1px solid rgba(255,255,255,0.2);
      font-weight: 600;
      transition: all 0.2s;
    }
    .btn-back:hover {
      background: rgba(255,255,255,0.15);
      transform: translateY(-2px);
    }
    </style>
"@
    
    return Get-HTMLPage -Content $content -Title "User Statistics"
}

function Start-MediaServer {
    param([int]$Port = $CONFIG.ServerPort)
    
    Write-Host "`n=== Starting NexusStack Server ===" -ForegroundColor Cyan
    Write-Host "URL: http://$($CONFIG.ServerHost):$Port" -ForegroundColor Green
    
    # Display IP whitelist status
    if ($CONFIG.IPWhitelist.Count -gt 0) {
        Write-Host "`n[Security] IP Whitelist: ENABLED" -ForegroundColor Yellow
        Write-Host "  Allowed IPs:" -ForegroundColor Gray
        foreach ($ip in $CONFIG.IPWhitelist) {
            Write-Host "    • $ip" -ForegroundColor Gray
        }
    }
    else {
        Write-Host "`n[Security] IP Whitelist: DISABLED (All IPs allowed)" -ForegroundColor Yellow
    }
    
    Write-Host "`nPress Ctrl+C to stop the server`n" -ForegroundColor Yellow
    
    # Create cache folders if they don't exist
    if (-not (Test-Path $CONFIG.PosterCacheFolder)) {
        New-Item -ItemType Directory -Path $CONFIG.PosterCacheFolder -Force | Out-Null
    }
    if (-not (Test-Path $CONFIG.AlbumArtCacheFolder)) {
        New-Item -ItemType Directory -Path $CONFIG.AlbumArtCacheFolder -Force | Out-Null
    }
    
    $Global:ServerRunning = $true
    
    # Register CTRL+C handler
    $null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
        $Global:ServerRunning = $false
        Write-Host "`n[!] Shutting down server..." -ForegroundColor Yellow
    }
    
    # Also handle Console.CancelKeyPress for better CTRL+C support
    [Console]::TreatControlCAsInput = $false
    $cancelHandler = {
        $Global:ServerRunning = $false
        Write-Host "`n[!] Received stop signal, shutting down..." -ForegroundColor Yellow
    }
    
    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add("http://$($CONFIG.ServerHost):$Port/")
    
    try {
        Initialize-AsyncStreaming
        
        # Initialize session cleanup timer (runs every 5 minutes)
        if ($null -eq $Global:SessionCleanupTimer) {
            $Global:SessionCleanupTimer = New-Object System.Timers.Timer
            $Global:SessionCleanupTimer.Interval = 300000  # 5 minutes
            $Global:SessionCleanupTimer.AutoReset = $true
            
            Register-ObjectEvent -InputObject $Global:SessionCleanupTimer -EventName Elapsed -Action {
                try {
                    $now = Get-Date
                    $timeout = 30  # 30 minutes of inactivity
                    $toRemove = @()
                    
                    foreach ($kvp in $Global:UserSessions.GetEnumerator()) {
                        $session = $kvp.Value
                        if ($session.LastActivity) {
                            $inactiveMinutes = ($now - $session.LastActivity).TotalMinutes
                            if ($inactiveMinutes -gt $timeout) {
                                $toRemove += $kvp.Key
                            }
                        }
                    }
                    
                    foreach ($sessionId in $toRemove) {
                        $removed = $null
                        if ($Global:UserSessions.TryRemove($sessionId, [ref]$removed)) {
                            Write-Host "[i] Session expired: $($removed.Username) (inactive for $timeout minutes)" -ForegroundColor Yellow
                        }
                    }
                } catch {
                    Write-Host "[✗] Session cleanup error: $_" -ForegroundColor Red
                }
            } | Out-Null
            
            $Global:SessionCleanupTimer.Start()
            Write-Host "[✓] Session cleanup timer started (30-minute timeout)" -ForegroundColor Green
        }
        
        $listener.Start()
        Write-Host "[✓] Server is running!" -ForegroundColor Green
        Write-Host "`nOpen your browser to: http://$($CONFIG.ServerHost):$Port`n" -ForegroundColor Cyan
        
        while ($Global:ServerRunning) {
            # Use async pattern to allow for CTRL+C interruption
            $contextTask = $listener.GetContextAsync()
            
            while (-not $contextTask.IsCompleted -and $Global:ServerRunning) {
                Start-Sleep -Milliseconds 100
                
                # Check for CTRL+C
                if ([Console]::KeyAvailable) {
                    $key = [Console]::ReadKey($true)
                    if ($key.Key -eq 'C' -and $key.Modifiers -eq 'Control') {
                        $Global:ServerRunning = $false
                        Write-Host "`n[!] CTRL+C detected, stopping server..." -ForegroundColor Yellow
                        break
                    }
                }
            }
            
            if (-not $Global:ServerRunning) {
                break
            }
            
            $context = $contextTask.Result
            $request = $context.Request
            $response = $context.Response
            
            # Get remote IP address
            $remoteIP = $request.RemoteEndPoint.Address.ToString()
            
            $url = $request.Url.LocalPath
            Write-Host "$(Get-Date -Format 'HH:mm:ss') - $($request.HttpMethod) $url - IP: $remoteIP" -ForegroundColor Gray
            
            # Check IP whitelist
            if (-not (Test-IPWhitelist -RemoteIP $remoteIP)) {
                Send-AccessDeniedResponse -Response $response -RemoteIP $remoteIP
                continue
            }
            
            # CHECK AUTHENTICATION (except for login/logout routes and static assets)
            # Get session cookie
            $sessionId = $null
            $currentUser = $null
            
            # If SecurityByPin is disabled, auto-login as admin
            if (-not $CONFIG.SecurityByPin) {
                # Check if we already have an admin session
                if (-not $Global:AdminAutoSession) {
                    # Create a persistent admin session
                    $adminUser = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB `
                        -Query "SELECT user_id, username, display_name, user_type FROM users WHERE user_type = 'admin' LIMIT 1"
                    
                    if ($adminUser) {
                        $autoSessionId = [guid]::NewGuid().ToString()
                        
                        # Construct user database path explicitly
                        $userDbPath = Join-Path $CONFIG.UsersDBPath "$($adminUser.username).db"
                        
                        # Debug: verify the path was created
                        if ([string]::IsNullOrWhiteSpace($userDbPath)) {
                            Write-Host "[!] WARNING: UserDbPath is empty! UsersDBPath=$($CONFIG.UsersDBPath), Username=$($adminUser.username)" -ForegroundColor Yellow
                            # Fallback to constructing it manually
                            $userDbPath = Join-Path (Join-Path $PSScriptRoot "users") "$($adminUser.username).db"
                            Write-Host "[i] Using fallback path: $userDbPath" -ForegroundColor Gray
                        }
                        
                        $Global:AdminAutoSession = @{
                            SessionId = $autoSessionId
                            UserId = $adminUser.user_id
                            Username = $adminUser.username
                            DisplayName = if ($adminUser.display_name) { $adminUser.display_name } else { $adminUser.username }
                            UserType = $adminUser.user_type
                            LoginTime = Get-Date
                            LastActivity = Get-Date
                            UserDbPath = $userDbPath
                        }
                        
                        Write-Host "[i] Auto-session created with UserDbPath: $($Global:AdminAutoSession.UserDbPath)" -ForegroundColor Cyan
                        
                        $Global:UserSessions[$autoSessionId] = $Global:AdminAutoSession
                        $sessionId = $autoSessionId
                        $currentUser = $Global:AdminAutoSession
                    }
                }
                else {
                    # Reuse existing admin session
                    $sessionId = $Global:AdminAutoSession.SessionId
                    $currentUser = $Global:AdminAutoSession
                    $currentUser.LastActivity = Get-Date
                }
            }
            else {
                # Normal PIN-based authentication
                if ($request.Cookies["PSMediaSession"]) {
                    $sessionId = $request.Cookies["PSMediaSession"].Value
                    
                    # Look up session
                    if ($Global:UserSessions.ContainsKey($sessionId)) {
                        $currentUser = $Global:UserSessions[$sessionId]
                        
                        # Update session last activity time
                        $currentUser.LastActivity = Get-Date
                    }
                    else {
                        # Invalid session - clear cookie
                        $sessionId = $null
                    }
                }
            }
            
            # Check if authentication is required for this URL
            $requiresAuth = $url -notmatch '^/(login|logout|poster/|albumart/|pdfpreview/|radiologo/|epubcover/|menu/)' `
                -and $url -notmatch '\.(jpg|jpeg|png|gif|css|js)$'
            
            if ($requiresAuth -and -not $currentUser) {
                # Redirect to login (only if SecurityByPin is enabled)
                if ($CONFIG.SecurityByPin) {
                    $response.StatusCode = 302
                    $response.RedirectLocation = "/login"
                    $response.Close()
                    continue
                }
            }
            
            # Set current user for this request
            $Global:CurrentUser = $currentUser
            
            try {
                # Handle POST requests for API endpoints
                if ($request.HttpMethod -eq 'POST' -and $url -eq '/api/update-movie') {
                    # Read POST data
                    $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                    $postData = $reader.ReadToEnd()
                    $reader.Close()
                    
                    try {
                        $data = ConvertFrom-Json $postData
                        
                        # Handle poster image if provided
                        $posterFileName = $null
                        if ($data.posterImage -and -not [string]::IsNullOrWhiteSpace($data.posterImage)) {
                            try {
                                # Extract base64 data (remove data:image/jpeg;base64, prefix)
                                $base64Data = $data.posterImage -replace '^data:image/[^;]+;base64,', ''
                                $imageBytes = [Convert]::FromBase64String($base64Data)
                                
                                # Create poster filename
                                $posterFileName = "movie_$($data.id).jpg"
                                $posterPath = Join-Path $CONFIG.PosterCacheFolder $posterFileName
                                
                                # Save the image
                                [System.IO.File]::WriteAllBytes($posterPath, $imageBytes)
                                
                                Write-Host "[✓] Poster saved: $posterFileName" -ForegroundColor Green
                            }
                            catch {
                                Write-Host "[✗] Error saving poster: $_" -ForegroundColor Red
                                # Continue with metadata update even if poster fails
                            }
                        }
                        
                        # Prepare SQL parameters
                        $sqlParams = @{
                            id = $data.id
                            title = $data.title
                            year = if ($data.year) { $data.year } else { $null }
                            genre = $data.genre
                            director = $data.director
                            cast = $data.cast
                            runtime = if ($data.runtime) { $data.runtime } else { $null }
                            rating = if ($data.rating) { $data.rating } else { $null }
                            overview = $data.overview
                        }
                        
                        # Build UPDATE query - include poster_url if new poster was uploaded
                        if ($posterFileName) {
                            $updateQuery = @"
UPDATE videos 
SET title = @title,
    year = @year,
    genre = @genre,
    director = @director,
    "cast" = @cast,
    runtime = @runtime,
    rating = @rating,
    overview = @overview,
    poster_url = @poster_url,
    manually_edited = 1
WHERE id = @id
"@
                            $sqlParams['poster_url'] = $posterFileName
                        }
                        else {
                            $updateQuery = @"
UPDATE videos 
SET title = @title,
    year = @year,
    genre = @genre,
    director = @director,
    "cast" = @cast,
    runtime = @runtime,
    rating = @rating,
    overview = @overview,
    manually_edited = 1
WHERE id = @id
"@
                        }
                        
                        # Update database with manually edited flag
                        Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query $updateQuery -SqlParameters $sqlParams
                        
                        # Return success response
                        $responseJson = @{ success = $true } | ConvertTo-Json
                        $buffer = [System.Text.Encoding]::UTF8.GetBytes($responseJson)
                        $response.ContentType = "application/json"
                        $response.ContentLength64 = $buffer.Length
                        $response.OutputStream.Write($buffer, 0, $buffer.Length)
                        $response.Close()
                        continue
                    }
                    catch {
                        # Return error response
                        $responseJson = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                        $buffer = [System.Text.Encoding]::UTF8.GetBytes($responseJson)
                        $response.ContentType = "application/json"
                        $response.StatusCode = 500
                        $response.ContentLength64 = $buffer.Length
                        $response.OutputStream.Write($buffer, 0, $buffer.Length)
                        $response.Close()
                        continue
                    }
                }
                
                # Route handling
                $html = switch -Regex ($url) {
                    # ============= AUTHENTICATION ROUTES =============
                    '^/login$' {
                        if ($request.HttpMethod -eq 'GET') {
                            # Check for user query parameter
                            $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                            $selectedUser = $queryParams['user']
                            
                            Get-LoginPage -SelectedUser $selectedUser
                        }
                        elseif ($request.HttpMethod -eq 'POST') {
                            # Handle login POST
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            # Parse form data
                            $formData = [System.Web.HttpUtility]::ParseQueryString($postData)
                            $username = $formData['username']
                            $pin = $formData['pin']
                            
                            # Authenticate
                            $session = Invoke-UserAuthentication -Username $username -PIN $pin
                            
                            if ($session) {
                                # Add LastActivity timestamp
                                $session.LastActivity = Get-Date
                                
                                # Store session
                                $Global:UserSessions.TryAdd($session.SessionId, $session) | Out-Null
                                
                                Write-Host "[✓] User logged in: $username (Session: $($session.SessionId))" -ForegroundColor Green
                                
                                # Set session cookie
                                $cookie = New-Object System.Net.Cookie
                                $cookie.Name = "PSMediaSession"
                                $cookie.Value = $session.SessionId
                                $cookie.Path = "/"
                                $cookie.HttpOnly = $true
                                # Cookie expires when browser closes (no Expires property set)
                                
                                $response.Cookies.Add($cookie)
                                
                                # Redirect to home
                                $response.StatusCode = 302
                                $response.RedirectLocation = "/"
                                $response.Close()
                                continue
                            }
                            else {
                                Write-Host "[✗] Failed login attempt: $username" -ForegroundColor Red
                                Get-LoginPage -Error "Invalid PIN code" -SelectedUser $username
                            }
                        }
                    }
                    
                    '^/logout$' {
                        # Get session ID from cookie
                        $sessionId = $null
                        if ($request.Cookies["PSMediaSession"]) {
                            $sessionId = $request.Cookies["PSMediaSession"].Value
                        }
                        
                        # Clear session from server
                        if ($sessionId) {
                            $removedSession = $null
                            if ($Global:UserSessions.TryRemove($sessionId, [ref]$removedSession)) {
                                Write-Host "[i] User logged out: $($removedSession.Username) (Session: $sessionId)" -ForegroundColor Yellow
                            }
                        }
                        
                        # Clear session cookie by setting it to expire immediately
                        $cookie = New-Object System.Net.Cookie
                        $cookie.Name = "PSMediaSession"
                        $cookie.Value = ""
                        $cookie.Path = "/"
                        $cookie.Expires = (Get-Date).AddDays(-1)  # Expire in the past
                        $response.Cookies.Add($cookie)
                        
                        # Redirect to login
                        $response.StatusCode = 302
                        $response.RedirectLocation = "/login"
                        $response.Close()
                        continue
                    }
                    
                    # ============= SETTINGS & ADMIN ROUTES =============
                    '^/settings$' {
                        Get-SettingsPage -UserSession $Global:CurrentUser
                    }
                    
                    '^/user-stats$' {
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $username = $queryParams['username']
                        if ([string]::IsNullOrWhiteSpace($username)) {
                            $username = $Global:CurrentUser.Username
                        }
                        Get-UserStatsPage -Username $username
                    }
                    
                    # ============= API ROUTES =============
                    '^/api/users/create$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Check if current user is admin
                                if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
                                    $result = @{ success = $false; error = "Access denied" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Create user
                                $success = New-UserAccount -Username $data.username -PIN $data.pin -UserType $data.usertype -DisplayName $data.displayname
                                
                                if ($success) {
                                    Write-Host "[✓] User created: $($data.username)" -ForegroundColor Green
                                    $result = @{ success = $true } | ConvertTo-Json
                                }
                                else {
                                    $result = @{ success = $false; error = "Failed to create user" } | ConvertTo-Json
                                }
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error creating user: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/users/delete$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Check if current user is admin
                                if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
                                    $result = @{ success = $false; error = "Access denied" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Prevent deleting current user
                                if ($data.username -eq $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Cannot delete currently logged in user" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Check if deleting last admin
                                $adminCount = (Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT COUNT(*) as count FROM users WHERE user_type = 'admin' AND is_active = 1").count
                                $targetUser = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "SELECT user_type FROM users WHERE username = @username" -SqlParameters @{ username = $data.username }
                                
                                if ($targetUser.user_type -eq 'admin' -and $adminCount -le 1) {
                                    $result = @{ success = $false; error = "Cannot delete the last admin user" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Delete user
                                Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query "DELETE FROM users WHERE username = @username" -SqlParameters @{ username = $data.username }
                                
                                # Delete user's database file
                                $userDbPath = Join-Path $CONFIG.UsersDBPath "$($data.username).db"
                                if (Test-Path $userDbPath) {
                                    Remove-Item -Path $userDbPath -Force
                                }
                                
                                Write-Host "[✓] User deleted: $($data.username)" -ForegroundColor Green
                                $result = @{ success = $true } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error deleting user: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/users/get$' {
                        if ($request.HttpMethod -eq 'GET') {
                            try {
                                $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                                $username = $queryParams['username']
                                
                                # Check if current user is admin
                                if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
                                    $result = @{ success = $false; error = "Access denied" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Get user data
                                $user = Invoke-SqliteQuery -DataSource $CONFIG.UsersDB `
                                    -Query "SELECT user_id, username, display_name, user_type, avatar_path, created_date, last_login FROM users WHERE username = @username" `
                                    -SqlParameters @{ username = $username }
                                
                                if ($user) {
                                    $result = @{ 
                                        success = $true
                                        data = @{
                                            user_id = $user.user_id
                                            username = $user.username
                                            display_name = $user.display_name
                                            user_type = $user.user_type
                                            avatar_path = $user.avatar_path
                                            created_date = $user.created_date
                                            last_login = $user.last_login
                                        }
                                    } | ConvertTo-Json
                                } else {
                                    $result = @{ success = $false; error = "User not found" } | ConvertTo-Json
                                }
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error getting user: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/users/edit$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Check if current user is admin
                                if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
                                    $result = @{ success = $false; error = "Access denied" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Build UPDATE query dynamically
                                $updates = @()
                                $params = @{ username = $data.username }
                                
                                if ($data.PSObject.Properties.Name -contains 'displayName') {
                                    $updates += "display_name = @displayName"
                                    $params.displayName = $data.displayName
                                }
                                
                                if ($data.PSObject.Properties.Name -contains 'userType') {
                                    $updates += "user_type = @userType"
                                    $params.userType = $data.userType
                                }
                                
                                if ($data.PSObject.Properties.Name -contains 'avatarPath') {
                                    $updates += "avatar_path = @avatarPath"
                                    $params.avatarPath = $data.avatarPath
                                }
                                
                                if ($data.PSObject.Properties.Name -contains 'pin' -and -not [string]::IsNullOrWhiteSpace($data.pin)) {
                                    # Encrypt new PIN
                                    $encrypted = Get-EncryptedPIN -PIN $data.pin
                                    $updates += "pin_hash = @pinHash, pin_salt = @pinSalt"
                                    $params.pinHash = $encrypted.Hash
                                    $params.pinSalt = $encrypted.Salt
                                }
                                
                                if ($updates.Count -gt 0) {
                                    $updateQuery = "UPDATE users SET $($updates -join ', ') WHERE username = @username"
                                    Invoke-SqliteQuery -DataSource $CONFIG.UsersDB -Query $updateQuery -SqlParameters $params
                                    
                                    Write-Host "[✓] User updated: $($data.username)" -ForegroundColor Green
                                    $result = @{ success = $true } | ConvertTo-Json
                                } else {
                                    $result = @{ success = $false; error = "No fields to update" } | ConvertTo-Json
                                }
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error updating user: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/users/upload-avatar$' {
                        if ($request.HttpMethod -eq 'POST') {
                            try {
                                # Check if current user is admin
                                if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
                                    $result = @{ success = $false; error = "Access denied" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Read raw bytes from request
                                $memStream = New-Object System.IO.MemoryStream
                                $request.InputStream.CopyTo($memStream)
                                $requestBytes = $memStream.ToArray()
                                $memStream.Close()
                                
                                # Convert to string for parsing headers (using Latin1 to preserve bytes)
                                $content = [System.Text.Encoding]::GetEncoding('ISO-8859-1').GetString($requestBytes)
                                
                                # Extract boundary
                                $boundary = $request.ContentType.Split('=')[1]
                                
                                # Extract username
                                $usernameMatch = [regex]::Match($content, 'name="username"[^\r\n]*\r\n\r\n([^\r\n]+)')
                                $username = if ($usernameMatch.Success) { $usernameMatch.Groups[1].Value } else { $null }
                                
                                # Find file section
                                $fileMatch = [regex]::Match($content, 'name="avatar"[^\r\n]*filename="([^"]+)"[^\r\n]*\r\nContent-Type: ([^\r\n]+)\r\n\r\n')
                                if (-not $fileMatch.Success -or -not $username) {
                                    throw "Invalid upload data"
                                }
                                
                                $filename = $fileMatch.Groups[1].Value
                                $contentType = $fileMatch.Groups[2].Value
                                
                                # Find binary data boundaries in BYTE array
                                $dataStartText = $fileMatch.Index + $fileMatch.Length
                                $boundaryBytes = [System.Text.Encoding]::ASCII.GetBytes("`r`n--$boundary")
                                
                                # Find where file data ends
                                $dataStart = $dataStartText
                                $dataEnd = -1
                                for ($i = $dataStart; $i -lt $requestBytes.Length - $boundaryBytes.Length; $i++) {
                                    $match = $true
                                    for ($j = 0; $j -lt $boundaryBytes.Length; $j++) {
                                        if ($requestBytes[$i + $j] -ne $boundaryBytes[$j]) {
                                            $match = $false
                                            break
                                        }
                                    }
                                    if ($match) {
                                        $dataEnd = $i
                                        break
                                    }
                                }
                                
                                if ($dataEnd -eq -1) {
                                    throw "Could not find end of file data"
                                }
                                
                                # Extract file bytes
                                $fileLength = $dataEnd - $dataStart
                                $fileBytes = New-Object byte[] $fileLength
                                [Array]::Copy($requestBytes, $dataStart, $fileBytes, 0, $fileLength)
                                
                                # Create avatars folder if it doesn't exist
                                if (-not (Test-Path $CONFIG.AvatarFolder)) {
                                    New-Item -Path $CONFIG.AvatarFolder -ItemType Directory -Force | Out-Null
                                }
                                
                                # Generate unique filename
                                $extension = [System.IO.Path]::GetExtension($filename)
                                $newFilename = "avatar_$username_$(Get-Date -Format 'yyyyMMddHHmmss')$extension"
                                $avatarPath = Join-Path $CONFIG.AvatarFolder $newFilename
                                
                                # Save bytes directly
                                [System.IO.File]::WriteAllBytes($avatarPath, $fileBytes)
                                
                                Write-Host "[✓] Avatar uploaded: $newFilename ($fileLength bytes)" -ForegroundColor Green
                                $result = @{ success = $true; filename = $newFilename } | ConvertTo-Json
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error uploading avatar: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/avatar/(.+)$' {
                        # Serve avatar images
                        $avatarFile = $matches[1]
                        $avatarPath = Join-Path $CONFIG.AvatarFolder $avatarFile
                        
                        if (Test-Path $avatarPath) {
                            $imageBytes = [System.IO.File]::ReadAllBytes($avatarPath)
                            $extension = [System.IO.Path]::GetExtension($avatarFile).ToLower()
                            $contentType = switch ($extension) {
                                '.png' { 'image/png' }
                                '.jpg' { 'image/jpeg' }
                                '.jpeg' { 'image/jpeg' }
                                '.gif' { 'image/gif' }
                                '.webp' { 'image/webp' }
                                default { 'image/jpeg' }
                            }
                            
                            $response.ContentType = $contentType
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    
                    '^/api/config/save$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Check if current user is admin
                                if (-not $Global:CurrentUser -or $Global:CurrentUser.UserType -ne 'admin') {
                                    $result = @{ success = $false; error = "Access denied" } | ConvertTo-Json
                                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                    $response.ContentType = "application/json"
                                    $response.ContentLength64 = $buffer.Length
                                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                    $response.Close()
                                    continue
                                }
                                
                                # Save configuration
                                $configPath = Join-Path $PSScriptRoot "PSMediaLib.conf"
                                Set-Content -Path $configPath -Value $data.config -Encoding UTF8
                                
                                Write-Host "[✓] Configuration saved by: $($Global:CurrentUser.Username)" -ForegroundColor Green
                                $result = @{ success = $true } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error saving configuration: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    # ============= TRACKING API ROUTES =============
                    '^/api/track/video$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Track video progress
                                if ($Global:CurrentUser) {
                                    # If path is empty but we have ID, look up the path and other info
                                    $videoPath = $data.path
                                    $videoId = $data.id
                                    $posterUrl = $data.posterUrl
                                    $mediaType = $data.mediaType
                                    
                                    if ([string]::IsNullOrWhiteSpace($videoPath) -and $data.id) {
                                        $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                                            -Query "SELECT filepath, poster_url, media_type FROM videos WHERE id = @id" `
                                            -SqlParameters @{ id = $data.id }
                                        if ($video) {
                                            $videoPath = $video.filepath
                                            if (-not $posterUrl) { $posterUrl = $video.poster_url }
                                            if (-not $mediaType) { $mediaType = $video.media_type }
                                        }
                                    }
                                    
                                    if (-not [string]::IsNullOrWhiteSpace($videoPath)) {
                                        Update-VideoProgress `
                                            -VideoPath $videoPath `
                                            -PositionSeconds $data.position `
                                            -DurationSeconds $data.duration `
                                            -VideoTitle $data.title `
                                            -VideoId $videoId `
                                            -PosterUrl $posterUrl `
                                            -MediaType $mediaType `
                                            -UserSession $Global:CurrentUser
                                        
                                        $result = @{ success = $true } | ConvertTo-Json
                                    } else {
                                        $result = @{ success = $false; error = "Video path not found" } | ConvertTo-Json
                                    }
                                } else {
                                    $result = @{ success = $false; error = "Not authenticated" } | ConvertTo-Json
                                }
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error tracking video: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/video/mark-watched$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                $result = @{ success = $false; error = "Unknown error" }
                                
                                if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Not authenticated" }
                                }
                                else {
                                    $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                    $videoId = $data.videoId
                                    $watched = $data.watched  # true = watched, false = unwatched
                                    
                                    # Get video info from main database
                                    $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                                        -Query "SELECT * FROM videos WHERE id = @id" `
                                        -SqlParameters @{ id = $videoId }
                                    
                                    if ($video) {
                                        $now = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                                        
                                        if ($watched) {
                                            # Mark as watched - set to 100% complete
                                            Invoke-SqliteQuery -DataSource $userDbPath -Query @"
INSERT INTO video_progress (video_id, video_path, video_title, poster_url, media_type, last_position_seconds, duration_seconds, percent_watched, watch_count, last_watched, completed, manually_marked)
VALUES (@video_id, @video_path, @video_title, @poster_url, @media_type, @duration, @duration, 100, 1, @timestamp, 1, 1)
ON CONFLICT(video_path) DO UPDATE SET
    percent_watched = 100,
    completed = 1,
    manually_marked = 1,
    last_watched = @timestamp,
    watch_count = watch_count + 1;
"@ -SqlParameters @{
                                                video_id = $videoId
                                                video_path = $video.filepath
                                                video_title = $video.title
                                                poster_url = $video.poster_url
                                                media_type = if ($video.media_type) { $video.media_type } else { "movie" }
                                                duration = if ($video.duration) { $video.duration } else { 0 }
                                                timestamp = $now
                                            }
                                        }
                                        else {
                                            # Mark as unwatched - reset progress
                                            Invoke-SqliteQuery -DataSource $userDbPath -Query @"
UPDATE video_progress 
SET percent_watched = 0, 
    completed = 0, 
    manually_marked = 1,
    last_position_seconds = 0
WHERE video_path = @video_path OR video_id = @video_id;
"@ -SqlParameters @{ video_path = $video.filepath; video_id = $videoId }
                                        }
                                        
                                        Write-Host "[✓] Video marked as $(if ($watched) { 'watched' } else { 'unwatched' }): $($video.title)" -ForegroundColor Green
                                        $result = @{ success = $true; watched = $watched }
                                    }
                                    else {
                                        $result = @{ success = $false; error = "Video not found" }
                                    }
                                }
                                
                                $json = $result | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error marking video: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/video/get-progress$' {
                        # Get video progress for a specific video
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $videoId = $queryParams['id']
                        
                        Write-Host "[i] Get progress request for video ID: $videoId" -ForegroundColor Cyan
                        
                        try {
                            $result = @{ success = $false; position = 0 }
                            
                            if ($Global:CurrentUser -and $Global:CurrentUser.Username -and $videoId) {
                                $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                
                                # Get video path first
                                $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                                    -Query "SELECT filepath FROM videos WHERE id = @id" `
                                    -SqlParameters @{ id = $videoId }
                                
                                if ($video) {
                                    Write-Host "[i] Found video path: $($video.filepath)" -ForegroundColor Gray
                                    
                                    $progress = Invoke-SqliteQuery -DataSource $userDbPath `
                                        -Query "SELECT * FROM video_progress WHERE video_path = @path OR video_id = @id LIMIT 1" `
                                        -SqlParameters @{ path = $video.filepath; id = $videoId }
                                    
                                    if ($progress) {
                                        Write-Host "[i] Found progress: Position=$($progress.last_position_seconds)s, Percent=$($progress.percent_watched)%, Completed=$($progress.completed)" -ForegroundColor Green
                                        
                                        if ($progress.last_position_seconds -gt 30 -and $progress.percent_watched -lt 90 -and $progress.completed -ne 1) {
                                            $result = @{
                                                success = $true
                                                position = $progress.last_position_seconds
                                                duration = $progress.duration_seconds
                                                percent = $progress.percent_watched
                                                completed = $progress.completed
                                            }
                                            Write-Host "[✓] Returning resume position: $($progress.last_position_seconds)s" -ForegroundColor Green
                                        }
                                        else {
                                            Write-Host "[i] Progress exists but not resumable (pos<30 or >90% or completed)" -ForegroundColor Yellow
                                            $result = @{ success = $true; position = 0 }
                                        }
                                    }
                                    else {
                                        Write-Host "[i] No progress found for this video" -ForegroundColor Yellow
                                        $result = @{ success = $true; position = 0 }
                                    }
                                }
                                else {
                                    Write-Host "[!] Video not found in database" -ForegroundColor Red
                                }
                            }
                            else {
                                Write-Host "[!] No user session or video ID" -ForegroundColor Red
                            }
                            
                            $json = $result | ConvertTo-Json
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                            $response.ContentType = "application/json"
                            $response.ContentLength64 = $buffer.Length
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                            continue
                        }
                        catch {
                            Write-Host "[✗] Get progress error: $_" -ForegroundColor Red
                            $result = @{ success = $false; position = 0 } | ConvertTo-Json
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                            $response.ContentType = "application/json"
                            $response.ContentLength64 = $buffer.Length
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                            continue
                        }
                    }
                    
                    '^/api/track/music$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Track music play
                                if ($Global:CurrentUser) {
                                    Update-MusicHistory `
                                        -MusicPath $data.path `
                                        -Artist $data.artist `
                                        -Title $data.title `
                                        -Album $data.album `
                                        -PlayTimeSeconds $data.playTime `
                                        -UserSession $Global:CurrentUser
                                    
                                    $result = @{ success = $true } | ConvertTo-Json
                                } else {
                                    $result = @{ success = $false; error = "Not authenticated" } | ConvertTo-Json
                                }
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error tracking music: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/track/radio$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                
                                # Track radio listening
                                if ($Global:CurrentUser) {
                                    Update-RadioHistory `
                                        -RadioUrl $data.url `
                                        -RadioName $data.name `
                                        -ListenTimeSeconds $data.listenTime `
                                        -UserSession $Global:CurrentUser
                                    
                                    $result = @{ success = $true } | ConvertTo-Json
                                } else {
                                    $result = @{ success = $false; error = "Not authenticated" } | ConvertTo-Json
                                }
                                
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error tracking radio: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    # ============= PLAYLIST API ROUTES =============
                    '^/api/playlist/create$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                $result = @{ success = $false; error = "Unknown error" }
                                
                                if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Not authenticated" }
                                }
                                elseif (-not $data.name -or $data.name.Length -lt 1 -or $data.name.Length -gt 30) {
                                    $result = @{ success = $false; error = "Name must be 1-30 characters" }
                                }
                                else {
                                    $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                    
                                    # Check playlist count limit (max 20)
                                    $count = (Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT COUNT(*) as count FROM playlists").count
                                    if ($count -ge 20) {
                                        $result = @{ success = $false; error = "Maximum 20 playlists allowed" }
                                    }
                                    else {
                                        $now = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                                        Invoke-SqliteQuery -DataSource $userDbPath -Query @"
INSERT INTO playlists (name, created_date, modified_date, track_count)
VALUES (@name, @created, @modified, 0)
"@ -SqlParameters @{ name = $data.name; created = $now; modified = $now }
                                        
                                        Write-Host "[✓] Playlist created: $($data.name)" -ForegroundColor Green
                                        $result = @{ success = $true }
                                    }
                                }
                                
                                $json = $result | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error creating playlist: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/playlist/rename$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                $result = @{ success = $false; error = "Unknown error" }
                                
                                if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Not authenticated" }
                                }
                                elseif (-not $data.name -or $data.name.Length -lt 1 -or $data.name.Length -gt 30) {
                                    $result = @{ success = $false; error = "Name must be 1-30 characters" }
                                }
                                else {
                                    $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                    $now = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                                    
                                    Invoke-SqliteQuery -DataSource $userDbPath -Query @"
UPDATE playlists SET name = @name, modified_date = @modified WHERE playlist_id = @id
"@ -SqlParameters @{ name = $data.name; modified = $now; id = $data.playlistId }
                                    
                                    Write-Host "[✓] Playlist renamed: $($data.name)" -ForegroundColor Green
                                    $result = @{ success = $true }
                                }
                                
                                $json = $result | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error renaming playlist: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/playlist/delete$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                $result = @{ success = $false; error = "Unknown error" }
                                
                                if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Not authenticated" }
                                }
                                else {
                                    $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                    
                                    # Delete tracks first, then playlist
                                    Invoke-SqliteQuery -DataSource $userDbPath -Query "DELETE FROM playlist_tracks WHERE playlist_id = @id" -SqlParameters @{ id = $data.playlistId }
                                    Invoke-SqliteQuery -DataSource $userDbPath -Query "DELETE FROM playlists WHERE playlist_id = @id" -SqlParameters @{ id = $data.playlistId }
                                    
                                    Write-Host "[✓] Playlist deleted: $($data.playlistId)" -ForegroundColor Green
                                    $result = @{ success = $true }
                                }
                                
                                $json = $result | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error deleting playlist: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/playlist/add-track$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                $result = @{ success = $false; error = "Unknown error" }
                                
                                if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Not authenticated" }
                                }
                                else {
                                    $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                    
                                    # Check if track already in playlist
                                    $existing = Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT track_id FROM playlist_tracks WHERE playlist_id = @plId AND music_id = @mId
"@ -SqlParameters @{ plId = $data.playlistId; mId = $data.musicId }
                                    
                                    if ($existing) {
                                        $result = @{ success = $false; error = "Track already in playlist" }
                                    }
                                    else {
                                        # Get next track order
                                        $maxOrder = (Invoke-SqliteQuery -DataSource $userDbPath -Query @"
SELECT COALESCE(MAX(track_order), 0) as max_order FROM playlist_tracks WHERE playlist_id = @id
"@ -SqlParameters @{ id = $data.playlistId }).max_order
                                        
                                        $now = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                                        
                                        # Add track
                                        Invoke-SqliteQuery -DataSource $userDbPath -Query @"
INSERT INTO playlist_tracks (playlist_id, music_id, music_path, title, artist, album, track_order, added_date)
VALUES (@plId, @mId, @path, @title, @artist, @album, @order, @added)
"@ -SqlParameters @{
                                            plId = $data.playlistId
                                            mId = $data.musicId
                                            path = $data.path
                                            title = $data.title
                                            artist = $data.artist
                                            album = $data.album
                                            order = $maxOrder + 1
                                            added = $now
                                        }
                                        
                                        # Update playlist track count and modified date
                                        Invoke-SqliteQuery -DataSource $userDbPath -Query @"
UPDATE playlists 
SET track_count = (SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id = @id),
    modified_date = @modified
WHERE playlist_id = @id
"@ -SqlParameters @{ id = $data.playlistId; modified = $now }
                                        
                                        Write-Host "[✓] Track added to playlist: $($data.title)" -ForegroundColor Green
                                        $result = @{ success = $true }
                                    }
                                }
                                
                                $json = $result | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error adding track: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    '^/api/playlist/remove-track$' {
                        if ($request.HttpMethod -eq 'POST') {
                            $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding)
                            $postData = $reader.ReadToEnd()
                            $reader.Close()
                            
                            try {
                                $data = ConvertFrom-Json $postData
                                $result = @{ success = $false; error = "Unknown error" }
                                
                                if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                                    $result = @{ success = $false; error = "Not authenticated" }
                                }
                                else {
                                    $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                                    
                                    # Get playlist_id before deleting
                                    $track = Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT playlist_id FROM playlist_tracks WHERE track_id = @id" -SqlParameters @{ id = $data.trackId }
                                    
                                    if ($track) {
                                        # Delete track
                                        Invoke-SqliteQuery -DataSource $userDbPath -Query "DELETE FROM playlist_tracks WHERE track_id = @id" -SqlParameters @{ id = $data.trackId }
                                        
                                        # Update playlist track count
                                        $now = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
                                        Invoke-SqliteQuery -DataSource $userDbPath -Query @"
UPDATE playlists 
SET track_count = (SELECT COUNT(*) FROM playlist_tracks WHERE playlist_id = @id),
    modified_date = @modified
WHERE playlist_id = @id
"@ -SqlParameters @{ id = $track.playlist_id; modified = $now }
                                        
                                        Write-Host "[✓] Track removed from playlist" -ForegroundColor Green
                                        $result = @{ success = $true }
                                    }
                                    else {
                                        $result = @{ success = $false; error = "Track not found" }
                                    }
                                }
                                
                                $json = $result | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($json)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                            catch {
                                Write-Host "[✗] Error removing track: $_" -ForegroundColor Red
                                $result = @{ success = $false; error = $_.Exception.Message } | ConvertTo-Json
                                $buffer = [System.Text.Encoding]::UTF8.GetBytes($result)
                                $response.ContentType = "application/json"
                                $response.ContentLength64 = $buffer.Length
                                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                                $response.Close()
                                continue
                            }
                        }
                    }
                    
                    # ============= MEDIA LIBRARY ROUTES =============
                    '^/$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $sortParams = $queryParams.GetValues('sort')
                        Get-HomePage -SortParams $sortParams
                    }
                    '^/videos$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filter = $queryParams['filter']
                        if ([string]::IsNullOrWhiteSpace($filter)) { $filter = "all" }
                        Get-VideosPage -Filter $filter
                    }
                    '^/videos/genres$' {
                        Get-GenreListPage
                    }
                    '^/videos/genre/(.+)$' {
                        $genreName = [System.Web.HttpUtility]::UrlDecode($matches[1])
                        Get-GenreVideosPage -Genre $genreName
                    }
                    '^/music$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filter = $queryParams['filter']
                        if ([string]::IsNullOrWhiteSpace($filter)) { $filter = "all" }
                        Get-MusicPage -Filter $filter
                    }
                    '^/playlists$' {
                        Get-PlaylistsPage
                    }
                    '^/playlist/(\d+)$' {
                        $playlistId = [int]$matches[1]
                        Get-PlaylistDetailPage -PlaylistId $playlistId
                    }
                    '^/pictures$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filter = $queryParams['filter']
                        if ([string]::IsNullOrWhiteSpace($filter)) { $filter = "all" }
                        Get-PicturesPage -Filter $filter
                    }
                    '^/pdfs$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filter = $queryParams['filter']
                        if ([string]::IsNullOrWhiteSpace($filter)) { $filter = "all" }
                        Get-PDFsPage -Filter $filter
                    }
                    '^/radio$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filter = $queryParams['filter']
                        if ([string]::IsNullOrWhiteSpace($filter)) { $filter = "all" }
                        Get-RadioPage -Filter $filter
                    }
                    '^/tv$' { 
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filter = $queryParams['filter']
                        $page = $queryParams['page']
                        if ([string]::IsNullOrWhiteSpace($filter)) { $filter = "all" }
                        if ([string]::IsNullOrWhiteSpace($page)) { $page = 1 } else { $page = [int]$page }
                        Get-TVPage -Filter $filter -Page $page
                    }
                    '^/analysis' { Get-AnalysisPage }
                    '^/search' {
                        # Search functionality
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $searchQuery = $queryParams['q']
                        $searchType = $queryParams['type']
                        
                        Get-SearchPage -Query $searchQuery -Type $searchType
                    }
                    '^/menu/(.+)$' {
                        # Serve menu icon images
                        $iconFile = $matches[1]
                        $scriptDir = Split-Path -Parent $PSCommandPath
                        $iconPath = Join-Path $scriptDir "menu" $iconFile
                        
                        if (Test-Path $iconPath) {
                            $imageBytes = [System.IO.File]::ReadAllBytes($iconPath)
                            $response.ContentType = "image/png"
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/poster/(.+)$' {
                        # Serve poster images
                        $posterFile = $matches[1]
                        $posterPath = Join-Path $CONFIG.PosterCacheFolder $posterFile
                        
                        Write-Host "  Poster requested: $posterFile" -ForegroundColor Cyan
                        Write-Host "  Full path: $posterPath" -ForegroundColor Gray
                        
                        if (Test-Path $posterPath) {
                            Write-Host "  ✓ Serving poster" -ForegroundColor Green
                            $imageBytes = [System.IO.File]::ReadAllBytes($posterPath)
                            $response.ContentType = "image/jpeg"
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ Poster not found!" -ForegroundColor Red
                            # Return 404 if poster not found
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/albumart/(.+)$' {
                        # NEW: Serve album artwork images
                        $artFile = $matches[1]
                        $artPath = Join-Path $CONFIG.AlbumArtCacheFolder $artFile
                        
                        Write-Host "  Album art requested: $artFile" -ForegroundColor Cyan
                        
                        if (Test-Path $artPath) {
                            Write-Host "  ✓ Serving album art" -ForegroundColor Green
                            $imageBytes = [System.IO.File]::ReadAllBytes($artPath)
                            $response.ContentType = "image/jpeg"
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ Album art not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/radiologos/(.+)$' {
                        # Serve radio station logos
                        $logoFile = $matches[1]
                        $logoPath = Join-Path $CONFIG.RadioLogoCacheFolder $logoFile
                        
                        Write-Host "  Radio logo requested: $logoFile" -ForegroundColor Cyan
                        
                        if (Test-Path $logoPath) {
                            Write-Host "  ✓ Serving radio logo" -ForegroundColor Green
                            
                            # Determine content type based on extension
                            $contentType = switch ([System.IO.Path]::GetExtension($logoFile).ToLower()) {
                                '.png' { 'image/png' }
                                '.jpg' { 'image/jpeg' }
                                '.jpeg' { 'image/jpeg' }
                                '.svg' { 'image/svg+xml' }
                                '.gif' { 'image/gif' }
                                '.webp' { 'image/webp' }
                                default { 'image/png' }
                            }
                            
                            $imageBytes = [System.IO.File]::ReadAllBytes($logoPath)
                            $response.ContentType = $contentType
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ Radio logo not found: $logoPath" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/tvlogos/(.+)$' {
                        # Serve TV channel logos
                        $logoFile = $matches[1]
                        $logoPath = Join-Path $CONFIG.TVLogoCacheFolder $logoFile
                        
                        if (Test-Path $logoPath) {
                            # Determine content type based on extension
                            $contentType = switch ([System.IO.Path]::GetExtension($logoFile).ToLower()) {
                                '.png' { 'image/png' }
                                '.jpg' { 'image/jpeg' }
                                '.jpeg' { 'image/jpeg' }
                                '.svg' { 'image/svg+xml' }
                                '.gif' { 'image/gif' }
                                '.webp' { 'image/webp' }
                                default { 'image/png' }
                            }
                            
                            $imageBytes = [System.IO.File]::ReadAllBytes($logoPath)
                            $response.ContentType = $contentType
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/videojs/(.+)$' {
                        # Serve local Video.js files for offline operation
                        $videojsFile = $matches[1]
                        $videojsPath = Join-Path $CONFIG.ToolsFolder "videojs\$videojsFile"
                        
                        if (Test-Path $videojsPath) {
                            $fileBytes = [System.IO.File]::ReadAllBytes($videojsPath)
                            
                            # Determine content type based on extension
                            $contentType = if ($videojsFile -like "*.css") { "text/css" } else { "application/javascript" }
                            
                            $response.ContentType = $contentType
                            $response.ContentLength64 = $fileBytes.Length
                            $response.OutputStream.Write($fileBytes, 0, $fileBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/pdfpreview/(.+)$' {
                        # Serve PDF preview/thumbnail or EPUB cover images
                        $previewFile = $matches[1]
                        $previewPath = Join-Path $CONFIG.PDFPreviewCacheFolder $previewFile
                        
                        # If not found in PDF cache, check EPUB cache
                        if (-not (Test-Path $previewPath)) {
                            $previewPath = Join-Path $CONFIG.EPUBCoverCacheFolder $previewFile
                        }
                        
                        Write-Host "  Preview requested: $previewFile" -ForegroundColor Cyan
                        
                        if (Test-Path $previewPath) {
                            Write-Host "  ✓ Serving preview" -ForegroundColor Green
                            $imageBytes = [System.IO.File]::ReadAllBytes($previewPath)
                            $response.ContentType = "image/jpeg"
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ PDF preview not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/extract-artwork$' {
                        # Trigger album artwork extraction
                        Get-HTMLPage -Content @"
<div style='padding:60px; text-align:center;'>
  <h1>🎨 Extracting Album Artwork...</h1>
  <p style='font-size:16px; color:var(--muted); margin:20px 0;'>
    This may take several minutes depending on your library size.
  </p>
  <p style='font-size:14px; color:var(--muted);'>
    Extraction is running. Please wait...
  </p>
  <div style='margin-top:30px;'>
    <a href='/music' class='btn' style='text-decoration:none; display:inline-block;'>← Back to Music</a>
  </div>
</div>
"@ -Title "Extracting Artwork"
                        
                        # Run extraction synchronously (same as PDF generation)
                        Extract-AllAlbumArtwork -Force:$false
                        
                        Write-Host "  ✓ Album artwork extraction complete" -ForegroundColor Green
                    }
                    '^/generate-pdf-thumbnails$' {
                        # Trigger PDF thumbnail generation
                        Get-HTMLPage -Content @"
<div style='padding:60px; text-align:center;'>
  <h1>🎨 Generating PDF Thumbnails...</h1>
  <p style='font-size:16px; color:var(--muted); margin:20px 0;'>
    Creating preview images for your PDF library.
  </p>
  <p style='font-size:14px; color:var(--muted);'>
    This process is running in the foreground. Please wait...
  </p>
  <div style='margin-top:30px;'>
    <a href='/pdfs' class='btn' style='text-decoration:none; display:inline-block;'>← Back to PDFs</a>
  </div>
</div>
"@ -Title "Generating PDF Thumbnails"
                        
                        # Run generation synchronously (simpler than background job)
                        Generate-AllPDFThumbnails
                        
                        Write-Host "  ✓ PDF thumbnail generation complete" -ForegroundColor Green
                    }
                    '^/movie/(.+)$' {
                        # Show movie details/info page (like Netflix/Plex)
                        $videoId = $matches[1]
                        
                        $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                            -Query "SELECT * FROM videos WHERE id = @id" `
                            -SqlParameters @{ id = $videoId }
                        
                        if ($video) {
                            $size = [math]::Round($video.size_bytes / 1GB, 2)
                            
                            # Format rating as stars (out of 10)
                            $ratingStars = ""
                            $ratingScore = ""
                            if ($video.rating) {
                                $rating = [math]::Round($video.rating, 1)
                                $fullStars = [math]::Floor($rating)
                                
                                for ($i = 1; $i -le $fullStars; $i++) {
                                    $ratingStars += "★"
                                }
                                for ($i = ($fullStars + 1); $i -le 10; $i++) {
                                    $ratingStars += "☆"
                                }
                                $ratingStars = $ratingStars.Substring(0, [math]::Min(10, $ratingStars.Length))
                                $ratingScore = "$rating"
                            } else {
                                $ratingStars = "No rating available"
                                $ratingScore = ""
                            }
                            
                            # Format runtime
                            $runtimeDisplay = if ($video.runtime) {
                                "$($video.runtime) min"
                            } elseif ($video.duration) {
                                $mins = [math]::Floor($video.duration / 60)
                                "${mins} min"
                            } else {
                                "Unknown"
                            }
                            
                            # Format year
                            $yearDisplay = if ($video.year) { $video.year } else { "Unknown" }
                            
                            # Format genres
                            $genreDisplay = if ($video.genre) {
                                $video.genre
                            } else {
                                "Unknown"
                            }
                            
                            # Format director
                            $directorDisplay = if ($video.director -and -not [string]::IsNullOrWhiteSpace($video.director)) {
                                $video.director
                            } else {
                                "Unknown"
                            }
                            
                            # Format actors
                            $actorsDisplay = if ($video.cast -and -not [string]::IsNullOrWhiteSpace($video.cast)) {
                                $video.cast
                            } else {
                                "Not available - fetch metadata from TMDB to populate cast information"
                            }
                            
                            # Poster URL
                            $posterUrl = if ($video.poster_url) {
                                "/poster/$($video.poster_url)"
                            } else {
                                ""
                            }
                            
                            # Backdrop for background - use backdrop image if available, otherwise fall back to poster
                            $backdropStyle = if ($video.backdrop_url) {
                                "background-image: linear-gradient(to right, rgba(10,20,16,0.75) 0%, rgba(10,20,16,0.65) 50%, rgba(10,20,16,0.55) 100%), url('/poster/$($video.backdrop_url)');"
                            } elseif ($video.poster_url) {
                                "background-image: linear-gradient(to right, rgba(10,20,16,0.95) 0%, rgba(10,20,16,0.85) 50%, rgba(10,20,16,0.7) 100%), url('/poster/$($video.poster_url)');"
                            } else {
                                "background: linear-gradient(135deg, rgba(52,211,153,0.1), rgba(34,197,94,0.05));"
                            }
                            
                            # Overview/Description
                            $overviewText = if ($video.overview -and -not [string]::IsNullOrWhiteSpace($video.overview)) {
                                $video.overview
                            } else {
                                "No description available. Fetch metadata from TMDB to populate the movie synopsis and details."
                            }
                            
                            # Format resolution badge
                            $resolutionBadge = if ($video.resolution) {
                                $video.resolution.ToUpper()
                            } else {
                                "1080"
                            }
                            
                            # Audio codec
                            $audioCodec = if ($video.audio_codec) {
                                $video.audio_codec.ToUpper()
                            } else {
                                "H.264"
                            }
                            
                            # Age rating
                            $ageRating = "12"
                            
                            # Format file size
                            $fileSizeDisplay = "${size}"
                            
                            # Parse audio tracks from database and format for display
                            $languagesDisplay = "Unknown"
                            $audioTrackBadges = ""
                            
                            # If audio_tracks not in database, try to extract on-the-fly
                            if ((-not $video.audio_tracks -or [string]::IsNullOrWhiteSpace($video.audio_tracks)) -and $Global:FFmpegPath) {
                                try {
                                    Write-Host "[i] Extracting audio tracks for video ID $videoId..." -ForegroundColor Gray
                                    $videoInfo = Get-VideoInfo -FilePath $video.filepath
                                    if ($videoInfo -and $videoInfo.AudioTracks) {
                                        # Update database with audio tracks
                                        Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query @"
UPDATE videos SET audio_tracks = @tracks WHERE id = @id
"@ -SqlParameters @{
                                            tracks = $videoInfo.AudioTracks
                                            id = $videoId
                                        }
                                        # Use the newly extracted tracks
                                        $video.audio_tracks = $videoInfo.AudioTracks
                                        Write-Host "[✓] Audio tracks extracted and saved" -ForegroundColor Green
                                    }
                                }
                                catch {
                                    Write-Verbose "Could not extract audio tracks: $_"
                                }
                            }
                            
                            if ($video.audio_tracks -and -not [string]::IsNullOrWhiteSpace($video.audio_tracks)) {
                                try {
                                    $audioTracks = $video.audio_tracks | ConvertFrom-Json
                                    if ($audioTracks) {
                                        # Create language display (comma-separated list)
                                        $languages = @()
                                        foreach ($track in $audioTracks) {
                                            $lang = if ($track.language -and $track.language -ne "und") { 
                                                $track.language.ToUpper() 
                                            } else { 
                                                "Unknown" 
                                            }
                                            if ($languages -notcontains $lang) {
                                                $languages += $lang
                                            }
                                        }
                                        $languagesDisplay = $languages -join ", "
                                        
                                        # Create audio track badges (language + channels)
                                        foreach ($track in $audioTracks) {
                                            $lang = if ($track.language -and $track.language -ne "und") { 
                                                $track.language.ToUpper() 
                                            } else { 
                                                "UNK" 
                                            }
                                            $channels = if ($track.channels) { 
                                                switch ($track.channels) {
                                                    1 { "Mono" }
                                                    2 { "Stereo" }
                                                    6 { "5.1" }
                                                    8 { "7.1" }
                                                    default { "$($track.channels)ch" }
                                                }
                                            } else { 
                                                "" 
                                            }
                                            $audioTrackBadges += "<span style='display:inline-block; padding:6px 12px; background:rgba(96,165,250,.15); border:1px solid rgba(96,165,250,.3); border-radius:6px; font-size:12px; font-weight:600; margin-right:6px; margin-top:4px;'>🔊 $lang $channels</span>"
                                        }
                                    }
                                }
                                catch {
                                    Write-Verbose "Could not parse audio tracks: $_"
                                }
                            }
                            
                            # Subtitles (hardcoded for now - would need subtitle track extraction)
                            $subtitlesDisplay = "Not available"
                            
                            $movieInfoContent = @"
<style>
.movie-info-container {
    position: relative;
    min-height: 100vh;
    color: var(--text);
}

.movie-backdrop {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    height: 100%;
    background-size: cover;
    background-position: center;
    z-index: 0;
    $backdropStyle
}

.movie-backdrop::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom, transparent 0%, rgba(10,20,16,0.5) 50%, var(--bg) 100%);
    z-index: 1;
}

.movie-content-wrapper {
    position: relative;
    z-index: 2;
    padding: 40px;
    max-width: 1400px;
    margin: 0 auto;
}

.movie-nav-tabs {
    display: flex;
    background: rgba(0,0,0,0.6);
    border-radius: 12px 12px 0 0;
    overflow: hidden;
    /* backdrop-filter removed for performance */
    border: 1px solid rgba(255,255,255,0.1);
    border-bottom: none;
}

.nav-tab {
    padding: 16px 32px;
    background: transparent;
    color: rgba(255,255,255,0.7);
    border: none;
    cursor: pointer;
    font-size: 15px;
    font-weight: 600;
    transition: all 0.3s ease;
    border-bottom: 3px solid transparent;
    flex: 1;
    text-align: center;
}

.nav-tab:hover {
    background: rgba(255,255,255,0.05);
    color: rgba(255,255,255,0.9);
}

.nav-tab.active {
    background: rgba(255,255,255,0.08);
    color: #fff;
    border-bottom-color: #60a5fa;
}

.movie-main-panel {
    background: rgba(0,0,0,0.7);
    border-radius: 0 0 16px 16px;
    padding: 40px;
    /* backdrop-filter removed for performance */
    border: 1px solid rgba(255,255,255,0.1);
    border-top: none;
}

.movie-layout {
    display: grid;
    grid-template-columns: 500px 1fr;
    gap: 40px;
    align-items: start;
}

.movie-poster-wrapper {
    position: relative;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 20px 60px rgba(0,0,0,0.6);
    transition: transform 0.3s ease;
}

.movie-poster-wrapper:hover {
    transform: scale(1.02);
}

.movie-poster {
    width: 100%;
    display: block;
    background: rgba(255,255,255,0.05);
    aspect-ratio: 16/9;
    object-fit: cover;
    border-radius: 8px;
}

.play-overlay {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 80px;
    height: 80px;
    background: rgba(96,165,250,0.9);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
    opacity: 0;
}

.movie-poster-wrapper:hover .play-overlay {
    opacity: 1;
}

.play-overlay:hover {
    background: #60a5fa;
    transform: translate(-50%, -50%) scale(1.1);
}

.play-icon {
    color: white;
    font-size: 32px;
    margin-left: 4px;
}

.movie-status-indicators {
    display: flex;
    gap: 8px;
    margin-top: 12px;
}

.status-indicator {
    background: rgba(34,197,94,0.15);
    border: 1px solid rgba(34,197,94,0.3);
    color: #4ade80;
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
}

.movie-title {
    font-size: 42px;
    font-weight: 700;
    margin: 0 0 16px 0;
    line-height: 1.2;
    color: #fff;
    text-shadow: 2px 2px 12px rgba(0,0,0,0.8);
    /* Limit title to 2 lines and truncate with ellipsis */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-word;
}

.movie-metadata-row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}

.metadata-item {
    color: rgba(255,255,255,0.8);
    font-size: 15px;
}

.metadata-label {
    color: #60a5fa;
    font-weight: 600;
}

.metadata-separator {
    color: rgba(255,255,255,0.3);
}

.movie-description {
    line-height: 1.8;
    color: rgba(255,255,255,0.85);
    font-size: 15px;
    margin: 20px 0;
}

.rating-section {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 20px 0;
}

.rating-stars {
    color: #fbbf24;
    font-size: 18px;
    letter-spacing: 2px;
}

.rating-score {
    background: rgba(251,191,36,0.15);
    border: 1px solid rgba(251,191,36,0.3);
    color: #fbbf24;
    padding: 6px 14px;
    border-radius: 8px;
    font-size: 16px;
    font-weight: 700;
}

.tech-specs-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 12px;
    margin: 20px 0;
}

.tech-spec {
    background: rgba(96,165,250,0.1);
    border: 1px solid rgba(96,165,250,0.2);
    padding: 12px;
    border-radius: 8px;
    text-align: center;
}

.spec-icon {
    font-size: 24px;
    margin-bottom: 8px;
}

.spec-value {
    font-size: 16px;
    font-weight: 700;
    color: #60a5fa;
    margin-bottom: 4px;
}

.spec-label {
    font-size: 11px;
    color: rgba(255,255,255,0.6);
    text-transform: uppercase;
}

.action-buttons {
    display: flex;
    gap: 12px;
    flex-wrap: wrap;
    margin-top: 24px;
}

.action-btn {
    padding: 14px 28px;
    border-radius: 10px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition: all 0.2s ease;
    border: none;
    display: inline-flex;
    align-items: center;
    gap: 10px;
}

.action-btn.primary {
    background: #60a5fa;
    color: #fff;
}

.action-btn.primary:hover {
    background: #3b82f6;
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(96,165,250,0.4);
}

.action-btn.secondary {
    background: rgba(255,255,255,0.1);
    color: #fff;
    border: 1px solid rgba(255,255,255,0.2);
}

.action-btn.secondary:hover {
    background: rgba(255,255,255,0.15);
    transform: translateY(-2px);
}

.info-label {
    color: #60a5fa;
    font-size: 13px;
    font-weight: 600;
}

.info-value {
    color: rgba(255,255,255,0.9);
}

@media (max-width: 1024px) {
    .movie-layout {
        grid-template-columns: 1fr;
    }
    .movie-poster-wrapper {
        max-width: 300px;
        margin: 0 auto;
    }
}

@media (max-width: 768px) {
    .movie-content-wrapper {
        padding: 20px;
    }
    .movie-main-panel {
        padding: 24px;
    }
    .movie-title {
        font-size: 28px;
    }
    .nav-tab {
        padding: 12px 16px;
        font-size: 13px;
    }
}
</style>

<div class="movie-info-container">
    <div class="movie-backdrop"></div>
    
    <div class="movie-content-wrapper">
        <div class="movie-nav-tabs">
            <button class="nav-tab active" onclick="showTab(event, 'overview')">Overview/Start</button>
            <button class="nav-tab" onclick="showTab(event, 'description')">Description</button>
            <button class="nav-tab" onclick="showTab(event, 'actors')">Actors</button>
        </div>
        
        <div class="movie-main-panel">
            <div id="overview-tab" class="tab-content">
                <div class="movie-layout">
                    <div class="movie-poster-section">
                        <div class="movie-poster-wrapper" onclick="window.location.href='/player/$videoId'">
                            $(if ($video.backdrop_url) {
                                "<img src='/poster/$($video.backdrop_url)' alt='$($video.title)' class='movie-poster' style='opacity: 0.85;'>"
                            } elseif ($posterUrl) {
                                "<img src='$posterUrl' alt='$($video.title)' class='movie-poster'>"
                            } else {
                                "<div class='movie-poster' style='background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(52,211,153,0.2)); display: flex; align-items: center; justify-content: center; font-size: 48px;'>🎬</div>"
                            })
                            <div class="play-overlay">
                                <span class="play-icon">▶</span>
                            </div>
                        </div>
                        <div class="movie-status-indicators">
                            <span class="status-indicator">● Movie</span>
                        </div>
                    </div>
                    
                    <div class="movie-details-section">
                        <h1 class="movie-title">$($video.title)</h1>
                        
                        <div class="movie-metadata-row">
                            <span class="metadata-item"><span class="metadata-label">Release date</span> $yearDisplay</span>
                            <span class="metadata-separator">|</span>
                            <span class="metadata-item"><span class="metadata-label">Running Time</span> $runtimeDisplay</span>
                        </div>
                        
                        <div class="movie-metadata-row">
                            <span class="metadata-item"><span class="metadata-label">Genre</span> $genreDisplay</span>
                            <span class="metadata-separator">|</span>
                            <span class="metadata-item"><span class="metadata-label">Director</span> $directorDisplay</span>
                        </div>
                        
                        <div class="movie-metadata-row">
                            <span class="metadata-item"><span class="metadata-label">Actors</span> $actorsDisplay</span>
                        </div>
                        
                        <div class="movie-description">
                            $overviewText
                        </div>
                        
                        <div class="rating-section">
                            <span class="rating-stars">$ratingStars</span>
                            $(if ($ratingScore) { "<span class='rating-score'>$ratingScore</span>" })
                        </div>
                        
                        <div class="tech-specs-grid">
                            <div class="tech-spec">
                                <div class="spec-icon">$ageRating</div>
                                <div class="spec-value">$ageRating</div>
                                <div class="spec-label">Age Rating</div>
                            </div>
                            <div class="tech-spec">
                                <div class="spec-icon">🎬</div>
                                <div class="spec-value">$resolutionBadge</div>
                                <div class="spec-label">Quality</div>
                            </div>
                            <div class="tech-spec">
                                <div class="spec-icon">🔊</div>
                                <div class="spec-value">$audioCodec</div>
                                <div class="spec-label">Audio</div>
                            </div>
                            <div class="tech-spec">
                                <div class="spec-icon">💾</div>
                                <div class="spec-value">$fileSizeDisplay</div>
                                <div class="spec-label">File Size</div>
                            </div>
                        </div>
                        
                        <div style="margin: 20px 0; padding: 16px; background: rgba(96,165,250,0.08); border: 1px solid rgba(96,165,250,0.15); border-radius: 10px;">
                            <div style="margin-bottom: 8px;">
                                <span class="info-label">Audio Tracks:</span> <span class="info-value">$languagesDisplay</span>
                                $(if ($audioTrackBadges) { "<div style='margin-top:8px;'>$audioTrackBadges</div>" })
                            </div>
                            <div>
                                <span class="info-label">Subtitles:</span> <span class="info-value">$subtitlesDisplay</span>
                            </div>
                        </div>
                        
                        <div class="action-buttons">
                            <a href="/player/$videoId" class="action-btn primary" id="playBtn">
                                <span style="font-size: 18px;">▶</span>
                                <span id="playBtnText">Play Movie</span>
                            </a>
                            <button onclick="toggleWatched()" class="action-btn" id="watchedBtn" style="background: rgba(139,92,246,0.2); color: #a78bfa; border: 1px solid rgba(139,92,246,0.3);">
                                <span id="watchedIcon">👁</span>
                                <span id="watchedText">Mark as Watched</span>
                            </button>
                            <button onclick="openEditModal()" class="action-btn" style="background: #10b981; color: #fff;">
                                <span>✏️</span>
                                <span>Edit Metadata</span>
                            </button>
                            <a href="/stream/$videoId" download="$($video.filename)" class="action-btn secondary">
                                <span>⬇</span>
                                <span>Download</span>
                            </a>
                            <a href="/videos" class="action-btn secondary">
                                <span>←</span>
                                <span>Back to Library</span>
                            </a>
                        </div>
                        
                        <div id="progressInfo" style="display: none; margin-top: 16px; padding: 12px 16px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); border-radius: 8px;">
                            <span style="color: #f87171;">▶ Resume from </span>
                            <span id="progressTime" style="color: #fff; font-weight: 600;"></span>
                            <span style="color: rgba(255,255,255,0.5);"> (<span id="progressPercent"></span>% watched)</span>
                        </div>
                    </div>
                </div>
            </div>
            
            <div id="description-tab" class="tab-content" style="display: none;">
                <div style="max-width: 900px;">
                    <h2 style="font-size: 28px; margin-bottom: 20px; color: #60a5fa;">Description</h2>
                    <div style="font-size: 16px; line-height: 1.8; color: rgba(255,255,255,0.85);">
                        $overviewText
                    </div>
                    
                    <div style="margin-top: 40px;">
                        <h3 style="font-size: 20px; margin-bottom: 16px; color: #60a5fa;">Movie Details</h3>
                        <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px;">
                            <div>
                                <div class="info-label">Director</div>
                                <div class="info-value">$directorDisplay</div>
                            </div>
                            <div>
                                <div class="info-label">Year</div>
                                <div class="info-value">$yearDisplay</div>
                            </div>
                            <div>
                                <div class="info-label">Genre</div>
                                <div class="info-value">$genreDisplay</div>
                            </div>
                            <div>
                                <div class="info-label">Runtime</div>
                                <div class="info-value">$runtimeDisplay</div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            <div id="actors-tab" class="tab-content" style="display: none;">
                <div style="max-width: 900px;">
                    <h2 style="font-size: 28px; margin-bottom: 20px; color: #60a5fa;">Cast</h2>
                    <div style="font-size: 16px; line-height: 1.8; color: rgba(255,255,255,0.85);">
                        $actorsDisplay
                    </div>
                    <div style="margin-top: 20px; padding: 16px; background: rgba(96,165,250,0.08); border: 1px solid rgba(96,165,250,0.15); border-radius: 10px; color: rgba(255,255,255,0.7);">
                        <em>Tip: Fetch metadata from TMDB using "Fetch Posters" in the Videos section to populate complete cast information.</em>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
function showTab(event, tabName) {
    const tabs = document.querySelectorAll('.tab-content');
    tabs.forEach(tab => tab.style.display = 'none');
    
    const navTabs = document.querySelectorAll('.nav-tab');
    navTabs.forEach(tab => tab.classList.remove('active'));
    
    document.getElementById(tabName + '-tab').style.display = 'block';
    event.target.classList.add('active');
}

function openEditModal() {
    document.getElementById('editModal').style.display = 'flex';
    
    // Setup poster file input handler
    const posterInput = document.getElementById('edit_poster');
    posterInput.addEventListener('change', handlePosterUpload);
}

function closeEditModal() {
    document.getElementById('editModal').style.display = 'none';
}

let posterImageData = null;

function handlePosterUpload(event) {
    const file = event.target.files[0];
    const errorDiv = document.getElementById('posterError');
    const previewDiv = document.getElementById('posterPreview');
    const previewImg = document.getElementById('posterPreviewImg');
    const dimensionsDiv = document.getElementById('posterDimensions');
    
    errorDiv.style.display = 'none';
    previewDiv.style.display = 'none';
    posterImageData = null;
    
    if (!file) return;
    
    // Check file type
    if (!file.type.match(/image\/(jpeg|jpg|png)/)) {
        errorDiv.textContent = 'Invalid file type. Please upload a JPG or PNG image.';
        errorDiv.style.display = 'block';
        event.target.value = '';
        return;
    }
    
    const reader = new FileReader();
    reader.onload = function(e) {
        const img = new Image();
        img.onload = function() {
            const width = img.width;
            const height = img.height;
            
            // Check minimum dimensions
            if (width < 500 || height < 750) {
                errorDiv.textContent = 'Image too small! Minimum size is 500x750px. Your image is ' + width + 'x' + height + 'px.';
                errorDiv.style.display = 'block';
                event.target.value = '';
                return;
            }
            
            // Create canvas for resizing
            const canvas = document.createElement('canvas');
            canvas.width = 500;
            canvas.height = 750;
            const ctx = canvas.getContext('2d');
            
            // Draw image resized to 500x750
            ctx.drawImage(img, 0, 0, 500, 750);
            
            // Convert to JPEG base64
            posterImageData = canvas.toDataURL('image/jpeg', 0.9);
            
            // Show preview
            previewImg.src = posterImageData;
            dimensionsDiv.textContent = 'Original: ' + width + 'x' + height + 'px → Resized to: 500x750px';
            previewDiv.style.display = 'block';
        };
        img.src = e.target.result;
    };
    reader.readAsDataURL(file);
}

function saveMetadata() {
    const formData = {
        id: $videoId,
        title: document.getElementById('edit_title').value,
        year: document.getElementById('edit_year').value,
        genre: document.getElementById('edit_genre').value,
        director: document.getElementById('edit_director').value,
        cast: document.getElementById('edit_cast').value,
        runtime: document.getElementById('edit_runtime').value,
        rating: document.getElementById('edit_rating').value,
        overview: document.getElementById('edit_overview').value,
        posterImage: posterImageData
    };
    
    fetch('/api/update-movie', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            alert('Metadata updated successfully!');
            location.reload();
        } else {
            alert('Error updating metadata: ' + data.error);
        }
    })
    .catch(error => {
        alert('Error: ' + error);
    });
}

// Watch progress and watched status
var videoId = $videoId;
var isWatched = false;
var savedPosition = 0;

function formatTime(seconds) {
    var hrs = Math.floor(seconds / 3600);
    var mins = Math.floor((seconds % 3600) / 60);
    var secs = Math.floor(seconds % 60);
    if (hrs > 0) {
        return hrs + ':' + String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
    }
    return mins + ':' + String(secs).padStart(2, '0');
}

function checkProgress() {
    fetch('/api/video/get-progress?id=' + videoId)
        .then(r => r.json())
        .then(data => {
            if (data.success && data.position > 0) {
                savedPosition = data.position;
                document.getElementById('playBtnText').textContent = 'Resume';
                document.getElementById('playBtn').href = '/player/' + videoId + '?resume=1';
                document.getElementById('progressInfo').style.display = 'block';
                document.getElementById('progressTime').textContent = formatTime(data.position);
                document.getElementById('progressPercent').textContent = Math.round(data.percent || 0);
                
                if (data.completed) {
                    updateWatchedUI(true);
                }
            }
            if (data.completed) {
                updateWatchedUI(true);
            }
        })
        .catch(function() {});
}

function updateWatchedUI(watched) {
    isWatched = watched;
    var btn = document.getElementById('watchedBtn');
    var icon = document.getElementById('watchedIcon');
    var text = document.getElementById('watchedText');
    
    if (watched) {
        btn.style.background = 'rgba(34,197,94,0.2)';
        btn.style.color = '#22c55e';
        btn.style.borderColor = 'rgba(34,197,94,0.3)';
        icon.textContent = '✓';
        text.textContent = 'Watched';
    } else {
        btn.style.background = 'rgba(139,92,246,0.2)';
        btn.style.color = '#a78bfa';
        btn.style.borderColor = 'rgba(139,92,246,0.3)';
        icon.textContent = '👁';
        text.textContent = 'Mark as Watched';
    }
}

function toggleWatched() {
    var newStatus = !isWatched;
    
    fetch('/api/video/mark-watched', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ videoId: videoId, watched: newStatus })
    })
    .then(r => r.json())
    .then(data => {
        if (data.success) {
            updateWatchedUI(data.watched);
            if (data.watched) {
                // Hide resume info if marked as watched
                document.getElementById('progressInfo').style.display = 'none';
                document.getElementById('playBtnText').textContent = 'Play Movie';
                document.getElementById('playBtn').href = '/player/' + videoId;
            }
        } else {
            alert(data.error || 'Failed to update');
        }
    })
    .catch(function() {
        alert('Error updating watched status');
    });
}

// Check progress on page load
document.addEventListener('DOMContentLoaded', checkProgress);
</script>

<!-- Edit Modal -->
<div id="editModal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.9); z-index:1000; align-items:center; justify-content:center; padding:20px; overflow-y:auto;">
    <div style="background:rgba(20,30,26,0.98); border:1px solid rgba(255,255,255,0.1); border-radius:16px; max-width:700px; width:100%; padding:32px; position:relative; margin:auto;">
        <button onclick="closeEditModal()" style="position:absolute; top:16px; right:16px; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2); color:#fff; width:36px; height:36px; border-radius:8px; cursor:pointer; font-size:20px; line-height:1;">×</button>
        
        <h2 style="color:#60a5fa; margin-bottom:24px; font-size:28px;">Edit Movie Metadata</h2>
        
        <div style="display:grid; gap:16px;">
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Title</label>
                <input type="text" id="edit_title" value="$($video.title -replace '"', '&quot;')" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
            </div>
            
            <div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
                <div>
                    <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Year</label>
                    <input type="number" id="edit_year" value="$($video.year)" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
                </div>
                <div>
                    <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Runtime (minutes)</label>
                    <input type="number" id="edit_runtime" value="$($video.runtime)" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
                </div>
            </div>
            
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Genre</label>
                <input type="text" id="edit_genre" value="$($video.genre -replace '"', '&quot;')" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
            </div>
            
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Director</label>
                <input type="text" id="edit_director" value="$($video.director -replace '"', '&quot;')" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
            </div>
            
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Cast / Actors</label>
                <input type="text" id="edit_cast" value="$($video.cast -replace '"', '&quot;')" placeholder="Comma-separated list of actor names" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
            </div>
            
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Rating (0-10)</label>
                <input type="number" step="0.1" min="0" max="10" id="edit_rating" value="$($video.rating)" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
            </div>
            
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Overview / Description</label>
                <textarea id="edit_overview" rows="6" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px; resize:vertical; font-family:inherit;">$($video.overview)</textarea>
            </div>
            
            <div>
                <label style="display:block; color:#60a5fa; font-size:13px; font-weight:600; margin-bottom:6px;">Poster Image</label>
                <div style="display:flex; gap:12px; align-items:end;">
                    <div style="flex:1;">
                        <input type="file" id="edit_poster" accept="image/jpeg,image/jpg,image/png" style="width:100%; padding:12px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#fff; font-size:15px;">
                        <div style="margin-top:6px; font-size:12px; color:rgba(255,255,255,0.6);">
                            Required: Min 500x750px • Will be resized to 500x750px if larger • JPG/PNG only
                        </div>
                    </div>
                </div>
                <div id="posterPreview" style="margin-top:12px; display:none;">
                    <img id="posterPreviewImg" style="max-width:150px; border-radius:8px; border:2px solid rgba(96,165,250,0.3);">
                    <div id="posterDimensions" style="margin-top:6px; font-size:13px; color:rgba(255,255,255,0.7);"></div>
                </div>
                <div id="posterError" style="margin-top:8px; padding:12px; background:rgba(239,68,68,0.1); border:1px solid rgba(239,68,68,0.3); border-radius:8px; color:#ef4444; font-size:13px; display:none;"></div>
            </div>
        </div>
        
        <div style="margin-top:24px; display:flex; gap:12px; justify-content:flex-end;">
            <button onclick="closeEditModal()" style="padding:12px 24px; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2); border-radius:8px; color:#fff; cursor:pointer; font-size:15px; font-weight:600;">Cancel</button>
            <button onclick="saveMetadata()" style="padding:12px 24px; background:#10b981; border:none; border-radius:8px; color:#fff; cursor:pointer; font-size:15px; font-weight:600;">Save Changes</button>
        </div>
        
        <div style="margin-top:16px; padding:12px; background:rgba(251,191,36,0.1); border:1px solid rgba(251,191,36,0.2); border-radius:8px; color:rgba(255,255,255,0.8); font-size:13px;">
            ⚠️ Note: Manually edited metadata will not be overwritten when fetching from TMDB.
        </div>
    </div>
</div>
"@
                            
                            Get-HTMLPage -Content $movieInfoContent -Title "$($video.title) - Movie Info"
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                        }
                    }
                    '^/player/(.+)$' {
                        # Show video player page with options
                        $videoId = $matches[1]
                        
                        $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                            -Query "SELECT * FROM videos WHERE id = @id" `
                            -SqlParameters @{ id = $videoId }
                        
                        if ($video) {
                            $size = [math]::Round($video.size_bytes / 1GB, 2)
                            
                            # Determine proper MIME type for the video
                            # For transcoded formats, always use video/mp4 since that's what the browser will receive
                            $extension = $video.format.ToLower().TrimStart('.')
                            $transcodeFormats = @('mkv', 'avi', 'wmv', 'flv', 'mov', 'mpg', 'mpeg', 'vob', 'ts', 'webm', 'divx', '3gp')
                            
                            # Check if this video needs transcoding (including MP4 codec check)
                            $needsTranscode = Test-VideoNeedsTranscoding -FilePath $video.filepath
                            
                            if ($needsTranscode -or ($transcodeFormats -contains $extension)) {
                                # This format will be transcoded to MP4
                                $videoMimeType = 'video/mp4'
                            }
                            else {
                                # Native formats
                                $videoMimeType = switch ($extension) {
                                    'mp4'  { 'video/mp4' }
                                    'webm' { 'video/webm' }
                                    default { 'video/mp4' }
                                }
                            }
                            
                            # Parse audio tracks from database for pre-playback selection
                            $audioTracksJson = "[]"
                            $hasMultipleAudioTracks = $false
                            if ($video.audio_tracks -and -not [string]::IsNullOrWhiteSpace($video.audio_tracks)) {
                                try {
                                    $audioTracksData = $video.audio_tracks | ConvertFrom-Json
                                    if ($audioTracksData -and $audioTracksData.Count -gt 1) {
                                        $hasMultipleAudioTracks = $true
                                        $audioTracksJson = $video.audio_tracks
                                    }
                                }
                                catch {
                                    Write-Verbose "Could not parse audio tracks for player: $_"
                                }
                            }
                            
                            $playerContent = @"
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />

<div style='max-width:1400px; margin:40px auto; padding:20px;'>
  <h1 style='margin-bottom:10px;'>$($video.title)</h1>
  <p style='color:var(--muted); margin-bottom:30px;'>$($video.format) • ${size}GB</p>
  
  <div id='audio-selector-overlay' style='display:$(if ($hasMultipleAudioTracks) { "flex" } else { "none" }); position:absolute; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.95); z-index:1000; align-items:center; justify-content:center; border-radius:12px;'>
    <div style='text-align:center; max-width:500px; padding:40px;'>
      <h2 style='color:#60a5fa; margin-bottom:12px; font-size:28px;'>🔊 Select Audio Track</h2>
      <p style='color:rgba(255,255,255,0.7); margin-bottom:32px; font-size:15px;'>Choose your preferred language before playback starts</p>
      <div id='audio-track-buttons' style='display:flex; flex-direction:column; gap:12px;'></div>
    </div>
  </div>
  
  <div style='position:relative; padding-bottom:56.25%; height:0; overflow:hidden; border-radius:12px; background:#000;'>
    <video id='my-video' controls preload='auto' 
           style='position:absolute; top:0; left:0; width:100%; height:100%;'>
      <p class='vjs-no-js'>
        To view this video please enable JavaScript, and consider upgrading to a web browser that
        <a href='https://videojs.com/html5-video-support/' target='_blank'>supports HTML5 video</a>
      </p>
    </video>
  </div>
  
  <div style='margin-top:20px; padding:16px; background:rgba(96,165,250,.1); border:1px solid rgba(96,165,250,.2); border-radius:12px;'>
    <div style='display:grid; grid-template-columns:1fr auto; gap:16px; align-items:center;'>
      <div style='font-size:13px; color:var(--text);'>
        <div style='margin-bottom:8px;'>
          <strong>Resolution:</strong> <span id='videoResolution'>Loading...</span> • 
          <strong>Duration:</strong> <span id='videoDuration'>--:--</span>
        </div>
        <div style='font-size:12px; color:var(--muted);'>
          <strong>Format:</strong> $($video.format.ToUpper()) • 
          <strong>Size:</strong> ${size}GB • 
          <strong>Player:</strong> Video.js
        </div>
      </div>
      <div style='font-size:12px; color:var(--muted); text-align:right;'>
        <div>⚙️ Settings for playback speed</div>
        <div style='margin-top:4px;'>⌨️ Space=Play F=Fullscreen M=Mute</div>
      </div>
    </div>
  </div>
  
  <div style='margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;'>
    <a href='/stream/$videoId' download='$($video.filename)' class='btn' style='text-decoration:none; display:inline-block;'>
      ⬇️ Download File
    </a>
    <a href='/videos' class='btn' style='text-decoration:none; display:inline-block;'>
      ← Back to Videos
    </a>
    <a href='/' class='btn' style='text-decoration:none; display:inline-block;'>
      🏠 Home
    </a>
  </div>
  
<script src='https://vjs.zencdn.net/8.10.0/video.min.js'></script>
<script>
// Audio track data from server
var audioTracks = $audioTracksJson;
var selectedAudioTrack = 0;  // Default to first track

// Populate audio track selector if multiple tracks exist
if (audioTracks.length > 1) {
    var buttonContainer = document.getElementById('audio-track-buttons');
    audioTracks.forEach(function(track, index) {
        var lang = track.language && track.language !== 'und' ? track.language.toUpperCase() : 'Track ' + (index + 1);
        var title = track.title || '';
        var channels = '';
        if (track.channels) {
            switch(track.channels) {
                case 1: channels = 'Mono'; break;
                case 2: channels = 'Stereo'; break;
                case 6: channels = '5.1'; break;
                case 8: channels = '7.1'; break;
                default: channels = track.channels + 'ch';
            }
        }
        
        var label = lang + (channels ? ' • ' + channels : '') + (title ? ' (' + title + ')' : '');
        
        var button = document.createElement('button');
        button.textContent = label;
        button.className = 'audio-track-select-btn';
        button.onclick = function() {
            selectedAudioTrack = index;
            hideAudioSelectorAndPlay();
        };
        buttonContainer.appendChild(button);
    });
}

function hideAudioSelectorAndPlay() {
    document.getElementById('audio-selector-overlay').style.display = 'none';
    console.log('Selected audio track:', selectedAudioTrack);
    // Initialize player with selected track
    initializePlayer();
}

// If no audio selector needed, initialize immediately
if (audioTracks.length <= 1) {
    initializePlayer();
}

var playerInitialized = false; // Guard against double initialization

function initializePlayer() {
// Prevent double initialization
if (playerInitialized) {
    console.log('Player already initialized, skipping...');
    return;
}
playerInitialized = true;

// Add Video.js classes before initialization
var videoEl = document.getElementById('my-video');
videoEl.classList.add('video-js');
videoEl.classList.add('vjs-big-play-centered');

var player = videojs('my-video', {
    controls: true,
    autoplay: false,
    preload: 'auto',
    responsive: true,
    playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
    controlBar: {
        pictureInPictureToggle: true,
        volumePanel: {
            inline: false
        },
        audioTrackButton: true  // Enable audio track selector button
    },
    userActions: {
        hotkeys: true  // Enable keyboard shortcuts
    }
});

// CRITICAL: Stop transcoding when user navigates away from the player
var currentTranscodeId = null;
var videoId = '$videoId';  // Video ID from server

// Check if video needs HLS transcoding using dedicated API (with selected audio track)
fetch('/api/stream-info/' + videoId + '?audioTrack=' + selectedAudioTrack)
    .then(response => response.json())
    .then(data => {
        if (data.type === 'hls') {
            // HLS transcoded video
            console.log('Loading HLS stream:', data.playlistUrl);
            currentTranscodeId = data.transcodeId;  // Track transcode ID for cleanup
            console.log('Tracking transcode:', currentTranscodeId);
            
            player.src({
                src: data.playlistUrl,
                type: 'application/x-mpegURL'
            });
        } else if (data.type === 'direct') {
            // Direct MP4 stream
            console.log('Using direct stream');
            player.src({
                src: '/stream/$videoId',
                type: '$videoMimeType'
            });
        }
    })
    .catch(error => {
        console.error('Error checking stream type:', error);
        // Fallback to direct stream
        player.src({
            src: '/stream/$videoId',
            type: '$videoMimeType'
        });
    });

// Display video information when metadata loads
player.on('loadedmetadata', function() {
    var videoWidth = player.videoWidth();
    var videoHeight = player.videoHeight();
    var resolution = videoWidth + 'x' + videoHeight + ' (' + videoHeight + 'p)';
    document.getElementById('videoResolution').textContent = resolution;
    
    var duration = player.duration();
    var minutes = Math.floor(duration / 60);
    var seconds = Math.floor(duration % 60);
    var durationText = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
    document.getElementById('videoDuration').textContent = durationText;
    
    console.log('Video Resolution:', resolution);
    console.log('Duration:', durationText);
    
    // Log audio tracks
    var audioTracks = player.audioTracks();
    console.log('Number of audio tracks:', audioTracks.length);
    for (var i = 0; i < audioTracks.length; i++) {
        var track = audioTracks[i];
        console.log('Audio Track', i + ':', {
            id: track.id,
            kind: track.kind,
            label: track.label,
            language: track.language,
            enabled: track.enabled
        });
    }
    
    // Always check for saved position
    var urlParams = new URLSearchParams(window.location.search);
    var autoResume = urlParams.get('resume') === '1';
    
    fetch('/api/video/get-progress?id=' + videoId)
        .then(function(r) { return r.json(); })
        .then(function(data) {
            console.log('Progress data:', data);
            if (data.success && data.position > 0 && data.position < duration - 30) {
                if (autoResume) {
                    // Auto-resume if ?resume=1 was passed
                    console.log('Auto-resuming from position:', data.position);
                    player.currentTime(data.position);
                    player.play();
                    showResumeNotice(data.position);
                } else {
                    // Show resume dialog
                    showResumeDialog(data.position, data.percent);
                }
            }
        })
        .catch(function(err) { console.error('Error checking progress:', err); });
    
    // Video.js automatically handles audio track switching for HLS streams
    // The audio track button will appear automatically if multiple tracks are detected
});

function formatTimeForDisplay(seconds) {
    var hrs = Math.floor(seconds / 3600);
    var mins = Math.floor((seconds % 3600) / 60);
    var secs = Math.floor(seconds % 60);
    if (hrs > 0) {
        return hrs + ':' + String(mins).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
    }
    return mins + ':' + String(secs).padStart(2, '0');
}

function showResumeNotice(position) {
    var resumeNotice = document.createElement('div');
    resumeNotice.style.cssText = 'position:absolute; top:10px; left:10px; background:rgba(239,68,68,0.9); color:white; padding:10px 20px; border-radius:8px; font-size:14px; font-weight:600; z-index:100;';
    resumeNotice.textContent = 'Resuming from ' + formatTimeForDisplay(position);
    player.el().appendChild(resumeNotice);
    setTimeout(function() { resumeNotice.remove(); }, 4000);
}

function showResumeDialog(position, percent) {
    var dialog = document.createElement('div');
    dialog.id = 'resumeDialog';
    dialog.style.cssText = 'position:absolute; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; z-index:1000;';
    dialog.innerHTML = 
        '<div style="background:rgba(30,40,35,0.98); border:1px solid rgba(255,255,255,0.1); border-radius:16px; padding:32px; text-align:center; max-width:400px;">' +
            '<div style="font-size:48px; margin-bottom:16px;">▶️</div>' +
            '<h3 style="color:#fff; margin:0 0 8px 0; font-size:20px;">Resume Watching?</h3>' +
            '<p style="color:rgba(255,255,255,0.7); margin:0 0 24px 0;">You were at <strong style="color:#ef4444;">' + formatTimeForDisplay(position) + '</strong> (' + Math.round(percent) + '% watched)</p>' +
            '<div style="display:flex; gap:12px; justify-content:center;">' +
                '<button id="resumeBtn" style="padding:12px 24px; background:#ef4444; color:#fff; border:none; border-radius:8px; font-size:15px; font-weight:600; cursor:pointer;">Resume</button>' +
                '<button id="startOverBtn" style="padding:12px 24px; background:rgba(255,255,255,0.1); color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:8px; font-size:15px; cursor:pointer;">Start Over</button>' +
            '</div>' +
        '</div>';
    
    player.el().appendChild(dialog);
    
    document.getElementById('resumeBtn').onclick = function() {
        dialog.remove();
        player.currentTime(position);
        player.play();
        showResumeNotice(position);
    };
    
    document.getElementById('startOverBtn').onclick = function() {
        dialog.remove();
        player.currentTime(0);
        player.play();
    };
}

// Better error handling
player.on('error', function() {
    var error = player.error();
    console.error('Playback error:', error);
    
    if (error) {
        var errorMessage = 'Video playback error. ';
        if (error.code === 4) {
            errorMessage += 'This format may not be supported in your browser. Try downloading the file instead.';
        }
        alert(errorMessage);
    }
});

// Log when video is ready
player.ready(function() {
    console.log('Video.js player initialized');
    console.log('Keyboard shortcuts: Space (play/pause), F (fullscreen), M (mute), ← → (seek ±5s), ↑ ↓ (volume)');
    
    // ===== VOLUME BOOST FEATURE (Web Audio API) =====
    var audioContext = null;
    var gainNode = null;
    var sourceNode = null;
    var currentBoost = 1.0; // 100% (no boost)
    
    // Initialize Web Audio API
    function initAudioBoost() {
        if (audioContext) return; // Already initialized
        
        try {
            var videoElement = player.el().querySelector('video');
            audioContext = new (window.AudioContext || window.webkitAudioContext)();
            sourceNode = audioContext.createMediaElementSource(videoElement);
            gainNode = audioContext.createGain();
            
            sourceNode.connect(gainNode);
            gainNode.connect(audioContext.destination);
            
            gainNode.gain.value = currentBoost;
            console.log('Audio boost initialized');
        } catch (e) {
            console.error('Failed to initialize audio boost:', e);
        }
    }
    
    // Set volume boost level
    function setVolumeBoost(boostLevel) {
        if (!audioContext) {
            initAudioBoost();
        }
        
        currentBoost = boostLevel;
        if (gainNode) {
            gainNode.gain.value = boostLevel;
        }
        
        // Update UI
        updateBoostIndicator(boostLevel);
        
        // Save preference
        localStorage.setItem('volumeBoost', boostLevel);
        
        console.log('Volume boost set to:', (boostLevel * 100) + '%');
    }
    
    // Create boost indicator
    var boostIndicator = document.createElement('div');
    boostIndicator.id = 'boost-indicator';
    boostIndicator.style.cssText = 'position:absolute; top:10px; right:10px; background:rgba(16,185,129,0.9); color:white; padding:8px 16px; border-radius:6px; font-size:14px; font-weight:600; display:none; z-index:100; box-shadow:0 4px 12px rgba(16,185,129,0.4);';
    player.el().appendChild(boostIndicator);
    
    function updateBoostIndicator(boostLevel) {
        var percentage = Math.round(boostLevel * 100);
        boostIndicator.textContent = '🔊 Volume Boost: ' + percentage + '%';
        boostIndicator.style.display = 'block';
        
        // Hide after 2 seconds
        setTimeout(function() {
            boostIndicator.style.display = 'none';
        }, 2000);
    }
    
    // Create custom boost button directly in control bar
    setTimeout(function() {
        var controlBar = player.controlBar.el();
        
        // Create boost button wrapper
        var boostButtonWrapper = document.createElement('div');
        boostButtonWrapper.className = 'vjs-boost-button-wrapper';
        boostButtonWrapper.style.cssText = 'position:relative; display:inline-block; width:3.5em; height:100%;';
        
        // Create boost button
        var boostBtn = document.createElement('button');
        boostBtn.className = 'vjs-control vjs-button vjs-boost-control';
        boostBtn.style.cssText = 'width:100%; height:100%; cursor:pointer;';
        boostBtn.innerHTML = '<span style="font-size:1.4em;">🔊+</span>';
        boostBtn.title = 'Volume Boost';
        
        // Create boost menu
        var boostMenu = document.createElement('div');
        boostMenu.className = 'vjs-boost-menu';
        boostMenu.style.cssText = 'display:none; position:absolute; bottom:3em; left:0; background:rgba(20,20,20,0.95); border-radius:8px; padding:8px; min-width:140px; z-index:1000; box-shadow:0 8px 24px rgba(0,0,0,0.5);';
        
        var boostLevels = [
            { label: '100% (Normal)', value: 1.0 },
            { label: '125%', value: 1.25 },
            { label: '150%', value: 1.5 },
            { label: '175%', value: 1.75 },
            { label: '200%', value: 2.0 },
            { label: '225%', value: 2.25 },
            { label: '250%', value: 2.5 }
        ];
        
        boostLevels.forEach(function(level) {
            var item = document.createElement('div');
            item.className = 'vjs-boost-menu-item';
            item.textContent = level.label;
            item.style.cssText = 'padding:10px 16px; cursor:pointer; color:rgba(255,255,255,0.9); font-size:14px; transition:all 0.2s;';
            
            item.addEventListener('mouseenter', function() {
                this.style.background = 'rgba(16,185,129,0.3)';
                this.style.color = '#10b981';
            });
            
            item.addEventListener('mouseleave', function() {
                this.style.background = 'transparent';
                this.style.color = 'rgba(255,255,255,0.9)';
            });
            
            item.addEventListener('click', function() {
                setVolumeBoost(level.value);
                boostMenu.style.display = 'none';
            });
            
            boostMenu.appendChild(item);
        });
        
        // Toggle menu on button click
        boostBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            boostMenu.style.display = boostMenu.style.display === 'block' ? 'none' : 'block';
        });
        
        // Close menu when clicking outside
        document.addEventListener('click', function(e) {
            if (!boostButtonWrapper.contains(e.target)) {
                boostMenu.style.display = 'none';
            }
        });
        
        boostButtonWrapper.appendChild(boostBtn);
        boostButtonWrapper.appendChild(boostMenu);
        
        // Insert before fullscreen button
        var fullscreenButton = controlBar.querySelector('.vjs-fullscreen-control');
        if (fullscreenButton) {
            controlBar.insertBefore(boostButtonWrapper, fullscreenButton);
        } else {
            controlBar.appendChild(boostButtonWrapper);
        }
        
        console.log('Volume boost button added to control bar');
    }, 100);
    
    // ===== ENHANCED VOLUME SLIDER CONTROL =====
    setTimeout(function() {
        var volumePanel = document.querySelector('.vjs-volume-panel');
        var volumeControl = document.querySelector('.vjs-volume-control');
        var muteButton = document.querySelector('.vjs-mute-control');
        var volumeSliderOpen = false;
        
        if (volumePanel && volumeControl && muteButton) {
            // Add a dedicated toggle area next to mute button
            var volumeToggle = document.createElement('div');
            volumeToggle.style.cssText = 'position:absolute; bottom:0; right:-5px; width:20px; height:100%; cursor:pointer; z-index:102;';
            volumeToggle.innerHTML = '<span style="position:absolute; bottom:12px; right:2px; font-size:10px; opacity:0.7;">▲</span>';
            volumePanel.style.position = 'relative';
            volumePanel.appendChild(volumeToggle);
            
            // Click to toggle volume slider
            volumeToggle.addEventListener('click', function(e) {
                e.stopPropagation();
                volumeSliderOpen = !volumeSliderOpen;
                if (volumeSliderOpen) {
                    volumeControl.style.opacity = '1';
                    volumeControl.style.visibility = 'visible';
                    volumeControl.style.pointerEvents = 'auto';
                    volumeControl.classList.add('vjs-slider-active');
                } else {
                    volumeControl.style.opacity = '0';
                    volumeControl.style.visibility = 'hidden';
                    volumeControl.style.pointerEvents = 'none';
                    volumeControl.classList.remove('vjs-slider-active');
                }
            });
            
            // Keep slider open while interacting with it
            volumeControl.addEventListener('mousedown', function() {
                volumeSliderOpen = true;
                volumeControl.classList.add('vjs-slider-active');
            });
            
            // Close slider when clicking elsewhere on the video
            player.el().addEventListener('click', function(e) {
                if (!volumePanel.contains(e.target) && volumeSliderOpen) {
                    volumeSliderOpen = false;
                    volumeControl.style.opacity = '0';
                    volumeControl.style.visibility = 'hidden';
                    volumeControl.style.pointerEvents = 'none';
                    volumeControl.classList.remove('vjs-slider-active');
                }
            });
            
            console.log('Enhanced volume slider control initialized');
        }
    }, 200);
    
    // Load saved boost preference
    var savedBoost = localStorage.getItem('volumeBoost');
    if (savedBoost) {
        setTimeout(function() {
            setVolumeBoost(parseFloat(savedBoost));
        }, 500);
    }
    
    // Initialize on first play
    player.one('play', function() {
        initAudioBoost();
    });
});

// ============= VIDEO PROGRESS TRACKING =============
var videoPath = '$($video.filepath -replace "'", "\'" -replace "`"", "\" -replace "\\", "\\\\")';
var videoTitle = '$($video.title -replace "'", "\'" -replace "`"", "\")';
var videoId = '$videoId';
var lastTrackedPosition = 0;
var trackingInterval;

// Track progress every 10 seconds while playing
player.on('play', function() {
    if (trackingInterval) {
        clearInterval(trackingInterval);
    }
    
    trackingInterval = setInterval(function() {
        var currentTime = player.currentTime();
        var duration = player.duration();
        
        // Only track if position changed significantly (>5 seconds)
        if (Math.abs(currentTime - lastTrackedPosition) > 5) {
            trackVideoProgress(currentTime, duration);
            lastTrackedPosition = currentTime;
        }
    }, 10000); // Every 10 seconds
});

// Stop tracking when paused
player.on('pause', function() {
    if (trackingInterval) {
        clearInterval(trackingInterval);
        trackingInterval = null;
    }
    
    // Save current position
    var currentTime = player.currentTime();
    var duration = player.duration();
    trackVideoProgress(currentTime, duration);
});

// Track when video ends
player.on('ended', function() {
    if (trackingInterval) {
        clearInterval(trackingInterval);
        trackingInterval = null;
    }
    
    var duration = player.duration();
    trackVideoProgress(duration, duration); // Mark as completed
});

// Function to send tracking data to server
function trackVideoProgress(position, duration) {
    fetch('/api/track/video', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            id: videoId,
            path: videoPath,
            title: videoTitle,
            position: position,
            duration: duration
        })
    }).catch(err => console.error('Tracking error:', err));
}

// Stop transcode when page is being unloaded
window.addEventListener('beforeunload', function() {
    // Final progress save
    if (player && !player.paused()) {
        var currentTime = player.currentTime();
        var duration = player.duration();
        navigator.sendBeacon('/api/track/video', JSON.stringify({
            path: videoPath,
            title: videoTitle,
            position: currentTime,
            duration: duration
        }));
    }
    
    if (currentTranscodeId) {
        // Use sendBeacon for reliable delivery during page unload
        var stopUrl = '/api/stop-transcode/' + currentTranscodeId;
        navigator.sendBeacon(stopUrl);
        console.log('Stopping transcode:', currentTranscodeId);
    }
});

// Also stop transcode when navigating using browser history
window.addEventListener('pagehide', function() {
    if (currentTranscodeId) {
        navigator.sendBeacon('/api/stop-transcode/' + currentTranscodeId);
    }
});

} // End of initializePlayer function
</script>

<style>
/* Audio track selector button styling */
.audio-track-select-btn {
    padding: 16px 24px;
    background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(52,211,153,0.2));
    border: 2px solid rgba(96,165,250,0.4);
    border-radius: 12px;
    color: #fff;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
    text-align: center;
}

.audio-track-select-btn:hover {
    background: linear-gradient(135deg, rgba(96,165,250,0.4), rgba(52,211,153,0.4));
    border-color: rgba(96,165,250,0.6);
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(96,165,250,0.3);
}

.audio-track-select-btn:active {
    transform: translateY(0);
}

/* ============================================
   CUSTOM VIDEO.JS SKIN - BRIGHT GREEN FLAT THEME
   Matching modern flat design with larger controls
   ============================================ */

.video-js {
    font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
    font-size: 14px;
}

/* ===== BIG PLAY BUTTON ===== */
.vjs-big-play-button {
    background-color: rgba(251, 191, 36, 0.9) !important; /* Bright yellow/gold */
    border: 4px solid rgba(251, 191, 36, 1) !important;
    border-radius: 50% !important;
    width: 100px !important;
    height: 100px !important;
    line-height: 92px !important;
    font-size: 54px !important;
    top: 50% !important;
    left: 50% !important;
    margin-top: -50px !important;
    margin-left: -50px !important;
    transition: all 0.3s ease !important;
}

.vjs-big-play-button:hover {
    background-color: rgba(251, 191, 36, 1) !important;
    transform: scale(1.1) !important;
}

.vjs-big-play-button .vjs-icon-placeholder:before {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ===== CONTROL BAR ===== */
.video-js .vjs-control-bar {
    background: linear-gradient(to top, rgba(0,0,0,0.85), rgba(0,0,0,0.6)) !important;
    height: 50px !important;
    padding: 0 10px !important;
}

/* ===== LARGER BUTTONS ===== */
.video-js .vjs-control {
    width: 3.5em !important;
}

.video-js .vjs-button > .vjs-icon-placeholder:before {
    font-size: 2em !important;
    line-height: 1.55 !important;
}

/* ===== PLAY/PAUSE BUTTON ===== */
.video-js .vjs-play-control {
    width: 4em !important;
}

.video-js .vjs-play-control .vjs-icon-placeholder:before {
    font-size: 2.2em !important;
}

/* ===== PROGRESS BAR - BRIGHT GREEN ===== */
.video-js .vjs-progress-control {
    position: absolute !important;
    top: -8px !important;
    left: 0 !important;
    right: 0 !important;
    width: 100% !important;
    height: 8px !important;
}

.video-js .vjs-progress-holder {
    height: 8px !important;
    margin: 0 !important;
}

.video-js .vjs-play-progress {
    background-color: #10b981 !important; /* Bright emerald green */
}

.video-js .vjs-play-progress:before {
    display: none !important; /* Hide the circle scrubber for cleaner look */
}

.video-js .vjs-load-progress {
    background-color: rgba(16, 185, 129, 0.3) !important;
}

.video-js .vjs-slider {
    background-color: rgba(255, 255, 255, 0.2) !important;
}

/* Progress bar hover effect */
.video-js .vjs-progress-control:hover .vjs-progress-holder {
    height: 12px !important;
    transition: height 0.2s ease !important;
}

.video-js .vjs-progress-control:hover {
    top: -10px !important;
}

/* ===== VOLUME CONTROL - REDESIGNED FOR BETTER USABILITY ===== */
.video-js .vjs-volume-panel {
    width: 3.5em !important;
    position: relative !important;
}

/* Volume panel - ensure it's above everything */
.video-js .vjs-volume-panel.vjs-volume-panel-vertical {
    width: 3.5em !important;
    position: relative !important;
    z-index: 100 !important;
}

/* Volume slider container - popup above the button */
.video-js .vjs-volume-panel .vjs-volume-control {
    position: absolute !important;
    bottom: 4em !important;
    left: 50% !important;
    transform: translateX(-50%) !important;
    width: 50px !important;
    height: 120px !important;
    opacity: 0 !important;
    visibility: hidden !important;
    pointer-events: none !important;
    transition: opacity 0.2s ease, visibility 0.2s ease !important;
    background: rgba(20, 20, 20, 0.95) !important;
    border-radius: 10px !important;
    padding: 15px 10px !important;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5) !important;
    z-index: 9999 !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
}

/* Show volume slider on hover - with generous hover area */
.video-js .vjs-volume-panel:hover .vjs-volume-control,
.video-js .vjs-volume-panel:focus-within .vjs-volume-control,
.video-js .vjs-volume-panel .vjs-volume-control:hover,
.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active {
    opacity: 1 !important;
    visibility: visible !important;
    pointer-events: auto !important;
}

/* Create an invisible bridge between button and slider to prevent losing hover */
.video-js .vjs-volume-panel::after {
    content: '' !important;
    position: absolute !important;
    bottom: 3em !important;
    left: 0 !important;
    width: 100% !important;
    height: 1.5em !important;
    z-index: 99 !important;
}

/* The actual volume bar (vertical slider track) */
.video-js .vjs-volume-bar.vjs-slider-vertical {
    width: 8px !important;
    height: 90px !important;
    margin: 0 auto !important;
    background-color: rgba(255, 255, 255, 0.3) !important;
    border-radius: 4px !important;
    cursor: pointer !important;
    position: relative !important;
}

/* Volume level fill */
.video-js .vjs-volume-level {
    background: linear-gradient(to top, #10b981, #34d399) !important;
    width: 100% !important;
    border-radius: 4px !important;
    position: absolute !important;
    bottom: 0 !important;
}

/* Volume slider handle/knob */
.video-js .vjs-volume-level::before {
    content: '' !important;
    position: absolute !important;
    top: 0 !important;
    left: 50% !important;
    transform: translateX(-50%) !important;
    width: 16px !important;
    height: 16px !important;
    background: #fff !important;
    border-radius: 50% !important;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4) !important;
}

/* Volume button (mute toggle) */
.video-js .vjs-volume-panel .vjs-mute-control {
    width: 3.5em !important;
    z-index: 101 !important;
}

.video-js .vjs-volume-panel .vjs-mute-control .vjs-icon-placeholder:before {
    font-size: 2em !important;
}

/* ===== TIME DISPLAY - HIDE COMPLETELY ===== */
.video-js .vjs-time-control {
    display: none !important;
}

.video-js .vjs-current-time,
.video-js .vjs-duration,
.video-js .vjs-time-divider,
.video-js .vjs-remaining-time {
    display: none !important;
}

/* ===== SETTINGS BUTTON (GEAR ICON) ===== */
.video-js .vjs-playback-rate,
.video-js .vjs-settings-button {
    width: 3.5em !important;
}

.video-js .vjs-settings-button .vjs-icon-placeholder:before,
.video-js .vjs-playback-rate .vjs-icon-placeholder:before {
    content: "⚙" !important;
    font-size: 2em !important;
}

/* Settings menu */
.video-js .vjs-menu {
    background-color: rgba(20, 20, 20, 0.95) !important;
    border-radius: 8px !important;
}

.video-js .vjs-menu-content {
    background-color: transparent !important;
}

.video-js .vjs-menu li {
    padding: 0.8em 1em !important;
    font-size: 1.1em !important;
}

.video-js .vjs-menu li:hover,
.video-js .vjs-menu li.vjs-selected {
    background-color: rgba(16, 185, 129, 0.3) !important;
    color: #10b981 !important;
}

/* ===== CC/SUBTITLES BUTTON ===== */
.video-js .vjs-subs-caps-button {
    width: 3.5em !important;
}

.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before {
    content: "CC" !important;
    font-size: 1.4em !important;
    font-weight: 700 !important;
    font-family: Arial, sans-serif !important;
}

/* ===== FULLSCREEN BUTTON ===== */
.video-js .vjs-fullscreen-control {
    width: 3.5em !important;
}

.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before {
    font-size: 2em !important;
}

/* ===== PICTURE-IN-PICTURE BUTTON ===== */
.video-js .vjs-picture-in-picture-control {
    width: 3.5em !important;
}

.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
    font-size: 1.8em !important;
}

/* ===== HOVER EFFECTS ===== */
.video-js .vjs-control:hover {
    background-color: rgba(16, 185, 129, 0.2) !important;
    border-radius: 4px !important;
}

.video-js .vjs-button:hover .vjs-icon-placeholder:before {
    color: #10b981 !important;
    text-shadow: 0 0 10px rgba(16, 185, 129, 0.5) !important;
}

/* ===== MENU BUTTON HOVER ===== */
.video-js .vjs-menu-button:hover .vjs-menu {
    display: block !important;
}

/* ===== SPINNER (LOADING) ===== */
.vjs-loading-spinner {
    border-color: rgba(16, 185, 129, 0.8) transparent transparent transparent !important;
}

/* ===== RESPONSIVE ADJUSTMENTS ===== */
.video-js.vjs-fluid,
.video-js.vjs-16-9,
.video-js.vjs-4-3 {
    width: 100%;
    max-width: 100%;
    height: 100%;
}

/* ===== LIVE BADGE (if applicable) ===== */
.video-js .vjs-live-control {
    background-color: #ef4444 !important;
    border-radius: 4px !important;
    padding: 0.3em 0.8em !important;
    font-weight: 600 !important;
    margin-right: 0.5em !important;
}

/* ===== CONTROL BAR SHOW/HIDE ANIMATION ===== */
.video-js.vjs-user-inactive.vjs-playing .vjs-control-bar {
    opacity: 0 !important;
    pointer-events: none !important;
    transition: opacity 0.5s ease !important;
}

.video-js.vjs-user-active .vjs-control-bar,
.video-js.vjs-paused .vjs-control-bar {
    opacity: 1 !important;
    pointer-events: auto !important;
    transition: opacity 0.3s ease !important;
}

/* ===== AUDIO TRACK BUTTON (if present) ===== */
.vjs-audio-button .vjs-icon-placeholder:before {
    content: "🔊" !important;
    font-size: 1.6em !important;
}

.vjs-audio-button .vjs-menu {
    left: -50px !important;
}

.vjs-audio-button .vjs-menu .vjs-menu-content {
    background-color: rgba(20, 20, 20, 0.95) !important;
    border-radius: 8px !important;
    max-height: 250px !important;
    overflow-y: auto !important;
}

/* ===== VOLUME BOOST BUTTON ===== */
.vjs-boost-button {
    width: 3.5em !important;
}

.vjs-boost-button .vjs-icon-placeholder:before {
    content: "🔊+" !important;
    font-size: 1.4em !important;
    font-weight: 700 !important;
}

.vjs-boost-button:hover {
    background-color: rgba(16, 185, 129, 0.2) !important;
}

.vjs-boost-menu {
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5) !important;
}

.vjs-boost-menu-item:first-child {
    border-top-left-radius: 8px !important;
    border-top-right-radius: 8px !important;
}

.vjs-boost-menu-item:last-child {
    border-bottom-left-radius: 8px !important;
    border-bottom-right-radius: 8px !important;
}

/* ===== TOOLTIP STYLING ===== */
/* Hide button text labels - show icons only */
.video-js .vjs-control .vjs-control-text {
    position: absolute !important;
    clip: rect(0 0 0 0) !important;
    width: 1px !important;
    height: 1px !important;
    overflow: hidden !important;
    border: 0 !important;
    padding: 0 !important;
    margin: -1px !important;
}

/* ===== SEEK BAR TOOLTIP ===== */
.video-js .vjs-progress-control .vjs-mouse-display {
    background-color: #10b981 !important;
    border-radius: 4px !important;
    padding: 4px 8px !important;
}

/* ===== ERROR DISPLAY ===== */
.video-js .vjs-error-display {
    background-color: rgba(239, 68, 68, 0.9) !important;
}

.video-js .vjs-error-display:before {
    content: "⚠" !important;
    font-size: 4em !important;
    margin-bottom: 0.5em !important;
}
</style>
"@
                            # Create standalone player page (no sidebar to avoid 404 errors)
                            # Check if Video.js is available locally
                            $videojsFolder = Join-Path $CONFIG.ToolsFolder "videojs"
                            $videojsCss = Join-Path $videojsFolder "video-js.min.css"
                            $videojsJs = Join-Path $videojsFolder "video.min.js"
                            $hasVideoJS = (Test-Path $videojsCss) -and (Test-Path $videojsJs)
                            
                            if ($hasVideoJS) {
                                # Use Video.js (offline, enhanced player)
                                $standalonePlayerPage = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>$($video.title) - Player</title>
    <link href="/videojs/video-js.min.css" rel="stylesheet" />
    <style>
        body {
            margin: 0;
            font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
            background: radial-gradient(950px 600px at 10% 15%, rgba(52,211,153,.18), transparent 55%),
                        radial-gradient(800px 520px at 90% 85%, rgba(34,197,94,.12), transparent 55%),
                        linear-gradient(180deg, #0a1410, #0d1410);
            color: rgba(255,255,255,.92);
            min-height: 100vh;
        }
        .btn {
            border: 1px solid rgba(255,255,255,.12);
            background: rgba(255,255,255,.08);
            color: rgba(255,255,255,.92);
            padding: 14px 20px;
            border-radius: 16px;
            cursor: pointer;
            display: inline-block;
            text-decoration: none;
            transition: transform .08s ease, background .2s ease, border-color .2s ease;
        }
        .btn:hover {
            transform: translateY(-1px);
            background: rgba(255,255,255,.08);
            border-color: rgba(255,255,255,.18);
        }
    </style>
</head>
<body>
$playerContent
<script src='/videojs/video.min.js'></script>
<script>
var player = videojs('my-video', {
    controls: true,
    autoplay: false,
    preload: 'auto',
    responsive: true,
    playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
    controlBar: {
        pictureInPictureToggle: true,
        volumePanel: {
            inline: false
        }
    }
});

player.on('loadedmetadata', function() {
    var videoWidth = player.videoWidth();
    var videoHeight = player.videoHeight();
    var resolution = videoWidth + 'x' + videoHeight + ' (' + videoHeight + 'p)';
    document.getElementById('videoResolution').textContent = resolution;
    
    var duration = player.duration();
    var minutes = Math.floor(duration / 60);
    var seconds = Math.floor(duration % 60);
    var durationText = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
    document.getElementById('videoDuration').textContent = durationText;
    
    console.log('Video Resolution:', resolution);
    console.log('Duration:', durationText);
});

player.on('error', function() {
    var error = player.error();
    console.error('Playback error:', error);
    
    if (error) {
        var errorMessage = 'Video playback error. ';
        if (error.code === 4) {
            errorMessage += 'This format may not be supported in your browser. Try downloading the file instead.';
        }
        alert(errorMessage);
    }
});

player.ready(function() {
    console.log('Video.js player initialized');
    console.log('Keyboard shortcuts: Space (play/pause), F (fullscreen), M (mute), ← → (seek ±5s), ↑ ↓ (volume)');
});
</script>
</body>
</html>
"@
                            }
                            else {
                                # Fallback to basic HTML5 player (no Video.js available)
                                $standalonePlayerPage = @"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>$($video.title) - Player</title>
    <style>
        body {
            margin: 0;
            font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
            background: radial-gradient(950px 600px at 10% 15%, rgba(52,211,153,.18), transparent 55%),
                        radial-gradient(800px 520px at 90% 85%, rgba(34,197,94,.12), transparent 55%),
                        linear-gradient(180deg, #0a1410, #0d1410);
            color: rgba(255,255,255,.92);
            min-height: 100vh;
        }
        .btn {
            border: 1px solid rgba(255,255,255,.12);
            background: rgba(255,255,255,.08);
            color: rgba(255,255,255,.92);
            padding: 14px 20px;
            border-radius: 16px;
            cursor: pointer;
            display: inline-block;
            text-decoration: none;
            transition: transform .08s ease, background .2s ease, border-color .2s ease;
        }
        .btn:hover {
            transform: translateY(-1px);
            background: rgba(255,255,255,.08);
            border-color: rgba(255,255,255,.18);
        }
        video {
            width: 100%;
            background: #000;
            border-radius: 12px;
        }
    </style>
</head>
<body>
<div style='max-width:1400px; margin:40px auto; padding:20px;'>
  <h1 style='margin-bottom:10px;'>$($video.title)</h1>
  <p style='color:rgba(255,255,255,.62); margin-bottom:30px;'>$($video.format) • ${size}GB</p>
  
  <div style='position:relative; padding-bottom:56.25%; height:0; overflow:hidden; border-radius:12px; background:#000;'>
    <video id='basicVideo' controls preload='auto' style='position:absolute; top:0; left:0; width:100%; height:100%;'>
      <source src='/stream/$videoId' type='$videoMimeType'>
      Your browser doesn't support this video format.
    </video>
  </div>
  
  <div style='margin-top:20px; padding:16px; background:rgba(251,191,36,.1); border:1px solid rgba(251,191,36,.3); border-radius:12px;'>
    <p style='font-size:14px; color:rgba(255,255,255,.92); margin:0 0 8px 0;'>
      ⚠️ <strong>Basic HTML5 Player</strong>
    </p>
    <p style='font-size:13px; color:rgba(255,255,255,.62); margin:0;'>
      Video.js not installed. Run the script and choose to download Video.js for enhanced features (playback speed control, better compatibility).
    </p>
  </div>
  
  <div style='margin-top:20px; padding:16px; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.1); border-radius:12px;'>
    <div style='display:grid; grid-template-columns:1fr auto; gap:16px; align-items:center;'>
      <div style='font-size:13px; color:rgba(255,255,255,.92);'>
        <div style='margin-bottom:8px;'>
          <strong>Resolution:</strong> <span id='videoResolution'>Loading...</span> • 
          <strong>Duration:</strong> <span id='videoDuration'>--:--</span>
        </div>
        <div style='font-size:12px; color:rgba(255,255,255,.62);'>
          <strong>Format:</strong> $($video.format.ToUpper()) • 
          <strong>Size:</strong> ${size}GB • 
          <strong>Player:</strong> HTML5
        </div>
      </div>
    </div>
  </div>
  
  <div style='margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;'>
    <a href='/stream/$videoId' download='$($video.filename)' class='btn'>
      ⬇️ Download File
    </a>
    <a href='/videos' class='btn'>
      ← Back to Videos
    </a>
    <a href='/' class='btn'>
      🏠 Home
    </a>
  </div>
  
  <div style='margin-top:20px; padding:16px; background:rgba(255,255,255,.05); border-radius:12px;'>
    <p style='font-size:13px; color:rgba(255,255,255,.62); margin:0;'>
      💡 <strong>For best quality:</strong> Download the file and open with VLC Media Player
    </p>
  </div>
</div>

<script>
const video = document.getElementById('basicVideo');

// Display video resolution when metadata loads
video.addEventListener('loadedmetadata', function() {
    const width = this.videoWidth;
    const height = this.videoHeight;
    const resolution = width + 'x' + height + ' (' + height + 'p)';
    document.getElementById('videoResolution').textContent = resolution;
    
    const duration = this.duration;
    const minutes = Math.floor(duration / 60);
    const seconds = Math.floor(duration % 60);
    const durationText = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
    document.getElementById('videoDuration').textContent = durationText;
});

video.addEventListener('error', function(e) {
    console.error('Video error:', e);
    alert('Error loading video. Try downloading the file instead.');
});
</script>
</body>
</html>
"@
                            }
                            
                            # Send standalone page directly (bypass normal HTML processing)
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($standalonePlayerPage)
                            $response.ContentLength64 = $buffer.Length
                            $response.ContentType = "text/html; charset=utf-8"
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>❌ Video not found</h1><p><a href='/'>← Back to Library</a></p></div>" -Title "Error"
                        }
                    }
                    '^/play-playlist/(\d+)(/shuffle)?$' {
                        # Play a user's playlist
                        $playlistId = [int]$matches[1]
                        $shuffleMode = $matches[2] -eq '/shuffle'
                        
                        if (-not $Global:CurrentUser -or -not $Global:CurrentUser.Username) {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>Please log in</h1><p><a href='/login'>← Login</a></p></div>" -Title "Error"
                            continue
                        }
                        
                        $userDbPath = Join-Path $CONFIG.UsersDBPath "$($Global:CurrentUser.Username).db"
                        
                        # Get playlist info
                        $playlist = Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT * FROM playlists WHERE playlist_id = @id" -SqlParameters @{ id = $playlistId }
                        
                        if (-not $playlist) {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>Playlist not found</h1><p><a href='/playlists'>← Back to Playlists</a></p></div>" -Title "Error"
                            continue
                        }
                        
                        # Get tracks in playlist
                        $orderBy = if ($shuffleMode) { "RANDOM()" } else { "track_order" }
                        $tracks = Invoke-SqliteQuery -DataSource $userDbPath -Query "SELECT * FROM playlist_tracks WHERE playlist_id = @id ORDER BY $orderBy" -SqlParameters @{ id = $playlistId }
                        
                        if (-not $tracks -or @($tracks).Count -eq 0) {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>Playlist is empty</h1><p><a href='/playlist/$playlistId'>← Add some music</a></p></div>" -Title "Empty Playlist"
                            continue
                        }
                        
                        # Build playlist queue JSON with album art
                        $trackArray = @($tracks)
                        $playlistQueue = @()
                        foreach ($t in $trackArray) {
                            # Get album art from main music database
                            $trackArt = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT album_art_cached, album_art_url FROM music WHERE id = @id" -SqlParameters @{ id = $t.music_id }
                            $artUrl = ""
                            if ($trackArt) {
                                if ($trackArt.album_art_cached -and -not [string]::IsNullOrWhiteSpace($trackArt.album_art_cached)) {
                                    $artUrl = "/albumart/$($trackArt.album_art_cached)"
                                } elseif ($trackArt.album_art_url -and -not [string]::IsNullOrWhiteSpace($trackArt.album_art_url)) {
                                    $artUrl = "/poster/$($trackArt.album_art_url)"
                                }
                            }
                            
                            $playlistQueue += @{
                                musicId = $t.music_id
                                title = $t.title
                                artist = $t.artist
                                album = $t.album
                                albumArt = $artUrl
                            }
                        }
                        $playlistQueueJson = ($playlistQueue | ConvertTo-Json -Compress) -replace '"', '&quot;'
                        
                        # Get first track details from main music database
                        $firstTrack = $trackArray[0]
                        $music = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT * FROM music WHERE id = @id" -SqlParameters @{ id = $firstTrack.music_id }
                        
                        if (-not $music -or -not (Test-Path -LiteralPath $music.filepath)) {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>First track not found</h1><p>The music file may have been moved or deleted.</p><p><a href='/playlist/$playlistId'>← Back to Playlist</a></p></div>" -Title "Error"
                            continue
                        }
                        
                        # Get album artwork
                        $albumArtUrl = ""
                        if ($music.album_art_cached -and -not [string]::IsNullOrWhiteSpace($music.album_art_cached)) {
                            $albumArtUrl = "/albumart/$($music.album_art_cached)"
                        } elseif ($music.album_art_url -and -not [string]::IsNullOrWhiteSpace($music.album_art_url)) {
                            $albumArtUrl = "/poster/$($music.album_art_url)"
                        }
                        
                        $musicBackgroundOpacity = 0.3
                        $randomBg = Get-Random -Minimum 1 -Maximum 7
                        $backdropStyle = "background-image: linear-gradient(to bottom, rgba(10,20,16,$musicBackgroundOpacity), rgba(10,20,16,$musicBackgroundOpacity)), url('/menu/bg-$randomBg.jpg');"
                        
                        $artistDisplay = if ($music.artist) { $music.artist } else { "Unknown Artist" }
                        $albumDisplay = if ($music.album) { $music.album } else { "Unknown Album" }
                        $shuffleIndicator = if ($shuffleMode) { "<span class='shuffle-badge'>🔀 SHUFFLE</span>" } else { "" }
                        
                        $playerContent = @"
<style>
.playlist-player-container {
    min-height: 100vh;
    color: var(--text);
    position: relative;
}
.playlist-backdrop {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background-size: cover;
    background-position: center;
    z-index: 0;
    $backdropStyle
}
.playlist-content {
    position: relative;
    z-index: 2;
    padding: 40px;
    max-width: 900px;
    margin: 0 auto;
}
.playlist-header {
    display: flex;
    align-items: center;
    gap: 24px;
    margin-bottom: 32px;
    background: rgba(0,0,0,0.5);
    padding: 24px;
    border-radius: 16px;
}
.playlist-cover {
    width: 120px;
    height: 120px;
    background: linear-gradient(135deg, rgba(99,102,241,0.3), rgba(139,92,246,0.3));
    border-radius: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 48px;
    flex-shrink: 0;
}
.playlist-info h1 { margin: 0 0 8px 0; font-size: 28px; display: flex; align-items: center; gap: 12px; }
.playlist-info .meta { color: rgba(255,255,255,0.6); font-size: 14px; }
.shuffle-badge {
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    padding: 4px 12px;
    border-radius: 20px;
    font-size: 12px;
}
.now-playing-section {
    background: rgba(0,0,0,0.6);
    border-radius: 16px;
    padding: 32px;
    margin-bottom: 24px;
}
.now-playing-label {
    font-size: 12px;
    text-transform: uppercase;
    color: #10b981;
    margin-bottom: 16px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.now-playing-label::before {
    content: '';
    width: 8px;
    height: 8px;
    background: #10b981;
    border-radius: 50%;
    animation: pulse 1.5s infinite;
}
@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}
.current-track {
    display: flex;
    align-items: center;
    gap: 20px;
    margin-bottom: 24px;
}
.current-art {
    width: 100px;
    height: 100px;
    background: rgba(255,255,255,0.1);
    background-size: cover;
    background-position: center;
    border-radius: 12px;
    flex-shrink: 0;
}
.current-info h2 { margin: 0 0 8px 0; font-size: 24px; }
.current-info .artist { color: rgba(255,255,255,0.7); font-size: 16px; }
.current-info .album { color: rgba(255,255,255,0.5); font-size: 14px; margin-top: 4px; }
.audio-player-wrapper {
    background: rgba(0,0,0,0.4);
    border-radius: 12px;
    padding: 20px;
}
.audio-player-wrapper audio {
    width: 100%;
    height: 50px;
}
.queue-section {
    background: rgba(0,0,0,0.5);
    border-radius: 16px;
    padding: 24px;
}
.queue-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
}
.queue-header h3 { margin: 0; font-size: 16px; color: rgba(255,255,255,0.8); }
.queue-position { color: rgba(255,255,255,0.5); font-size: 14px; }
.queue-list { max-height: 300px; overflow-y: auto; }
.queue-item {
    display: flex;
    align-items: center;
    padding: 12px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.2s;
    gap: 12px;
}
.queue-item:hover { background: rgba(255,255,255,0.05); }
.queue-item.current { background: rgba(16,185,129,0.15); }
.queue-item .num { width: 24px; text-align: center; color: rgba(255,255,255,0.4); font-size: 13px; }
.queue-item.current .num { color: #10b981; }
.queue-item .title { flex: 1; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.queue-item .artist { color: rgba(255,255,255,0.5); font-size: 13px; }
.back-link {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: rgba(255,255,255,0.7);
    text-decoration: none;
    margin-bottom: 24px;
    font-size: 14px;
}
.back-link:hover { color: #fff; }
</style>

<div class="playlist-player-container">
    <div class="playlist-backdrop"></div>
    <div class="playlist-content">
        <a href="/playlist/$playlistId" class="back-link">← Back to $($playlist.name)</a>
        
        <div class="playlist-header">
            <div class="playlist-cover">📋</div>
            <div class="playlist-info">
                <h1>$($playlist.name) $shuffleIndicator</h1>
                <div class="meta">$(@($tracks).Count) tracks</div>
            </div>
        </div>
        
        <div class="now-playing-section">
            <div class="now-playing-label">Now Playing</div>
            <div class="current-track">
                <div class="current-art" id="currentArt" style="$(if ($albumArtUrl) { "background-image: url('$albumArtUrl');" })"></div>
                <div class="current-info">
                    <h2 id="currentTitle">$($music.title)</h2>
                    <div class="artist" id="currentArtist">$artistDisplay</div>
                    <div class="album" id="currentAlbum">$albumDisplay</div>
                </div>
            </div>
            <div class="audio-player-wrapper">
                <audio id="audioPlayer" controls autoplay>
                    <source src="/stream-audio/$($music.id)" type="audio/mpeg">
                </audio>
            </div>
        </div>
        
        <div class="queue-section">
            <div class="queue-header">
                <h3>🎵 Queue</h3>
                <span class="queue-position" id="queuePosition">Track 1 of $(@($tracks).Count)</span>
            </div>
            <div class="queue-list" id="queueList">
"@
                        # Add queue items
                        $trackNum = 0
                        foreach ($t in $trackArray) {
                            $trackNum++
                            $currentClass = if ($trackNum -eq 1) { "current" } else { "" }
                            $playerContent += @"
                <div class="queue-item $currentClass" data-index="$($trackNum - 1)" onclick="playTrackAtIndex($($trackNum - 1))">
                    <span class="num">$trackNum</span>
                    <span class="title">$($t.title)</span>
                    <span class="artist">$($t.artist)</span>
                </div>
"@
                        }
                        
                        $playerContent += @"
            </div>
        </div>
    </div>
</div>

<script>
var playlistQueue = $($playlistQueue | ConvertTo-Json -Compress);
var currentIndex = 0;
var audioPlayer = document.getElementById('audioPlayer');

function playTrackAtIndex(index) {
    if (index < 0 || index >= playlistQueue.length) return;
    
    currentIndex = index;
    var track = playlistQueue[index];
    
    // Update audio source
    audioPlayer.src = '/stream-audio/' + track.musicId;
    audioPlayer.play();
    
    // Update display
    document.getElementById('currentTitle').textContent = track.title || 'Unknown';
    document.getElementById('currentArtist').textContent = track.artist || 'Unknown Artist';
    document.getElementById('currentAlbum').textContent = track.album || '';
    document.getElementById('queuePosition').textContent = 'Track ' + (index + 1) + ' of ' + playlistQueue.length;
    
    // Update album art
    var artEl = document.getElementById('currentArt');
    if (track.albumArt && track.albumArt.length > 0) {
        artEl.style.backgroundImage = "url('" + track.albumArt + "')";
    } else {
        artEl.style.backgroundImage = "none";
    }
    
    // Update queue highlighting
    document.querySelectorAll('.queue-item').forEach(function(item, i) {
        item.classList.toggle('current', i === index);
    });
    
    // Scroll current track into view
    var currentItem = document.querySelector('.queue-item.current');
    if (currentItem) {
        currentItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
}

// Auto-play next track when current ends
audioPlayer.addEventListener('ended', function() {
    if (currentIndex < playlistQueue.length - 1) {
        playTrackAtIndex(currentIndex + 1);
    }
});

// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
    if (e.target.tagName === 'INPUT') return;
    
    switch(e.key) {
        case ' ':
            e.preventDefault();
            if (audioPlayer.paused) audioPlayer.play();
            else audioPlayer.pause();
            break;
        case 'ArrowRight':
            if (currentIndex < playlistQueue.length - 1) playTrackAtIndex(currentIndex + 1);
            break;
        case 'ArrowLeft':
            if (currentIndex > 0) playTrackAtIndex(currentIndex - 1);
            break;
    }
});
</script>
"@
                        
                        Get-HTMLPage -Content $playerContent -Title "Playing: $($playlist.name)"
                        continue
                    }
                    '^/play-audio/(.+)$' {
                        # Unified audio player with optional shuffle mode
                        $musicId = $matches[1]
                        $shuffleMode = $false
                        
                        # Check if shuffle mode
                        if ($musicId -eq "shuffle") {
                            $shuffleMode = $true
                            # Get a random song
                            $maxAttempts = 10
                            $attempt = 0
                            $foundValidSong = $false
                            $music = $null
                            
                            while (-not $foundValidSong -and $attempt -lt $maxAttempts) {
                                $attempt++
                                $music = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB `
                                    -Query "SELECT * FROM music ORDER BY RANDOM() LIMIT 1"
                                
                                if ($music -and (Test-Path -LiteralPath $music.filepath)) {
                                    $foundValidSong = $true
                                    Write-Host "  ✓ Found valid song for shuffle: $($music.title)" -ForegroundColor Green
                                }
                                elseif ($music) {
                                    Write-Host "  ⚠ Skipping missing file (attempt $attempt): $($music.title)" -ForegroundColor Yellow
                                }
                            }
                            
                            if (-not $foundValidSong) {
                                Get-HTMLPage -Content "<h1>No music found</h1><p><a href='/music'>← Back to Music</a></p>" -Title "Error"
                                continue
                            }
                            
                            $musicId = $music.id
                        }
                        else {
                            $music = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB `
                                -Query "SELECT * FROM music WHERE id = @id" `
                                -SqlParameters @{ id = $musicId }
                        }
                        
                        if ($music) {
                            # Get album artwork URLs
                            $albumArtUrl = ""
                            if ($music.album_art_cached -and -not [string]::IsNullOrWhiteSpace($music.album_art_cached)) {
                                $albumArtUrl = "/albumart/$($music.album_art_cached)"
                            }
                            elseif ($music.album_art_url -and -not [string]::IsNullOrWhiteSpace($music.album_art_url)) {
                                $albumArtUrl = "/poster/$($music.album_art_url)"
                            }
                            
                            # Backdrop style with music background image (adjustable transparency)
                            # Set transparency level (0.0 = fully transparent, 1.0 = fully opaque)
                            $musicBackgroundOpacity = 0.3
                            $randomBg = Get-Random -Minimum 1 -Maximum 7
                            $backdropStyle = "background-image: linear-gradient(to bottom, rgba(10,20,16,$musicBackgroundOpacity), rgba(10,20,16,$musicBackgroundOpacity)), url('/menu/bg-$randomBg.jpg');"
                            
                            # Format artist
                            $artistDisplay = if ($music.artist -and -not [string]::IsNullOrWhiteSpace($music.artist) -and $music.artist -notmatch '^[A-Z]:$') {
                                $music.artist
                            } else {
                                "Unknown Artist"
                            }
                            
                            # Format album
                            $albumDisplay = if ($music.album -and -not [string]::IsNullOrWhiteSpace($music.album)) {
                                $music.album
                            } else {
                                "Unknown Album"
                            }
                            
                            # Format duration
                            $durationDisplay = if ($music.duration) {
                                $mins = [math]::Floor($music.duration / 60)
                                $secs = $music.duration % 60
                                "${mins}:$("{0:D2}" -f $secs)"
                            } else {
                                "Unknown"
                            }
                            
                            # Format file size
                            $sizeDisplay = if ($music.size_bytes) {
                                $sizeMB = [math]::Round($music.size_bytes / 1MB, 2)
                                "${sizeMB} MB"
                            } else {
                                "Unknown"
                            }
                            
                            # Format bitrate
                            $bitrateDisplay = if ($music.bitrate) {
                                "$($music.bitrate) kbps"
                            } else {
                                "Unknown"
                            }
                            
                            # Format codec
                            $codecDisplay = if ($music.format) {
                                $music.format.ToUpper()
                            } else {
                                "Unknown"
                            }
                            
                            # Prepare JavaScript-safe strings for tracking (escape before injecting into JavaScript)
                            $jsMusicPath = $music.filepath -replace '\\', '\\' -replace '"', '\"' -replace "'", "\'"
                            $jsMusicArtist = if ($music.artist) { $music.artist -replace '\\', '\\' -replace '"', '\"' -replace "'", "\'" } else { '' }
                            $jsMusicTitle = if ($music.title) { $music.title -replace '\\', '\\' -replace '"', '\"' -replace "'", "\'" } else { '' }
                            $jsMusicAlbum = if ($music.album) { $music.album -replace '\\', '\\' -replace '"', '\"' -replace "'", "\'" } else { '' }

                            $playerContent = @"
<style>
.music-player-container {
    position: relative;
    min-height: 100vh;
    color: var(--text);
}

.music-backdrop {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    z-index: 0;
    $backdropStyle
}

.music-content-wrapper {
    position: relative;
    z-index: 2;
    padding: 40px;
    max-width: 1200px;
    margin: 0 auto;
}

.music-nav-tabs {
    display: flex;
    background: rgba(0,0,0,0.6);
    border-radius: 12px 12px 0 0;
    overflow: hidden;
    /* backdrop-filter removed for performance */
    border: 1px solid rgba(255,255,255,0.1);
    border-bottom: none;
}

.music-nav-tab {
    padding: 16px 32px;
    background: transparent;
    color: rgba(255,255,255,0.7);
    border: none;
    cursor: pointer;
    font-size: 15px;
    font-weight: 600;
    transition: all 0.3s ease;
    border-bottom: 3px solid transparent;
    flex: 1;
    text-align: center;
}

.music-nav-tab:hover {
    background: rgba(255,255,255,0.05);
    color: rgba(255,255,255,0.9);
}

.music-nav-tab.active {
    background: rgba(255,255,255,0.08);
    color: #fff;
    border-bottom-color: #60a5fa;
}

.music-main-panel {
    background: rgba(0,0,0,0.7);
    border-radius: 0 0 16px 16px;
    padding: 40px;
    /* backdrop-filter removed for performance */
    border: 1px solid rgba(255,255,255,0.1);
    border-top: none;
}

.music-layout {
    display: grid;
    grid-template-columns: 300px 1fr;
    gap: 40px;
    align-items: start;
}

.album-art-wrapper {
    position: relative;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}

.album-art {
    width: 100%;
    display: block;
    background: linear-gradient(135deg, rgba(96,165,250,0.2), rgba(52,211,153,0.2));
    aspect-ratio: 1;
    object-fit: cover;
}

.music-status-indicator {
    display: inline-block;
    background: rgba(34,197,94,0.15);
    border: 1px solid rgba(34,197,94,0.3);
    color: #4ade80;
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    margin-top: 12px;
}

.music-status-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 12px;
    flex-wrap: wrap;
}

.music-status-row .music-status-indicator {
    margin-top: 0;
}

.shuffle-mode-indicator {
    display: inline-block;
    background: rgba(96,165,250,0.15);
    border: 1px solid rgba(96,165,250,0.3);
    color: #60a5fa;
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
}

.song-title {
    font-size: 36px;
    font-weight: 700;
    margin: 0 0 12px 0;
    line-height: 1.2;
    color: #fff;
}

.artist-name {
    font-size: 20px;
    color: #60a5fa;
    margin-bottom: 8px;
}

.album-name {
    font-size: 16px;
    color: rgba(255,255,255,0.6);
    margin-bottom: 24px;
}

.audio-player-wrapper {
    margin: 24px 0;
}

.visualizer-container {
    display: flex;
    justify-content: center;
    align-items: flex-end;
    gap: 4px;
    height: 200px;
    margin: 20px 0;
    background: rgba(0,0,0,0.3);
    border-radius: 8px;
    padding: 10px;
    position: relative;
    cursor: pointer;
    overflow: hidden;
}

.visualizer-container:hover {
    background: rgba(0,0,0,0.4);
}

.visualizer-style-indicator {
    position: absolute;
    top: 10px;
    right: 10px;
    background: rgba(96,165,250,0.2);
    color: #60a5fa;
    padding: 4px 12px;
    border-radius: 12px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    pointer-events: none;
    z-index: 10;
}

.visualizer-bar {
    width: 6px;
    background: linear-gradient(to top, #06b6d4, #3b82f6, #8b5cf6);
    border-radius: 3px 3px 0 0;
    transition: height 0.1s ease;
}

.visualizer-container.style-rainbow-dots .visualizer-bar {
    width: 8px;
    height: 8px !important;
    border-radius: 50%;
    position: absolute;
    transition: all 0.15s ease;
}

.visualizer-container.style-waveform {
    align-items: center;
}

.visualizer-container.style-waveform .visualizer-bar {
    width: 3px;
    background: linear-gradient(to bottom, #3b82f6, #06b6d4, #3b82f6);
    border-radius: 2px;
    transition: height 0.08s ease;
}

.visualizer-container.style-circular {
    align-items: center;
    justify-content: center;
}

.visualizer-container.style-circular .visualizer-bar {
    position: absolute;
    width: 4px;
    background: linear-gradient(to top, #06b6d4, #8b5cf6);
    border-radius: 2px;
    transform-origin: center bottom;
    transition: height 0.1s ease;
}

.visualizer-container.style-blocks .visualizer-bar {
    width: 12px;
    background: #3b82f6;
    border-radius: 0;
    transition: height 0.12s ease, background-color 0.2s ease;
}

.visualizer-container.style-mirrored {
    align-items: center;
}

.visualizer-container.style-mirrored .visualizer-bar {
    width: 5px;
    background: linear-gradient(to top, #8b5cf6, #06b6d4, #8b5cf6);
    border-radius: 3px;
    transition: height 0.1s ease;
}

.track-info-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
    margin: 24px 0;
}

.info-card {
    background: rgba(96,165,250,0.1);
    border: 1px solid rgba(96,165,250,0.2);
    padding: 16px;
    border-radius: 8px;
}

.info-label {
    color: #60a5fa;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
    margin-bottom: 6px;
}

.info-value {
    color: rgba(255,255,255,0.9);
    font-size: 16px;
    font-weight: 600;
}

.action-buttons {
    display: flex;
    gap: 12px;
    margin-top: 24px;
}

.action-btn {
    padding: 12px 24px;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition: all 0.2s ease;
    border: none;
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.action-btn.secondary {
    background: rgba(255,255,255,0.1);
    color: #fff;
    border: 1px solid rgba(255,255,255,0.2);
}

.action-btn.secondary:hover {
    background: rgba(255,255,255,0.15);
    transform: translateY(-2px);
}

@media (max-width: 1024px) {
    .music-layout {
        grid-template-columns: 1fr;
    }
    .album-art-wrapper {
        max-width: 300px;
        margin: 0 auto;
    }
}

@media (max-width: 768px) {
    .music-content-wrapper {
        padding: 20px;
    }
    .music-main-panel {
        padding: 24px;
    }
    .song-title {
        font-size: 24px;
    }
    .music-nav-tab {
        padding: 12px 16px;
        font-size: 13px;
    }
}
</style>

<div class="music-player-container">
    <div class="music-backdrop"></div>
    
    <div class="music-content-wrapper">
        <div class="music-nav-tabs">
            <button class="music-nav-tab active" onclick="showMusicTab(event, 'overview')">Overview</button>
            <button class="music-nav-tab" onclick="showMusicTab(event, 'album')">Album Information</button>
            <button class="music-nav-tab" onclick="showMusicTab(event, 'artist')">Artist Information</button>
        </div>
        
        <div class="music-main-panel">
            <div id="overview-tab" class="tab-content">
                <div class="music-layout">
                    <div>
                        <div class="album-art-wrapper">
                            $(if ($albumArtUrl) {
                                "<img src='$albumArtUrl' alt='Album Art' class='album-art'>"
                            } else {
                                "<div class='album-art' style='display: flex; align-items: center; justify-content: center; font-size: 64px;'>🎵</div>"
                            })
                        </div>
                        <div class="music-status-row">
                            <span class="music-status-indicator">● MUSIC</span>
                            $(if ($shuffleMode) {
                                "<span class='shuffle-mode-indicator'>🔀 SHUFFLE MODE</span>"
                            })
                        </div>
                    </div>
                    
                    <div>
                        <h1 class="song-title">$($music.title)</h1>
                        <div class="artist-name">$artistDisplay</div>
                        <div class="album-name">$albumDisplay</div>
                        
                        <div class="visualizer-container" id="visualizer">
                            <!-- Bars created by JavaScript -->
                        </div>
                        
                        <div class="audio-player-wrapper">
                            <audio id="audioPlayer" controls preload="auto" style="width: 100%; border-radius: 8px;">
                                <source src="/stream-audio/$musicId" type="audio/mpeg">
                            </audio>
                        </div>
                        
                        <div class="track-info-grid">
                            <div class="info-card">
                                <div class="info-label">Duration</div>
                                <div class="info-value">$durationDisplay</div>
                            </div>
                            <div class="info-card">
                                <div class="info-label">File Size</div>
                                <div class="info-value">$sizeDisplay</div>
                            </div>
                            <div class="info-card">
                                <div class="info-label">Bitrate</div>
                                <div class="info-value">$bitrateDisplay</div>
                            </div>
                            <div class="info-card">
                                <div class="info-label">Format</div>
                                <div class="info-value">$codecDisplay</div>
                            </div>
                        </div>
                        
                        <div class="action-buttons">
                            <a href="/music" class="action-btn secondary">
                                <span>←</span>
                                <span>Back to Music</span>
                            </a>
                            $(if ($shuffleMode) {
                                "<a href='/play-audio/shuffle' class='action-btn secondary'>
                                    <span>⏭</span>
                                    <span>Next Random</span>
                                </a>"
                            } else {
                                "<a href='/play-audio/shuffle' class='action-btn secondary'>
                                    <span>🔀</span>
                                    <span>Shuffle Play</span>
                                </a>"
                            })
                        </div>
                    </div>
                </div>
            </div>
            
            <div id="album-tab" class="tab-content" style="display: none;">
                <div style="max-width: 900px;">
                    <h2 style="font-size: 28px; margin-bottom: 20px; color: #60a5fa;">Album Information</h2>
                    <div style="font-size: 16px; line-height: 1.8; color: rgba(255,255,255,0.85); margin-bottom: 24px;">
                        <strong>Album:</strong> $albumDisplay<br>
                        <strong>Artist:</strong> $artistDisplay<br>
                        <strong>Track:</strong> $($music.title)
                    </div>
                    $(if ($albumArtUrl) {
                        "<img src='$albumArtUrl' alt='Album Art' style='max-width: 400px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);'>"
                    })
                </div>
            </div>
            
            <div id="artist-tab" class="tab-content" style="display: none;">
                <div style="max-width: 900px;">
                    <h2 style="font-size: 28px; margin-bottom: 20px; color: #60a5fa;">Artist Information</h2>
                    <div style="font-size: 16px; line-height: 1.8; color: rgba(255,255,255,0.85);">
                        <strong>Artist:</strong> $artistDisplay<br>
                        <strong>Current Track:</strong> $($music.title)<br>
                        <strong>From Album:</strong> $albumDisplay
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<script>
function showMusicTab(event, tabName) {
    try {
        const tabs = document.querySelectorAll('.tab-content');
        tabs.forEach(tab => tab.style.display = 'none');
        
        const navTabs = document.querySelectorAll('.music-nav-tab');
        navTabs.forEach(tab => tab.classList.remove('active'));
        
        document.getElementById(tabName + '-tab').style.display = 'block';
        event.target.classList.add('active');
    } catch (error) {
        console.error('Error switching tabs:', error);
        // Fallback: force show the requested tab
        try {
            document.getElementById(tabName + '-tab').style.display = 'block';
        } catch (e) {
            console.error('Critical tab switching error:', e);
        }
    }
}

// Visualizer setup with multiple styles
const audio = document.getElementById('audioPlayer');
const visualizer = document.getElementById('visualizer');

const VISUALIZER_STYLES = [
    { name: 'Classic Bars', className: 'style-classic', barCount: 32 },
    { name: 'Rainbow Dots', className: 'style-rainbow-dots', barCount: 40 },
    { name: 'Waveform', className: 'style-waveform', barCount: 60 },
    { name: 'Circular', className: 'style-circular', barCount: 24 },
    { name: 'Blocks', className: 'style-blocks', barCount: 16 },
    { name: 'Mirrored', className: 'style-mirrored', barCount: 28 }
];

let currentStyleIndex = 0;
let bars = [];
let audioContext = null;
let analyser = null;
let dataArray = null;
let source = null;

// Create style indicator
const styleIndicator = document.createElement('div');
styleIndicator.className = 'visualizer-style-indicator';
visualizer.appendChild(styleIndicator);

function createBars(count) {
    bars.forEach(bar => bar.remove());
    bars = [];
    for (let i = 0; i < count; i++) {
        const bar = document.createElement('div');
        bar.className = 'visualizer-bar';
        bar.style.height = '4px';
        visualizer.appendChild(bar);
        bars.push(bar);
    }
}

function switchVisualizerStyle() {
    currentStyleIndex = (currentStyleIndex + 1) % VISUALIZER_STYLES.length;
    const style = VISUALIZER_STYLES[currentStyleIndex];
    VISUALIZER_STYLES.forEach(s => visualizer.classList.remove(s.className));
    visualizer.classList.add(style.className);
    styleIndicator.textContent = style.name;
    createBars(style.barCount);
}

visualizer.addEventListener('click', function(e) {
    if (e.target === visualizer || e.target.classList.contains('visualizer-bar')) {
        switchVisualizerStyle();
    }
});

switchVisualizerStyle();

function setupAudioAnalyser() {
    try {
        if (!audioContext) {
            // Check if we can create a new AudioContext
            const AudioContextClass = window.AudioContext || window.webkitAudioContext;
            if (!AudioContextClass) {
                console.warn('AudioContext not supported in this browser');
                return;
            }
            
            audioContext = new AudioContextClass();
            analyser = audioContext.createAnalyser();
            analyser.fftSize = 128;
            source = audioContext.createMediaElementSource(audio);
            source.connect(analyser);
            analyser.connect(audioContext.destination);
            const bufferLength = analyser.frequencyBinCount;
            dataArray = new Uint8Array(bufferLength);
            console.log('Audio analyser initialized successfully');
        }
        if (audioContext && audioContext.state === 'suspended') {
            audioContext.resume().then(function() {
                console.log('AudioContext resumed');
            }).catch(function(err) {
                console.warn('Failed to resume AudioContext:', err);
            });
        }
    } catch (error) {
        console.error('Audio analyser setup failed:', error);
        // If it's an AudioContext limit error, try to recover
        if (error.name === 'NotSupportedError' || error.message.includes('context')) {
            console.error('CRITICAL: AudioContext limit may have been reached');
            console.error('This typically means old contexts were not properly closed');
            // Don't set up visualizer, but don't break the page either
            audioContext = null;
            analyser = null;
            dataArray = null;
        }
    }
}

// Cleanup function to close AudioContext before page unload
function cleanupAudioContext() {
    if (audioContext && audioContext.state !== 'closed') {
        try {
            audioContext.close();
            console.log('AudioContext closed successfully');
        } catch (error) {
            console.log('Error closing AudioContext:', error);
        }
    }
}

function animateVisualizer() {
    if (!analyser || !dataArray) {
        requestAnimationFrame(animateVisualizer);
        return;
    }
    analyser.getByteFrequencyData(dataArray);
    const style = VISUALIZER_STYLES[currentStyleIndex];
    switch(style.className) {
        case 'style-classic': animateClassicBars(); break;
        case 'style-rainbow-dots': animateRainbowDots(); break;
        case 'style-waveform': animateWaveform(); break;
        case 'style-circular': animateCircular(); break;
        case 'style-blocks': animateBlocks(); break;
        case 'style-mirrored': animateMirrored(); break;
    }
    requestAnimationFrame(animateVisualizer);
}

function animateClassicBars() {
    for (let i = 0; i < bars.length; i++) {
        const value = dataArray[i] || 0;
        const height = (value / 255) * 100;
        const minHeight = audio.paused ? 2 : 4;
        bars[i].style.height = Math.max(minHeight, height) + '%';
    }
}

function animateRainbowDots() {
    const centerX = visualizer.offsetWidth / 2;
    const centerY = visualizer.offsetHeight / 2;
    const maxRadius = Math.min(centerX, centerY) - 20;
    for (let i = 0; i < bars.length; i++) {
        const value = dataArray[Math.floor(i * dataArray.length / bars.length)] || 0;
        const angle = (i / bars.length) * Math.PI * 2;
        const radius = 20 + (value / 255) * maxRadius;
        const x = centerX + Math.cos(angle) * radius - 4;
        const y = centerY + Math.sin(angle) * radius - 4;
        bars[i].style.left = x + 'px';
        bars[i].style.top = y + 'px';
        const hue = (i / bars.length) * 360;
        bars[i].style.background = 'hsl(' + hue + ', 70%, 60%)';
        bars[i].style.boxShadow = '0 0 ' + (value / 25) + 'px hsl(' + hue + ', 70%, 60%)';
    }
}

function animateWaveform() {
    for (let i = 0; i < bars.length; i++) {
        const value = dataArray[Math.floor(i * dataArray.length / bars.length)] || 0;
        const height = (value / 255) * 80;
        const minHeight = audio.paused ? 2 : 3;
        bars[i].style.height = Math.max(minHeight, height) + '%';
    }
}

function animateCircular() {
    const centerX = visualizer.offsetWidth / 2;
    const centerY = visualizer.offsetHeight / 2;
    const baseRadius = 40;
    for (let i = 0; i < bars.length; i++) {
        const value = dataArray[Math.floor(i * dataArray.length / bars.length)] || 0;
        const angle = (i / bars.length) * Math.PI * 2 - Math.PI / 2;
        const height = 10 + (value / 255) * 80;
        const x = centerX + Math.cos(angle) * baseRadius;
        const y = centerY + Math.sin(angle) * baseRadius;
        bars[i].style.left = x + 'px';
        bars[i].style.top = y + 'px';
        bars[i].style.height = height + 'px';
        bars[i].style.transform = 'rotate(' + (angle + Math.PI / 2) + 'rad)';
    }
}

function animateBlocks() {
    for (let i = 0; i < bars.length; i++) {
        const value = dataArray[Math.floor(i * dataArray.length / bars.length)] || 0;
        const height = (value / 255) * 100;
        const minHeight = audio.paused ? 2 : 4;
        bars[i].style.height = Math.max(minHeight, height) + '%';
        const intensity = Math.floor((value / 255) * 100);
        if (intensity > 70) {
            bars[i].style.background = '#8b5cf6';
        } else if (intensity > 40) {
            bars[i].style.background = '#3b82f6';
        } else {
            bars[i].style.background = '#06b6d4';
        }
    }
}

function animateMirrored() {
    const half = Math.floor(bars.length / 2);
    for (let i = 0; i < half; i++) {
        const value = dataArray[Math.floor(i * dataArray.length / half)] || 0;
        const height = (value / 255) * 90;
        const minHeight = audio.paused ? 2 : 4;
        const finalHeight = Math.max(minHeight, height) + '%';
        bars[i].style.height = finalHeight;
        bars[bars.length - 1 - i].style.height = finalHeight;
    }
}

animateVisualizer();

audio.addEventListener('play', function() {
    setupAudioAnalyser();
});

function tryPlay() {
    const playPromise = audio.play();
    if (playPromise !== undefined) {
        playPromise.then(() => {
            setupAudioAnalyser();
        }).catch(error => {
            console.log('Autoplay prevented:', error);
        });
    }
}

document.addEventListener('DOMContentLoaded', function() {
    setTimeout(tryPlay, 500);
});

// ============= MUSIC TRACKING =============
var musicPath = '$jsMusicPath';
var musicArtist = '$jsMusicArtist';
var musicTitle = '$jsMusicTitle';
var musicAlbum = '$jsMusicAlbum';
var playStartTime = null;
var totalPlayTime = 0;

audio.addEventListener('play', function() {
    playStartTime = Date.now();
});

// Add error handling for audio playback failures
audio.addEventListener('error', function(e) {
    console.error('Audio playback error:', e);
    console.error('Error code:', audio.error ? audio.error.code : 'unknown');
    console.error('Error message:', audio.error ? audio.error.message : 'unknown');
    
    // In shuffle mode, skip to next song on error
    $(if ($shuffleMode) {
        "console.log('Skipping to next song due to playback error...');
        setTimeout(function() {
            cleanupAudioContext();
            window.location.href = '/play-audio/shuffle';
        }, 500);"
    } else {
        "alert('Audio playback error. Please try again or select a different track.');"
    })
});

// Add stalled event handler (for network issues)
audio.addEventListener('stalled', function() {
    console.warn('Audio playback stalled');
});

// Add waiting event handler (for buffering)
audio.addEventListener('waiting', function() {
    console.log('Audio buffering...');
});

audio.addEventListener('pause', function() {
    if (playStartTime) {
        var playDuration = (Date.now() - playStartTime) / 1000; // seconds
        totalPlayTime += playDuration;
        playStartTime = null;
        
        // Track after 30 seconds of play time
        if (totalPlayTime >= 30) {
            trackMusic();
        }
    }
});

audio.addEventListener('ended', function() {
    if (playStartTime) {
        var playDuration = (Date.now() - playStartTime) / 1000;
        totalPlayTime += playDuration;
        playStartTime = null;
    }
    
    // Track the music play
    if (totalPlayTime > 0) {
        // Use sendBeacon for reliable tracking before navigation
        navigator.sendBeacon('/api/track/music', JSON.stringify({
            path: musicPath,
            artist: musicArtist,
            title: musicTitle,
            album: musicAlbum,
            playTime: totalPlayTime
        }));
        totalPlayTime = 0;
    }
    
    // Shuffle mode: navigate to next random song after a small delay
    $(if ($shuffleMode) {
        "setTimeout(function() {
            cleanupAudioContext(); // Close AudioContext before navigation
            window.location.href = '/play-audio/shuffle';
        }, 100);"
    })
});

function trackMusic() {
    if (totalPlayTime > 0) {
        fetch('/api/track/music', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                path: musicPath,
                artist: musicArtist,
                title: musicTitle,
                album: musicAlbum,
                playTime: totalPlayTime
            })
        }).then(() => {
            totalPlayTime = 0; // Reset after tracking
        }).catch(err => console.error('Music tracking error:', err));
    }
}

// Track on page unload
window.addEventListener('beforeunload', function() {
    if (playStartTime) {
        var playDuration = (Date.now() - playStartTime) / 1000;
        totalPlayTime += playDuration;
    }
    if (totalPlayTime > 0) {
        navigator.sendBeacon('/api/track/music', JSON.stringify({
            path: musicPath,
            artist: musicArtist,
            title: musicTitle,
            album: musicAlbum,
            playTime: totalPlayTime
        }));
    }
    // Always cleanup AudioContext on page unload
    cleanupAudioContext();
});

</script>
"@
                            
                            $standalonePlayerPage = Get-HTMLPage -Content $playerContent -Title $music.title
                            
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($standalonePlayerPage)
                            $response.ContentLength64 = $buffer.Length
                            $response.ContentType = "text/html; charset=utf-8"
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Get-HTMLPage -Content "<h1>Music not found</h1>" -Title "Error"
                        }
                    }
                    '^/stream-audio/(.+)$' {
                        # Stream audio file with range support for seeking
                        $musicId = $matches[1]
                        
                        Write-Host "  Audio stream requested for music ID: $musicId" -ForegroundColor Cyan
                        
                        $music = Invoke-SqliteQuery -DataSource $CONFIG.MusicDB `
                            -Query "SELECT filepath, format FROM music WHERE id = @id" `
                            -SqlParameters @{ id = $musicId }
                        
                        if ($music -and (Test-Path -LiteralPath $music.filepath)) {
                            Write-Host "  ✓ Streaming audio: $($music.filepath)" -ForegroundColor Green
                            
                            $contentType = switch ($music.format.ToLower()) {
                                'mp3'  { 'audio/mpeg' }
                                'flac' { 'audio/flac' }
                                'wav'  { 'audio/wav' }
                                'm4a'  { 'audio/mp4' }
                                'aac'  { 'audio/aac' }
                                'ogg'  { 'audio/ogg' }
                                'wma'  { 'audio/x-ms-wma' }
                                'opus' { 'audio/opus' }
                                default { 'audio/mpeg' }
                            }
                            
                            # ASYNC STREAMING (v2.9) - Non-blocking!
                            $streamId = Start-AsyncFileStream -Response $response `
                                -Request $request `
                                -FilePath $music.filepath `
                                -ContentType $contentType `
                                -AdditionalHeaders @{
                                    'Accept-Ranges' = 'bytes'
                                    'Cache-Control' = 'no-cache'
                                }
                            continue
                        }
                        else {
                            Write-Host "  ✗ Audio file not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/stream/(.+)$' {
                        # Serve video files - direct streaming only
                        $videoId = $matches[1]
                        
                        Write-Host "`n[STREAM REQUEST] Video ID: $videoId" -ForegroundColor Yellow
                        
                        # Get video from database
                        $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                            -Query "SELECT id, filepath, format, title FROM videos WHERE id = @id" `
                            -SqlParameters @{ id = $videoId }
                        
                        if ($video -and (Test-Path -LiteralPath $video.filepath)) {
                            # Determine if transcoding is needed
                            $streamInfo = Get-SmartVideoStream -FilePath $video.filepath -VideoId $videoId
                            
                            if ($streamInfo) {
                                if ($streamInfo.Type -eq "HLS") {
                                    # For HLS, redirect to the playlist
                                    Write-Host "[→] Redirecting to HLS playlist: $($streamInfo.PlaylistURL)" -ForegroundColor Magenta
                                    $response.StatusCode = 302
                                    $response.RedirectLocation = $streamInfo.PlaylistURL
                                    $response.Close()
                                    continue
                                }
                                else {
                                    # Direct streaming for MP4
                                    Write-Host "[✓] Direct streaming MP4" -ForegroundColor Green
                                    # Mark response as async-handled to prevent double-close
                                    $response | Add-Member -NotePropertyName '_AsyncHandled' -NotePropertyValue $true -Force
                                    
                                    $streamResult = Start-AsyncFileStream -Response $response -Request $request `
                                        -FilePath $video.filepath -ContentType "video/mp4"
                                    
                                    if ($null -eq $streamResult) {
                                        Write-Host "[!] Failed to start async stream (response may be closed)" -ForegroundColor Yellow
                                        # Don't close response again if it's already handled
                                        if ($response -and -not $response.OutputStream.CanWrite) {
                                            # Response already closed, just continue
                                        } else {
                                            try { $response.Close() } catch {}
                                        }
                                    }
                                }
                                continue  # Exit route handling after async stream starts
                            }
                            else {
                                $response.StatusCode = 500
                                $response.Close()
                                continue
                            }
                        }
                        else {
                            Write-Host "[✗] Video not found: $videoId" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/fetch-posters' {
                        Update-AllMoviePosters -Silent
                        Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>✅ Poster Fetch Complete!</h1><p>Movie posters have been downloaded.</p><p><a href='/'>← Back to Library</a></p></div>" -Title "Posters Downloaded"
                    }
                    '^/refresh-genres' {
                        Update-AllGenres
                        Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>✅ Genres Refreshed!</h1><p>Movie genres have been updated from TMDB.</p><p><a href='/videos/genres'>View Genres</a> • <a href='/videos'>← Back to Videos</a></p></div>" -Title "Genres Updated"
                    }
                    '^/rescan' { 
                        Write-Host "Starting rescan..." -ForegroundColor Yellow
                        Invoke-MediaScan
                        Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>🔄 Rescan Complete!</h1><p><a href='/'>← Back to Library</a></p></div>" -Title "Rescan Complete"
                    }
                    '^/favicon.ico$' {
                        $faviconPath = Join-Path $PSScriptRoot "favicon.ico"
                        if (Test-Path $faviconPath) {
                            $iconBytes = [System.IO.File]::ReadAllBytes($faviconPath)
                            $response.ContentType = "image/x-icon"
                            $response.ContentLength64 = $iconBytes.Length
                            $response.OutputStream.Write($iconBytes, 0, $iconBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/play' {
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $filePath = $queryParams['path']
                        
                        if ($filePath -and (Test-Path $filePath)) {
                            # Open file with default application
                            Start-Process $filePath
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>▶️ Opening file...</h1><p>$filePath</p><p><a href='/'>← Back to Library</a></p></div>" -Title "Playing..."
                        }
                        else {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>❌ File not found</h1><p><a href='/'>← Back to Library</a></p></div>" -Title "Error"
                        }
                    }
                    '^/image/(.+)$' {
                        # Serve image files
                        $encodedPath = $matches[1]
                        $imagePath = [System.Web.HttpUtility]::UrlDecode($encodedPath)
                        
                        if (Test-Path $imagePath) {
                            Write-Host "  ✓ Serving image: $imagePath" -ForegroundColor Green
                            $imageBytes = [System.IO.File]::ReadAllBytes($imagePath)
                            $extension = [System.IO.Path]::GetExtension($imagePath).ToLower()
                            
                            $contentType = switch ($extension) {
                                '.jpg'  { 'image/jpeg' }
                                '.jpeg' { 'image/jpeg' }
                                '.png'  { 'image/png' }
                                '.gif'  { 'image/gif' }
                                '.bmp'  { 'image/bmp' }
                                '.webp' { 'image/webp' }
                                default { 'image/jpeg' }
                            }
                            
                            $response.ContentType = $contentType
                            $response.ContentLength64 = $imageBytes.Length
                            $response.OutputStream.Write($imageBytes, 0, $imageBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ Image not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/view-image' {
                        # View image in full screen
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $encodedPath = $queryParams['path']
                        $imagePath = [System.Web.HttpUtility]::UrlDecode($encodedPath)
                        
                        if ($imagePath -and (Test-Path $imagePath)) {
                            $filename = [System.IO.Path]::GetFileName($imagePath)
                            $imageContent = @"
<div style='max-width:100%; margin:0; padding:20px; background:#000;'>
  <div style='margin-bottom:20px; display:flex; justify-content:space-between; align-items:center;'>
    <h2 style='margin:0; color:#fff;'>$filename</h2>
    <a href='/pictures' style='color:#60a5fa; text-decoration:none;'>← Back to Pictures</a>
  </div>
  <img src='/image/$encodedPath' style='max-width:100%; height:auto; display:block; margin:0 auto; border-radius:8px;' />
</div>
"@
                            Get-HTMLPage -Content $imageContent -Title "$filename"
                        }
                        else {
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>❌ Image not found</h1><p><a href='/pictures'>← Back to Pictures</a></p></div>" -Title "Error"
                        }
                    }
                    '^/view-pdf' {
                        # View PDF in browser
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $encodedPath = $queryParams['path']
                        $pdfPath = [System.Web.HttpUtility]::UrlDecode($encodedPath)
                        
                        if ($pdfPath -and (Test-Path $pdfPath)) {
                            Write-Host "  ✓ Serving PDF: $pdfPath" -ForegroundColor Green
                            $pdfBytes = [System.IO.File]::ReadAllBytes($pdfPath)
                            $response.ContentType = "application/pdf"
                            $response.ContentLength64 = $pdfBytes.Length
                            $response.AddHeader("Content-Disposition", "inline")
                            $response.OutputStream.Write($pdfBytes, 0, $pdfBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ PDF not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/epubjs/(.+)$' {
                        # Serve EPUB.js library files
                        $filename = $matches[1]
                        $epubjsPath = Join-Path $PSScriptRoot "tools\epubjs\$filename"
                        
                        if (Test-Path $epubjsPath) {
                            Write-Host "  ✓ Serving EPUB.js: $filename" -ForegroundColor Green
                            $jsBytes = [System.IO.File]::ReadAllBytes($epubjsPath)
                            $response.ContentType = "application/javascript"
                            $response.ContentLength64 = $jsBytes.Length
                            $response.OutputStream.Write($jsBytes, 0, $jsBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ EPUB.js file not found: $filename" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/read-epub' {
                        # EPUB Reader page
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $encodedPath = $queryParams['path']
                        $epubPath = [System.Web.HttpUtility]::UrlDecode($encodedPath)
                        
                        if ($epubPath -and (Test-Path $epubPath)) {
                            $epubTitle = [System.IO.Path]::GetFileNameWithoutExtension($epubPath)
                            $encodedForReader = [System.Web.HttpUtility]::UrlEncode($epubPath)
                            
                            $readerHtml = @"
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>$epubTitle</title>
    <script src="/epubjs/jszip.min.js"></script>
    <script src="/epubjs/epub.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #1a1a1a; color: #fff; overflow: hidden; }
        #reader-container { display: flex; flex-direction: column; height: 100vh; }
        #toolbar { background: rgba(30, 30, 30, 0.95); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding: 12px 20px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
        #book-title { font-size: 16px; font-weight: 600; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        #controls { display: flex; gap: 10px; align-items: center; }
        .btn { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); color: #fff; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 14px; transition: all 0.2s; white-space: nowrap; }
        .btn:hover { background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); }
        #viewer-wrapper { flex: 1; position: relative; overflow: hidden; }
        #viewer { width: 100%; height: 100%; background: #ffffff; }
        #loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; font-size: 18px; color: rgba(255, 255, 255, 0.7); }
        #loading.hidden { display: none; }
        .spinner { border: 4px solid rgba(255, 255, 255, 0.1); border-top: 4px solid #6366f1; border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite; margin: 0 auto 16px auto; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        #prev-area, #next-area { position: absolute; top: 0; bottom: 0; width: 50%; cursor: pointer; z-index: 10; display: flex; align-items: center; transition: background 0.2s; }
        #prev-area { left: 0; justify-content: flex-start; padding-left: 20px; }
        #next-area { right: 0; justify-content: flex-end; padding-right: 20px; }
        #prev-area:hover, #next-area:hover { background: rgba(0, 0, 0, 0.1); }
        .nav-arrow { font-size: 48px; opacity: 0; transition: opacity 0.2s; pointer-events: none; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); }
        #prev-area:hover .nav-arrow, #next-area:hover .nav-arrow { opacity: 0.7; }
        #progress-bar { height: 3px; background: rgba(255, 255, 255, 0.1); position: relative; }
        #progress-fill { height: 100%; background: linear-gradient(90deg, #6366f1, #8b5cf6); width: 0%; transition: width 0.3s; }
        #toc-panel { position: fixed; left: -300px; top: 0; bottom: 0; width: 300px; background: rgba(20, 20, 20, 0.98); backdrop-filter: blur(10px); border-right: 1px solid rgba(255, 255, 255, 0.1); transition: left 0.3s; z-index: 100; overflow-y: auto; }
        #toc-panel.open { left: 0; }
        #toc-header { padding: 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); display: flex; justify-content: space-between; align-items: center; }
        #toc-close { cursor: pointer; font-size: 24px; opacity: 0.6; }
        #toc-close:hover { opacity: 1; }
        #toc-list { list-style: none; }
        #toc-list li { padding: 12px 16px; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.05); transition: background 0.2s; }
        #toc-list li:hover { background: rgba(255, 255, 255, 0.05); }
    </style>
</head>
<body>
    <div id="reader-container">
        <div id="toolbar">
            <div id="book-title">$epubTitle</div>
            <div id="controls">
                <button class="btn" onclick="toggleTOC()">📚 Contents</button>
                <div style="display: flex; gap: 8px; align-items: center;">
                    <span id="page-info" style="font-size: 14px; color: rgba(255,255,255,0.8);">Page 1</span>
                    <input type="number" id="page-input" placeholder="Go to page" min="1" style="width: 100px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: #fff; padding: 6px 10px; border-radius: 6px; font-size: 13px;">
                    <button class="btn" onclick="goToPage()">Go</button>
                </div>
                <button class="btn" onclick="window.location.href='/pdfs'">✕ Close</button>
            </div>
        </div>
        <div id="viewer-wrapper">
            <div id="loading"><div class="spinner"></div><div>Loading...</div></div>
            <div id="viewer"></div>
            <div id="prev-area" onclick="prevPage()"><span class="nav-arrow">‹</span></div>
            <div id="next-area" onclick="nextPage()"><span class="nav-arrow">›</span></div>
        </div>
        <div id="progress-bar"><div id="progress-fill"></div></div>
    </div>
    <div id="toc-panel">
        <div id="toc-header"><strong>Contents</strong><span id="toc-close" onclick="toggleTOC()">×</span></div>
        <ul id="toc-list"></ul>
    </div>
    <script>
        let book, rendition;
        async function initReader() {
            try {
                console.log('Fetching EPUB...');
                const res = await fetch('/serve-epub?path=$encodedForReader');
                const arrayBuffer = await res.arrayBuffer();
                console.log('Fetched', arrayBuffer.byteLength, 'bytes');
                
                book = ePub(arrayBuffer, { openAs: "binary" });
                console.log('Book created');
                
                rendition = book.renderTo('viewer', { width: '100%', height: '100vh' });
                
                // Force white/clear background - override any EPUB internal styles
                rendition.themes.register('light', {
                    'body': {
                        'background': '#ffffff !important',
                        'background-color': '#ffffff !important',
                        'color': '#1a1a1a !important'
                    },
                    'html': {
                        'background': '#ffffff !important',
                        'background-color': '#ffffff !important'
                    },
                    '*': {
                        'background-color': 'transparent !important'
                    },
                    'p, div, span, section, article, main, aside, header, footer, nav, h1, h2, h3, h4, h5, h6': {
                        'background': 'transparent !important',
                        'background-color': 'transparent !important',
                        'color': '#1a1a1a !important'
                    },
                    'a': {
                        'color': '#2563eb !important'
                    },
                    'img': {
                        'background': 'transparent !important'
                    },
                    'table, tr, td, th': {
                        'background': 'transparent !important',
                        'background-color': 'transparent !important',
                        'border-color': '#e5e5e5 !important'
                    }
                });
                rendition.themes.select('light');
                
                // Also inject CSS directly into each section
                rendition.hooks.content.register((contents) => {
                    contents.addStylesheetRules({
                        'body': { 'background': '#ffffff !important', 'background-color': '#ffffff !important', 'color': '#1a1a1a !important' },
                        'html': { 'background': '#ffffff !important', 'background-color': '#ffffff !important' },
                        '*': { 'background-color': 'inherit !important' },
                        'p, div, span, section, article': { 'background': 'transparent !important', 'color': '#1a1a1a !important' }
                    });
                });
                
                await rendition.display();
                console.log('Displayed!');
                
                document.getElementById('loading').classList.add('hidden');
                
                book.loaded.navigation.then((nav) => {
                    const tocList = document.getElementById('toc-list');
                    nav.toc.forEach(ch => {
                        const li = document.createElement('li');
                        li.textContent = ch.label;
                        li.onclick = () => { rendition.display(ch.href); toggleTOC(); };
                        tocList.appendChild(li);
                    });
                }).catch(e => console.warn('No TOC'));
                
                book.locations.generate(1024).then(() => {
                    rendition.on('relocated', (loc) => {
                        const pct = book.locations.percentageFromCfi(loc.start.cfi);
                        document.getElementById('progress-fill').style.width = (pct * 100) + '%';
                        
                        // Update page number display
                        const currentPage = book.locations.locationFromCfi(loc.start.cfi);
                        const totalPages = book.locations.total;
                        document.getElementById('page-info').textContent = 'Page ' + currentPage + ' of ' + totalPages;
                    });
                });
            } catch (e) {
                console.error('Error:', e);
                document.getElementById('loading').innerHTML = '<div style="color:#ef4444;">Error: ' + e.message + '</div>';
            }
            
            document.addEventListener('keydown', (e) => {
                if (e.key === 'ArrowLeft') prevPage();
                if (e.key === 'ArrowRight') nextPage();
                if (e.key === 'Enter' && document.activeElement.id === 'page-input') goToPage();
            });
        }
        function prevPage() { if (rendition) rendition.prev(); }
        function nextPage() { if (rendition) rendition.next(); }
        function toggleTOC() { document.getElementById('toc-panel').classList.toggle('open'); }
        function goToPage() {
            const pageNum = parseInt(document.getElementById('page-input').value);
            if (pageNum && book && book.locations) {
                const cfi = book.locations.cfiFromLocation(pageNum);
                if (cfi) {
                    rendition.display(cfi);
                    document.getElementById('page-input').value = '';
                }
            }
        }
        window.onload = initReader;
    </script>
</body>
</html>
"@
                            
                            $htmlBytes = [System.Text.Encoding]::UTF8.GetBytes($readerHtml)
                            $response.ContentType = "text/html; charset=utf-8"
                            $response.ContentLength64 = $htmlBytes.Length
                            $response.OutputStream.Write($htmlBytes, 0, $htmlBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ EPUB not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/serve-epub' {
                        # Serve EPUB file for epub.js
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $encodedPath = $queryParams['path']
                        $epubPath = [System.Web.HttpUtility]::UrlDecode($encodedPath)
                        
                        if ($epubPath -and (Test-Path $epubPath)) {
                            Write-Host "  ✓ Serving EPUB data" -ForegroundColor Green
                            $epubBytes = [System.IO.File]::ReadAllBytes($epubPath)
                            $response.ContentType = "application/epub+zip"
                            $response.ContentLength64 = $epubBytes.Length
                            $response.AddHeader("Access-Control-Allow-Origin", "*")
                            $response.OutputStream.Write($epubBytes, 0, $epubBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    '^/download-epub' {
                        # Download EPUB file
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $encodedPath = $queryParams['path']
                        $epubPath = [System.Web.HttpUtility]::UrlDecode($encodedPath)
                        
                        if ($epubPath -and (Test-Path $epubPath)) {
                            Write-Host "  ✓ Serving EPUB: $epubPath" -ForegroundColor Green
                            $epubBytes = [System.IO.File]::ReadAllBytes($epubPath)
                            $filename = [System.IO.Path]::GetFileName($epubPath)
                            $response.ContentType = "application/epub+zip"
                            $response.ContentLength64 = $epubBytes.Length
                            $response.AddHeader("Content-Disposition", "attachment; filename=`"$filename`"")
                            $response.OutputStream.Write($epubBytes, 0, $epubBytes.Length)
                            $response.Close()
                            continue
                        }
                        else {
                            Write-Host "  ✗ EPUB not found!" -ForegroundColor Red
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                    }
                    
                    # HLS playlist route
                    '^/hls/([^/]+)/playlist\.m3u8$' {
                        $transcodeId = $matches[1]
                        $playlistPath = Join-Path $PSScriptRoot "hls_temp\$transcodeId\playlist.m3u8"
                        
                        Write-Host "[HLS] Playlist request: $transcodeId" -ForegroundColor Cyan
                        
                        # Check if transcode was already stopped - return 404 immediately
                        if (-not $Global:ActiveTranscodes.ContainsKey($transcodeId)) {
                            Write-Host "[HLS] ✗ Transcode already stopped - returning 404" -ForegroundColor Yellow
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                        
                        Write-Host "[HLS] Looking for: $playlistPath" -ForegroundColor Gray
                        
                        # Wait for playlist to be created (up to 20 seconds)
                        $waited = 0
                        while (-not (Test-Path $playlistPath) -and $waited -lt 20) {
                            # Check if transcode was stopped during wait
                            if (-not $Global:ActiveTranscodes.ContainsKey($transcodeId)) {
                                Write-Host "[HLS] ✗ Transcode stopped during wait - aborting" -ForegroundColor Yellow
                                $response.StatusCode = 404
                                $response.Close()
                                continue
                            }
                            
                            Start-Sleep -Milliseconds 500
                            $waited += 0.5
                            
                            # Show progress every 2 seconds
                            if ([int]$waited % 2 -eq 0) {
                                Write-Host "[HLS] Still waiting for playlist... (${waited}s)" -ForegroundColor Gray
                            }
                        }
                        
                        if (Test-Path $playlistPath) {
                            Write-Host "[HLS] ✓ Playlist found! Serving..." -ForegroundColor Green
                            $response.ContentType = "application/vnd.apple.mpegurl"
                            $response.Headers.Add("Cache-Control", "no-cache")
                            
                            $content = Get-Content -Path $playlistPath -Raw
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($content)
                            $response.ContentLength64 = $buffer.Length
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                        }
                        else {
                            Write-Host "[HLS] ✗ Playlist not found after ${waited}s" -ForegroundColor Red
                            
                            # Check if transcode is still running
                            if ($Global:ActiveTranscodes.ContainsKey($transcodeId)) {
                                $transcodeInfo = $Global:ActiveTranscodes[$transcodeId]
                                if ($transcodeInfo.Process -and -not $transcodeInfo.Process.HasExited) {
                                    Write-Host "[HLS] FFmpeg is still running, but no playlist yet" -ForegroundColor Yellow
                                }
                                else {
                                    Write-Host "[HLS] FFmpeg process has exited!" -ForegroundColor Red
                                    Write-Host "[HLS] Last errors:" -ForegroundColor Yellow
                                    $transcodeInfo.ErrorOutput | Select-Object -Last 5 | ForEach-Object {
                                        Write-Host "    $_" -ForegroundColor Gray
                                    }
                                }
                            }
                            
                            $response.StatusCode = 404
                            $response.Close()
                        }
                        continue
                    }
                    
                    # HLS segment route
                    '^/hls/([^/]+)/segment_(\d+)\.ts$' {
                        $transcodeId = $matches[1]
                        $segmentNum = $matches[2]
                        $segmentPath = Join-Path $PSScriptRoot "hls_temp\$transcodeId\segment_$segmentNum.ts"
                        
                        if (Test-Path $segmentPath) {
                            $response.ContentType = "video/mp2t"
                            $response.Headers.Add("Cache-Control", "public, max-age=31536000")
                            
                            $fileBytes = [System.IO.File]::ReadAllBytes($segmentPath)
                            $response.ContentLength64 = $fileBytes.Length
                            $response.OutputStream.Write($fileBytes, 0, $fileBytes.Length)
                            $response.Close()
                        }
                        else {
                            # Debug: Show what segments actually exist
                            $hlsDir = Join-Path $PSScriptRoot "hls_temp\$transcodeId"
                            if (Test-Path $hlsDir) {
                                $existingSegments = Get-ChildItem -Path $hlsDir -Filter "segment_*.ts" | Select-Object -First 5 -ExpandProperty Name
                                Write-Host "[HLS] Segment $segmentNum not found. Available segments: $($existingSegments -join ', ')..." -ForegroundColor Yellow
                            } else {
                                Write-Host "[HLS] Segment not found and transcode directory doesn't exist: $segmentPath" -ForegroundColor Yellow
                            }
                            
                            $response.StatusCode = 404
                            $response.Close()
                        }
                        continue
                    }
                    
                    '^/api/stream-info/(.+)$' {
                        # API endpoint to check stream type WITHOUT starting transcode
                        $videoId = $matches[1]
                        
                        # Parse query parameters for audio track selection
                        $queryParams = [System.Web.HttpUtility]::ParseQueryString($request.Url.Query)
                        $selectedAudioTrack = if ($queryParams['audioTrack']) { [int]$queryParams['audioTrack'] } else { 0 }
                        
                        $video = Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB `
                            -Query "SELECT id, filepath, format FROM videos WHERE id = @id" `
                            -SqlParameters @{ id = $videoId }
                        
                        Write-Host "[API] Stream info request for video ID: $videoId (audio track: $selectedAudioTrack)" -ForegroundColor Cyan
                        if ($video) {
                            Write-Host "[API] Video found in DB: $($video.filepath)" -ForegroundColor Gray
                            
                            # Check file existence with retry for network shares
                            # Escape special characters in path for Test-Path (square brackets are wildcards)
                            $escapedPath = $video.filepath -replace '\[','`[' -replace '\]','`]'
                            
                            $fileExists = $false
                            $maxRetries = 3
                            for ($i = 1; $i -le $maxRetries; $i++) {
                                $fileExists = Test-Path -LiteralPath $video.filepath
                                if ($fileExists) {
                                    break
                                }
                                if ($i -lt $maxRetries) {
                                    Write-Host "[API] File not found on attempt $i, retrying... (network share delay?)" -ForegroundColor Yellow
                                    Start-Sleep -Milliseconds 500
                                }
                            }
                            
                            Write-Host "[API] File exists: $fileExists" -ForegroundColor $(if($fileExists){'Green'}else{'Red'})
                            
                            if (-not $fileExists) {
                                # Additional diagnostics
                                Write-Host "[API] File path: $($video.filepath)" -ForegroundColor Gray
                                $parentPath = Split-Path -Parent $video.filepath
                                Write-Host "[API] Parent directory exists: $(Test-Path -LiteralPath $parentPath)" -ForegroundColor Gray
                                if (Test-Path -LiteralPath $parentPath) {
                                    Write-Host "[API] Parent directory accessible, file missing!" -ForegroundColor Red
                                } else {
                                    Write-Host "[API] Parent directory NOT accessible (network share issue?)" -ForegroundColor Red
                                }
                            }
                        } else {
                            Write-Host "[API] Video not in database!" -ForegroundColor Red
                        }
                        
                        if ($video -and $fileExists) {
                            # Use comprehensive codec checking (not just container format)
                            $needsTranscoding = Test-VideoNeedsTranscoding -FilePath $video.filepath
                            
                            if ($needsTranscoding) {
                                # Will need HLS transcoding - start it now with selected audio track
                                $streamInfo = Get-SmartVideoStream -FilePath $video.filepath -VideoId $videoId -AudioTrackIndex $selectedAudioTrack
                                
                                if ($streamInfo) {
                                    $jsonResponse = @{
                                        type = "hls"
                                        playlistUrl = $streamInfo.PlaylistURL
                                        transcodeId = $streamInfo.TranscodeId
                                    } | ConvertTo-Json
                                } else {
                                    # Transcode failed to start
                                    $response.StatusCode = 500
                                    $jsonResponse = @{
                                        error = "Failed to start transcode"
                                    } | ConvertTo-Json
                                }
                            }
                            else {
                                # Direct streaming - MP4 with compatible codecs
                                $jsonResponse = @{
                                    type = "direct"
                                } | ConvertTo-Json
                            }
                            
                            $response.ContentType = "application/json"
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($jsonResponse)
                            $response.ContentLength64 = $buffer.Length
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                        }
                        else {
                            Write-Host "[API] Video not found or file inaccessible: ID $videoId" -ForegroundColor Red
                            $response.StatusCode = 404
                            $jsonResponse = @{
                                error = if ($video) { "Video file not found at path: $($video.filepath)" } else { "Video not in database" }
                            } | ConvertTo-Json
                            $response.ContentType = "application/json"
                            $buffer = [System.Text.Encoding]::UTF8.GetBytes($jsonResponse)
                            $response.ContentLength64 = $buffer.Length
                            $response.OutputStream.Write($buffer, 0, $buffer.Length)
                            $response.Close()
                        }
                        continue
                    }
                    
                    '^/api/stop-transcode/(.+)$' {
                        # API endpoint to stop a specific transcode (called when user leaves player page)
                        $transcodeId = $matches[1]
                        
                        Write-Host "[API] Stop transcode request: $transcodeId" -ForegroundColor Yellow
                        
                        if ($Global:ActiveTranscodes.ContainsKey($transcodeId)) {
                            $transcodeInfo = $Global:ActiveTranscodes[$transcodeId]
                            
                            # Stop the FFmpeg process
                            if ($transcodeInfo.Process -and -not $transcodeInfo.Process.HasExited) {
                                Write-Host "[✓] Stopping transcode: $transcodeId" -ForegroundColor Green
                                $transcodeInfo.Process.Kill()
                                Start-Sleep -Milliseconds 500
                            }
                            
                            # Remove from active transcodes
                            $Global:ActiveTranscodes.TryRemove($transcodeId, [ref]$null) | Out-Null
                            
                            # Clean up temp directory
                            $hlsDir = $transcodeInfo.HLSDirectory
                            if (Test-Path $hlsDir) {
                                Remove-Item -Path $hlsDir -Recurse -Force -ErrorAction SilentlyContinue
                            }
                            
                            # Send success response (204 No Content)
                            $response.StatusCode = 204
                            $response.Close()
                        }
                        else {
                            # Transcode not found or already stopped (still return success)
                            $response.StatusCode = 204
                            $response.Close()
                        }
                        continue
                    }
                    
                    default { 
                        # Handle 404s - don't render error page for resource requests
                        $isResourceRequest = $url -match '\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf)$'
                        
                        if ($isResourceRequest) {
                            # Silently return 404 for missing resources (no error page)
                            $response.StatusCode = 404
                            $response.Close()
                            continue
                        }
                        else {
                            # Show error page for actual page requests
                            Get-HTMLPage -Content "<div style='padding:40px; text-align:center;'><h1>404 - Page Not Found</h1><p><a href='/'>← Back to Home</a></p></div>" -Title "404"
                        }
                    }
                }
                
                # Only send HTML response if we got HTML back (not for streaming/binary routes)
                if ($html) {
                    $buffer = [System.Text.Encoding]::UTF8.GetBytes($html)
                    $response.ContentLength64 = $buffer.Length
                    $response.ContentType = "text/html; charset=utf-8"
                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                }
            }
            catch {
                Write-Host "[!] Error processing request: $_" -ForegroundColor Red
            }
            finally {
                # Check if response is being handled asynchronously (v2.9)
                if ($response.PSObject.Properties.Name -contains '_AsyncHandled') {
                    # Don't close - async stream job will handle it
                }
                elseif (-not $response.OutputStream.CanWrite) {
                    # Response already closed
                }
                else {
                    try { $response.Close() } catch { }
                }
            }
        }
    }
    catch {
        Write-Host "[✗] Server error: $_" -ForegroundColor Red
    }
    finally {
        # Stop session cleanup timer
        if ($null -ne $Global:SessionCleanupTimer) {
            $Global:SessionCleanupTimer.Stop()
            $Global:SessionCleanupTimer.Dispose()
            $Global:SessionCleanupTimer = $null
        }
        
        # Clear all sessions
        $Global:UserSessions.Clear()
        
        Cleanup-AsyncStreaming
        Stop-AllTranscodes
        if ($listener.IsListening) {
            $listener.Stop()
        }
        $listener.Close()
        
        # Unregister event handlers
        Get-EventSubscriber -SourceIdentifier PowerShell.Exiting -ErrorAction SilentlyContinue | Unregister-Event
        
        Write-Host "`n[✓] Server stopped gracefully" -ForegroundColor Green
    }
}

# ============================================================================
# MAIN EXECUTION
# ============================================================================

function Start-PSMediaLibrary {
    param(
        [bool]$DoAutoScan = $false,
        [bool]$DoFetchPosters = $false,
        [bool]$DoExtractAlbumArt = $false,
        [bool]$DoGeneratePDFThumbnails = $false,
        [bool]$DoStartServer = $false,
        [bool]$IsSilent = $false
    )
    
    Clear-Host
    Write-Host @"
╔══════════════════════════════════════════════════════════════╗
║                      NexusStack                              ║
║            PowerShell Media Library Manager                  ║
╚══════════════════════════════════════════════════════════════╝
"@ -ForegroundColor Cyan

    if ($IsSilent) {
        Write-Host "`n[Silent Mode] Starting with minimal output..." -ForegroundColor Gray
    }

    # Check dependencies
    if (-not (Test-PSSQLiteModule)) {
        Write-Host "`nExiting due to missing dependencies." -ForegroundColor Red
        return
    }
    
    # Check Pode module (required for web server)
    if (-not (Test-PodeModule)) {
        Write-Host "`nExiting due to missing dependencies." -ForegroundColor Red
        return
    }
    
    # Check FFmpeg (optional - for album artwork)
    if (-not $IsSilent) {
        Test-FFmpegAvailability | Out-Null
    }
    
    # Check Ghostscript (optional - for PDF preview generation)
    if (-not $IsSilent) {
        Test-GhostscriptAvailability | Out-Null
    }
    
    # Check Video.js (optional - for enhanced video player)
    if (-not $IsSilent) {
        Test-VideoJSAvailability | Out-Null
    }
    
    Write-Host "`n" ("═" * 70) -ForegroundColor DarkGray
    
    # Initialize FFmpeg transcoding (ALWAYS - required for MKV/AVI/MOV support)
    Write-Host "" # Blank line
    try {
        Initialize-FFmpegTranscoding
    }
    catch {
        Write-Host "[✗] Error initializing FFmpeg: $_" -ForegroundColor Red
        Write-Host "    Transcoding will not be available" -ForegroundColor Yellow
    }
    
    Write-Host "`n" ("═" * 70) -ForegroundColor DarkGray
    
    # Initialize databases
    Write-Host "`n=== Initializing Databases ===" -ForegroundColor Cyan
    
    # Initialize user management database FIRST
    Initialize-UsersDatabase -DatabasePath $CONFIG.UsersDB
    
    # Initialize media databases
    Initialize-Database -DatabasePath $CONFIG.MoviesDB -Type "movies"
    Initialize-Database -DatabasePath $CONFIG.MusicDB -Type "music"
    Initialize-Database -DatabasePath $CONFIG.PicturesDB -Type "pictures"
    Initialize-Database -DatabasePath $CONFIG.PDFDB -Type "pdfs"
    
    Write-Host "`n" ("═" * 70) -ForegroundColor DarkGray
    Write-Host "`n┌─ MEDIA SCAN ─────────────────────────────────────────────────────┐" -ForegroundColor Yellow
    
    # Handle media scan
    $shouldScan = $DoAutoScan
    if (-not $IsSilent -and -not $DoAutoScan) {
        $scanChoice = Read-Host "│ Would you like to scan your media folders now? (Y/N)"
        Write-Host "└──────────────────────────────────────────────────────────────────┘" -ForegroundColor Yellow
        $shouldScan = ($scanChoice -eq 'Y' -or $scanChoice -eq 'y')
    }
    
    if ($shouldScan) {
        Invoke-MediaScan
    }
    else {
        # Load stats from database
        $Global:MediaStats.TotalVideos = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COUNT(*) as count FROM videos").count
        $Global:MediaStats.TotalMusic = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(*) as count FROM music").count
        $Global:MediaStats.TotalPictures = (Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query "SELECT COUNT(*) as count FROM images").count
        $Global:MediaStats.TotalPDFs = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COUNT(*) as count FROM pdfs").count
        
        # Calculate sizes
        $videoSizeQuery = (Invoke-SqliteQuery -DataSource $CONFIG.MoviesDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM videos").total
        $musicSizeQuery = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM music").total
        $pictureSizeQuery = (Invoke-SqliteQuery -DataSource $CONFIG.PicturesDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM images").total
        $pdfSizeQuery = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COALESCE(SUM(size_bytes), 0) as total FROM pdfs").total
        
        $Global:MediaStats.VideoSizeGB = [math]::Round($videoSizeQuery / 1GB, 2)
        $Global:MediaStats.MusicSizeGB = [math]::Round($musicSizeQuery / 1GB, 2)
        $Global:MediaStats.PicturesSizeGB = [math]::Round($pictureSizeQuery / 1GB, 2)
        $Global:MediaStats.PDFSizeGB = [math]::Round($pdfSizeQuery / 1GB, 2)
        $Global:MediaStats.TotalSizeGB = [math]::Round(($videoSizeQuery + $musicSizeQuery + $pictureSizeQuery + $pdfSizeQuery) / 1GB, 2)
    }
    
    Write-Host "`n" ("═" * 70) -ForegroundColor DarkGray
    
    # Show smart analysis (skip in silent mode)
    if (-not $IsSilent) {
        Write-Host "`n=== Smart Analysis Features ===" -ForegroundColor Cyan
        Get-LowQualityMedia
        Get-IncompleteMetadata
        Get-WatchRecommendations
        Get-CleanupSuggestions
    }
    
    Write-Host "`n" ("═" * 70) -ForegroundColor DarkGray
    Write-Host "`n┌─ OPTIONAL ENHANCEMENTS ──────────────────────────────────────────┐" -ForegroundColor Magenta
    
    # Handle poster fetching
    if (-not [string]::IsNullOrWhiteSpace($CONFIG.TMDB_APIKey)) {
        $shouldFetchPosters = $DoFetchPosters
        if (-not $IsSilent -and -not $DoFetchPosters) {
            $fetchPostersChoice = Read-Host "│ Fetch movie posters from TMDB? (Y/N)"
            $shouldFetchPosters = ($fetchPostersChoice -eq 'Y' -or $fetchPostersChoice -eq 'y')
        }
        
        if ($shouldFetchPosters) {
            Update-AllMoviePosters
        }
    }
    
    # Handle album artwork extraction
    if ($Global:FFmpegPath) {
        $musicCount = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(*) as count FROM music").count
        if ($musicCount -gt 0) {
            $shouldExtractArt = $DoExtractAlbumArt
            if (-not $IsSilent -and -not $DoExtractAlbumArt) {
                $extractArtChoice = Read-Host "│ Extract album artwork from music files? (Y/N)"
                $shouldExtractArt = ($extractArtChoice -eq 'Y' -or $extractArtChoice -eq 'y')
            }
            
            if ($shouldExtractArt) {
                # Check if any artwork already exists
                $existingArtCount = (Invoke-SqliteQuery -DataSource $CONFIG.MusicDB -Query "SELECT COUNT(*) as count FROM music WHERE album_art_cached IS NOT NULL AND album_art_cached != ''").count
                
                $forceExtract = $false
                if ($existingArtCount -gt 0 -and -not $IsSilent) {
                    Write-Host "[i] Found $existingArtCount files with existing artwork" -ForegroundColor Yellow
                    $forceChoice = Read-Host "Force re-extract all artwork (ignoring existing)? (Y/N)"
                    $forceExtract = ($forceChoice -eq 'Y' -or $forceChoice -eq 'y')
                }
                
                Extract-AllAlbumArtwork -Force:$forceExtract
            }
        }
    }
    
    # Handle PDF thumbnail generation
    $pdfCount = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COUNT(*) as count FROM pdfs").count
    if ($pdfCount -gt 0) {
        # Check if Ghostscript or Windows native PDF rendering is available
        $windowsNative = $PSVersionTable.PSVersion.Major -ge 5 -and [Environment]::OSVersion.Version.Major -ge 10
        $gsAvailable = (Get-Command gs -ErrorAction SilentlyContinue) -or (Get-Command gswin64c -ErrorAction SilentlyContinue) -or (Get-Command gswin32c -ErrorAction SilentlyContinue)
        
        if ($windowsNative -or $gsAvailable) {
            $shouldGeneratePDFThumbnails = $DoGeneratePDFThumbnails
            if (-not $IsSilent -and -not $DoGeneratePDFThumbnails) {
                $generatePDFChoice = Read-Host "│ Generate PDF preview thumbnails? (Y/N)"
                $shouldGeneratePDFThumbnails = ($generatePDFChoice -eq 'Y' -or $generatePDFChoice -eq 'y')
            }
            
            if ($shouldGeneratePDFThumbnails) {
                # Check if any thumbnails already exist
                $existingThumbnailCount = (Invoke-SqliteQuery -DataSource $CONFIG.PDFDB -Query "SELECT COUNT(*) as count FROM pdfs WHERE preview_image IS NOT NULL AND preview_image != ''").count
                
                $forceGenerate = $false
                if ($existingThumbnailCount -gt 0 -and -not $IsSilent) {
                    Write-Host "[i] Found $existingThumbnailCount PDFs with existing thumbnails" -ForegroundColor Yellow
                    $forceChoice = Read-Host "Force re-generate all thumbnails (ignoring existing)? (Y/N)"
                    $forceGenerate = ($forceChoice -eq 'Y' -or $forceChoice -eq 'y')
                }
                
                Generate-AllPDFThumbnails -Force:$forceGenerate
            }
        }
    }
    
    Write-Host "└──────────────────────────────────────────────────────────────────┘" -ForegroundColor Magenta
    Write-Host "`n" ("═" * 70) -ForegroundColor DarkGray
    Write-Host "`n┌─ WEB SERVER ─────────────────────────────────────────────────────┐" -ForegroundColor Green
    
    # Handle web server startup
    $shouldStartServer = $DoStartServer -or $IsSilent  # Silent mode always starts server
    if (-not $IsSilent -and -not $DoStartServer) {
        $startServerChoice = Read-Host "│ Start web server? (Y/N)"
        Write-Host "└──────────────────────────────────────────────────────────────────┘" -ForegroundColor Green
        $shouldStartServer = ($startServerChoice -eq 'Y' -or $startServerChoice -eq 'y')
    }
    
    if ($shouldStartServer) {
        Write-Host "`n[✓] Starting web server on port $($CONFIG.ServerPort)..." -ForegroundColor Green
        Start-MediaServer -Port $CONFIG.ServerPort
    }
    else {
        Write-Host "`nTo start the server later, run: Start-MediaServer" -ForegroundColor Gray
    }
}

# Auto-start if run directly
if ($MyInvocation.InvocationName -ne '.') {
    # Handle Silent mode
    if ($Silent) {
        Start-PSMediaLibrary -IsSilent $true
    }
    else {
        Start-PSMediaLibrary `
            -DoAutoScan $AutoScan `
            -DoFetchPosters $FetchPosters `
            -DoExtractAlbumArt $ExtractAlbumArt `
            -DoGeneratePDFThumbnails $GeneratePDFThumbnails `
            -DoStartServer $StartServer
    }
}
