An open API service indexing awesome lists of open source software.

https://github.com/c-kick/vpro-cinema-plex

Plex Metadata Provider that fetches Dutch movie descriptions from VPRO Cinema. Uses NPO POMS API and TMDB alternate title lookup.
https://github.com/c-kick/vpro-cinema-plex

dutch metadata metadata-agent metadata-api metadata-extraction plex plex-agent

Last synced: 14 days ago
JSON representation

Plex Metadata Provider that fetches Dutch movie descriptions from VPRO Cinema. Uses NPO POMS API and TMDB alternate title lookup.

Awesome Lists containing this project

README

          

# VPRO Cinema Metadata Provider for Plex

A custom metadata provider that supplies Dutch film descriptions
from [VPRO Cinema](https://www.cinema.nl/) to Plex Media Server.

> **Note:** As of v4.0.0, this provider only supports **movies**. TV series support has been removed because VPRO's data sources don't provide complete series metadata (seasons, episodes, episode descriptions), which caused Plex scanning failures.

See [how it works](#how-it-works).

![Example of the Metadata Provider in action!](https://github.com/user-attachments/assets/002b61b3-c05c-4888-a1c6-c34bf38d6dd1)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://www.docker.com/)
[![Plex 1.40+](https://img.shields.io/badge/Plex-1.40%2B-E5A00D.svg?logo=plex&logoColor=white)](https://www.plex.tv/)

## Features

- πŸ‡³πŸ‡± Dutch film reviews/descriptions from VPRO Cinema's database
- 🎬 Movies only (TV series not supported β€” see note above)
- πŸ”ž Kijkwijzer content ratings (Dutch age classification: AL, 6, 9, 12, 14, 16, 18)
- πŸ” Direct NPO POMS API access
- 🌍 Smart title matching via TMDB β€” works in both directions (Translated β†’ Original and Original β†’ Translated)
- πŸ”Ž Cinema.nl fallback with IMDB verification when POMS API returns no results
- πŸ“¦ TMDB fallback metadata β€” movies not in VPRO can still be added to Plex with basic info
- πŸ’Ύ Persistent caching (with short 1-hour TTL for not-found entries)
- 🐳 Docker-ready with health checks
- πŸ”— Combines with other providers (returns description + content rating by default)
- βš™οΈ Configurable: optionally return VPRO images and/or ratings

## Background

For years I wanted to automatically pull the excellent Dutch film reviews
from [VPRO Cinema](https://www.cinema.nl) (formerly vprogids.nl/cinema) into Plex. I made several
attempts over the years, but without an official NPO API, I never got it to work. After getting tired of manually
copying descriptions into Plex β€” only to have them overwritten by the next metadata refresh β€” I teamed up with Claude to
build a proper solution. After some experimentation (first with scraping, then reverse-engineering the NPO's internal
POMS API), I finally got a working Plex agent! I decided to share it with the community, hoping it can help others too.

Feel free to use, fork, and contribute, but note that the API is not officially supported by NPO, so the approach is
technically a bit dodgy and may break at any time. Though it has been working excellently for me, so far!

## Prerequisites

**Required:**

- **Docker** and **Docker Compose** β€” [Install Docker](https://docs.docker.com/get-docker/)
- **Plex Media Server 1.40+** β€” Uses the new [Custom Metadata Providers API](https://developer.plex.tv/pms/)

**Recommended:**

- **TMDB API Key** β€” Enables smart alternate title lookup. Many films are indexed in VPRO Cinema under their original (
often French, German, or Dutch) title rather than the English title. With a TMDB API key, the provider automatically
discovers and tries alternate titles in both directions:
- **English β†’ Original**: "Downfall" β†’ "Der Untergang" (via IMDB ID from Plex)
- **Original β†’ English**: "Der Untergang" β†’ "Downfall" (via TMDB title search)

Get your free API key at: https://www.themoviedb.org/settings/api

## Quick Start

### 1. Clone and configure

```bash
git clone https://github.com/c-kick/vpro-cinema-plex.git
cd vpro-cinema-plex
cp env.example .env
```

Edit `.env` to add your TMDB API key (optional but recommended):

```bash
TMDB_API_KEY=your_tmdb_api_key_here
```

### 2. Build and run

```bash
docker-compose up -d
```

### 3. Verify

```bash
curl "http://localhost:5100/health"
curl "http://localhost:5100/test?title=Apocalypse+Now&year=1979"
```

Alternative: Add to existing Docker stack (Portainer)

Portainer often can't access local build contexts. Build the image on your server first:

```bash
cd /path/to/vpro-cinema-plex
docker build -t vpro-plex-provider:latest .
```

Add to your stack:

```yaml
vpro-plex-provider:
image: vpro-plex-provider:latest
pull_policy: never
container_name: vpro-plex-provider
restart: unless-stopped
ports:
- "5100:5100"
environment:
- TZ=Europe/Amsterdam
- LOG_LEVEL=INFO
- TMDB_API_KEY=your_tmdb_api_key_here # Optional but recommended
- CACHE_DIR=/app/cache
- POMS_CACHE_FILE=/app/cache/credentials.json
- VPRO_RETURN_SUMMARY=true # Dutch descriptions (main feature)
- VPRO_RETURN_CONTENT_RATING=true # Kijkwijzer age ratings
- VPRO_RETURN_IMAGES=false # Set to true to use VPRO posters
- VPRO_RETURN_RATING=false # Experimental: VPRO ratings (see limitations)
volumes:
- /path/to/vpro-cinema-plex/cache:/app/cache
networks:
- your-plex-network # Must be on same network as Plex
```
Note: if you use the plex network, you can use 'vpro-plex-provider' (instead of `localhost`, as in the examples below)
to directly reference the agent in the provider URL in Plex: `http://vpro-plex-provider:5100/movies`

## Plex Configuration

> **Important:** Replace `localhost` with your server's IP if Plex runs on a different host.

| Endpoint | Provider Name | Use For |
|--------------------------------|---------------------------------|----------|
| `http://localhost:5100/movies` | VPRO Cinema (Dutch Summaries) | Movies |

### Register the provider

1. In Plex, go to **Settings** β†’ **Metadata Agents** β†’ **Metadata Providers**
2. Click **+ Add Provider**, paste `http://localhost:5100/movies`, save

image

### Create the agent

**Movie Agent:**

1. Under **Metadata Agents**, click **+ Add Agent**
2. Title: "VPRO + Plex Movie"
3. Primary provider: `VPRO Cinema (Dutch Summaries)`
4. Add "Plex Movie" as additional provider (click **+**)
5. Optionally add "Plex Local Media"
6. Save

image

### Configure your libraries

1. **Settings** β†’ **Manage Libraries** β†’ click `...` next to library β†’ **Edit Library**
2. **Advanced** tab β†’ **Agent** β†’ select your new agent
3. Save and repeat for other movie libraries

### Refresh metadata

For existing content: Select items β†’ `...` β†’ **Refresh Metadata**

New content will automatically use the provider on scan.

## How It Works

When configured as a Metadata Provider alongside Plex Movie (see [Plex Configuration](#plex-configuration)), the VPRO agent runs a lookup cascade while Plex Movie fetches its metadata in parallel. Results are merged with VPRO as primary β€” meaning Dutch summaries and Kijkwijzer ratings take precedence. Everything VPRO doesn't return (posters, cast, genres, etc.) is filled in by Plex Movie automatically.

vpro-how-it-works drawio (2) drawio (2)

## Testing & Debugging

### CLI search (no caching)

```bash
# Basic search
docker exec vpro-plex-provider python vpro_lookup.py "Apocalypse Now" --year 1979

# With IMDB ID + verbose output
docker exec vpro-plex-provider python vpro_lookup.py "Downfall" --year 2004 --imdb tt0363163 -v

# Test cinema.nl fallback directly (bypass POMS API)
docker exec vpro-plex-provider python vpro_lookup.py "Der Untergang" --year 2004 --skip-poms -v
```

### HTTP endpoints (with caching)

```bash
# Test search
curl "http://localhost:5100/test?title=Le+dernier+mΓ©tro&year=1980"

# Test cinema.nl fallback directly (bypass POMS API)
curl "http://localhost:5100/test?title=Der+Untergang&year=2004&skip_poms=1"

# Plex metadata endpoint
curl "http://localhost:5100/movies/library/metadata/vpro-apocalypse-now-1979-tt0078788-m"

# Cache operations
curl "http://localhost:5100/cache"
curl "http://localhost:5100/cache?key=vpro-apocalypse-now-1979-tt0078788-m"
curl -X POST "http://localhost:5100/cache/clear"
curl -X POST "http://localhost:5100/cache/delete?key=vpro-apocalypse-now-1979-tt0078788-m"
curl -X POST "http://localhost:5100/cache/delete?pattern=apocalypse"
```

### Credential management

The POMS API uses hardcoded default credentials that have been working reliably.

```bash
# View cached credentials
docker exec vpro-plex-provider cat cache/credentials.json
```

> **Note:** Credential auto-refresh from vprogids.nl is no longer functional since the migration to cinema.nl, but the default credentials continue to work with the POMS API.

### Logs

```bash
docker-compose logs -f
```

## Environment Variables

| Variable | Default | Description |
|-----------------------------|--------------------|-------------------------------------------------------------|
| `PORT` | 5100 | Server port |
| `LOG_LEVEL` | INFO | DEBUG, INFO, WARNING, ERROR |
| `CACHE_DIR` | ./cache | Cache directory path |
| `TMDB_API_KEY` | *(none)* | TMDB API key for alternate title lookup |
| `POMS_CACHE_FILE` | ./credentials.json | Path to cached POMS credentials |
| `VPRO_RETURN_SUMMARY` | true | Return VPRO Dutch summary/description |
| `VPRO_RETURN_CONTENT_RATING`| true | Return Kijkwijzer content rating (AL, 6, 9, 12, 14, 16, 18) |
| `VPRO_RETURN_IMAGES` | false | Return VPRO images (recommended for Dutch films β€” better poster coverage than TMDB) |
| `VPRO_RETURN_RATING` | false | Return VPRO rating (experimental, see [Limitations](#limitations)) |

## API Reference

| Endpoint | Method | Description |
|-----------------------------------------|--------|--------------------------------------------------|
| `/movies` | GET | Movie provider info (type 1) |
| `/movies/library/metadata/` | GET | Plex metadata lookup for movies |
| `/movies/library/metadata/matches` | POST | Plex match endpoint for movies |
| `/movies/library/metadata//images` | GET | Returns VPRO images if enabled, otherwise empty |
| `/movies/library/metadata//extras` | GET | Returns empty (no extras) |
| `/health` | GET | Simple health check (version only) |
| `/health/ready` | GET | Detailed health with checks, cache stats, config |
| `/health/live` | GET | Liveness probe (always returns ok) |
| `/test` | GET | Test search: `?title=X&year=Y&imdb=ttZ&skip_poms=1&skip_tmdb=1` |
| `/cache` | GET | List cached entries or view specific: `?key=X` |
| `/cache/clear` | POST | Clear all cached entries (preserves credentials) |
| `/cache/delete` | POST | Delete specific entries: `?key=X` or `?pattern=X`|

## File Structure

```
vpro-cinema-plex/
β”œβ”€β”€ docker-compose.yml # Docker Compose config
β”œβ”€β”€ Dockerfile # Container definition
β”œβ”€β”€ env.example # Environment template (copy to .env)
β”œβ”€β”€ requirements.txt # Python dependencies
β”‚
β”œβ”€β”€ vpro_metadata_provider.py # Flask HTTP server for Plex
β”œβ”€β”€ vpro_lookup.py # Search orchestrator + CLI
β”œβ”€β”€ poms_client.py # NPO POMS API + TMDB clients
β”œβ”€β”€ vpro_scraper.py # Cinema.nl fallback + page scraper
β”œβ”€β”€ models.py # Shared data models (VPROFilm)
β”‚
β”œβ”€β”€ cache.py # Disk cache with sharding
β”œβ”€β”€ credentials.py # POMS credential management
β”œβ”€β”€ http_client.py # HTTP session factory
β”œβ”€β”€ text_utils.py # Title matching utilities
β”œβ”€β”€ logging_config.py # Logging configuration
β”œβ”€β”€ metrics.py # Simple metrics collection
β”œβ”€β”€ constants.py # Shared constants
β”‚
β”œβ”€β”€ LICENSE # MIT License
└── README.md # This file
```

## Troubleshooting

| Problem | Solution |
|-----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Provider not in Plex | Verify running: `curl http://localhost:5100/health`. Check network from Plex to provider. |
| No Dutch descriptions | Test film exists: `docker exec vpro-plex-provider python vpro_lookup.py "TITLE" --year YEAR -v`. Check logs: `docker-compose logs --tail=100`. Clear cache and retry. |
| Metadata not updating after port change | Restart Plex server (known Plex bug with URL changes). |
| POMS auth errors | Default credentials should work. If issues persist, check NPO API availability |
| Film not found | Try original title: `"Der Untergang"` instead of `"Downfall"`. Or provide IMDB ID: `--imdb tt0363163` |
| TMDB alternate titles not working | Verify `"configured": true` and `"status": "ok"` under `tmdb` in `/health/ready` response. |
| Still it's not working | Restart your Plex server. |

## Updating

**Standalone docker-compose:**
```bash
git pull && docker-compose down && docker-compose build --no-cache && docker-compose up -d
```

**Portainer stack:**
```bash
cd /path/to/vpro-cinema-plex
git pull
docker build -t vpro-plex-provider:latest .
# Then redeploy the stack in Portainer
```

Verify: `curl http://localhost:5100/health`

Cache and `.env` are preserved during updates.

Upgrade notes for specific versions

### v4.0.0 β€” TV series support removed (breaking change)

TV series support has been removed. Only movies are now supported.

**Migration:**
1. Remove the series provider from Plex (`http://localhost:5100/series`)
2. Remove any TV Show agents that used VPRO as primary provider
3. For TV libraries, switch to standard Plex Series agent
4. Clear cache: `curl -X POST "http://localhost:5100/cache/clear"`

### v3.1.0 β€” Breaking URL changes

| Old | New |
|----------------------------|--------------------------------|
| `http://localhost:5100/` | `http://localhost:5100/movies` |
| `http://localhost:5100/tv` | `http://localhost:5100/series` |

Provider names also changed (added `- Movies` / `- Series` suffix).

**Migration:** Remove old providers in Plex, add new URLs, update agents, restart Plex.

### v3.0.0 β€” series support (two-provider architecture)

Single provider β†’ two providers (`/movies` and `/series`). Required by Plex API for proper secondary provider combining.

**Migration:** Remove old provider, add both new URLs, create separate TV Show agent.

## Changelog

### v4.1.0 β€” Improved poster and image handling

- **Poster extraction from Cinema.nl** β€” Now extracts the main movie poster from the top of Cinema.nl pages (marked as PROMO_PORTRAIT), in addition to stills from the Afbeeldingen section. Uses alt-text matching and URL heuristics to identify posters reliably.
- **Fixed dead vprogids.nl image URLs** β€” POMS API returns old vprogids.nl URLs that now return HTTP 410 Gone. The provider now converts these to cinema.nl URLs and scrapes for fresh images.vpro.nl URLs.
- **Proper image embedding in metadata** β€” Images are now embedded directly in the metadata response (thumb, art, Image array) per the Plex TMDB example, not just via the /images endpoint.
- **Conditional images feature** β€” The provider only declares the "images" feature when `VPRO_RETURN_IMAGES=true`. When disabled, Plex correctly falls back to secondary agents (Plex Movie) for artwork.
- **Rating array support** β€” VPRO ratings are now returned in the Rating array format with `cinemanl://image.rating` scheme. Note: Plex UI only displays ratings with recognized schemes (imdb://, rottentomatoes://), so the rating is stored but won't show an icon.
- **WebP to JPEG conversion** β€” Image URLs are converted from .webp to .jpg for better Plex compatibility.
- **Correct Plex image types** β€” Fixed image type mapping to use Plex's expected values (`coverPoster`, `background`) instead of incorrect ones (`poster`, `art`).

### v4.0.0 β€” Movies only (breaking change)
- **⚠️ TV series support removed** β€” VPRO's data sources don't provide complete series metadata (seasons, episodes, episode descriptions), which caused Plex scanning failures. This provider now only supports movies.
- **TMDB fallback metadata** β€” When a movie isn't found in VPRO sources, the provider now returns basic metadata from TMDB (title, year, IMDB ID) so Plex can still add the movie. The description is omitted to let secondary providers (Plex Movie) fill it in.
- **Reduced not-found cache TTL** β€” Changed from 7 days to 1 hour to allow quicker retries for newly indexed content
- **Simplified API** β€” Removed `/series` endpoint and all TV-related routes

**Migration from v3.x:**
1. Remove the series provider from Plex (`http://localhost:5100/series`)
2. Remove any TV Show agents that used VPRO as primary provider
3. For TV libraries, switch to standard Plex Series agent
4. Clear cache: `curl -X POST "http://localhost:5100/cache/clear"`

### v3.4.0
- **Cinema.nl direct scraper** β€” Replaced DuckDuckGo/Startpage web search with direct cinema.nl scraping
- **IMDB verification** β€” Cinema.nl fallback now verifies matches using IMDB IDs for reliable matching
- **Image extraction** β€” Cinema.nl fallback extracts high-resolution images from the Afbeeldingen section
- **Migration complete** β€” vprogids.nl/cinema has fully migrated to cinema.nl
- **Search optimizations** β€” Year-in-query and model=cinema parameter for better search ranking
- **Circuit breaker** β€” Prevents hammering cinema.nl after repeated failures
- **Credential refresh deprecated** β€” Auto-refresh from vprogids.nl no longer works (site returns 404)

### v3.3.0
- **Kijkwijzer content ratings** β€” Dutch age classification (AL, 6, 9, 12, 14, 16, 18) now extracted from POMS API
- **Configurable metadata fields** β€” New environment variables to control what metadata is returned:
- `VPRO_RETURN_SUMMARY` (default: true) β€” Dutch descriptions
- `VPRO_RETURN_CONTENT_RATING` (default: true) β€” Kijkwijzer ratings
- `VPRO_RETURN_IMAGES` (default: false) β€” VPRO poster images
- `VPRO_RETURN_RATING` (default: false) β€” VPRO appreciation ratings (experimental, see [Limitations](#limitations))
- **Fix Match thumbnails** β€” Images now display in Plex's Fix Match dialog when `VPRO_RETURN_IMAGES=true`
- **Health endpoint improvements** β€” `/health/ready` now shows configured feature flags
- **Selective cache deletion** β€” New `/cache/delete` endpoint for targeted cache management

### v3.2.0
- Added debug logging for troubleshooting
- Docker environment variable passthrough improvements

### v3.1.0
- Breaking URL changes: `/` β†’ `/movies`, `/tv` β†’ `/series`
- Provider name suffix changes (`- Movies` / `- Series`)

### v3.0.0
- Two-provider architecture for proper Plex secondary agent combining
- Added series/TV show support (removed in v4.0.0)

## Limitations

- **Movies only** β€” TV series support was removed in v4.0.0 because VPRO's data sources don't provide complete series metadata
- **POMS API is undocumented** β€” Not officially supported by NPO; may change without notice
- **Not all content covered** β€” Only films reviewed by VPRO Cinema; movies not found get basic TMDB fallback metadata
- **Artwork optional** β€” Disabled by default; enable `VPRO_RETURN_IMAGES` or use Plex Movie fallback
- **Ratings display limited** β€” Plex's Custom Metadata Provider API may store `audienceRating` values, but the rating *icon* displayed in the UI is controlled by the library's "Ratings Source" setting (Rotten Tomatoes, IMDb, or TMDb), not by the provider. Custom `ratingImage` URI schemes are not supported. This is a Plex architectural limitation β€” see the [Plex Dev/API Forum](https://forums.plex.tv/c/dev-api-corner/) for updates
- **Credential refresh broken** β€” vprogids.nl/cinema has migrated to cinema.nl; auto-refresh no longer works but default credentials still function
- **Cinema.nl fallback** β€” Direct scraping; may break if site structure changes

## License

MIT β€” Do whatever you want with it.

## Credits

- [VPRO Cinema](https://www.cinema.nl) for the Dutch film reviews
- [TMDB](https://www.themoviedb.org/) for alternate title data
- Klaas (c_kick/hnldesign) β€” Original idea and development
- Claude (Anthropic) β€” Implementation assistance