Full Code of ysdragon/StreamBot for AI

main fe918ed4cc73 cached
55 files
183.6 KB
51.1k tokens
200 symbols
1 requests
Download .txt
Repository: ysdragon/StreamBot
Branch: main
Commit: fe918ed4cc73
Files: 55
Total size: 183.6 KB

Directory structure:
gitextract_raib73cm/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── docker-image.yml
├── .gitignore
├── Dockerfile
├── Dockerfile.node
├── README.md
├── docker-compose-node.yml
├── docker-compose-warp.yml
├── docker-compose.yml
├── egg-stream-bot.json
├── package.json
├── src/
│   ├── commands/
│   │   ├── base.ts
│   │   ├── config.ts
│   │   ├── help.ts
│   │   ├── list.ts
│   │   ├── manager.ts
│   │   ├── ping.ts
│   │   ├── play.ts
│   │   ├── preview.ts
│   │   ├── queue.ts
│   │   ├── skip.ts
│   │   ├── status.ts
│   │   ├── stop.ts
│   │   └── ytsearch.ts
│   ├── config.ts
│   ├── events/
│   │   ├── client/
│   │   │   └── ready.ts
│   │   ├── messageCreate.ts
│   │   └── voiceStateUpdate.ts
│   ├── index.ts
│   ├── server/
│   │   ├── index.ts
│   │   ├── middleware/
│   │   │   ├── auth.ts
│   │   │   └── multer.ts
│   │   ├── public/
│   │   │   ├── css/
│   │   │   │   └── main.css
│   │   │   ├── js/
│   │   │   │   └── main.js
│   │   │   └── site.webmanifest
│   │   ├── routes/
│   │   │   ├── auth.ts
│   │   │   ├── dashboard.ts
│   │   │   ├── preview.ts
│   │   │   └── upload.ts
│   │   ├── utils/
│   │   │   └── helpers.ts
│   │   └── views/
│   │       ├── layouts/
│   │       │   └── main.ejs
│   │       └── pages/
│   │           ├── dashboard.ejs
│   │           ├── login.ejs
│   │           └── preview.ejs
│   ├── services/
│   │   ├── media.ts
│   │   ├── queue.ts
│   │   └── streaming.ts
│   ├── types/
│   │   └── index.ts
│   └── utils/
│       ├── ffmpeg.ts
│       ├── gen-hash.ts
│       ├── logger.ts
│       ├── shared.ts
│       ├── youtube.ts
│       └── yt-dlp.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "npm" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/docker-image.yml
================================================
name: StreamBot Docker Image CI

on:
  push:
    branches: [ "main" ]
    paths-ignore:
      - '.env.example'
      - 'docker-compose.yml'
      - 'LICENSE'
      - 'README.md'
  release:
    types: [published]
    
jobs:
  build:
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - runner: ubuntu-latest
            platform: linux/amd64
          - runner: ubuntu-24.04-arm
            platform: linux/arm64
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Quay.io
        uses: docker/login-action@v3
        with:
          registry: quay.io
          username: ${{ secrets.QUAY_USERNAME }}
          password: ${{ secrets.QUAY_ROBOT_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5.5.1
        with:
          images: quay.io/ydrag0n/streambot
          
      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile
          platforms: ${{ matrix.platform }}
          labels: ${{ steps.meta.outputs.labels }}
          outputs: type=image,name=quay.io/ydrag0n/streambot,push-by-digest=true,name-canonical=true,push=true
          cache-from: type=gha
          cache-to: type=gha,mode=max
          
      - name: Export digest
        run: |
          mkdir -p /tmp/digests
          digest="${{ steps.build.outputs.digest }}"
          touch "/tmp/digests/${digest#sha256:}"
          
      - name: Upload digest
        uses: actions/upload-artifact@v4
        with:
          name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
          path: /tmp/digests/*
          if-no-files-found: error
          retention-days: 1

  build-node:
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - runner: ubuntu-latest
            platform: linux/amd64
          - runner: ubuntu-24.04-arm
            platform: linux/arm64
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Quay.io
        uses: docker/login-action@v3
        with:
          registry: quay.io
          username: ${{ secrets.QUAY_USERNAME }}
          password: ${{ secrets.QUAY_ROBOT_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5.5.1
        with:
          images: quay.io/ydrag0n/streambot
          
      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: Dockerfile.node
          platforms: ${{ matrix.platform }}
          labels: ${{ steps.meta.outputs.labels }}
          outputs: type=image,name=quay.io/ydrag0n/streambot,push-by-digest=true,name-canonical=true,push=true
          cache-from: type=gha
          cache-to: type=gha,mode=max
          
      - name: Export digest
        run: |
          mkdir -p /tmp/digests-node
          digest="${{ steps.build.outputs.digest }}"
          touch "/tmp/digests-node/${digest#sha256:}"
          
      - name: Upload digest
        uses: actions/upload-artifact@v4
        with:
          name: digests-node-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
          path: /tmp/digests-node/*
          if-no-files-found: error
          retention-days: 1

  merge:
    runs-on: ubuntu-latest
    needs:
      - build
    steps:
      - name: Download digests
        uses: actions/download-artifact@v4
        with:
          path: /tmp/digests
          pattern: digests-*
          merge-multiple: true
          
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Login to Quay.io
        uses: docker/login-action@v3
        with:
          registry: quay.io
          username: ${{ secrets.QUAY_USERNAME }}
          password: ${{ secrets.QUAY_ROBOT_TOKEN }}
          
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5.5.1
        with:
          images: quay.io/ydrag0n/streambot
          
      - name: Create manifest list and push
        working-directory: /tmp/digests
        run: |
          docker buildx imagetools create \
            --tag quay.io/ydrag0n/streambot:latest \
            --tag quay.io/ydrag0n/streambot:${{ github.ref_name }} \
            $(printf 'quay.io/ydrag0n/streambot@sha256:%s ' *)

  merge-node:
    runs-on: ubuntu-latest
    needs:
      - build-node
    steps:
      - name: Download digests
        uses: actions/download-artifact@v4
        with:
          path: /tmp/digests-node
          pattern: digests-node-*
          merge-multiple: true
          
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Login to Quay.io
        uses: docker/login-action@v3
        with:
          registry: quay.io
          username: ${{ secrets.QUAY_USERNAME }}
          password: ${{ secrets.QUAY_ROBOT_TOKEN }}
          
      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5.5.1
        with:
          images: quay.io/ydrag0n/streambot
          
      - name: Create manifest list and push
        working-directory: /tmp/digests-node
        run: |
          docker buildx imagetools create \
            --tag quay.io/ydrag0n/streambot:node \
            --tag quay.io/ydrag0n/streambot:node-${{ github.ref_name }} \
            $(printf 'quay.io/ydrag0n/streambot@sha256:%s ' *)

================================================
FILE: .gitignore
================================================
LICENSE
node_modules/
dist/
.env
bun.lockb
videos/
tmp/
*.sock
cookies.txt

================================================
FILE: Dockerfile
================================================
# Use Debian (trixie) as the base image
FROM node:trixie

# Set the working directory
WORKDIR /home/bots/StreamBot

# Install minimal dependencies
RUN apt-get update && apt-get install -y curl ca-certificates unzip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Install bun and add to PATH
ENV BUN_INSTALL="/usr/local/"
RUN curl -fsSL https://bun.sh/install | bash

# Install remaining dependencies and clean cache
RUN apt-get update && apt-get install -y \
    build-essential \
    python3 \
    ffmpeg && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Copy package.json
COPY package.json ./

# Install dependencies
RUN bun install

# Trust all packages
RUN bun pm trust --all

# Copy the rest of the application code
COPY . .

# Verify the application builds
RUN bun run build

# Specify the port number the container should expose
EXPOSE 3000

# Create videos folder
RUN mkdir -p ./videos

# Command to run the application
CMD ["bun", "run", "start"]


================================================
FILE: Dockerfile.node
================================================
# Use Debian (trixie) as the base image
FROM node:trixie

# Set the working directory
WORKDIR /home/bots/StreamBot

# Install minimal dependencies
RUN apt-get update && apt-get install -y curl ca-certificates unzip && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Install remaining dependencies and clean cache
RUN apt-get update && apt-get install -y \
    build-essential \
    python3 \
    ffmpeg && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Copy package.json
COPY package.json ./

# Install dependencies
RUN npm install

# Copy the rest of the application code
COPY . .

# Verify the application builds
RUN npm run build

# Specify the port number the container should expose
EXPOSE 3000

# Create videos folder
RUN mkdir -p ./videos

# Command to run the application
CMD ["npm", "run", "start:node"]


================================================
FILE: README.md
================================================
<div align="center">

<img src="src/server/public/favicon.svg" alt="StreamBot Logo" width="400" height="120"/>

# StreamBot

**A powerful Discord self-bot for streaming videos from multiple sources with a web management interface**

![GitHub release](https://img.shields.io/github/v/release/ysdragon/StreamBot)
[![CodeFactor](https://www.codefactor.io/repository/github/ysdragon/streambot/badge)](https://www.codefactor.io/repository/github/ysdragon/streambot)

[![Ceasefire Now](https://badge.techforpalestine.org/default)](https://techforpalestine.org/learn-more)

</div>

## 📑 Table of Contents

- [✨ Features](#-features)
- [📋 Requirements](#-requirements)
- [🚀 Installation](#-installation)
- [🎮 Usage](#-usage)
- [🐳 Docker Setup](#-docker-setup)
- [🎯 Commands](#-commands)
- [⚙️ Configuration](#%EF%B8%8F-configuration)
- [🌐 Web Interface](#-web-interface)
- [🤝 Contributing](#-contributing)
- [⚠️ Disclaimer](#%EF%B8%8F-disclaimer)
- [📝 License](#-license)

## ✨ Features

- 📁 **Local Video Streaming**: Stream videos from your local videos folder
- 🎬 **YouTube Integration**: Stream YouTube videos with smart search functionality
- 📺 **YouTube Live Streams**: Direct streaming support for YouTube live content
- 🟣 **Twitch Support**: Stream Twitch live streams and video-on-demand (VODs)
- 🔗 **Direct URL Streaming**: Stream from any URL supported by [yt-dlp](https://github.com/yt-dlp/yt-dlp) (thousands of video sites including Vimeo, Dailymotion, Facebook, Instagram, news sites, and more)
- 🎵 **Queue System**: Queue multiple videos with auto-play and skip functionality
- 🌐 **Web Management Interface**: Full-featured web dashboard for video library management
- 📤 **Video Upload**: Upload videos through the web interface or download from remote URLs
- 🖼️ **Video Previews**: Generate and view thumbnail previews for all videos
- ⚙️ **Runtime Configuration**: Adjust streaming parameters and bot settings during runtime

## 📋 Requirements

- **[Bun](https://bun.sh/) v1.1.39+** (recommended) or **[Node.js](https://nodejs.org/) v21+**
- **[FFmpeg](https://www.ffmpeg.org/)** (the bot will attempt to install it automatically if missing, but manual installation is recommended)
- **[yt-dlp](https://github.com/yt-dlp/yt-dlp)** (automatically downloaded and updated by the bot)

### 💡 Optional
- 🎮 **GPU with hardware acceleration** for improved streaming performance
- 🌐 **High-speed internet** for remote video streaming and downloads
- 💾 **Sufficient disk space** for video storage and cache

## 🚀 Installation

This project is [hosted on GitHub](https://github.com/ysdragon/StreamBot).

1. **Clone the repository:**
```bash
git clone https://github.com/ysdragon/StreamBot
cd StreamBot
```

2. **Install dependencies:**

With Bun (recommended):
```bash
bun install
```

With npm:
```bash
npm install
```

3. **Configure environment:**
   - Copy `.env.example` to `.env`
   - Update the configuration values (see [⚙️ Configuration](#%EF%B8%8F-configuration) section)
   - See the [wiki](https://github.com/ysdragon/StreamBot/wiki/Get-Discord-user-token) for instructions on obtaining your Discord token

4. **Setup complete!** 🎉 Required directories for videos and cache will be created automatically on first run.

## 🎮 Usage

### 🚀 Starting the Bot

**With Bun (recommended):**
```bash
bun run start
```

**With Node.js:**
```bash
npm run build
npm run start:node
```

**With web interface enabled:**
Set `SERVER_ENABLED=true` in your `.env` file. The web interface runs alongside the bot automatically.

To run only the web interface without the bot:
```bash
bun run server       # With Bun
npm run server:node  # With Node.js (after building)
```

### 📹 Video Playback

All videos are played through a queue system that automatically advances to the next video when the current one ends.

The `play` command automatically detects the input type:
- 📁 Local files from your `VIDEOS_DIR`
- 🎬 YouTube videos (by URL or search query)
- 🟣 Twitch streams (live or VOD)
- 🔗 Any URL supported by yt-dlp

Use `ytsearch` to find YouTube videos, then `play` with the results to stream them. Use `list` to browse your local video collection.

## 🐳 Docker Setup

StreamBot provides ready-to-use Docker configurations for easy deployment.

### 📦 Standard Deployment

1. **Create project directory:**
```bash
mkdir streambot && cd streambot
```

2. **Download Docker Compose configuration:**
```bash
wget https://raw.githubusercontent.com/ysdragon/StreamBot/main/docker-compose.yml
```

3. **Configure environment:**
   - Edit `docker-compose.yml` to set your environment variables
   - Ensure video storage directories are properly mounted

4. **Launch StreamBot:**
```bash
docker compose up -d
```

### ☁️ Cloudflare WARP Deployment

For enhanced network capabilities with Cloudflare WARP:

1. **Download WARP configuration:**
```bash
wget https://raw.githubusercontent.com/ysdragon/StreamBot/main/docker-compose-warp.yml
```

2. **Configure WARP settings:**
   - Add your WARP license key to `docker-compose-warp.yml`
   - Update Discord token and other required environment variables

3. **Launch with WARP:**
```bash
docker compose -f docker-compose-warp.yml up -d
```

> ⚠️ **Note:** The web interface is not available in WARP mode because the WARP container uses network isolation that prevents external access to the web server port.

## 🎯 Commands

### 📺 Playback Commands

| Command | Description | Aliases |
|---------|-------------|---------|
| `play <video_name\|url\|search_query>` | Play local video, URL, or search YouTube videos | |
| `ytsearch <query>` | Search for videos on YouTube | |
| `stop` | Stop current video playback and clear queue | `leave`, `s` |
| `skip` | Skip the currently playing video | `next` |
| `queue` | Display the current video queue | |
| `list` | Show available local videos | |

### 🔧 Utility Commands

| Command | Description | Aliases |
|---------|-------------|---------|
| `status` | Show current streaming status | |
| `preview <video_name>` | Generate preview thumbnails for a video | |
| `ping` | Check bot latency | |
| `help` | Show available commands | |

### 🛡️ Administration Commands

| Command | Description | Aliases |
|---------|-------------|---------|
| `config [parameter] [value]` | View or adjust bot configuration parameters (Admin only) | `cfg`, `set` |

## ⚙️ Configuration

StreamBot is configured through environment variables in a `.env` file. Copy `.env.example` to `.env` and modify the values as needed.

### 🔐 Discord Self-Bot Configuration

```bash
# Required: Your Discord self-bot token
# See: https://github.com/ysdragon/StreamBot/wiki/Get-Discord-user-token
TOKEN="YOUR_BOT_TOKEN_HERE"

# Command prefix for bot commands
PREFIX="$"

# Discord server where the bot will operate
GUILD_ID="YOUR_SERVER_ID"

# Channel where bot will respond to commands
COMMAND_CHANNEL_ID="COMMAND_CHANNEL_ID"

# Voice/video channel where bot will stream
VIDEO_CHANNEL_ID="VIDEO_CHANNEL_ID"

# Admin user IDs - comma-separated or JSON array format
# Examples:
#   ADMIN_IDS="123456789,987654321"
#   ADMIN_IDS=["123456789","987654321"]
ADMIN_IDS=["YOUR_USER_ID_HERE"]
```

### 📁 File Management

```bash
# Directory where video files are stored
VIDEOS_DIR="./videos"

# Directory for caching video preview thumbnails
PREVIEW_CACHE_DIR="./tmp/preview-cache"
```

### 🍪 Content Source Configuration

```bash
# Path to browser cookies for accessing private/premium content
# Supports: YouTube Premium, age-restricted content, private videos
YTDLP_COOKIES_PATH=""
```

### 🎥 Streaming Configuration

```bash
# Video Quality Settings
STREAM_RESPECT_VIDEO_PARAMS="false"  # Use original video parameters if true
STREAM_BITRATE_OVERRIDE="false"      # If true, use STREAM_BITRATE_KBPS even when respecting video params
STREAM_WIDTH="1280"                  # Output resolution width
STREAM_HEIGHT="720"                  # Output resolution height
STREAM_MAX_WIDTH="0"                 # Max width cap (0 = disabled)
STREAM_MAX_HEIGHT="0"                # Max height cap (0 = disabled)
STREAM_FPS="30"                      # Target frame rate

# Bitrate Settings (affects quality and bandwidth usage)
STREAM_BITRATE_KBPS="2000"           # Target bitrate (higher = better quality)
STREAM_MAX_BITRATE_KBPS="2500"       # Maximum allowed bitrate

# Performance & Encoding
STREAM_HARDWARE_ACCELERATION="false" # Use GPU acceleration if available
STREAM_VIDEO_CODEC="H264"            # Codec: H264, H265, VP8, VP9, AV1

# H.264/H.265 Encoding Preset (quality vs speed tradeoff)
# Options: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow
STREAM_H26X_PRESET="ultrafast"
```

### 🌐 Web Interface Configuration

```bash
# Enable/disable the web management interface
SERVER_ENABLED="false"

# Web interface authentication
SERVER_USERNAME="admin"
SERVER_PASSWORD="admin"  # Plain text, bcrypt, or argon2 hash

# Web server port
SERVER_PORT="8080"
```

### 🍪 Using Cookies with yt-dlp

To access private or premium content (like YouTube Premium videos), you can provide a cookies file:

1. **Export cookies from your browser** using a browser extension:
   - [Get cookies.txt LOCALLY](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) (Chromium-based browsers)
   - [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/) (Firefox-based browsers)

2. **Save the cookies file** (usually named `cookies.txt`) to a location accessible by the bot

3. **Configure the path** in your `.env` file:
   ```bash
   YTDLP_COOKIES_PATH="./cookies.txt"
   ```
   Or use the config command at runtime:
   ```
   $config ytdlpCookiesPath ./cookies.txt
   ```

4. **Restart the bot** if you updated the `.env` file

## 🌐 Web Interface

When enabled (`SERVER_ENABLED="true"`), StreamBot provides a web-based management interface.

### ✨ Features

- 📋 **Video Library Management**: Browse your video collection with file sizes and detailed information
- 📤 **Local File Upload**: Upload videos directly with progress tracking
- 🌐 **Remote URL Download**: Download videos from URLs directly to your library
- 🖼️ **Video Previews**: Generate and view thumbnail screenshots from different parts of each video
- 🗑️ **File Management**: Delete videos from your library
- 📊 **Video Metadata**: View detailed information (duration, resolution, codec, etc.)

### 🔗 Access

After enabling and restarting the bot, access the interface at `http://localhost:8080` (or your configured `SERVER_PORT`).

## 🤝 Contributing

Contributions are welcome! Feel free to:
- 🐛 Report bugs via [issues](https://github.com/ysdragon/StreamBot/issues/new)
- 🔧 Submit [pull requests](https://github.com/ysdragon/StreamBot/pulls)
- 💡 Suggest new features

## ⚠️ Disclaimer

This bot may violate Discord's Terms of Service. Use at your own risk.

I disavow before Allah any unethical use of this project.

إبراء الذمة: أتبرأ من أي استخدام غير أخلاقي لهذا المشروع أمام الله.

## 📝 License

Licensed under MIT License. See [LICENSE](https://github.com/ysdragon/StreamBot/blob/main/LICENSE) for details.


================================================
FILE: docker-compose-node.yml
================================================
services:
  streambot:
    image: quay.io/ydrag0n/streambot:node
    container_name: streambot
    restart: always
    environment:
      # Selfbot options
      TOKEN: "" # Your Discord self-bot token
      PREFIX: "$" # The prefix used to trigger your self-bot commands
      GUILD_ID: "" # The ID of the Discord server your self-bot will be running on
      COMMAND_CHANNEL_ID: "" # The ID of the Discord channel where your self-bot will respond to commands
      VIDEO_CHANNEL_ID: "" # The ID of the Discord voice/video channel where your self-bot will stream videos
      ADMIN_IDS: '["YOUR_USER_ID_HERE"]' # A list of Discord user IDs that are considered administrators (comma-separated or JSON array format)
      # General options
      VIDEOS_DIR: "./videos" # The local path where you store video files
      PREVIEW_CACHE_DIR: "./tmp/preview-cache" # The local path where your self-bot will cache video preview thumbnails

      # yt-dlp options
      YTDLP_COOKIES_PATH: "" # Path to cookies file for yt-dlp (for accessing age-restricted or premium content)

      # Stream options
      STREAM_RESPECT_VIDEO_PARAMS: "false" # This option is used to respect video parameters such as width, height, fps, bitrate, and max bitrate.
      STREAM_BITRATE_OVERRIDE: "false" # If true, use STREAM_BITRATE_KBPS even when respecting video params
      STREAM_MAX_WIDTH: "0" # Max width cap (0 = disabled)
      STREAM_MAX_HEIGHT: "0" # Max height cap (0 = disabled)
      STREAM_WIDTH: "1280" # The width of the video stream in pixels
      STREAM_HEIGHT: "720" # The height of the video stream in pixels
      STREAM_FPS: "30" # The frames per second (FPS) of the video stream
      STREAM_BITRATE_KBPS: "2000" # The bitrate of the video stream in kilobits per second (Kbps)
      STREAM_MAX_BITRATE_KBPS: "2500" # The maximum bitrate of the video stream in kilobits per second (Kbps)
      STREAM_HARDWARE_ACCELERATION: "false" # Whether to use hardware acceleration for video decoding, set to "true" to enable, "false" to disable
      STREAM_VIDEO_CODEC: "H264" # The video codec to use for the stream, can be "H264" or "H265" or "VP8"
      
      # STREAM_H26X_PRESET: Determines the encoding preset for H26x video streams. 
      # If the STREAM_H26X_PRESET environment variable is set, it parses the value 
      # using the parsePreset function. If not set, it defaults to 'ultrafast' for 
      # optimal encoding speed. This preset is only applicable when the codec is 
      # H26x; otherwise, it should be disabled or ignored.
      # Available presets: "ultrafast", "superfast", "veryfast", "faster", 
      # "fast", "medium", "slow", "slower", "veryslow".
      STREAM_H26X_PRESET: "ultrafast"

      # Videos server options
      SERVER_ENABLED: "false" # Whether to enable the built-in video server
      SERVER_USERNAME: "admin" # The username for the video server's admin interface
      SERVER_PASSWORD: "admin" # The password for the video server's admin interface
      SERVER_PORT: "3000" # The port number the video server will listen on
    volumes:
      - ./videos:/home/bots/StreamBot/videos
    ports:
      - 3000:3000


================================================
FILE: docker-compose-warp.yml
================================================
services:
  streambot:
    image: quay.io/ydrag0n/streambot:latest
    container_name: streambot
    restart: always
    environment:
      # Selfbot options
      TOKEN: "" # Your Discord self-bot token
      PREFIX: "$" # The prefix used to trigger your self-bot commands
      GUILD_ID: "" # The ID of the Discord server your self-bot will be running on
      COMMAND_CHANNEL_ID: "" # The ID of the Discord channel where your self-bot will respond to commands
      VIDEO_CHANNEL_ID: "" # The ID of the Discord voice/video channel where your self-bot will stream videos
      ADMIN_IDS: ["YOUR_USER_ID_HERE"] # A list of Discord user IDs that are considered administrators (comma-separated or JSON array format)
      # General options
      VIDEOS_DIR: "./videos" # The local path where you store video files
      PREVIEW_CACHE_DIR: "./tmp/preview-cache" # The local path where your self-bot will cache video preview thumbnails

      # yt-dlp options
      YTDLP_COOKIES_PATH: "" # Path to cookies file for yt-dlp (for accessing age-restricted or premium content)

      # Stream options
      STREAM_RESPECT_VIDEO_PARAMS: "false" # This option is used to respect video parameters such as width, height, fps, bitrate, and max bitrate.
      STREAM_BITRATE_OVERRIDE: "false" # If true, use STREAM_BITRATE_KBPS even when respecting video params
      STREAM_MAX_WIDTH: "0" # Max width cap (0 = disabled)
      STREAM_MAX_HEIGHT: "0" # Max height cap (0 = disabled)
      STREAM_WIDTH: "1280" # The width of the video stream in pixels
      STREAM_HEIGHT: "720" # The height of the video stream in pixels
      STREAM_FPS: "30" # The frames per second (FPS) of the video stream
      STREAM_BITRATE_KBPS: "2000" # The bitrate of the video stream in kilobits per second (Kbps)
      STREAM_MAX_BITRATE_KBPS: "2500" # The maximum bitrate of the video stream in kilobits per second (Kbps)
      STREAM_HARDWARE_ACCELERATION: "false" # Whether to use hardware acceleration for video decoding, set to "true" to enable, "false" to disable
      STREAM_VIDEO_CODEC: "H264" # The video codec to use for the stream, can be "H264" or "H265" or "VP8"
      
      # STREAM_H26X_PRESET: Determines the encoding preset for H26x video streams. 
      # If the STREAM_H26X_PRESET environment variable is set, it parses the value 
      # using the parsePreset function. If not set, it defaults to 'ultrafast' for 
      # optimal encoding speed. This preset is only applicable when the codec is 
      # H26x; otherwise, it should be disabled or ignored.
      # Available presets: "ultrafast", "superfast", "veryfast", "faster", 
      # "fast", "medium", "slow", "slower", "veryslow".
      STREAM_H26X_PRESET: "ultrafast"

      # Videos server options
      SERVER_ENABLED: "false" # Whether to enable the built-in video server
      SERVER_USERNAME: "admin" # The username for the video server's admin interface
      SERVER_PASSWORD: "admin" # The password for the video server's admin interface
      SERVER_PORT: "3000" # The port number the video server will listen on
    volumes:
      - ./videos:/home/bots/StreamBot/videos
    network_mode: "service:warp"
    depends_on:
      - warp
  warp:
    image: caomingjun/warp
    container_name: warp
    restart: always
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - '1080:1080'
      - '3000:3000'
    environment:
      - WARP_SLEEP=2
      - WARP_LICENSE_KEY= # Your Cloudflare Warp license key
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv4.conf.all.src_valid_mark=1
    volumes:
      - ./data:/var/lib/cloudflare-warp

================================================
FILE: docker-compose.yml
================================================
services:
  streambot:
    image: quay.io/ydrag0n/streambot:latest
    container_name: streambot
    restart: always
    environment:
      # Selfbot options
      TOKEN: "" # Your Discord self-bot token
      PREFIX: "$" # The prefix used to trigger your self-bot commands
      GUILD_ID: "" # The ID of the Discord server your self-bot will be running on
      COMMAND_CHANNEL_ID: "" # The ID of the Discord channel where your self-bot will respond to commands
      VIDEO_CHANNEL_ID: "" # The ID of the Discord voice/video channel where your self-bot will stream videos
      ADMIN_IDS: '["YOUR_USER_ID_HERE"]' # A list of Discord user IDs that are considered administrators (comma-separated or JSON array format)
      # General options
      VIDEOS_DIR: "./videos" # The local path where you store video files
      PREVIEW_CACHE_DIR: "./tmp/preview-cache" # The local path where your self-bot will cache video preview thumbnails

      # yt-dlp options
      YTDLP_COOKIES_PATH: "" # Path to cookies file for yt-dlp (for accessing age-restricted or premium content)

      # Stream options
      STREAM_RESPECT_VIDEO_PARAMS: "false" # This option is used to respect video parameters such as width, height, fps, bitrate, and max bitrate.
      STREAM_BITRATE_OVERRIDE: "false" # If true, use STREAM_BITRATE_KBPS even when respecting video params
      STREAM_MAX_WIDTH: "0" # Max width cap (0 = disabled)
      STREAM_MAX_HEIGHT: "0" # Max height cap (0 = disabled)
      STREAM_WIDTH: "1280" # The width of the video stream in pixels
      STREAM_HEIGHT: "720" # The height of the video stream in pixels
      STREAM_FPS: "30" # The frames per second (FPS) of the video stream
      STREAM_BITRATE_KBPS: "2000" # The bitrate of the video stream in kilobits per second (Kbps)
      STREAM_MAX_BITRATE_KBPS: "2500" # The maximum bitrate of the video stream in kilobits per second (Kbps)
      STREAM_HARDWARE_ACCELERATION: "false" # Whether to use hardware acceleration for video decoding, set to "true" to enable, "false" to disable
      STREAM_VIDEO_CODEC: "H264" # The video codec to use for the stream, can be "H264" or "H265" or "VP8"
      
      # STREAM_H26X_PRESET: Determines the encoding preset for H26x video streams. 
      # If the STREAM_H26X_PRESET environment variable is set, it parses the value 
      # using the parsePreset function. If not set, it defaults to 'ultrafast' for 
      # optimal encoding speed. This preset is only applicable when the codec is 
      # H26x; otherwise, it should be disabled or ignored.
      # Available presets: "ultrafast", "superfast", "veryfast", "faster", 
      # "fast", "medium", "slow", "slower", "veryslow".
      STREAM_H26X_PRESET: "ultrafast"

      # Videos server options
      SERVER_ENABLED: "false" # Whether to enable the built-in video server
      SERVER_USERNAME: "admin" # The username for the video server's admin interface
      SERVER_PASSWORD: "admin" # The password for the video server's admin interface
      SERVER_PORT: "3000" # The port number the video server will listen on
    volumes:
      - ./videos:/home/bots/StreamBot/videos
    ports:
      - 3000:3000


================================================
FILE: egg-stream-bot.json
================================================
{
    "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
    "meta": {
        "version": "PTDL_v2",
        "update_url": null
    },
    "exported_at": "2025-06-30T09:36:07+03:00",
    "name": "StreamBot",
    "author": "ysdragon@protonmail.com",
    "description": "A self bot to stream videos to Discord.",
    "features": null,
    "docker_images": {
        "Bun Latest": "ghcr.io\/parkervcp\/yolks:bun_latest"
    },
    "file_denylist": [],
    "startup": "if [[ -d .git ]] && [[ {{AUTO_UPDATE}} == \"1\" ]]; then git pull; fi; if [ -f \/home\/container\/package.json ]; then bun install; fi; bun run start",
    "config": {
        "files": "{\r\n    \".env\": {\r\n        \"parser\": \"file\",\r\n        \"find\": {\r\n            \"TOKEN\": \"TOKEN= \\\"{{env.TOKEN}}\\\"\"\r\n        }\r\n    }\r\n}",
        "startup": "{\r\n    \"done\": [\r\n        \"change this text 1\",\r\n        \"change this text 2\"\r\n    ]\r\n}",
        "logs": "{}",
        "stop": "^^C"
    },
    "scripts": {
        "installation": {
            "script": "#!\/bin\/bash\r\n# Bun App Installation Script\r\n#\r\n# Server Files: \/mnt\/server\r\n\r\n# Define color codes\r\ndeclare -A colors=(\r\n    [\"GREEN\"]='\\033[0;32m'\r\n    [\"YELLOW\"]='\\033[0;33m'\r\n    [\"NC\"]='\\033[0m'\r\n)\r\n\r\n# Logger function\r\nlog() {\r\n    local level=$1\r\n    local message=$2\r\n    local color=${colors[$3]}\r\n    \r\n    if [ -z \"$color\" ]; then\r\n        color=${colors[NC]}\r\n    fi\r\n    \r\n    printf \"${color}[$level]${colors[NC]} $message\\n\"\r\n}\r\n\r\nlog \"INFO\" \"Installing deps...\" \"GREEN\"\r\napt update\r\napt install -qq -y git curl jq file unzip make gcc g++ python3 python3-dev libtool ffmpeg\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\n\r\nmkdir -p \/mnt\/server\r\ncd \/mnt\/server\r\n\r\nGIT_ADDRESS=\"https:\/\/github.com\/ysdragon\/StreamBot.git\"\r\n\r\n## pull git js bot repo\r\nif [ \"$(ls -A \/mnt\/server)\" ]; then\r\n    log \"INFO\" \"\/mnt\/server directory is not empty.\" \"GREEN\"\r\n    if [ -d .git ]; then\r\n        log \"INFO\" \".git directory exists\" \"GREEN\"\r\n        if [ -f .git\/config ]; then\r\n            log \"INFO\" \"loading info from git config\" \"GREEN\"\r\n            ORIGIN=$(git config --get remote.origin.url)\r\n        else\r\n            exit 10\r\n        fi\r\n    fi\r\n\r\n    if [ \"${ORIGIN}\" == \"${GIT_ADDRESS}\" ]; then\r\n        log \"INFO\" \"Updating from GitHub...\" \"GREEN\"\r\n        git pull\r\n    fi\r\nelse\r\n    if [ -z ${BOT_VERSION} ]; then\r\n        log \"INFO\" \"Installing default branch (main)...\" \"GREEN\"\r\n        git clone ${GIT_ADDRESS} .\r\n    else\r\n        log \"INFO\" \"Installing version ${BOT_VERSION}\" \"GREEN\"\r\n        git clone --single-branch --branch v${BOT_VERSION} ${GIT_ADDRESS} .\r\n    fi\r\nfi\r\n\r\n# Copy .env.example to .env\r\ncp .env.example .env\r\n\r\nlog \"INFO\" \"Installation completed!\" \"GREEN\"\r\n\r\nexit 0",
            "container": "ghcr.io\/parkervcp\/installers:debian",
            "entrypoint": "bash"
        }
    },
    "variables": [
        {
            "name": "Auto Update",
            "description": "Pull the latest files on startup.\r\n0 = false (default)\r\n1 = true",
            "env_variable": "AUTO_UPDATE",
            "default_value": "0",
            "user_viewable": true,
            "user_editable": true,
            "rules": "required|boolean",
            "field_type": "text"
        },
        {
            "name": "Bot Version",
            "description": "The bot version to install.",
            "env_variable": "BOT_VERSION",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "nullable|string",
            "field_type": "text"
        },
        {
            "name": "Bot Token",
            "description": "Your real user token for the bot.\r\nhttps:\/\/github.com\/ysdragon\/StreamBot\/wiki\/Get-Discord-user-token",
            "env_variable": "TOKEN",
            "default_value": "",
            "user_viewable": true,
            "user_editable": true,
            "rules": "required|string|max:100",
            "field_type": "text"
        }
    ]
}

================================================
FILE: package.json
================================================
{
  "name": "streambot",
  "version": "2.0.2",
  "description": "A self bot to stream movies/videos to Discord.",
  "main": "dist/index.js",
  "type": "module",
  "scripts": {
    "start": "bun src/index.ts",
    "start:node": "node dist/index.js",
    "server": "bun src/server/index.ts",
    "server:node": "node dist/server/index.js",
    "build": "tsc",
    "watch": "tsc -w",
    "gen-hash": "bun src/utils/gen-hash.ts",
    "gen-hash:node": "node dist/utils/gen-hash.js"
  },
  "author": "ysdragon",
  "license": "MIT",
  "dependencies": {
    "@dank074/discord-video-stream": "6.0.0",
    "@types/bcrypt": "^6.0.0",
    "@types/bun": "^1.3.10",
    "argon2": "^0.44.0",
    "axios": "^1.13.6",
    "bcrypt": "^6.0.0",
    "discord.js-selfbot-v13": "^3.7.1",
    "dotenv": "^17.3.1",
    "ejs": "^5.0.1",
    "express": "^5.2.1",
    "express-ejs-layouts": "^2.5.1",
    "express-session": "^1.19.0",
    "fluent-ffmpeg": "^2.1.3",
    "got": "^14.6.6",
    "multer": "^2.1.1",
    "play-dl": "^1.9.7",
    "twitch-m3u8": "^1.1.5",
    "winston": "^3.19.0"
  },
  "devDependencies": {
    "@types/express": "^5.0.6",
    "@types/express-session": "^1.18.2",
    "@types/multer": "^2.1.0",
    "typescript": "^5.9.3"
  }
}


================================================
FILE: src/commands/base.ts
================================================
import { Command, CommandContext } from "../types/index.js";
import { DiscordUtils } from "../utils/shared.js";

export abstract class BaseCommand implements Command {
	abstract name: string;
	abstract description: string;
	abstract usage: string;
	aliases?: string[];

	constructor(commandManager?: any) {
	}

	abstract execute(context: CommandContext): Promise<void>;

	protected async sendError(message: any, error: string): Promise<void> {
		await DiscordUtils.sendError(message, error);
	}

	protected async sendSuccess(message: any, description: string): Promise<void> {
		await DiscordUtils.sendSuccess(message, description);
	}

	protected async sendInfo(message: any, title: string, description: string): Promise<void> {
		await DiscordUtils.sendInfo(message, title, description);
	}

	protected async sendList(message: any, items: string[], type?: string): Promise<void> {
		await DiscordUtils.sendList(message, items, type);
	}

	protected async sendPlaying(message: any, title: string): Promise<void> {
		await DiscordUtils.sendPlaying(message, title);
	}

	protected async sendFinishMessage(message: any): Promise<void> {
		await DiscordUtils.sendFinishMessage(message);
	}
}

================================================
FILE: src/commands/config.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";
import config, { parseBoolean, parseVideoCodec, parsePreset } from "../config.js";
import logger from "../utils/logger.js";

export default class ConfigCommand extends BaseCommand {
	name = "config";
	description = "View or adjust bot configuration parameters (Admin only)";
	usage = "config [parameter] [value]";
	aliases = ["cfg", "set"];

	async execute(context: CommandContext): Promise<void> {
		// Check if user is an admin
		if (!this.isAdmin(context.message.author.id)) {
			await this.sendError(context.message, "You don't have permission to use this command. Admin access required.");
			logger.warn(`Unauthorized config command attempt by user ${context.message.author.id}`);
			return;
		}

		const args = context.args;

		// If no arguments, show current config
		if (args.length === 0) {
			await this.showConfig(context);
			return;
		}

		// If one argument, show specific parameter
		if (args.length === 1) {
			await this.showParameter(context, args[0]);
			return;
		}

		// If two or more arguments, set parameter
		const parameter = args[0].toLowerCase();
		const value = args.slice(1).join(' ');
		await this.setParameter(context, parameter, value);
	}

	private async showConfig(context: CommandContext): Promise<void> {
		const configInfo = [
			"**Stream Options:**",
			`• respect_video_params: ${config.respect_video_params}`,
			`• bitrateOverride: ${config.bitrateOverride}`,
			`• width: ${config.width}`,
			`• height: ${config.height}`,
			`• fps: ${config.fps}`,
			`• bitrateKbps: ${config.bitrateKbps}`,
			`• maxBitrateKbps: ${config.maxBitrateKbps}`,
			`• maxWidth: ${config.maxWidth || 'None'}`,
			`• maxHeight: ${config.maxHeight || 'None'}`,
			`• hardwareAcceleratedDecoding: ${config.hardwareAcceleratedDecoding}`,
			`• h26xPreset: ${config.h26xPreset}`,
			`• videoCodec: ${config.videoCodec}`,
			"",
			"**General Options:**",
			`• videosDir: ${config.videosDir}`,
			`• previewCacheDir: ${config.previewCacheDir}`,
			"",
			"**yt-dlp Options:**",
			`• ytdlpCookiesPath: ${config.ytdlpCookiesPath || '(not set)'}`,
			"",
			"Use `config <parameter>` to view a specific parameter",
			"Use `config <parameter> <value>` to change a parameter"
		].join('\n');

		await this.sendInfo(context.message, 'Bot Configuration', configInfo);
	}

	private async showParameter(context: CommandContext, parameter: string): Promise<void> {
		// Find the actual config key case-insensitively
		const key = Object.keys(config).find(k => k.toLowerCase() === parameter.toLowerCase());

		if (!key) {
			await this.sendError(context.message, `Unknown parameter: ${parameter}`);
			return;
		}

		const value = (config as any)[key];
		await this.sendInfo(context.message, `Config: ${key}`, `Current value: \`${value}\``);
	}

	private async setParameter(context: CommandContext, parameter: string, value: string): Promise<void> {
		// Find the actual config key case-insensitively
		const key = Object.keys(config).find(k => k.toLowerCase() === parameter.toLowerCase());

		if (!key) {
			await this.sendError(context.message, `Unknown parameter: ${parameter}`);
			return;
		}

		try {
			switch (key) {
				// Boolean parameters
				case 'respect_video_params':
				case 'bitrateOverride':
				case 'hardwareAcceleratedDecoding':
					const boolValue = parseBoolean(value);
					(config as any)[key] = boolValue;
					await this.sendSuccess(context.message, `Set ${key} to \`${boolValue}\``);
					logger.info(`Config updated: ${key} = ${boolValue}`);
					break;

				// Number parameters
				case 'width':
				case 'height':
				case 'fps':
				case 'bitrateKbps':
				case 'maxBitrateKbps':
				case 'maxWidth':
				case 'maxHeight':
					const numValue = parseInt(value);
					
					// Validate non-negative
					if (isNaN(numValue) || numValue < 0) {
						await this.sendError(context.message, `Invalid number value: ${value}. Must be non-negative.`);
						return;
					}
					
					// Validate positive for essential params
					if (['width', 'height', 'fps', 'bitrateKbps', 'maxBitrateKbps'].includes(key) && numValue === 0) {
						await this.sendError(context.message, `Invalid number value: ${value}. Must be greater than 0.`);
						return;
					}

					(config as any)[key] = numValue;
					await this.sendSuccess(context.message, `Set ${key} to \`${numValue}\``);
					logger.info(`Config updated: ${key} = ${numValue}`);
					break;

				// Video codec
				case 'videoCodec':
					const codec = parseVideoCodec(value);
					if (!codec) {
						await this.sendError(context.message, `Invalid video codec. Valid options: VP8, H264, H265`);
						return;
					}
					config.videoCodec = codec;
					await this.sendSuccess(context.message, `Set videoCodec to \`${codec}\``);
					logger.info(`Config updated: videoCodec = ${codec}`);
					break;

				// H26x preset
				case 'h26xPreset':
					const preset = parsePreset(value);
					if (!preset) {
						await this.sendError(context.message, `Invalid preset. Valid options: ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow`);
						return;
					}
					config.h26xPreset = preset;
					await this.sendSuccess(context.message, `Set h26xPreset to \`${preset}\``);
					logger.info(`Config updated: h26xPreset = ${preset}`);
					break;

				// String parameters
				case 'videosDir':
				case 'previewCacheDir':
				case 'ytdlpCookiesPath':
					(config as any)[key] = value;
					await this.sendSuccess(context.message, `Set ${key} to \`${value}\``);
					logger.info(`Config updated: ${key} = ${value}`);
					break;

				default:
					await this.sendError(context.message, `Cannot modify parameter: ${key}`);
					return;
			}
		} catch (error) {
			logger.error(`Error setting config parameter ${parameter}:`, error);
			await this.sendError(context.message, `Failed to set ${parameter}: ${error}`);
		}
	}

	private isAdmin(userId: string): boolean {
		// If no admins configured, allow all users (backwards compatibility)
		if (!config.adminIds || config.adminIds.length === 0) {
			return true;
		}
		return config.adminIds.includes(userId);
	}
}


================================================
FILE: src/commands/help.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";
import { CommandManager } from "./manager.js";

export default class HelpCommand extends BaseCommand {
	name = "help";
	description = "Show available commands";
	usage = "help";

	constructor(private commandManager: CommandManager) {
		super(commandManager);
	}

	async execute(context: CommandContext): Promise<void> {
		const commandList = this.commandManager.getCommandList();

		const helpText = [
			'📽 **Available Commands**',
			'',
			commandList,
		].join('\n');

		await context.message.react('📋');
		await context.message.reply(helpText);
	}
}

================================================
FILE: src/commands/list.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext, Video } from "../types/index.js";
import fs from 'fs';
import path from 'path';
import config from "../config.js";

export default class ListCommand extends BaseCommand {
	name = "list";
	description = "Show available local videos";
	usage = "list";

	async execute(context: CommandContext): Promise<void> {
		// Always refresh video list from filesystem
		const videoFiles = fs.readdirSync(config.videosDir);
		const refreshedVideos = videoFiles.map(file => {
			const fileName = path.parse(file).name;
			return { name: fileName, path: path.join(config.videosDir, file) };
		});

		// Update the videos array in context
		context.videos.length = 0;
		context.videos.push(...refreshedVideos);

		const videoList = refreshedVideos.map((video, index) => `${index + 1}. \`${video.name}\``);
		if (videoList.length > 0) {
			await this.sendList(context.message,
				[`(${refreshedVideos.length} videos found)`, ...videoList]);
		} else {
			await this.sendError(context.message, 'No videos found');
		}
	}
}

================================================
FILE: src/commands/manager.ts
================================================
import { Command, CommandContext } from "../types/index.js";
import fs from 'fs';
import path from 'path';
import logger from '../utils/logger.js';
import config from '../config.js';
import { ErrorUtils } from '../utils/shared.js';

export class CommandManager {
	private commands: Map<string, Command> = new Map();
	private aliases: Map<string, string> = new Map();

	constructor() {
		this.loadCommands();
	}

	private async loadCommands(): Promise<void> {
		// Prefer src for Bun (TypeScript native), dist for Node.js (compiled JS)
		const isBun = typeof Bun !== 'undefined';
		const commandsPath = path.join(process.cwd(), isBun ? 'src' : 'dist', 'commands');

		if (!commandsPath) {
			logger.error('Could not find commands directory in either dist/ or src/');
			return;
		}
		
		try {
			const commandFiles = fs.readdirSync(commandsPath)
				.filter(file => (file.endsWith('.ts') || file.endsWith('.js')) && !file.endsWith('.d.ts') && !file.startsWith('base.') && !file.startsWith('manager.'));

			for (const file of commandFiles) {
				try {
					// Use appropriate extension based on directory
					const isDist = commandsPath.includes('dist');
					const fileName = isDist ? file.replace('.ts', '.js') : file;
					const filePath = path.join(commandsPath, fileName);
					const commandModule = await import(filePath);

					// Look for default export or named export
					let CommandClass = commandModule.default || commandModule[Object.keys(commandModule)[0]];

					if (CommandClass && this.isCommand(CommandClass)) {
						const command = new CommandClass(this) as Command;

						// Register command
						this.commands.set(command.name.toLowerCase(), command);

						// Register aliases
						if (command.aliases) {
							for (const alias of command.aliases) {
								this.aliases.set(alias.toLowerCase(), command.name.toLowerCase());
							}
						}
					} else {
						logger.warn(`File ${file} does not export a valid command`);
						logger.debug(`Exported keys: ${Object.keys(commandModule).join(', ')}`);
						if (CommandClass) {
							logger.debug(`CommandClass properties: ${Object.keys(CommandClass.prototype || {}).join(', ')}`);
						}
					}
				} catch (error) {
					await ErrorUtils.handleError(error, `loading command from ${file}`);
				}
			}

			logger.info(`Loaded ${this.commands.size} commands`);
		} catch (error) {
			await ErrorUtils.handleError(error, 'loading commands');
		}
	}

	private isCommand(obj: any): obj is new (commandManager?: CommandManager) => Command {
		if (!obj) return false;

		const proto = obj.prototype;
		if (!proto) return false;

		const hasExecute = 'execute' in proto;
		return hasExecute;
	}

	public getCommand(name: string): Command | null {
		const commandName = name.toLowerCase();
		return this.commands.get(commandName) || this.commands.get(this.aliases.get(commandName) || '') || null;
	}

	public getAllCommands(): Command[] {
		return Array.from(this.commands.values());
	}

	public async executeCommand(commandName: string, context: CommandContext): Promise<boolean> {
		const command = this.getCommand(commandName);

		if (!command) {
			return false;
		}

		try {
			await command.execute(context);
			return true;
		} catch (error) {
			await ErrorUtils.handleError(error, `executing command ${commandName}`, context.message);
			return false;
		}
	}

	public getCommandList(): string {
		const commands = this.getAllCommands();
		const prefix = config.prefix || '!';
		return commands.map(cmd =>
			`**${cmd.name}**: ${cmd.description}\nUsage: \`${prefix}${cmd.usage}\``
		).join('\n');
	}
}

================================================
FILE: src/commands/ping.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";

export default class PingCommand extends BaseCommand {
	name = "ping";
	description = "Check bot latency";
	usage = "ping";

	async execute(context: CommandContext): Promise<void> {
		const sent = await context.message.reply('🏓 Pinging...');
		const timeDiff = sent.createdTimestamp - context.message.createdTimestamp;
		await sent.edit(`🏓 Pong! Latency: ${timeDiff}ms`);
	}
}

================================================
FILE: src/commands/play.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";
import { MediaService } from "../services/media.js";
import { ErrorUtils, GeneralUtils } from '../utils/shared.js';
import fs from 'fs';
import path from 'path';
import config from "../config.js";

export default class PlayCommand extends BaseCommand {
	name = "play";
	description = "Play local video, URL, or search YouTube videos";
	usage = "play <video_name|url|search_query>";

	private mediaService: MediaService;

	constructor() {
		super();
		this.mediaService = new MediaService();
	}

	async execute(context: CommandContext): Promise<void> {
		const input = context.args.join(' ');

		if (!input) {
			await this.sendError(context.message, 'Please provide a video name, URL, or search query.');
			return;
		}

		// Check if input is a URL (YouTube, Twitch, or direct link)
		if (GeneralUtils.isValidUrl(input)) {
			await this.handleUrl(context, input);
		} else {
			// Refresh video list from disk before matching
			const videoFiles = fs.readdirSync(config.videosDir);
			const refreshedVideos = videoFiles.map(file => ({
				name: path.parse(file).name,
				path: path.join(config.videosDir, file)
			}));
			context.videos.length = 0;
			context.videos.push(...refreshedVideos);

			// Case-insensitive match
			const video = context.videos.find(m => m.name.toLowerCase() === input.toLowerCase());

			if (video) {
				await this.handleLocalVideo(context, video);
			} else {
				// Treat as search query
				await this.handleSearchQuery(context, input);
			}
		}
	}


	private async handleLocalVideo(context: CommandContext, video: any): Promise<void> {
		// Add to queue instead of playing immediately
		const success = await context.streamingService.addToQueue(context.message, video.path, video.name);

		if (success) {
			// If not currently playing, start playing from queue
			if (!context.streamStatus.playing) {
				await context.streamingService.playFromQueue(context.message);
			}
		}
	}

	private async handleUrl(context: CommandContext, url: string): Promise<void> {
		try {
			// For lazy processing, just add to queue - resolution happens when playing
			const success = await context.streamingService.addToQueue(context.message, url);

			if (success) {
				// If not currently playing, start playing from queue
				if (!context.streamStatus.playing) {
					await context.streamingService.playFromQueue(context.message);
				}
			}
		} catch (error) {
			await ErrorUtils.handleError(error, `processing URL: ${url}`, context.message);
		}
	}

	private async handleSearchQuery(context: CommandContext, query: string): Promise<void> {
		try {
			// For lazy processing, add the search query to queue
			// The actual search and resolution will happen when it's time to play
			const success = await context.streamingService.addToQueue(context.message, query, `Search: ${query}`);

			if (success) {
				// If not currently playing, start playing from queue
				if (!context.streamStatus.playing) {
					await context.streamingService.playFromQueue(context.message);
				}
			}
		} catch (error) {
			await ErrorUtils.handleError(error, 'adding search query to queue', context.message);
		}
	}
}

================================================
FILE: src/commands/preview.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";
import { MessageAttachment } from "discord.js-selfbot-v13";
import path from 'path';
import { ffmpegScreenshot } from "../utils/ffmpeg.js";
import logger from '../utils/logger.js';

export default class PreviewCommand extends BaseCommand {
	name = "preview";
	description = "Generate preview thumbnails for a video";
	usage = "preview <video_name>";

	async execute(context: CommandContext): Promise<void> {
		const vid = context.args.join(' ');

		if (!vid) {
			await this.sendError(context.message, 'Please provide a video name.');
			return;
		}

		const vid_name = context.videos.find(m => m.name === vid);

		if (!vid_name) {
			await this.sendError(context.message, 'Video not found');
			return;
		}

		// React with camera emoji
		context.message.react('📸');

		// Reply with message to indicate that the preview is being generated
		context.message.reply('📸 **Generating preview thumbnails...**');

		try {
			const videoFilename = vid_name.name + path.extname(vid_name.path);
			const thumbnails = await ffmpegScreenshot(videoFilename);

			if (thumbnails.length > 0) {
				const attachments: MessageAttachment[] = [];
				for (const screenshotPath of thumbnails) {
					attachments.push(new MessageAttachment(screenshotPath));
				}

				// Message content
				const content = `📸 **Preview**: \`${vid_name.name}\``;

				// Send message with attachments
				await context.message.reply({
					content,
					files: attachments
				});
			} else {
				await this.sendError(context.message, 'Failed to generate preview thumbnails.');
			}
		} catch (error) {
			logger.error('Error generating preview thumbnails:', error);
			await this.sendError(context.message, 'Failed to generate preview thumbnails.');
		}
	}
}

================================================
FILE: src/commands/queue.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";

export default class QueueCommand extends BaseCommand {
	name = "queue";
	description = "Display the current video queue";
	usage = "queue";

	async execute(context: CommandContext): Promise<void> {
		const queueItems = context.streamingService.getQueueService().getQueue();
		const currentItem = context.streamingService.getQueueService().getCurrent();
		const queueStatus = context.streamingService.getQueueService().getQueueStatus();

		if (queueItems.length === 0) {
			await this.sendInfo(context.message, 'Queue', 'The queue is currently empty.');
			return;
		}

		let queueText = `📋 **Queue** (${queueItems.length} item${queueItems.length !== 1 ? 's' : ''})\n\n`;

		if (queueStatus.isPlaying && currentItem) {
			const status = currentItem.resolved ? '▶️' : '⏳';
			const title = currentItem.resolved ? currentItem.title : `${currentItem.title} (resolving...)`;
			queueText += `${status} **Currently Playing:**\n\`${title}\` (requested by ${currentItem.requestedBy})\n\n`;
		}

		queueText += '**Up Next:**\n';

		// Show all items that are not currently playing
		const upcomingItems = queueItems.filter(item => !queueStatus.isPlaying || item.id !== currentItem?.id);

		if (upcomingItems.length === 0) {
			if (queueStatus.isPlaying && currentItem) {
				queueText += '*No upcoming items*\n';
			} else {
				queueText += '*Queue is empty*\n';
			}
		} else {
			upcomingItems.forEach((item, index) => {
				const position = queueStatus.isPlaying ? index + 1 : index;
				const addedTime = item.addedAt.toLocaleTimeString();
				const status = item.resolved ? '' : '⏳';
				const title = item.resolved ? item.title : `${item.title} (pending)`;
				queueText += `${position + 1}. ${status} \`${title}\` (by ${item.requestedBy}) - Added at ${addedTime}\n`;
			});
		}

		// Split message if it's too long (Discord has a 2000 character limit)
		if (queueText.length > 1900) {
			const firstPart = queueText.substring(0, 1900) + '...';
			const secondPart = '...(continued)\n' + queueText.substring(1900);

			await context.message.channel.send(firstPart);
			await context.message.channel.send(secondPart);
		} else {
			await context.message.channel.send(queueText);
		}
	}
}

================================================
FILE: src/commands/skip.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";

export default class SkipCommand extends BaseCommand {
	name = "skip";
	description = "Skip the currently playing video";
	usage = "skip";
	aliases = ["next"];

	async execute(context: CommandContext): Promise<void> {
		const currentItem = context.streamingService.getQueueService().getCurrent();
		const queueLength = context.streamingService.getQueueService().getLength();

		if (!context.streamStatus.playing) {
			await this.sendError(context.message, 'No video is currently playing.');
			return;
		}

		if (queueLength === 0) {
			await this.sendError(context.message, 'No videos in queue to skip to.');
			return;
		}

		// Skip the current video
		await context.streamingService.skipCurrent(context.message);
	}
}

================================================
FILE: src/commands/status.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";

export default class StatusCommand extends BaseCommand {
	name = "status";
	description = "Show current streaming status";
	usage = "status";

	async execute(context: CommandContext): Promise<void> {
		await this.sendInfo(context.message, 'Status',
			`Joined: ${context.streamStatus.joined}\nPlaying: ${context.streamStatus.playing}`);
	}
}

================================================
FILE: src/commands/stop.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";
import logger from '../utils/logger.js';

export default class StopCommand extends BaseCommand {
 	name = "stop";
 	description = "Stop current video playback and clear queue";
 	usage = "stop";
	aliases = ["leave", "s"];

	async execute(context: CommandContext): Promise<void> {
		if (!context.streamStatus.joined) {
			await this.sendError(context.message, '**Already Stopped!**');
			return;
		}

		try {
			context.streamStatus.manualStop = true;

			await this.sendSuccess(context.message, 'Stopped playing video and cleared queue.');
			logger.info("Stopped playing video and cleared queue.");

			// Use streaming service to handle the stop and clear queue
			await context.streamingService.stopAndClearQueue();

		} catch (error) {
			logger.error("Error during force termination:", error);
		}
	}
}

================================================
FILE: src/commands/ytsearch.ts
================================================
import { BaseCommand } from "./base.js";
import { CommandContext } from "../types/index.js";
import { MediaService } from "../services/media.js";

export default class YTSearchCommand extends BaseCommand {
	name = "ytsearch";
	description = "Search for videos on YouTube";
	usage = "ytsearch <query>";

	private mediaService: MediaService;

	constructor() {
		super();
		this.mediaService = new MediaService();
	}

	async execute(context: CommandContext): Promise<void> {
		const query = context.args.join(' ');

		if (!query) {
			await this.sendError(context.message, 'Please provide a search query.');
			return;
		}

		try {
			const searchResults = await this.mediaService.searchYouTube(query);
			if (searchResults.length > 0) {
				await this.sendList(context.message, searchResults, "ytsearch");
			} else {
				await this.sendError(context.message, 'No videos found.');
			}
		} catch (error) {
			await this.sendError(context.message, 'Failed to search for videos.');
		}
	}
}

================================================
FILE: src/config.ts
================================================
import dotenv from "dotenv"

dotenv.config({ quiet: true });

const VALID_VIDEO_CODECS = ['VP8', 'H264', 'H265', 'VP9', 'AV1'];

export function parseVideoCodec(value: string): "VP8" | "H264" | "H265" {
	if (typeof value === "string") {
		value = value.trim().toUpperCase();
	}
	if (VALID_VIDEO_CODECS.includes(value)) {
		return value as "VP8" | "H264" | "H265";
	}
	return "H264";
}

export function parsePreset(value: string): "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow" {
	if (typeof value === "string") {
		value = value.trim().toLowerCase();
	}
	switch (value) {
		case "ultrafast":
		case "superfast":
		case "veryfast":
		case "faster":
		case "fast":
		case "medium":
		case "slow":
		case "slower":
		case "veryslow":
			return value as "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow";
		default:
			return "ultrafast";
	}
}

export function parseBoolean(value: string | undefined): boolean {
	if (typeof value === "string") {
		value = value.trim().toLowerCase();
	}
	switch (value) {
		case "true":
			return true;
		default:
			return false;
	}
}

function parseAdminIds(value: string): string[] {
	try {
		// Try to parse as JSON array first
		const parsed = JSON.parse(value);
		if (Array.isArray(parsed)) {
			return parsed.filter(id => typeof id === 'string' && id.trim() !== '');
		}
	} catch {
		// If not JSON, try comma-separated values
		if (value.includes(',')) {
			return value.split(',').map(id => id.trim()).filter(id => id !== '');
		}
	}
	// Single value
	return value.trim() ? [value.trim()] : [];
}

export default {
	// Selfbot options
	token: process.env.TOKEN || '',
	prefix: process.env.PREFIX || '',
	guildId: process.env.GUILD_ID ? process.env.GUILD_ID : '',
	cmdChannelId: process.env.COMMAND_CHANNEL_ID ? process.env.COMMAND_CHANNEL_ID : '',
	videoChannelId: process.env.VIDEO_CHANNEL_ID ? process.env.VIDEO_CHANNEL_ID : '',
	adminIds: process.env.ADMIN_IDS ? parseAdminIds(process.env.ADMIN_IDS) : [],

	// General options
	videosDir: process.env.VIDEOS_DIR ? process.env.VIDEOS_DIR : './videos',
	previewCacheDir: process.env.PREVIEW_CACHE_DIR ? process.env.PREVIEW_CACHE_DIR : './tmp/preview-cache',

	// yt-dlp options
	ytdlpCookiesPath: process.env.YTDLP_COOKIES_PATH ? process.env.YTDLP_COOKIES_PATH : '',

	// Stream options
	respect_video_params: process.env.STREAM_RESPECT_VIDEO_PARAMS ? parseBoolean(process.env.STREAM_RESPECT_VIDEO_PARAMS) : false,
	bitrateOverride: process.env.STREAM_BITRATE_OVERRIDE ? parseBoolean(process.env.STREAM_BITRATE_OVERRIDE) : false,
	width: process.env.STREAM_WIDTH ? parseInt(process.env.STREAM_WIDTH) : 1280,
	height: process.env.STREAM_HEIGHT ? parseInt(process.env.STREAM_HEIGHT) : 720,
	fps: process.env.STREAM_FPS ? parseInt(process.env.STREAM_FPS) : 30,
	bitrateKbps: process.env.STREAM_BITRATE_KBPS ? parseInt(process.env.STREAM_BITRATE_KBPS) : 1000,
	maxBitrateKbps: process.env.STREAM_MAX_BITRATE_KBPS ? parseInt(process.env.STREAM_MAX_BITRATE_KBPS) : 2500,
	maxWidth: process.env.STREAM_MAX_WIDTH ? parseInt(process.env.STREAM_MAX_WIDTH) : 0,
	maxHeight: process.env.STREAM_MAX_HEIGHT ? parseInt(process.env.STREAM_MAX_HEIGHT) : 0,
	hardwareAcceleratedDecoding: process.env.STREAM_HARDWARE_ACCELERATION ? parseBoolean(process.env.STREAM_HARDWARE_ACCELERATION) : false,
	h26xPreset: process.env.STREAM_H26X_PRESET ? parsePreset(process.env.STREAM_H26X_PRESET) : 'ultrafast',
	videoCodec: process.env.STREAM_VIDEO_CODEC ? parseVideoCodec(process.env.STREAM_VIDEO_CODEC) : 'H264',

	// Videos server options
	server_enabled: process.env.SERVER_ENABLED ? parseBoolean(process.env.SERVER_ENABLED) : false,
	server_username: process.env.SERVER_USERNAME ? process.env.SERVER_USERNAME : 'admin',
	server_password: process.env.SERVER_PASSWORD ? process.env.SERVER_PASSWORD : 'admin',
	server_port: parseInt(process.env.SERVER_PORT ? process.env.SERVER_PORT : '8080'),
}

================================================
FILE: src/events/client/ready.ts
================================================
import { Client, ActivityOptions } from "discord.js-selfbot-v13";
import logger from "../../utils/logger.js";
import { DiscordUtils } from "../../utils/shared.js";

export async function handleReady(client: Client): Promise<void> {
	if (client.user) {
		logger.info(`${client.user.tag} is ready`);
		client.user.setActivity(DiscordUtils.status_idle() as ActivityOptions);
	}
}

================================================
FILE: src/events/messageCreate.ts
================================================
import { Message } from "discord.js-selfbot-v13";
import { CommandManager } from "../commands/manager.js";
import { CommandContext, Video, StreamStatus } from "../types/index.js";
import config from "../config.js";

export async function handleMessageCreate(
	message: Message,
	videos: Video[],
	streamStatus: StreamStatus,
	streamingService: any,
	commandManager: CommandManager
): Promise<void> {
	// Ignore bots, self, non-command channels, and non-commands
	if (
		message.author.bot ||
		message.author.id === message.client.user?.id ||
		!message.content.startsWith(config.prefix)
	) return;

	// Split command and arguments
	const args = message.content.slice(config.prefix!.length).trim().split(/ +/);

	// If no command provided, ignore
	if (args.length === 0) {
		return;
	}

	const commandName = args.shift()!.toLowerCase();

	const context: CommandContext = {
		message,
		args,
		videos,
		streamStatus,
		streamingService
	};

	const executed = await commandManager.executeCommand(commandName, context);

	if (!executed) {
		await message.react('❌');
		await message.reply(`❌ **Error**: Unknown command. Use \`${config.prefix}help\` to see available commands.`);
	}
}


================================================
FILE: src/events/voiceStateUpdate.ts
================================================
import { VoiceState } from "discord.js-selfbot-v13";
import { Client } from "discord.js-selfbot-v13";
import { DiscordUtils } from "../utils/shared.js";
import { StreamStatus } from "../types/index.js";

export async function handleVoiceStateUpdate(
	oldState: VoiceState,
	newState: VoiceState,
	streamStatus: StreamStatus,
	client: Client
): Promise<void> {
	// When exit channel
	if (oldState.member?.user.id == client.user?.id) {
		if (oldState.channelId && !newState.channelId) {
			streamStatus.joined = false;
			streamStatus.joinsucc = false;
			streamStatus.playing = false;
			streamStatus.channelInfo = {
				guildId: "",
				channelId: "",
				cmdChannelId: ""
			}
			client.user?.setActivity(DiscordUtils.status_idle());
		}
	}

	// When join channel success
	if (newState.member?.user.id == client.user?.id) {
		if (newState.channelId && !oldState.channelId) {
			streamStatus.joined = true;
			if (newState.guild.id == streamStatus.channelInfo.guildId && newState.channelId == streamStatus.channelInfo.channelId) {
				streamStatus.joinsucc = true;
			}
		}
	}
}

================================================
FILE: src/index.ts
================================================
import { Client } from "discord.js-selfbot-v13";
import config from "./config.js";
import fs from 'fs';
import path from 'path';
import logger from './utils/logger.js';
import { downloadExecutable, checkForUpdatesAndUpdate } from './utils/yt-dlp.js';

// Import event handlers
import { handleReady } from './events/client/ready.js';
import { handleMessageCreate } from './events/messageCreate.js';
import { handleVoiceStateUpdate } from './events/voiceStateUpdate.js';

// Import services
import { StreamingService } from './services/streaming.js';
import { MediaService } from './services/media.js';
import { CommandManager } from './commands/manager.js';
import { QueueService } from './services/queue.js';

// Download yt-dlp and check for updates
(async () => {
	try {
		await downloadExecutable();
		await checkForUpdatesAndUpdate();
	} catch (error) {
		logger.error("Error during initial yt-dlp setup/update:", error);
	}
})();

// Create a new instance of Client
const client = new Client();

// Stream status object
const queueService = new QueueService();
const streamStatus = {
	joined: false,
	joinsucc: false,
	playing: false,
	manualStop: false,
	channelInfo: {
		guildId: config.guildId,
		channelId: config.videoChannelId,
		cmdChannelId: config.cmdChannelId
	},
	queue: queueService.getQueueStatus()
}

// Create services
const streamingService = new StreamingService(client, streamStatus);
const mediaService = new MediaService();
const commandManager = new CommandManager();

// Create the videosFolder dir if it doesn't exist
if (!fs.existsSync(config.videosDir)) {
	fs.mkdirSync(config.videosDir);
}

// Create previewCache parent dir if it doesn't exist
if (!fs.existsSync(path.dirname(config.previewCacheDir))) {
	fs.mkdirSync(path.dirname(config.previewCacheDir), { recursive: true });
}

// Create the previewCache dir if it doesn't exist
if (!fs.existsSync(config.previewCacheDir)) {
	fs.mkdirSync(config.previewCacheDir);
}

// Get all video files
const videoFiles = fs.readdirSync(config.videosDir);

// Create an array of video objects
let videos = videoFiles.map(file => {
	const fileName = path.parse(file).name;
	return { name: fileName, path: path.join(config.videosDir, file) };
});

// Print available videos
if (videos.length > 0) {
	logger.info(`Available videos:\n${videos.map(m => m.name).join('\n')}`);
}

// Event handlers
client.on("ready", async () => {
	await handleReady(client);
});

client.on('voiceStateUpdate', async (oldState, newState) => {
	await handleVoiceStateUpdate(oldState, newState, streamStatus, client);
});

client.on('messageCreate', async (message) => {
	await handleMessageCreate(message, videos, streamStatus, streamingService, commandManager);
});

// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
	if (!(error instanceof Error && error.message.includes('SIGTERM'))) {
		logger.error('Uncaught Exception:', error);
		return
	}
});

// Run server if enabled in config
if (config.server_enabled) {
	// Run server.ts
	import('./server/index.js');
}

// Login to Discord
client.login(config.token);

================================================
FILE: src/server/index.ts
================================================
import express from "express";
import session from "express-session";
import expressLayouts from "express-ejs-layouts";
import path from "path";
import fs from "fs";
import config from "../config.js";
import logger from "../utils/logger.js";
import { stringify, prettySize } from "./utils/helpers.js";

// Import middleware
import { requireAuth } from "./middleware/auth.js";

// Import route handlers
import authRoutes from "./routes/auth.js";
import dashboardRoutes from "./routes/dashboard.js";
import uploadRoutes from "./routes/upload.js";
import previewRoutes from "./routes/preview.js";

const app = express();

// Configure EJS templating engine
app.set('view engine', 'ejs');
app.set('views', path.join(process.cwd(), 'src/server/views'));

// Use express-ejs-layouts
app.use(expressLayouts);
app.set('layout', 'layouts/main');

// Middleware
app.use(express.urlencoded({ extended: true }));
app.use(session({
	secret: 'streambot-2024',
	resave: false,
	saveUninitialized: true,
	cookie: { secure: process.env.NODE_ENV === 'production' }
}));

// Serve static files from public directory
app.use(express.static(path.join(process.cwd(), 'src/server/public')));

// Make helper functions available to all templates
app.use((req, res, next) => {
	res.locals.stringify = stringify;
	res.locals.prettySize = prettySize;
	next();
});

// Apply authentication middleware to all routes except login
app.use(requireAuth);

// Routes
app.use('/', authRoutes);
app.use('/', dashboardRoutes);
app.use('/', uploadRoutes);
app.use('/', previewRoutes);

// Create necessary directories
if (!fs.existsSync(config.videosDir)) {
	fs.mkdirSync(config.videosDir);
}

if (!fs.existsSync(path.dirname(config.previewCacheDir))) {
	fs.mkdirSync(path.dirname(config.previewCacheDir), { recursive: true });
}

if (!fs.existsSync(config.previewCacheDir)) {
	fs.mkdirSync(config.previewCacheDir);
}

// Start server
app.listen(config.server_port, () => {
	logger.info(`Server is running on port ${config.server_port}`);
});

export default app;

================================================
FILE: src/server/middleware/auth.ts
================================================
import { Request, Response, NextFunction } from 'express';

export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
	if ((req.session as { user?: unknown }).user) {
		next();
	} else {
		res.redirect("/login");
	}
};

export const requireAuth = (req: Request, res: Response, next: NextFunction) => {
	if (req.path === "/login") {
		next();
	} else {
		authMiddleware(req, res, next);
	}
};

================================================
FILE: src/server/middleware/multer.ts
================================================
import multer, { StorageEngine } from 'multer';
import path from 'path';
import config from '../../config.js';

// Define the type for the file object
interface MulterFile extends Express.Multer.File {
	originalname: string;
}

// Configure multer storage
export const storage: StorageEngine = multer.diskStorage({
	destination: (req: Express.Request, file: MulterFile, cb: (error: Error | null, destination: string) => void) => {
		cb(null, config.videosDir);
	},
	filename: (req: Express.Request, file: MulterFile, cb: (error: Error | null, filename: string) => void) => {
		cb(null, file.originalname);
	},
});

// Create the multer upload instance
export const upload = multer({ storage });

================================================
FILE: src/server/public/css/main.css
================================================
:root {
	--bs-body-color: #212529;
	--bs-body-bg: #f8f9fa;
	--bs-border-color: #dee2e6;
}

.dark-mode {
	--bs-body-color: #f8f9fa;
	--bs-body-bg: #212529;
	--bs-border-color: #495057;
}

body {
	color: var(--bs-body-color);
	background-color: var(--bs-body-bg);
	transition: background-color 0.3s ease, color 0.3s ease;
}

.card,
.modal-content {
	background-color: var(--bs-body-bg);
	border-color: var(--bs-border-color);
	box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}

.table {
	color: var(--bs-body-color);
	font-size: 0.875rem;
}


.theme-toggle {
	position: fixed;
	top: 20px;
	right: 20px;
	z-index: 1001;
}

.logout-button {
	position: fixed;
	top: 20px;
	right: 90px;
	z-index: 1001;
}

.dark-mode .nav-tabs .nav-link.active {
	color: #fff;
	background-color: #495057;
	border-color: #495057;
}

.dark-mode .table-striped>tbody>tr:nth-of-type(odd) {
	color: #fff;
	background-color: rgba(255, 255, 255, 0.05);
}

.dark-mode .table-striped>tbody>tr:nth-of-type(even) {
	background-color: rgba(255, 255, 255, 0.02);
}

.dark-mode .table td {
	color: #f8f9fa;
	border-color: #495057;
}

.dark-mode .table th {
	color: #f8f9fa;
	border-color: #495057;
	background-color: #343a40;
}

.dark-mode .btn-outline-primary,
.dark-mode .btn-outline-secondary,
.dark-mode .btn-outline-danger {
	color: #fff;
}

.dark-mode input[type="file"] {
	color: var(--bs-body-color);
}

.dark-mode input[type="file"]::file-selector-button {
	color: var(--bs-body-color);
	background-color: var(--bs-body-bg);
	border-color: var(--bs-border-color);
}

.dark-mode input[type="file"]::file-selector-button:hover {
	color: var(--bs-body-color);
	background-color: rgba(255, 255, 255, 0.1);
	border-color: var(--bs-border-color);
}

.dark-mode input[type="file"]::file-selector-button:focus {
	color: var(--bs-body-color);
	background-color: rgba(255, 255, 255, 0.15);
	border-color: #80bdff;
	box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}


input[type="file"].user-interacted:invalid {
	border-color: #dc3545;
}

input[type="file"]:valid {
	border-color: #28a745;
}


input[type="file"] {
	border-color: var(--bs-border-color);
}


input[type="file"]:not(.user-interacted) {
	border-color: var(--bs-border-color) !important;
}

.form-text {
	font-size: 0.875rem;
	margin-top: 0.25rem;
}


.modal-content {
	border: none;
	border-radius: 0.5rem;
	box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}

.modal-header {
	border-bottom: 1px solid var(--bs-border-color);
	border-radius: 0.5rem 0.5rem 0 0;
	padding: 1rem 1.5rem;
}

.modal-header .btn-close {
	position: absolute;
	right: 1rem;
	top: 50%;
	transform: translateY(-50%);
	margin: 0;
	padding: 0.5rem;
	border-radius: 0.25rem;
}

.modal-body {
	padding: 1.5rem;
	text-align: center;
}

.modal-footer {
	border-top: 1px solid var(--bs-border-color);
	border-radius: 0 0 0.5rem 0.5rem;
	padding: 1rem 1.5rem;
}

.modal-footer .btn {
	min-width: 100px;
}

.dark-mode .modal-content {
	background-color: var(--bs-body-bg);
	color: var(--bs-body-color);
}

.dark-mode .modal-header {
	background-color: var(--bs-body-bg);
	border-color: var(--bs-border-color);
	color: var(--bs-body-color);
}

.dark-mode .modal-body {
	background-color: var(--bs-body-bg);
	color: var(--bs-body-color);
}

.dark-mode .modal-footer {
	background-color: var(--bs-body-bg);
	border-color: var(--bs-border-color);
}

.dark-mode .modal-backdrop {
	background-color: rgba(0, 0, 0, 0.8);
}


.dark-mode #deleteFileName {
	color: #f8f9fa !important;
}

.dark-mode .modal .modal-title {
	color: #f8f9fa;
}

.dark-mode .modal .text-muted {
	color: #adb5bd !important;
}

.dark-mode .modal .modal-body h6 {
	color: #f8f9fa;
}



.modal-header .btn-close:hover {
	background-color: rgba(0, 0, 0, 0.1);
}

.dark-mode .modal-header .btn-close:hover {
	background-color: rgba(255, 255, 255, 0.1);
}


.modal.fade .modal-dialog {
	transition: transform 0.3s ease-out;
	transform: translate(0, -50px);
}

.modal.show .modal-dialog {
	transform: none;
}

@media (prefers-reduced-motion: reduce) {
	.modal.fade .modal-dialog {
		transition: none;
	}
}


.dark-mode input::placeholder {
	color: #6c757d;
}

.dark-mode .form-control {
	color: #f8f9fa;
	background-color: #343a40;
	border-color: #495057;
}

.dark-mode .form-control:focus {
	color: #f8f9fa;
	background-color: #343a40;
	border-color: #80bdff;
	box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}

.dark-mode .text-muted {
	color: #adb5bd !important;
}

.dark-mode .card-header {
	color: #f8f9fa;
	background-color: #495057;
	border-bottom: 1px solid #6c757d;
}

.dark-mode .card-title {
	color: #f8f9fa;
}

.card {
	border: 1px solid rgba(255, 255, 255, 0.125);
	transition: transform 0.3s ease-in-out;
}

.card:hover {
	transform: translateY(-5px);
}


.table td {
	word-break: break-word;
}

#imageSlider {
	max-width: 100%;
	height: auto;
}

.carousel-item img {
	object-fit: contain;
	height: 400px;
	background-color: #000;
	transition: opacity 0.3s ease-in-out;
}

.carousel-item img[loading="lazy"] {
	opacity: 0;
}

.carousel-item img[loading="lazy"].loaded {
	opacity: 1;
}


.toast-container {
	position: fixed;
	top: 20px;
	right: 20px;
	z-index: 9999;
	max-width: 400px;
	pointer-events: none;
}

@media (max-width: 576px) {
	.toast-container {
		left: 20px;
		right: 20px;
		max-width: none;
	}
}


.toast {
	display: flex;
	align-items: flex-start;
	min-width: 300px;
	max-width: 400px;
	margin-bottom: 10px;
	padding: 16px 20px;
	border-radius: 12px;
	box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
	border: 1px solid transparent;
	backdrop-filter: blur(10px);
	pointer-events: auto;
	transform: translateX(100%);
	transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
	position: relative;
	overflow: hidden;
}

.toast.show {
	transform: translateX(0);
	animation: toastSlideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.toast.removing {
	transform: translateX(100%);
	opacity: 0;
	animation: toastSlideOut 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.toast.success {
	background-color: #d1e7dd;
	border-color: #badbcc;
	color: #0f5132;
}

.toast.error {
	background-color: #f8d7da;
	border-color: #f5c2c7;
	color: #721c24;
}

.toast.warning {
	background-color: #fff3cd;
	border-color: #ffecb5;
	color: #664d03;
}

.toast.info {
	background-color: #d1ecf1;
	border-color: #bee5eb;
	color: #0c5460;
}

.toast.dark-mode {
	background-color: rgba(33, 37, 41, 0.95);
	border-color: rgba(73, 80, 87, 0.5);
	color: #f8f9fa;
}

.toast.dark-mode.success {
	background-color: rgba(25, 135, 84, 0.2);
	border-color: rgba(25, 135, 84, 0.3);
	color: #d1e7dd;
}

.toast.dark-mode.error {
	background-color: rgba(220, 53, 69, 0.2);
	border-color: rgba(220, 53, 69, 0.3);
	color: #f8d7da;
}

.toast.dark-mode.warning {
	background-color: rgba(255, 193, 7, 0.2);
	border-color: rgba(255, 193, 7, 0.3);
	color: #fff3cd;
}

.toast.dark-mode.info {
	background-color: rgba(13, 202, 240, 0.2);
	border-color: rgba(13, 202, 240, 0.3);
	color: #d1ecf1;
}


.toast-icon {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 24px;
	height: 24px;
	margin-right: 12px;
	margin-top: 2px;
	flex-shrink: 0;
}

.toast-icon i {
	font-size: 16px;
}

.toast.success .toast-icon {
	color: #198754;
}

.toast.error .toast-icon {
	color: #dc3545;
}

.toast.warning .toast-icon {
	color: #ffc107;
}

.toast.info .toast-icon {
	color: #0dcaf0;
}

.toast.dark-mode .toast-icon {
	color: #f8f9fa;
}


.toast-content {
	flex: 1;
	font-size: 14px;
	line-height: 1.4;
	font-weight: 500;
}

.toast-title {
	font-weight: 600;
	margin-bottom: 4px;
	font-size: 15px;
}

.toast-message {
	font-weight: 400;
	margin: 0;
}


.toast-close {
	background: none;
	border: none;
	color: currentColor;
	opacity: 0.6;
	cursor: pointer;
	padding: 4px;
	margin-left: 12px;
	border-radius: 4px;
	transition: opacity 0.2s ease;
	flex-shrink: 0;
}

.toast-close:hover {
	opacity: 1;
	background-color: rgba(0, 0, 0, 0.1);
}

.toast.dark-mode .toast-close:hover {
	background-color: rgba(255, 255, 255, 0.1);
}

.toast-close i {
	font-size: 14px;
}


.toast-progress {
	position: absolute;
	bottom: 0;
	left: 0;
	height: 3px;
	background-color: currentColor;
	opacity: 0.3;
	transition: width linear;
	border-radius: 0 0 12px 12px;
}


@keyframes toastSlideIn {
	from {
		transform: translateX(100%);
		opacity: 0;
	}

	to {
		transform: translateX(0);
		opacity: 1;
	}
}

@keyframes toastSlideOut {
	from {
		transform: translateX(0);
		opacity: 1;
	}

	to {
		transform: translateX(100%);
		opacity: 0;
	}
}



@media (prefers-reduced-motion: reduce) {
	.toast {
		transition: none;
		animation: none;
	}

	.toast.show,
	.toast.removing {
		transform: translateX(0);
	}
}

.btn-icon {
	padding: 0.25rem 0.5rem;
	font-size: 0.875rem;
	line-height: 1.5;
	border-radius: 0.2rem;
}

.btn-icon svg,
.btn-icon i {
	width: 1rem;
	height: 1rem;
	font-size: 1rem;
}

.login-bg {
	background-color: var(--bs-body-bg);
	min-height: 100vh;
}

.login-card {
	background-color: var(--bs-body-bg);
	border-color: var(--bs-border-color);
}

.login-card .card-body {
	padding: 2rem;
}

.login-logo {
	width: 64px;
	height: 64px;
	margin-bottom: 1rem;
}

.form-control,
.input-group-text {
	background-color: var(--bs-body-bg);
	border-color: var(--bs-border-color);
	color: var(--bs-body-color);
}

.form-control:focus {
	background-color: var(--bs-body-bg);
	color: var(--bs-body-color);
}

.btn-primary {
	background-color: #007bff;
	border-color: #007bff;
}

.btn-primary:hover {
	background-color: #0056b3;
	border-color: #0056b3;
}

.dark-mode .btn-outline-primary:hover {
	color: #fff;
	background-color: #0d6efd;
	border-color: #0d6efd;
}

.dark-mode .btn-outline-secondary:hover {
	color: #fff;
	background-color: #6c757d;
	border-color: #6c757d;
}

.dark-mode .btn-outline-danger:hover {
	color: #fff;
	background-color: #dc3545;
	border-color: #dc3545;
}

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

.spinner-border-sm {
	width: 1rem;
	height: 1rem;
}

.loading-overlay {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	background-color: rgba(0, 0, 0, 0.7);
	display: none;
	align-items: center;
	justify-content: center;
	z-index: 9999;
	color: white;
}

.loading-overlay.show {
	display: flex;
}

.progress-indicator {
	background-color: #007bff;
	height: 4px;
	position: fixed;
	top: 0;
	left: 0;
	transition: width 0.3s ease;
	z-index: 10000;
}


.btn:focus,
.form-control:focus,
.nav-link:focus {
	outline: 2px solid #007bff;
	outline-offset: 2px;
	box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}


.skip-link {
	position: absolute;
	top: -40px;
	left: 6px;
	background: #000;
	color: #fff;
	padding: 8px;
	text-decoration: none;
	z-index: 10000;
}

.skip-link:focus {
	top: 6px;
}


@media (prefers-contrast: high) {
	:root {
		--bs-body-color: #000000;
		--bs-body-bg: #ffffff;
		--bs-border-color: #000000;
	}

	.dark-mode {
		--bs-body-color: #ffffff;
		--bs-body-bg: #000000;
		--bs-border-color: #ffffff;
	}
}


.progress {
	border-radius: 0.375rem;
	box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}

.progress-bar {
	transition: width 0.3s ease;
	font-weight: 600;
	display: flex;
	align-items: center;
	justify-content: center;
}

.progress-bar.bg-info {
	background-color: #0dcaf0 !important;
}

#localProgressText,
#remoteProgressText {
	font-size: 0.875rem;
	font-weight: 600;
}


.upload-form.uploading .btn-primary {
	pointer-events: none;
}

.upload-form.uploading .btn-secondary:not(.d-none) {
	display: inline-flex !important;
}


.dark-mode .progress {
	background-color: rgba(255, 255, 255, 0.1);
}

.dark-mode .progress-bar {
	background-color: #0d6efd;
}

.dark-mode .progress-bar.bg-info {
	background-color: #0dcaf0 !important;
}

.dark-mode .progress-bar.bg-success {
	background-color: #198754 !important;
}


.image-container {
	position: relative;
	width: 100%;
	height: 400px;
	background-color: #000;
	overflow: hidden;
}

.image-loading,
.image-error {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	display: flex;
	align-items: center;
	justify-content: center;
	background-color: #000;
	color: #fff;
}

.image-loading.show {
	display: flex;
}

.image-error.show {
	display: flex;
}

.preview-image {
	width: 100%;
	height: 100%;
	object-fit: contain;
	background-color: #000;
	transition: opacity 0.3s ease-in-out;
}

.preview-image.loaded {
	opacity: 1;
}

.preview-image.loading {
	opacity: 0;
}

.preview-image.error {
	display: none;
}

.spinner-border {
	width: 3rem;
	height: 3rem;
}

.image-loading .spinner-border {
	animation: spinner-border 0.75s linear infinite;
}

@keyframes spinner-border {
	to {
		transform: rotate(360deg);
	}
}

.dark-mode .image-loading,
.dark-mode .image-error {
	background-color: #000;
	color: #f8f9fa;
}

.dark-mode .image-loading .text-muted,
.dark-mode .image-error .text-muted {
	color: #adb5bd !important;
}



@media (prefers-reduced-motion: reduce) {
	* {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
	}

	.card:hover {
		transform: none;
	}

	.spinner-border {
		animation: none;
	}
}

================================================
FILE: src/server/public/js/main.js
================================================

const themeToggle = document.getElementById('themeToggle');
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');

function setTheme(isDark) {
	document.body.classList.toggle('dark-mode', isDark);
	const icon = themeToggle.querySelector('i');
	if (icon) {
		icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
	}
}

setTheme(prefersDarkScheme.matches);

themeToggle.addEventListener('click', () => {
	document.body.classList.toggle('dark-mode');
	setTheme(document.body.classList.contains('dark-mode'));
});

prefersDarkScheme.addEventListener('change', (e) => setTheme(e.matches));

class ToastManager {
	constructor() {
		this.container = document.getElementById('toast-container');
		this.toasts = new Set();
		this.queue = [];
		this.maxToasts = 5;
		this.defaultDuration = 4000;
		this.init();
	}

	init() {
		if (!this.container) {
			console.error('Toast container not found');
			return;
		}

		
		if (!this.container) {
			this.container = document.createElement('div');
			this.container.id = 'toast-container';
			this.container.className = 'toast-container';
			this.container.setAttribute('role', 'region');
			this.container.setAttribute('aria-label', 'Notifications');
			this.container.setAttribute('aria-live', 'polite');
			document.body.appendChild(this.container);
		}
	}

	getIcon(type) {
		const icons = {
			success: 'fas fa-check-circle',
			error: 'fas fa-exclamation-circle',
			warning: 'fas fa-exclamation-triangle',
			info: 'fas fa-info-circle'
		};
		return icons[type] || icons.info;
	}

	getAriaRole(type) {
		return type === 'error' ? 'alert' : 'status';
	}

	createToast(message, type = 'info', title = '', duration = null) {
		const toastId = 'toast-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);

		const toast = document.createElement('div');
		toast.id = toastId;
		toast.className = `toast ${type}`;
		toast.setAttribute('role', this.getAriaRole(type));
		toast.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite');
		toast.setAttribute('aria-atomic', 'true');

		
		if (document.body.classList.contains('dark-mode')) {
			toast.classList.add('dark-mode');
		}

		const content = `
			<div class="toast-icon">
				<i class="${this.getIcon(type)}" aria-hidden="true"></i>
			</div>
			<div class="toast-content">
				${title ? `<div class="toast-title">${title}</div>` : ''}
				<div class="toast-message">${message}</div>
			</div>
			<button class="toast-close" onclick="toastManager.removeToast('${toastId}')" aria-label="Close notification">
				<i class="fas fa-times" aria-hidden="true"></i>
			</button>
			<div class="toast-progress" style="width: 100%"></div>
		`;

		toast.innerHTML = content;
		this.container.appendChild(toast);

		
		setTimeout(() => {
			toast.classList.add('show');
		}, 10);

		
		const progressBar = toast.querySelector('.toast-progress');
		if (progressBar) {
			progressBar.style.transition = `width ${duration || this.defaultDuration}ms linear`;
			setTimeout(() => {
				progressBar.style.width = '0%';
			}, 10);
		}

		
		const autoRemove = setTimeout(() => {
			this.removeToast(toastId);
		}, duration || this.defaultDuration);

		
		const toastObj = {
			id: toastId,
			element: toast,
			timer: autoRemove
		};

		this.toasts.add(toastObj);

		
		this.manageQueue();

		return toastId;
	}

	removeToast(toastId) {
		const toast = document.getElementById(toastId);
		if (!toast) return;

		
		const toastObj = Array.from(this.toasts).find(t => t.id === toastId);
		if (toastObj) {
			clearTimeout(toastObj.timer);
			this.toasts.delete(toastObj);
		}

		
		toast.classList.add('removing');
		setTimeout(() => {
			if (toast.parentNode) {
				toast.parentNode.removeChild(toast);
			}
		}, 300);

		
		this.processQueue();
	}

	manageQueue() {
		
		if (this.toasts.size >= this.maxToasts) {
			const oldestToast = this.toasts.values().next().value;
			if (oldestToast) {
				this.removeToast(oldestToast.id);
			}
		}
	}

	processQueue() {
		
		if (this.queue.length > 0 && this.toasts.size < this.maxToasts) {
			const nextToast = this.queue.shift();
			this.createToast(...nextToast);
		}
	}

	
	success(message, title = 'Success', duration = null) {
		return this.createToast(message, 'success', title, duration);
	}

	error(message, title = 'Error', duration = null) {
		return this.createToast(message, 'error', title, duration);
	}

	warning(message, title = 'Warning', duration = null) {
		return this.createToast(message, 'warning', title, duration);
	}

	info(message, title = 'Info', duration = null) {
		return this.createToast(message, 'info', title, duration);
	}
}


const toastManager = new ToastManager();


function showToast(message, type = 'info', title = '', duration = null) {
	return toastManager.createToast(message, type, title, duration);
}


function showSuccessToast(message, title = 'Success', duration = null) {
	return toastManager.success(message, title, duration);
}

function showErrorToast(message, title = 'Error', duration = null) {
	return toastManager.error(message, title, duration);
}

function showWarningToast(message, title = 'Warning', duration = null) {
	return toastManager.warning(message, title, duration);
}

function showInfoToast(message, title = 'Info', duration = null) {
	return toastManager.info(message, title, duration);
}

function copyFileName(name) {
	// Try modern clipboard API first
	if (navigator.clipboard && window.isSecureContext) {
		navigator.clipboard.writeText(name).then(() => {
			showSuccessToast(name + " copied to clipboard!", "Copied!");
		}).catch(async err => {
			console.error('Failed to copy text: ', err);
			await fallbackCopyTextToClipboard(name);
		});
	} else {
		fallbackCopyTextToClipboard(name);
	}
}

async function fallbackCopyTextToClipboard(text) {
	const textArea = document.createElement("textarea");
	textArea.value = text;

	// Avoid scrolling to bottom
	textArea.style.top = "0";
	textArea.style.left = "0";
	textArea.style.position = "fixed";
	textArea.style.opacity = "0";

	document.body.appendChild(textArea);
	textArea.focus();
	textArea.select();

	try {
		// Use modern clipboard API if available, even in fallback
		if (navigator.clipboard && window.isSecureContext) {
			await navigator.clipboard.writeText(text);
			showSuccessToast(text + " copied to clipboard!", "Copied!");
		} else {
			// Final fallback: select and inform user to copy manually
			const selection = window.getSelection();
			const range = document.createRange();
			range.selectNodeContents(textArea);
			selection.removeAllRanges();
			selection.addRange(range);
			textArea.setSelectionRange(0, text.length);

			// Show info message that user needs to manually copy
			showInfoToast("Text selected - please copy manually (Ctrl+C or Cmd+C)", "Manual Copy");
		}
	} catch (err) {
		console.error('Fallback: Could not copy text: ', err);
		showErrorToast("Failed to copy to clipboard", "Copy Error");
	}

	document.body.removeChild(textArea);
}


function initLazyLoading() {
	if ('IntersectionObserver' in window) {
		const imageObserver = new IntersectionObserver((entries, observer) => {
			entries.forEach(entry => {
				if (entry.isIntersecting) {
					const img = entry.target;
					img.classList.add('loaded');
					observer.unobserve(img);
				}
			});
		});

		
		document.querySelectorAll('img[loading="lazy"]').forEach(img => {
			imageObserver.observe(img);
		});
	} else {
		
		document.querySelectorAll('img[loading="lazy"]').forEach(img => {
			img.classList.add('loaded');
		});
	}
}


document.addEventListener('DOMContentLoaded', initLazyLoading);


function initFormLoadingStates() {
	
	const localForm = document.getElementById('localUploadForm');
	const localBtn = document.getElementById('localUploadBtn');
	const localSpinner = localBtn?.querySelector('.spinner-border');

	if (localForm && localBtn && localSpinner) {
		localForm.addEventListener('submit', function (e) {
			localBtn.disabled = true;
			localSpinner.classList.remove('d-none');
			localBtn.querySelector('span:not(.spinner-border)').textContent = 'Uploading...';
		});
	}

	
	const remoteForm = document.getElementById('remoteUploadForm');
	const remoteBtn = document.getElementById('remoteUploadBtn');
	const remoteSpinner = remoteBtn?.querySelector('.spinner-border');

	if (remoteForm && remoteBtn && remoteSpinner) {
		remoteForm.addEventListener('submit', function (e) {
			remoteBtn.disabled = true;
			remoteSpinner.classList.remove('d-none');
			remoteBtn.querySelector('span:not(.spinner-border)').textContent = 'Uploading...';
		});
	}
}


function showGlobalLoading(message = 'Loading...') {
	const overlay = document.getElementById('loadingOverlay');
	const text = document.getElementById('loadingText');
	const progress = document.getElementById('progressIndicator');

	if (text) text.textContent = message;
	if (overlay) overlay.classList.add('show');
	if (progress) progress.style.width = '0%';

	
	if (faviconManager) faviconManager.showLoading();
}

function hideGlobalLoading() {
	const overlay = document.getElementById('loadingOverlay');
	const progress = document.getElementById('progressIndicator');

	if (overlay) overlay.classList.remove('show');
	if (progress) progress.style.width = '0%';

	
	if (faviconManager) faviconManager.hideLoading();
}

function updateProgress(percentage) {
	const progress = document.getElementById('progressIndicator');
	if (progress) {
		progress.style.width = Math.min(100, Math.max(0, percentage)) + '%';
	}
}


function validateVideoFile(file) {
	const videoTypes = [
		'video/mp4',
		'video/avi',
		'video/mov',
		'video/quicktime',
		'video/x-msvideo',
		'video/webm',
		'video/mkv',
		'video/x-matroska',
		'video/3gpp',
		'video/x-flv'
	];

	
	if (videoTypes.includes(file.type)) {
		return true;
	}

	
	const videoExtensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.3gp', '.m4v', '.asf', '.rm', '.vob'];
	const fileName = file.name.toLowerCase();
	const hasVideoExtension = videoExtensions.some(ext => fileName.endsWith(ext));

	return hasVideoExtension;
}


document.addEventListener('DOMContentLoaded', function () {
	initFormLoadingStates();

	
	const localFileInput = document.getElementById('localFileInput');
	if (localFileInput) {
		
		localFileInput.addEventListener('focus', function (e) {
			e.target.classList.add('user-interacted');
		});

		localFileInput.addEventListener('change', function (e) {
			const file = e.target.files[0];
			if (file && !validateVideoFile(file)) {
				showWarningToast('Please select a valid video file (MP4, AVI, MOV, MKV, WebM, etc.)', 'Invalid File Type');
				e.target.value = '';
			}
		});
	}

	
	document.addEventListener('keydown', function (e) {
		const deleteModal = document.getElementById('deleteModal');
		const modal = bootstrap.Modal.getInstance(deleteModal);

		
		if (e.key === 'Escape' && modal && deleteModal.classList.contains('show')) {
			modal.hide();
		}
	});
});


function showDeleteModal(fileName, deleteUrl) {
	const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
	const fileNameElement = document.getElementById('deleteFileName');
	const confirmBtn = document.getElementById('confirmDeleteBtn');

	
	fileNameElement.textContent = fileName;

	
	confirmBtn.href = deleteUrl;

	
	confirmBtn.addEventListener('click', function (e) {
		
		setTimeout(() => {
			
			modal.hide();
		}, 100);
	});

	
	modal.show();
}


function handleDeleteSuccess() {
	const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
	if (modal) {
		modal.hide();
	}
	showSuccessToast('Video file deleted successfully!', 'File Deleted');
	
	setTimeout(() => {
		window.location.reload();
	}, 1500);
}


let localUploadXHR = null;
let remoteUploadXHR = null;


function startLocalUpload() {
	const fileInput = document.getElementById('localFileInput');
	const progressContainer = document.getElementById('localProgressContainer');
	const progressBar = document.getElementById('localProgressBar');
	const progressText = document.getElementById('localProgressText');
	const progressStatus = document.getElementById('localProgressStatus');
	const uploadBtn = document.getElementById('localUploadBtn');
	const cancelBtn = document.getElementById('localCancelBtn');

	if (!fileInput.files[0]) {
		showWarningToast('Please select a file first', 'No File Selected');
		return;
	}

	const file = fileInput.files[0];
	const formData = new FormData();
	formData.append('file', file);

	
	progressContainer.classList.remove('d-none');
	uploadBtn.classList.add('d-none');
	cancelBtn.classList.remove('d-none');

	
	localUploadXHR = new XMLHttpRequest();

	localUploadXHR.upload.addEventListener('progress', function (e) {
		if (e.lengthComputable) {
			const percentComplete = Math.round((e.loaded / e.total) * 100);
			progressBar.style.width = percentComplete + '%';
			progressText.textContent = percentComplete + '%';
			progressStatus.textContent = `Uploading... ${formatFileSize(e.loaded)} / ${formatFileSize(e.total)}`;
		}
	});

	localUploadXHR.addEventListener('load', function () {
		try {
			if (localUploadXHR.status === 200) {
				const response = JSON.parse(localUploadXHR.responseText);
				progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
				progressBar.classList.add('bg-success');
				progressText.textContent = '100%';
				progressStatus.textContent = 'Upload completed successfully!';

				showSuccessToast('File uploaded successfully!', 'Upload Complete');
				setTimeout(() => {
					window.location.reload();
				}, 2000);
			} else {
				showErrorToast('Upload failed. Please try again.', 'Upload Error');
				resetLocalUpload();
			}
		} catch (error) {
			console.error('Error processing upload response:', error);
			showErrorToast('Upload failed due to processing error.', 'Upload Error');
			resetLocalUpload();
		}
	});

	localUploadXHR.addEventListener('error', function () {
		showErrorToast('Upload failed. Please check your connection.', 'Connection Error');
		resetLocalUpload();
	});

	localUploadXHR.open('POST', '/api/upload');
	localUploadXHR.send(formData);
}

function cancelLocalUpload() {
	if (localUploadXHR) {
		localUploadXHR.abort();
		// Clean up event listeners to prevent memory leaks
		localUploadXHR.onload = null;
		localUploadXHR.onerror = null;
		localUploadXHR.upload.onprogress = null;
	}
	resetLocalUpload();
	showInfoToast('Upload cancelled', 'Upload Cancelled');
}

function resetLocalUpload() {
	const progressContainer = document.getElementById('localProgressContainer');
	const progressBar = document.getElementById('localProgressBar');
	const progressText = document.getElementById('localProgressText');
	const progressStatus = document.getElementById('localProgressStatus');
	const uploadBtn = document.getElementById('localUploadBtn');
	const cancelBtn = document.getElementById('localCancelBtn');

	progressContainer.classList.add('d-none');
	progressBar.style.width = '0%';
	progressBar.classList.add('progress-bar-animated', 'progress-bar-striped');
	progressBar.classList.remove('bg-success');
	progressText.textContent = '0%';
	progressStatus.textContent = 'Starting upload...';
	uploadBtn.classList.remove('d-none');
	cancelBtn.classList.add('d-none');

	localUploadXHR = null;
}


function startRemoteUpload() {
	const urlInput = document.getElementById('remoteFileInput');
	const progressContainer = document.getElementById('remoteProgressContainer');
	const progressBar = document.getElementById('remoteProgressBar');
	const progressText = document.getElementById('remoteProgressText');
	const progressStatus = document.getElementById('remoteProgressStatus');
	const uploadBtn = document.getElementById('remoteUploadBtn');
	const cancelBtn = document.getElementById('remoteCancelBtn');

	if (!urlInput.value) {
		showWarningToast('Please enter a valid URL', 'Missing URL');
		return;
	}

	
	progressContainer.classList.remove('d-none');
	uploadBtn.classList.add('d-none');
	cancelBtn.classList.remove('d-none');

	
	let progress = 0;
	const progressInterval = setInterval(() => {
		progress += Math.random() * 15; 

		if (progress >= 90) {
			progress = 90; 
			clearInterval(progressInterval);
		}

		progressBar.style.width = Math.round(progress) + '%';
		progressText.textContent = Math.round(progress) + '%';
		progressStatus.textContent = 'Downloading...';
	}, 500);

	
	const formData = new FormData();
	formData.append('link', urlInput.value);

	remoteUploadXHR = new XMLHttpRequest();

	remoteUploadXHR.addEventListener('load', function () {
		clearInterval(progressInterval);

		try {
			if (remoteUploadXHR.status === 200) {
				const response = JSON.parse(remoteUploadXHR.responseText);
				progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
				progressBar.classList.add('bg-success');
				progressBar.style.width = '100%';
				progressText.textContent = '100%';
				progressStatus.textContent = 'Download completed successfully!';

				showSuccessToast('Remote file downloaded successfully!', 'Download Complete');
				setTimeout(() => {
					window.location.reload();
				}, 2000);
			} else {
				showErrorToast('Download failed. Please check the URL.', 'Download Error');
				resetRemoteUpload();
			}
		} catch (error) {
			console.error('Error processing download response:', error);
			showErrorToast('Download failed due to processing error.', 'Download Error');
			resetRemoteUpload();
		}
	});

	remoteUploadXHR.addEventListener('error', function () {
		clearInterval(progressInterval);
		showErrorToast('Download failed. Please check your connection.', 'Connection Error');
		resetRemoteUpload();
	});

	remoteUploadXHR.open('POST', '/api/remote_upload');
	remoteUploadXHR.send(formData);
}

function cancelRemoteUpload() {
	if (remoteUploadXHR) {
		remoteUploadXHR.abort();
		// Clean up event listeners to prevent memory leaks
		remoteUploadXHR.onload = null;
		remoteUploadXHR.onerror = null;
	}
	resetRemoteUpload();
	showInfoToast('Download cancelled', 'Download Cancelled');
}

function resetRemoteUpload() {
	const progressContainer = document.getElementById('remoteProgressContainer');
	const progressBar = document.getElementById('remoteProgressBar');
	const progressText = document.getElementById('remoteProgressText');
	const progressStatus = document.getElementById('remoteProgressStatus');
	const uploadBtn = document.getElementById('remoteUploadBtn');
	const cancelBtn = document.getElementById('remoteCancelBtn');

	progressContainer.classList.add('d-none');
	progressBar.style.width = '0%';
	progressBar.classList.add('progress-bar-animated', 'progress-bar-striped');
	progressBar.classList.remove('bg-success');
	progressText.textContent = '0%';
	progressStatus.textContent = 'Starting download...';
	uploadBtn.classList.remove('d-none');
	cancelBtn.classList.add('d-none');

	remoteUploadXHR = null;
}


function formatFileSize(bytes) {
	if (bytes === 0) return '0 Bytes';
	const k = 1024;
	const sizes = ['Bytes', 'KB', 'MB', 'GB'];
	const i = Math.floor(Math.log(bytes) / Math.log(k));
	return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}


class PreviewImageManager {
	constructor() {
		this.init();
	}

	init() {
		this.setupImageLoading();
		this.setupLazyLoading();
	}

	setupImageLoading() {
		const imageContainers = document.querySelectorAll('.image-container');

		imageContainers.forEach((container, index) => {
			const img = container.querySelector('.preview-image');
			const loadingDiv = container.querySelector('.image-loading');
			const errorDiv = container.querySelector('.image-error');

			if (!img) return;

			
			this.showLoadingState(container);

			
			img.addEventListener('load', () => {
				this.showImageState(container);
				img.classList.add('loaded');
			});

			
			img.addEventListener('error', () => {
				this.showErrorState(container);
			});

			
			if (img.complete && img.naturalHeight !== 0) {
				this.showImageState(container);
				img.classList.add('loaded');
			}
		});
	}

	setupLazyLoading() {
		
		if ('IntersectionObserver' in window) {
			const imageObserver = new IntersectionObserver((entries, observer) => {
				entries.forEach(entry => {
					if (entry.isIntersecting) {
						const container = entry.target;
						const img = container.querySelector('.preview-image');

						if (img && !img.src) {
							
							const carouselItem = container.closest('.carousel-item');
							if (carouselItem) {
								const previewUrl = carouselItem.getAttribute('data-preview-url');
								if (previewUrl) {
									img.src = previewUrl;
									this.showLoadingState(container);
								}
							}
						}

						observer.unobserve(container);
					}
				});
			});

			
			document.querySelectorAll('.image-container').forEach(container => {
				imageObserver.observe(container);
			});
		}
	}

	showLoadingState(container) {
		const img = container.querySelector('.preview-image');
		const loadingDiv = container.querySelector('.image-loading');
		const errorDiv = container.querySelector('.image-error');

		if (loadingDiv) loadingDiv.classList.remove('d-none');
		if (loadingDiv) loadingDiv.classList.add('show');
		if (errorDiv) errorDiv.classList.add('d-none');
		if (errorDiv) errorDiv.classList.remove('show');
		if (img) img.style.display = 'none';
	}

	showImageState(container) {
		const img = container.querySelector('.preview-image');
		const loadingDiv = container.querySelector('.image-loading');
		const errorDiv = container.querySelector('.image-error');

		if (loadingDiv) loadingDiv.classList.add('d-none');
		if (loadingDiv) loadingDiv.classList.remove('show');
		if (errorDiv) errorDiv.classList.add('d-none');
		if (errorDiv) errorDiv.classList.remove('show');
		if (img) img.style.display = 'block';
	}

	showErrorState(container) {
		const img = container.querySelector('.preview-image');
		const loadingDiv = container.querySelector('.image-loading');
		const errorDiv = container.querySelector('.image-error');

		if (loadingDiv) loadingDiv.classList.add('d-none');
		if (loadingDiv) loadingDiv.classList.remove('show');
		if (errorDiv) errorDiv.classList.remove('d-none');
		if (errorDiv) errorDiv.classList.add('show');
		if (img) img.style.display = 'none';
	}

	retryLoad(container) {
		const img = container.querySelector('.preview-image');
		const carouselItem = container.closest('.carousel-item');

		if (img && carouselItem) {
			const previewUrl = carouselItem.getAttribute('data-preview-url');
			if (previewUrl) {
				
				img.src = '';
				this.showLoadingState(container);

				
				setTimeout(() => {
					img.src = previewUrl;
				}, 500);
			}
		}
	}
}


const previewImageManager = new PreviewImageManager();


class FaviconManager {
	constructor() {
		this.faviconSvg = '/favicon.svg';
		this.faviconPng = '/favicon-32x32.png';
		this.init();
	}

	init() {
		this.updateFavicon();
		this.setupThemeListener();
		this.setupLoadingListener();
	}

	updateFavicon() {
		const isDark = document.body.classList.contains('dark-mode');
		const favicon = document.querySelector('link[rel="icon"][type="image/svg+xml"]');

		if (favicon) {
			
			const timestamp = Date.now();
			favicon.href = `${this.faviconSvg}?t=${timestamp}`;
		}
	}

	setupThemeListener() {
		
		const observer = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
					this.updateFavicon();
				}
			});
		});

		observer.observe(document.body, {
			attributes: true,
			attributeFilter: ['class']
		});
	}

	setupLoadingListener() {
		
		document.body.classList.add('loading');

		
		window.addEventListener('load', () => {
			document.body.classList.remove('loading');
		});

		
		const originalFetch = window.fetch;
		window.fetch = function (...args) {
			document.body.classList.add('loading');
			return originalFetch.apply(this, args).finally(() => {
				
				setTimeout(() => {
					document.body.classList.remove('loading');
				}, 100);
			});
		};
	}

	showLoading() {
		document.body.classList.add('loading');
	}

	hideLoading() {
		document.body.classList.remove('loading');
	}
}

const faviconManager = new FaviconManager();



================================================
FILE: src/server/public/site.webmanifest
================================================
{
  "name": "StreamBot Video Manager",
  "short_name": "StreamBot",
  "description": "Upload, manage, and preview your video files",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/favicon-16x16.png",
      "sizes": "16x16",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/favicon-32x32.png",
      "sizes": "32x32",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/apple-touch-icon.png",
      "sizes": "180x180",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

================================================
FILE: src/server/routes/auth.ts
================================================
import { Router } from 'express';
import bcrypt from 'bcrypt';
import argon2 from 'argon2';
import config from '../../config.js';
import logger from '../../utils/logger.js';

const router = Router();

// Login route - GET
router.get("/login", (req, res) => {
	res.render('pages/login', {
		error: req.query.error === '1',
		showLogout: false
	});
});

// Login route - POST
router.post("/login", async (req, res) => {
	const { username, password } = req.body;
	
	let isPasswordMatch = false;
	
	// Check if the stored password is a hash or plain text
	if (config.server_password.startsWith('$argon2')) {
		// Argon2 hash
		try {
			isPasswordMatch = await argon2.verify(config.server_password, password);
		} catch (err) {
			logger.error("Error verifying argon2 password:", err);
			isPasswordMatch = false;
		}
	} else if (config.server_password.startsWith('$2')) {
		// Bcrypt hash
		isPasswordMatch = await bcrypt.compare(password, config.server_password);
	} else {
		// Plain text (not recommended)
		isPasswordMatch = password === config.server_password;
	}
	
	if (username === config.server_username && isPasswordMatch) {
		(req.session as { user?: unknown }).user = username;
		res.redirect("/");
	} else {
		res.redirect("/login?error=1");
	}
});

// Logout route
router.get("/logout", (req, res) => {
	req.session.destroy((err) => {
		if (err) {
			logger.error("Error destroying session:", err);
		}
		res.redirect("/login");
	});
});

export default router;

================================================
FILE: src/server/routes/dashboard.ts
================================================
import { Router } from 'express';
import fs from 'fs';
import path from 'path';
import config from '../../config.js';
import logger from '../../utils/logger.js';
import { prettySize } from '../utils/helpers.js';

const router = Router();

// Main dashboard route
router.get("/", (req, res) => {
	fs.readdir(config.videosDir, (err, files) => {
		if (err) {
			logger.error(err);
			res.status(500).send("Internal Server Error");
			return;
		}

		const fileList = files.map((file) => {
			const stats = fs.statSync(path.join(config.videosDir, file));
			return { name: file, size: prettySize(stats.size) };
		});

		res.render('pages/dashboard', {
			files: fileList,
			showLogout: true
		});
	});
});


export default router;

================================================
FILE: src/server/routes/preview.ts
================================================
import { Router } from 'express';
import fs from 'fs';
import path from 'path';
import ffmpeg from 'fluent-ffmpeg';
import config from '../../config.js';
import logger from '../../utils/logger.js';
import { ffmpegScreenshot } from '../../utils/ffmpeg.js';
import { stringify } from '../utils/helpers.js';

const router = Router();

// Preview route
router.get("/preview/:file", (req, res) => {
	const file = req.params.file;
	if (!fs.existsSync(path.join(config.videosDir, file))) {
		res.status(404).send("Not Found");
		return;
	}

	ffmpeg.ffprobe(`${config.videosDir}/${file}`, (err, metadata) => {
		if (err) {
			logger.error(err);
			res.status(500).send("Internal Server Error");
			return;
		}

		// Generate preview images
		const previews = [];
		for (let i = 1; i <= 5; i++) {
			previews.push(`/api/preview/${file}/${i}`);
		}

		res.render('pages/preview', {
			filename: file,
			metadata: metadata,
			previews: previews,
			showLogout: true
		});
	});
});

// Generate preview of video file using ffmpeg, cache it to previewCache and serve it
router.get("/api/preview/:file/:id", async (req, res) => {
	const file = req.params.file;
	const id = parseInt(req.params.id, 10);

	// id should be 1, 2, 3, 4 or 5
	if (id < 1 || id > 5) {
		res.status(404).send("Not Found");
		return;
	}

	// check if preview exists
	const previewFile = path.resolve(config.previewCacheDir, `${file}-${id}.jpg`);
	if (fs.existsSync(previewFile)) {
		res.sendFile(previewFile);
	} else {
		try {
			await ffmpegScreenshot(file);
		} catch (err) {
			logger.error(err);
			res.status(500).send("Internal Server Error");
			return;
		}
		res.sendFile(previewFile);
	}
});

// Delete route
router.get("/delete/:file", (req, res) => {
	const file = req.params.file;
	const filePath = path.join(config.videosDir, file);

	if (fs.existsSync(filePath)) {
		fs.unlink(filePath, (err) => {
			if (err) {
				logger.error(err);
				res.status(500).send("Internal Server Error");
			} else {
				res.redirect("/");
			}
		});
	} else {
		res.status(404).send("Not Found");
	}
});

export default router;

================================================
FILE: src/server/routes/upload.ts
================================================
import { Router } from 'express';
import axios from 'axios';
import https from 'https';
import fs from 'fs';
import path from 'path';
import config from '../../config.js';
import logger from '../../utils/logger.js';
import { upload } from '../middleware/multer.js';

const router = Router();
const agent = new https.Agent({ rejectUnauthorized: false });

// Upload route - local file with progress support
router.post("/api/upload", upload.single("file"), (req, res) => {
 	res.json({
 		success: true,
 		message: "File uploaded successfully",
 		filename: req.file.filename,
 		size: req.file.size
 	});
 });

// Upload route - remote file with progress support
router.post("/api/remote_upload", upload.single("link"), async (req, res) => {
 	const link = req.body.link;
 	const filename = link.substring(link.lastIndexOf('/') + 1);
 	const filepath = path.join(config.videosDir, filename);

 	try {
 		// First, get the file info to determine size
 		const headResponse = await axios.head(link, { httpsAgent: agent });
 		const totalSize = parseInt(headResponse.headers['content-length'], 10);

 		// Set up progress tracking
 		let downloaded = 0;
 		let lastProgressSent = 0;

 		const response = await axios.get(link, {
 			responseType: "stream",
 			httpsAgent: agent,
 			onDownloadProgress: (progressEvent) => {
 				downloaded = progressEvent.loaded;
 				const percentCompleted = Math.round((downloaded * 100) / totalSize);

 				// Send progress updates every 2%
 				if (percentCompleted - lastProgressSent >= 2) {
 					lastProgressSent = percentCompleted;
 					logger.info(`Remote download progress: ${percentCompleted}%`);
 				}
 			}
 		});

 		const writer = fs.createWriteStream(filepath);

 		response.data.on('data', (chunk) => {
 			downloaded += chunk.length;
 			const percentCompleted = Math.round((downloaded * 100) / totalSize);

 			// Send progress updates every 2%
 			if (percentCompleted - lastProgressSent >= 2) {
 				lastProgressSent = percentCompleted;
 				logger.info(`Remote download progress: ${percentCompleted}%`);
 			}
 		});

 		response.data.pipe(writer);

 		writer.on("finish", () => {
 			res.json({
 				success: true,
 				message: "Remote file downloaded successfully",
 				filename: filename,
 				size: downloaded
 			});
 		});

 		writer.on("error", (err) => {
 			logger.error(err);
 			res.status(500).json({ success: false, message: "Error downloading file" });
 		});
 	} catch (err) {
 		logger.error(err);
 		res.status(500).json({ success: false, message: "Error downloading file" });
 	}
 });

export default router;

================================================
FILE: src/server/utils/helpers.ts
================================================
// Helper function to stringify objects for display in templates
export function stringify(obj: any): string {
	// if string, return it
	if (typeof obj === "string") {
		return obj;
	}

	if (Array.isArray(obj)) {
		return `<ul>${obj.map(item => {
			return `<li>${stringify(item)}</li>`;
		}).join("")}</ul>`;
	} else {
		if (typeof obj === "object") {
			return `<ul>${Object.keys(obj).map(key => {
				return `<li>${key}: ${stringify(obj[key])}</li>`;
			}).join("")}</ul>`;
		} else {
			return String(obj);
		}
	}
}

// Helper function to format file size
export function prettySize(bytes: number): string {
	const units = ["B", "KB", "MB", "GB", "TB"];
	let i = 0;
	while (bytes >= 1024 && i < units.length - 1) {
		bytes /= 1024;
		i++;
	}
	return `${bytes.toFixed(2)} ${units[i]}`;
}

================================================
FILE: src/server/views/layouts/main.ejs
================================================
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta name="description" content="StreamBot Video Manager - Upload, manage, and preview your video files">
	<meta name="keywords" content="video, streaming, upload, manager, preview">
	<meta name="author" content="StreamBot">
	<meta name="robots" content="noindex, nofollow">

	<!-- Open Graph / Facebook -->
	<meta property="og:type" content="website">
	<meta property="og:title" content="StreamBot Video Manager">
	<meta property="og:description" content="Upload, manage, and preview your video files">
	<meta property="og:site_name" content="StreamBot">

	<!-- Twitter -->
	<meta property="twitter:card" content="summary_large_image">
	<meta property="twitter:title" content="StreamBot Video Manager">
	<meta property="twitter:description" content="Upload, manage, and preview your video files">

	<meta http-equiv="X-Content-Type-Options" content="nosniff">
	<meta http-equiv="X-Frame-Options" content="DENY">
	<meta http-equiv="X-XSS-Protection" content="1; mode=block">
	<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">

	<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/css/bootstrap.min.css" as="style" crossorigin>
	<link rel="preload" href="/css/main.css" as="style">
	<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.min.js" as="script" crossorigin>

	<link rel="dns-prefetch" href="//cdnjs.cloudflare.com">

	<!-- Favicon -->
	<link rel="icon" type="image/svg+xml" href="/favicon.svg">
	<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
	<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
	<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
	<link rel="manifest" href="/site.webmanifest">
	<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#007bff">
	<meta name="msapplication-TileColor" content="#007bff">
	<meta name="theme-color" content="#ffffff">

	<title>StreamBot Video Manager</title>

	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/css/bootstrap.min.css" integrity="sha512-2bBQCjcnw658Lho4nlXJcc6WkV/UxpE/sAokbXPxQNGqmNdQrWqtw26Ns9kFF/yG792pKR1Sx8/Y1Lf1XN4GKA==" crossorigin="anonymous" referrerpolicy="no-referrer">
	<link rel="stylesheet" href="/css/main.css">

	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
	<noscript><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"></noscript>
</head>
<body>
	<a href="#main-content" class="skip-link">Skip to main content</a>
	<div id="toast-container" class="toast-container" role="region" aria-label="Notifications" aria-live="polite"></div>
	<div id="loadingOverlay" class="loading-overlay" role="dialog" aria-modal="true" aria-labelledby="loadingText" aria-hidden="true">
		<div class="text-center">
			<div class="spinner-border text-light mb-3" role="status" aria-hidden="true">
				<span class="visually-hidden">Loading...</span>
			</div>
			<div id="loadingText">Loading...</div>
		</div>
	</div>
	<div class="progress-indicator" id="progressIndicator" style="width: 0%;" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Loading progress"></div>
	<div class="theme-toggle">
		<button id="themeToggle" class="btn btn-outline-primary" aria-label="Toggle dark mode" title="Toggle dark mode">
			<i class="fas fa-moon" aria-hidden="true"></i>
		</button>
	</div>

	<% if (typeof showLogout !== 'undefined' && showLogout) { %>
	<div class="logout-button">
		<a href="/logout" class="btn btn-danger">
			<i class="fas fa-sign-out-alt me-1" aria-hidden="true"></i>Logout
		</a>
	</div>
	<% } %>

	<%- body %>

	<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.8/js/bootstrap.min.js" integrity="sha512-nKXmKvJyiGQy343jatQlzDprflyB5c+tKCzGP3Uq67v+lmzfnZUi/ZT+fc6ITZfSC5HhaBKUIvr/nTLCV+7F+Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

	<script defer src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
	<script defer src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js" integrity="sha512-TPh2Oxlg1zp+kz3nFA0C5vVC6leG/6mm1z9+mA81MI5eaUVqasPLO8Cuk4gMF4gUfP5etR73rgU/8PNMsSesoQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
	<script defer src="/js/main.js"></script>

	<script>
		document.addEventListener('DOMContentLoaded', function() {
			// Show loading toast for better UX
			const toast = document.getElementById('toast');
			if (toast) {
				toast.style.display = 'none';
			}
		});
	</script>
</body>
</html>

================================================
FILE: src/server/views/pages/dashboard.ejs
================================================
<div class="container my-5" id="main-content">
	<div class="d-flex justify-content-between align-items-center mb-4">
		<h1 class="mb-0">StreamBot Video Manager</h1>
	</div>

	<ul class="nav nav-tabs mb-3" id="myTab" role="tablist">
		<li class="nav-item" role="presentation">
			<button class="nav-link active" id="list-tab"
				data-bs-toggle="tab" data-bs-target="#list" type="button" role="tab"
				aria-controls="list" aria-selected="true" aria-label="View file list">File List</button>
		</li>
		<li class="nav-item" role="presentation">
			<button class="nav-link" id="upload-tab"
				data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab"
				aria-controls="upload" aria-selected="false" aria-label="Upload new files">Upload</button>
		</li>
	</ul>

	<div class="tab-content" id="myTabContent">
		<div class="tab-pane fade show active" id="list" role="tabpanel" aria-labelledby="list-tab">
			<div class="card">
				<div class="card-header">
					<h5 class="card-title mb-0">Video Files</h5>
				</div>
				<div class="card-body">
					<div class="table-responsive">
						<table class="table table-striped" role="table" aria-label="Video files list">
							<thead>
								<tr role="row">
									<th scope="col" role="columnheader">Name</th>
									<th scope="col" role="columnheader">Size</th>
									<th scope="col" role="columnheader">Actions</th>
								</tr>
							</thead>
							<tbody>
								<% if (typeof files !== 'undefined' && files.length > 0) { %>
									<% files.forEach(function(file) { %>
									<tr>
										<td><%= file.name %></td>
										<td><%= file.size %></td>
										<td>
											<a href="/preview/<%= encodeURIComponent(file.name) %>" class="btn btn-sm btn-outline-primary me-1 btn-icon"
											   aria-label="Preview <%= file.name %>" title="Preview <%= file.name %>">
												<i class="fas fa-eye" aria-hidden="true"></i>
											</a>
											<button class="btn btn-sm btn-outline-secondary me-1 btn-icon"
													onclick="copyFileName('<%= file.name %>')"
													aria-label="Copy <%= file.name %> to clipboard"
													title="Copy <%= file.name %> to clipboard">
												<i class="fas fa-clipboard" aria-hidden="true"></i>
											</button>
											<button class="btn btn-sm btn-outline-danger btn-icon"
													aria-label="Delete <%= file.name %>"
													title="Delete <%= file.name %>"
													onclick="showDeleteModal('<%= file.name %>', '/delete/<%= encodeURIComponent(file.name) %>')">
												<i class="fas fa-trash" aria-hidden="true"></i>
											</button>
										</td>
									</tr>
									<% }); %>
								<% } else { %>
									<tr>
										<td colspan="3" class="text-center text-muted">No video files found</td>
									</tr>
								<% } %>
							</tbody>
						</table>
					</div>
				</div>
			</div>
		</div>
		<div class="tab-pane fade" id="upload" role="tabpanel" aria-labelledby="upload-tab">
			<div class="row">
				<div class="col-md-6 mb-3">
					<div class="card">
						<div class="card-header">
							<h5 class="card-title mb-0">Upload</h5>
						</div>
						<div class="card-body">
							<form action="/api/upload" method="post" enctype="multipart/form-data" id="localUploadForm">
								<div class="mb-3">
									<label for="localFileInput" class="form-label">Choose video file</label>
									<input type="file" class="form-control" id="localFileInput" name="file" accept="video/*" required>
								</div>
								<div class="mb-3 d-none" id="localProgressContainer">
									<label class="form-label">Upload Progress</label>
									<div class="progress" style="height: 25px;">
										<div class="progress-bar progress-bar-striped progress-bar-animated"
											 id="localProgressBar" role="progressbar" style="width: 0%">
											<span id="localProgressText">0%</span>
										</div>
									</div>
									<small class="text-muted" id="localProgressStatus">Starting upload...</small>
								</div>
								<div class="d-flex gap-2">
									<button type="button" class="btn btn-primary" id="localUploadBtn" onclick="startLocalUpload()">
										<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
										<i class="fas fa-upload me-1" aria-hidden="true"></i>Upload
									</button>
									<button type="button" class="btn btn-secondary d-none" id="localCancelBtn" onclick="cancelLocalUpload()">
										<i class="fas fa-times me-1" aria-hidden="true"></i>Cancel
									</button>
								</div>
							</form>
						</div>
					</div>
				</div>
				<div class="col-md-6 mb-3">
					<div class="card">
						<div class="card-header">
							<h5 class="card-title mb-0">Remote Upload</h5>
						</div>
						<div class="card-body">
							<form action="/api/remote_upload" method="post" enctype="multipart/form-data" id="remoteUploadForm">
								<div class="mb-3">
									<label for="remoteFileInput" class="form-label">Enter video URL</label>
									<input type="url" class="form-control" id="remoteFileInput" name="link" placeholder="https://example.com/video.mp4" required>
								</div>
								<div class="mb-3 d-none" id="remoteProgressContainer">
									<label class="form-label">Download Progress</label>
									<div class="progress" style="height: 25px;">
										<div class="progress-bar progress-bar-striped progress-bar-animated bg-info"
											 id="remoteProgressBar" role="progressbar" style="width: 0%">
											<span id="remoteProgressText">0%</span>
										</div>
									</div>
									<small class="text-muted" id="remoteProgressStatus">Starting download...</small>
								</div>
								<div class="d-flex gap-2">
									<button type="button" class="btn btn-primary" id="remoteUploadBtn" onclick="startRemoteUpload()">
										<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
										<i class="fas fa-download me-1" aria-hidden="true"></i>Download
									</button>
									<button type="button" class="btn btn-secondary d-none" id="remoteCancelBtn" onclick="cancelRemoteUpload()">
										<i class="fas fa-times me-1" aria-hidden="true"></i>Cancel
									</button>
								</div>
							</form>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>

	<!-- Delete Confirmation Modal -->
	<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
		<div class="modal-dialog modal-dialog-centered">
			<div class="modal-content">
				<div class="modal-header">
					<h5 class="modal-title" id="deleteModalLabel">
						<i class="fas fa-exclamation-triangle text-warning me-2" aria-hidden="true"></i>
						Confirm Deletion
					</h5>
					<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="margin: 0;"></button>
				</div>
				<div class="modal-body">
					<div class="text-center mb-3">
						<i class="fas fa-trash text-danger fa-3x mb-3" aria-hidden="true"></i>
						<h6 class="mb-3">Are you sure you want to delete this video file?</h6>
						<p class="mb-2 text-muted">
							<strong id="deleteFileName"></strong>
						</p>
						<small class="text-muted">This action cannot be undone.</small>
					</div>
				</div>
				<div class="modal-footer justify-content-center">
					<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
						<i class="fas fa-times me-1" aria-hidden="true"></i>Cancel
					</button>
					<a id="confirmDeleteBtn" href="#" class="btn btn-danger">
						<i class="fas fa-trash me-1" aria-hidden="true"></i>Delete File
					</a>
				</div>
			</div>
		</div>
	</div>
</div>

================================================
FILE: src/server/views/pages/login.ejs
================================================
<div class="container-fluid min-vh-100 d-flex align-items-center justify-content-center py-5 login-bg">
	<div class="card shadow-lg login-card" style="max-width: 400px; width: 100%;">
		<div class="card-body p-5">
			<div class="text-center mb-4">
				<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-camera-reels text-primary login-logo" viewBox="0 0 16 16">
					<path d="M6 3a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM1 3a2 2 0 1 0 4 0 2 2 0 0 0-4 0z"/>
					<path d="M9 6h.5a2 2 0 0 1 1.983 1.738l3.11-1.382A1 1 0 0 1 16 7.269v7.462a1 1 0 0 1-1.406.913l-3.111-1.382A2 2 0 0 1 9.5 16H2a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h7zm6 8.73V7.27l-3.5 1.555v4.35l3.5 1.556zM1 8v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1z"/>
					<path d="M9 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM7 3a2 2 0 1 1 4 0 2 2 0 0 1-4 0z"/>
				</svg>
				<h2 class="mt-3 mb-0 font-weight-bold">StreamBot Video Manager</h2>
				<p class="text-muted">Please sign in to continue</p>
			</div>
			<% if (typeof error !== 'undefined' && error) { %>
			<div class="alert alert-danger" role="alert">Invalid username or password</div>
			<% } %>
			<form action="/login" method="POST">
				<div class="mb-3">
					<label for="username" class="form-label">Username</label>
					<div class="input-group">
						<span class="input-group-text">
							<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
								<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
							</svg>
						</span>
						<input type="text" class="form-control" id="username" name="username" required autofocus>
					</div>
				</div>
				<div class="mb-3">
					<label for="password" class="form-label">Password</label>
					<div class="input-group">
						<span class="input-group-text">
							<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-lock" viewBox="0 0 16 16">
								<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z"/>
							</svg>
						</span>
						<input type="password" class="form-control" id="password" name="password" required>
					</div>
				</div>
				<div class="d-grid">
					<button type="submit" class="btn btn-primary btn-lg">Sign In</button>
				</div>
			</form>
		</div>
	</div>
</div>

================================================
FILE: src/server/views/pages/preview.ejs
================================================
<div class="container-fluid py-4">
	<h1 class="h3 mb-4">File Preview</h1>
	<h2 class="h5 mb-4"><%= filename %></h2>
	<div class="row">
		<div class="col-lg-4 mb-4">
			<div class="card">
				<div class="card-header">
					<h3 class="h5 mb-0">Metadata</h3>
				</div>
				<div class="card-body p-0">
					<div class="table-responsive">
						<table class="table table-striped table-sm mb-0">
							<tbody>
								<% if (typeof metadata !== 'undefined' && metadata.format) { %>
									<% Object.entries(metadata.format).forEach(function([key, value]) { %>
									<tr>
										<td class="fw-bold"><%= key %></td>
										<td><%- stringify(value) %></td>
									</tr>
									<% }); %>
								<% } %>
							</tbody>
						</table>
					</div>
				</div>
			</div>
		</div>
		<div class="col-lg-8">
			<div class="card">
				<div class="card-header">
					<h3 class="h5 mb-0">Preview</h3>
				</div>
				<div class="card-body p-0">
					<div id="imageSlider" class="carousel slide" data-bs-ride="carousel">
						<div class="carousel-inner">
							<% if (typeof previews !== 'undefined' && previews.length > 0) { %>
								<% previews.forEach(function(preview, index) { %>
								<div class="carousel-item <%= index === 0 ? 'active' : '' %>" data-preview-url="<%= preview %>">
									<div class="image-container" data-image-index="<%= index %>">
										<img src="<%= preview %>" class="d-block w-100 preview-image" alt="Preview <%= index + 1 %>" loading="<%= index < 2 ? 'eager' : 'lazy' %>" style="display: none;">
										<div class="image-loading d-none">
											<div class="d-flex flex-column align-items-center justify-content-center h-100">
												<div class="spinner-border text-primary mb-3" role="status">
													<span class="visually-hidden">Generating preview...</span>
												</div>
												<div class="text-center">
													<small class="text-muted">Generating preview image <%= index + 1 %>...</small>
												</div>
											</div>
										</div>
										<div class="image-error d-none">
											<div class="d-flex flex-column align-items-center justify-content-center h-100">
												<i class="fas fa-exclamation-triangle text-warning fa-3x mb-3" aria-hidden="true"></i>
												<div class="text-center mb-3">
													<small class="text-muted">Failed to load preview <%= index + 1 %></small>
												</div>
												<button class="btn btn-sm btn-outline-primary" onclick="previewImageManager.retryLoad(this.closest('.image-container'))">
													<i class="fas fa-redo me-1" aria-hidden="true"></i>Retry
												</button>
											</div>
										</div>
									</div>
								</div>
								<% }); %>
							<% } else { %>
								<div class="carousel-item active">
									<div class="d-flex align-items-center justify-content-center" style="height: 400px; background-color: #000; color: #fff;">
										<span>No preview available</span>
									</div>
								</div>
							<% } %>
						</div>
						<% if (typeof previews !== 'undefined' && previews.length > 1) { %>
						<button class="carousel-control-prev" type="button" data-bs-target="#imageSlider" data-bs-slide="prev">
							<span class="carousel-control-prev-icon" aria-hidden="true"></span>
							<span class="visually-hidden">Previous</span>
						</button>
						<button class="carousel-control-next" type="button" data-bs-target="#imageSlider" data-bs-slide="next">
							<span class="carousel-control-next-icon" aria-hidden="true"></span>
							<span class="visually-hidden">Next</span>
						</button>
						<% } %>
					</div>
				</div>
			</div>
		</div>
	</div>
	<div class="mt-4">
		<a href="/" class="btn btn-primary">Back to File List</a>
	</div>
</div>


================================================
FILE: src/services/media.ts
================================================
import { getStream, getVod } from 'twitch-m3u8';
import { TwitchStream, MediaSource } from '../types/index.js';
import config from "../config.js";
import logger from '../utils/logger.js';
import { Youtube } from '../utils/youtube.js';
import ytdl, { downloadToTempFile } from '../utils/yt-dlp.js';
import { GeneralUtils } from '../utils/shared.js';
import { YTResponse } from '../types/index.js';
import path from 'path';

export class MediaService {
	private youtube: Youtube;

	constructor() {
		this.youtube = new Youtube();
	}

	public async resolveMediaSource(url: string): Promise<MediaSource | null> {
		try {
			if (url.includes('youtube.com/') || url.includes('youtu.be/')) {
				return await this._resolveYouTubeSource(url);
			} else if (url.includes('twitch.tv/')) {
				return await this._resolveTwitchSource(url);
			} else if (GeneralUtils.isLocalFile(url)) {
				return this._resolveLocalSource(url);
			} else if (GeneralUtils.isValidUrl(url)) {
				return this._resolveDirectUrlSource(url);
			} else {
				return this.searchAndPlayYouTube(url);
			}

			return null;
		} catch (error) {
			logger.error("Failed to resolve media source:", error);
			return null;
		}
	}

	private async _resolveYouTubeSource(url: string): Promise<MediaSource | null> {
		const videoDetails = await this.youtube.getVideoInfo(url);
		if (!videoDetails) return null;

		const isLive = videoDetails.videoDetails?.isLiveContent || false;
		const streamUrl = isLive ? await this.youtube.getLiveStreamUrl(url) : url;

		if (streamUrl) {
			return {
				url: streamUrl,
				title: videoDetails.title,
				type: 'youtube',
				isLive: isLive,
			};
		}
		return null;
	}

	public async getTwitchStreamUrl(url: string): Promise<string | null> {
		try {
			// Handle VODs
			if (url.includes('/videos/')) {
				const vodId = url.split('/videos/').pop() as string;
				const vodInfo = await getVod(vodId);
				const vod = vodInfo.find((stream: TwitchStream) => stream.resolution === `${config.width}x${config.height}`) || vodInfo[0];
				if (vod?.url) {
					return vod.url;
				}
				logger.error("No VOD URL found");
				return null;
			} else {
				const twitchId = url.split('/').pop() as string;
				const streams = await getStream(twitchId);
				const stream = streams.find((stream: TwitchStream) => stream.resolution === `${config.width}x${config.height}`) || streams[0];
				if (stream?.url) {
					return stream.url;
				}
				logger.error("No Stream URL found");
				return null;
			}
		} catch (error) {
			logger.error("Failed to get Twitch stream URL:", error);
			return null;
		}
	}

	public async downloadYouTubeVideo(url: string): Promise<string | null> {
		try {
			const ytDlpDownloadOptions = {
				format: `bestvideo[height<=${config.height || 720}][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=${config.height || 720}]+bestaudio/best[height<=${config.height || 720}]/best`,
				noPlaylist: true,
			};

			const tempFilePath = await downloadToTempFile(url, ytDlpDownloadOptions);
			return tempFilePath;
		} catch (error) {
			logger.error("Failed to download YouTube video:", error);
			return null;
		}
	
	}

	private async _resolveTwitchSource(url: string): Promise<MediaSource | null> {
		const streamUrl = await this.getTwitchStreamUrl(url);
		if (streamUrl) {
			const twitchId = url.split('/').pop() as string;
			return {
				url: streamUrl,
				title: `twitch.tv/${twitchId}`,
				type: 'twitch'
			};
		}
		return null;
	}

	private _resolveLocalSource(url: string): MediaSource {
		return {
			url,
			title: path.basename(url, path.extname(url)),
			type: 'local'
		};
	}

	private async _resolveDirectUrlSource(url: string): Promise<MediaSource> {
		// First try to get metadata using yt-dlp
		try {
			const metadata = await ytdl(url, {
				dumpJson: true,
				skipDownload: true,
				noWarnings: true,
				quiet: true
			}) as YTResponse;

			// If yt-dlp succeeds, use the extracted metadata
			if (metadata && metadata.title) {
				// Get the best available format URL
				let streamUrl = url;
				if (metadata.formats && Array.isArray(metadata.formats) && metadata.formats.length > 0) {
					// Find the format with both audio and video, preferring higher quality
					const bestFormat = metadata.formats
						.filter((format) => format.url && format.ext !== 'm3u8') // Avoid HLS streams
						.sort((a, b) => {
							// Prefer formats with both audio and video
							const aScore = (a.vcodec && a.vcodec !== 'none' ? 1 : 0) + (a.acodec && a.acodec !== 'none' ? 1 : 0) + (a.height || 0) / 1000;
							const bScore = (b.vcodec && b.vcodec !== 'none' ? 1 : 0) + (b.acodec && b.acodec !== 'none' ? 1 : 0) + (b.height || 0) / 1000;
							return bScore - aScore;
						})[0];

					if (bestFormat && bestFormat.url) {
						streamUrl = bestFormat.url;
					}
				}

				return {
					url: streamUrl,
					title: metadata.title,
					type: 'url'
				};
			}
		} catch (error) {
			// yt-dlp failed, log debug info and continue to fallback
			logger.debug("yt-dlp failed to extract metadata for URL:", url, error);
		}

		// Fallback to original URL parsing logic
		let title = "Direct URL";
		try {
			const urlObj = new URL(url);
			const pathname = urlObj.pathname;
			const filename = pathname.split('/').pop();

			if (filename && filename.includes('.')) {
				title = decodeURIComponent(filename.replace(/\.[^/.]+$/, ""));
			} else if (pathname !== '/' && pathname.length > 1) {
				const pathSegment = pathname.split('/').pop();
				if (pathSegment) {
					title = decodeURIComponent(pathSegment);
				}
			}
		} catch (e) {
			logger.debug("Could not parse URL for title extraction:", url);
		}

		return {
			url,
			title,
			type: 'url'
		};
	}

	public async searchYouTube(query: string, limit: number = 5): Promise<string[]> {
		try {
			return await this.youtube.search(query, limit);
		} catch (error) {
			logger.error("Failed to search YouTube:", error);
			return [];
		}
	}

	public async searchAndPlayYouTube(query: string): Promise<MediaSource | null> {
		try {
			const searchResult = await this.youtube.searchAndGetPageUrl(query);
			if (searchResult.pageUrl && searchResult.title) {
				return {
					url: searchResult.pageUrl,
					title: searchResult.title,
					type: 'youtube'
				};
			}
			return null;
		} catch (error) {
			logger.error("Failed to search and play YouTube:", error);
			return null;
		}
	}
}

================================================
FILE: src/services/queue.ts
================================================
import { VideoQueue, QueueItem, MediaSource } from "../types/index.js";
import { MediaService } from "./media.js";
import logger from '../utils/logger.js';

export class QueueService {
	private mediaService: MediaService;
	private queue: VideoQueue;

	constructor() {
		this.mediaService = new MediaService();
		this.queue = {
			items: [],
			currentIndex: -1,
			isPlaying: false
		};
	}

	/**
	 * Add a video to the queue
	 */
	public async addToQueue(
		mediaSource: MediaSource,
		requestedBy: string,
		originalInput?: string
	): Promise<QueueItem> {
		return this.add(
			mediaSource.url,
			mediaSource.title,
			requestedBy,
			mediaSource.type,
			mediaSource.isLive,
			originalInput || mediaSource.url
		);
	}

	/**
	 * Add a media source to the queue
	 */
	public async add(
		url: string,
		title: string,
		requestedBy: string,
		type: 'youtube' | 'twitch' | 'local' | 'url' = 'url',
		isLive: boolean = false,
		originalInput?: string
	): Promise<QueueItem> {
		const queueItem: QueueItem = {
			id: this.generateId(),
			url,
			title,
			type,
			isLive,
			requestedBy,
			addedAt: new Date(),
			originalInput: originalInput || url,
			resolved: originalInput === url,
		};

		this.queue.items.push(queueItem);
		logger.info(`Added to queue: ${title} (requested by ${requestedBy}, resolved: ${queueItem.resolved})`);

		return queueItem;
	}

	/**
	 * Get the next item in the queue
	 */
	getNext(): QueueItem | null {
		if (this.queue.items.length === 0) {
			this.queue.currentIndex = -1;
			return null;
		}

		if (this.queue.currentIndex < this.queue.items.length - 1) {
			this.queue.currentIndex++;
			return this.queue.items[this.queue.currentIndex];
		}

		// No more items, reset currentIndex to -1
		this.queue.currentIndex = -1;
		return null;
	}

	/**
	 * Get the current playing item
	 */
	getCurrent(): QueueItem | null {
		if (this.queue.items.length === 0) {
			this.queue.currentIndex = -1;
			return null;
		}

		if (this.queue.currentIndex >= 0 && this.queue.currentIndex < this.queue.items.length) {
			return this.queue.items[this.queue.currentIndex];
		}

		// If currentIndex is invalid, try to find a valid index
		if (this.queue.items.length > 0) {
			// If currentIndex is too high, set it to the last item
			if (this.queue.currentIndex >= this.queue.items.length) {
				this.queue.currentIndex = this.queue.items.length - 1;
				return this.queue.items[this.queue.currentIndex];
			}
			// If currentIndex is negative, set it to 0
			if (this.queue.currentIndex < 0) {
				this.queue.currentIndex = 0;
				return this.queue.items[this.queue.currentIndex];
			}
		}

		return null;
	}

	/**
	 * Skip to the next item in the queue
	 */
	skip(): QueueItem | null {
		const currentItem = this.getCurrent();
		if (currentItem) {
			// Remove the current item since we're skipping it
			this.removeFromQueue(currentItem.id);
		}

		const nextItem = this.getNext();

		// Ensure currentIndex is valid after skip
		if (nextItem && this.queue.currentIndex >= 0) {
			// Verify the current item matches what we expect
			const verifyCurrent = this.getCurrent();
			if (!verifyCurrent || verifyCurrent.id !== nextItem.id) {
				const correctIndex = this.queue.items.findIndex(item => item.id === nextItem.id);
				if (correctIndex !== -1) {
					this.queue.currentIndex = correctIndex;
				}
			}
		}

		return nextItem;
	}

	/**
	 * Remove an item from the queue by ID
	 */
	removeFromQueue(id: string): boolean {
		const index = this.queue.items.findIndex(item => item.id === id);
		if (index !== -1) {
			this.queue.items.splice(index, 1);

			// Adjust current index if necessary
			if (index < this.queue.currentIndex) {
				// Removed item was before current item, decrement index
				this.queue.currentIndex--;
			} else if (index === this.queue.currentIndex) {
				// Removed the current item itself, decrement index
				this.queue.currentIndex--;
			}
			return true;
		}
		return false;
	}

	/**
	 * Clear the entire queue
	 */
	clearQueue(): void {
		this.queue.items = [];
		this.queue.currentIndex = -1;
		this.queue.isPlaying = false;
		logger.info('Queue cleared');
	}

	/**
	 * Reset the current index to -1 (no current item)
	 */
	resetCurrentIndex(): void {
		this.queue.currentIndex = -1;
	}

	/**
	 * Get all items in the queue
	 */
	getQueue(): QueueItem[] {
		return [...this.queue.items];
	}

	/**
	 * Get the queue status
	 */
	getQueueStatus(): VideoQueue {
		return { ...this.queue };
	}

	/**
	 * Set the playing state
	 */
	setPlaying(isPlaying: boolean): void {
		this.queue.isPlaying = isPlaying;
	}

	/**
	 * Check if the queue is empty
	 */
	isEmpty(): boolean {
		return this.queue.items.length === 0;
	}

	/**
	 * Get the queue length
	 */
	getLength(): number {
		return this.queue.items.length;
	}

	/**
	 * Move an item to a different position in the queue
	 */
	moveItem(fromIndex: number, toIndex: number): boolean {
		if (fromIndex < 0 || fromIndex >= this.queue.items.length ||
			toIndex < 0 || toIndex >= this.queue.items.length) {
			return false;
		}

		const item = this.queue.items.splice(fromIndex, 1)[0];
		this.queue.items.splice(toIndex, 0, item);

		// Adjust current index if necessary
		if (fromIndex === this.queue.currentIndex) {
			this.queue.currentIndex = toIndex;
		} else if (fromIndex < this.queue.currentIndex && toIndex >= this.queue.currentIndex) {
			this.queue.currentIndex--;
		} else if (fromIndex > this.queue.currentIndex && toIndex <= this.queue.currentIndex) {
			this.queue.currentIndex++;
		}

		return true;
	}

	/**
	 * Generate a unique ID for queue items
	 */
	private generateId(): string {
		return Date.now().toString(36) + Math.random().toString(36).substr(2);
	}
}

================================================
FILE: src/services/streaming.ts
================================================
import { Client, Message } from "discord.js-selfbot-v13";
import { Streamer, Utils, prepareStream, playStream } from "@dank074/discord-video-stream";
import fs from 'fs';
import config from "../config.js";
import { MediaService } from './media.js';
import { QueueService } from './queue.js';
import { getVideoParams } from "../utils/ffmpeg.js";
import logger from '../utils/logger.js';
import { DiscordUtils, ErrorUtils } from '../utils/shared.js';
import { QueueItem, StreamStatus } from '../types/index.js';

export class StreamingService {
 	private streamer: Streamer;
 	private mediaService: MediaService;
 	private queueService: QueueService;
 	private controller: AbortController | null = null;
 	private streamStatus: StreamStatus;
 	private failedVideos: Set<string> = new Set();
 	private isSkipping: boolean = false;

 	constructor(client: Client, streamStatus: StreamStatus) {
 		this.streamer = new Streamer(client);
 		this.mediaService = new MediaService();
 		this.queueService = new QueueService();
 		this.streamStatus = streamStatus;
 	}

	public getStreamer(): Streamer {
		return this.streamer;
	}

	public getQueueService(): QueueService {
		return this.queueService;
	}

	private markVideoAsFailed(videoSource: string): void {
		this.failedVideos.add(videoSource);
		logger.info(`Marked video as failed: ${videoSource}`);
	}

	public async addToQueue(
		message: Message,
		videoSource: string,
		title?: string
	): Promise<boolean> {
		try {
			const username = message.author.username;
			const mediaSource = await this.mediaService.resolveMediaSource(videoSource);

			if (mediaSource) {
				const queueItem = await this.queueService.addToQueue(mediaSource, username);
				await DiscordUtils.sendSuccess(message, `Added to queue: \`${queueItem.title}\``);
				return true;
			} else {
				// Fallback for unresolved sources
				const queueItem = await this.queueService.add(
					videoSource,
					title || videoSource,
					username,
					'url',
					false,
					videoSource
				);
				await DiscordUtils.sendSuccess(message, `Added to queue: \`${queueItem.title}\``);
				return true;
			}
		} catch (error) {
			await ErrorUtils.handleError(error, `adding to queue: ${videoSource}`, message);
			return false;
		}
	}


	public async playFromQueue(message: Message): Promise<void> {
		if (this.streamStatus.playing) {
			await DiscordUtils.sendError(message, 'Already playing a video. Use skip command to skip current video.');
			return;
		}

		const nextItem = this.queueService.getNext();
		if (!nextItem) {
			await DiscordUtils.sendError(message, 'Queue is empty.');
			return;
		}

		this.queueService.setPlaying(true);
		await this.playVideoFromQueueItem(message, nextItem);
	}

	public async skipCurrent(message: Message): Promise<void> {
		if (!this.streamStatus.playing) {
			await DiscordUtils.sendError(message, 'No video is currently playing.');
			return;
		}

		// Check if this is the last item in the queue
		const queueLength = this.queueService.getLength();
		const isLastItem = queueLength <= 1;

		// Prevent concurrent skip operations only if there are more items in queue
		if (this.isSkipping && !isLastItem) {
			await DiscordUtils.sendError(message, 'Skip already in progress.');
			return;
		}

		this.isSkipping = true;

		try {
			// Stop the current stream immediately
			this.streamStatus.manualStop = true;
			this.controller?.abort();
			this.streamer.stopStream();

			const currentItem = this.queueService.getCurrent(); // Get item being skipped
			const nextItem = this.queueService.skip(); // Advance the queue

			if (!nextItem) {
				// No more items in queue - stop playback and leave voice channel
				await DiscordUtils.sendInfo(message, 'Queue', 'No more videos in queue.');
				this.queueService.setPlaying(false);
				await this.cleanupStreamStatus();
				return;
			}

			const currentTitle = currentItem ? currentItem.title : 'current video';
			await DiscordUtils.sendInfo(message, 'Skipping', `Skipping \`${currentTitle}\`. Playing next: \`${nextItem.title}\``);

			// Reset manual stop flag since we're starting a new video
			this.streamStatus.manualStop = false;

			// Skip cleanup since we're playing the next item immediately
			await this.playVideoFromQueueItem(message, nextItem);
		} finally {
			this.isSkipping = false;
		}
	}

	private async playVideoFromQueueItem(message: Message, queueItem: QueueItem): Promise<void> {
		// Ensure queue is marked as playing
		this.queueService.setPlaying(true);

		// Collect video parameters if respect_video_params is enabled
		let videoParams = undefined;
		if (config.respect_video_params) {
			videoParams = await this.getVideoParameters(queueItem.url);
		}

		// Log playing video
		logger.info(`Playing from queue: ${queueItem.title} (${queueItem.url})`);

		// Use streaming service to play the video with video parameters
		await this.playVideo(message, queueItem.url, queueItem.title, videoParams);
	}

	private async getVideoParameters(videoUrl: string): Promise<{ width: number, height: number, fps?: number, bitrate?: number } | undefined> {
		try {
			const resolution = await getVideoParams(videoUrl);
			logger.info(`Video parameters: ${resolution.width}x${resolution.height}, FPS: ${resolution.fps || 'unknown'}, Bitrate: ${resolution.bitrate || 'unknown'}`);
			
			let bitrateKbps: number | undefined;
			if (resolution.bitrate) {
				bitrateKbps = Math.round(parseInt(resolution.bitrate) / 1000);
			}

			return {
				width: resolution.width,
				height: resolution.height,
				fps: resolution.fps,
				bitrate: bitrateKbps
			};
		} catch (error) {
			await ErrorUtils.handleError(error, 'determining video parameters');
			return undefined;
		}
	}

	private async ensureVoiceConnection(guildId: string, channelId: string, title?: string): Promise<void> {
		// Only join voice if not already connected
		if (!this.streamStatus.joined || !this.streamer.voiceConnection) {
			await this.streamer.joinVoice(guildId, channelId);
			this.streamStatus.joined = true;
		}
		this.streamStatus.playing = true;
		this.streamStatus.channelInfo = { guildId, channelId, cmdChannelId: config.cmdChannelId! };

		if (title) {
			this.streamer.client.user?.setActivity(DiscordUtils.status_watch(title));
		}

		// Wait for voice connection to be fully ready
		await new Promise(resolve => setTimeout(resolve, 2000));

		// Verify voice connection exists
		if (!this.streamer.voiceConnection) {
			throw new Error('Voice connection is not established');
		}
	}

	private setupStreamConfiguration(videoParams?: { width: number, height: number, fps?: number, bitrate?: number }): any {
		let width = videoParams?.width || config.width;
		let height = videoParams?.height || config.height;
		let frameRate = videoParams?.fps || config.fps;
		let bitrateVideo = config.bitrateKbps;

		// If respecting video params, use video bitrate unless overridden
		if (videoParams && videoParams.bitrate && !config.bitrateOverride) {
			bitrateVideo = videoParams.bitrate;
		}

		// Resolution capping
		if (config.maxWidth > 0 || config.maxHeight > 0) {
			const ratio = width / height;
			if (config.maxWidth > 0 && width > config.maxWidth) {
				width = config.maxWidth;
				height = Math.round(width / ratio);
			}
			if (config.maxHeight > 0 && height > config.maxHeight) {
				height = config.maxHeight;
				width = Math.round(height * ratio);
			}
			// Ensure even dimensions
			width = Math.round(width / 2) * 2;
			height = Math.round(height / 2) * 2;
		}

		return {
			width,
			height,
			frameRate,
			bitrateVideo,
			bitrateVideoMax: config.maxBitrateKbps,
			videoCodec: Utils.normalizeVideoCodec(config.videoCodec),
			hardwareAcceleratedDecoding: config.hardwareAcceleratedDecoding,
			minimizeLatency: false,
			h26xPreset: config.h26xPreset
		};
	}

	private async executeStream(inputForFfmpeg: any, streamOpts: any, message: Message, title: string, videoSource: string): Promise<void> {
		const { command, output: ffmpegOutput } = prepareStream(inputForFfmpeg, streamOpts, this.controller!.signal);

		command.on("error", (err, stdout, stderr) => {
			// Don't log error if it's due to manual stop
			if (!this.streamStatus.manualStop && this.controller && !this.controller.signal.aborted) {
				logger.error("An error happened with ffmpeg:", err.message);
				if (stdout) {
					logger.error("ffmpeg stdout:", stdout);
				}
				if (stderr) {
					logger.error("ffmpeg stderr:", stderr);
				}
				this.controller.abort();
			}
		});

		await playStream(ffmpegOutput, this.streamer, undefined, this.controller!.signal)
			.catch((err) => {
				if (this.controller && !this.controller.signal.aborted) {
					logger.error('playStream error:', err);
					// Send error message to user
					DiscordUtils.sendError(message, `Stream error: ${err.message || 'Unknown error'}`).catch(e =>
						logger.error('Failed to send error message:', e)
					);
				}
				if (this.controller && !this.controller.signal.aborted) this.controller.abort();
			});

		// Only log as finished if we didn't have an error and weren't manually stopped
		if (this.controller && !this.controller.signal.aborted && !this.streamStatus.manualStop) {
			logger.info(`Finished playing: ${title || videoSource}`);
		} else if (this.streamStatus.manualStop) {
			logger.info(`Stopped playing: ${title || videoSource}`);
		} else {
			logger.info(`Failed playing: ${title || videoSource}`);
		}
	}

	private async handleQueueAdvancement(message: Message): Promise<void> {
		await DiscordUtils.sendFinishMessage(message);

		// The video finished playing, so remove it from the queue
		const finishedItem = this.queueService.getCurrent();
		if (finishedItem) {
			this.queueService.removeFromQueue(finishedItem.id);
		}

		// Get the next item in the queue.
		const nextItem = this.queueService.getNext();

		if (nextItem) {
			logger.info(`Auto-playing next item from queue: ${nextItem.title}`);
			setTimeout(() => {
				this.playVideoFromQueueItem(message, nextItem).catch(err =>
					ErrorUtils.handleError(err, 'auto-playing next item')
				);
			}, 1000);
		} else {
			// No more items in the queue, so stop playback and clean up
			this.queueService.setPlaying(false);
			logger.info('No more items in queue, playback stopped');
			await this.cleanupStreamStatus();
		}
	}

	private async handleDownload(message: Message, videoSource: string, title?: string): Promise<string | null> {
		const downloadMessage = await message.reply(`📥 Downloading \`${title || 'YouTube video'}\`...`).catch(e => {
			logger.warn("Failed to send 'Downloading...' message:", e);
			return null;
		});

		try {
			logger.info(`Downloading ${title || videoSource}...`);
			const tempFilePath = await this.mediaService.downloadYouTubeVideo(videoSource);

			if (tempFilePath) {
				logger.info(`Finished downloading ${title || videoSource}`);
				if (downloadMessage) {
					await downloadMessage.delete().catch(e => logger.warn("Failed to delete 'Downloading...' message:", e));
				}
				return tempFilePath;
			}
			throw new Error('Download failed, no temp file path returned.');
		} catch (error) {
			logger.error(`Failed to download YouTube video: ${videoSource}`, error);
			const errorMessage = `❌ Failed to download \`${title || 'YouTube video'}\`.`;
			if (downloadMessage) {
				await downloadMessage.edit(errorMessage).catch(e => logger.warn("Failed to edit 'Downloading...' message:", e));
			} else {
				await DiscordUtils.sendError(message, `Failed to download video: ${error instanceof Error ? error.message : String(error)}`);
			}
			return null;
		}
	}

	private async prepareVideoSource(message: Message, videoSource: string, title?: string): Promise<{ inputForFfmpeg: any, tempFilePath: string | null }> {
		const mediaSource = await this.mediaService.resolveMediaSource(videoSource);

		if (mediaSource && mediaSource.type === 'youtube' && !mediaSource.isLive) {
			const tempFilePath = await this.handleDownload(message, videoSource, title);
			if (tempFilePath) {
				return { inputForFfmpeg: tempFilePath, tempFilePath };
			}
			// Download failed, throw to stop playback
			throw new Error('Failed to prepare video source due to download failure.');
		}

		return { inputForFfmpeg: mediaSource ? mediaSource.url : videoSource, tempFilePath: null };
	}

	private async executeStreamWorkflow(input: any, options: any, message: Message, title: string, source: string): Promise<void> {
		this.controller = new AbortController();
		await this.executeStream(input, options, message, title, source);
	}

	private async finalizeStream(message: Message, tempFile: string | null): Promise<void> {
		if (!this.streamStatus.manualStop && this.controller && !this.controller.signal.aborted) {
			await this.handleQueueAdvancement(message);
		} else {
			this.queueService.setPlaying(false);
			this.queueService.resetCurrentIndex();
			await this.cleanupStreamStatus();
		}

		if (tempFile) {
			try {
				fs.unlinkSync(tempFile);
			} catch (cleanupError) {
				logger.error(`Failed to delete temp file ${tempFile}:`, cleanupError);
			}
		}
	}

	public async playVideo(message: Message, videoSource: string, title?: string, videoParams?: { width: number, height: number, fps?: number, bitrate?: number }): Promise<void> {
		const [guildId, channelId] = [config.guildId, config.videoChannelId];
		this.streamStatus.manualStop = false;

		if (title) {
			const currentQueueItem = this.queueService.getCurrent();
			if (currentQueueItem?.title === title) {
				this.queueService.setPlaying(true);
			}
		}

		let tempFile: string | null = null;
		try {
			const { inputForFfmpeg, tempFilePath } = await this.prepareVideoSource(message, videoSource, title);
			tempFile = tempFilePath;

			await this.ensureVoiceConnection(guildId, channelId, title);
			await DiscordUtils.sendPlaying(message, title || videoSource);

			const streamOpts = this.setupStreamConfiguration(videoParams);
			await this.executeStreamWorkflow(inputForFfmpeg, streamOpts, message, title || videoSource, videoSource);
		} catch (error) {
			await ErrorUtils.handleError(error, `playing video: ${title || videoSource}`);
			if (this.controller && !this.controller.signal.aborted) this.controller.abort();
			this.markVideoAsFailed(videoSource);
		} finally {
			await this.finalizeStream(message, tempFile);
		}
	}

	public async cleanupStreamStatus(): Promise<void> {
		try {
			this.controller?.abort();
			this.streamer.stopStream();

			// Only leave voice if we're not playing another video
			// Check if there are items in queue that might be played
			const hasQueueItems = !this.queueService.isEmpty();
			if (!hasQueueItems) {
				this.streamer.leaveVoice();
				this.streamStatus.joined = false;
				this.streamStatus.joinsucc = false;
			}

			this.streamer.client.user?.setActivity(DiscordUtils.status_idle());

			// Reset all status flags
			this.streamStatus.playing = false;
			this.streamStatus.manualStop = false;
			this.streamStatus.channelInfo = {
				guildId: "",
				channelId: "",
				cmdChannelId: "",
			};
		} catch (error) {
			await ErrorUtils.handleError(error, "cleanup stream status");
		}
	}

	public async stopAndClearQueue(): Promise<void> {
		// Clear the queue
		this.queueService.clearQueue();
		logger.info("Queue cleared by stop command");

		// Then cleanup the stream
		await this.cleanupStreamStatus();
	}

}

================================================
FILE: src/types/index.ts
================================================
export interface VideoFormat {
	hasVideo: boolean;
	hasAudio: boolean;
	url: string;
	bitrate?: number;
	qualityLabel?: string;
	container?: string;
	isLiveContent?: boolean;
}

export interface YouTubeVideo {
	id?: string;
	title: string;
	formats: VideoFormat[];
	videoDetails?: {
		isLiveContent: boolean;
	};
}

export interface TwitchStream {
	quality: string;
	resolution: string;
	url: string;
}

export interface YTFormat {
	asr: number,
	filesize: number,
	format_id: string,
	format_note: string,
	fps: number,
	height: number,
	quality: number,
	tbr: number,
	vbr?: number,
	url: string,
	width: number,
	ext: string,
	vcodec: string,
	acodec: string,
	abr: number,
	downloader_options: unknown,
	container: string,
	format: string,
	protocol: string,
	http_headers: unknown
}

export interface YTThumbnail {
	height: number,
	url: string,
	width: number,
	resolution: string,
	id: string,
}

export interface YTResponse {
	id: string,
	title: string,
	formats: YTFormat[],
	thumbnails: YTThumbnail[],
	description: string,
	upload_date: string,
	uploader: string,
	uploader_id: string,
	uploader_url: string,
	channel_id: string,
	channel_url: string,
	duration: number,
	view_count: number,
	average_rating: number,
	age_limit: number,
	webpage_url: string,
	categories: string[],
	tags: string[],
	is_live: boolean,
	like_count: number,
	dislike_count: number,
	channel: string,
	track: string,
	artist: string,
	creator: string,
	alt_title: string,
	extractor: string,
	webpage_url_basename: string,
	extractor_key: string,
	playlist: string,
	playlist_index: number,
	thumbnail: string,
	display_id: string,
	requested_subtitles: unknown,
	asr: number,
	filesize: number,
	format_id: string,
	format_note: string,
	fps: number,
	height: number,
	quality: number,
	tbr: number,
	url: string,
	width: number,
	ext: string,
	vcodec: string,
	acodec: string,
	abr: number,
	downloader_options: { http_chunk_size: number },
	container: string,
	format: string,
	protocol: string,
	http_headers: unknown,
	fulltitle: string,
	_filename: string
}

export interface YTFlags {
	help?: boolean,
	version?: boolean,
	update?: boolean,
	ignoreErrors?: boolean,
	abortOnError?: boolean,
	dumpUserAgent?: boolean,
	listExtractors?: boolean,
	extractorDescriptions?: boolean,
	forceGenericExtractor?: boolean,
	defaultSearch?: string,
	ignoreConfig?: boolean,
	configLocation?: string,
	flatPlaylist?: boolean,
	markWatched?: boolean,
	noColor?: boolean,
	proxy?: string,
	socketTimeout?: number,
	sourceAddress?: string,
	forceIpv4?: boolean,
	forceIpv6?: boolean,
	geoVerificationProxy?: string,
	geoBypass?: boolean,
	geoBypassCountry?: string,
	geoBypassIpBlock?: string,
	playlistStart?: number,
	playlistEnd?: number | "last",
	playlistItems?: string,
	matchTitle?: string,
	rejectTitle?: string,
	maxDownloads?: number,
	minFilesize?: string,
	maxFilesize?: string,
	date?: string,
	datebefore?: string,
	dateafter?: string,
	minViews?: number,
	maxViews?: number,
	matchFilter?: string,
	noPlaylist?: boolean,
	yesPlaylist?: boolean,
	ageLimit?: number,
	downloadArchive?: string,
	includeAds?: boolean,
	limitRate?: string,
	retries?: number | "infinite",
	skipUnavailableFragments?: boolean,
	abortOnUnavailableFragment?: boolean,
	keepFragments?: boolean,
	bufferSize?: string,
	noResizeBuffer?: boolean,
	httpChunkSize?: string,
	playlistReverse?: boolean,
	playlistRandom?: boolean,
	xattrSetFilesize?: boolean,
	hlsPreferNative?: boolean,
	hlsPreferFfmpeg?: boolean,
	hlsUseMpegts?: boolean,
	externalDownloader?: string,
	externalDownloaderArgs?: string,
	batchFile?: string,
	id?: boolean,
	output?: string,
	outputNaPlaceholder?: string,
	autonumberStart?: number,
	restrictFilenames?: boolean,
	noOverwrites?: boolean,
	continue?: boolean,
	noPart?: boolean,
	noMtime?: boolean,
	writeDescription?: boolean,
	writeInfoJson?: boolean,
	writeAnnotations?: boolean,
	loadInfoJson?: string,
	cookies?: string,
	cacheDir?: string,
	noCacheDir?: boolean,
	rmCacheDir?: boolean,
	writeThumbnail?: boolean,
	writeAllThumbnails?: boolean,
	listThumbnails?: boolean,
	quiet?: boolean,
	noWarnings?: boolean,
	simulate?: boolean,
	skipDownload?: boolean,
	getUrl?: boolean,
	getTitle?: boolean,
	getId?: boolean,
	getThumbnail?: boolean,
	getDuration?: boolean,
	getFilename?: boolean,
	getFormat?: boolean,
	dumpJson?: boolean,
	dumpSingleJson?: boolean,
	printJson?: boolean,
	newline?: boolean,
	noProgress?: boolean,
	consoleTitle?: boolean,
	verbose?: boolean,
	dumpPages?: boolean,
	writePages?: boolean,
	printTraffic?: boolean,
	callHome?: boolean,
	encoding?: string,
	noCheckCertificate?: boolean,
	preferInsecure?: boolean,
	userAgent?: string,
	referer?: string,
	addHeader?: string,
	bidiWorkaround?: boolean,
	sleepInterval?: number,
	maxSleepInterval?: number,
	format?: string,
	allFormats?: boolean,
	preferFreeFormats?: boolean,
	listFormats?: boolean,
	youtubeSkipDashManifest?: boolean,
	mergeOutputFormat?: string,
	writeSub?: boolean,
	writeAutoSub?: boolean,
	allSubs?: boolean,
	listSubs?: boolean,
	subFormat?: string,
	subLang?: string,
	username?: string,
	password?: string,
	twofactor?: string,
	netrc?: boolean,
	videoPassword?: string,
	apMso?: string,
	apUsername?: string,
	apPassword?: string,
	apListMso?: boolean,
	extractAudio?: boolean,
	audioFormat?: string,
	audioQuality?: number,
	recodeVideo?: string,
	postprocessorArgs?: string,
	keepVideo?: boolean,
	noPostOverwrites?: boolean,
	embedSubs?: boolean,
	embedThumbnail?: boolean,
	addMetadata?: boolean,
	metadataFromFile?: string,
	xattrs?: boolean,
	fixup?: string,
	preferAvconv?: boolean,
	preferFfmpeg?: boolean,
	ffmpegLocation?: string,
	exec?: string,
	convertSubs?: string
}

export interface CommandContext {
	message: any;
	args: string[];
	videos: Video[];
	streamStatus: StreamStatus;
	streamingService: any;
}

export interface StreamStatus {
 	joined: boolean;
 	joinsucc: boolean;
 	playing: boolean;
 	manualStop: boolean;
 	channelInfo: {
 		guildId: string;
 		channelId: string;
 		cmdChannelId: string;
 	};
 	queue: VideoQueue;
}

export interface Video {
	name: string;
	path: string;
}

export interface Command {
	name: string;
	description: string;
	usage: string;
	aliases?: string[];
	execute(context: CommandContext): Promise<void>;
}

export interface MediaSource {
 	url: string;
 	title: string;
 	type: 'youtube' | 'twitch' | 'local' | 'url';
 	isLive?: boolean;
}

export interface QueueItem {
  	id: string;
  	url: string;
  	title: string;
  	type: MediaSource['type'];
  	isLive?: boolean;
  	requestedBy: string;
  	addedAt: Date;
  	originalInput?: string;
  	resolved?: boolean;
}

export interface VideoQueue {
  items: QueueItem[];
  currentIndex: number;
  isPlaying: boolean;
}

================================================
FILE: src/utils/ffmpeg.ts
================================================
import config from "../config.js";
import ffmpeg from "fluent-ffmpeg"
import logger from "./logger.js";

const ffmpegRunning: { [key: string]: boolean } = {};

export async function ffmpegScreenshot(video: string): Promise<string[]> {
	return new Promise<string[]>((resolve, reject) => {
		if (ffmpegRunning[video]) {
			// Wait for ffmpeg to finish
			const wait = (images: string[]) => {
				if (ffmpegRunning[video] == false) {
					resolve(images);
				}
				setTimeout(() => wait(images), 100);
			}
			wait([]);
			return;
		}
		ffmpegRunning[video] = true;
		const ts = ['10%', '30%', '50%', '70%', '90%'];
		const images: string[] = [];

		const takeScreenshots = (i: number) => {
			if (i >= ts.length) {
				ffmpegRunning[video] = false;
				resolve(images);
				return;
			}
			ffmpeg(`${config.videosDir}/${video}`)
				.on("end", () => {
					const screenshotPath = `${config.previewCacheDir}/${video}-${i + 1}.jpg`;
					images.push(screenshotPath);
					takeScreenshots(i + 1);
				})
				.on("error", (err: Error) => {
					ffmpegRunning[video] = false;
					reject(err);
				})
				.screenshots({
					count: 1,
					filename: `${video}-${i + 1}.jpg`,
					timestamps: [ts[i]],
					folder: config.previewCacheDir
				});
		};

		takeScreenshots(0);
	});
}

// Checking video params
export async function getVideoParams(videoPath: string): Promise<{ width: number, height: number, bitrate: string, maxbitrate: string, fps: number }> {
	return new Promise((resolve, reject) => {
		ffmpeg.ffprobe(videoPath, (err, metadata) => {
			if (err) {
				return reject(err);
			}

			const videoStream = metadata.streams.find(stream => stream.codec_type === 'video');

			if (videoStream && videoStream.width && videoStream.height && videoStream.bit_rate) {
				const rFrameRate = videoStream.r_frame_rate || videoStream.avg_frame_rate;

				if (rFrameRate) {
					const [numerator, denominator] = rFrameRate.split('/').map(Number);
					videoStream.fps = numerator / denominator;
				} else {
					videoStream.fps = 0
				}

				resolve({ width: videoStream.width, height: videoStream.height, bitrate: videoStream.bit_rate, maxbitrate: videoStream.maxBitrate, fps: videoStream.fps });
			} else {
				reject(new Error('Unable to get Resolution.'));
			}
		});
	});
}



================================================
FILE: src/utils/gen-hash.ts
================================================
import * as bcrypt from 'bcrypt';
import argon2 from 'argon2';

const password = process.argv[2];
const hashType = process.argv[3] || 'argon2';

if (!password) {
	console.error('Usage: bun/node run gen-hash <password> [type]');
    console.error('\tExample: bun/node run gen-hash mySecurePassword123 argon2');
	console.error('\tExample: bun/node run gen-hash mySecurePassword123 bcrypt');
	console.error('\nSupported types: argon2 (default), bcrypt');
	process.exit(1);
}

if (hashType !== 'argon2' && hashType !== 'bcrypt') {
	console.error(`Error: Invalid hash type "${hashType}"`);
	console.error('Supported types: argon2, bcrypt');
	process.exit(1);
}

let hash: string;

if (hashType === 'argon2') {
	hash = await argon2.hash(password, {
		type: argon2.argon2id,
		memoryCost: 65536, // 64 MB
		timeCost: 3,
		parallelism: 4
	});
	console.log('\n✅ Argon2 hash generated successfully!\n');
} else {
	const saltRounds = 10;
	hash = bcrypt.hashSync(password, saltRounds);
	console.log('\n✅ Bcrypt hash generated successfully!\n');
}

const escapedHash = hash.replace(/\$/g, '\\$'); 
console.log('Add this to your .env file:');
console.log(`SERVER_PASSWORD = "${escapedHash}"`);
console.log('\nRaw hash:');
console.log(escapedHash);


================================================
FILE: src/utils/logger.ts
================================================
import winston from 'winston';

// Custom log format
const logFormat = winston.format.combine(
	winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
	winston.format.colorize(),
	winston.format.printf(({ level, message, timestamp }) => {
		return `[${timestamp}] ${level}: ${message}`;
	})
);

// Create logger instance
const logger = winston.createLogger({
	level: 'info',
	format: logFormat,
	transports: [
		// Console output
		new winston.transports.Console()
	]
});

export default logger;


================================================
FILE: src/utils/shared.ts
================================================
import { Message, ActivityOptions } from "discord.js-selfbot-v13";
import config from "../config.js";
import logger from "./logger.js";
import fs from 'fs';

/**
 * Shared utility functions for Discord bot operations
 */
export const DiscordUtils = {
	/**
	 * Create idle status for Discord bot
	 */
	status_idle(): ActivityOptions {
		return {
			name: config.prefix + "help",
			type: 'WATCHING'
		};
	},

	/**
	 * Create watching status for Discord bot
	 */
	status_watch(name: string): ActivityOptions {
		return {
			name: `${name}`,
			type: 'WATCHING'
		};
	},

	/**
	 * Send error message with reaction
	 */
	async sendError(message: Message, error: string): Promise<void> {
		await message.react('❌');
		await message.reply(`❌ **Error**: ${error}`);
	},

	/**
	 * Send success message with reaction
	 */
	async sendSuccess(message: Message, description: string): Promise<void> {
		await message.react('✅');
		await message.channel.send(`✅ **Success**: ${description}`);
	},

	/**
	 * Send info message with reaction
	 */
	async sendInfo(message: Message, title: string, description: string): Promise<void> {
		await message.react('ℹ️');
		await message.channel.send(`ℹ️ **${title}**: ${description}`);
	},

	/**
	 * Send playing message with reaction
	 */
	async sendPlaying(message: Message, title: string): Promise<void> {
		const content = `📽 **Now Playing**: \`${title}\``;
		await Promise.all([
			message.react('▶️'),
			message.reply(content)
		]);
	},

	/**
	 * Send finish message
	 */
	async sendFinishMessage(message: Message): Promise<void> {
		const content = '⏹️ **Finished**: Finished playing video.';
		await message.channel.send(content);
	},

	/**
	 * Send list message with reaction
	 */
	async sendList(message: Message, items: string[], type?: string): Promise<void> {
		await message.react('📋');
		if (type == "ytsearch") {
			await message.reply(`📋 **Search Results**:\n${items.join('\n')}`);
		} else if (type == "refresh") {
			await message.reply(`📋 **Video list refreshed**:\n${items.join('\n')}`);
		} else {
			await message.channel.send(`📋 **Local Videos List**:\n${items.join('\n')}`);
		}
	}
};

/**
 * Error handling utilities
 */
export const ErrorUtils = {
	/**
	 * Handle and log errors consistently
	 */
	async handleError(error: any, context: string, message?: Message): Promise<void> {
		logger.error(`Error in ${context}:`, error);

		if (message) {
			await DiscordUtils.sendError(message, `An error occurred: ${error.message || 'Unknown error'}`);
		}
	},

	/**
	 * Handle async operation errors
	 */
	async withErrorHandling<T>(
		operation: () => Promise<T>,
		context: string,
		message?: Message
	): Promise<T | null> {
		try {
			return await operation();
		} catch (error) {
			await this.handleError(error, context, message);
			return null;
		}
	}
};

/**
 * General utility functions
 */
export const GeneralUtils = {
	/**
	 * Check if input is a valid streaming URL
	 */
	isValidUrl(input: string): boolean {
		if (!input || typeof input !== 'string') {
			return false;
		}

		// Check for common streaming platforms
		return input.includes('youtube.com/') ||
			   input.includes('youtu.be/') ||
			   input.includes('twitch.tv/') ||
			   input.startsWith('http://') ||
			   input.startsWith('https://');
	},

	/**
	 * Check if a path is a local file
	 */
	isLocalFile(filePath: string): boolean {
		try {
			return fs.existsSync(filePath) && fs.lstatSync(filePath).isFile();
		} catch (error) {
			return false;
		}
	}
};

================================================
FILE: src/utils/youtube.ts
================================================
import ytdl_dlp from './yt-dlp.js';
import logger from './logger.js';
import yts from 'play-dl';
import { YouTubeVideo, YTResponse } from '../types/index.js';

export class Youtube {
	async getVideoInfo(url: string): Promise<YouTubeVideo | null> {
		try {
			const videoData = await ytdl_dlp(url, { dumpSingleJson: true, noPlaylist: true }) as YTResponse;

			if (typeof videoData === 'object' && videoData !== null && videoData.id && videoData.title) {
				return {
					id: videoData.id,
					title: videoData.title,
					formats: [],
					videoDetails: {
						isLiveContent: videoData.is_live === true || (videoData as any).live_status === 'is_live'
					}
				};
			}
			logger.warn(`Failed to parse video info from yt-dlp for URL: ${url}. Data: ${JSON.stringify(videoData)}`);
			return null;
		} catch (error) {
			logger.error(`Failed to get video info using yt-dlp for URL ${url}:`, error);
			return null;
		}
	}

	async searchAndGetPageUrl(title: string): Promise<{ pageUrl: string | null, title: string | null }> {
		try {
			const results = await yts.search(title, { limit: 1 });
			if (results.length === 0 || !results[0]?.url) {
				logger.warn(`No video found on YouTube for title: "${title}" using play-dl.`);
				return { pageUrl: null, title: null };
			}
			
			return { pageUrl: results[0].url, title: results[0].title || null };
		} catch (error) {
			logger.error(`Video search for page URL failed for title "${title}":`, error);
			return { pageUrl: null, title: null };
		}
	}

	async search(query: string, limit: number = 5): Promise<string[]> {
		try {
			const searchResults = await yts.search(query, { limit });
			return searchResults.map((video, index) =>
				`${index + 1}. \`${video.title}\``
			);
		} catch (error) {
			logger.warn(`No videos found with the given title: "${query}"`);
			return [];
		}
	}

	async getLiveStreamUrl(youtubePageUrl: string): Promise<string | null> {
		try {
			const streamUrl = await ytdl_dlp(youtubePageUrl, {
				getUrl: true,
				format: 'best[protocol=https]/best[protocol=http]/best',
				noPlaylist: true,
				quiet: true,
			});

			if (typeof streamUrl === 'string' && streamUrl.trim()) {
				logger.info(`Got live stream URL for ${youtubePageUrl}: ${streamUrl.trim()}`);
				return streamUrl.trim();
			}
			logger.warn(`yt-dlp did not return a valid live stream URL for: ${youtubePageUrl}. Received: ${streamUrl}`);
			return null;
		} catch (error) {
			logger.error(`Failed to get live stream URL using yt-dlp for ${youtubePageUrl}:`, error);
			return null;
		}
	}
}


================================================
FILE: src/utils/yt-dlp.ts
================================================
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
import nodePath from "node:path";
import process from "node:process";
import os from "node:os";
import crypto from "node:crypto";
import got from "got";
import { YTFlags } from "../types/index.js";
import logger from "./logger.js";
import config from "../config.js";
import { spawn } from "node:child_process";

let determinedFilename: string;
const platform = process.platform;
const arch = process.arch;

if (platform === "win32") {
	if (arch === "x64") {
		determinedFilename = "yt-dlp.exe";
	}
	else if (arch === "ia32") {    
		determinedFilename = "yt-dlp_x86.exe";
	}
} else if (platform === "darwin") {
	determinedFilename = "yt-dlp_macos";
} else if (platform === "linux") {
	if (arch === "arm64") {
		determinedFilename = "yt-dlp_linux_aarch64";
	} else if (arch === "arm") {
		determinedFilename = "yt-dlp_linux_armv7l";
	} else if (arch === "x64") {
		determinedFilename = "yt-dlp";
	} else {
		logger.warn(`Unsupported Linux architecture '${arch}' for yt-dlp. Falling back to generic 'yt-dlp'. Download might fail.`);
		determinedFilename = "yt-dlp";
	}
} else {
	logger.warn(`Unsupported OS '${platform}' for yt-dlp. Attempting to use generic 'yt-dlp'. Download might fail.`);
	determinedFilename = "yt-dlp";
}

const filename = determinedFilename;
const scriptsPath = nodePath.resolve(process.cwd(), "scripts");
const exePath = nodePath.resolve(scriptsPath, filename);

function args(url: string, options: Partial<YTFlags>): string[] {
	const optArgs: string[] = [];
	
	// Add cookies file if configured
	if (config.ytdlpCookiesPath && existsSync(config.ytdlpCookiesPath)) {
		optArgs.push('--cookies');
		optArgs.push(config.ytdlpCookiesPath);
	}
	
	for (const [key, val] of Object.entries(options)) {
		if (val === null || val === undefined) {
			continue;
		}

		const flag = key.replaceAll(/[A-Z]/gu, ms => `-${ms.toLowerCase()}`);

		if (typeof val === "boolean") {
			if (val) {
				optArgs.push(`--${flag}`);
			} else {
				optArgs.push(`--no-${flag}`);
			}
		} else {
			optArgs.push(`--${flag}`);
			optArgs.push(String(val));
		}
	}
	return [url, ...optArgs];
}

function json(str: string) {
	try {
		return JSON.parse(str);
	} catch {
		return str;
	}
}

export async function downloadExecutable() {
	if (!existsSync(exePath)) {
		logger.info("yt-dlp not found, downloading...");
		const releases = await got.get("https://api.github.com/repos/yt-dlp/yt-dlp/releases?per_page=1").json();
		const release = releases[0];
		const asset = release.assets.find(ast => ast.name === filename);
		const version = release.tag_name;
		
		await new Promise((resolve, reject) => {
			got.get(asset.browser_download_url).buffer().then(x => {
				mkdirSync(scriptsPath, { recursive: true });
				writeFileSync(exePath, x, { mode: 0o777 });
				return 0;
			}).then(resolve).catch(reject);
		});
		logger.info(`yt-dlp ${version} downloaded successfully`);
	}
}

export function exec(url: string, options: Partial<YTFlags> = {}, spawnOptions: Record<string, any> = {}) {
	return spawn(exePath, args(url, options), {
		windowsHide: true,
		...spawnOptions,
		stdio: ["ignore", "pipe", "pipe"]
	});
}

export default async function ytdl(url: string, options: Partial<YTFlags> = {}, spawnOptions: Record<string, any> = {}) {
	return new Promise((resolve, reject) => {
		let data = "";
		let errorData = "";

		const proc = exec(url, options, spawnOptions);

		proc.stdout?.on('data', (chunk) => {
			data += chunk.toString();
		});

		proc.stderr?.on('data', (chunk) => {
			errorData += chunk.toString();
		});

		proc.on('close', (exitCode) => {
			if (exitCode !== 0) {
				logger.error(`yt-dlp process exited with code ${exitCode}. Stderr: ${errorData}`);
				reject(new Error(`yt-dlp failed with exit code ${exitCode}: ${errorData || data}`));
			} else {
				resolve(json(data));
			}
		});

		proc.on('error', (error) => {
			logger.error(`yt-dlp process error:`, error);
			reject(error);
		});
	});
}

export async function downloadToTempFile(url: string, options: Partial<YTFlags> = {}): Promise<string> {
	await downloadExecutable();

	const tempDir = os.tmpdir();
	const tempFilename = `ytdlp_temp_${crypto.randomBytes(6).toString('hex')}.mp4`;
	const tempFilePath = nodePath.join(tempDir, tempFilename);

	const downloadOptions: Partial<YTFlags> = {
		...options,
		output: tempFilePath,
		quiet: true,
		noWarnings: true,
	};

	const proc = spawn(exePath, args(url, downloadOptions), {
		windowsHide: true,
		stdio: ["ignore", "ignore", "pipe"]
	});

	let errorData = "";
	proc.stderr?.on('data', (chunk) => {
		errorData += chunk.toString();
	});

	const exitCode = await new Promise<number>((resolve) => {
		proc.on('close', (code) => resolve(code || 0));
		proc.on('error', () => resolve(1));
	});

	if (exitCode !== 0) {
		if (existsSync(tempFilePath)) {
			try {
				unlinkSync(tempFilePath);
			} catch (cleanupError) {
				logger.warn(`Failed to cleanup temp file ${tempFilePath} after yt-dlp error:`, cleanupError);
			}
		}
		const errorMessage = `yt-dlp failed to download to temp file. Exit code: ${exitCode}. Stderr: ${errorData.trim()}`;
		logger.error(errorMessage);
		throw new Error(errorMessage);
	}

	if (!existsSync(tempFilePath)) {
		const errorMessage = `yt-dlp exited successfully but temp file ${tempFilePath} was not created. Stderr: ${errorData.trim()}`;
		logger.error(errorMessage);
		throw new Error(errorMessage);
	}
	
	return tempFilePath;
}

export async function checkForUpdatesAndUpdate(): Promise<void> {
	try {
		await downloadExecutable();
		const updateProc = spawn(exePath, ["--update"], {
			stdio: ["ignore", "pipe", "pipe"],
		});

		let stdoutData = "";
		let stderrData = "";

		updateProc.stdout?.on('data', (chunk) => {
			stdoutData += chunk.toString();
		});

		updateProc.stderr?.on('data', (chunk) => {
			stderrData += chunk.toString();
		});

		const exitCode = await new Promise<number>((resolve) => {
			updateProc.on('close', (code) => resolve(code || 0));
			updateProc.on('error', () => resolve(1));
		});

		if (exitCode === 0) {
			if (stdoutData.includes("Updated yt-dlp to")) {
				logger.info(`yt-dlp updated successfully. Output: ${stdoutData.trim()}`);
			}
		} else {
			logger.warn(`yt-dlp update check failed or an update was not straightforward. Exit code: ${exitCode}.`);
			if (stdoutData.trim()) logger.warn(`yt-dlp update stdout: ${stdoutData.trim()}`);
			if (stderrData.trim()) logger.error(`yt-dlp update stderr: ${stderrData.trim()}`);
		}
	} catch (error) {
		logger.error("Error during yt-dlp update check process:", error);
	}
}

================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "noImplicitAny": false,
    "removeComments": true,
    "preserveConstEnums": true,
    "noEmitHelpers": true,
    "outDir": "dist",
    "rootDir": "src",
    /* Language and Environment */
    "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    /* Modules */
    "module": "NodeNext", /* Specify what module code is generated. */
    "moduleResolution": "NodeNext",
    /* Emit */
    "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
    /* Type Checking */
    "strict": true, /* Enable all strict type-checking options. */
    "strictNullChecks": false, /* When type checking, take into account null and undefined. */
    "skipLibCheck": true, /* Skip type checking all .d.ts files. */

    "allowJs": true, 
    "declaration": true
  }
}
Download .txt
gitextract_raib73cm/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       └── docker-image.yml
├── .gitignore
├── Dockerfile
├── Dockerfile.node
├── README.md
├── docker-compose-node.yml
├── docker-compose-warp.yml
├── docker-compose.yml
├── egg-stream-bot.json
├── package.json
├── src/
│   ├── commands/
│   │   ├── base.ts
│   │   ├── config.ts
│   │   ├── help.ts
│   │   ├── list.ts
│   │   ├── manager.ts
│   │   ├── ping.ts
│   │   ├── play.ts
│   │   ├── preview.ts
│   │   ├── queue.ts
│   │   ├── skip.ts
│   │   ├── status.ts
│   │   ├── stop.ts
│   │   └── ytsearch.ts
│   ├── config.ts
│   ├── events/
│   │   ├── client/
│   │   │   └── ready.ts
│   │   ├── messageCreate.ts
│   │   └── voiceStateUpdate.ts
│   ├── index.ts
│   ├── server/
│   │   ├── index.ts
│   │   ├── middleware/
│   │   │   ├── auth.ts
│   │   │   └── multer.ts
│   │   ├── public/
│   │   │   ├── css/
│   │   │   │   └── main.css
│   │   │   ├── js/
│   │   │   │   └── main.js
│   │   │   └── site.webmanifest
│   │   ├── routes/
│   │   │   ├── auth.ts
│   │   │   ├── dashboard.ts
│   │   │   ├── preview.ts
│   │   │   └── upload.ts
│   │   ├── utils/
│   │   │   └── helpers.ts
│   │   └── views/
│   │       ├── layouts/
│   │       │   └── main.ejs
│   │       └── pages/
│   │           ├── dashboard.ejs
│   │           ├── login.ejs
│   │           └── preview.ejs
│   ├── services/
│   │   ├── media.ts
│   │   ├── queue.ts
│   │   └── streaming.ts
│   ├── types/
│   │   └── index.ts
│   └── utils/
│       ├── ffmpeg.ts
│       ├── gen-hash.ts
│       ├── logger.ts
│       ├── shared.ts
│       ├── youtube.ts
│       └── yt-dlp.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (200 symbols across 28 files)

FILE: src/commands/base.ts
  method constructor (line 10) | constructor(commandManager?: any) {
  method sendError (line 15) | protected async sendError(message: any, error: string): Promise<void> {
  method sendSuccess (line 19) | protected async sendSuccess(message: any, description: string): Promise<...
  method sendInfo (line 23) | protected async sendInfo(message: any, title: string, description: strin...
  method sendList (line 27) | protected async sendList(message: any, items: string[], type?: string): ...
  method sendPlaying (line 31) | protected async sendPlaying(message: any, title: string): Promise<void> {
  method sendFinishMessage (line 35) | protected async sendFinishMessage(message: any): Promise<void> {

FILE: src/commands/config.ts
  class ConfigCommand (line 6) | class ConfigCommand extends BaseCommand {
    method execute (line 12) | async execute(context: CommandContext): Promise<void> {
    method showConfig (line 40) | private async showConfig(context: CommandContext): Promise<void> {
    method showParameter (line 70) | private async showParameter(context: CommandContext, parameter: string...
    method setParameter (line 83) | private async setParameter(context: CommandContext, parameter: string,...
    method isAdmin (line 174) | private isAdmin(userId: string): boolean {

FILE: src/commands/help.ts
  class HelpCommand (line 5) | class HelpCommand extends BaseCommand {
    method constructor (line 10) | constructor(private commandManager: CommandManager) {
    method execute (line 14) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/list.ts
  class ListCommand (line 7) | class ListCommand extends BaseCommand {
    method execute (line 12) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/manager.ts
  class CommandManager (line 8) | class CommandManager {
    method constructor (line 12) | constructor() {
    method loadCommands (line 16) | private async loadCommands(): Promise<void> {
    method isCommand (line 71) | private isCommand(obj: any): obj is new (commandManager?: CommandManag...
    method getCommand (line 81) | public getCommand(name: string): Command | null {
    method getAllCommands (line 86) | public getAllCommands(): Command[] {
    method executeCommand (line 90) | public async executeCommand(commandName: string, context: CommandConte...
    method getCommandList (line 106) | public getCommandList(): string {

FILE: src/commands/ping.ts
  class PingCommand (line 4) | class PingCommand extends BaseCommand {
    method execute (line 9) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/play.ts
  class PlayCommand (line 9) | class PlayCommand extends BaseCommand {
    method constructor (line 16) | constructor() {
    method execute (line 21) | async execute(context: CommandContext): Promise<void> {
    method handleLocalVideo (line 55) | private async handleLocalVideo(context: CommandContext, video: any): P...
    method handleUrl (line 67) | private async handleUrl(context: CommandContext, url: string): Promise...
    method handleSearchQuery (line 83) | private async handleSearchQuery(context: CommandContext, query: string...

FILE: src/commands/preview.ts
  class PreviewCommand (line 8) | class PreviewCommand extends BaseCommand {
    method execute (line 13) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/queue.ts
  class QueueCommand (line 4) | class QueueCommand extends BaseCommand {
    method execute (line 9) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/skip.ts
  class SkipCommand (line 4) | class SkipCommand extends BaseCommand {
    method execute (line 10) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/status.ts
  class StatusCommand (line 4) | class StatusCommand extends BaseCommand {
    method execute (line 9) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/stop.ts
  class StopCommand (line 5) | class StopCommand extends BaseCommand {
    method execute (line 11) | async execute(context: CommandContext): Promise<void> {

FILE: src/commands/ytsearch.ts
  class YTSearchCommand (line 5) | class YTSearchCommand extends BaseCommand {
    method constructor (line 12) | constructor() {
    method execute (line 17) | async execute(context: CommandContext): Promise<void> {

FILE: src/config.ts
  constant VALID_VIDEO_CODECS (line 5) | const VALID_VIDEO_CODECS = ['VP8', 'H264', 'H265', 'VP9', 'AV1'];
  function parseVideoCodec (line 7) | function parseVideoCodec(value: string): "VP8" | "H264" | "H265" {
  function parsePreset (line 17) | function parsePreset(value: string): "ultrafast" | "superfast" | "veryfa...
  function parseBoolean (line 37) | function parseBoolean(value: string | undefined): boolean {
  function parseAdminIds (line 49) | function parseAdminIds(value: string): string[] {

FILE: src/events/client/ready.ts
  function handleReady (line 5) | async function handleReady(client: Client): Promise<void> {

FILE: src/events/messageCreate.ts
  function handleMessageCreate (line 6) | async function handleMessageCreate(

FILE: src/events/voiceStateUpdate.ts
  function handleVoiceStateUpdate (line 6) | async function handleVoiceStateUpdate(

FILE: src/server/middleware/multer.ts
  type MulterFile (line 6) | interface MulterFile extends Express.Multer.File {

FILE: src/server/public/js/main.js
  function setTheme (line 5) | function setTheme(isDark) {
  class ToastManager (line 22) | class ToastManager {
    method constructor (line 23) | constructor() {
    method init (line 32) | init() {
    method getIcon (line 50) | getIcon(type) {
    method getAriaRole (line 60) | getAriaRole(type) {
    method createToast (line 64) | createToast(message, type = 'info', title = '', duration = null) {
    method removeToast (line 130) | removeToast(toastId) {
    method manageQueue (line 153) | manageQueue() {
    method processQueue (line 163) | processQueue() {
    method success (line 172) | success(message, title = 'Success', duration = null) {
    method error (line 176) | error(message, title = 'Error', duration = null) {
    method warning (line 180) | warning(message, title = 'Warning', duration = null) {
    method info (line 184) | info(message, title = 'Info', duration = null) {
  function showToast (line 193) | function showToast(message, type = 'info', title = '', duration = null) {
  function showSuccessToast (line 198) | function showSuccessToast(message, title = 'Success', duration = null) {
  function showErrorToast (line 202) | function showErrorToast(message, title = 'Error', duration = null) {
  function showWarningToast (line 206) | function showWarningToast(message, title = 'Warning', duration = null) {
  function showInfoToast (line 210) | function showInfoToast(message, title = 'Info', duration = null) {
  function copyFileName (line 214) | function copyFileName(name) {
  function fallbackCopyTextToClipboard (line 228) | async function fallbackCopyTextToClipboard(text) {
  function initLazyLoading (line 268) | function initLazyLoading() {
  function initFormLoadingStates (line 296) | function initFormLoadingStates() {
  function showGlobalLoading (line 325) | function showGlobalLoading(message = 'Loading...') {
  function hideGlobalLoading (line 338) | function hideGlobalLoading() {
  function updateProgress (line 349) | function updateProgress(percentage) {
  function validateVideoFile (line 357) | function validateVideoFile(file) {
  function showDeleteModal (line 418) | function showDeleteModal(fileName, deleteUrl) {
  function handleDeleteSuccess (line 443) | function handleDeleteSuccess() {
  function startLocalUpload (line 460) | function startLocalUpload() {
  function cancelLocalUpload (line 528) | function cancelLocalUpload() {
  function resetLocalUpload (line 540) | function resetLocalUpload() {
  function startRemoteUpload (line 561) | function startRemoteUpload() {
  function cancelRemoteUpload (line 638) | function cancelRemoteUpload() {
  function resetRemoteUpload (line 649) | function resetRemoteUpload() {
  function formatFileSize (line 670) | function formatFileSize(bytes) {
  class PreviewImageManager (line 679) | class PreviewImageManager {
    method constructor (line 680) | constructor() {
    method init (line 684) | init() {
    method setupImageLoading (line 689) | setupImageLoading() {
    method setupLazyLoading (line 721) | setupLazyLoading() {
    method showLoadingState (line 754) | showLoadingState(container) {
    method showImageState (line 766) | showImageState(container) {
    method showErrorState (line 778) | showErrorState(container) {
    method retryLoad (line 790) | retryLoad(container) {
  class FaviconManager (line 814) | class FaviconManager {
    method constructor (line 815) | constructor() {
    method init (line 821) | init() {
    method updateFavicon (line 827) | updateFavicon() {
    method setupThemeListener (line 838) | setupThemeListener() {
    method setupLoadingListener (line 854) | setupLoadingListener() {
    method showLoading (line 876) | showLoading() {
    method hideLoading (line 880) | hideLoading() {

FILE: src/server/utils/helpers.ts
  function stringify (line 2) | function stringify(obj: any): string {
  function prettySize (line 24) | function prettySize(bytes: number): string {

FILE: src/services/media.ts
  class MediaService (line 11) | class MediaService {
    method constructor (line 14) | constructor() {
    method resolveMediaSource (line 18) | public async resolveMediaSource(url: string): Promise<MediaSource | nu...
    method _resolveYouTubeSource (line 39) | private async _resolveYouTubeSource(url: string): Promise<MediaSource ...
    method getTwitchStreamUrl (line 57) | public async getTwitchStreamUrl(url: string): Promise<string | null> {
    method downloadYouTubeVideo (line 85) | public async downloadYouTubeVideo(url: string): Promise<string | null> {
    method _resolveTwitchSource (line 101) | private async _resolveTwitchSource(url: string): Promise<MediaSource |...
    method _resolveLocalSource (line 114) | private _resolveLocalSource(url: string): MediaSource {
    method _resolveDirectUrlSource (line 122) | private async _resolveDirectUrlSource(url: string): Promise<MediaSourc...
    method searchYouTube (line 189) | public async searchYouTube(query: string, limit: number = 5): Promise<...
    method searchAndPlayYouTube (line 198) | public async searchAndPlayYouTube(query: string): Promise<MediaSource ...

FILE: src/services/queue.ts
  class QueueService (line 5) | class QueueService {
    method constructor (line 9) | constructor() {
    method addToQueue (line 21) | public async addToQueue(
    method add (line 39) | public async add(
    method getNext (line 68) | getNext(): QueueItem | null {
    method getCurrent (line 87) | getCurrent(): QueueItem | null {
    method skip (line 117) | skip(): QueueItem | null {
    method removeFromQueue (line 144) | removeFromQueue(id: string): boolean {
    method clearQueue (line 165) | clearQueue(): void {
    method resetCurrentIndex (line 175) | resetCurrentIndex(): void {
    method getQueue (line 182) | getQueue(): QueueItem[] {
    method getQueueStatus (line 189) | getQueueStatus(): VideoQueue {
    method setPlaying (line 196) | setPlaying(isPlaying: boolean): void {
    method isEmpty (line 203) | isEmpty(): boolean {
    method getLength (line 210) | getLength(): number {
    method moveItem (line 217) | moveItem(fromIndex: number, toIndex: number): boolean {
    method generateId (line 241) | private generateId(): string {

FILE: src/services/streaming.ts
  class StreamingService (line 12) | class StreamingService {
    method constructor (line 21) | constructor(client: Client, streamStatus: StreamStatus) {
    method getStreamer (line 28) | public getStreamer(): Streamer {
    method getQueueService (line 32) | public getQueueService(): QueueService {
    method markVideoAsFailed (line 36) | private markVideoAsFailed(videoSource: string): void {
    method addToQueue (line 41) | public async addToQueue(
    method playFromQueue (line 74) | public async playFromQueue(message: Message): Promise<void> {
    method skipCurrent (line 90) | public async skipCurrent(message: Message): Promise<void> {
    method playVideoFromQueueItem (line 138) | private async playVideoFromQueueItem(message: Message, queueItem: Queu...
    method getVideoParameters (line 155) | private async getVideoParameters(videoUrl: string): Promise<{ width: n...
    method ensureVoiceConnection (line 177) | private async ensureVoiceConnection(guildId: string, channelId: string...
    method setupStreamConfiguration (line 199) | private setupStreamConfiguration(videoParams?: { width: number, height...
    method executeStream (line 239) | private async executeStream(inputForFfmpeg: any, streamOpts: any, mess...
    method handleQueueAdvancement (line 278) | private async handleQueueAdvancement(message: Message): Promise<void> {
    method handleDownload (line 305) | private async handleDownload(message: Message, videoSource: string, ti...
    method prepareVideoSource (line 335) | private async prepareVideoSource(message: Message, videoSource: string...
    method executeStreamWorkflow (line 350) | private async executeStreamWorkflow(input: any, options: any, message:...
    method finalizeStream (line 355) | private async finalizeStream(message: Message, tempFile: string | null...
    method playVideo (line 373) | public async playVideo(message: Message, videoSource: string, title?: ...
    method cleanupStreamStatus (line 403) | public async cleanupStreamStatus(): Promise<void> {
    method stopAndClearQueue (line 432) | public async stopAndClearQueue(): Promise<void> {

FILE: src/types/index.ts
  type VideoFormat (line 1) | interface VideoFormat {
  type YouTubeVideo (line 11) | interface YouTubeVideo {
  type TwitchStream (line 20) | interface TwitchStream {
  type YTFormat (line 26) | interface YTFormat {
  type YTThumbnail (line 49) | interface YTThumbnail {
  type YTResponse (line 57) | interface YTResponse {
  type YTFlags (line 115) | interface YTFlags {
  type CommandContext (line 268) | interface CommandContext {
  type StreamStatus (line 276) | interface StreamStatus {
  type Video (line 289) | interface Video {
  type Command (line 294) | interface Command {
  type MediaSource (line 302) | interface MediaSource {
  type QueueItem (line 309) | interface QueueItem {
  type VideoQueue (line 321) | interface VideoQueue {

FILE: src/utils/ffmpeg.ts
  function ffmpegScreenshot (line 7) | async function ffmpegScreenshot(video: string): Promise<string[]> {
  function getVideoParams (line 53) | async function getVideoParams(videoPath: string): Promise<{ width: numbe...

FILE: src/utils/shared.ts
  method status_idle (line 13) | status_idle(): ActivityOptions {
  method status_watch (line 23) | status_watch(name: string): ActivityOptions {
  method sendError (line 33) | async sendError(message: Message, error: string): Promise<void> {
  method sendSuccess (line 41) | async sendSuccess(message: Message, description: string): Promise<void> {
  method sendInfo (line 49) | async sendInfo(message: Message, title: string, description: string): Pr...
  method sendPlaying (line 57) | async sendPlaying(message: Message, title: string): Promise<void> {
  method sendFinishMessage (line 68) | async sendFinishMessage(message: Message): Promise<void> {
  method sendList (line 76) | async sendList(message: Message, items: string[], type?: string): Promis...
  method handleError (line 95) | async handleError(error: any, context: string, message?: Message): Promi...
  method withErrorHandling (line 106) | async withErrorHandling<T>(
  method isValidUrl (line 127) | isValidUrl(input: string): boolean {
  method isLocalFile (line 143) | isLocalFile(filePath: string): boolean {

FILE: src/utils/youtube.ts
  class Youtube (line 6) | class Youtube {
    method getVideoInfo (line 7) | async getVideoInfo(url: string): Promise<YouTubeVideo | null> {
    method searchAndGetPageUrl (line 29) | async searchAndGetPageUrl(title: string): Promise<{ pageUrl: string | ...
    method search (line 44) | async search(query: string, limit: number = 5): Promise<string[]> {
    method getLiveStreamUrl (line 56) | async getLiveStreamUrl(youtubePageUrl: string): Promise<string | null> {

FILE: src/utils/yt-dlp.ts
  function args (line 45) | function args(url: string, options: Partial<YTFlags>): string[] {
  function json (line 75) | function json(str: string) {
  function downloadExecutable (line 83) | async function downloadExecutable() {
  function exec (line 102) | function exec(url: string, options: Partial<YTFlags> = {}, spawnOptions:...
  function ytdl (line 110) | async function ytdl(url: string, options: Partial<YTFlags> = {}, spawnOp...
  function downloadToTempFile (line 141) | async function downloadToTempFile(url: string, options: Partial<YTFlags>...
  function checkForUpdatesAndUpdate (line 192) | async function checkForUpdatesAndUpdate(): Promise<void> {
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (210K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 525,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/docker-image.yml",
    "chars": 5723,
    "preview": "name: StreamBot Docker Image CI\n\non:\n  push:\n    branches: [ \"main\" ]\n    paths-ignore:\n      - '.env.example'\n      - '"
  },
  {
    "path": ".gitignore",
    "chars": 74,
    "preview": "LICENSE\nnode_modules/\ndist/\n.env\nbun.lockb\nvideos/\ntmp/\n*.sock\ncookies.txt"
  },
  {
    "path": "Dockerfile",
    "chars": 984,
    "preview": "# Use Debian (trixie) as the base image\nFROM node:trixie\n\n# Set the working directory\nWORKDIR /home/bots/StreamBot\n\n# In"
  },
  {
    "path": "Dockerfile.node",
    "chars": 838,
    "preview": "# Use Debian (trixie) as the base image\nFROM node:trixie\n\n# Set the working directory\nWORKDIR /home/bots/StreamBot\n\n# In"
  },
  {
    "path": "README.md",
    "chars": 11090,
    "preview": "<div align=\"center\">\n\n<img src=\"src/server/public/favicon.svg\" alt=\"StreamBot Logo\" width=\"400\" height=\"120\"/>\n\n# Stream"
  },
  {
    "path": "docker-compose-node.yml",
    "chars": 3153,
    "preview": "services:\n  streambot:\n    image: quay.io/ydrag0n/streambot:node\n    container_name: streambot\n    restart: always\n    e"
  },
  {
    "path": "docker-compose-warp.yml",
    "chars": 3643,
    "preview": "services:\n  streambot:\n    image: quay.io/ydrag0n/streambot:latest\n    container_name: streambot\n    restart: always\n   "
  },
  {
    "path": "docker-compose.yml",
    "chars": 3155,
    "preview": "services:\n  streambot:\n    image: quay.io/ydrag0n/streambot:latest\n    container_name: streambot\n    restart: always\n   "
  },
  {
    "path": "egg-stream-bot.json",
    "chars": 4292,
    "preview": "{\n    \"_comment\": \"DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO\",\n    \"meta\": {\n     "
  },
  {
    "path": "package.json",
    "chars": 1228,
    "preview": "{\n  \"name\": \"streambot\",\n  \"version\": \"2.0.2\",\n  \"description\": \"A self bot to stream movies/videos to Discord.\",\n  \"mai"
  },
  {
    "path": "src/commands/base.ts",
    "chars": 1188,
    "preview": "import { Command, CommandContext } from \"../types/index.js\";\nimport { DiscordUtils } from \"../utils/shared.js\";\n\nexport "
  },
  {
    "path": "src/commands/config.ts",
    "chars": 6168,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\nimport config, { parseBoole"
  },
  {
    "path": "src/commands/help.ts",
    "chars": 647,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\nimport { CommandManager } f"
  },
  {
    "path": "src/commands/list.ts",
    "chars": 1070,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext, Video } from \"../types/index.js\";\nimport fs from 'fs';"
  },
  {
    "path": "src/commands/manager.ts",
    "chars": 3583,
    "preview": "import { Command, CommandContext } from \"../types/index.js\";\nimport fs from 'fs';\nimport path from 'path';\nimport logger"
  },
  {
    "path": "src/commands/ping.ts",
    "chars": 470,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\n\nexport default class PingC"
  },
  {
    "path": "src/commands/play.ts",
    "chars": 3218,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\nimport { MediaService } fro"
  },
  {
    "path": "src/commands/preview.ts",
    "chars": 1817,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\nimport { MessageAttachment "
  },
  {
    "path": "src/commands/queue.ts",
    "chars": 2279,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\n\nexport default class Queue"
  },
  {
    "path": "src/commands/skip.ts",
    "chars": 815,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\n\nexport default class SkipC"
  },
  {
    "path": "src/commands/status.ts",
    "chars": 435,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\n\nexport default class Statu"
  },
  {
    "path": "src/commands/stop.ts",
    "chars": 900,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\nimport logger from '../util"
  },
  {
    "path": "src/commands/ytsearch.ts",
    "chars": 986,
    "preview": "import { BaseCommand } from \"./base.js\";\nimport { CommandContext } from \"../types/index.js\";\nimport { MediaService } fro"
  },
  {
    "path": "src/config.ts",
    "chars": 3966,
    "preview": "import dotenv from \"dotenv\"\n\ndotenv.config({ quiet: true });\n\nconst VALID_VIDEO_CODECS = ['VP8', 'H264', 'H265', 'VP9', "
  },
  {
    "path": "src/events/client/ready.ts",
    "chars": 376,
    "preview": "import { Client, ActivityOptions } from \"discord.js-selfbot-v13\";\nimport logger from \"../../utils/logger.js\";\nimport { D"
  },
  {
    "path": "src/events/messageCreate.ts",
    "chars": 1183,
    "preview": "import { Message } from \"discord.js-selfbot-v13\";\nimport { CommandManager } from \"../commands/manager.js\";\nimport { Comm"
  },
  {
    "path": "src/events/voiceStateUpdate.ts",
    "chars": 1078,
    "preview": "import { VoiceState } from \"discord.js-selfbot-v13\";\nimport { Client } from \"discord.js-selfbot-v13\";\nimport { DiscordUt"
  },
  {
    "path": "src/index.ts",
    "chars": 3087,
    "preview": "import { Client } from \"discord.js-selfbot-v13\";\nimport config from \"./config.js\";\nimport fs from 'fs';\nimport path from"
  },
  {
    "path": "src/server/index.ts",
    "chars": 2025,
    "preview": "import express from \"express\";\nimport session from \"express-session\";\nimport expressLayouts from \"express-ejs-layouts\";\n"
  },
  {
    "path": "src/server/middleware/auth.ts",
    "chars": 418,
    "preview": "import { Request, Response, NextFunction } from 'express';\n\nexport const authMiddleware = (req: Request, res: Response, "
  },
  {
    "path": "src/server/middleware/multer.ts",
    "chars": 694,
    "preview": "import multer, { StorageEngine } from 'multer';\nimport path from 'path';\nimport config from '../../config.js';\n\n// Defin"
  },
  {
    "path": "src/server/public/css/main.css",
    "chars": 13020,
    "preview": ":root {\n\t--bs-body-color: #212529;\n\t--bs-body-bg: #f8f9fa;\n\t--bs-border-color: #dee2e6;\n}\n\n.dark-mode {\n\t--bs-body-color"
  },
  {
    "path": "src/server/public/js/main.js",
    "chars": 24165,
    "preview": "\nconst themeToggle = document.getElementById('themeToggle');\nconst prefersDarkScheme = window.matchMedia('(prefers-color"
  },
  {
    "path": "src/server/public/site.webmanifest",
    "chars": 685,
    "preview": "{\n  \"name\": \"StreamBot Video Manager\",\n  \"short_name\": \"StreamBot\",\n  \"description\": \"Upload, manage, and preview your v"
  },
  {
    "path": "src/server/routes/auth.ts",
    "chars": 1470,
    "preview": "import { Router } from 'express';\nimport bcrypt from 'bcrypt';\nimport argon2 from 'argon2';\nimport config from '../../co"
  },
  {
    "path": "src/server/routes/dashboard.ts",
    "chars": 726,
    "preview": "import { Router } from 'express';\nimport fs from 'fs';\nimport path from 'path';\nimport config from '../../config.js';\nim"
  },
  {
    "path": "src/server/routes/preview.ts",
    "chars": 2086,
    "preview": "import { Router } from 'express';\nimport fs from 'fs';\nimport path from 'path';\nimport ffmpeg from 'fluent-ffmpeg';\nimpo"
  },
  {
    "path": "src/server/routes/upload.ts",
    "chars": 2584,
    "preview": "import { Router } from 'express';\nimport axios from 'axios';\nimport https from 'https';\nimport fs from 'fs';\nimport path"
  },
  {
    "path": "src/server/utils/helpers.ts",
    "chars": 790,
    "preview": "// Helper function to stringify objects for display in templates\nexport function stringify(obj: any): string {\n\t// if st"
  },
  {
    "path": "src/server/views/layouts/main.ejs",
    "chars": 5129,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, init"
  },
  {
    "path": "src/server/views/pages/dashboard.ejs",
    "chars": 7692,
    "preview": "<div class=\"container my-5\" id=\"main-content\">\n\t<div class=\"d-flex justify-content-between align-items-center mb-4\">\n\t\t<"
  },
  {
    "path": "src/server/views/pages/login.ejs",
    "chars": 2672,
    "preview": "<div class=\"container-fluid min-vh-100 d-flex align-items-center justify-content-center py-5 login-bg\">\n\t<div class=\"car"
  },
  {
    "path": "src/server/views/pages/preview.ejs",
    "chars": 3749,
    "preview": "<div class=\"container-fluid py-4\">\n\t<h1 class=\"h3 mb-4\">File Preview</h1>\n\t<h2 class=\"h5 mb-4\"><%= filename %></h2>\n\t<di"
  },
  {
    "path": "src/services/media.ts",
    "chars": 6368,
    "preview": "import { getStream, getVod } from 'twitch-m3u8';\nimport { TwitchStream, MediaSource } from '../types/index.js';\nimport c"
  },
  {
    "path": "src/services/queue.ts",
    "chars": 5684,
    "preview": "import { VideoQueue, QueueItem, MediaSource } from \"../types/index.js\";\nimport { MediaService } from \"./media.js\";\nimpor"
  },
  {
    "path": "src/services/streaming.ts",
    "chars": 15327,
    "preview": "import { Client, Message } from \"discord.js-selfbot-v13\";\nimport { Streamer, Utils, prepareStream, playStream } from \"@d"
  },
  {
    "path": "src/types/index.ts",
    "chars": 6724,
    "preview": "export interface VideoFormat {\n\thasVideo: boolean;\n\thasAudio: boolean;\n\turl: string;\n\tbitrate?: number;\n\tqualityLabel?: "
  },
  {
    "path": "src/utils/ffmpeg.ts",
    "chars": 2276,
    "preview": "import config from \"../config.js\";\nimport ffmpeg from \"fluent-ffmpeg\"\nimport logger from \"./logger.js\";\n\nconst ffmpegRun"
  },
  {
    "path": "src/utils/gen-hash.ts",
    "chars": 1275,
    "preview": "import * as bcrypt from 'bcrypt';\r\nimport argon2 from 'argon2';\r\n\r\nconst password = process.argv[2];\r\nconst hashType = p"
  },
  {
    "path": "src/utils/logger.ts",
    "chars": 502,
    "preview": "import winston from 'winston';\n\n// Custom log format\nconst logFormat = winston.format.combine(\n\twinston.format.timestamp"
  },
  {
    "path": "src/utils/shared.ts",
    "chars": 3487,
    "preview": "import { Message, ActivityOptions } from \"discord.js-selfbot-v13\";\nimport config from \"../config.js\";\nimport logger from"
  },
  {
    "path": "src/utils/youtube.ts",
    "chars": 2547,
    "preview": "import ytdl_dlp from './yt-dlp.js';\nimport logger from './logger.js';\nimport yts from 'play-dl';\nimport { YouTubeVideo, "
  },
  {
    "path": "src/utils/yt-dlp.ts",
    "chars": 6603,
    "preview": "import { existsSync, mkdirSync, writeFileSync, unlinkSync } from \"node:fs\";\nimport nodePath from \"node:path\";\nimport pro"
  },
  {
    "path": "tsconfig.json",
    "chars": 1365,
    "preview": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig to read more about this file */\n    \"emitDecoratorMetadata"
  }
]

About this extraction

This page contains the full source code of the ysdragon/StreamBot GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (183.6 KB), approximately 51.1k tokens, and a symbol index with 200 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!