diff --git a/.env.example b/.env.example index ca55685..abb6cc5 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/README.md b/README.md index 911e503..c0369e5 100644 --- a/README.md +++ b/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 diff --git a/docker-compose-server.yml b/docker-compose-server.yml index 0793de2..7426cf3 100644 --- a/docker-compose-server.yml +++ b/docker-compose-server.yml @@ -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 diff --git a/scripts/media-archive.sh b/scripts/media-archive.sh new file mode 100755 index 0000000..9f0af69 --- /dev/null +++ b/scripts/media-archive.sh @@ -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 " + echo " media-archive.sh archive --older-than " + 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 " + echo "" + echo "List available archives:" + echo " ls /mnt/hetzner-media/archive//" + 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 [options]" + echo "" + echo "Commands:" + echo " status Show storage status and usage" + echo " list-old List content older than $DAYS_OLD days" + echo " archive Archive specific content to Hetzner" + echo " archive --older-than Archive all content older than N days" + echo " restore 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 diff --git a/scripts/setup-hetzner-sshfs.sh b/scripts/setup-hetzner-sshfs.sh new file mode 100755 index 0000000..c911a33 --- /dev/null +++ b/scripts/setup-hetzner-sshfs.sh @@ -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 "" diff --git a/scripts/setup-hetzner-storage.sh b/scripts/setup-hetzner-storage.sh new file mode 100755 index 0000000..fbb2b51 --- /dev/null +++ b/scripts/setup-hetzner-storage.sh @@ -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 ""