{"id":51130757,"url":"https://github.com/anders94/eyezo-server","last_synced_at":"2026-06-25T12:01:26.272Z","repository":{"id":358304531,"uuid":"1240718508","full_name":"anders94/eyezo-server","owner":"anders94","description":"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.","archived":false,"fork":false,"pushed_at":"2026-06-17T22:11:52.000Z","size":71,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-18T00:09:32.478Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/anders94.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-16T13:35:28.000Z","updated_at":"2026-06-17T22:11:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/anders94/eyezo-server","commit_stats":null,"previous_names":["anders94/video-server","anders94/eyezo-server"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/anders94/eyezo-server","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anders94%2Feyezo-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anders94%2Feyezo-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anders94%2Feyezo-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anders94%2Feyezo-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/anders94","download_url":"https://codeload.github.com/anders94/eyezo-server/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/anders94%2Feyezo-server/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34773843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-25T12:01:25.475Z","updated_at":"2026-06-25T12:01:26.258Z","avatar_url":"https://github.com/anders94.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Eyezo Server\n\nA 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.\n\n## Features\n\n- **Read-only file serving**: Serves video files without modifying the directory tree\n- **Built-in web UI**: Visit the server root in a browser to browse and play your videos — no extra setup\n- **REST API**: Clean REST endpoints for browsing directories and streaming videos\n- **HTTP Range Requests**: Full support for video seeking/scrubbing\n- **Thumbnail Generation**: Automatic thumbnail extraction at 1% into each video\n- **Metadata Extraction**: FFmpeg-based video metadata extraction (duration, resolution, codec)\n- **SQLite Database**: Caches metadata and thumbnail information\n- **CORS Enabled**: Works with web and iOS clients\n- **System File Filtering**: Automatically excludes .DS_Store, Thumbs.db, hidden files, etc.\n- **Directory-first**: Always reads directly from filesystem (no filename caching)\n\n## Clients\n\n- [eyezo-ios](https://github.com/anders94/eyezo-ios)\n- [eyezo-tvos](https://github.com/anders94/eyezo-tvos)\n- [eyezo-visionos](https://github.com/anders94/eyezo-visionos)\n\n## Prerequisites\n\n- Node.js 16+\n- FFmpeg installed on your system\n  - macOS: `brew install ffmpeg`\n  - Ubuntu/Debian: `apt install ffmpeg`\n  - Windows: Download from https://ffmpeg.org/\n\n## Usage\n\n### Quick Start (npx)\n\nRun the server directly without installing — npx downloads it on first use:\n\n```bash\nnpx eyezo /path/to/your/videos\n```\n\n### Installation\n\nTo install from source:\n\n```bash\nnpm install\n```\n\nThen start the server:\n\n```bash\nnode eyezo.js /path/to/your/videos\n```\n\nThe server will start on port 3000 by default. You can customize the port and host:\n\n```bash\nPORT=8080 HOST=localhost node eyezo.js /path/to/your/videos\n```\n\n### Development Mode (Auto-reload)\n\n```bash\nnpm run dev /path/to/your/videos\n```\n\n## Web Interface\n\nOpen the server's root URL in any browser to use the built-in web UI:\n\n```\nhttp://localhost:3000/\n```\n\nThe UI is a single self-contained page (no build step) served from `public/index.html`. It lets you:\n\n- Browse folders with breadcrumb navigation\n- See a thumbnail grid with duration, resolution, and file size\n- Play videos in the browser, with seeking via HTTP range requests\n- Resume playback — watch progress is saved automatically and restored on reopen\n\nThe 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.\n\n## API Endpoints\n\n### Health \u0026 Configuration\n\n```\nGET  /api/health          - Health check\nGET  /api/config          - Server configuration\n```\n\n### Directory Browsing\n\n```\nGET  /api/browse          - Browse root directory\nGET  /api/browse/*        - Browse subdirectory\n\nQuery Parameters:\n  - sort: name|date|size (default: name)\n  - order: asc|desc (default: asc)\n\nExample:\n  GET /api/browse/movies/action?sort=name\u0026order=asc\n```\n\n**Response:**\n```json\n{\n  \"path\": \"/movies/action\",\n  \"parent\": \"/movies\",\n  \"directories\": [\n    {\n      \"name\": \"2024\",\n      \"path\": \"/full/path/movies/action/2024\",\n      \"relativePath\": \"movies/action/2024\",\n      \"urlPath\": \"movies/action/2024\",\n      \"modified\": 1715875200\n    }\n  ],\n  \"videos\": [\n    {\n      \"name\": \"movie.mp4\",\n      \"path\": \"/full/path/movie.mp4\",\n      \"relativePath\": \"movies/action/movie.mp4\",\n      \"urlPath\": \"movies/action/movie.mp4\",\n      \"size\": 1073741824,\n      \"modified\": 1715875200,\n      \"extension\": \".mp4\",\n      \"mimeType\": \"video/mp4\",\n      \"duration\": 7200.5,\n      \"thumbnailUrl\": \"/api/thumbnail/movies/action/movie.mp4\"\n    }\n  ],\n  \"totalDirectories\": 1,\n  \"totalVideos\": 1\n}\n```\n\n### Video Streaming\n\n```\nGET  /api/video/*         - Stream video file with range request support\n\nHeaders:\n  Range: bytes=start-end  (optional, for seeking)\n\nExample:\n  GET /api/video/movies/action/movie.mp4\n  GET /api/video/movies/action/movie.mp4\n  Headers: Range: bytes=0-1023\n```\n\nThe server returns:\n- `200 OK` for full file streaming\n- `206 Partial Content` for range requests\n- Proper `Accept-Ranges`, `Content-Range`, and `Content-Type` headers\n\n### Thumbnails\n\n```\nGET  /api/thumbnail/*     - Get video thumbnail (JPEG)\n\nExample:\n  GET /api/thumbnail/movies/action/movie.mp4\n```\n\nThumbnails are:\n- Generated lazily on first request\n- Extracted at 1% into the video\n- 320x180 resolution\n- Cached in `~/.local/eyezo-server/thumbnails/`\n\n### Metadata\n\n```\nGET  /api/metadata/*      - Get video metadata\n\nExample:\n  GET /api/metadata/movies/action/movie.mp4\n```\n\n**Response:**\n```json\n{\n  \"relativePath\": \"movies/action/movie.mp4\",\n  \"size\": 1073741824,\n  \"modified\": 1715875200,\n  \"duration\": 7200.5,\n  \"width\": 1920,\n  \"height\": 1080,\n  \"codec\": \"h264\",\n  \"bitrate\": 5000000,\n  \"hasThumbnail\": true,\n  \"lastScanned\": 1715875300\n}\n```\n\n### Scanning\n\n```\nPOST /api/scan            - Trigger metadata/thumbnail generation\n\nBody:\n  { \"path\": \"movies/action\" }  // Optional, scans all if omitted\n```\n\nThis endpoint:\n- Recursively scans the specified directory (or all videos if no path)\n- Extracts metadata for all videos\n- Generates thumbnails for all videos\n- Updates the database\n\n### Watch Progress\n\n```\nPOST   /api/watch-progress       - Save playback position for a video\nGET    /api/watch-progress/*     - Get saved position for a video\nDELETE /api/watch-progress/*     - Clear saved position for a video\n```\n\n**Save body:**\n```json\n{ \"path\": \"movies/action/movie.mp4\", \"position\": 123.45 }\n```\n\n**Get response:**\n```json\n{\n  \"path\": \"movies/action/movie.mp4\",\n  \"position\": 123.45,\n  \"lastWatched\": 1715875300\n}\n```\n\nUsed by the built-in web UI to resume playback where you left off. `position` is in seconds; a never-watched video returns `0`.\n\n## Data Storage\n\n### Database\nSQLite database stored at: `~/.local/eyezo-server/database.sqlite`\n\nContains:\n- Video metadata (duration, resolution, codec, bitrate)\n- Thumbnail status\n- Configuration\n- Scan history\n\n### Thumbnails\nThumbnail cache stored at: `~/.local/eyezo-server/thumbnails/`\n\nFilenames are MD5 hashes of the video's relative path.\n\n## Supported Video Formats\n\n- MP4 (.mp4, .m4v)\n- MKV (.mkv)\n- WebM (.webm)\n- QuickTime (.mov)\n- AVI (.avi)\n- WMV (.wmv)\n- FLV (.flv)\n- MPEG (.mpg, .mpeg, .m2v)\n- 3GP (.3gp, .3g2)\n- MPEG-TS (.mts, .ts, .m2ts)\n- VOB (.vob)\n- OGG Video (.ogv)\n\n## Client Usage Examples\n\n### Web Browser (HTML)\n\nThe server ships with a [built-in web UI](#web-interface) — just open the root URL. The snippet below shows how to embed the video and thumbnail endpoints in your own page instead:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n  \u003ctitle\u003eVideo Player\u003c/title\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cvideo controls width=\"800\"\u003e\n    \u003csource src=\"http://localhost:3000/api/video/movies/action/movie.mp4\" type=\"video/mp4\"\u003e\n  \u003c/video\u003e\n\n  \u003cimg src=\"http://localhost:3000/api/thumbnail/movies/action/movie.mp4\" alt=\"Thumbnail\"\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### JavaScript (Fetch API)\n\n```javascript\n// Browse directory\nconst response = await fetch('http://localhost:3000/api/browse/movies');\nconst data = await response.json();\n\nconsole.log('Videos:', data.videos);\nconsole.log('Subdirectories:', data.directories);\n\n// Get metadata\nconst metadata = await fetch('http://localhost:3000/api/metadata/movies/movie.mp4');\nconst videoInfo = await metadata.json();\n\nconsole.log('Duration:', videoInfo.duration);\nconsole.log('Resolution:', `${videoInfo.width}x${videoInfo.height}`);\n```\n\n### iOS (Swift)\n\n```swift\nimport AVKit\n\nlet url = URL(string: \"http://localhost:3000/api/video/movies/movie.mp4\")!\nlet player = AVPlayer(url: url)\nlet playerViewController = AVPlayerViewController()\nplayerViewController.player = player\n\npresent(playerViewController, animated: true) {\n    player.play()\n}\n```\n\n## Testing\n\n### Manual Testing with cURL\n\n```bash\n# Health check\ncurl http://localhost:3000/api/health\n\n# Browse root\ncurl http://localhost:3000/api/browse\n\n# Browse subdirectory\ncurl http://localhost:3000/api/browse/movies/action\n\n# Stream video (full)\ncurl http://localhost:3000/api/video/movie.mp4 -o test.mp4\n\n# Stream video (range request)\ncurl -H \"Range: bytes=0-1023\" http://localhost:3000/api/video/movie.mp4\n\n# Get thumbnail\ncurl http://localhost:3000/api/thumbnail/movie.mp4 -o thumb.jpg\n\n# Get metadata\ncurl http://localhost:3000/api/metadata/movie.mp4\n\n# Trigger scan\ncurl -X POST http://localhost:3000/api/scan \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"path\": \"movies\"}'\n```\n\n## Security Considerations\n\n### Path Traversal Protection\n\nThe server validates all paths to prevent directory traversal attacks. Requests like:\n\n```\nGET /api/video/../../etc/passwd\n```\n\nWill be rejected with a `403 Forbidden` error.\n\n### No Authentication\n\nThis 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:\n\n- Running it behind a reverse proxy (nginx, Apache) with authentication\n- Using a VPN\n- Implementing IP-based access control\n\n## Performance\n\n- **Streaming**: Uses Node.js streams for efficient memory usage with large files\n- **Database**: Synchronous SQLite for fast metadata queries\n- **Caching**: Thumbnails and metadata are cached to reduce FFmpeg overhead\n- **CORS**: Preflight requests are cached for 24 hours\n\n## Troubleshooting\n\n### FFmpeg Not Found\n\nIf you get errors about FFmpeg:\n\n```bash\n# macOS\nbrew install ffmpeg\n\n# Ubuntu/Debian\nsudo apt update \u0026\u0026 sudo apt install ffmpeg\n\n# Verify installation\nffmpeg -version\n```\n\n### Port Already in Use\n\nChange the port:\n\n```bash\nPORT=8080 node eyezo.js /path/to/videos\n```\n\n### Permission Denied\n\nEnsure the video directory is readable:\n\n```bash\nls -la /path/to/videos\n```\n\n### Database Locked\n\nIf you get database locked errors, ensure only one instance of the server is running.\n\n## Project Structure\n\n```\neyezo-server/\n├── package.json\n├── README.md\n├── .gitignore\n├── eyezo.js                     # Entry point\n├── public/\n│   └── index.html               # Built-in web UI (served at /)\n├── src/\n│   ├── app.js                   # Fastify app setup\n│   ├── config/\n│   │   ├── constants.js         # System file patterns, video extensions\n│   │   └── database.js          # SQLite connection and schema\n│   ├── routes/\n│   │   ├── index.js             # Health check, config endpoints\n│   │   ├── browse.js            # Directory browsing\n│   │   ├── video.js             # Video streaming\n│   │   ├── thumbnail.js         # Thumbnail serving\n│   │   ├── metadata.js          # Metadata and scanning\n│   │   └── watch-progress.js    # Watch progress save/resume\n│   ├── services/\n│   │   ├── filesystem.js        # Directory traversal, filtering\n│   │   ├── video-stream.js      # Range request handling\n│   │   ├── thumbnail.js         # Thumbnail generation\n│   │   ├── metadata.js          # Metadata extraction\n│   │   └── database.js          # Database queries\n│   └── utils/\n│       ├── mime-types.js        # MIME type detection\n│       ├── path-utils.js        # Path validation\n│       └── error-handler.js     # Error classes\n└── scripts/\n    └── init-db.js               # Database initialization\n```\n\n## License\n\nMIT\n\n## Contributing\n\nIssues and pull requests are welcome!\n\n## Acknowledgments\n\n- Built with [Fastify](https://www.fastify.io/)\n- Video processing with [FFmpeg](https://ffmpeg.org/)\n- Database with [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanders94%2Feyezo-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanders94%2Feyezo-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanders94%2Feyezo-server/lists"}