No description
  • TypeScript 67.5%
  • JavaScript 21.7%
  • CSS 7.2%
  • HTML 3.3%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sebastian Leheis bbf9832ee8
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Point Docker users at the published image
Images now live at git.ar21.de/sebleh/moviematch, so the instruction to
build one locally first is out of date. The compose file pulls :latest
and carries a commented-out build line for anyone who would rather build
from the checkout. Both docker run examples name the published image too.

A release can be pinned with a version tag such as :2.2.0. Those numbers
match the version field in package.json and the v-prefixed git tags, so
one number identifies a release everywhere.

While in there: the compose documentation claimed a named volume called
moviematch-data. It has always been a bind mount of ./data, which is why
that directory has to belong to UID 1000. Anyone backing up by the old
description would have saved the wrong thing.
2026-09-21 17:30:02 +02:00
docs Point Docker users at the published image 2026-09-21 17:30:02 +02:00
i18n Mark the films you have already watched 2026-09-15 21:49:45 +02:00
public Mark the films you have already watched 2026-09-15 21:49:45 +02:00
src Mark the films you have already watched 2026-09-15 21:49:45 +02:00
.dockerignore initial commit 2026-09-08 01:28:00 +02:00
.editorconfig redesign finish 2026-09-08 10:51:11 +02:00
.env.example Bring the README in line with how sign-ins actually work 2026-09-11 22:52:53 +02:00
.gitignore Stop tracking FEATURES.md 2026-09-11 23:25:36 +02:00
.woodpecker.yaml Add CI 2026-09-21 17:16:32 +02:00
docker-compose.yml Point Docker users at the published image 2026-09-21 17:30:02 +02:00
Dockerfile initial commit 2026-09-08 01:28:00 +02:00
LICENSE Record the fork's own copyright, and raise the version to 2.0.3 2026-09-15 16:49:10 +02:00
package-lock.json Mark the films you have already watched 2026-09-15 21:49:45 +02:00
package.json Mark the films you have already watched 2026-09-15 21:49:45 +02:00
README.markdown Point Docker users at the published image 2026-09-21 17:30:02 +02:00
tsconfig.json initial commit 2026-09-08 01:28:00 +02:00

MovieMatch

What is this?

Have you ever spent longer deciding on a movie than it'd take to just watch a random movie? MovieMatch is an app that helps you and your friends pick a movie to watch from a Plex or Jellyfin media server.

How it works

MovieMatch connects to your Plex or Jellyfin server and gets a list of movies from any libraries marked as a movie library.

As many people as you want connect to your MovieMatch server and get a list of shuffled movies. Swipe right to 👍, swipe left to 👎.

A title is a match when every participant in the room has rated it and all ratings are positive. Participants are all users who have made at least one swipe. In single-user mode (when only one person has swiped), all their likes are matches. Once a second person makes their first swipe, they become a participant and only titles rated positively by both are matches. Existing matches may disappear at this point if they don't meet the new criteria. Note that matches can also disappear if someone swipes left on a title that was previously a match.

Rooms and ratings persist across server restarts — thanks to SQLite, your matches and ratings are saved to disk and will be available when MovieMatch starts again.

Screenshots

The screens below were captured against a mock media server, so every title, name and picture in them is made up.

Joining a room

The password field and the playlist checkbox only appear when the server runs with BACKEND=jellyfin. Leave the password empty to join without signing in.

Login form with name, room code, optional Jellyfin password and playlist checkbox

Swiping

Swipe right to 👍, left to 👎, or use the buttons below the deck.

Card deck showing a film poster with the title and year

The back of a card

Tap a card to turn it over: title linked to the media server, year and director, the summary, and the rating from your own library.

Back of a card showing summary, year, director and rating

Matches

The room code, the share and export buttons, and every title everyone agreed on.

Match list with room code, action buttons and match count

The user menu

After signing in with Jellyfin your profile picture appears in the top right corner. Without a picture in Jellyfin, the first letter of your name is shown instead.

Open user menu with a log out entry

Losing the connection

A small banner counts down to the next attempt. Ratings made while offline are kept and sent once the connection is back.

Floating banner reading "Offline — neuer Versuch in 3 s" above the card deck

Getting started

With Node.js (local)

  • Ensure you have Node.js 24 or later installed
  • Clone or download this repository
  • Create a .env file in the repository root (see .env.example for an example)
  • Run npm ci to install dependencies
  • Run npm run build to compile TypeScript
  • Run node dist/index.js to start the server

Open localhost:8000

With Docker

Images are published to git.ar21.de/sebleh/moviematch. Use :latest for the newest build, or a version tag such as :2.2.0 to pin a release and decide yourself when to move on. The version numbers match the version field in package.json and the v-prefixed git tags.

docker pull git.ar21.de/sebleh/moviematch:latest

To build the image from this checkout instead:

docker build -t moviematch:local .

Important: Directory Permissions

When mounting a host directory into the container, it must be writable by UID 1000 (the node user inside the container). If the directory is not writable, the container will start but fail when the first user tries to join.

Set up the directory with the correct permissions:

mkdir -p ./data && sudo chown -R 1000:1000 ./data

If you don't set the correct permissions, you will see an error message like:

Failed to initialize database.
Database path: /data/moviematch.db
Directory: /data (running as UID 1000, GID 1000)

If you mounted a host directory into the container, make sure it is writable.
Example fix: mkdir -p ./data && sudo chown -R 1000:1000 ./data

The easiest way to run MovieMatch with Docker is using docker-compose. See the docker-compose documentation for details.

With Docker directly

Plex example:
docker run -d \
  -e BACKEND=plex \
  -e PLEX_URL=https://plex.example.com:32400 \
  -e PLEX_TOKEN=your_token_here \
  -p 8000:8000 \
  -v ./data:/data \
  git.ar21.de/sebleh/moviematch:latest
Jellyfin example:
docker run -d \
  -e BACKEND=jellyfin \
  -e JELLYFIN_URL=https://jellyfin.example.com \
  -e JELLYFIN_API_KEY=your_api_key_here \
  -p 8000:8000 \
  -v ./data:/data \
  git.ar21.de/sebleh/moviematch:latest

The -v ./data:/data flag binds a host directory to the container, so your ratings and matches persist across restarts.

Configuration

The following variables are supported via a .env file or environment variables.

Name Description Required Default
BACKEND Which media server to use: plex or jellyfin No plex
PLEX_URL URL of the Plex server, e.g. https://plex.example.com:32400 Only when BACKEND=plex null
PLEX_TOKEN Plex authentication token (How to find yours) Only when BACKEND=plex null
JELLYFIN_URL URL of the Jellyfin server, e.g. https://jellyfin.example.com Only when BACKEND=jellyfin null
JELLYFIN_API_KEY Jellyfin API key (Dashboard → API Keys) Only when BACKEND=jellyfin null
JELLYFIN_USER_ID Jellyfin user whose libraries are used. Must be a 32-character hex id or a GUID. If unset, the first user on the server is used. No first user
DATABASE_PATH Path to the SQLite database file. The directory is created if missing. No ./data/moviematch.db
PORT The port the server will run on No 8000
ROOT_PATH The root path to use when loading resources. For example, if MovieMatch is on a sub-path, set this to that sub-path (without a trailing slash) No ''
LIBRARY_FILTER A comma-delimited list of libraries to include, e.g. Films or Films,Television. For Jellyfin, matches against library/view names. No The first library with the type of DEFAULT_SECTION_TYPE_FILTER
COLLECTION_FILTER A comma-delimited list of collections to include, e.g. Marvel or Marvel,HBO. For Jellyfin, this filters by BoxSet names. No ''
DEFAULT_SECTION_TYPE_FILTER The first library with this type will be chosen as a default library. For Jellyfin: movie → movies, show → tvshows, artist → music, photo → homevideos No movie
LINK_TYPE The method to use for opening match links (Plex only; Jellyfin always links to its web UI) No app (can be app, http, or plex.tv)
LOG_LEVEL How much the server should log No INFO (can be DEBUG, INFO, WARNING, ERROR, or CRITICAL)
MOVIE_BATCH_SIZE How many movies to load initially. Leave this alone unless you run out of cards really quickly. No 25
RATE_LIMIT_ENABLED Enable rate limiting to protect against abuse. When enabled, requests exceeding the per-minute limits will not receive a response and will time out. No true
RATE_LIMIT_HTTP_PER_MINUTE Maximum HTTP requests per IP address per minute No 300
RATE_LIMIT_WS_PER_MINUTE Maximum WebSocket connection attempts per IP address per minute No 20
RATE_LIMIT_MESSAGES_PER_MINUTE Maximum WebSocket messages per IP address per minute No 300
RATE_LIMIT_LOGIN_PER_MINUTE Maximum Jellyfin sign-in attempts per IP address per minute No 10
SESSION_TTL_HOURS How long a Jellyfin sign-in is kept in server memory. When the time is up, the session is discarded and its access token is invalidated at the Jellyfin server. No 12
TRUST_PROXY Trust X-Forwarded-For header for client IP detection. Only enable if MovieMatch runs behind a trusted reverse proxy (nginx, HAProxy, Apache). When disabled, each rate limit applies per proxy IP. When enabled, limits apply per origin IP. No false

Share, Export and Import

Flip Cards

Tap on a card to flip it and see additional information: title (linked to the media server), year and director, summary, and rating (if available). The rating comes from your Plex or Jellyfin library — no external service is queried. Clicking the title link opens the media in your server in a new tab.

Already Watched

When you are signed in with a Jellyfin account, a green tick appears in the top right corner of a card whose title you have already marked as watched in Jellyfin.

The status is read with your own Jellyfin account, not the one the server uses to read the library, so it is your viewing history you see and nobody else's. It is shown and nothing more: it is never written to the database and never logged. Without a Jellyfin sign-in no tick appears, and Plex does not offer the feature at all.

If the lookup fails, the tick is simply missing; everything else carries on unaffected.

Undo Last Rating

An Undo button below the thumbs appears after your first swipe. It removes your most recent rating and returns the card to the deck. If this causes a title to no longer be a match, it is removed from everyone's match list. The undo history is only kept for the current session; reloading the page disables the button.

When viewing matches, a Share button (centered in the matches section) lets you share the room with others:

  • On devices that support it, opens the native share menu
  • Otherwise, copies the link to your clipboard

The link includes the room code as a URL parameter (?room=<CODE>) and will pre-fill the code when someone joins.

Export Matches to CSV

Matches can be exported to CSV format via:

GET /api/rooms/<CODE>/matches.csv

The CSV contains the following columns: Title, Year, Director, Rating, Type, Likes, Users, Link.

An Export button in the matches view provides convenient access to this endpoint.

Export Personal Likes to CSV

You can also export only your own positive ratings:

GET /api/rooms/<CODE>/likes.csv?user=<Name>

The CSV contains the following columns: Title, Year, Director, Rating, Type, Link (no Likes or Users).

A Likes button in the matches view provides convenient access. Note that like the matches endpoint, this is unauthenticated: anyone who knows the room code and a person's name can download their likes.

Import a CSV

Starting a room with different people means swiping the same films again. An Import CSV button in the matches view takes a list you exported earlier and marks those titles as liked by you.

Both exports are accepted, the room one and your personal one. Only the Link column is read: it carries the identifier your media server uses for the title. The Likes and Users columns are never even sent to the server, so an import can only ever create your own ratings, never someone else's. A room export listing Anna as the one who liked a film produces a rating by whoever imports it, and nothing else.

A few things follow from matching on the identifier alone:

  • A file from a different media server finds nothing, and so does one exported before the library was rebuilt. The import reports how many entries it matched, so zero is visible rather than silent.
  • Titles you have already rated in this room are left alone. The import only fills in what is missing; it never overturns a decision made here.
  • At most 1000 entries are read from a file.

The page reloads once the import is done, because the deck and the match list are built when you join and know nothing of the new ratings.

FAQ

Can a user get my media server credentials?

No. The client never talks directly to your media server. All requests that need authentication (querying content, retrieving images, etc.) are made by the MovieMatch server itself.

Only a subset of the server response is sent to the client to minimize the chance of sensitive information leaking.

Can it do TV shows too?

Yes, you can include a TV library in your LIBRARY_FILTER list or set DEFAULT_SECTION_TYPE_FILTER=show.

What data does MovieMatch store?

MovieMatch uses SQLite to persist:

  • Room metadata (room codes, creation time)
  • User metadata (names, creation time per room, and optionally the Jellyfin user ID if the user authenticated with Jellyfin)
  • User ratings for each movie (which users swiped right/left on which movies)

The database file is stored at the path specified by DATABASE_PATH (default: ./data/moviematch.db). Deleting this file will erase all saved ratings and matches.

All other data is kept in memory while the server runs. Passwords are never stored anywhere. A Jellyfin access token is held in server memory only, never on disk, and only for as long as the sign-in session lasts (see SESSION_TTL_HOURS).

Do you gather any data outside the database?

No. The server is entirely local to you and will work offline.

Do you support languages other than English?

Yes. The server will use your browser's preferred language by default if it's supported. Otherwise it'll fall back to English.

The translations can be found in the i18n folder.

The file names follow BCP47 naming. Feel free to submit a Pull Request if you'd like your language to be supported.

Can I run MovieMatch behind a reverse proxy?

Yes, you can read some documentation here


⚠️ Security Notice

MovieMatch does not include authentication. Anyone with access to the room code (or who can guess the CSV export endpoint) can view the matches and ratings for that room.

Do not expose an unprotected MovieMatch instance to the public internet. Use a reverse proxy with authentication, a firewall, or run it on a private network only.

Jellyfin User Authentication

MovieMatch supports optional authentication with a Jellyfin user account (available only with BACKEND=jellyfin). This feature is controlled by a Jellyfin password field in the login form, below the room code field. Leave it empty to join without authentication. Below the password field is an optional checkbox to automatically sync matches to a Jellyfin playlist. Both the password field and the checkbox appear whenever the server runs with BACKEND=jellyfin; the checkbox only takes effect if you actually enter a password, since the playlist is created in the account you sign in with.

When you authenticate, your password is transmitted from the browser to the MovieMatch server and then to the Jellyfin server. On unsecured connections (plain HTTP), the password travels in cleartext and can be intercepted — HTTPS is strongly recommended.

MovieMatch never stores passwords, and no Jellyfin credential is ever written to disk. The access token returned by Jellyfin is held in server memory as part of a session, which is addressed by an HttpOnly cookie and survives a page reload. The session is discarded after SESSION_TTL_HOURS (12 hours by default), or immediately when you log out, and its access token is invalidated at the Jellyfin server at that point. A sweeper checks for expired sessions every five minutes. When you log in with a Jellyfin account, the server records your Jellyfin user ID in the database — only the ID, never the password or token. This ID is used to enforce name reservations: once a name is used with Jellyfin authentication in a room, that name becomes locked to that specific Jellyfin account. Subsequent logins with that name require the same Jellyfin password. If you later run the server with BACKEND=plex, the lock remains in place and the name becomes unusable by anyone. Names that were never used with Jellyfin remain freely available.

Staying Signed In

The sign-in survives a page reload. If you reload, MovieMatch takes you straight back to the room you were in, without asking for the password again — the session is addressed by the cookie described above.

Two controls are available once you are signed in:

  • Change room sits with the other buttons below the match list. It returns you to the login form so you can join a different room. Your name and your Jellyfin session are kept, so no password is needed as long as you keep the same name. The page reloads, which clears the card deck, the match list and the undo history.
  • Your Jellyfin profile picture appears in the top right corner. Clicking it opens a menu with a Log out entry. Logging out ends the session on the MovieMatch server, invalidates the access token at the Jellyfin server, and forgets your name and room in the browser, so nothing of yours is left behind on a shared machine. If you have no profile picture in Jellyfin, the first letter of your name is shown instead.

The picture is fetched by the MovieMatch server and passed on to the browser, which never receives a Jellyfin token. The server identifies you from the session cookie alone, so nobody can request someone else's picture.

Jellyfin Playlist Sync

If you check the create playlist option, MovieMatch automatically keeps a playlist in sync with the room's current matches. The playlist is created in your Jellyfin account and named after the room participants and code, for example Anna, Bert – ABCD. When participants are added, the name updates. As matches change (titles added or removed), the playlist is kept in sync — titles are added when they become matches and removed when they no longer meet the criteria. Each authenticated user gets their own playlist in their account. Only participants authenticated with Jellyfin maintain playlists; unauthenticated users swipe normally but do not contribute to any playlist.

Credits and license

MovieMatch was written by Luke Channings. This repository is a fork of its v1 branch and stays under the same Apache License 2.0 — the full text is in LICENSE.

The fork changed a good deal of the original:

  • ported from Deno to Node.js and TypeScript
  • Jellyfin added as a second backend next to Plex
  • rooms, users and ratings kept in SQLite instead of memory
  • optional sign-in with a Jellyfin account, with matches synced to a playlist
  • rate limiting, CSV export, share links, an undo button, a reworked interface

The Apache License grants no rights to the name or the logo of the original project.