Framekeeper is a self-hosted web application for inspecting, organizing, and deduplicating a movie and TV library stored on a NAS. It scans video files, extracts technical metadata with ffprobe, groups likely duplicates, scores their quality, and helps you reclaim space without immediately deleting anything.
The interface automatically uses Spanish (Spain) when the browser's preferred language is Spanish, and English for every other language. This documentation is in English.
- Browses movies and TV series from a single web interface.
- Groups series by show and season.
- Parses release names to identify titles, years, seasons, episodes, quality tags, languages, and release groups.
- Uses
ffprobeto inspect resolution, codecs, HDR, bitrate, audio channels, languages, container, and duration. - Detects likely duplicate movies and episodes.
- Assigns configurable quality scores and recommends which copy to keep.
- Runs library scans in the background with progress reporting and cancellation.
- Caches scan results and technical metadata in SQLite.
- Optionally displays movie and TV poster artwork from TMDB.
- Moves unwanted files into a recoverable trash directory on the NAS.
- Restores trashed files or permanently removes them after explicit confirmation.
- Checks the NAS mount and can retry mounting it through a narrowly scoped
sudoersrule.
Framekeeper normalizes movie and series names, groups matching releases, and compares the files in each group. Its score combines:
- Resolution
- Source quality
- HDR format
- Audio codec and channel layout
- Bitrate relative to the other copies in the group
The weights are configured in config.json. When scores are effectively tied, file size is used as a tie-breaker. Recommendations are guidance only: Framekeeper does not automatically delete duplicate files.
Moving an item to trash relocates it to a #trash-mdmgr directory inside the corresponding movie or series library. It can then be restored from the UI. Emptying the trash permanently deletes the selected files and requires explicit confirmation.
Framekeeper creates a .plexignore file in each existing trash directory so Plex does not index recoverable files as movies or TV seasons. After upgrading, restart Framekeeper and rescan the affected Plex library to remove any trash items that Plex indexed previously.
Framekeeper changes real files on the NAS. Before using its trash features, make sure your library is backed up and the configured movie, series, and trash paths are correct.
Credentials are deliberately kept outside this repository. config.json, .env files, credential files, private keys, the local SQLite database, and local editor/agent settings are excluded by .gitignore.
- Linux
- Python 3.10 or newer
- Flask
ffmpeg/ffprobemount.cifsfromcifs-utilswhen using the automatic NAS mount setup- A CIFS/SMB-accessible NAS library
On Debian or Ubuntu, the system packages can be installed with:
sudo apt update
sudo apt install python3 python3-flask ffmpeg sqlite3 cifs-utilsAlternatively, install Flask in a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install FlaskCopy the example configuration and edit the local copy:
cp config.example.json config.jsonThe configuration has four sections:
| Section | Purpose |
|---|---|
nas |
NAS address, SMB share, mount point, library folders, trash folder, credential file, and mount wrapper |
db_path |
Path of the local SQLite database |
server |
Address and port used by the Flask server |
tmdb |
Optional TMDB read token and metadata language used for poster artwork |
scoring |
Weights used for duplicate quality recommendations |
Example NAS settings:
{
"nas": {
"host": "nas.local",
"share": "media",
"mount_point": "/mnt/nas-media",
"movies_dir": "MOVIES",
"series_dir": "SERIES",
"trash_dirname": "#trash-mdmgr",
"credentials_file": "/home/YOUR_USER/.config/mdmgr/nas.cred",
"mount_wrapper": "/usr/local/sbin/mdmgr-mount-nas.sh"
}
}movies_dir and series_dir are resolved relative to mount_point.
Framekeeper can look up movie and TV posters from TMDB. Create an API Read Access
Token in your TMDB account settings, then either add it to the local config.json:
"tmdb": {
"read_access_token": "YOUR_TMDB_API_READ_ACCESS_TOKEN",
"language": "es-ES"
}or provide it through the TMDB_READ_ACCESS_TOKEN environment variable. The
token stays on the server. Matches and missing results are cached in SQLite, and
temporary TMDB errors are retried later. Restart Framekeeper after changing the
configuration; posters are loaded as the movie and series views are opened.
TMDB integration is entirely optional. If the token is omitted or left empty, Framekeeper does not contact TMDB and the library remains fully functional with placeholder artwork.
Create the credential file outside the repository:
mkdir -p ~/.config/mdmgr
chmod 700 ~/.config/mdmgr
printf 'username=YOUR_NAS_USER\npassword=YOUR_NAS_PASSWORD\n' > ~/.config/mdmgr/nas.cred
chmod 600 ~/.config/mdmgr/nas.credDo not commit this file or paste its contents into issues, logs, or documentation.
The application checks whether the configured mount point is mounted when it starts. If it is not, it invokes the configured wrapper with non-interactive sudo.
After configuring config.json and creating the credential file, install the generated wrapper and its restricted sudoers rule:
sudo sh scripts/install_sudoers.shThe installer:
- Reads the NAS host, share, and mount point from the local configuration.
- Generates
/usr/local/sbin/mdmgr-mount-nas.shwithout embedding the NAS password. - Installs a
sudoersentry that permits only that wrapper to run without a password. - Restricts the NAS credential file to the local user.
Review both generated files before relying on this setup, especially on multi-user machines.
Start the application from the project directory:
python3 app.pyOpen http://127.0.0.1:5115 in a browser, or use the host and port from config.json.
The SQLite schema is initialized automatically. The database is local runtime state and is not committed to Git.
If the server binds to 0.0.0.0, other devices on the network may be able to reach it. Framekeeper currently has no authentication layer, so do not expose it directly to the public internet. Prefer binding to 127.0.0.1, a trusted private network, or placing it behind an authenticated reverse proxy.
- Open Framekeeper and confirm that the NAS is mounted.
- Select Escanear biblioteca to index the library.
- Browse movies and series or open the duplicates view.
- Compare the metadata and quality score for each duplicate group.
- Move unwanted copies to the recoverable trash.
- Verify the remaining library before permanently emptying the trash.
api/ Flask JSON API endpoints
db/ SQLite access and schema
mount/ NAS mount status and retry logic
scanner/ File walking, parsing, ffprobe, scoring, and deduplication
scripts/ Restricted CIFS mount and sudoers setup
static/ Browser interface (HTML, CSS, and JavaScript)
tests/ Unit and API tests
app.py Application entry point
config.py Configuration loader
The browser UI uses a JSON API under /api:
/api/mount— mount status and retry/api/scan— start, monitor, and cancel scans/api/movies— movie listing and details/api/series— show, season, and episode views/api/duplicates— duplicate groups and score details/api/trash— move, list, restore, and permanently purge files
Mutating trash endpoints require an explicit confirm value. The API is intended for the bundled UI and trusted local use; it is not currently authenticated.
Run the test suite with:
python3 -m unittest discover -vThe tests cover filename parsing, title normalization, quality scoring, recommendations, and series trash behavior.
- The web interface currently supports Spanish and English only.
- Filename parsing is heuristic and may not recognize every naming convention.
- Duplicate recommendations depend on filename quality and available
ffprobemetadata. - There is no built-in user authentication or authorization.
- Automatic mounting currently targets Linux CIFS/SMB environments.
- Permanently emptied trash cannot be restored by Framekeeper.
Bug reports and focused pull requests are welcome. Please avoid including media filenames, NAS addresses, filesystem paths, credentials, database files, or other private library information in commits and issue reports.
