Migrate admin services to jefflix.lol and add Hetzner Storage Box integration
- Move sonarr, radarr, lidarr, prowlarr to *.jefflix.lol - Move jellyseerr (requests) and qbittorrent (downloads) to jefflix.lol - Add Hetzner Storage Box SSHFS mount scripts for u521871 - Add media-archive.sh for content migration between local and Hetzner - Update all volume mounts to use /mnt/hetzner-media for media storage - Update README with new URLs and tiered storage documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b85484d65
commit
4113ccf83d
|
|
@ -33,3 +33,9 @@ VPN_COUNTRY=Germany
|
|||
|
||||
# Jellyseerr
|
||||
# Configure via web UI at https://requests.jeffemmett.com
|
||||
|
||||
# Hetzner Storage Box (Optional - for tiered storage/archival)
|
||||
# Sign up at: https://www.hetzner.com/storage/storage-box/
|
||||
# After signing up, run: sudo ./scripts/setup-hetzner-storage.sh
|
||||
HETZNER_USER=
|
||||
HETZNER_PASS=
|
||||
|
|
|
|||
128
README.md
128
README.md
|
|
@ -41,12 +41,15 @@ A self-hosted media server stack with automated request management, running on N
|
|||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Local NVMe Storage (3TB) │
|
||||
│ /media/movies /media/shows /media/music │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hetzner Storage Box (Optional Tier) │
|
||||
│ /media/archive/movies /media/archive/shows (Cold storage)│
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Users request media** via Jellyseerr (`requests.jeffemmett.com`)
|
||||
1. **Users request media** via Jellyseerr (`requests.jefflix.lol`)
|
||||
2. **Jellyseerr forwards** requests to Sonarr/Radarr/Lidarr
|
||||
3. **Prowlarr searches** configured indexers for the content
|
||||
4. **qBittorrent downloads** the files (optionally through VPN)
|
||||
|
|
@ -83,19 +86,19 @@ Add these hostnames to your Cloudflare tunnel config (`/root/cloudflared/config.
|
|||
|
||||
```yaml
|
||||
# Media Request System
|
||||
- hostname: requests.jeffemmett.com
|
||||
- hostname: requests.jefflix.lol
|
||||
service: http://localhost:80
|
||||
- hostname: invite.jeffemmett.com
|
||||
- hostname: invite.jefflix.lol
|
||||
service: http://localhost:80
|
||||
- hostname: sonarr.jeffemmett.com
|
||||
- hostname: sonarr.jefflix.lol
|
||||
service: http://localhost:80
|
||||
- hostname: radarr.jeffemmett.com
|
||||
- hostname: radarr.jefflix.lol
|
||||
service: http://localhost:80
|
||||
- hostname: prowlarr.jeffemmett.com
|
||||
- hostname: prowlarr.jefflix.lol
|
||||
service: http://localhost:80
|
||||
- hostname: lidarr.jeffemmett.com
|
||||
- hostname: lidarr.jefflix.lol
|
||||
service: http://localhost:80
|
||||
- hostname: downloads.jeffemmett.com
|
||||
- hostname: downloads.jefflix.lol
|
||||
service: http://localhost:80
|
||||
```
|
||||
|
||||
|
|
@ -104,34 +107,34 @@ Then restart cloudflared: `docker restart cloudflared`
|
|||
### 4. Configure Services (First Time Setup)
|
||||
|
||||
#### Step 1: Configure Prowlarr (Indexers)
|
||||
1. Go to `https://prowlarr.jeffemmett.com`
|
||||
1. Go to `https://prowlarr.jefflix.lol`
|
||||
2. Add indexers (torrent sites you have access to)
|
||||
3. Go to Settings → Apps → Add Radarr, Sonarr, Lidarr
|
||||
|
||||
#### Step 2: Configure Download Client
|
||||
1. Go to `https://downloads.jeffemmett.com`
|
||||
1. Go to `https://downloads.jefflix.lol`
|
||||
2. Default login: `admin` / `adminadmin` (change immediately!)
|
||||
3. Settings → Downloads → Default Save Path: `/downloads`
|
||||
|
||||
#### Step 3: Configure Radarr (Movies)
|
||||
1. Go to `https://radarr.jeffemmett.com`
|
||||
1. Go to `https://radarr.jefflix.lol`
|
||||
2. Settings → Media Management → Root Folder: `/movies`
|
||||
3. Settings → Download Clients → Add qBittorrent:
|
||||
- Host: `qbittorrent`
|
||||
- Port: `8080`
|
||||
|
||||
#### Step 4: Configure Sonarr (TV Shows)
|
||||
1. Go to `https://sonarr.jeffemmett.com`
|
||||
1. Go to `https://sonarr.jefflix.lol`
|
||||
2. Settings → Media Management → Root Folder: `/tv`
|
||||
3. Settings → Download Clients → Add qBittorrent (same as above)
|
||||
|
||||
#### Step 5: Configure Lidarr (Music)
|
||||
1. Go to `https://lidarr.jeffemmett.com`
|
||||
1. Go to `https://lidarr.jefflix.lol`
|
||||
2. Settings → Media Management → Root Folder: `/music`
|
||||
3. Settings → Download Clients → Add qBittorrent (same as above)
|
||||
|
||||
#### Step 6: Configure Jellyseerr (Requests)
|
||||
1. Go to `https://requests.jeffemmett.com`
|
||||
1. Go to `https://requests.jefflix.lol`
|
||||
2. Sign in with Jellyfin (use your Jellyfin server URL: `http://jellyfin:8096`)
|
||||
3. Add Radarr/Sonarr servers:
|
||||
- Hostname: `radarr` or `sonarr` (internal Docker hostname)
|
||||
|
|
@ -159,13 +162,13 @@ Then restart cloudflared: `docker restart cloudflared`
|
|||
|---------|-----|-------------|
|
||||
| Jellyfin | https://movies.jeffemmett.com | Video streaming (movies & TV) |
|
||||
| Navidrome | https://music.jeffemmett.com | Music streaming server |
|
||||
| Jellyseerr | https://requests.jeffemmett.com | Media request interface |
|
||||
| Jellyseerr | https://requests.jefflix.lol | Media request interface |
|
||||
| Wizarr | https://invite.jeffemmett.com | User invitation system |
|
||||
| Sonarr | https://sonarr.jeffemmett.com | TV show management |
|
||||
| Radarr | https://radarr.jeffemmett.com | Movie management |
|
||||
| Lidarr | https://lidarr.jeffemmett.com | Music management |
|
||||
| Prowlarr | https://prowlarr.jeffemmett.com | Indexer management |
|
||||
| qBittorrent | https://downloads.jeffemmett.com | Download client |
|
||||
| Sonarr | https://sonarr.jefflix.lol | TV show management |
|
||||
| Radarr | https://radarr.jefflix.lol | Movie management |
|
||||
| Lidarr | https://lidarr.jefflix.lol | Music management |
|
||||
| Prowlarr | https://prowlarr.jefflix.lol | Indexer management |
|
||||
| qBittorrent | https://downloads.jefflix.lol | Download client |
|
||||
|
||||
## Security Recommendations
|
||||
|
||||
|
|
@ -221,8 +224,91 @@ network_mode: "service:gluetun"
|
|||
│ └── complete/ # Completed downloads
|
||||
└── cache/
|
||||
└── jellyfin/ # Transcoding cache
|
||||
|
||||
/mnt/hetzner-media/ # Hetzner Storage Box (optional)
|
||||
└── archive/
|
||||
├── movies/ # Archived movies
|
||||
├── shows/ # Archived TV shows
|
||||
└── music/ # Archived music
|
||||
```
|
||||
|
||||
## Tiered Storage with Hetzner Storage Box
|
||||
|
||||
For additional storage capacity, you can add a Hetzner Storage Box as an archive tier. This is cost-effective (~€3/TB/month) and keeps older content accessible without filling up local NVMe.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Jellyfin Libraries │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ /media (Local NVMe - Fast) /media/archive (Hetzner) │
|
||||
│ ├── movies/ ← Active ├── movies/ ← Archived │
|
||||
│ ├── shows/ ← Recent ├── shows/ ← Old seasons │
|
||||
│ └── music/ ← Favorites └── music/ ← Full lib │
|
||||
│ │
|
||||
│ Hot storage: New downloads Cold storage: Older media │
|
||||
│ Fast transcoding & streaming Slower but cost-effective │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Sign up for Hetzner Storage Box**: https://www.hetzner.com/storage/storage-box/
|
||||
- Recommended: BX21 (5-10TB) for ~€11/month
|
||||
- Enable SMB/CIFS in Hetzner Console
|
||||
|
||||
2. **Run setup script on Netcup**:
|
||||
```bash
|
||||
ssh netcup
|
||||
cd /opt/media-server
|
||||
sudo ./scripts/setup-hetzner-storage.sh
|
||||
```
|
||||
|
||||
3. **Restart media stack**:
|
||||
```bash
|
||||
docker compose down && docker compose up -d
|
||||
```
|
||||
|
||||
4. **Add archive library in Jellyfin**:
|
||||
- Dashboard → Libraries → Add Media Library
|
||||
- Content type: Movies (or Shows)
|
||||
- Folder: `/media/archive/movies`
|
||||
|
||||
### Managing Archived Content
|
||||
|
||||
Use the `media-archive.sh` script to move content between local and Hetzner:
|
||||
|
||||
```bash
|
||||
# Check storage status
|
||||
./scripts/media-archive.sh status
|
||||
|
||||
# List content older than 90 days
|
||||
./scripts/media-archive.sh list-old
|
||||
|
||||
# Archive a specific movie
|
||||
./scripts/media-archive.sh archive movies "The Matrix (1999)"
|
||||
|
||||
# Archive all shows older than 180 days
|
||||
./scripts/media-archive.sh archive shows --older-than 180
|
||||
|
||||
# Restore content from archive
|
||||
./scripts/media-archive.sh restore movies "The Matrix (1999)"
|
||||
|
||||
# Backup all local media to Hetzner
|
||||
./scripts/media-archive.sh sync
|
||||
```
|
||||
|
||||
### Pricing Reference
|
||||
|
||||
| Plan | Storage | Monthly |
|
||||
|------|---------|---------|
|
||||
| BX11 | 1 TB | ~€3.50 |
|
||||
| BX21 | 5-10 TB | ~€11 |
|
||||
| BX31 | 20 TB | ~€25 |
|
||||
| BX41 | 40 TB | ~€47 |
|
||||
|
||||
## Upload Script Usage
|
||||
|
||||
```bash
|
||||
|
|
@ -288,7 +374,7 @@ Lyrics are automatically fetched from LRCLIB and displayed in Symfonium during p
|
|||
|
||||
### Spotify Playlist Import
|
||||
Lidarr syncs with your Spotify playlists:
|
||||
1. Go to `https://lidarr.jeffemmett.com`
|
||||
1. Go to `https://lidarr.jefflix.lol`
|
||||
2. Settings → Import Lists → Add → Spotify Playlists
|
||||
3. Authenticate with Spotify
|
||||
4. Select playlists to monitor
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ services:
|
|||
volumes:
|
||||
- ./config/jellyfin:/config
|
||||
- ./cache/jellyfin:/cache
|
||||
- ./media:/media
|
||||
# Hetzner Storage Box - all media served from here
|
||||
- /mnt/hetzner-media/media:/media:ro
|
||||
networks:
|
||||
- media-network
|
||||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jellyfin.rule=Host(`movies.jeffemmett.com`) || Host(`movies.jefflix.lol`) || Host(`jefflix.lol`) || Host(`www.jefflix.lol`)"
|
||||
- "traefik.http.routers.jellyfin.rule=Host(`movies.jeffemmett.com`) || Host(`movies.jefflix.lol`)"
|
||||
- "traefik.http.routers.jellyfin.entrypoints=web"
|
||||
- "traefik.http.routers.jellyfin.middlewares=jellyfin-headers"
|
||||
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
|
||||
|
|
@ -38,7 +39,7 @@ services:
|
|||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.jellyseerr.rule=Host(`requests.jeffemmett.com`) || Host(`requests.jefflix.lol`)"
|
||||
- "traefik.http.routers.jellyseerr.rule=Host(`requests.jefflix.lol`)"
|
||||
- "traefik.http.routers.jellyseerr.entrypoints=web"
|
||||
- "traefik.http.routers.jellyseerr.middlewares=jellyseerr-headers"
|
||||
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
|
||||
|
|
@ -60,7 +61,8 @@ services:
|
|||
- ND_IMAGECACHESIZE=500MB
|
||||
volumes:
|
||||
- ./config/navidrome:/data
|
||||
- ./media/music:/music:ro
|
||||
# Hetzner Storage Box - music library
|
||||
- /mnt/hetzner-media/media/music:/music:ro
|
||||
networks:
|
||||
- media-network
|
||||
- traefik-public
|
||||
|
|
@ -83,14 +85,15 @@ services:
|
|||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./config/sonarr:/config
|
||||
- ./media/shows:/tv
|
||||
- ./downloads:/downloads
|
||||
# Hetzner Storage Box - TV shows and downloads
|
||||
- /mnt/hetzner-media/media/shows:/tv
|
||||
- /mnt/hetzner-media/downloads:/downloads
|
||||
networks:
|
||||
- media-network
|
||||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.sonarr.rule=Host(`sonarr.jeffemmett.com`)"
|
||||
- "traefik.http.routers.sonarr.rule=Host(`sonarr.jefflix.lol`)"
|
||||
- "traefik.http.routers.sonarr.entrypoints=web"
|
||||
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
|
@ -105,14 +108,15 @@ services:
|
|||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./config/radarr:/config
|
||||
- ./media/movies:/movies
|
||||
- ./downloads:/downloads
|
||||
# Hetzner Storage Box - movies and downloads
|
||||
- /mnt/hetzner-media/media/movies:/movies
|
||||
- /mnt/hetzner-media/downloads:/downloads
|
||||
networks:
|
||||
- media-network
|
||||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.radarr.rule=Host(`radarr.jeffemmett.com`)"
|
||||
- "traefik.http.routers.radarr.rule=Host(`radarr.jefflix.lol`)"
|
||||
- "traefik.http.routers.radarr.entrypoints=web"
|
||||
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
|
@ -132,7 +136,7 @@ services:
|
|||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.prowlarr.rule=Host(`prowlarr.jeffemmett.com`)"
|
||||
- "traefik.http.routers.prowlarr.rule=Host(`prowlarr.jefflix.lol`)"
|
||||
- "traefik.http.routers.prowlarr.entrypoints=web"
|
||||
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
|
@ -147,14 +151,15 @@ services:
|
|||
- TZ=Europe/Berlin
|
||||
volumes:
|
||||
- ./config/lidarr:/config
|
||||
- ./media/music:/music
|
||||
- ./downloads:/downloads
|
||||
# Hetzner Storage Box - music and downloads
|
||||
- /mnt/hetzner-media/media/music:/music
|
||||
- /mnt/hetzner-media/downloads:/downloads
|
||||
networks:
|
||||
- media-network
|
||||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.lidarr.rule=Host(`lidarr.jeffemmett.com`)"
|
||||
- "traefik.http.routers.lidarr.rule=Host(`lidarr.jefflix.lol`)"
|
||||
- "traefik.http.routers.lidarr.entrypoints=web"
|
||||
- "traefik.http.services.lidarr.loadbalancer.server.port=8686"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
|
@ -188,7 +193,7 @@ services:
|
|||
profiles:
|
||||
- vpn
|
||||
|
||||
# Download client (routes through VPN when vpn profile enabled)
|
||||
# Download client - downloads directly to Hetzner Storage Box
|
||||
qbittorrent:
|
||||
image: linuxserver/qbittorrent:latest
|
||||
container_name: qbittorrent
|
||||
|
|
@ -200,7 +205,8 @@ services:
|
|||
- WEBUI_PORT=8080
|
||||
volumes:
|
||||
- ./config/qbittorrent:/config
|
||||
- ./downloads:/downloads
|
||||
# Hetzner Storage Box - all downloads go here
|
||||
- /mnt/hetzner-media/downloads:/downloads
|
||||
# When VPN enabled, use gluetun network
|
||||
# network_mode: "service:gluetun"
|
||||
networks:
|
||||
|
|
@ -208,7 +214,7 @@ services:
|
|||
- traefik-public
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.qbittorrent.rule=Host(`downloads.jeffemmett.com`)"
|
||||
- "traefik.http.routers.qbittorrent.rule=Host(`downloads.jefflix.lol`)"
|
||||
- "traefik.http.routers.qbittorrent.entrypoints=web"
|
||||
- "traefik.http.services.qbittorrent.loadbalancer.server.port=8080"
|
||||
- "traefik.docker.network=traefik-public"
|
||||
|
|
@ -247,7 +253,7 @@ services:
|
|||
- TRANSMISSION_WEB_HOME=/web
|
||||
volumes:
|
||||
- ./config/transmission:/config
|
||||
- ./downloads:/downloads
|
||||
- /mnt/hetzner-media/downloads:/downloads
|
||||
- ./watch:/watch
|
||||
ports:
|
||||
- 9091:9091
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
#!/bin/bash
|
||||
# Media Archive Manager - Move content between local and Hetzner storage
|
||||
# Usage: media-archive.sh [archive|restore|status|list-old]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
LOCAL_MEDIA="/opt/media-server/media"
|
||||
HETZNER_MEDIA="/mnt/hetzner-media/archive"
|
||||
LOG_FILE="/opt/media-server/logs/archive.log"
|
||||
DAYS_OLD=90 # Content older than this is considered for archiving
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Ensure log directory exists
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
log() {
|
||||
echo "[$(date -Iseconds)] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
check_mount() {
|
||||
if ! mountpoint -q "$HETZNER_MEDIA" 2>/dev/null; then
|
||||
# Try to trigger automount
|
||||
ls "$HETZNER_MEDIA" &>/dev/null 2>&1
|
||||
sleep 2
|
||||
if ! mountpoint -q "$HETZNER_MEDIA" 2>/dev/null; then
|
||||
echo -e "${RED}Error: Hetzner storage not mounted at $HETZNER_MEDIA${NC}"
|
||||
echo "Run: sudo systemctl start mnt-hetzner\\x2dmedia.mount"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo -e "${GREEN}Hetzner storage mounted${NC}"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo -e "${BLUE}Storage Status${NC}"
|
||||
echo "=============="
|
||||
echo ""
|
||||
|
||||
echo -e "${YELLOW}Local Storage (/opt/media-server/media):${NC}"
|
||||
if [ -d "$LOCAL_MEDIA" ]; then
|
||||
du -sh "$LOCAL_MEDIA"/* 2>/dev/null || echo " (empty or not accessible)"
|
||||
else
|
||||
echo " Not found"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo -e "${YELLOW}Hetzner Storage (/mnt/hetzner-media):${NC}"
|
||||
if mountpoint -q "/mnt/hetzner-media" 2>/dev/null || ls "/mnt/hetzner-media" &>/dev/null 2>&1; then
|
||||
df -h "/mnt/hetzner-media"
|
||||
echo ""
|
||||
echo "Archive contents:"
|
||||
du -sh "$HETZNER_MEDIA"/* 2>/dev/null || echo " (empty)"
|
||||
else
|
||||
echo " Not mounted"
|
||||
fi
|
||||
}
|
||||
|
||||
list_old_content() {
|
||||
echo -e "${BLUE}Content older than $DAYS_OLD days${NC}"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
for media_type in movies shows music; do
|
||||
echo -e "${YELLOW}$media_type:${NC}"
|
||||
if [ -d "$LOCAL_MEDIA/$media_type" ]; then
|
||||
find "$LOCAL_MEDIA/$media_type" -maxdepth 1 -mindepth 1 -type d -mtime +$DAYS_OLD 2>/dev/null | while read dir; do
|
||||
size=$(du -sh "$dir" 2>/dev/null | cut -f1)
|
||||
name=$(basename "$dir")
|
||||
echo " [$size] $name"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
archive_content() {
|
||||
local media_type=$1
|
||||
local name=$2
|
||||
|
||||
if [ -z "$media_type" ] || [ -z "$name" ]; then
|
||||
echo "Usage: media-archive.sh archive <movies|shows|music> <name>"
|
||||
echo " media-archive.sh archive <movies|shows|music> --older-than <days>"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " media-archive.sh archive movies 'The Matrix (1999)'"
|
||||
echo " media-archive.sh archive shows --older-than 180"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_mount
|
||||
|
||||
# Handle batch archival
|
||||
if [ "$name" == "--older-than" ]; then
|
||||
local days=${3:-$DAYS_OLD}
|
||||
echo -e "${YELLOW}Archiving $media_type older than $days days...${NC}"
|
||||
|
||||
find "$LOCAL_MEDIA/$media_type" -maxdepth 1 -mindepth 1 -type d -mtime +$days 2>/dev/null | while read dir; do
|
||||
local item_name=$(basename "$dir")
|
||||
archive_single "$media_type" "$item_name"
|
||||
done
|
||||
return
|
||||
fi
|
||||
|
||||
archive_single "$media_type" "$name"
|
||||
}
|
||||
|
||||
archive_single() {
|
||||
local media_type=$1
|
||||
local name=$2
|
||||
local source="$LOCAL_MEDIA/$media_type/$name"
|
||||
local dest="$HETZNER_MEDIA/$media_type/$name"
|
||||
|
||||
if [ ! -d "$source" ]; then
|
||||
echo -e "${RED}Error: $source not found${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local size=$(du -sh "$source" 2>/dev/null | cut -f1)
|
||||
echo -e "${BLUE}Archiving: $name ($size)${NC}"
|
||||
|
||||
# Create destination directory
|
||||
mkdir -p "$HETZNER_MEDIA/$media_type"
|
||||
|
||||
# Use rsync for reliable transfer with progress
|
||||
log "Archiving $media_type/$name ($size) to Hetzner"
|
||||
rsync -avh --progress --remove-source-files "$source/" "$dest/"
|
||||
|
||||
# Remove empty source directory
|
||||
rmdir "$source" 2>/dev/null || true
|
||||
|
||||
echo -e "${GREEN}Archived: $name${NC}"
|
||||
log "Completed archiving $media_type/$name"
|
||||
}
|
||||
|
||||
restore_content() {
|
||||
local media_type=$1
|
||||
local name=$2
|
||||
|
||||
if [ -z "$media_type" ] || [ -z "$name" ]; then
|
||||
echo "Usage: media-archive.sh restore <movies|shows|music> <name>"
|
||||
echo ""
|
||||
echo "List available archives:"
|
||||
echo " ls /mnt/hetzner-media/archive/<type>/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_mount
|
||||
|
||||
local source="$HETZNER_MEDIA/$media_type/$name"
|
||||
local dest="$LOCAL_MEDIA/$media_type/$name"
|
||||
|
||||
if [ ! -d "$source" ]; then
|
||||
echo -e "${RED}Error: $source not found in archive${NC}"
|
||||
echo ""
|
||||
echo "Available in $media_type archive:"
|
||||
ls -1 "$HETZNER_MEDIA/$media_type/" 2>/dev/null || echo " (empty)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local size=$(du -sh "$source" 2>/dev/null | cut -f1)
|
||||
echo -e "${BLUE}Restoring: $name ($size)${NC}"
|
||||
|
||||
log "Restoring $media_type/$name ($size) from Hetzner"
|
||||
rsync -avh --progress "$source/" "$dest/"
|
||||
|
||||
echo -e "${GREEN}Restored: $name${NC}"
|
||||
echo "Content available at: $dest"
|
||||
log "Completed restoring $media_type/$name"
|
||||
}
|
||||
|
||||
sync_to_hetzner() {
|
||||
# One-way sync from local to Hetzner (backup mode)
|
||||
check_mount
|
||||
|
||||
echo -e "${BLUE}Syncing local media to Hetzner (backup)...${NC}"
|
||||
|
||||
for media_type in movies shows music; do
|
||||
if [ -d "$LOCAL_MEDIA/$media_type" ]; then
|
||||
echo -e "${YELLOW}Syncing $media_type...${NC}"
|
||||
mkdir -p "$HETZNER_MEDIA/$media_type"
|
||||
rsync -avh --progress "$LOCAL_MEDIA/$media_type/" "$HETZNER_MEDIA/$media_type/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}Sync complete${NC}"
|
||||
}
|
||||
|
||||
# Main command handler
|
||||
case "${1:-status}" in
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
list-old|list)
|
||||
list_old_content
|
||||
;;
|
||||
archive)
|
||||
archive_content "$2" "$3" "$4"
|
||||
;;
|
||||
restore)
|
||||
restore_content "$2" "$3"
|
||||
;;
|
||||
sync|backup)
|
||||
sync_to_hetzner
|
||||
;;
|
||||
help|--help|-h)
|
||||
echo "Media Archive Manager"
|
||||
echo "====================="
|
||||
echo ""
|
||||
echo "Usage: media-archive.sh <command> [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " status Show storage status and usage"
|
||||
echo " list-old List content older than $DAYS_OLD days"
|
||||
echo " archive <type> <name> Archive specific content to Hetzner"
|
||||
echo " archive <type> --older-than <days> Archive all content older than N days"
|
||||
echo " restore <type> <name> Restore content from Hetzner to local"
|
||||
echo " sync Sync all local media to Hetzner (backup)"
|
||||
echo ""
|
||||
echo "Types: movies, shows, music"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " media-archive.sh status"
|
||||
echo " media-archive.sh list-old"
|
||||
echo " media-archive.sh archive movies 'The Matrix (1999)'"
|
||||
echo " media-archive.sh archive shows --older-than 180"
|
||||
echo " media-archive.sh restore movies 'The Matrix (1999)'"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $1"
|
||||
echo "Run 'media-archive.sh help' for usage"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
#!/bin/bash
|
||||
# Setup Hetzner Storage Box via SSHFS on Netcup RS 8000
|
||||
# Storage Box: u521871 (BX21 - 5-10TB)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
HETZNER_USER="u521871"
|
||||
HETZNER_HOST="${HETZNER_USER}.your-storagebox.de"
|
||||
HETZNER_PORT="23"
|
||||
SSH_KEY="/root/.ssh/hetzner_storagebox"
|
||||
MOUNT_POINT="/mnt/hetzner-media"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}Hetzner Storage Box SSHFS Setup${NC}"
|
||||
echo "=================================="
|
||||
echo "User: $HETZNER_USER"
|
||||
echo "Host: $HETZNER_HOST"
|
||||
echo "Mount: $MOUNT_POINT"
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Please run as root (sudo)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check SSH key exists
|
||||
if [ ! -f "$SSH_KEY" ]; then
|
||||
echo -e "${RED}SSH key not found at $SSH_KEY${NC}"
|
||||
echo "Generate one with: ssh-keygen -t ed25519 -f $SSH_KEY -C 'storagebox@netcup'"
|
||||
echo "Then upload with: cat ${SSH_KEY}.pub | ssh -p23 ${HETZNER_USER}@${HETZNER_HOST} install-ssh-key"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install sshfs
|
||||
echo -e "${GREEN}Installing SSHFS...${NC}"
|
||||
apt-get update -qq
|
||||
apt-get install -y sshfs
|
||||
|
||||
# Create mount point
|
||||
echo -e "${GREEN}Creating mount point...${NC}"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
|
||||
# Test SSH connection (use 'ls' as Hetzner has restricted shell)
|
||||
echo -e "${GREEN}Testing SSH connection...${NC}"
|
||||
if ! ssh -p "$HETZNER_PORT" -i "$SSH_KEY" -o BatchMode=yes -o ConnectTimeout=10 \
|
||||
"${HETZNER_USER}@${HETZNER_HOST}" "ls" &>/dev/null; then
|
||||
echo -e "${RED}SSH connection failed!${NC}"
|
||||
echo "Make sure:"
|
||||
echo " 1. SSH is enabled in Hetzner Console"
|
||||
echo " 2. Your key is uploaded: cat ${SSH_KEY}.pub | ssh -p23 ${HETZNER_USER}@${HETZNER_HOST} install-ssh-key"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}SSH connection successful${NC}"
|
||||
|
||||
# Create directory structure on Hetzner
|
||||
echo -e "${GREEN}Creating directory structure on Hetzner...${NC}"
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p media/movies" 2>/dev/null || true
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p media/shows" 2>/dev/null || true
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p media/music" 2>/dev/null || true
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/complete/movies" 2>/dev/null || true
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/complete/tv" 2>/dev/null || true
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/complete/music" 2>/dev/null || true
|
||||
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/incomplete" 2>/dev/null || true
|
||||
echo -e "${GREEN}Directories created${NC}"
|
||||
|
||||
# Create systemd mount unit
|
||||
echo -e "${GREEN}Creating systemd mount unit...${NC}"
|
||||
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.mount << EOF
|
||||
[Unit]
|
||||
Description=Hetzner Storage Box (u521871 - SSHFS)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Mount]
|
||||
What=${HETZNER_USER}@${HETZNER_HOST}:/
|
||||
Where=${MOUNT_POINT}
|
||||
Type=fuse.sshfs
|
||||
Options=port=${HETZNER_PORT},IdentityFile=${SSH_KEY},allow_other,default_permissions,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,_netdev,uid=1000,gid=1000
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Create automount unit for on-demand mounting
|
||||
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.automount << EOF
|
||||
[Unit]
|
||||
Description=Automount Hetzner Storage Box
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Automount]
|
||||
Where=${MOUNT_POINT}
|
||||
TimeoutIdleSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Enable user_allow_other in fuse.conf
|
||||
if ! grep -q "^user_allow_other" /etc/fuse.conf 2>/dev/null; then
|
||||
echo "user_allow_other" >> /etc/fuse.conf
|
||||
fi
|
||||
|
||||
# Reload systemd and start mount
|
||||
echo -e "${GREEN}Enabling and starting mount...${NC}"
|
||||
systemctl daemon-reload
|
||||
systemctl enable mnt-hetzner\\x2dmedia.mount
|
||||
systemctl start mnt-hetzner\\x2dmedia.mount
|
||||
|
||||
# Verify mount
|
||||
sleep 2
|
||||
if mountpoint -q "$MOUNT_POINT"; then
|
||||
echo -e "${GREEN}Mount successful!${NC}"
|
||||
echo ""
|
||||
df -h "$MOUNT_POINT"
|
||||
echo ""
|
||||
echo "Directory structure:"
|
||||
ls -la "$MOUNT_POINT"
|
||||
else
|
||||
echo -e "${RED}Mount may have failed. Checking status...${NC}"
|
||||
systemctl status mnt-hetzner\\x2dmedia.mount
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save config reference
|
||||
cat > /opt/media-server/.hetzner-config << EOF
|
||||
HETZNER_USER=$HETZNER_USER
|
||||
HETZNER_HOST=$HETZNER_HOST
|
||||
MOUNT_POINT=$MOUNT_POINT
|
||||
SSH_KEY=$SSH_KEY
|
||||
MOUNT_TYPE=sshfs
|
||||
SETUP_DATE=$(date -Iseconds)
|
||||
EOF
|
||||
chmod 600 /opt/media-server/.hetzner-config
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Storage mounted at: $MOUNT_POINT"
|
||||
echo ""
|
||||
echo "Directory structure:"
|
||||
echo " $MOUNT_POINT/media/movies - Movie library"
|
||||
echo " $MOUNT_POINT/media/shows - TV show library"
|
||||
echo " $MOUNT_POINT/media/music - Music library"
|
||||
echo " $MOUNT_POINT/downloads/ - qBittorrent downloads"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Deploy updated docker-compose: docker compose up -d"
|
||||
echo " 2. Configure Jellyfin library paths"
|
||||
echo " 3. Verify qBittorrent download path"
|
||||
echo ""
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
#!/bin/bash
|
||||
# Setup Hetzner Storage Box mount on Netcup RS 8000
|
||||
# Run this script ON the Netcup server after signing up for Hetzner Storage Box
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Hetzner Storage Box Setup${NC}"
|
||||
echo "=========================="
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Please run as root (sudo)${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prompt for Hetzner credentials if not provided via env
|
||||
if [ -z "$HETZNER_USER" ]; then
|
||||
read -p "Enter Hetzner Storage Box username (e.g., u123456): " HETZNER_USER
|
||||
fi
|
||||
|
||||
if [ -z "$HETZNER_PASS" ]; then
|
||||
read -sp "Enter Hetzner Storage Box password: " HETZNER_PASS
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Validate input
|
||||
if [ -z "$HETZNER_USER" ] || [ -z "$HETZNER_PASS" ]; then
|
||||
echo -e "${RED}Error: Username and password are required${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HETZNER_HOST="${HETZNER_USER}.your-storagebox.de"
|
||||
MOUNT_POINT="/mnt/hetzner-media"
|
||||
CREDENTIALS_FILE="/etc/smbcredentials/hetzner"
|
||||
|
||||
echo -e "${YELLOW}Configuration:${NC}"
|
||||
echo " Host: $HETZNER_HOST"
|
||||
echo " User: $HETZNER_USER"
|
||||
echo " Mount: $MOUNT_POINT"
|
||||
echo ""
|
||||
|
||||
# Install required packages
|
||||
echo -e "${GREEN}Installing CIFS utilities...${NC}"
|
||||
apt-get update -qq
|
||||
apt-get install -y cifs-utils
|
||||
|
||||
# Create mount point
|
||||
echo -e "${GREEN}Creating mount point...${NC}"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
mkdir -p "$MOUNT_POINT/archive"
|
||||
mkdir -p "$MOUNT_POINT/movies"
|
||||
mkdir -p "$MOUNT_POINT/shows"
|
||||
mkdir -p "$MOUNT_POINT/music"
|
||||
|
||||
# Create credentials file
|
||||
echo -e "${GREEN}Creating credentials file...${NC}"
|
||||
mkdir -p /etc/smbcredentials
|
||||
cat > "$CREDENTIALS_FILE" << EOF
|
||||
username=$HETZNER_USER
|
||||
password=$HETZNER_PASS
|
||||
EOF
|
||||
chmod 600 "$CREDENTIALS_FILE"
|
||||
|
||||
# Create systemd mount unit
|
||||
echo -e "${GREEN}Creating systemd mount unit...${NC}"
|
||||
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.mount << EOF
|
||||
[Unit]
|
||||
Description=Hetzner Storage Box (Media Archive)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Mount]
|
||||
What=//${HETZNER_HOST}/${HETZNER_USER}
|
||||
Where=${MOUNT_POINT}
|
||||
Type=cifs
|
||||
Options=credentials=${CREDENTIALS_FILE},iocharset=utf8,rw,vers=3.0,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,nofail,_netdev
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Create automount unit for on-demand mounting
|
||||
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.automount << EOF
|
||||
[Unit]
|
||||
Description=Automount Hetzner Storage Box
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Automount]
|
||||
Where=${MOUNT_POINT}
|
||||
TimeoutIdleSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Reload systemd and enable mount
|
||||
echo -e "${GREEN}Enabling mount...${NC}"
|
||||
systemctl daemon-reload
|
||||
systemctl enable mnt-hetzner\\x2dmedia.automount
|
||||
systemctl start mnt-hetzner\\x2dmedia.automount
|
||||
|
||||
# Test the mount
|
||||
echo -e "${GREEN}Testing mount...${NC}"
|
||||
if mountpoint -q "$MOUNT_POINT" || ls "$MOUNT_POINT" &>/dev/null; then
|
||||
echo -e "${GREEN}Mount successful!${NC}"
|
||||
df -h "$MOUNT_POINT"
|
||||
else
|
||||
echo -e "${YELLOW}Mount will activate on first access (automount enabled)${NC}"
|
||||
# Trigger automount by accessing
|
||||
ls "$MOUNT_POINT" &>/dev/null
|
||||
sleep 2
|
||||
if mountpoint -q "$MOUNT_POINT"; then
|
||||
echo -e "${GREEN}Mount successful!${NC}"
|
||||
df -h "$MOUNT_POINT"
|
||||
else
|
||||
echo -e "${RED}Mount may have failed. Check with: systemctl status mnt-hetzner\\x2dmedia.mount${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create directory structure on Hetzner
|
||||
echo -e "${GREEN}Creating directory structure on Hetzner...${NC}"
|
||||
mkdir -p "$MOUNT_POINT/archive/movies"
|
||||
mkdir -p "$MOUNT_POINT/archive/shows"
|
||||
mkdir -p "$MOUNT_POINT/archive/music"
|
||||
|
||||
# Save config for reference
|
||||
cat > /opt/media-server/.hetzner-config << EOF
|
||||
HETZNER_USER=$HETZNER_USER
|
||||
HETZNER_HOST=$HETZNER_HOST
|
||||
MOUNT_POINT=$MOUNT_POINT
|
||||
SETUP_DATE=$(date -Iseconds)
|
||||
EOF
|
||||
chmod 600 /opt/media-server/.hetzner-config
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Setup complete!${NC}"
|
||||
echo ""
|
||||
echo "Mount point: $MOUNT_POINT"
|
||||
echo ""
|
||||
echo "Directory structure:"
|
||||
echo " $MOUNT_POINT/archive/movies - Archived movies"
|
||||
echo " $MOUNT_POINT/archive/shows - Archived TV shows"
|
||||
echo " $MOUNT_POINT/archive/music - Archived music"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Run: docker compose down && docker compose up -d"
|
||||
echo " 2. Use media-archive.sh to move content to Hetzner"
|
||||
echo ""
|
||||
Loading…
Reference in New Issue