20 HDHomeRun Guide - seadogger-tech/seadogger-homelab GitHub Wiki
Integration of HDHomeRun XMLTV guide data with Jellyfin using an in-cluster HTTP proxy that works around Jellyfin's gzip decompression bug.
⚠️ Important: Jellyfin cannot parse gzip-compressed XMLTV data from the HDHomeRun API. We use an in-cluster proxy that serves uncompressed XMLTV to Jellyfin.
This system provides automated EPG (Electronic Program Guide) data from HDHomeRun devices to Jellyfin using a lightweight HTTP proxy deployed in the Jellyfin namespace.
Architecture:
-
XMLTV Proxy runs in the
jellyfinnamespace -
Fetches from HDHomeRun API with
Accept-Encoding: identity(no gzip) -
Serves uncompressed XMLTV to Jellyfin at
http://xmltv-proxy.jellyfin/xmltv.xml - Credentials stored in Kubernetes Secret (managed via Ansible)
Why a Proxy?
Jellyfin has a bug where it sends Accept-Encoding: gzip to XMLTV URLs but fails to decompress the response. The HDHomeRun API returns gzip-compressed data by default, causing Jellyfin to fail with "Data at the root level is invalid" errors. The proxy solves this by explicitly requesting uncompressed data.
- Gzip workaround – Explicitly requests uncompressed XMLTV data
- Lightweight – Python HTTP server in a 20MB Alpine container
- Always fresh – Fetches latest guide data on each Jellyfin request
- Credential management – Stored in Kubernetes Secret via Ansible
- Low resources – 50m CPU / 64Mi RAM requests
Location: deployments/jellyfin/xmltv-proxy.yaml
- ConfigMap: Python HTTP server script
-
Deployment: Single replica pod running
python:3.11-alpine -
Service: ClusterIP at
xmltv-proxy.jellyfinon port 80 -
Environment: Credentials loaded from
hdhomerun-credentialsSecret
Location: ansible/tasks/jellyfin_secrets.yml
Creates the hdhomerun-credentials Secret from ansible/config.yml variables:
-
hdhomerun_email- HDHomeRun account email -
hdhomerun_device_ids- Comma-separated device IDs
Add your HDHomeRun credentials to ansible/config.yml:
# --- HDHomeRun Credentials (for Jellyfin XMLTV Proxy) ---
hdhomerun_email: "[email protected]"
hdhomerun_device_ids: "YOUR_DEVICE_ID"Note: Find your Device ID on the HDHomeRun website or on the device label.
Run the Ansible playbook to create the Secret in the jellyfin namespace:
cd ansible
ansible-playbook -i hosts.ini main.yml --tags jellyfinThis creates the hdhomerun-credentials Secret with your email and device IDs.
The proxy deployment is managed by ArgoCD from the repository:
# Check if deployment exists
kubectl get deployment -n jellyfin xmltv-proxy
# View proxy logs
kubectl logs -n jellyfin -l app=xmltv-proxy --tail=20
# Check service
kubectl get svc -n jellyfin xmltv-proxyExpected Output:
XMLTV Proxy server running on port 8080
Proxying: https://api.hdhomerun.com/api/xmltv?Email=...&DeviceIDs=...
Verify the proxy returns valid XMLTV data:
# From within the cluster
kubectl exec -n jellyfin deployment/jellyfin -- \
curl -s http://xmltv-proxy.jellyfin/xmltv.xml | head -20Expected Output:
<?xml version="1.0" encoding="utf-8"?>
<tv source-info-url="https://www.hdhomerun.com/" source-info-name="HDHomeRun">
<channel id="US26588.hdhomerun.com">
<display-name>WKCFDT</display-name>
...Configure Jellyfin to use the XMLTV proxy:
- Navigate to Dashboard → Live TV → TV Guide Data Providers
- Click Add → Select XMLTV
- Set the File or URL to:
http://xmltv-proxy.jellyfin/xmltv.xml - Set Refresh interval: Daily (guide data is fetched fresh on each request)
- Click Save
After saving the configuration:
- Navigate to Dashboard → Scheduled Tasks
- Find Refresh Guide task
- Click Run Now
- Check Dashboard → Live TV → Guide to see program listings
- The guide should populate with 14 days of program data (HDHomeRun DVR subscription required)
Check deployment status:
kubectl get deployment -n jellyfin xmltv-proxy
kubectl get pods -n jellyfin -l app=xmltv-proxyView proxy logs:
kubectl logs -n jellyfin -l app=xmltv-proxy --tail=50Expected log output:
XMLTV Proxy server running on port 8080
Proxying: https://api.hdhomerun.com/api/xmltv?Email=...&DeviceIDs=...
Error: secret "hdhomerun-credentials" not found
Solution: Create the Secret via Ansible:
cd ansible
ansible-playbook -i hosts.ini main.yml --tags jellyfinVerify Secret exists:
kubectl get secret -n jellyfin hdhomerun-credentialsCheck proxy logs for errors:
kubectl logs -n jellyfin -l app=xmltv-proxy | grep -i errorCommon errors:
-
"HDHOMERUN_EMAIL and HDHOMERUN_DEVICE_IDS environment variables must be set"
- Solution: Ensure Secret is created and mounted correctly
-
"Failed to fetch XMLTV"
- Solution: Check network connectivity to HDHomeRun API
- Test:
curl https://api.hdhomerun.com/api/xmltv?Email=...&DeviceIDs=...
Error: "Data at the root level is invalid. Line 1, position 1"
This error indicates:
- Jellyfin is receiving gzip-compressed data (the proxy workaround isn't working)
- Or Jellyfin cannot reach the proxy
Solutions:
-
Verify proxy is accessible from Jellyfin:
kubectl exec -n jellyfin deployment/jellyfin -- \ curl -I http://xmltv-proxy.jellyfin/xmltv.xmlShould return:
HTTP/1.0 200 OKwithContent-Type: application/xml -
Check first bytes of response:
kubectl exec -n jellyfin deployment/jellyfin -- \ curl -s http://xmltv-proxy.jellyfin/xmltv.xml | head -c 100
Should start with:
<?xml version="1.0" encoding="utf-8"?> -
Force refresh in Jellyfin:
- Dashboard → Scheduled Tasks → Refresh Guide → Run Now
-
Check Jellyfin logs:
kubectl logs -n jellyfin deployment/jellyfin --tail=100 | grep -i "xmltv\|guide\|error"
Check proxy resource usage:
kubectl top pod -n jellyfin -l app=xmltv-proxyRestart proxy if needed:
kubectl rollout restart deployment/xmltv-proxy -n jellyfin-
Jellyfin Request: Jellyfin sends HTTP request to
http://xmltv-proxy.jellyfin/xmltv.xml -
Proxy Fetch: Proxy fetches from HDHomeRun API with
Accept-Encoding: identity(no gzip) - Uncompressed Response: HDHomeRun API returns uncompressed XMLTV (10+ MB)
-
Pass Through: Proxy serves the XML directly to Jellyfin with
Content-Encoding: identity
sequenceDiagram
participant J as Jellyfin Pod
participant P as XMLTV Proxy
participant H as HDHomeRun API
participant S as K8s Secret
Note over P,S: Startup: Load credentials
P->>S: Read HDHOMERUN_EMAIL<br/>and HDHOMERUN_DEVICE_IDS
S-->>P: Return credentials
Note over J,H: Guide Refresh Request
J->>P: GET http://xmltv-proxy.jellyfin/xmltv.xml<br/>(with Accept-Encoding: gzip)
P->>H: GET /api/xmltv?Email=...&DeviceIDs=...<br/>(with Accept-Encoding: identity)
Note over H: API returns<br/>uncompressed XMLTV<br/>(~10MB)
H-->>P: 200 OK<br/>Content-Type: application/xml<br/>Uncompressed XMLTV data
P-->>J: 200 OK<br/>Content-Encoding: identity<br/>Uncompressed XMLTV data
Note over J: Successfully parses<br/>XMLTV and populates<br/>14-day guide
Language: Python 3.11 (stdlib only, no dependencies)
Base Image: python:3.11-alpine (20MB)
HTTP Server: http.server.HTTPServer (Python stdlib)
Configuration: Environment variables from Kubernetes Secret
Key Code:
- Explicitly requests uncompressed:
req.add_header('Accept-Encoding', 'identity') - Sets response header:
Content-Encoding: identity - No caching - always fetches fresh data
- 09-Apps - Jellyfin application details
- 08-Security-and-Certificates - Kubernetes Secrets management
- HDHomeRun Official Docs
- Jellyfin Live TV Guide
- XMLTV Format Specification
- HDHomeRun XMLTV API