npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eyezo

v1.0.0

Published

Lightweight Node.js video server that serves directory trees via REST API with HTTP range request support

Readme

Eyezo Server

A lightweight, standards-driven Node.js video server that serves video files from a directory tree via REST API with HTTP range request support for streaming to iOS and web clients.

Features

  • Read-only file serving: Serves video files without modifying the directory tree
  • Built-in web UI: Visit the server root in a browser to browse and play your videos — no extra setup
  • REST API: Clean REST endpoints for browsing directories and streaming videos
  • HTTP Range Requests: Full support for video seeking/scrubbing
  • Thumbnail Generation: Automatic thumbnail extraction at 1% into each video
  • Metadata Extraction: FFmpeg-based video metadata extraction (duration, resolution, codec)
  • SQLite Database: Caches metadata and thumbnail information
  • CORS Enabled: Works with web and iOS clients
  • System File Filtering: Automatically excludes .DS_Store, Thumbs.db, hidden files, etc.
  • Directory-first: Always reads directly from filesystem (no filename caching)

Clients

Prerequisites

  • Node.js 16+
  • FFmpeg installed on your system
    • macOS: brew install ffmpeg
    • Ubuntu/Debian: apt install ffmpeg
    • Windows: Download from https://ffmpeg.org/

Usage

Quick Start (npx)

Run the server directly without installing — npx downloads it on first use:

npx eyezo /path/to/your/videos

Installation

To install from source:

npm install

Then start the server:

node eyezo.js /path/to/your/videos

The server will start on port 3000 by default. You can customize the port and host:

PORT=8080 HOST=localhost node eyezo.js /path/to/your/videos

Development Mode (Auto-reload)

npm run dev /path/to/your/videos

Web Interface

Open the server's root URL in any browser to use the built-in web UI:

http://localhost:3000/

The UI is a single self-contained page (no build step) served from public/index.html. It lets you:

  • Browse folders with breadcrumb navigation
  • See a thumbnail grid with duration, resolution, and file size
  • Play videos in the browser, with seeking via HTTP range requests
  • Resume playback — watch progress is saved automatically and restored on reopen

The root URL uses content negotiation: browsers (requests sending Accept: text/html) receive the web UI, while API clients receive the JSON API descriptor as before, so existing clients are unaffected.

API Endpoints

Health & Configuration

GET  /api/health          - Health check
GET  /api/config          - Server configuration

Directory Browsing

GET  /api/browse          - Browse root directory
GET  /api/browse/*        - Browse subdirectory

Query Parameters:
  - sort: name|date|size (default: name)
  - order: asc|desc (default: asc)

Example:
  GET /api/browse/movies/action?sort=name&order=asc

Response:

{
  "path": "/movies/action",
  "parent": "/movies",
  "directories": [
    {
      "name": "2024",
      "path": "/full/path/movies/action/2024",
      "relativePath": "movies/action/2024",
      "urlPath": "movies/action/2024",
      "modified": 1715875200
    }
  ],
  "videos": [
    {
      "name": "movie.mp4",
      "path": "/full/path/movie.mp4",
      "relativePath": "movies/action/movie.mp4",
      "urlPath": "movies/action/movie.mp4",
      "size": 1073741824,
      "modified": 1715875200,
      "extension": ".mp4",
      "mimeType": "video/mp4",
      "duration": 7200.5,
      "thumbnailUrl": "/api/thumbnail/movies/action/movie.mp4"
    }
  ],
  "totalDirectories": 1,
  "totalVideos": 1
}

Video Streaming

GET  /api/video/*         - Stream video file with range request support

Headers:
  Range: bytes=start-end  (optional, for seeking)

Example:
  GET /api/video/movies/action/movie.mp4
  GET /api/video/movies/action/movie.mp4
  Headers: Range: bytes=0-1023

The server returns:

  • 200 OK for full file streaming
  • 206 Partial Content for range requests
  • Proper Accept-Ranges, Content-Range, and Content-Type headers

Thumbnails

GET  /api/thumbnail/*     - Get video thumbnail (JPEG)

Example:
  GET /api/thumbnail/movies/action/movie.mp4

Thumbnails are:

  • Generated lazily on first request
  • Extracted at 1% into the video
  • 320x180 resolution
  • Cached in ~/.local/eyezo-server/thumbnails/

Metadata

GET  /api/metadata/*      - Get video metadata

Example:
  GET /api/metadata/movies/action/movie.mp4

Response:

{
  "relativePath": "movies/action/movie.mp4",
  "size": 1073741824,
  "modified": 1715875200,
  "duration": 7200.5,
  "width": 1920,
  "height": 1080,
  "codec": "h264",
  "bitrate": 5000000,
  "hasThumbnail": true,
  "lastScanned": 1715875300
}

Scanning

POST /api/scan            - Trigger metadata/thumbnail generation

Body:
  { "path": "movies/action" }  // Optional, scans all if omitted

This endpoint:

  • Recursively scans the specified directory (or all videos if no path)
  • Extracts metadata for all videos
  • Generates thumbnails for all videos
  • Updates the database

Watch Progress

POST   /api/watch-progress       - Save playback position for a video
GET    /api/watch-progress/*     - Get saved position for a video
DELETE /api/watch-progress/*     - Clear saved position for a video

Save body:

{ "path": "movies/action/movie.mp4", "position": 123.45 }

Get response:

{
  "path": "movies/action/movie.mp4",
  "position": 123.45,
  "lastWatched": 1715875300
}

Used by the built-in web UI to resume playback where you left off. position is in seconds; a never-watched video returns 0.

Data Storage

Database

SQLite database stored at: ~/.local/eyezo-server/database.sqlite

Contains:

  • Video metadata (duration, resolution, codec, bitrate)
  • Thumbnail status
  • Configuration
  • Scan history

Thumbnails

Thumbnail cache stored at: ~/.local/eyezo-server/thumbnails/

Filenames are MD5 hashes of the video's relative path.

Supported Video Formats

  • MP4 (.mp4, .m4v)
  • MKV (.mkv)
  • WebM (.webm)
  • QuickTime (.mov)
  • AVI (.avi)
  • WMV (.wmv)
  • FLV (.flv)
  • MPEG (.mpg, .mpeg, .m2v)
  • 3GP (.3gp, .3g2)
  • MPEG-TS (.mts, .ts, .m2ts)
  • VOB (.vob)
  • OGG Video (.ogv)

Client Usage Examples

Web Browser (HTML)

The server ships with a built-in web UI — just open the root URL. The snippet below shows how to embed the video and thumbnail endpoints in your own page instead:

<!DOCTYPE html>
<html>
<head>
  <title>Video Player</title>
</head>
<body>
  <video controls width="800">
    <source src="http://localhost:3000/api/video/movies/action/movie.mp4" type="video/mp4">
  </video>

  <img src="http://localhost:3000/api/thumbnail/movies/action/movie.mp4" alt="Thumbnail">
</body>
</html>

JavaScript (Fetch API)

// Browse directory
const response = await fetch('http://localhost:3000/api/browse/movies');
const data = await response.json();

console.log('Videos:', data.videos);
console.log('Subdirectories:', data.directories);

// Get metadata
const metadata = await fetch('http://localhost:3000/api/metadata/movies/movie.mp4');
const videoInfo = await metadata.json();

console.log('Duration:', videoInfo.duration);
console.log('Resolution:', `${videoInfo.width}x${videoInfo.height}`);

iOS (Swift)

import AVKit

let url = URL(string: "http://localhost:3000/api/video/movies/movie.mp4")!
let player = AVPlayer(url: url)
let playerViewController = AVPlayerViewController()
playerViewController.player = player

present(playerViewController, animated: true) {
    player.play()
}

Testing

Manual Testing with cURL

# Health check
curl http://localhost:3000/api/health

# Browse root
curl http://localhost:3000/api/browse

# Browse subdirectory
curl http://localhost:3000/api/browse/movies/action

# Stream video (full)
curl http://localhost:3000/api/video/movie.mp4 -o test.mp4

# Stream video (range request)
curl -H "Range: bytes=0-1023" http://localhost:3000/api/video/movie.mp4

# Get thumbnail
curl http://localhost:3000/api/thumbnail/movie.mp4 -o thumb.jpg

# Get metadata
curl http://localhost:3000/api/metadata/movie.mp4

# Trigger scan
curl -X POST http://localhost:3000/api/scan \
  -H "Content-Type: application/json" \
  -d '{"path": "movies"}'

Security Considerations

Path Traversal Protection

The server validates all paths to prevent directory traversal attacks. Requests like:

GET /api/video/../../etc/passwd

Will be rejected with a 403 Forbidden error.

No Authentication

This server does not include authentication. It's designed for use on trusted networks (home/local network). If you need to expose it to the internet, consider:

  • Running it behind a reverse proxy (nginx, Apache) with authentication
  • Using a VPN
  • Implementing IP-based access control

Performance

  • Streaming: Uses Node.js streams for efficient memory usage with large files
  • Database: Synchronous SQLite for fast metadata queries
  • Caching: Thumbnails and metadata are cached to reduce FFmpeg overhead
  • CORS: Preflight requests are cached for 24 hours

Troubleshooting

FFmpeg Not Found

If you get errors about FFmpeg:

# macOS
brew install ffmpeg

# Ubuntu/Debian
sudo apt update && sudo apt install ffmpeg

# Verify installation
ffmpeg -version

Port Already in Use

Change the port:

PORT=8080 node eyezo.js /path/to/videos

Permission Denied

Ensure the video directory is readable:

ls -la /path/to/videos

Database Locked

If you get database locked errors, ensure only one instance of the server is running.

Project Structure

eyezo-server/
├── package.json
├── README.md
├── .gitignore
├── eyezo.js                     # Entry point
├── public/
│   └── index.html               # Built-in web UI (served at /)
├── src/
│   ├── app.js                   # Fastify app setup
│   ├── config/
│   │   ├── constants.js         # System file patterns, video extensions
│   │   └── database.js          # SQLite connection and schema
│   ├── routes/
│   │   ├── index.js             # Health check, config endpoints
│   │   ├── browse.js            # Directory browsing
│   │   ├── video.js             # Video streaming
│   │   ├── thumbnail.js         # Thumbnail serving
│   │   ├── metadata.js          # Metadata and scanning
│   │   └── watch-progress.js    # Watch progress save/resume
│   ├── services/
│   │   ├── filesystem.js        # Directory traversal, filtering
│   │   ├── video-stream.js      # Range request handling
│   │   ├── thumbnail.js         # Thumbnail generation
│   │   ├── metadata.js          # Metadata extraction
│   │   └── database.js          # Database queries
│   └── utils/
│       ├── mime-types.js        # MIME type detection
│       ├── path-utils.js        # Path validation
│       └── error-handler.js     # Error classes
└── scripts/
    └── init-db.js               # Database initialization

License

MIT

Contributing

Issues and pull requests are welcome!

Acknowledgments