Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
TMDB Example Provider
https://github.com/plexinc/tmdb-example-provider
Admin
A Plex-compatible custom metadata provider for TV shows that integrates with TheMovieDB.org API to
...
Tokens:
43,047
Snippets:
159
Trust Score:
9.2
Update:
4 months ago
Context
Skills
Chat
Benchmark
74.8
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# TMDB Plex Custom Metadata Provider This project implements a Plex-compatible Custom Metadata Provider for TV shows using TheMovieDB.org (TMDB) API. It serves as a reference implementation demonstrating how to build a metadata provider that integrates with Plex Media Server's custom agent system. The provider translates TMDB's comprehensive TV show data into Plex's metadata schema, enabling Plex to display rich information about TV shows, seasons, and episodes including images, cast/crew details, ratings, and alternative episode orderings. The provider exposes a REST API that Plex queries to retrieve TV show metadata. It implements three core features: metadata retrieval by ratingKey (unique identifier), content matching based on search hints (title, year, external IDs), and support for episode groups that enable alternative episode orderings like DVD order or story arc order. The system uses a custom GUID scheme to uniquely identify content and maintains compatibility with external database IDs (IMDB, TVDB) for cross-referencing. ## API Endpoints ### Get MediaProvider Definition Returns the provider's capabilities including supported metadata types and available features. ```bash curl http://localhost:3000/tv ``` ```json { "MediaProvider": { "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "title": "TheMovieDB Example TV Provider", "version": "1.0.0", "Types": [ { "type": 2, "Scheme": [ { "scheme": "tv.plex.agents.custom.example.themoviedb.tv" } ] }, { "type": 3, "Scheme": [ { "scheme": "tv.plex.agents.custom.example.themoviedb.tv" } ] }, { "type": 4, "Scheme": [ { "scheme": "tv.plex.agents.custom.example.themoviedb.tv" } ] } ], "Feature": [ { "type": "metadata", "key": "/library/metadata" }, { "type": "match", "key": "/library/metadata/matches" } ] } } ``` ### Match TV Show by Title Search for TV shows by title and optionally year, returning metadata for best match. ```bash curl -X POST http://localhost:3000/tv/library/metadata/matches \ -H "Content-Type: application/json" \ -H "X-Plex-Language: en-US" \ -H "X-Plex-Country: US" \ -d '{ "type": 2, "title": "Adventure Time", "year": 2010 }' ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "show", "ratingKey": "tmdb-show-15260", "key": "/library/metadata/tmdb-show-15260/children", "guid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "title": "Adventure Time", "originallyAvailableAt": "2010-04-05", "year": 2010, "summary": "Hook up with Finn and Jake as they travel...", "thumb": "https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg", "contentRating": "TV-PG", "Genre": [ {"tag": "Animation"}, {"tag": "Comedy"}, {"tag": "Sci-Fi & Fantasy"} ], "Guid": [ {"id": "imdb://tt1305826"}, {"id": "tmdb://15260"}, {"id": "tvdb://152831"} ], "Network": [ {"tag": "Cartoon Network"} ], "Rating": [ { "image": "themoviedb://image.rating", "type": "audience", "value": 8.77 } ] } ] } } ``` ### Match by External ID Find content using external database identifiers (IMDB, TVDB). ```bash curl -X POST http://localhost:3000/tv/library/metadata/matches \ -H "Content-Type: application/json" \ -d '{ "type": 2, "guid": "tvdb://152831" }' ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "show", "ratingKey": "tmdb-show-15260", "guid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "title": "Adventure Time" } ] } } ``` ### Manual Search with Multiple Results Return multiple matches for manual selection. ```bash curl -X POST http://localhost:3000/tv/library/metadata/matches \ -H "Content-Type: application/json" \ -d '{ "type": 2, "title": "Star", "manual": 1 }' ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 5, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 5, "Metadata": [ { "type": "show", "ratingKey": "tmdb-show-67198", "title": "Star Trek: Discovery", "year": 2017 }, { "type": "show", "ratingKey": "tmdb-show-1429", "title": "Star Trek: The Next Generation", "year": 1987 } ] } } ``` ### Match Season Find specific season by show title and season number. ```bash curl -X POST http://localhost:3000/tv/library/metadata/matches \ -H "Content-Type: application/json" \ -d '{ "type": 3, "parentTitle": "Adventure Time", "index": 1 }' ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "season", "ratingKey": "tmdb-season-15260-1", "key": "/library/metadata/tmdb-season-15260-1/children", "guid": "tv.plex.agents.custom.example.themoviedb.tv://season/tmdb-season-15260-1", "title": "Season 1", "index": 1, "parentRatingKey": "tmdb-show-15260", "parentGuid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "parentType": "show", "parentTitle": "Adventure Time" } ] } } ``` ### Match Episode by Season and Episode Number Find specific episode using show title, season number, and episode number. ```bash curl -X POST http://localhost:3000/tv/library/metadata/matches \ -H "Content-Type: application/json" \ -d '{ "type": 4, "grandparentTitle": "Adventure Time", "parentIndex": 1, "index": 5 }' ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "episode", "ratingKey": "tmdb-episode-15260-1-5", "guid": "tv.plex.agents.custom.example.themoviedb.tv://episode/tmdb-episode-15260-1-5", "title": "The Enchiridion!", "index": 5, "parentIndex": 1, "parentRatingKey": "tmdb-season-15260-1", "parentGuid": "tv.plex.agents.custom.example.themoviedb.tv://season/tmdb-season-15260-1", "grandparentRatingKey": "tmdb-show-15260", "grandparentGuid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "grandparentTitle": "Adventure Time" } ] } } ``` ### Match Episode by Air Date Find episode using show title and air date when season/episode numbers are unavailable. ```bash curl -X POST http://localhost:3000/tv/library/metadata/matches \ -H "Content-Type: application/json" \ -d '{ "type": 4, "grandparentTitle": "Adventure Time", "date": "2010-04-26" }' ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "episode", "ratingKey": "tmdb-episode-15260-1-5", "title": "The Enchiridion!", "originallyAvailableAt": "2010-04-26" } ] } } ``` ### Get TV Show Metadata by RatingKey Retrieve complete metadata for a TV show including seasons as children. ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-show-15260?includeChildren=1" \ -H "X-Plex-Language: en-US" \ -H "X-Plex-Country: US" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "show", "ratingKey": "tmdb-show-15260", "key": "/library/metadata/tmdb-show-15260/children", "guid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "title": "Adventure Time", "originalTitle": "Adventure Time with Finn & Jake", "originallyAvailableAt": "2010-04-05", "year": 2010, "summary": "Hook up with Finn and Jake as they travel the Land of Ooo...", "tagline": "They're not righteous, they're wrongteous!", "thumb": "https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg", "art": "https://image.tmdb.org/t/p/original/6W4w99VhD8VtEFvQ1IlLHyLkOyM.jpg", "contentRating": "TV-PG", "duration": 660000, "studio": "Cartoon Network Studios", "Image": [ { "type": "coverPoster", "url": "https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg", "alt": "Adventure Time" }, { "type": "background", "url": "https://image.tmdb.org/t/p/original/6W4w99VhD8VtEFvQ1IlLHyLkOyM.jpg", "alt": "Adventure Time" } ], "Genre": [ {"tag": "Animation"}, {"tag": "Comedy"}, {"tag": "Sci-Fi & Fantasy"} ], "Role": [ { "tag": "Jeremy Shada", "role": "Finn the Human", "order": 1, "thumb": "https://image.tmdb.org/t/p/original/q1hEGc1F6SbWCNEpYaX8MRJFJHj.jpg" }, { "tag": "John DiMaggio", "role": "Jake the Dog", "order": 2 } ], "Children": { "size": 10, "Metadata": [ { "type": "season", "ratingKey": "tmdb-season-15260-1", "title": "Season 1", "index": 1 } ] } } ] } } ``` ### Get Season Metadata with Episodes Retrieve season metadata including all episode children. ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-season-15260-1?includeChildren=1" \ -H "X-Plex-Language: en-US" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "season", "ratingKey": "tmdb-season-15260-1", "key": "/library/metadata/tmdb-season-15260-1/children", "guid": "tv.plex.agents.custom.example.themoviedb.tv://season/tmdb-season-15260-1", "title": "Season 1", "originallyAvailableAt": "2010-04-05", "year": 2010, "index": 1, "parentRatingKey": "tmdb-show-15260", "parentKey": "/library/metadata/tmdb-show-15260", "parentGuid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "parentType": "show", "parentTitle": "Adventure Time", "Children": { "size": 26, "Metadata": [ { "type": "episode", "ratingKey": "tmdb-episode-15260-1-1", "title": "Slumber Party Panic", "index": 1, "parentIndex": 1 } ] } } ] } } ``` ### Get Episode Metadata Retrieve complete metadata for a specific episode. ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-episode-15260-1-5" \ -H "X-Plex-Language: en-US" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "episode", "ratingKey": "tmdb-episode-15260-1-5", "key": "/library/metadata/tmdb-episode-15260-1-5", "guid": "tv.plex.agents.custom.example.themoviedb.tv://episode/tmdb-episode-15260-1-5", "title": "The Enchiridion!", "originallyAvailableAt": "2010-04-26", "year": 2010, "summary": "After saving Princess Bubblegum from the Ice King...", "thumb": "https://image.tmdb.org/t/p/original/2rqMkuFaZEeYTqoU1oQlBHcPPOq.jpg", "duration": 660000, "index": 5, "parentIndex": 1, "parentRatingKey": "tmdb-season-15260-1", "parentGuid": "tv.plex.agents.custom.example.themoviedb.tv://season/tmdb-season-15260-1", "parentType": "season", "parentTitle": "Season 1", "grandparentRatingKey": "tmdb-show-15260", "grandparentGuid": "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260", "grandparentType": "show", "grandparentTitle": "Adventure Time", "Image": [ { "type": "snapshot", "url": "https://image.tmdb.org/t/p/original/2rqMkuFaZEeYTqoU1oQlBHcPPOq.jpg", "alt": "The Enchiridion!" } ], "Rating": [ { "image": "themoviedb://image.rating", "type": "audience", "value": 7.8 } ], "Director": [ { "tag": "Larry Leichliter", "role": "Director" } ], "Writer": [ { "tag": "Pendleton Ward", "role": "Writer" } ] } ] } } ``` ### Get All Images for Item Retrieve all available image assets for a show, season, or episode. ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-show-15260/images" \ -H "X-Plex-Language: en-US" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 47, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 47, "Image": [ { "type": "coverPoster", "url": "https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg", "alt": "Adventure Time" }, { "type": "coverPoster", "url": "https://image.tmdb.org/t/p/original/5skGRX8wf8hN2L3KjDfZHOZwaVF.jpg", "alt": "Adventure Time" }, { "type": "background", "url": "https://image.tmdb.org/t/p/original/6W4w99VhD8VtEFvQ1IlLHyLkOyM.jpg", "alt": "Adventure Time" }, { "type": "clearLogo", "url": "https://image.tmdb.org/t/p/original/ynNOEdKBkLRu28zmJSLv0NVZwHK.png", "alt": "Adventure Time" } ] } } ``` ### Get Children with Paging Retrieve children (seasons or episodes) with pagination support. ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-show-15260/children" \ -H "X-Plex-Container-Size: 5" \ -H "X-Plex-Container-Start: 1" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 10, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 5, "Metadata": [ { "type": "season", "ratingKey": "tmdb-season-15260-0", "title": "Specials", "index": 0 }, { "type": "season", "ratingKey": "tmdb-season-15260-1", "title": "Season 1", "index": 1 } ] } } ``` ### Get Grandchildren (All Episodes) Retrieve all episodes across all seasons with pagination. ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-show-15260/grandchildren" \ -H "X-Plex-Container-Size: 20" \ -H "X-Plex-Container-Start: 1" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 283, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 20, "Metadata": [ { "type": "episode", "ratingKey": "tmdb-episode-15260-1-1", "title": "Slumber Party Panic", "index": 1, "parentIndex": 1 } ] } } ``` ### Alternative Episode Ordering with Episode Groups Retrieve show metadata with alternative season structure (DVD order, story arcs, etc.). ```bash curl "http://localhost:3000/tv/library/metadata/tmdb-show-15260?includeChildren=1&episodeOrder=5a6bba1d0e0a26527f005e71" \ -H "X-Plex-Language: en-US" ``` ```json { "MediaContainer": { "offset": 0, "totalSize": 1, "identifier": "tv.plex.agents.custom.example.themoviedb.tv", "size": 1, "Metadata": [ { "type": "show", "ratingKey": "tmdb-show-15260", "title": "Adventure Time", "SeasonType": [ { "id": "5a6bba1d0e0a26527f005e71", "source": "tmdb", "tag": "DVD", "title": "DVD Order" } ], "Children": { "size": 9, "Metadata": [ { "type": "season", "ratingKey": "tmdb-season-15260-0-dvd-5a6bba1d0e0a26527f005e71", "title": "DVD Season 1", "index": 0 } ] } } ] } } ``` ## Core Services ### TMDBClient Service HTTP client for interacting with TheMovieDB.org API with automatic request/response logging. ```typescript import { TMDBClient } from './services/TMDBClient'; // Initialize client with API key const client = new TMDBClient('your_api_key_here'); // Search for TV shows const searchResults = await client.searchTVShows('Adventure Time', { language: 'en-US', year: 2010, page: 1 }); // Get complete show details with external IDs and credits const showDetails = await client.getTVShowDetails(15260, { language: 'en-US', appendToResponse: ['external_ids', 'images', 'content_ratings', 'episode_groups', 'aggregate_credits'] }); // Get season with all episodes const seasonDetails = await client.getSeasonDetails(15260, 1, { language: 'en-US' }); // Get episode details const episodeDetails = await client.getEpisodeDetails(15260, 1, 5, { language: 'en-US' }); // Find show by external ID (IMDB or TVDB) const findResult = await client.findTVShowByExternalId('tt1305826', 'imdb_id', { language: 'en-US' }); // Get all images for a show const images = await client.getTVShowImages(15260, { language: 'en-US' }); // Get episode group details for alternative ordering const episodeGroup = await client.getEpisodeGroupDetails('5a6bba1d0e0a26527f005e71'); // Construct image URLs const imageUrl = await client.getImageURL('/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg', 'original'); // Returns: "https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg" ``` ### MatchService Matches content based on search hints and returns metadata. ```typescript import { MatchService } from './services/MatchService'; const matchService = new MatchService('your_api_key_here'); // Match TV show by title and year const showMatch = await matchService.match( { type: 2, title: 'Adventure Time', year: 2010 }, { language: 'en-US', country: 'US' } ); // Match by external GUID const guidMatch = await matchService.match( { type: 2, guid: 'tvdb://152831' }, { language: 'en-US', country: 'US' } ); // Manual search (returns multiple results) const manualSearch = await matchService.match( { type: 2, title: 'Star', manual: 1 }, { language: 'en-US', country: 'US' } ); // Match season by show title and season number const seasonMatch = await matchService.match( { type: 3, parentTitle: 'Adventure Time', index: 1 }, { language: 'en-US', country: 'US' } ); // Match episode by show title, season, and episode number const episodeMatch = await matchService.match( { type: 4, grandparentTitle: 'Adventure Time', parentIndex: 1, index: 5 }, { language: 'en-US', country: 'US' } ); // Match episode by air date (when season/episode unknown) const dateMatch = await matchService.match( { type: 4, grandparentTitle: 'Adventure Time', date: '2010-04-26' }, { language: 'en-US', country: 'US' } ); // Filter adult content const filteredMatch = await matchService.match( { type: 2, title: 'Example Show', includeAdult: 0 }, { language: 'en-US', country: 'US' } ); ``` ### MetadataService Retrieves complete metadata by ratingKey with support for children, images, and pagination. ```typescript import { MetadataService } from './services/MetadataService'; const metadataService = new MetadataService('your_api_key_here'); // Get show metadata without children const show = await metadataService.getMetadata('tmdb-show-15260', { language: 'en-US', country: 'US', includeChildren: false }); // Get show metadata with all seasons as children const showWithSeasons = await metadataService.getMetadata('tmdb-show-15260', { language: 'en-US', country: 'US', includeChildren: true }); // Get show with alternative episode ordering (episode groups) const showWithEpisodeGroup = await metadataService.getMetadata('tmdb-show-15260', { language: 'en-US', country: 'US', includeChildren: true, episodeOrder: '5a6bba1d0e0a26527f005e71' }); // Get season metadata with all episodes const season = await metadataService.getMetadata('tmdb-season-15260-1', { language: 'en-US', country: 'US', includeChildren: true }); // Get episode metadata const episode = await metadataService.getMetadata('tmdb-episode-15260-1-5', { language: 'en-US', country: 'US' }); // Get all images for an item const images = await metadataService.getImages('tmdb-show-15260', { language: 'en-US' }); // Get children with pagination (seasons for show, episodes for season) const children = await metadataService.getChildren( 'tmdb-show-15260', { language: 'en-US', country: 'US' }, { containerSize: 20, containerStart: 1 } ); // Get children with alternative ordering const alternativeChildren = await metadataService.getChildren( 'tmdb-show-15260', { language: 'en-US', country: 'US', episodeOrder: '5a6bba1d0e0a26527f005e71' }, { containerSize: 20, containerStart: 1 } ); // Get grandchildren (all episodes) with pagination const grandchildren = await metadataService.getGrandchildren( 'tmdb-show-15260', { language: 'en-US', country: 'US' }, { containerSize: 50, containerStart: 1 } ); ``` ### TMDBMapper Transforms TMDB API responses into Plex-compatible metadata structures. ```typescript import { TMDBMapper } from './mappers/TMDBMapper'; import { TMDBClient } from './services/TMDBClient'; const client = new TMDBClient('your_api_key_here'); const imageBaseURL = await client.getImageBaseURL(); const mapper = new TMDBMapper({ imageBaseURL }); // Map TMDB TV show to Plex ShowMetadata const tmdbShow = await client.getTVShowDetails(15260, { language: 'en-US' }); const showMetadata = mapper.mapTVShow(tmdbShow, { includeChildren: true, country: 'US', episodeGroups: tmdbShow.episode_groups?.results }); // Map TMDB season to Plex SeasonMetadata const tmdbSeason = await client.getSeasonDetails(15260, 1, { language: 'en-US' }); const seasonMetadata = mapper.mapSeason( tmdbSeason, 15260, 'Adventure Time', 'tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260', 'https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg', { includeChildren: true } ); // Map TMDB episode to Plex EpisodeMetadata const tmdbEpisode = await client.getEpisodeDetails(15260, 1, 5, { language: 'en-US' }); const episodeMetadata = mapper.mapEpisode( tmdbEpisode, 15260, 'Adventure Time', 'tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260', 'Season 1', 'tv.plex.agents.custom.example.themoviedb.tv://season/tmdb-season-15260-1', 'https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg', 'https://image.tmdb.org/t/p/original/5skGRX8wf8hN2L3KjDfZHOZwaVF.jpg' ); // Map episode groups to alternative season structure const episodeGroupDetails = await client.getEpisodeGroupDetails('5a6bba1d0e0a26527f005e71'); const episodeGroupSeasons = mapper.mapEpisodeGroupSeasons( episodeGroupDetails, 15260, 'Adventure Time', 'tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260', 'https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg' ); // Map episode group episodes with custom ordering const group = episodeGroupDetails.groups[0]; const tmdbEpisodes = []; // Fetch TMDB episodes for all episodes in group const episodeGroupEpisodes = mapper.mapEpisodeGroupEpisodes( group, tmdbEpisodes, '5a6bba1d0e0a26527f005e71', episodeGroupDetails.type, 15260, 'Adventure Time', 'tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260', 'DVD Season 1', 0, 'https://image.tmdb.org/t/p/original/qk3eQ8jW4opJ48gFWYUXWaMT4l.jpg' ); ``` ## GUID Utilities Functions for constructing and parsing Plex-compatible GUIDs. ```typescript import { constructGuid, parseGuid, constructMetadataKey, constructMetadataKeyWithChildren, createExternalGuid, validateRatingKey } from './utils/guid'; // Construct provider GUID const guid = constructGuid( 'tv.plex.agents.custom.example.themoviedb.tv', 'show', 'tmdb-show-15260' ); // Returns: "tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260" // Parse GUID to extract components const parsed = parseGuid('tv.plex.agents.custom.example.themoviedb.tv://show/tmdb-show-15260'); // Returns: { // scheme: "tv.plex.agents.custom.example.themoviedb.tv", // metadataType: "show", // ratingKey: "tmdb-show-15260" // } // Construct metadata endpoint path const key = constructMetadataKey('tmdb-show-15260'); // Returns: "/library/metadata/tmdb-show-15260" // Construct metadata endpoint with children const keyWithChildren = constructMetadataKeyWithChildren('tmdb-show-15260'); // Returns: "/library/metadata/tmdb-show-15260/children" // Create external GUID reference const imdbGuid = createExternalGuid('imdb', 'tt1305826'); // Returns: "imdb://tt1305826" const tvdbGuid = createExternalGuid('tvdb', 152831); // Returns: "tvdb://152831" // Validate ratingKey format (only ASCII letters, numbers, dashes, underscores) const isValid = validateRatingKey('tmdb-show-15260'); // Returns: true const isInvalid = validateRatingKey('invalid key with spaces'); // Returns: false ``` ## Environment Configuration ```typescript import { config, validateConfig } from './config/env'; // Validate configuration on startup try { validateConfig(); console.log('Configuration is valid'); } catch (error) { console.error('Configuration error:', error.message); process.exit(1); } // Access configuration const apiKey = config.tmdb.apiKey; const port = config.server.port; // Environment variables (.env file): // TMDB_API_KEY=your_tmdb_api_key // PORT=3000 // LOG_LEVEL=info ``` ## Express Application Setup ```typescript import { createApp } from './app'; import { config, validateConfig } from './config/env'; // Validate environment configuration validateConfig(); // Create Express application const app = createApp(); // Start server app.listen(config.server.port, () => { console.log(`Server listening on port ${config.server.port}`); console.log(`API Documentation: http://localhost:${config.server.port}/api-docs`); }); // Health check endpoint // GET http://localhost:3000/health // Returns: {"status": "ok"} ``` ## Summary The TMDB Plex Custom Metadata Provider demonstrates a complete implementation of Plex's Media Provider API specification, serving as both a functional provider and educational reference. It handles all aspects of TV show metadata delivery including hierarchical relationships (shows → seasons → episodes), localized content, content ratings by country, cast/crew information, multiple image types, and complex features like alternative episode orderings through episode groups. The matching system supports multiple search strategies: title-based search with year filtering, external database lookups via IMDB/TVDB IDs, season and episode matching by indices, and episode matching by air dates for content lacking proper episode numbering. Integration with Plex occurs through the provider's REST endpoints which Plex queries based on the MediaProvider definition. Developers can extend this template to add movie support, implement collection features, add more external database integrations, or adapt it for other metadata sources beyond TMDB. The project follows TypeScript best practices with comprehensive type definitions, includes unit tests for all core components, provides Swagger/OpenAPI documentation accessible at `/api-docs`, and implements request/response logging for debugging. The modular architecture separates concerns cleanly: services handle business logic, mappers transform data structures, routes define API endpoints, and utilities provide reusable functions for GUID construction and validation.