A self-hosted, password-protected web file manager with deep Jellyfin integration β browse, upload, organize, identify, and thumbnail your media library from any browser.
Shift + Click to select a massive range of files, or click to toggle individual items. A floating action bar allows you to Batch Move, Batch Delete, or Batch Generate Thumbnails for all selected items simultaneously..txt, .md, .csv, .srt files)localStorageSTORAGE_ROOT, returning matching files and folders with their parent paths. Sorting preferences and Jellyfin thumbnails are automatically applied to search results.-poster.jpg, .nfo, and .bif sidecar files are automatically hidden from the UI so your grid stays cleanreqLogin middlewareReferer header from the same host; bypasses are silently redirected to the root explorerSTORAGE_ROOT before touching the disk; invalid paths return 403[ Title ] badge beneath each filename; state persisted via localStorageBlack_Adam_2022_Χͺ.Χ_1080P.mkv correctly resolves to βBlack AdamβAccessible from the file context menu as βCreate Thumbnailβ:
frontend/logo.png as the background image (falls back to a dark gradient if the file is missing)<canvas> and previewed in the dialog before applying<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 scanDonβ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.
# 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>.
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
API_KEY in your .envImportant: Your
STORAGE_PATHand 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.
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.
βββ 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
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-titlesAccepts 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-thumbnailAccepts { "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-thumbnailAccepts { "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.
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:
/api/jellyfin-titles/Items?Recursive=true&Fields=Path) in a single callPath field (the absolute disk path Jellyfin scanned) is matched against STORAGE_ROOT + relPathThis means a file named Black_Adam_2022_1080P.mkv correctly resolves to βBlack Adamβ even though the filename shares no resemblance to the title.
user_create.py β Bulk Jellyfin User CreationCreates 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.
This guide explains how to containerize JellyBeans using Docker and Docker Compose.
Note: Ensure your environment variables are set correctly before deploying:
- Use
STORAGE_PATH(notMEDIA_ROOT) andAPI_KEY(notJELLYFIN_API_KEY)ADMIN_USERandADMIN_PASSare required to access the explorer- Do not mount your media volume as read-only (
:ro) β the app needs write access for deletes, renames, and thumbnails
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"]
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
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
| 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_USERandADMIN_PASSfrom their defaults before deploying to any public-facing server. The app writes-poster.jpgfiles and metadata directly into your media folders, so ensure your Docker volume is mounted with read/write access.
reqLogin middleware β unauthenticated requests are redirected to /login.htmlSTORAGE_ROOT using startsWith() before any disk operation β path traversal (e.g. ../../etc/passwd) is rejected with a 403Referer header from the same hostSESSION_SECRET β set this to a long random value in production.env; there is no multi-user or role system at the application levelPull requests are welcome. For significant changes, open an issue first to discuss what youβd like to change.
MIT