Generate XMLTV guide data from GraceNote/TMS listings for use with Jellyfin, Plex, Emby, TVHeadend, and other DVR/IPTV software.
- Scrapes 14 days of GraceNote/TMS program listings and outputs standard XMLTV format
- Enriches programs with TMDB poster images, ratings, descriptions, and release dates
- Enriches channel icons via the tv-logo/tv-logos project
- Runs as a long-lived server with automatic 24-hour refresh, or as a one-shot scrape for cron jobs
- First-run ZIP/postal-code setup with cable, satellite, and over-the-air lineup selection
- Guide data cached on disk — fast restarts without re-scraping
- Automatic XMLTV file rotation with 7-day retention
- Optional Jellyfin Live TV integration with in-browser streaming
- Optional channel filter to limit guide output to Jellyfin-available channels
- Bonus: built-in retro TV guide web UI ("The Grid")
- Run the scraper in server mode (see below).
- Open
http://<your-host>:8080/setupand choose your provider lineup. - When the first guide finishes building, click the XMLTV guide URL on the setup page to copy it, or add this equivalent URL to your DVR software:
http://<your-host>:8080/xmlguide.xmltv - Guide data refreshes automatically every 24 hours.
Alternatively, use --guide-only mode with a cron job and point your DVR software at the local xmlguide.xmltv file.
-
Clone the repo:
git clone https://github.com/daniel-widrick/GraceNoteScraper.git cd GraceNoteScraper -
Copy the environment file for optional integrations:
cp .env.example .env # Optionally add a TMDB token, Jellyfin settings, or legacy GN_* settings -
Start the container:
docker compose up -d
-
Open
http://<your-host>:8080/setup, enter your ZIP or postal code, and choose a provider lineup. -
After the first guide finishes building, click the XMLTV guide URL shown on the setup page to copy it into your DVR software.
Setup, guide data, caches, and images are persisted in a Docker volume. The container restarts automatically and refreshes guide data every 24 hours.
To view logs:
docker compose logs -fTo rebuild after pulling updates:
docker compose up -d --build- Docker and Docker Compose, or Go 1.27+ for building from source
- (Optional) A TMDB API read access token for poster images and metadata
If you prefer to run without Docker:
go build -o gracenotescraper .
cp .env.example .env
# Edit .env
./gracenotescraperScrapes once, writes xmlguide.xmltv to the working directory, and exits. Useful for cron-based setups where you don't need the server running.
Run server mode once to save a provider through /setup, or provide complete legacy GN_* settings, before using this mode.
./gracenotescraper --guide-only| Variable | Description | Default |
|---|---|---|
CONFIG_PATH |
Saved non-secret setup configuration | config.json |
GN_HEADEND |
Legacy/bootstrap GraceNote headend ID; use with GN_LINEUP and GN_ZIPCODE |
— |
GN_LINEUP |
Legacy/bootstrap full lineup string | — |
GN_COUNTRY |
Country code | USA |
GN_ZIPCODE |
Legacy/bootstrap ZIP or postal code | — |
GN_LANGUAGE |
Language code | en-us |
GN_DEVICE |
Device identifier | - |
TMDB_TOKEN |
TMDB read access token (optional) | — |
BASE_URL |
Server base URL — rewrites XMLTV image URLs to use the built-in proxy cache (e.g. http://192.168.1.50:8080) |
— |
PORT |
HTTP server port | 8080 |
JELLYFIN_URL |
Jellyfin server URL (optional — enables live TV integration) | — |
JELLYFIN_API_KEY |
Jellyfin API key | — |
JELLYFIN_CHANNEL_FILTER |
Set to any non-empty value to filter guide to only Jellyfin-available channels. Requires JELLYFIN_URL and JELLYFIN_API_KEY. |
— |
A saved CONFIG_PATH selection takes precedence over legacy GN_* settings. Delete or move that file if you intentionally want to bootstrap from environment settings again.
| Endpoint | Description |
|---|---|
GET /setup |
Choose or change the active provider lineup |
GET /api/setup/config |
Read the current non-secret lineup selection |
GET /api/setup/providers?postalCode=... |
Find Gracenote lineups for an area |
POST /api/setup/provider |
Save the selected provider and queue a fresh guide |
GET /xmlguide.xmltv |
XMLTV guide data (point your DVR here) |
GET /api/guide.json |
Guide data as JSON |
GET /api/lineup.json |
Every channel position in the active provider lineup as JSON (see below) |
GET / |
The Grid — built-in web UI |
GET /img?url=... |
Image proxy with local cache |
GET /api/livetv/config |
Returns {"enabled":true/false} — whether Jellyfin live TV is configured |
GET /api/livetv/channels |
Proxies Jellyfin channel list (requires JELLYFIN_URL and JELLYFIN_API_KEY) |
GET /api/livetv/tune?id=<channelId> |
Starts a live stream for the given channel and returns an HLS URL |
POST /api/livetv/stop |
Forwards a playback-stop notification to Jellyfin to end a live stream |
/api/lineup.json describes the provider lineup itself rather than the schedule: one entry per channel number, never collapsed, so a station carried at two numbers appears twice. It returns 503 with a Retry-After header until the first guide has been built.
{
"generated": "2026-09-13T04:10:22Z",
"source": {
"providerName": "Local Over the Air Broadcast",
"providerType": "OTA",
"location": "",
"lineupId": "USA-lineupId-DEFAULT",
"headendId": "lineupId",
"postalCode": "13490",
"country": "USA",
"device": "-",
"language": "en-us"
},
"positions": [
{
"number": "2.1",
"stationId": "53158",
"placementId": "531580",
"callSign": "WKTVDT",
"affiliate": "NATIONAL BROADCASTING COMPANY",
"affiliateCallSign": "",
"filters": ["sports", "news"],
"logoUrl": "https://raw.githubusercontent.com/tv-logo/tv-logos/main/countries/united-states/nbc-us.png"
}
]
}filters are Gracenote's own station tags with the filter- prefix removed. placementId is Gracenote's row identifier and is not stable across scrapes; use number plus stationId to identify a position. providerName, providerType, and location are filled from the saved setup when it matches the lineup the guide was built from.
The server includes a built-in retro-styled TV guide web UI at the root URL. If no provider is configured, / redirects to /setup. Once configured, the guide auto-scrolls through your channel lineup and shows program details, posters, and metadata. Handy for a quick glance at what's on without opening your DVR app.
The grid download and conversion live in the scrape package, so another Go program can fetch a lineup's listings without running the server:
import (
"context"
"fmt"
"github.com/daniel-widrick/GraceNoteScraper/scrape"
"github.com/daniel-widrick/GraceNoteScraper/web"
)
func main() {
prefs := web.Preferences{Country: "USA", ZipCode: "13490", Headend: "lineupId", LineupId: "USA-lineupId-DEFAULT", Device: "-", Language: "en-us"}
g, err := scrape.Fetch(context.Background(), prefs, scrape.Options{Days: 1})
if err != nil {
panic(err)
}
fmt.Printf("%d stations, %d lineup positions, %d programs\n", len(g.Channels), len(g.Lineup), len(g.Programs))
}Fetch returns a guide.TVGuide: Channels deduplicated by station, Lineup with every position, Programs, and Source. It pauses five seconds between grid requests by default and skips slots that fail; it returns scrape.ErrNoData only when every slot failed. Logo and TMDB enrichment are not part of the package. Lineup discovery by postal code is available through web.NewProviderClient().FindProviders.
The default grid client is web.NewClient, which retries failed requests and keeps a raw-response cache in a grid_cache/ directory under the working directory. Pass your own Options.Fetcher to change that.
appconfig/ Persisted non-secret provider configuration
main.go Entry point, HTTP server, enrichment, image proxy
scrape/ Grid download loop and guide assembly (importable)
guide/ GraceNote data types and XMLTV conversion
web/ HTTP client for GraceNote API
tmdb/ TMDB client and cache
tvlogo/ TV logo resolver and cache
util/ Shared helpers
index.html The Grid web UI (embedded at build time)
setup.html Provider-selection UI (embedded at build time)
guide.tmpl XMLTV output template (embedded at build time)
Portions of this project were developed with the assistance of generative AI (Claude).
