JellyBeans

πŸ—‚οΈ VOD File Manager

A self-hosted, password-protected web file manager with deep Jellyfin integration β€” browse, upload, organize, identify, and thumbnail your media library from any browser.


✨ Features

πŸ“ File Management

🎨 UI & Navigation

πŸ”’ Security

πŸ’Ύ Disk Space Monitoring

🎞️ Jellyfin Integration

πŸ–ΌοΈ Thumbnail Generator

Accessible from the file context menu as β€œCreate Thumbnail”:

  1. The server reads the file’s Jellyfin title (or falls back to the filename)
  2. A styled SVG poster is generated server-side at 1000 Γ— 1500 px using frontend/logo.png as the background image (falls back to a dark gradient if the file is missing)
  3. Text layout is fully adaptive β€” font size and characters-per-line scale automatically based on title length (45–100 px), multi-word titles wrap cleanly, and a semi-transparent backdrop is drawn behind the text
  4. The SVG is rasterised to JPEG (quality 90%) on the client via <canvas> and previewed in the dialog before applying
  5. On confirmation, the JPEG is saved to disk alongside the video as <filename>-poster.jpg and Jellyfin is triggered to perform a FullRefresh on the matched item β€” so the new thumbnail appears in Jellyfin immediately without a manual library scan

⚑ Bulk Thumbnail Generation

Don’t want to generate posters one by one? Enter Select Mode, choose as many video files as you want, and click πŸ–ΌοΈ Thumbnails from the bulk action bar.


πŸš€ Getting Started

Prerequisites

Installation

# 1. Clone the repository
git clone https://github.com/pook27/JellyBeans
cd JellyBeans

# 2. Install dependencies
npm install

# 3. Create your environment file
vim .env
# Then edit .env with your values (see Configuration below)

# 4. Start the server
node server.js

The app will be available at http://localhost:<PORT>.


βš™οΈ Configuration

Create a .env file in the project root:

# Server
PORT=3000

# Path to the directory that will be served (relative to project root)
STORAGE_PATH=./storage

# Login credentials
ADMIN_USER=admin
ADMIN_PASS=yourpassword

# Session secret (use a long random string in production)
SESSION_SECRET=change_me_to_something_random

# Jellyfin integration (optional β€” title overlays, posters, and thumbnail upload)
JELLYFIN_URL=http://your-jellyfin-host:8096
API_KEY=your_jellyfin_api_key

Getting a Jellyfin API Key

  1. Open Jellyfin β†’ Dashboard β†’ API Keys
  2. Click + to create a new key
  3. Paste it into API_KEY in your .env

Important: Your STORAGE_PATH and Jellyfin’s library paths must point to the same physical directory (or the same Docker volume mount). Title and thumbnail matching works by comparing absolute filesystem paths, so they must align exactly.

Custom Thumbnail Background

Place a file named logo.png inside the frontend/ directory. This image is used as the background for generated poster thumbnails (stretched to fill 1000 Γ— 1500 px). If the file is absent, the generator falls back to a dark blue gradient.


πŸ“ Project Structure

β”œβ”€β”€ server.js              # Express server β€” all routes and API logic
β”œβ”€β”€ jellybeans-audit.log   # (Auto-generated) JSONL audit trail of file operations
β”œβ”€β”€ frontend/
β”‚   β”œβ”€β”€ index.html         # Main explorer template (uses  placeholders)
β”‚   β”œβ”€β”€ index.js           # Client-side logic (upload, modals, Jellyfin toggle)
β”‚   β”œβ”€β”€ activity.html      # Activity log page template
β”‚   β”œβ”€β”€ utils.js           # Universal export mapping file extensions to emoji icons
β”‚   β”œβ”€β”€ login.html         # Login page with cinematic poster-wall background
β”‚   β”œβ”€β”€ login.js           # Fetches Jellyfin posters and builds animated rows
β”‚   β”œβ”€β”€ style.css          # Full UI stylesheet
β”‚   └── logo.png           # (Optional) Background image for thumbnail generator
β”œβ”€β”€ user_create.py         # Utility: bulk-create numbered Jellyfin user accounts
β”œβ”€β”€ .env                   # Your local config (never committed)
β”œβ”€β”€ .gitignore
└── package.json

πŸ”Œ API Reference

All API routes require an active login session unless otherwise noted.

Method Route Description
POST /login Authenticate with username + password
GET /logout Destroy session and redirect to login
GET /explorer/* Render directory listing UI
POST /upload Upload one or more files to a target path
GET /download/* Download a file by path
GET /api/info/* Get file metadata (size, date, image dimensions, word count)
POST /api/mkdir Create a new folder
POST /api/rename Rename a file (extension preserved)
POST /api/move Move a file (returns 409 on name conflict)
POST /api/list-dirs List subdirectories (used by the move dialog’s mini explorer)
GET /api/disk-space Get used / free / total disk space via statfs
POST /api/jellyfin-titles Batch-resolve Jellyfin display titles and poster URLs for a list of file paths
GET /api/login-posters Return up to 50 Jellyfin poster image URLs for the login background
POST /api/generate-thumbnail Generate a styled SVG poster for a given title
POST /api/set-jellyfin-thumbnail Save a JPEG poster to disk and trigger a Jellyfin item refresh
GET /api/search Recursively search the entire storage root for matching files and folders
GET /api/audit Fetch the JSON activity log history
GET /activity Render the Activity Log HTML page

POST /api/jellyfin-titles

Accepts a list of relative file paths and returns a map of { relativePath: { title, posterUrl } } for any files that have an exact path match in the Jellyfin library.

// Request
{ "paths": ["Movies/Black_Adam_2022.mkv", "Movies/Goldfinger.mkv"] }

// Response
{
  "Movies/Black_Adam_2022.mkv": { "title": "Black Adam", "posterUrl": "http://..." },
  "Movies/Goldfinger.mkv":      { "title": "Goldfinger",  "posterUrl": "http://..." }
}

Files with no Jellyfin match are omitted β€” no poster or title badge is shown for them.

POST /api/generate-thumbnail

Accepts { "title": "My Movie" } and returns a raw image/svg+xml response. The SVG is 1000 Γ— 1500 px, uses logo.png as the background, and auto-sizes the title text based on length.

POST /api/set-jellyfin-thumbnail

Accepts { "path": "Movies/file.mkv", "imageBase64": "<jpeg data>" }. Saves the image as Movies/file-poster.jpg alongside the source file, then searches Jellyfin for the matching item and triggers a FullRefresh so the thumbnail updates in Jellyfin without a manual scan.


🎞️ Jellyfin Title Matching β€” How It Works

The title overlay feature is designed to be filename-agnostic. It doesn’t try to parse or clean up your filenames to guess the title β€” instead it uses the filesystem path as a unique identifier:

  1. The frontend collects the relative paths of all file cards currently on screen
  2. One batch request is sent to /api/jellyfin-titles
  3. The server fetches all items from Jellyfin (/Items?Recursive=true&Fields=Path) in a single call
  4. Each item’s Path field (the absolute disk path Jellyfin scanned) is matched against STORAGE_ROOT + relPath
  5. Only exact matches are returned β€” there is no fuzzy fallback

This means a file named Black_Adam_2022_1080P.mkv correctly resolves to β€œBlack Adam” even though the filename shares no resemblance to the title.


πŸ› οΈ Utilities

user_create.py β€” Bulk Jellyfin User Creation

Creates numbered Jellyfin user accounts in a given range. Reads API_KEY from .env and writes a shell script (user_script.sh) of curl commands, which you then execute against your Jellyfin server.

pip install python-dotenv
python user_create.py
# Then review user_script.sh and run it:
bash user_script.sh

By default the script generates accounts for user IDs 100–499 and 600–699 (the range 500–599 is intentionally skipped). Edit the loop bounds in the script to match your needs.


🐳 Docker Deployment Guide

This guide explains how to containerize JellyBeans using Docker and Docker Compose.

Note: Ensure your environment variables are set correctly before deploying:


1. Dockerfile

Create a file named Dockerfile in your root directory:

# ---- Build Stage ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

# ---- Runtime Stage ----
FROM node:20-alpine
WORKDIR /app

# Create a non-root user for security
RUN addgroup -g 1001 -S jellybeans && \
    adduser -u 1001 -S jellybeans -G jellybeans

# Copy dependencies and application code
COPY --from=builder /app/node_modules ./node_modules
COPY . .

# Ensure the user has permissions for the app directory
RUN chown -R jellybeans:jellybeans /app
USER jellybeans

EXPOSE 3000

# ---- Default Environment Variables ----
ENV PORT=3000
ENV STORAGE_PATH=/media
ENV JELLYFIN_URL=http://jellyfin:8096
ENV API_KEY=
ENV SESSION_SECRET=change-me-to-a-random-string
ENV ADMIN_USER=admin
ENV ADMIN_PASS=password123

# Mount your media library here
VOLUME ["/media"]

CMD ["node", "server.js"]

2. Running with Docker CLI

Build the image:

docker build -t jellybeans .

Run the container (ensure the local path has read/write permissions):

docker run -d \
  --name jellybeans \
  -p 3000:3000 \
  -v /path/to/your/media:/media \
  -e JELLYFIN_URL=http://your-jellyfin-ip:8096 \
  -e API_KEY=your_jellyfin_api_key \
  -e ADMIN_USER=my_user \
  -e ADMIN_PASS=my_strong_password \
  -e SESSION_SECRET=$(openssl rand -hex 32) \
  jellybeans

3. Running with Docker Compose

Create a docker-compose.yml file for easier management:

version: '3.8'
services:
  jellybeans:
    image: jellybeans
    build: .
    container_name: jellybeans
    ports:
      - "3000:3000"
    volumes:
      - /path/to/your/media:/media
    environment:
      - PORT=3000
      - STORAGE_PATH=/media
      - JELLYFIN_URL=http://jellyfin:8096
      - API_KEY=your_api_key_here
      - ADMIN_USER=admin
      - ADMIN_PASS=password123
      - SESSION_SECRET=a-very-secret-string
    restart: unless-stopped

βš™οΈ Configuration Notes

Variable Description
STORAGE_PATH Must match the internal path of your volume mount (default: /media)
API_KEY Your Jellyfin API key
ADMIN_USER / ADMIN_PASS Login credentials for the JellyBeans explorer
SESSION_SECRET A random string used to sign sessions β€” change before deploying

⚠️ Security: Change ADMIN_USER and ADMIN_PASS from their defaults before deploying to any public-facing server. The app writes -poster.jpg files and metadata directly into your media folders, so ensure your Docker volume is mounted with read/write access.


πŸ›‘οΈ Security Notes


🀝 Contributing

Pull requests are welcome. For significant changes, open an issue first to discuss what you’d like to change.


πŸ“œ License

MIT