A modern, Docker-friendly custom Plex metadata provider built with C# / ASP.NET Core / .NET 10.
This project exists to fix one of the most annoying match problems in Plex metadata: when multiple movies or shows share the same title, Plex can sometimes latch onto an older title even when the file clearly refers to the current one.
This provider uses a date-aware ranking strategy so that if the filename does not contain a year, the provider prefers the title whose release date is closest to the current date.
If metadata sources contain both of these:
Rooster (2013)Rooster (2026)
and Plex sends only Rooster with no year, this provider will choose:
Rooster (2026)
If the file contains a year, such as Rooster.2013.1080p.mkv, the provider strongly prefers the matching year instead.
Plex introduced Custom Metadata Providers as HTTP-based providers that can run locally, remotely, or inside Docker. Plex also recommends splitting movie and TV support into separate providers, which this project does.
This implementation is intended to be:
- easier to maintain for .NET developers
- easy to run in Docker
- a solid base for open-source collaboration
- explicit and predictable about ambiguous-title matching
- flexible about where metadata comes from
- Separate custom providers for:
- Movies at
/movie - TV at
/tv
- Movies at
- ASP.NET Core / .NET 10 implementation
- Docker support
- Source adapters behind a provider abstraction layer
- In-memory caching for upstream lookups
- OMDb for movies
- TVMaze for TV
- TMDb as an optional richer fallback / secondary source
- Exact external ID matching when Plex supplies:
tmdb://...imdb://...tvdb://...for TV
- Year extraction from filenames
- Season / episode extraction from filenames
- Air-date extraction from filenames
- Manual search mode with ranked results
- Exact-title preference over loose title matches
- Date-aware ambiguous-title selection
- Global ranking across the configured source order
- Optional Plex API reconciliation so new matches can align with items that already exist in your Plex library
- Show matching
- Season matching
- Episode matching
- Show metadata
- Season metadata
- Episode metadata
- Children endpoints for shows and seasons
- Image endpoints
- Movie matching
- Movie metadata
- Image endpoints
- Unit tests for:
- filename parsing
- rating key parsing
- ambiguous-title ranking
- Solution file included
- Verified with
dotnet build,dotnet test, anddotnet publish
This is the heart of the project.
- If Plex supplies a supported external ID, that wins first.
- If a year exists in the match request or filename, exact or nearest-year matches are strongly preferred.
- If no year exists, the provider ranks same-title results by absolute distance to today using:
release_datefor moviesfirst_air_datefor TV shows
- Ties are broken by:
- newer release date
- higher popularity
- stable title ordering
Manual searches return multiple candidates, but they are still sorted using the exact same logic.
Examples the parser understands:
Rooster.2013.1080p.WEB-DL.mkv
Rooster.S01E02.1080p.WEB-DL.mkv
Rooster.1x02.1080p.WEB-DL.mkv
Daily.Report.2026-03-23.1080p.WEB-DL.mkv
The project now uses a source abstraction layer so movie and TV metadata providers can be mixed and ordered independently.
- search candidates
- resolve by external ID
- retrieve metadata by source ID
- search shows
- resolve by external ID
- retrieve show metadata
- retrieve season metadata
- retrieve episode metadata
- retrieve episode by air date
This means the project is no longer hard-wired to TMDb. Users can prefer more privacy-friendly or simpler sources while still keeping TMDb available as an optional fallback.
Omdb,Tmdb
TvMaze,Tmdb
- Movies try OMDb first, then TMDb if needed
- TV tries TVMaze first, then TMDb if needed
- OMDb is lighter-weight and easier to access, but less rich than TMDb
- TVMaze is great for TV matching and episode data, but it is TV-only
- TMDb remains the richest overall source, but is now optional rather than mandatory
PlexModernMetadataProvider/
├─ src/
│ └─ PlexModernMetadataProvider.Api/
│ ├─ Models/
│ ├─ Options/
│ ├─ Services/
│ ├─ appsettings.json
│ ├─ PlexModernMetadataProvider.Api.csproj
│ └─ Program.cs
├─ tests/
│ └─ PlexModernMetadataProvider.Tests/
│ ├─ Services/
│ └─ PlexModernMetadataProvider.Tests.csproj
├─ Dockerfile
├─ docker-compose.yml
├─ PlexModernMetadataProvider.slnx
└─ README.md
- .NET 10 SDK for local development
- or Docker
- a Plex Media Server version that supports Custom Metadata Providers
- depending on your source order:
- TVMaze: no API key required for the public API
- OMDb: API key required
- TMDb: API key or read access token required
This project was verified with:
dotnet --version
10.0.103
Copy the example environment file:
Copy-Item .env.example .envYou can independently control movie and TV source priority:
Provider__MovieSourceOrder=Omdb,Tmdb
Provider__TvSourceOrder=TvMaze,Tmdb
For many users this is enough:
Provider__MovieSourceOrder=Omdb
Provider__TvSourceOrder=TvMaze
Provider__OMDb__ApiKey=your_omdb_api_key
Provider__MovieSourceOrder=Omdb,Tmdb
Provider__TvSourceOrder=TvMaze,Tmdb
Provider__OMDb__ApiKey=your_omdb_api_key
Provider__TMDb__ReadAccessToken=your_tmdb_read_access_token
ASPNETCORE_URLS=http://+:3000
Provider__DefaultLanguage=en-US
Provider__DefaultCountry=US
Provider__MaxManualMatches=10
Provider__MovieSourceOrder=Omdb,Tmdb
Provider__TvSourceOrder=TvMaze,Tmdb
Provider__OMDb__ApiKey=
Provider__OMDb__RequestTimeoutSeconds=15
Provider__OMDb__CacheTtlMinutes=15
Provider__TVMaze__RequestTimeoutSeconds=15
Provider__TVMaze__CacheTtlMinutes=15
Provider__TMDb__ApiKey=
Provider__TMDb__ReadAccessToken=
Provider__TMDb__RequestTimeoutSeconds=15
Provider__TMDb__CacheTtlMinutes=15
Provider__Plex__EnableReconciliation=false
Provider__Plex__BaseUrl=http://host.docker.internal:32400
Provider__Plex__Token=
Provider__Plex__RequestTimeoutSeconds=10
Provider__Plex__CacheTtlMinutes=5
The provider can optionally query your Plex server first before it performs its normal source search. This is designed to reduce the chance of creating a second match for a movie or show that Plex has already matched previously using another provider such as Plex Series or Plex Movie.
Without reconciliation, a custom provider can still return a perfectly valid result, but Plex may treat it as a separate provider-owned identity if the existing item in the library was originally matched through a different primary provider. That can lead to:
- split or duplicate shows
- rescans drifting to a different canonical source
- season and episode requests resolving against the wrong show when titles collide
When reconciliation is enabled, the provider does this before normal matching:
- Searches your Plex library over the Plex HTTP API using the requested title and media type.
- Requests Plex GUID information with
includeGuids=1. - Parses the existing item's GUID data and extracts supported external IDs such as:
tmdb://...tvdb://...imdb://...tvmaze://...
- Ignores Plex-local GUIDs such as
plex://..., because those are server-local identities and cannot be used as stable custom-provider keys. - Tries to resolve one of the configured upstream sources to that same external identity.
- If successful, returns metadata for that same canonical item and skips the normal title search path.
- If Plex returns no good existing candidate, or the result is still ambiguous, the provider falls back to the normal ranking/search behavior.
- Movies: direct movie match reconciliation
- Shows: direct show reconciliation
- Seasons: the provider reconciles the parent show first, then resolves the requested season from that show
- Episodes: the provider reconciles the parent show first, then resolves the episode by season/episode number or by air date
Reconciliation is intentionally conservative. It does not:
- reuse Plex's local DB
ratingKeyas the provider key - read Plex's SQLite database directly
- force a match when Plex returns multiple exact same-title candidates without a distinguishing year
- automatically merge items that are already split in an existing library
For already-split items, you will usually still need a one-time cleanup step after enabling reconciliation:
- Refresh Metadata
- Fix Match
- or Unmatch + Match
Enable reconciliation if any of these are true:
- you already have a populated Plex library and want new scans to align with existing matches
- you are migrating from Plex Series / Plex Movie to this provider
- you are seeing duplicate or split shows after introducing the custom provider
- you want future rescans to prefer the same canonical external IDs Plex already knows about
If you are starting with a completely new, empty library, reconciliation is optional rather than required.
Provider__Plex__EnableReconciliation=false
Provider__Plex__BaseUrl=http://host.docker.internal:32400
Provider__Plex__Token=
Provider__Plex__RequestTimeoutSeconds=10
Provider__Plex__CacheTtlMinutes=5
Provider__Plex__EnableReconciliation=true
Provider__Plex__BaseUrl=http://host.docker.internal:32400
Provider__Plex__Token=YOUR_PLEX_TOKEN
Use the Plex container or service name instead of host.docker.internal:
Provider__Plex__EnableReconciliation=true
Provider__Plex__BaseUrl=http://plex:32400
Provider__Plex__Token=YOUR_PLEX_TOKEN
Provider__Plex__EnableReconciliation=true
Provider__Plex__BaseUrl=http://localhost:32400
Provider__Plex__Token=YOUR_PLEX_TOKEN
Provider__MovieSourceOrder=Omdb,Tmdb
Provider__TvSourceOrder=TvMaze,Tmdb
Provider__OMDb__ApiKey=your_omdb_api_key
Provider__TMDb__ReadAccessToken=your_tmdb_read_access_token
Provider__Plex__EnableReconciliation=true
Provider__Plex__BaseUrl=http://host.docker.internal:32400
Provider__Plex__Token=YOUR_PLEX_TOKEN
Provider__Plex__RequestTimeoutSecondscontrols how long the provider will wait for the Plex HTTP API before giving up and falling back to normal matching.Provider__Plex__CacheTtlMinutescontrols how long successful Plex reconciliation lookups are cached in memory.
If the Plex API is slow or temporarily unavailable, the provider logs a warning and continues with its normal source search rather than failing the entire match request.
Treat the Plex token like a credential. Do not commit it into source control, and prefer keeping it only in .env, your container environment, or a secrets manager.
If reconciliation appears to do nothing, check these in order:
Provider__Plex__EnableReconciliation=trueProvider__Plex__BaseUrlis reachable from the provider container/process, not just from your browserProvider__Plex__Tokenis valid- Plex already has a matching movie/show in the library
- That existing item exposes useful external GUIDs such as TMDb / TVDb / IMDb / TVMaze
- The configured upstream sources can resolve one of those external IDs
If Plex returns multiple same-title items with no distinguishing year, the provider intentionally avoids forcing a reconciliation match and falls back to normal ranking.
dotnet restore .\PlexModernMetadataProvider.slnxdotnet build .\PlexModernMetadataProvider.slnxdotnet run --project .\src\PlexModernMetadataProvider.Api\PlexModernMetadataProvider.Api.csprojdotnet test .\PlexModernMetadataProvider.slnxdotnet publish .\src\PlexModernMetadataProvider.Api\PlexModernMetadataProvider.Api.csproj -c Release -o .\publishdocker compose up --build -ddocker compose downThe container listens on:
http://localhost:3000
Health endpoint:
GET /health
Plex recommends separate providers for movies and TV. This project exposes both.
If Plex runs on the host and the provider runs in Docker on the same machine:
TV: http://host.docker.internal:3000/tv
Movie: http://host.docker.internal:3000/movie
If Plex and the provider run on the same Docker network, use the service/container name instead:
TV: http://plex-modern-metadata-provider-dotnet:3000/tv
Movie: http://plex-modern-metadata-provider-dotnet:3000/movie
- Open Plex Settings.
- Go to Metadata Agents / Custom Metadata Providers.
- Add the TV provider URL.
- Add the Movie provider URL.
- Create or assign custom agents for the relevant library types.
- Make this provider the primary provider if you want its matching logic to drive metadata selection.
- If you want the provider to reconcile against items already matched in Plex, configure the optional Plex reconciliation settings in
.envbefore you refresh the library. - Keep local media assets enabled afterward if you also want local posters, backgrounds, or subtitle-related local data.
- Refresh metadata for the target library.
GET /health
GET /movie
POST /movie/library/metadata/matches
GET /movie/library/metadata/{ratingKey}
GET /movie/library/metadata/{ratingKey}/images
GET /movie/library/metadata/{ratingKey}/extras
GET /tv
POST /tv/library/metadata/matches
GET /tv/library/metadata/{ratingKey}
GET /tv/library/metadata/{ratingKey}/images
GET /tv/library/metadata/{ratingKey}/extras
GET /tv/library/metadata/{ratingKey}/children
{
"type": 1,
"title": "Rooster",
"filename": "Rooster.1080p.WEB-DL.mkv"
}{
"type": 4,
"grandparentTitle": "Rooster",
"parentIndex": 1,
"index": 2,
"filename": "Rooster.S01E02.1080p.WEB-DL.mkv"
}- prefers latest / most current title when year is absent
- still respects explicit years when available
- still prioritizes exact title matches over weaker partial matches
- uses external IDs immediately when supplied
- keeps metadata retrieval tied to the source that originally matched the item
- blindly preferring an old title because the name happens to match
- assuming the oldest result is correct when Plex provides weak input
- mixing movie and TV provider definitions into one provider root
- scraping unofficial web pages instead of using public APIs
- OMDb is currently used for movies only
- TVMaze is currently used for TV only
- TMDb still provides richer images and richer credits than the lighter-weight sources
- No collection endpoint yet
- No persistent cache yet
- No provider-side authentication layer yet
- Plex reconciliation depends on the Plex HTTP API being reachable and exposing useful external GUIDs
- Existing items that are already split in Plex may still need a one-time Fix Match or Refresh Metadata pass after reconciliation is enabled
- Upstream metadata quality still depends on the chosen source
- Fanart.tv artwork adapter
- Wikidata supplemental adapter
- optional TheTVDB adapter
- persistent distributed cache
- collection support
- richer manual-match diagnostics
- provider-specific tuning rules
Verified in this repo:
dotnet build .\PlexModernMetadataProvider.slnx
dotnet test .\PlexModernMetadataProvider.slnx
dotnet publish .\src\PlexModernMetadataProvider.Api\PlexModernMetadataProvider.Api.csproj -c Release -o .\publishAll three completed successfully during project validation.
- Plex custom metadata providers announcement
- Plex Media Server developer docs
- Plex TMDb example provider
- TMDb API docs
- OMDb API
- TVMaze API
MIT