A self-hosted, password-protected web file manager with deep Jellyfin integration β browse, upload, organize, identify, and thumbnail your media library from any browser.
- Upload files β drag-and-drop or click-to-browse, with real-time progress bar
- Custom filename on upload β single-file uploads show an editable name field; the original extension is always preserved automatically
- Multi-file upload β select and upload multiple files in one batch
- Download β download any file directly from the file context menu
- Rename β rename files in-place; the extension is preserved unless you explicitly change it
- Move β relocate files using a built-in mini folder explorer inside the modal; handles name conflicts with a rename-and-move prompt
- Delete β permanently delete files with a confirmation dialog
- Create folders β add new subdirectories from any location
- Bulk Actions & Smart Selection β Google Drive-style file selection. Hold
Shift + Clickto 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. - File info panel β per-file details including size on disk, upload/modification date, image resolution (for image files), and word count (for
.txt,.md,.csv,.srtfiles) - Activity Log β an audit trail accessible via the top actions bar that tracks and displays every file modification (uploads, deletes, renames, moves, and thumbnail generations).
- β Grid / β° List view β switchable layout, persisted across sessions via
localStorage - π Global library search β debounced recursive search across your entire
STORAGE_ROOT, returning matching files and folders with their parent paths. Sorting preferences and Jellyfin thumbnails are automatically applied to search results. - Smart file sorting β folders always appear first, then files grouped by type (video, image, audio, etc.), then alphabetically within each group
- Automatic file-type icons β emoji icons assigned by extension across images, video, audio, archives, executables, code, documents, and web files; unrecognised files fall back to π
- Advanced File Sorting β Sort your current directory dynamically by Name, Date, or Size. Toggle between Ascending (β) and Descending (β) order with a single click. Folders are always smartly pinned to the top of the grid regardless of the sort metric, and your sorting preferences are persisted across sessions.
- Hidden file filtering β
-poster.jpg,.nfo, and.bifsidecar files are automatically hidden from the UI so your grid stays clean - Sticky header β the toolbar, upload form, and storage bar remain visible while scrolling; a subtle shadow appears when the header is pinned
- Responsive design β works on mobile and desktop
- Session-based authentication β login wall with server-side session management; all routes are protected by
reqLoginmiddleware - URL-hopping protection β navigating directly to a deep directory URL is blocked unless the request carries a valid
Refererheader from the same host; bypasses are silently redirected to the root explorer - Path traversal protection β every file operation validates the resolved absolute path starts with
STORAGE_ROOTbefore touching the disk; invalid paths return 403
- Real-time storage bar at the top of the screen shows used / free / total space
- Fill colour shifts from blue β amber at 75% used β red at 90% used, so storage pressure is instantly visible before uploads
- Poster thumbnails β fetches and displays official Jellyfin primary-image posters as 2:3 ratio cards in grid view and compact square thumbnails in list view
- Title overlays β toggleable
[ Title ]badge beneath each filename; state persisted vialocalStorage - Batch path matching β a single request resolves all visible file cards at once using exact filesystem-path comparison (no fuzzy guessing), so
Black_Adam_2022_Χͺ.Χ_1080P.mkvcorrectly resolves to "Black Adam" - Cinematic login background β the login page dynamically fetches up to 50 Jellyfin posters and renders them as four animated, alternating-direction rows behind a frosted-glass login panel; degrades gracefully if Jellyfin is unavailable
Accessible from the file context menu as "Create Thumbnail":
- The server reads the file's Jellyfin title (or falls back to the filename)
- A styled SVG poster is generated server-side at 1000 Γ 1500 px using
frontend/logo.pngas the background image (falls back to a dark gradient if the file is missing) - 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
- The SVG is rasterised to JPEG (quality 90%) on the client via
<canvas>and previewed in the dialog before applying - On confirmation, the JPEG is saved to disk alongside the video as
<filename>-poster.jpgand Jellyfin is triggered to perform aFullRefreshon the matched item β so the new thumbnail appears in Jellyfin immediately without a manual library scan
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.
- The server will sequentially process every selected video, generate adaptive SVGs, rasterise them, and push them to Jellyfin.
- A live, un-interruptible progress bar modal displays the exact file currently being processed and reports any individual failures at the end of the batch run.
- Node.js v18+
- A running Jellyfin server (optional, for title overlays, poster art, and thumbnail upload)
# 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.jsThe 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
# 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_keyCreate a .users file in the project root containing plaintext user credentials (one per line):
user1 pass1
user2 pass2
admin secretpassword
Format: username password (space-separated)
Note: Since this is a local, self-hosted application with direct filesystem access, plaintext credentials are acceptable. Ensure proper filesystem permissions and do not expose this file over the network.
- Open Jellyfin β Dashboard β API Keys
- Click + to create a new key
- Paste it into
API_KEYin your.env
Important: 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 {{mustache}} 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)
βββ .users # Your local users (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 |
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.
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.
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.
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:
- The frontend collects the relative paths of all file cards currently on screen
- One batch request is sent to
/api/jellyfin-titles - The server fetches all items from Jellyfin (
/Items?Recursive=true&Fields=Path) in a single call - Each item's
Pathfield (the absolute disk path Jellyfin scanned) is matched againstSTORAGE_ROOT + relPath - 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.
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.shBy 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)- 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
# 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 SESSION_SECRET=$(openssl rand -hex 32) \
jellybeansCreate 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
- ./.users:/app/.users:ro
environment:
- PORT=3000
- STORAGE_PATH=/media
- JELLYFIN_URL=http://jellyfin:8096
- API_KEY=your_api_key_here
- SESSION_SECRET=a-very-secret-string
restart: unless-stoppedImportant: Mount your
.usersfile into the container as read-only (:ro) to provide login credentials.
| Variable | Description |
|---|---|
STORAGE_PATH |
Must match the internal path of your volume mount (default: /media) |
API_KEY |
Your Jellyfin API key |
SESSION_SECRET |
A random string used to sign sessions β change before deploying |
.users file |
Plaintext user credentials (one per line in format: username password) β required for login |
β οΈ Security: The app writes-poster.jpgfiles and metadata directly into your media folders, so ensure your Docker volume is mounted with read/write access.
- Authentication: Users log in via the
.usersfile; credentials are plaintext since this is a self-hosted local application - All file operation routes are protected by
reqLoginmiddleware β unauthenticated requests are redirected to/login.html - All file paths are validated against
STORAGE_ROOTusingstartsWith()before any disk operation β path traversal (e.g.../../etc/passwd) is rejected with a 403 - URL-hopping to deep directories is blocked unless the request has a valid
Refererheader from the same host - Activity Log: All file operations are logged with the authenticated username in
jellybeans-audit.logfor audit purposes - Sessions use a configurable
SESSION_SECRETβ set this to a long random value in production
Pull requests are welcome. For significant changes, open an issue first to discuss what you'd like to change.
MIT