Initial commit: TVH2Jellyfin application

A Docker-based Python application that transfers completed recordings
from TVHeadend to Jellyfin media storage via SCP.

Features:
- Monitors TVHeadend for completed recordings via JSON API
- Transfers files via SCP with SSH key authentication
- Organizes files by show name
- Deletes recordings from TVHeadend after successful transfer
- Tracks transferred files to avoid duplicates
- Dry run mode for testing without making changes
This commit is contained in:
2026-01-07 17:43:09 +00:00
commit a8b247a83f
25 changed files with 2962 additions and 0 deletions

211
README.md Normal file
View File

@@ -0,0 +1,211 @@
# TVH2Jellyfin
Transfer completed recordings from TVHeadend to Jellyfin media storage.
## Features
- Monitors TVHeadend for completed recordings via JSON API
- Pulls files from TVHeadend server via SCP
- Writes to local Jellyfin media directory (Docker volume)
- Organizes files by show name: `{dest_path}/{show_name}/{episode}.ts`
- Deletes recordings from TVHeadend after successful transfer
- Tracks transferred files to avoid duplicates
- Runs as a Docker container
- Dry run mode for testing without making changes
## Prerequisites
1. SSH key access from the Docker host to the TVHeadend server
2. TVHeadend API access (username/password)
3. Docker and Docker Compose
## Setup
### 1. Generate SSH Key (if needed)
```bash
ssh-keygen -t rsa -b 4096 -f ~/.ssh/tvh2jellyfin
ssh-copy-id -i ~/.ssh/tvh2jellyfin.pub user@tvheadend-server
```
### 2. Configure Environment
Copy the example environment file and edit with your settings:
```bash
cp .env.example .env
```
Edit `.env` with your configuration:
```bash
# TVHeadend server
TVH_HOST=192.168.1.100
TVH_PORT=9981
TVH_USER=admin
TVH_PASS=your_password
# SSH user on TVHeadend server (defaults to TVH_USER)
TVH_SSH_USER=admin
# SSH key path on Docker host
SSH_KEY_PATH=~/.ssh/tvh2jellyfin
# Jellyfin media library path on Docker host
JELLYFIN_MEDIA_PATH=/mnt/jellyfin/tv
# Poll interval in seconds (default: 300 = 5 minutes)
POLL_INTERVAL=300
```
### 3. Build and Run
```bash
docker compose up -d
```
### 4. View Logs
```bash
docker compose logs -f
```
## Configuration Options
| Variable | Description | Default |
|----------|-------------|---------|
| `TVH_HOST` | TVHeadend server hostname/IP | `localhost` |
| `TVH_PORT` | TVHeadend HTTP port | `9981` |
| `TVH_USER` | TVHeadend API username | (required) |
| `TVH_PASS` | TVHeadend API password | (required) |
| `TVH_SSH_USER` | SSH username for TVHeadend server | `TVH_USER` |
| `SSH_KEY_PATH` | Path to SSH private key on Docker host | `~/.ssh/id_rsa` |
| `JELLYFIN_MEDIA_PATH` | Jellyfin media path on Docker host | `/mnt/jellyfin/tv` |
| `POLL_INTERVAL` | Seconds between checks | `300` |
| `LOG_LEVEL` | Logging level | `INFO` |
| `DRY_RUN` | Test mode (no transfers/deletions) | `false` |
## How It Works
```
┌─────────────────────┐ ┌─────────────────────┐
│ TVHeadend Server │ │ Docker Host │
│ │ │ │
│ ┌───────────────┐ │ HTTP API │ ┌───────────────┐ │
│ │ TVHeadend │◄─┼────────────────────┼──│ tvh2jellyfin │ │
│ │ API (:9981) │ │ │ │ container │ │
│ └───────────────┘ │ │ └───────┬───────┘ │
│ │ │ │ │
│ ┌───────────────┐ │ SCP (SSH) │ ▼ │
│ │ Recordings │──┼────────────────────┼──►┌─────────────┐ │
│ │ /recordings │ │ │ │ /media/tv │ │
│ └───────────────┘ │ │ │ (Jellyfin) │ │
│ │ │ └─────────────┘ │
└─────────────────────┘ └─────────────────────┘
```
1. Container queries TVHeadend API for completed recordings
2. Downloads files via SCP from TVHeadend server
3. Writes to local Jellyfin media volume
4. Removes recording from TVHeadend via API
## File Organization
Recordings are organized by show name:
```
/mnt/jellyfin/tv/
├── Show Name/
│ ├── Show Name - Episode Title.ts
│ └── Show Name - 2024-01-15 2000.ts (if no episode title)
└── Another Show/
└── Another Show - Episode.ts
```
## Dry Run Mode
Test what would happen without actually transferring or deleting anything:
```bash
DRY_RUN=true docker compose up
```
In dry run mode:
- Connects to TVHeadend API and lists completed recordings
- Shows what files would be transferred and where
- Shows which recordings have already been transferred
- Does NOT connect via SSH
- Does NOT transfer any files
- Does NOT delete recordings from TVHeadend
- Does NOT update the state file
- Exits after a single check (no polling loop)
Example output:
```
[DRY RUN] Starting sync cycle...
[DRY RUN] Found 2 new recording(s) to transfer
[DRY RUN] Would process: The News - Evening Edition
[DRY RUN] Source: /recordings/The News - Evening Edition.ts
[DRY RUN] Destination: /media/tv/The News/The News - Evening Edition.ts
[DRY RUN] Size: 1250.5 MB
[DRY RUN] Would delete from TVHeadend after transfer
```
## Development
### Install Dependencies
```bash
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
```
### Run Tests
```bash
pytest
```
### Run Locally
```bash
export TVH_HOST=...
export TVH_USER=...
export TVH_PASS=...
export TVH_SSH_USER=...
export DEST_PATH=/path/to/destination
python -m tvh2jellyfin.main
```
## Troubleshooting
### SSH Connection Issues
Ensure the SSH key has correct permissions:
```bash
chmod 600 ~/.ssh/tvh2jellyfin
```
Test SSH connection manually:
```bash
ssh -i ~/.ssh/tvh2jellyfin user@tvheadend-server
```
### TVHeadend API Issues
Test API access:
```bash
curl -u admin:password http://tvheadend:9981/api/serverinfo
```
### Permission Issues
Ensure the Jellyfin media directory is writable:
```bash
touch /mnt/jellyfin/tv/.test && rm /mnt/jellyfin/tv/.test
```
### View Transfer History
The state file (`data/state.json`) contains all transfer records.