Skip to content

cplieger/tautulli-remap

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tautulli-remap

License: GPL-3.0 GitHub release Image Size Platforms base: Distroless

Tautulli rating key remapper for Plex library changes

Overview

Periodically queries Tautulli's API for history entries with stale rating keys (entries pointing to items that no longer exist in Plex). For each stale entry, it attempts to find the correct current rating key in Plex using three matching strategies: GUID match (primary), title+year match (fallback), and title-only with media type guard (optional). Supports dry-run mode for safe testing.

Example use case: After reorganizing Plex libraries, moving media between folders, or re-adding content, Tautulli's history entries break because Plex assigns new rating keys. This tool automatically finds the correct new keys and updates Tautulli's database, preserving your watch history and statistics.

This is a distroless, rootless container — it runs as nonroot on gcr.io/distroless/static with no shell or package manager. It has zero external Go dependencies (stdlib-only).

Container Registries

This image is published to both GHCR and Docker Hub:

Registry Image
GHCR ghcr.io/cplieger/tautulli-remap
Docker Hub docker.io/cplieger/tautulli-remap
# Pull from GHCR
docker pull ghcr.io/cplieger/tautulli-remap:latest

# Pull from Docker Hub
docker pull cplieger/tautulli-remap:latest

Both registries receive identical images and tags. Use whichever you prefer.

Quick Start

services:
  tautulli-remap:
    image: ghcr.io/cplieger/tautulli-remap:latest
    container_name: tautulli-remap
    restart: unless-stopped
    user: "1000:1000"  # match your host user
    mem_limit: 128m

    environment:
      TZ: "Europe/Paris"
      TAUTULLI_URL: "\\http://tautulli:8181"
      TAUTULLI_APIKEY: "your-tautulli-apikey"
      PLEX_URL: "\\http://plex:32400"
      PLEX_TOKEN: "your-plex-token"
      SCHEDULE_HOURS: "24"  # 0 = run once and exit
      FALLBACK_TITLE_YEAR: "true"
      FALLBACK_TITLE_ONLY: "false"  # risk of false matches
      DRY_RUN: "true"  # set to false to apply changes

    healthcheck:
      test:
        - CMD
        - /tautulli-remap
        - health
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s

Deployment

  1. Set TAUTULLI_URL and PLEX_URL to your instances (Docker DNS names like http://tautulli:8181 work if on the same network).
  2. Set TAUTULLI_APIKEY (found in Tautulli → Settings → Web Interface → API Key) and PLEX_TOKEN (see Plex support).
  3. Start with DRY_RUN: "true" (the default) to see what would change without applying anything. Check the container logs.
  4. Once satisfied, set DRY_RUN: "false" to apply the remaps.
  5. FALLBACK_TITLE_YEAR and FALLBACK_TITLE_ONLY control how aggressively the tool matches when GUID matching fails. Title-only matching risks false positives on common titles.

For additional configuration options not covered by this image's environment variables, refer to the Tautulli documentation.

Environment Variables

Variable Description Default Required
TZ Container timezone Europe/Paris No
TAUTULLI_URL Tautulli instance URL (Docker DNS name or LAN IP) \http://tautulli:8181 No
TAUTULLI_APIKEY Tautulli API key (Settings → Web Interface → API Key) - Yes
PLEX_URL Plex Media Server URL (Docker DNS name or LAN IP) \http://plex:32400 No
PLEX_TOKEN Plex authentication token (see Plex support article) - Yes
SCHEDULE_HOURS Hours between remap runs (0 = run once and exit) 24 No
FALLBACK_TITLE_YEAR Try title+year matching when GUID match fails true No
FALLBACK_TITLE_ONLY Try title-only matching as last resort (risk of false matches) false No
DRY_RUN Log what would change without applying — set to false to apply true No

Docker Healthcheck

The container includes a built-in Docker healthcheck. After each scheduled run, the main process creates or removes a marker file at /tmp/.healthy. The health subcommand checks for this file's existence.

When it becomes unhealthy:

  • Tautulli API is unreachable (wrong URL, container down, network issue)
  • Plex API is unreachable (wrong URL, invalid token, container down)
  • API returns errors (invalid API key, rate limiting)

When it recovers:

  • The next successful scheduled run (where both APIs respond and the remap logic completes) recreates the marker file. No restart required.
  • A run where "nothing to remap" is found still counts as successful.

On first boot: The container runs immediately, then schedules subsequent runs. If Tautulli or Plex aren't ready yet (e.g. still starting), the first run fails and the container reports unhealthy until the next scheduled run succeeds. Make use of depends_on to solve this.

To check health manually:

docker inspect --format='{{json .State.Health.Log}}' tautulli-remap | python3 -m json.tool
Type Command Meaning
Docker /tautulli-remap health Exit 0 = last run completed successfully

Code Quality

Metric Value
Test Coverage 91.4%
Tests 164
Cyclomatic Complexity (avg) 6.3
Cognitive Complexity (avg) 8.8
Mutation Efficacy 77.0% (59 runs)
Test Framework Property-based (rapid) + table-driven

Tests cover the full matching pipeline (GUID, title+year, and title-only strategies with type guards, fallback chains, and whitespace handling), all Tautulli and Plex API functions with HTTP mock servers (including retry logic, pagination, context cancellation), stale key detection, Plex library indexing, remapping with dry-run/live modes, and the end-to-end run function. Property-based tests verify GUID normalization idempotency and panic-freedom, and rating key type coercion round-trips.

Not tested: main() (signal handling and scheduler loop) — a thin wrapper around the tested core logic, validated by Docker healthchecks in production.

Security Review

No vulnerabilities found. All scans clean across 8 tools.

Tool Result
govulncheck No vulnerabilities in call graph
golangci-lint (gosec, gocritic) 0 issues
trivy 0 vulnerabilities
grype 0 vulnerabilities
gitleaks No secrets detected
semgrep 1 info (false positive)
hadolint Clean

No network listener; connects outbound to Tautulli and Plex only. DRY_RUN=true by default prevents accidental changes. API tokens are never logged. Stdlib-only (zero external deps). Runs as nonroot on a distroless base image with no shell.

Details for advanced users: All HTTP clients use explicit timeouts (2 min client, 30s per-request). Response bodies capped via io.LimitReader (50 MB Tautulli, 100 MB Plex). Rating keys validated as numeric before URL interpolation (prevents path traversal). Plex token sent via X-Plex-Token header, not query string. No unsafe, reflect, os/exec, or file I/O beyond the health marker.

Dependencies

All dependencies are updated automatically via Renovate and pinned by digest or version for reproducibility.

Dependency Version Source
golang 1.26-alpine Go
gcr.io/distroless/static-debian13 nonroot Distroless

Design Principles

  • Always up to date: Base images, packages, and libraries are updated automatically via Renovate. Unlike many community Docker images that ship outdated or abandoned dependencies, these images receive continuous updates.
  • Minimal attack surface: When possible, pure Go apps use gcr.io/distroless/static:nonroot (no shell, no package manager, runs as non-root). Apps requiring system packages use Alpine with the minimum necessary privileges.
  • Digest-pinned: Every FROM instruction pins a SHA256 digest. All GitHub Actions are digest-pinned.
  • Multi-platform: Built for linux/amd64 and linux/arm64.
  • Healthchecks: Every container includes a Docker healthcheck.
  • Provenance: Build provenance is attested via GitHub Actions, verifiable with gh attestation verify.

Credits

This is an original tool that builds upon Tautulli. Inspired by SwiftPanda16's Tautulli rating key update script.

Disclaimer

These images are built with care and follow security best practices, but they are intended for homelab use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

This project is licensed under the GNU General Public License v3.0.