McGarrah Technical Blog

Jellyfin Media Integrity Scanner: Why Your Media Library Needs a Health Check

· 6 min read

Your Jellyfin library looks healthy. Every thumbnail loads. Every title appears in the right collection. But somewhere in those terabytes of media, files are silently broken — and you won’t know until someone hits play and gets a black screen, audio glitch, or a crash halfway through a movie.

This is the first article in a series documenting the development of jellyfin-plugin-media-integrity-scanner — a Jellyfin plugin that performs production-safe integrity scanning of your media library. The plugin detects corrupt, truncated, and damaged media files without impacting playback performance or overwhelming your storage infrastructure.

The Problem: Silent Media Corruption

Media files degrade for reasons that have nothing to do with Jellyfin itself:

The common thread: Jellyfin’s library scan only checks that files exist and have parseable metadata. It does not validate that the actual media streams are playable from start to finish.

Why Existing Solutions Fall Short

The Jellyfin ecosystem has a few tools that touch this space:

What’s missing is a purpose-built tool that:

  1. Validates media streams at the byte level using ffmpeg
  2. Runs safely alongside a production Jellyfin instance
  3. Throttles I/O to avoid impacting concurrent playback
  4. Tracks scan results persistently (SQLite)
  5. Provides admin dashboard visibility into library health
  6. Responds to library changes (new files, deletions) automatically

The Project: jellyfin-plugin-media-integrity-scanner

Plugin name: Media Integrity Scanner
Repository: github.com/mcgarrah/jellyfin-plugin-media-integrity-scanner
Namespace: Jellyfin.Plugin.MediaIntegrityScanner
Target: Jellyfin 10.11+ / .NET 9

The naming follows the established Jellyfin plugin convention (jellyfin-plugin-{purpose}) and the C# namespace convention (Jellyfin.Plugin.{PascalCaseName}).

Why .NET 9 and Jellyfin 10.11+

Jellyfin 10.11 (released February 2025) moved the server runtime to .NET 9. Official plugins like jellyfin-plugin-trakt have already followed. The plugin template README still references .NET 8, but it lags behind the server — the Jellyfin server itself, community packaging (Synology, Docker), and first-party plugins have all moved to .NET 9.

Targeting 10.11+ means we get access to the latest APIs (including the EF Core database refactor that shipped in 10.11) and align with where the ecosystem is heading. The next major release (Jellyfin 12.0) will continue on .NET 9. Users on 10.9 or 10.10 are a shrinking group and will need to upgrade for 12.0 regardless.

License: GPL-2.0-or-later

The plugin is licensed GPL-2.0-or-later to match Jellyfin server’s own license. The community uses a mix (MIT, GPL-2.0, GPL-3.0), but matching the server keeps the door open for potential inclusion as a core Jellyfin plugin down the road. GPL-3.0 would have been a compatibility gray area since Jellyfin inherited GPL-2.0 from the Emby fork without explicit “or later” language.

Build environment note: The initial v0.1.0 scaffold builds and passes CI via GitHub Actions (Ubuntu runner, .NET 9 SDK, Jellyfin 10.11.11 NuGet packages). A dedicated Proxmox LXC container is being provisioned for local development and integration testing with jellyfin-ffmpeg and a live Jellyfin instance.

Design Principles

  1. Production-safe by default — Scans are throttled, pausable, and never hold locks on media files during playback.
  2. Two-phase scanning — Fast metadata/header checks first, then opt-in deep byte-stream validation for flagged or all files.
  3. Storage-aware throttling — Configurable I/O limits that respect shared storage (CephFS, NFS, SMB) where other services depend on the same bandwidth.
  4. Persistent state — SQLite database tracks what’s been scanned, when, and the result — so rescans are incremental, not full-library.
  5. Event-driven updates — Hooks into Jellyfin library events to scan new files on add and clean up records on delete.

Article Series Outline

This is a six-part series covering the full development lifecycle:

# Article Focus
1 Introduction (this post) Problem statement, project scope, architecture overview
2 Architecture & Design Decisions Plugin vs. script, scanning strategy, throttling model, SQLite schema
3 Building the Scanner Core FFmpeg integration, cross-platform paths, .NET 9 plugin structure
4 The Dashboard & API Admin UI, REST API controller, real-time scan status
5 Deployment & Operations Proxmox/CephFS deployment, scheduling, monitoring, CI/CD
6 v0.1.1 Release: Update Checker & Auto-Update Update checker, session-aware auto-restart, packaging fixes — what real installs and real testing found that no amount of planning caught first

Architecture at a Glance

flowchart TD
    subgraph JF["Jellyfin Server"]
        subgraph Plugin["Media Integrity Scanner Plugin"]
            LEM["Library Event Monitor"]
            SE["Scan Engine<br/>bounded, thread-safe"]
            DB[("SQLite Cache")]
            API["REST API Controller"]
            UI["Admin Dashboard"]

            LEM -- "queue on add/update" --> SE
            SE -- "store results" --> DB
            DB -- "query" --> API
            API -- "render" --> UI
        end
    end

    SE -- "decode" --> FFmpeg["FFmpeg"]
    SE -- "read-only" --> Media[("Media Files")]

My Infrastructure Context

This plugin is being developed against my homelab setup:

CephFS adds a specific constraint: aggressive sequential reads from a scan can saturate OSD throughput and impact other clients. The throttling design accounts for this.

Current Status (v0.1.0)

The project scaffold is complete and published on GitHub:

The v0.1.0 release is installable in Jellyfin but not yet functional — it establishes the plugin structure, interfaces, and build pipeline. Implementation begins with the scan engine in the next development cycle.

Update: Implementation Complete

Every item in the checklist above is now done: the scanner engine, SQLite persistence, REST API, admin dashboard, library event hooks, and a real Docker-based integration test suite alongside 113 unit tests. There’s also a proper in-app settings page — configuration is no longer a hand-edited XML file, as the “Current Status” section above implied it might stay.

Two real bugs turned up along the way, both only caught once the test suite started asserting on actual response data instead of just HTTP status: the dashboard was reading its JSON fields in the wrong casing (details in the dashboard article), and the deep-scan “already scanned, skip it” check didn’t account for which scan phase had actually run (details in the scanner core article). Both are fixed and covered by regression tests now.

What’s Next

The next article dives into architecture decisions: why a plugin instead of a standalone script, how the two-phase scanning strategy works, the SQLite schema design, and the throttling model that keeps production playback smooth.

Resources

Categories: homelab, media-server

About the Author: Michael McGarrah is a Cloud Architect with 25+ years in enterprise infrastructure, machine learning, and system administration. He holds an M.S. in Computer Science (AI/ML) from Georgia Tech and a B.S. in Computer Science from NC State University, and is currently pursuing an Executive MBA at UNC Wilmington. LinkedIn · Substack · GitHub · ORCID · Google Scholar · Resume