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
212 lines
6.0 KiB
Markdown
212 lines
6.0 KiB
Markdown
# 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.
|