diff --git a/README.md b/README.md index 456a75b..d5af92a 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,68 @@ -## Riven Distributables +# Riven Scripts -This repository contains helper scripts and installers for deploying Riven on -different platforms (for example, Proxmox VE and Unraid). Each platform has its -own subdirectory and documentation. +This repository contains official helper scripts and installers for deploying +**Riven** on supported platforms. + +Each platform has its own subdirectory with a dedicated installer and +documentation. --- -## Quick start: Proxmox VE LXC +## Quick Start + +### Proxmox VE (LXC) -To create a Debian 12, unprivileged LXC running Riven on a Proxmox VE host, -run this from the **Proxmox host shell**: +To create a **Debian 12 unprivileged LXC** configured to run Riven on a +**Proxmox VE host**, run the following command **from the Proxmox host shell**: ```bash -bash -c "$(wget -qLO - https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/riven.sh)" +bash -c "$(wget -qLO - https://raw.githubusercontent.com/AquaHorizonGaming/Riven-Scripts/main/proxmox/install.sh)" ``` -For detailed Proxmox instructions (requirements, configuration, and troubleshooting), -see: +This installer handles: +- LXC creation and configuration +- Docker + FUSE setup +- Required mount propagation for Riven +- Optional GPU passthrough support +Full Proxmox documentation: - [`proxmox/README.md`](proxmox/README.md) -Additional installers (such as Unraid) will live in their own subdirectories -with their own README files. +--- + +### Ubuntu (Bare Metal / VM) + +To install Riven directly on an **Ubuntu system** (VM or bare metal), +run the installer below: + +```bash +sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/Riven-Scripts/main/ubuntu/install.sh)" +``` + +This installer handles: +- System dependency installation +- Docker and Docker Compose setup +- Riven service preparation and directory layout + +Full Ubuntu documentation: +- [`ubuntu/readme.md`](ubuntu/readme.md) + +--- + +## Repository Structure + +```text +Riven-Scripts/ +├── proxmox/ # Proxmox VE LXC installer + docs +├── ubuntu/ # Ubuntu installer + docs +└── README.md # This file +``` + +--- + +## Notes + +- Each platform is self-contained and documented independently +- Always follow the README inside the platform directory for configuration + and troubleshooting +- Additional platforms can be added later using the same structure diff --git a/db-tools/db_pegger_9000.sh b/db-tools/db_pegger_9000.sh new file mode 100644 index 0000000..2926f98 --- /dev/null +++ b/db-tools/db_pegger_9000.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +# Function to check if the Docker container is running +check_container_status() { + docker ps | grep -q "riven-db" + if [ $? -ne 0 ]; then + echo "Error: The 'riven-db' container is not running. Please start the container and try again." + exit 1 + fi +} + +# Function to perform a backup +backup_database() { + local timestamp=$(date +"%Y-%m-%d_%H-%M-%S") + local backup_file="/tmp/riven_backup_${timestamp}.sql" + + echo "Do you want to create a backup of the database before proceeding?" + read -p "Type 'y' to back up or 'n' to skip: " backup_choice + + if [[ "$backup_choice" =~ ^[Yy]$ ]]; then + echo "Creating a backup of the database..." + + # Use docker exec to dump the database + docker exec riven-db pg_dump -U postgres -d riven -f "$backup_file" + + if [ $? -eq 0 ]; then + echo "Backup created successfully: $backup_file" + else + echo "Error: Backup failed." + exit 1 + fi + else + echo "No backup created. Proceeding with the reset." + fi +} + +# Function to fetch and display state items with confirmation +fetch_state_items() { + local state=$1 + echo "Do you want to see the items in the '$state' state?" + read -p "Type 'y' to view them or 'n' to skip: " view_choice + if [[ "$view_choice" =~ ^[Yy]$ ]]; then + echo "Fetching items in the '$state' state..." + docker exec riven-db psql -U postgres -d riven -c "SELECT id, title, last_state, scraped_times FROM \"MediaItem\" WHERE last_state = '$state';" + else + echo "Skipping viewing '$state' items." + fi +} + +# Function to reset state items (Unknown, Paused, Failed) +reset_state_items() { + local state=$1 + echo "You are about to reset the '$state' items to 'Indexed'." + echo "This will reset the following attributes:" + echo " - 'scraped_times' to 0" + echo " - 'scraped_at' to NULL" + echo " - 'active_stream' to NULL" + read -p "Press Enter to confirm or CTRL+C to cancel..." + + # Perform the reset in the database + echo "Resetting '$state' items..." + docker exec riven-db psql -U postgres -d riven -c " + BEGIN; + UPDATE \"MediaItem\" + SET last_state = 'Indexed', + scraped_at = NULL, + scraped_times = 0, + active_stream = NULL + WHERE last_state = '$state'; + COMMIT; + " + if [ $? -eq 0 ]; then + echo "'$state' items successfully reset to 'Indexed'." + else + echo "Error: Database update failed for '$state'. Rolling back changes." + docker exec riven-db psql -U postgres -d riven -c "ROLLBACK;" + exit 1 + fi +} + +# Function to ask the user which states they want to reset +ask_reset_states() { + echo "Select which states you want to reset (you can choose multiple states):" + echo "1) Unknown" + echo "2) Paused" + echo "3) Failed" + read -p "Enter your choices (e.g., '1 3' for Unknown and Failed, '2 3' for Paused and Failed, etc.): " -a choices + + # Loop through the choices and perform actions for each selected state + for choice in "${choices[@]}"; do + case $choice in + 1) + fetch_state_items "Unknown" + reset_state_items "Unknown" + ;; + 2) + fetch_state_items "Paused" + reset_state_items "Paused" + ;; + 3) + fetch_state_items "Failed" + reset_state_items "Failed" + ;; + *) + echo "Invalid choice: $choice. Skipping." + ;; + esac + done +} + +# Function to display current state counts after reset +show_current_counts() { + echo "Fetching current counts of MediaItem states..." + + indexed_count=$(docker exec riven-db psql -U postgres -d riven -t -c "SELECT count(*) FROM \"MediaItem\" WHERE last_state = 'Indexed';") + paused_count=$(docker exec riven-db psql -U postgres -d riven -t -c "SELECT count(*) FROM \"MediaItem\" WHERE last_state = 'Paused';") + unknown_count=$(docker exec riven-db psql -U postgres -d riven -t -c "SELECT count(*) FROM \"MediaItem\" WHERE last_state = 'Unknown';") + failed_count=$(docker exec riven-db psql -U postgres -d riven -t -c "SELECT count(*) FROM \"MediaItem\" WHERE last_state = 'Failed';") + completed_count=$(docker exec riven-db psql -U postgres -d riven -t -c "SELECT count(*) FROM \"MediaItem\" WHERE last_state = 'Completed';") + + echo "Current MediaItem States:" + echo " - Indexed: $indexed_count" + echo " - Paused: $paused_count" + echo " - Unknown: $unknown_count" + echo " - Failed: $failed_count" + echo " - Completed: $completed_count" +} + +# Main script execution +echo "Starting the reset process for MediaItem states..." + +# Step 1: Check if the Docker container is running +check_container_status + +# Step 2: Ask the user if they want to create a backup +backup_database + +# Step 3: Ask the user which states they want to reset (can select multiple) +ask_reset_states + +# Step 4: Show the current counts after the reset +show_current_counts + +echo "Script completed successfully." diff --git a/db-tools/readme.md b/db-tools/readme.md new file mode 100644 index 0000000..8f75262 --- /dev/null +++ b/db-tools/readme.md @@ -0,0 +1,196 @@ +# 🗄️ Riven Database Maintenance Tool (PostgreSQL) + +This tool provides **safe, interactive maintenance and recovery operations** for the **Riven PostgreSQL database** used in Docker-based Riven deployments. + +It is designed to be: +- ✅ Production-safe +- 🧭 Beginner-friendly +- 🔁 Repeatable +- 🛡️ Confirmation-guarded for destructive actions + +Works on: +- Ubuntu installs +- Proxmox LXC (unprivileged) +- Any Docker-based Riven setup + +--- + +## 📚 Table of Contents + +- [Run the Database Maintenance Tool](#run-the-database-maintenance-tool) +- [What This Tool Does](#what-this-tool-does) +- [Available Operations](#available-operations) + - [Backup Database](#backup-database) + - [Vacuum & Analyze](#vacuum--analyze) + - [Clean Stale / Orphaned Data](#clean-stale--orphaned-data) + - [Reset Database (Destructive)](#reset-database-destructive) +- [Recommended Usage](#recommended-usage) +- [Restart Services After Maintenance](#restart-services-after-maintenance) +- [Backup Files](#backup-files) +- [Safety Guarantees](#safety-guarantees) +- [When NOT to Use This Tool](#when-not-to-use-this-tool) +- [Summary](#summary) + +--- + + +## ▶️ Run the Database Maintenance Tool + +Run this **directly on the system where Riven is installed**: + + sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/Riven-Scripts/main/db-tools/riven-db-maintenance.sh | sed 's/\r$//')" + +This command automatically fixes Windows CRLF line-ending issues before execution. + +--- + + +## 🧠 What This Tool Does + +This script provides an **interactive menu** for managing the **Riven PostgreSQL database container**. + +It automatically: +- Detects the Riven database container +- Verifies PostgreSQL connectivity +- Prevents unsafe or accidental operations +- Prompts before any destructive action + +--- + + +## 🛠 Available Operations + + +### 🗂️ Backup Database + +Creates a timestamped PostgreSQL dump: +- Safe to run at any time +- Stored locally on the host +- Strongly recommended before any cleanup or reset + +--- + + +### 🧹 Vacuum & Analyze + +Performs standard PostgreSQL maintenance: +- Reclaims unused space +- Improves query performance +- Updates planner statistics + +Safe for routine maintenance. + +--- + + +### 🧽 Clean Stale / Orphaned Data + +Removes broken or unused records caused by: +- Failed scrapes +- Interrupted downloads +- Partial imports + +⚠️ Requires confirmation before execution. + +--- + + +### 🔄 Reset Database (Destructive) + +🚨 **THIS WILL DELETE ALL RIVEN DATABASE DATA** 🚨 + +Includes: +- Scrape history +- Index state +- Download records +- Cached metadata + +Use only if: +- The database is corrupted +- Riven cannot recover normally +- You plan to rescrape everything + +A backup is **strongly recommended first**. + +--- + + +## 🔁 Recommended Usage + +### Routine maintenance +1. Backup +2. Vacuum & Analyze +3. Exit + +### If Riven behaves incorrectly +1. Backup +2. Clean stale/orphaned data +3. Restart Riven + +### Last-resort recovery +1. Backup +2. Reset database +3. Restart Riven +4. Reconfigure and rescrape + +--- + + +## 🔄 Restart Services After Maintenance + +After completing any operation: + + docker restart riven + +If you use a media server, restart only what applies: + + docker restart jellyfin + docker restart plex + docker restart emby + +--- + + +## 📂 Backup Files + +Backups are created with timestamps: + + riven-db-backup-YYYY-MM-DD_HH-MM-SS.sql + +Store these somewhere safe before performing destructive actions. + +--- + + +## 🛡️ Safety Guarantees + +This tool: +- ❌ Does NOT modify `.env` +- ❌ Does NOT touch media files +- ❌ Does NOT modify mounts +- ❌ Does NOT uninstall containers +- ✅ Uses confirmations for destructive actions +- ✅ Is safe to run multiple times + +--- + + +## 🚫 When NOT to Use This Tool + +Do **not** use this script to: +- Install Riven +- Update containers +- Fix mount issues +- Replace the installer or updater + +Use the appropriate **installer**, **updater**, or **remount-cycle script** instead. + +--- + + +## ✅ Summary + +- Interactive PostgreSQL maintenance for Riven +- Backup-first, safety-focused workflow +- Handles common performance and corruption issues +- Works across Ubuntu and Proxmox LXC deployments diff --git a/db-tools/riven-db-maintenance.sh b/db-tools/riven-db-maintenance.sh new file mode 100644 index 0000000..f8294aa --- /dev/null +++ b/db-tools/riven-db-maintenance.sh @@ -0,0 +1,520 @@ +#!/bin/bash + +# ============================================================ +# Riven Show Cleanup & Management Script (Enhanced Version) +# ============================================================ +# This script helps you manage show states in the Riven database +# including deleting episodes, marking as unreleased, and +# updating states for episodes, seasons, and shows. +# ============================================================ + +# Function to check if the Docker container is running +check_container_status() { + docker ps | grep -q "riven-db" + if [ $? -ne 0 ]; then + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ ERROR: The 'riven-db' container is not running! ║" + echo "║ Please start the container and try again. ║" + echo "╚════════════════════════════════════════════════════════════╝" + exit 1 + fi +} + +# Function to create a backup of the database +backup_database() { + echo "" + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ DATABASE BACKUP ║" + echo "╚════════════════════════════════════════════════════════════╝" + echo "" + echo "RECOMMENDED: Creating a backup allows you to restore your" + echo "database if something goes wrong. The backup will be stored" + echo "at '/tmp/riven_backup.sql' on your host machine." + echo "" + read -p "Do you want to create a backup? (y/n): " backup_choice + if [[ "$backup_choice" =~ ^[Yy]$ ]]; then + echo "" + echo "Creating backup of the database..." + # Backup inside the container at /tmp/riven_backup.sql + docker exec riven-db pg_dump -U postgres -d riven -f /tmp/riven_backup.sql + if [ $? -eq 0 ]; then + echo "✓ Backup successful inside the container at /tmp/riven_backup.sql." + # Copy the backup file from the container to the host + docker cp riven-db:/tmp/riven_backup.sql /tmp/riven_backup.sql + if [ $? -eq 0 ]; then + echo "✓ Backup file successfully copied to the host at /tmp/riven_backup.sql." + echo "" + else + echo "✗ Error: Failed to copy the backup file from the container to the host." + echo "Exiting script for safety." + exit 1 + fi + else + echo "✗ Error: Backup failed inside the container." + echo "Exiting script for safety." + exit 1 + fi + else + echo "⚠ Skipping backup. Proceeding without backup protection." + echo "" + fi +} + +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ Riven Show Cleanup & Management Script ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" + +# Step 0: Check if the Docker container is running +check_container_status + +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ FIND YOUR SHOW ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "Search for the show by:" +echo " 1) TVDB ID (exact match - fastest if you know the ID)" +echo " 2) Show Name (partial match - searches titles containing your text)" +echo "" +read -p "Enter choice (1 or 2): " search_choice + +if [ "$search_choice" == "1" ]; then + read -p "Enter TVDB ID: " tvdb_id + search_condition="mi_show.tvdb_id = '$tvdb_id'" +elif [ "$search_choice" == "2" ]; then + read -p "Enter show name (partial match works): " show_name + search_condition="mi_show.title ILIKE '%$show_name%'" +else + echo "✗ Invalid choice. Exiting." + exit 1 +fi + +# Find the parent show(s) +result=$(docker exec riven-db psql -U postgres -d riven -t -A -F',' -c " +SELECT mi_show.id, mi_show.title +FROM \"MediaItem\" mi_show +WHERE mi_show.type = 'show' +AND $search_condition;") + +show_count=$(echo "$result" | grep -v '^$' | wc -l) + +if [ "$show_count" -eq 0 ]; then + echo "✗ No shows found. Exiting." + exit 0 +elif [ "$show_count" -gt 1 ]; then + echo "" + echo "Multiple shows found. Please choose the correct one:" + echo "──────────────────────────────────────────────────" + echo "$result" | while IFS=',' read -r id title; do + echo " ID: $id | Title: $title" + done + echo "" + read -p "Enter the ID of the show you want to manage: " show_id + show_title=$(docker exec riven-db psql -U postgres -d riven -t -c "SELECT title FROM \"MediaItem\" WHERE id = $show_id;" | xargs) +else + show_id=$(echo "$result" | cut -d',' -f1) + show_title=$(echo "$result" | cut -d',' -f2) + echo "" + echo "✓ Found: $show_title (ID: $show_id)" + echo "" + read -p "Proceed with this show? (y/n): " confirm + [[ ! "$confirm" =~ ^[Yy](es)?$ ]] && exit 0 +fi + +final_condition="parent.id = $show_id" + +# Create backup before making any changes +backup_database + +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ CURRENT STATE SUMMARY ║" +echo "║ Show: $show_title" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +docker exec riven-db psql -U postgres -d riven -c " +SELECT mi.last_state AS state, COUNT(*) AS count +FROM \"MediaItem\" mi +INNER JOIN \"Episode\" e ON mi.id = e.id +INNER JOIN \"Season\" s ON e.parent_id = s.id +INNER JOIN \"Show\" sh ON s.parent_id = sh.id +INNER JOIN \"MediaItem\" parent ON sh.id = parent.id +WHERE $final_condition +GROUP BY mi.last_state;" + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ STEP 1: DELETE EPISODES ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "⚠ WHAT THIS DOES:" +echo " • Permanently REMOVES episodes from the database" +echo " • Riven will FORGET about these episodes and STOP trying to find them" +echo " • Deleted episodes will NOT appear in your library or search results" +echo " • To restore deleted episodes, you must do a FULL SHOW RESET/RE-ADD" +echo "" +echo "WHEN TO USE THIS:" +echo " • Remove episodes that don't exist (specials, bonus content, etc.)" +echo " • Clean up incorrectly indexed episodes" +echo " • Remove episodes you never want Riven to search for" +echo "" +echo "Available states you might want to delete:" +echo " • Indexed - Episodes that were found but not yet scraped" +echo " • Unknown - Episodes that Riven couldn't find streams for" +echo " • Failed - Episodes that had errors during processing" +echo " • Scraped - Episodes that have been scraped but not downloaded" +echo "" +read -p "Enter state name to DELETE those episodes (or press Enter to skip): " raw_target_state + +# Format input to Title Case (e.g., indexed -> Indexed) +target_state=$(echo "${raw_target_state,,}" | sed 's/./\u&/') + +deleted_count=0 +affected_seasons="" + +if [ ! -z "$target_state" ]; then + echo "" + echo "⚠ WARNING: You are about to DELETE all episodes with state: $target_state" + read -p "Are you absolutely sure? Type 'DELETE' to confirm: " delete_confirm + + if [ "$delete_confirm" == "DELETE" ]; then + affected_seasons=$(docker exec riven-db psql -U postgres -d riven -t -c " + SELECT DISTINCT s.number + FROM \"MediaItem\" mi + INNER JOIN \"Episode\" e ON mi.id = e.id + INNER JOIN \"Season\" s ON e.parent_id = s.id + INNER JOIN \"Show\" sh ON s.parent_id = sh.id + INNER JOIN \"MediaItem\" parent ON sh.id = parent.id + WHERE $final_condition AND mi.last_state = '$target_state' + ORDER BY s.number;" | xargs | sed 's/ /,/g') + + if [ ! -z "$affected_seasons" ]; then + docker exec riven-db psql -U postgres -d riven -c "DELETE FROM \"MediaItem\" WHERE id IN (SELECT mi.id FROM \"MediaItem\" mi INNER JOIN \"Episode\" e ON mi.id = e.id INNER JOIN \"Season\" s ON e.parent_id = s.id INNER JOIN \"Show\" sh ON s.parent_id = sh.id INNER JOIN \"MediaItem\" parent ON sh.id = parent.id WHERE $final_condition AND mi.last_state = '$target_state');" + echo "✓ Episodes deleted in Season(s): $affected_seasons" + deleted_count=1 + else + echo "ℹ No episodes found with state '$target_state'" + fi + else + echo "⚠ Deletion cancelled. No episodes were deleted." + fi +else + echo "ℹ Skipping deletion step." +fi + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ STEP 2: MARK EPISODES AS UNRELEASED ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "ℹ WHAT THIS DOES:" +echo " • Changes episode state to 'Unreleased'" +echo " • Tells Riven these episodes are not yet available/aired" +echo " • Riven will SKIP these episodes during scraping" +echo " • Episodes remain in the database (not deleted)" +echo "" +echo "WHEN TO USE THIS:" +echo " • Mark future episodes that haven't aired yet" +echo " • Temporarily disable specific episodes from being searched" +echo " • Useful for shows with irregular release schedules" +echo "" +read -p "Do you want to mark specific episodes as 'Unreleased'? (y/n): " do_unreleased + +if [[ "$do_unreleased" =~ ^[Yy](es)?$ ]]; then + read -p "Enter Season Number: " target_season + echo "" + echo "Enter Episode Number(s):" + echo " • For specific episodes: 1,2,3" + echo " • For ALL episodes in the season: type 'A' or 'All'" + echo "" + read -p "Episode Number(s): " target_eps + + # Handle 'A', 'a', or 'All' + if [[ "${target_eps,,}" =~ ^a(ll)?$ ]]; then + ep_condition="s.number = $target_season" + echo "" + echo "Marking ALL episodes in Season $target_season as Unreleased..." + else + ep_condition="s.number = $target_season AND e.number IN ($target_eps)" + echo "" + echo "Marking episodes $target_eps in Season $target_season as Unreleased..." + fi + + docker exec riven-db psql -U postgres -d riven -c " + UPDATE \"MediaItem\" SET last_state = 'Unreleased' + WHERE id IN ( + SELECT mi.id FROM \"MediaItem\" mi + INNER JOIN \"Episode\" e ON mi.id = e.id + INNER JOIN \"Season\" s ON e.parent_id = s.id + WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) + AND $ep_condition + );" + echo "✓ Episodes set to Unreleased." +else + echo "ℹ Skipping unreleased marking step." +fi + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ STEP 3: RESET SEASON(S) TO INDEXED ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "ℹ WHAT THIS DOES:" +echo " • Resets ALL episodes in selected season(s) to 'Indexed' state" +echo " • Also resets the season itself to 'Indexed'" +echo " • Clears scraping history (scraped_at, scraped_times, active_stream)" +echo " • Tells Riven to start fresh and re-scrape these episodes" +echo "" +echo "WHEN TO USE THIS:" +echo " • Force Riven to re-search for better quality streams" +echo " • Reset after changing scraper settings" +echo " • Clear 'Unknown' or 'Failed' states and try again" +echo " • Start over with specific seasons" +echo "" +read -p "Do you want to reset entire season(s) to 'Indexed'? (y/n): " do_reset + +if [[ "$do_reset" =~ ^[Yy](es)?$ ]]; then + echo "" + echo "Enter Season Number(s):" + echo " • For specific seasons: 1,2,3" + echo " • For ALL seasons: type 'A' or 'All'" + echo "" + read -p "Season Number(s): " reset_seasons + + # Handle 'A', 'a', or 'All' + if [[ "${reset_seasons,,}" =~ ^a(ll)?$ ]]; then + echo "" + echo "Resetting ALL episodes and seasons to 'Indexed'..." + + # Reset all episodes to Indexed + docker exec riven-db psql -U postgres -d riven -c " + UPDATE \"MediaItem\" + SET last_state = 'Indexed', + scraped_at = NULL, + scraped_times = 0, + active_stream = NULL + WHERE id IN ( + SELECT mi.id FROM \"MediaItem\" mi + INNER JOIN \"Episode\" e ON mi.id = e.id + INNER JOIN \"Season\" s ON e.parent_id = s.id + WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) + );" + + # Reset all seasons to Indexed + docker exec riven-db psql -U postgres -d riven -c " + UPDATE \"MediaItem\" + SET last_state = 'Indexed', + scraped_at = NULL, + scraped_times = 0, + active_stream = NULL + WHERE type = 'season' AND id IN ( + SELECT s.id FROM \"Season\" s + WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) + );" + + echo "✓ All episodes and seasons reset to 'Indexed'." + else + echo "" + echo "Resetting Season(s) $reset_seasons to 'Indexed'..." + + # Reset episodes in specified seasons to Indexed + docker exec riven-db psql -U postgres -d riven -c " + UPDATE \"MediaItem\" + SET last_state = 'Indexed', + scraped_at = NULL, + scraped_times = 0, + active_stream = NULL + WHERE id IN ( + SELECT mi.id FROM \"MediaItem\" mi + INNER JOIN \"Episode\" e ON mi.id = e.id + INNER JOIN \"Season\" s ON e.parent_id = s.id + WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) + AND s.number IN ($reset_seasons) + );" + + # Reset specified seasons to Indexed + docker exec riven-db psql -U postgres -d riven -c " + UPDATE \"MediaItem\" + SET last_state = 'Indexed', + scraped_at = NULL, + scraped_times = 0, + active_stream = NULL + WHERE type = 'season' AND id IN ( + SELECT s.id FROM \"Season\" s + WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) + AND s.number IN ($reset_seasons) + );" + + echo "✓ Season(s) $reset_seasons and their episodes reset to 'Indexed'." + fi +else + echo "ℹ Skipping season reset step." +fi + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ STEP 4: UPDATE SEASON STATES ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "ℹ WHAT THIS DOES:" +echo " • Sets the state for entire season(s)" +echo " • Affects how Riven treats the season during updates" +echo "" +echo "STATE MEANINGS:" +echo " • Completed: All episodes are done, stop checking for new ones" +echo " • Ongoing: Season is airing, keep checking for new episodes" +echo " • Unreleased: Season hasn't aired yet, skip for now" +echo " • PartiallyCompleted: Some episodes done, still working on others" +echo " • Indexed: Ready to be scraped/processed" +echo " • Scraped: Has been scraped, ready for download" +echo " • Paused: Temporarily stop processing this season" +echo "" + +if [ $deleted_count -gt 0 ]; then + echo "You deleted episodes in Season(s): $affected_seasons" + echo "Choose a state for these affected seasons:" + echo "" + echo " 1) Completed" + echo " 2) Ongoing" + echo " 3) Unreleased" + echo " 4) PartiallyCompleted" + echo " 5) Indexed" + echo " 6) Scraped" + echo " 7) Paused" + echo "" + read -p "Choice (1-7): " del_ms_choice + case $del_ms_choice in + 1) ms_state="Completed" ;; + 2) ms_state="Ongoing" ;; + 3) ms_state="Unreleased" ;; + 4) ms_state="PartiallyCompleted" ;; + 5) ms_state="Indexed" ;; + 6) ms_state="Scraped" ;; + 7) ms_state="Paused" ;; + esac + if [ ! -z "$ms_state" ]; then + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = '$ms_state' WHERE type = 'season' AND id IN (SELECT s.id FROM \"Season\" s WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) AND s.number IN ($affected_seasons));" + echo "✓ Seasons $affected_seasons updated to $ms_state." + fi +else + read -p "Update states for any seasons? Enter season numbers (e.g. 1,2) or press Enter to skip: " manual_seasons + if [ ! -z "$manual_seasons" ]; then + echo "" + echo "Choose state for Season(s) $manual_seasons:" + echo "" + echo " 1) Completed" + echo " 2) Ongoing" + echo " 3) Unreleased" + echo " 4) PartiallyCompleted" + echo " 5) Indexed" + echo " 6) Scraped" + echo " 7) Paused" + echo "" + read -p "Choice (1-7): " ms_choice + case $ms_choice in + 1) ms_state="Completed" ;; + 2) ms_state="Ongoing" ;; + 3) ms_state="Unreleased" ;; + 4) ms_state="PartiallyCompleted" ;; + 5) ms_state="Indexed" ;; + 6) ms_state="Scraped" ;; + 7) ms_state="Paused" ;; + esac + if [ ! -z "$ms_state" ]; then + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = '$ms_state' WHERE type = 'season' AND id IN (SELECT s.id FROM \"Season\" s WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) AND s.number IN ($manual_seasons));" + echo "✓ Seasons $manual_seasons updated to $ms_state." + fi + else + echo "ℹ Skipping manual season state update." + fi +fi + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ STEP 5: UPDATE SHOW STATE ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "ℹ WHAT THIS DOES:" +echo " • Sets the state for THE ENTIRE SHOW" +echo " • Affects how Riven monitors and updates this show" +echo "" +echo "STATE MEANINGS:" +echo " • Completed: Show is finished, no new episodes expected" +echo " • Ongoing: Show is actively airing, check for new episodes" +echo " • Indexed: Show is ready to be processed/scraped" +echo " • Unreleased: Show hasn't premiered yet" +echo " • PartiallyCompleted: Some seasons done, others ongoing" +echo "" +echo "Choose state for THE ENTIRE SHOW: $show_title" +echo "" +echo " 1) Completed" +echo " 2) Ongoing" +echo " 3) Indexed" +echo " 4) Unreleased" +echo " 5) PartiallyCompleted" +echo "" +read -p "Choice (1-5 or press Enter to skip): " show_choice + +case $show_choice in + 1) + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = 'Completed' WHERE id = $show_id;" + echo "✓ Show set to 'Completed'." + ;; + 2) + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = 'Ongoing' WHERE id = $show_id;" + echo "✓ Show set to 'Ongoing'." + ;; + 3) + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = 'Indexed' WHERE id = $show_id;" + echo "✓ Show set to 'Indexed'." + ;; + 4) + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = 'Unreleased' WHERE id = $show_id;" + echo "✓ Show set to 'Unreleased'." + ;; + 5) + docker exec riven-db psql -U postgres -d riven -c "UPDATE \"MediaItem\" SET last_state = 'PartiallyCompleted' WHERE id = $show_id;" + echo "✓ Show set to 'PartiallyCompleted'." + ;; + *) + echo "ℹ Skipping show state update." + ;; +esac + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ FINAL VERIFICATION ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +echo "Show State:" +echo "───────────" +docker exec riven-db psql -U postgres -d riven -c "SELECT title, last_state FROM \"MediaItem\" WHERE id = $show_id;" + +echo "" +echo "Season States:" +echo "──────────────" +docker exec riven-db psql -U postgres -d riven -c "SELECT s.number as season, mi.last_state FROM \"Season\" s INNER JOIN \"MediaItem\" mi ON s.id = mi.id WHERE s.parent_id = (SELECT id FROM \"Show\" WHERE id = $show_id) ORDER BY s.number;" + +echo "" +echo "Episode State Summary:" +echo "──────────────────────" +docker exec riven-db psql -U postgres -d riven -c " +SELECT mi.last_state AS state, COUNT(*) AS count +FROM \"MediaItem\" mi +INNER JOIN \"Episode\" e ON mi.id = e.id +INNER JOIN \"Season\" s ON e.parent_id = s.id +INNER JOIN \"Show\" sh ON s.parent_id = sh.id +INNER JOIN \"MediaItem\" parent ON sh.id = parent.id +WHERE $final_condition +GROUP BY mi.last_state +ORDER BY mi.last_state;" + +echo "" +echo "╔════════════════════════════════════════════════════════════╗" +echo "║ SCRIPT COMPLETED! ║" +echo "╚════════════════════════════════════════════════════════════╝" +echo "" +if [[ "$backup_choice" =~ ^[Yy]$ ]]; then + echo "ℹ Your database backup is located at: /tmp/riven_backup.sql" + echo "" +fi +echo "Done." \ No newline at end of file diff --git a/proxmox/README.md b/proxmox/README.md index 8bdab24..58bb94f 100644 --- a/proxmox/README.md +++ b/proxmox/README.md @@ -1,136 +1,129 @@ -# Proxmox LXC Helper Script for Riven +# 🧊 Riven Proxmox LXC Installer (Docker-based) -This repository contains a Proxmox helper script that creates a Debian 12, unprivileged -LXC container running the Riven backend and frontend on bare metal (no Docker). +This installer deploys **Riven** inside an **unprivileged Debian 12 LXC** on Proxmox, fully containerized with Docker. +It is designed to be **safe, repeatable, and beginner-proof**, while still supporting advanced features like GPU passthrough and multiple media servers. --- -## Requirements +## ▶️ Run on Proxmox Host -- Proxmox VE **8.1 or later** (including 9.x) -- Internet connectivity from the Proxmox host and the LXC template mirrors -- A storage pool that can host LXC containers +Run this **directly on the Proxmox host shell**: -The helper will create an **unprivileged** container (CT_TYPE=1) with sensible defaults: - -- OS: Debian 12 -- CPU: 4 vCPU -- RAM: 8 GB -- Disk: 40 GB +```bash +sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/Riven-Scripts/main/proxmox/install.sh)" +``` -You can override these values via the script's **Advanced Settings** dialog. +The installer is fully interactive and will guide you through all required selections. --- -## Creating the Riven LXC - -Run this from a **Proxmox VE host shell**: - -```bash -bash -c "$(wget -qLO - https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/riven.sh)" -``` - -The script will: +## 🛠 What This Installer Does + +### LXC Creation & System Setup +- Creates an **unprivileged Debian 12 LXC** +- Enables required container features: + - nesting + - keyctl + - fuse +- Passes `/dev/fuse` into the container (required for Riven VFS) +- Optionally passes `/dev/dri` for **GPU acceleration** + +### Docker Environment +- Installs **Docker Engine** +- Installs **Docker Compose plugin** +- Applies sane defaults for Docker-in-LXC operation + +### Riven Deployment +- Deploys: + - Riven backend + - Riven frontend + - PostgreSQL database +- Uses a unified filesystem layout under `/srv/riven` + +### Media Server Support (Optional) +Media servers are included via Docker Compose **profiles**: +- Jellyfin +- Plex +- Emby + +You can enable one or more at any time. -- Validate your Proxmox version (8.1+) -- Create a new **unprivileged** Debian 12 LXC -- Enable FUSE and mount `/dev/fuse` inside the container -- Install and configure PostgreSQL inside the LXC -- Install the Riven backend (Python/uv) and frontend (Node/pnpm) bare metal -- Create systemd services for both backend and frontend so they start on boot +--- -After the script completes, you should be able to reach: +## 🌐 Access URLs -- Riven backend at: `http://:8080` -- Riven frontend at: `http://:3000` +Once the container is running: -`` is the IP address assigned to the LXC (shown in the script output and in `pct list`). +- **Riven Backend:** + `http://:8080` ---- +- **Riven Frontend:** + `http://:3000` -## What the installer sets up - -Inside the Riven LXC, the installer configures: - -- **Directories** - - `/riven` – Riven backend checkout & virtualenv - - `/riven/data` – data directory (used by the frontend's SQLite DB by default) - - `/mount` – FUSE mountpoint for the Riven virtual filesystem (VFS) - - `/opt/riven-frontend` – Riven frontend app - - `/etc/riven` – configuration directory - -- **Database** - - PostgreSQL with database `riven` - - `postgres` user password set to `postgres` (local-only, inside the CT) - -- **Environment files** - - Backend: `/etc/riven/backend.env` - - `RIVEN_API_KEY` – randomly generated hex key used by the backend - - `RIVEN_DATABASE_HOST=postgresql+psycopg2://postgres:postgres@127.0.0.1/riven` - - `RIVEN_FILESYSTEM_MOUNT_PATH=/mount` - - `RIVEN_LIBRARY_PATH=/mnt/riven` (path the media servers will see) - - `RIVEN_FILESYSTEM_CACHE_DIR=/dev/shm/riven-cache` - - Frontend: `/etc/riven/frontend.env` - - `DATABASE_URL=/riven/data/riven.db` (SQLite) - - `BACKEND_URL=http://127.0.0.1:8080` - - `BACKEND_API_KEY=$RIVEN_API_KEY` (same value as backend) - - `AUTH_SECRET` – randomly generated, used by the frontend for auth - - `ORIGIN=http://localhost:3000` - -- **Systemd services** (inside the CT) - - `riven-backend.service` - - `riven-frontend.service` - -Both services are enabled and will start automatically when the LXC boots. +### Get the Container IP +From the Proxmox host: +```bash +pct exec -- hostname -I +``` --- -## Checking status and logs +## ⚠️ Required Configuration (IMPORTANT) -Assuming your Riven container ID is `106`. +🚨 **Riven will NOT function without a configured media server** 🚨 -### Enter the container +You **must** edit the Riven configuration before first use. +### Edit configuration inside the container ```bash -pct enter 106 +/srv/riven/backend/settings.json ``` -### Check service status +Configure **at least one** media server (Jellyfin, Plex, or Emby). +### Restart Riven after editing ```bash -systemctl status riven-backend -systemctl status riven-frontend +pct exec -- docker restart riven ``` -### View live logs +--- -```bash -journalctl -u riven-backend -f -journalctl -u riven-frontend -f -``` +## 🎬 Optional Media Servers -You can also run these directly from the Proxmox host without entering the CT: +To enable a media server, run **inside the container**: ```bash -lxc-attach -n 106 -- journalctl -u riven-backend -f -lxc-attach -n 106 -- journalctl -u riven-frontend -f -``` +cd /srv/riven/app ---- +docker compose --profile jellyfin up -d +docker compose --profile plex up -d +docker compose --profile emby up -d +``` -## Customizing configuration +You may enable **only one** or **multiple**, depending on your setup. -You can edit the environment files inside the Riven CT to customize settings: +--- -- `/etc/riven/backend.env` -- `/etc/riven/frontend.env` +## 🔄 Upgrade Riven -After making changes, restart the services: +To update Riven and its containers, run **inside the container**: ```bash -systemctl restart riven-backend riven-frontend +/srv/riven/app/upgrade.sh ``` -For advanced configuration (content providers, scrapers, ranking, etc.), -refer to the upstream Riven documentation and `.env.example` file in the -Riven repository. +This safely: +- Stops containers +- Pulls updates +- Restarts services in the correct order + +--- + +## ✅ Summary + +- Fully automated Proxmox LXC deployment +- Unprivileged, secure-by-default container +- Docker-based, easy to upgrade +- Optional GPU support +- Optional Jellyfin / Plex / Emby integration +- Unified filesystem layout for easy backups diff --git a/proxmox/build.func b/proxmox/build.func deleted file mode 100644 index 99e1080..0000000 --- a/proxmox/build.func +++ /dev/null @@ -1,734 +0,0 @@ -variables() { - NSAPP=$(echo ${APP,,} | tr -d ' ') # This function sets the NSAPP variable by converting the value of the APP variable to lowercase and removing any spaces. - var_install="${NSAPP}-install" # sets the var_install variable by appending "-install" to the value of NSAPP. - INTEGER='^[0-9]+([.][0-9]+)?$' # it defines the INTEGER regular expression pattern. -} - -# This function sets various color variables using ANSI escape codes for formatting text in the terminal. -color() { - YW=$(echo "\033[33m") - BL=$(echo "\033[36m") - RD=$(echo "\033[01;31m") - BGN=$(echo "\033[4;92m") - GN=$(echo "\033[1;92m") - DGN=$(echo "\033[32m") - CL=$(echo "\033[m") - CM="${GN}✓${CL}" - CROSS="${RD}✗${CL}" - BFR="\\r\\033[K" - HOLD=" " -} - -# This function enables error handling in the script by setting options and defining a trap for the ERR signal. -catch_errors() { - set -Eeuo pipefail - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -} - -# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message. -error_handler() { - if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi - printf "\e[?25h" - local exit_code="$?" - local line_number="$1" - local command="$2" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" -} - -# This function displays a spinner. -spinner() { - local chars="/-\|" - local spin_i=0 - printf "\e[?25l" - while true; do - printf "\r \e[36m%s\e[0m" "${chars:spin_i++%${#chars}:1}" - sleep 0.1 - done -} - -# This function displays an informational message with a yellow color. -msg_info() { - local msg="$1" - echo -ne " ${HOLD} ${YW}${msg} " - spinner & - SPINNER_PID=$! -} - -# This function displays a success message with a green color. -msg_ok() { - if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi - printf "\e[?25h" - local msg="$1" - echo -e "${BFR} ${CM} ${GN}${msg}${CL}" -} - -# This function displays a error message with a red color. -msg_error() { - if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi - printf "\e[?25h" - local msg="$1" - echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" -} - -# Check if the shell is using bash -shell_check() { - if [[ "$(basename "$SHELL")" != "bash" ]]; then - clear - msg_error "Your default shell is currently not set to Bash. To use these scripts, please switch to the Bash shell." - echo -e "\nExiting..." - sleep 2 - exit - fi -} - -# Run as root only -root_check() { - if [[ "$(id -u)" -ne 0 || $(ps -o comm= -p $PPID) == "sudo" ]]; then - clear - msg_error "Please run this script as root." - echo -e "\nExiting..." - sleep 2 - exit - fi -} - -# This function checks the version of Proxmox Virtual Environment (PVE) and exits if the version is not supported. -pve_check() { - # Extract the numeric pve-manager version, e.g. 8.1.3 or 9.0.10 - local PVE_VER - PVE_VER=$(pveversion | awk -F/ '/pve-manager/ {print $2}' | cut -d- -f1) - # Require 8.1 or later (supports future 9.x, 10.x, ...) - if dpkg --compare-versions "$PVE_VER" lt "8.1"; then - msg_error "This version of Proxmox Virtual Environment is not supported" - echo -e "Detected PVE version: $PVE_VER" - echo -e "Requires Proxmox Virtual Environment Version 8.1 or later." - echo -e "Exiting..." - sleep 2 - exit - fi -} - -# This function checks the system architecture and exits if it's not "amd64". -arch_check() { - if [ "$(dpkg --print-architecture)" != "amd64" ]; then - echo -e "\n ${CROSS} This script will not work with PiMox! \n" - echo -e "\n Visit https://github.com/asylumexp/Proxmox for ARM64 support. \n" - echo -e "Exiting..." - sleep 2 - exit - fi -} - -# This function checks if the script is running through SSH and prompts the user to confirm if they want to proceed or exit. -ssh_check() { - if command -v pveversion >/dev/null 2>&1 && [ -n "${SSH_CLIENT:+x}" ]; then - if whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH DETECTED" --yesno "It's advisable to utilize the Proxmox shell rather than SSH, as there may be potential complications with variable retrieval. Proceed using SSH?" 10 72; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Proceed using SSH" "You've chosen to proceed using SSH. If any issues arise, please run the script in the Proxmox shell before creating a repository issue." 10 72 - else - clear - echo "Exiting due to SSH usage. Please consider using the Proxmox shell." - exit - fi - fi -} - -# This function displays the default values for various settings. -echo_default() { - echo -e "${DGN}Using Distribution: ${BGN}$var_os${CL}" - echo -e "${DGN}Using $var_os Version: ${BGN}$var_version${CL}" - echo -e "${DGN}Using Container Type: ${BGN}$CT_TYPE${CL}" - echo -e "${DGN}Using Root Password: ${BGN}Automatic Login${CL}" - echo -e "${DGN}Using Container ID: ${BGN}$NEXTID${CL}" - echo -e "${DGN}Using Hostname: ${BGN}$NSAPP${CL}" - echo -e "${DGN}Using Disk Size: ${BGN}$var_disk${CL}${DGN}GB${CL}" - echo -e "${DGN}Allocated Cores ${BGN}$var_cpu${CL}" - echo -e "${DGN}Allocated Ram ${BGN}$var_ram${CL}" - echo -e "${DGN}Using Bridge: ${BGN}vmbr0${CL}" - echo -e "${DGN}Using Static IP Address: ${BGN}dhcp${CL}" - echo -e "${DGN}Using Gateway IP Address: ${BGN}Default${CL}" - echo -e "${DGN}Using Apt-Cacher IP Address: ${BGN}Default${CL}" - echo -e "${DGN}Disable IPv6: ${BGN}No${CL}" - echo -e "${DGN}Using Interface MTU Size: ${BGN}Default${CL}" - echo -e "${DGN}Using DNS Search Domain: ${BGN}Host${CL}" - echo -e "${DGN}Using DNS Server Address: ${BGN}Host${CL}" - echo -e "${DGN}Using MAC Address: ${BGN}Default${CL}" - echo -e "${DGN}Using VLAN Tag: ${BGN}Default${CL}" - echo -e "${DGN}Enable Root SSH Access: ${BGN}No${CL}" - echo -e "${DGN}Enable Verbose Mode: ${BGN}No${CL}" - echo -e "${BL}Creating a ${APP} LXC using the above default settings${CL}" -} - -# This function is called when the user decides to exit the script. It clears the screen and displays an exit message. -exit-script() { - clear - echo -e "⚠ User exited script \n" - exit -} - -# This function allows the user to configure advanced settings for the script. -advanced_settings() { - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Here is an instructional tip:" "To make a selection, use the Spacebar." 8 58 - - if [ "${APP:-}" = "Riven" ]; then - # For Riven we fix the distribution to the defaults defined in riven.sh - # (Debian 12) and do not offer an OS/version choice here. - whiptail --backtitle "Proxmox VE Helper Scripts" \ - --msgbox --title "Default distribution for $APP" \ - "${var_os} ${var_version} \n \nThis installer is fixed to this distribution for support." 10 58 - else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox --title "Default distribution for $APP" "${var_os} ${var_version} \n \nIf the default Linux distribution is not adhered to, script support will be discontinued. \n" 10 58 - if [ "$var_os" != "alpine" ]; then - var_os="" - while [ -z "$var_os" ]; do - if var_os=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DISTRIBUTION" --radiolist "Choose Distribution:" 10 58 2 \ - "debian" "" OFF \ - "ubuntu" "" OFF \ - 3>&1 1>&2 2>&3); then - if [ -n "$var_os" ]; then - echo -e "${DGN}Using Distribution: ${BGN}$var_os${CL}" - fi - else - exit-script - fi - done - fi - - if [ "$var_os" == "debian" ]; then - var_version="" - while [ -z "$var_version" ]; do - if var_version=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "DEBIAN VERSION" --radiolist "Choose Version" 10 58 2 \ - "11" "Bullseye" OFF \ - "12" "Bookworm" OFF \ - 3>&1 1>&2 2>&3); then - if [ -n "$var_version" ]; then - echo -e "${DGN}Using $var_os Version: ${BGN}$var_version${CL}" - fi - else - exit-script - fi - done - fi - - if [ "$var_os" == "ubuntu" ]; then - var_version="" - while [ -z "$var_version" ]; do - if var_version=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "UBUNTU VERSION" --radiolist "Choose Version" 10 58 3 \ - "20.04" "Focal" OFF \ - "22.04" "Jammy" OFF \ - "24.04" "Noble" OFF \ - 3>&1 1>&2 2>&3); then - if [ -n "$var_version" ]; then - echo -e "${DGN}Using $var_os Version: ${BGN}$var_version${CL}" - fi - else - exit-script - fi - done - fi - fi - - CT_TYPE="" - while [ -z "$CT_TYPE" ]; do - if CT_TYPE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "CONTAINER TYPE" --radiolist "Choose Type" 10 58 2 \ - "1" "Unprivileged" OFF \ - "0" "Privileged" OFF \ - 3>&1 1>&2 2>&3); then - if [ -n "$CT_TYPE" ]; then - echo -e "${DGN}Using Container Type: ${BGN}$CT_TYPE${CL}" - fi - else - exit-script - fi - done - - while true; do - if PW1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nSet Root Password (needed for root ssh access)" 9 58 --title "PASSWORD (leave blank for automatic login)" 3>&1 1>&2 2>&3); then - if [[ ! -z "$PW1" ]]; then - if [[ "$PW1" == *" "* ]]; then - whiptail --msgbox "Password cannot contain spaces. Please try again." 8 58 - elif [ ${#PW1} -lt 5 ]; then - whiptail --msgbox "Password must be at least 5 characters long. Please try again." 8 58 - else - if PW2=$(whiptail --backtitle "Proxmox VE Helper Scripts" --passwordbox "\nVerify Root Password" 9 58 --title "PASSWORD VERIFICATION" 3>&1 1>&2 2>&3); then - if [[ "$PW1" == "$PW2" ]]; then - PW="-password $PW1" - echo -e "${DGN}Using Root Password: ${BGN}********${CL}" - break - else - whiptail --msgbox "Passwords do not match. Please try again." 8 58 - fi - else - exit-script - fi - fi - else - PW1="Automatic Login" - PW="" - echo -e "${DGN}Using Root Password: ${BGN}$PW1${CL}" - break - fi - else - exit-script - fi - done - - - if CT_ID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Container ID" 8 58 $NEXTID --title "CONTAINER ID" 3>&1 1>&2 2>&3); then - if [ -z "$CT_ID" ]; then - CT_ID="$NEXTID" - echo -e "${DGN}Using Container ID: ${BGN}$CT_ID${CL}" - else - echo -e "${DGN}Container ID: ${BGN}$CT_ID${CL}" - fi - else - exit - fi - - if CT_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Hostname" 8 58 $NSAPP --title "HOSTNAME" 3>&1 1>&2 2>&3); then - if [ -z "$CT_NAME" ]; then - HN="$NSAPP" - else - HN=$(echo ${CT_NAME,,} | tr -d ' ') - fi - echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}" - else - exit-script - fi - - if DISK_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Disk Size in GB" 8 58 $var_disk --title "DISK SIZE" 3>&1 1>&2 2>&3); then - if [ -z "$DISK_SIZE" ]; then - DISK_SIZE="$var_disk" - echo -e "${DGN}Using Disk Size: ${BGN}$DISK_SIZE${CL}" - else - if ! [[ $DISK_SIZE =~ $INTEGER ]]; then - echo -e "${RD}⚠ DISK SIZE MUST BE AN INTEGER NUMBER!${CL}" - advanced_settings - fi - echo -e "${DGN}Using Disk Size: ${BGN}$DISK_SIZE${CL}" - fi - else - exit-script - fi - - if CORE_COUNT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate CPU Cores" 8 58 $var_cpu --title "CORE COUNT" 3>&1 1>&2 2>&3); then - if [ -z "$CORE_COUNT" ]; then - CORE_COUNT="$var_cpu" - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - else - echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}" - fi - else - exit-script - fi - - if RAM_SIZE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Allocate RAM in MiB" 8 58 $var_ram --title "RAM" 3>&1 1>&2 2>&3); then - if [ -z "$RAM_SIZE" ]; then - RAM_SIZE="$var_ram" - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - else - echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}" - fi - else - exit-script - fi - - if BRG=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Bridge" 8 58 vmbr0 --title "BRIDGE" 3>&1 1>&2 2>&3); then - if [ -z "$BRG" ]; then - BRG="vmbr0" - echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" - else - echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}" - fi - else - exit-script - fi - - while true; do - NET=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Static IPv4 CIDR Address (/24)" 8 58 dhcp --title "IP ADDRESS" 3>&1 1>&2 2>&3) - exit_status=$? - if [ $exit_status -eq 0 ]; then - if [ "$NET" = "dhcp" ]; then - echo -e "${DGN}Using IP Address: ${BGN}$NET${CL}" - break - else - if [[ "$NET" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]]; then - echo -e "${DGN}Using IP Address: ${BGN}$NET${CL}" - break - else - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "$NET is an invalid IPv4 CIDR address. Please enter a valid IPv4 CIDR address or 'dhcp'" 8 58 - fi - fi - else - exit-script - fi - done - - if [ "$NET" != "dhcp" ]; then - while true; do - GATE1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Enter gateway IP address" 8 58 --title "Gateway IP" 3>&1 1>&2 2>&3) - if [ -z "$GATE1" ]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Gateway IP address cannot be empty" 8 58 - elif [[ ! "$GATE1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then - whiptail --backtitle "Proxmox VE Helper Scripts" --msgbox "Invalid IP address format" 8 58 - else - GATE=",gw=$GATE1" - echo -e "${DGN}Using Gateway IP Address: ${BGN}$GATE1${CL}" - break - fi - done - else - GATE="" - echo -e "${DGN}Using Gateway IP Address: ${BGN}Default${CL}" - fi - - if [ "$var_os" == "alpine" ]; then - APT_CACHER="" - APT_CACHER_IP="" - else - if APT_CACHER_IP=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set APT-Cacher IP (leave blank for default)" 8 58 --title "APT-Cacher IP" 3>&1 1>&2 2>&3); then - APT_CACHER="${APT_CACHER_IP:+yes}" - echo -e "${DGN}Using APT-Cacher IP Address: ${BGN}${APT_CACHER_IP:-Default}${CL}" - else - exit-script - fi - fi - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "IPv6" --yesno "Disable IPv6?" 10 58); then - DISABLEIP6="yes" - else - DISABLEIP6="no" - fi - echo -e "${DGN}Disable IPv6: ${BGN}$DISABLEIP6${CL}" - - if MTU1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set Interface MTU Size (leave blank for default)" 8 58 --title "MTU SIZE" 3>&1 1>&2 2>&3); then - if [ -z $MTU1 ]; then - MTU1="Default" - MTU="" - else - MTU=",mtu=$MTU1" - fi - echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}" - else - exit-script - fi - - if SD=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Search Domain (leave blank for HOST)" 8 58 --title "DNS Search Domain" 3>&1 1>&2 2>&3); then - if [ -z $SD ]; then - SX=Host - SD="" - else - SX=$SD - SD="-searchdomain=$SD" - fi - echo -e "${DGN}Using DNS Search Domain: ${BGN}$SX${CL}" - else - exit-script - fi - - if NX=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a DNS Server IP (leave blank for HOST)" 8 58 --title "DNS SERVER IP" 3>&1 1>&2 2>&3); then - if [ -z $NX ]; then - NX=Host - NS="" - else - NS="-nameserver=$NX" - fi - echo -e "${DGN}Using DNS Server IP Address: ${BGN}$NX${CL}" - else - exit-script - fi - - if MAC1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a MAC Address(leave blank for default)" 8 58 --title "MAC ADDRESS" 3>&1 1>&2 2>&3); then - if [ -z $MAC1 ]; then - MAC1="Default" - MAC="" - else - MAC=",hwaddr=$MAC1" - echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}" - fi - else - exit-script - fi - - if VLAN1=$(whiptail --backtitle "Proxmox VE Helper Scripts" --inputbox "Set a Vlan(leave blank for default)" 8 58 --title "VLAN" 3>&1 1>&2 2>&3); then - if [ -z $VLAN1 ]; then - VLAN1="Default" - VLAN="" - else - VLAN=",tag=$VLAN1" - fi - echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}" - else - exit-script - fi - - # Ask whether to install and host the Riven frontend in this container. - # If disabled, only the backend API will be installed here and you can - # host the frontend elsewhere. - if (whiptail --backtitle "Proxmox VE Helper Scripts" \ - --title "RIVEN FRONTEND" \ - --yesno "Install and host the Riven frontend in this container?\n\nIf you select No, only the backend API will be installed here. You can host the frontend on another machine or container." \ - 12 78); then - RIVEN_INSTALL_FRONTEND="yes" - else - RIVEN_INSTALL_FRONTEND="no" - fi - echo -e "${DGN}Install frontend in this CT: ${BGN}$RIVEN_INSTALL_FRONTEND${CL}" - - # Optional: allow user to specify the external frontend URL / origin. - # If left blank, the installer will auto-detect and use http://:3000. - if [ "$RIVEN_INSTALL_FRONTEND" != "no" ]; then - if RIVEN_FRONTEND_ORIGIN=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --inputbox "Set the external URL you'll use to access the Riven frontend (ORIGIN).\n\nExamples:\n http://:3000\n https://riven.example.com\n\nLeave blank to auto-detect using the CT IP on port 3000." \ - 15 78 --title "RIVEN FRONTEND ORIGIN" 3>&1 1>&2 2>&3); then - if [ -n "$RIVEN_FRONTEND_ORIGIN" ]; then - echo -e "${DGN}Using Frontend Origin: ${BGN}$RIVEN_FRONTEND_ORIGIN${CL}" - else - echo -e "${DGN}Using Frontend Origin: ${BGN}Auto-detect (CT IP:3000)${CL}" - fi - else - exit-script - fi - else - RIVEN_FRONTEND_ORIGIN="" - fi - - # Ask which optional media servers to install inside this Riven container. - # Default is that no media servers are installed unless explicitly chosen. - if MEDIA_SELECTION=$(whiptail --backtitle "Proxmox VE Helper Scripts" \ - --checklist "Select optional media servers to install INSIDE this Riven container.\n\nIf you leave all options OFF, no media servers will be installed." \ - 20 78 6 \ - "plex" "Plex Media Server" OFF \ - "jellyfin" "Jellyfin media server" OFF \ - "emby" "Emby media server" OFF \ - 3>&1 1>&2 2>&3); then - # Initialize all media selections to "no"; we'll flip to "yes" when chosen. - RIVEN_MEDIA_PLEX="no" - RIVEN_MEDIA_JELLYFIN="no" - RIVEN_MEDIA_EMBY="no" - # whiptail returns tokens like: "plex" "emby"; strip quotes and iterate. - MEDIA_SELECTION=$(echo "$MEDIA_SELECTION" | tr -d '"') - for choice in $MEDIA_SELECTION; do - case "$choice" in - plex) RIVEN_MEDIA_PLEX="yes" ;; - jellyfin) RIVEN_MEDIA_JELLYFIN="yes" ;; - emby) RIVEN_MEDIA_EMBY="yes" ;; - esac - done - echo -e "${DGN}Install Plex in this CT: ${BGN}$RIVEN_MEDIA_PLEX${CL}" - echo -e "${DGN}Install Jellyfin in this CT: ${BGN}$RIVEN_MEDIA_JELLYFIN${CL}" - echo -e "${DGN}Install Emby in this CT: ${BGN}$RIVEN_MEDIA_EMBY${CL}" - else - exit-script - fi - - if [[ "$PW" == -password* ]]; then - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "SSH ACCESS" --yesno "Enable Root SSH Access?" 10 58); then - SSH="yes" - else - SSH="no" - fi - echo -e "${DGN}Enable Root SSH Access: ${BGN}$SSH${CL}" - else - SSH="no" - echo -e "${DGN}Enable Root SSH Access: ${BGN}$SSH${CL}" - fi - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --defaultno --title "VERBOSE MODE" --yesno "Enable Verbose Mode?" 10 58); then - VERB="yes" - else - VERB="no" - fi - echo -e "${DGN}Enable Verbose Mode: ${BGN}$VERB${CL}" - - if (whiptail --backtitle "Proxmox VE Helper Scripts" --title "ADVANCED SETTINGS COMPLETE" --yesno "Ready to create ${APP} LXC?" 10 58); then - echo -e "${RD}Creating a ${APP} LXC using the above advanced settings${CL}" - else - clear - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings - fi -} - -install_script() { - pve_check - shell_check - root_check - arch_check - ssh_check - - if systemctl is-active -q ping-instances.service; then - systemctl -q stop ping-instances.service - fi - NEXTID=$(pvesh get /cluster/nextid) - timezone=$(cat /etc/timezone) - header_info - echo -e "${RD}Using Advanced Settings${CL}" - advanced_settings -} - -start() { - if command -v pveversion >/dev/null 2>&1; then - if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC" --yesno "This will create a new ${APP} LXC. Proceed?" 10 58); then - clear - echo -e "⚠ User exited script \n" - exit - fi - SPINNER_PID="" - install_script - fi - - if ! command -v pveversion >/dev/null 2>&1; then - if ! (whiptail --backtitle "Proxmox VE Helper Scripts" --title "${APP} LXC UPDATE" --yesno "Support/Update functions for ${APP} LXC. Proceed?" 10 58); then - clear - echo -e "⚠ User exited script \n" - exit - fi - SPINNER_PID="" - update_script - fi -} - -# This function collects user settings and integrates all the collected information. -build_container() { -# if [ "$VERB" == "yes" ]; then set -x; fi - - if [ "$CT_TYPE" == "1" ]; then - # Unprivileged container - FEATURES="keyctl=1,nesting=1,fuse=1" - else - # Privileged container - FEATURES="nesting=1,fuse=1" - fi - - - TEMP_DIR=$(mktemp -d) - pushd $TEMP_DIR >/dev/null - if [ "$var_os" == "alpine" ]; then - export FUNCTIONS_FILE_PATH="$(curl -s https://raw.githubusercontent.com/tteck/Proxmox/main/misc/alpine-install.func)" - else - export FUNCTIONS_FILE_PATH="$(curl -s https://raw.githubusercontent.com/tteck/Proxmox/main/misc/install.func)" - fi - export CACHER="$APT_CACHER" - export CACHER_IP="$APT_CACHER_IP" - export tz="$timezone" - export DISABLEIPV6="$DISABLEIP6" - export APPLICATION="$APP" - export app="$NSAPP" - export PASSWORD="$PW" - export VERBOSE="$VERB" - export SSH_ROOT="${SSH}" - export RIVEN_FRONTEND_ORIGIN - export RIVEN_INSTALL_FRONTEND - export RIVEN_MEDIA_PLEX - export RIVEN_MEDIA_JELLYFIN - export RIVEN_MEDIA_EMBY - export CTID="$CT_ID" - export CTTYPE="$CT_TYPE" - export PCT_OSTYPE="$var_os" - export PCT_OSVERSION="$var_version" - export PCT_DISK_SIZE="$DISK_SIZE" - export PCT_OPTIONS=" - -features $FEATURES - -hostname $HN - -tags proxmox-helper-scripts - $SD - $NS - -net0 name=eth0,bridge=$BRG$MAC,ip=$NET$GATE$VLAN$MTU - -onboot 1 - -cores $CORE_COUNT - -memory $RAM_SIZE - -unprivileged $CT_TYPE - $PW - " - # This executes create_lxc.sh and creates the container and .conf file - bash -c "$(wget -qLO - https://raw.githubusercontent.com/tteck/Proxmox/main/ct/create_lxc.sh)" || exit - - LXC_CONFIG=/etc/pve/lxc/${CTID}.conf - if [ "$CT_TYPE" == "0" ]; then - cat <>$LXC_CONFIG -# USB passthrough -lxc.cgroup2.devices.allow: a -lxc.cap.drop: -lxc.cgroup2.devices.allow: c 188:* rwm -lxc.cgroup2.devices.allow: c 189:* rwm -lxc.mount.entry: /dev/serial/by-id dev/serial/by-id none bind,optional,create=dir -lxc.mount.entry: /dev/ttyUSB0 dev/ttyUSB0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM0 dev/ttyACM0 none bind,optional,create=file -lxc.mount.entry: /dev/ttyACM1 dev/ttyACM1 none bind,optional,create=file -# FUSE support -lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file,rw 0 0 -EOF - else - # For unprivileged containers, we need to ensure FUSE device is accessible - cat <>$LXC_CONFIG -# FUSE support -lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file,rw 0 0 -EOF - fi - - if [ "$CT_TYPE" == "0" ]; then - if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" ]]; then - cat <>$LXC_CONFIG -# VAAPI hardware transcoding -lxc.cgroup2.devices.allow: c 226:0 rwm -lxc.cgroup2.devices.allow: c 226:128 rwm -lxc.cgroup2.devices.allow: c 29:0 rwm -lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file -lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir -lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file -EOF - fi - else - if [[ "$APP" == "Channels" || "$APP" == "Emby" || "$APP" == "Frigate" || "$APP" == "Jellyfin" || "$APP" == "Plex" || "$APP" == "Scrypted" || "$APP" == "Tdarr" || "$APP" == "Unmanic" ]]; then - if [[ -e "/dev/dri/renderD128" ]]; then - if [[ -e "/dev/dri/card0" ]]; then - cat <>$LXC_CONFIG -# VAAPI hardware transcoding -dev0: /dev/dri/card0,gid=44 -dev1: /dev/dri/renderD128,gid=104 -EOF - else - cat <>$LXC_CONFIG -# VAAPI hardware transcoding -dev0: /dev/dri/card1,gid=44 -dev1: /dev/dri/renderD128,gid=104 -EOF - fi - fi - fi - fi - - # This starts the container and executes -install.sh - msg_info "Starting LXC Container" - pct start "$CTID" - msg_ok "Started LXC Container" - if [ "$var_os" == "alpine" ]; then - sleep 3 - pct exec "$CTID" -- /bin/sh -c 'cat </etc/apk/repositories -http://dl-cdn.alpinelinux.org/alpine/latest-stable/main -http://dl-cdn.alpinelinux.org/alpine/latest-stable/community -#http://dl-cdn.alpinelinux.org/alpine/v3.19/main -#http://dl-cdn.alpinelinux.org/alpine/v3.19/community -EOF' - pct exec "$CTID" -- ash -c "apk add bash >/dev/null" - fi - lxc-attach -n "$CTID" -- bash -c "$(wget -qLO - https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/$var_install.sh)" || exit - -} - -# This function sets the description of the container. -description() { - IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1) - pct set "$CTID" -description "
- - # ${APP} LXC - - -
" - if [[ -f /etc/systemd/system/ping-instances.service ]]; then - systemctl start ping-instances.service - fi -} \ No newline at end of file diff --git a/proxmox/changelog.md b/proxmox/changelog.md new file mode 100644 index 0000000..ccedae5 --- /dev/null +++ b/proxmox/changelog.md @@ -0,0 +1,87 @@ +# 📦 Proxmox Riven Installer — Change Log + +## Version: 1.2 +Release type: Structural + UX improvement (layout-changing) + +--- + +## 🔌 Network Interface Handling +- Added automatic detection of usable network interfaces +- Excludes invalid/virtual interfaces: + - lo, docker*, veth*, virbr*, tun*, tap* +- Added whiptail-based TUI selector for interface selection +- Removed requirement to manually type interface names + +--- + +## 📁 Storage & Volume Layout (Major Change) +### Old +- Templates and container data stored in separate mounts +- Multiple bind mounts required +- Higher complexity and user confusion + +### New +Unified storage root: +``` +/srv/riven +├── templates +├── containers +├── config +├── data +└── mount +``` + +Benefits: +- Single bind mount +- Cleaner backups +- Simpler permissions +- Easier Docker volume management + +--- + +## 📦 LXC Configuration +- Replaced multiple mp entries with a single mount: + mp0: /srv/riven,mp=/srv/riven +- All services operate within unified root + +--- + +## 🐳 Docker / Compose +- Updated volume mappings to reference /srv/riven paths +- Reduced mount propagation edge cases +- Simplified compose configuration + +--- + +## 🛠 Installer Script Improvements +- Added interface auto-detection logic +- Added interactive UI menus +- Centralized directory creation +- Reduced hard-coded paths +- Improved comments and readability + +--- + +## 📄 Documentation +- Updated README to reflect: + - New storage layout + - New network selection behavior + - Single-mount architecture + +--- + +## ⚠️ Migration Notes +- Existing installs using split mounts should: + 1. Move data into /srv/riven + 2. Update LXC config to single mount + 3. Restart containers + +No data formats were changed — only paths. + +--- + +## ✅ Summary +- Removed manual network configuration +- Simplified storage architecture +- Reduced user error +- Improved maintainability diff --git a/proxmox/create_lxc.sh b/proxmox/create_lxc.sh deleted file mode 100644 index ff4e918..0000000 --- a/proxmox/create_lxc.sh +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env bash - -# This function sets color variables for formatting output in the terminal -YW=$(echo "\033[33m") -BL=$(echo "\033[36m") -RD=$(echo "\033[01;31m") -GN=$(echo "\033[1;92m") -CL=$(echo "\033[m") -CM="${GN}✓${CL}" -CROSS="${RD}✗${CL}" -BFR="\\r\\033[K" -HOLD=" " - -# This sets error handling options and defines the error_handler function to handle errors -set -Eeuo pipefail -trap 'error_handler $LINENO "$BASH_COMMAND"' ERR - -# This function handles errors -function error_handler() { - if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID > /dev/null; then kill $SPINNER_PID > /dev/null; fi - printf "\e[?25h" - local exit_code="$?" - local line_number="$1" - local command="$2" - local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" - echo -e "\n$error_message\n" -} - -# This function displays an informational message with a yellow color. -function msg_info() { - local msg="$1" - echo -ne " ${HOLD} ${YW}${msg}...${CL}" -} - -# This function displays a success message with a green color. -function msg_ok() { - local msg="$1" - echo -e "\r ${CM} ${GN}${msg}${CL}" -} - -# This function displays a error message with a red color. -function msg_error() { - local msg="$1" - echo -e "\r ${CROSS} ${RD}${msg}${CL}" -} - -# This checks for the presence of valid Container Storage and Template Storage locations -msg_info "Validating Storage" -VALIDCT=$(pvesm status -content rootdir | awk 'NR>1') -if [ -z "$VALIDCT" ]; then - msg_error "Unable to detect a valid Container Storage location." - exit 1 -fi -VALIDTMP=$(pvesm status -content vztmpl | awk 'NR>1') -if [ -z "$VALIDTMP" ]; then - msg_error "Unable to detect a valid Template Storage location." - exit 1 -fi - -# This function is used to select the storage class and determine the corresponding storage content type and label. -function select_storage() { - local CLASS=$1 - local CONTENT - local CONTENT_LABEL - case $CLASS in - container) - CONTENT='rootdir' - CONTENT_LABEL='Container' - ;; - template) - CONTENT='vztmpl' - CONTENT_LABEL='Container template' - ;; - *) false || exit "Invalid storage class." ;; - esac - - # This Queries all storage locations - local -a MENU - while read -r line; do - local TAG=$(echo $line | awk '{print $1}') - local TYPE=$(echo $line | awk '{printf "%-10s", $2}') - local FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}') - local ITEM=" Type: $TYPE Free: $FREE " - local OFFSET=2 - if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then - local MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET)) - fi - MENU+=("$TAG" "$ITEM" "OFF") - done < <(pvesm status -content $CONTENT | awk 'NR>1') - - # Select storage location - if [ $((${#MENU[@]}/3)) -eq 1 ]; then - printf ${MENU[0]} - else - local STORAGE - while [ -z "${STORAGE:+x}" ]; do - STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \ - "Which storage pool you would like to use for the ${CONTENT_LABEL,,}?\nTo make a selection, use the Spacebar.\n" \ - 16 $(($MSG_MAX_LENGTH + 23)) 6 \ - "${MENU[@]}" 3>&1 1>&2 2>&3) || exit "Menu aborted." - done - printf $STORAGE - fi -} - -# Test if required variables are set -[[ "${CTID:-}" ]] || exit "You need to set 'CTID' variable." -[[ "${PCT_OSTYPE:-}" ]] || exit "You need to set 'PCT_OSTYPE' variable." - -# Test if ID is valid -[ "$CTID" -ge "100" ] || exit "ID cannot be less than 100." - -# Test if ID is in use -if pct status $CTID &>/dev/null; then - echo -e "ID '$CTID' is already in use." - unset CTID - exit "Cannot use ID that is already in use." -fi - -# Get template storage -TEMPLATE_STORAGE=$(select_storage template) || exit -msg_ok "Using ${BL}$TEMPLATE_STORAGE${CL} ${GN}for Template Storage." - -# Get container storage -CONTAINER_STORAGE=$(select_storage container) || exit -msg_ok "Using ${BL}$CONTAINER_STORAGE${CL} ${GN}for Container Storage." - -# Update LXC template list -msg_info "Updating LXC Template List" -pveam update >/dev/null -msg_ok "Updated LXC Template List" - -# Get LXC template string -TEMPLATE_SEARCH=${PCT_OSTYPE}-${PCT_OSVERSION:-} -mapfile -t TEMPLATES < <(pveam available -section system | sed -n "s/.*\($TEMPLATE_SEARCH.*\)/\1/p" | sort -t - -k 2 -V) -[ ${#TEMPLATES[@]} -gt 0 ] || exit "Unable to find a template when searching for '$TEMPLATE_SEARCH'." -TEMPLATE="${TEMPLATES[-1]}" - -# Download LXC template if needed -if ! pveam list $TEMPLATE_STORAGE | grep -q $TEMPLATE; then - msg_info "Downloading LXC Template" - pveam download $TEMPLATE_STORAGE $TEMPLATE >/dev/null || - exit "A problem occured while downloading the LXC template." - msg_ok "Downloaded LXC Template" -fi - -# Combine all options -DEFAULT_PCT_OPTIONS=( - -arch $(dpkg --print-architecture)) - -PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}}) -[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8}) - -# Create container -msg_info "Creating LXC Container" -pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[@]} >/dev/null || - exit "A problem occured while trying to create container." -msg_ok "LXC Container ${BL}$CTID${CL} ${GN}was successfully created." \ No newline at end of file diff --git a/proxmox/lxc/docker-compose.yml b/proxmox/lxc/docker-compose.yml new file mode 100644 index 0000000..07063fd --- /dev/null +++ b/proxmox/lxc/docker-compose.yml @@ -0,0 +1,127 @@ +services: + riven-db: + image: postgres:17-alpine + container_name: riven-db + restart: unless-stopped + env_file: [.env] + environment: + PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - riven-pg-data:/var/lib/postgresql/data/pgdata + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + + riven: + image: spoked/riven:dev + container_name: riven + restart: unless-stopped + shm_size: 1024m + ports: + - "8080:8080" + tty: true + cap_add: + - SYS_ADMIN + security_opt: + - apparmor:unconfined + devices: + - /dev/fuse + env_file: [.env] + environment: + TZ: ${TZ} + PUID: 1000 + PGID: 1000 + RIVEN_FORCE_ENV: "true" + RIVEN_API_KEY: ${BACKEND_API_KEY} + RIVEN_DATABASE_HOST: postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@riven-db:5432/${POSTGRES_DB} + RIVEN_FILESYSTEM_MOUNT_PATH: /mount + # Unified host storage layout uses /srv/riven; inside the container we point updaters at /mount + RIVEN_UPDATERS_LIBRARY_PATH: /mount + RIVEN_FILESYSTEM_CACHE_DIR: /dev/shm/riven-cache + volumes: + - /srv/riven/backend:/riven/data + - /srv/riven/mount:/mount:rshared + depends_on: + riven-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:8080 >/dev/null || exit 1"] + interval: 30s + timeout: 10s + retries: 10 + + riven-frontend: + image: spoked/riven-frontend:dev + container_name: riven-frontend + restart: unless-stopped + ports: + - "3000:3000" + env_file: [.env] + environment: + TZ: ${TZ} + DATABASE_URL: /riven/data/riven.db + BACKEND_URL: http://riven:8080 + BACKEND_API_KEY: ${BACKEND_API_KEY} + AUTH_SECRET: ${AUTH_SECRET} + ORIGIN: http://localhost:3000 + volumes: + - riven-frontend-data:/riven/data + depends_on: + riven: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:3000 >/dev/null || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + + jellyfin: + image: jellyfin/jellyfin + container_name: jellyfin + profiles: ["jellyfin"] + restart: unless-stopped + ports: + - "8096:8096" + devices: + - /dev/dri:/dev/dri + volumes: + - /srv/riven/mount:/media:ro + - /srv/riven/media/jellyfin:/config + + plex: + image: lscr.io/linuxserver/plex + container_name: plex + profiles: ["plex"] + restart: unless-stopped + network_mode: host + environment: + - PUID=1000 + - PGID=1000 + - VERSION=docker + devices: + - /dev/dri:/dev/dri + volumes: + - /srv/riven/mount:/media:ro + - /srv/riven/media/plex:/config + + emby: + image: emby/embyserver + container_name: emby + profiles: ["emby"] + restart: unless-stopped + ports: + - "8097:8096" + devices: + - /dev/dri:/dev/dri + volumes: + - /srv/riven/mount:/media:ro + - /srv/riven/media/emby:/config + +volumes: + riven-frontend-data: + riven-pg-data: diff --git a/proxmox/lxc/lxc-bootstrap.sh b/proxmox/lxc/lxc-bootstrap.sh new file mode 100644 index 0000000..6e7f2f4 --- /dev/null +++ b/proxmox/lxc/lxc-bootstrap.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "== LXC Bootstrap: install Docker + Compose ==" + +export DEBIAN_FRONTEND=noninteractive + +apt-get update +apt-get install -y ca-certificates curl gnupg lsb-release fuse3 uidmap jq + +# Install Docker from Docker's official repo +install -m 0755 -d /etc/apt/keyrings +if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg +fi + +ARCH="$(dpkg --print-architecture)" +CODENAME="$(. /etc/os-release && echo "$VERSION_CODENAME")" + +cat >/etc/apt/sources.list.d/docker.list </dev/null 2>&1 || { echo "ERROR: Missing $1"; exit 1; }; } +require_cmd pveam +require_cmd pvesm +require_cmd pct + +# Find Debian 12 template (download if missing) +TPL_NAME="$(pveam available --section system | awk '/debian-12-standard/ {print $2}' | tail -n1)" +if [[ -z "$TPL_NAME" ]]; then + echo "ERROR: Could not find Debian 12 template in pveam catalog." + exit 1 +fi + +if ! pveam list "$STORAGE" | awk '{print $1}' | grep -qx "$TPL_NAME"; then + echo "Downloading Debian 12 template to storage '$STORAGE'..." + pveam download "$STORAGE" "$TPL_NAME" +fi + +# Root password (random) for convenience +ROOT_PASS="$(openssl rand -base64 18 | tr -d '\n' | tr -d '=+/')" +echo "CT root password (SAVE THIS): $ROOT_PASS" + +# Create container +echo "Creating CT $CTID..." +pct create "$CTID" "$STORAGE:vztmpl/$TPL_NAME" \ + --hostname "$HOSTNAME" \ + --unprivileged 1 \ + --features "nesting=1,keyctl=1,fuse=1" \ + --cores "$CORES" \ + --memory "$MEM_MB" \ + --swap 1024 \ + --rootfs "${STORAGE}:${DISK_GB}" \ + --net0 "$NET0" \ + --password "$ROOT_PASS" \ + --onboot 1 \ + --start 1 + +CONF="/etc/pve/lxc/${CTID}.conf" + +# Docker-in-LXC hardening/compat +# (These are common requirements for Docker in an unprivileged LXC on Proxmox.) +{ + echo "" + echo "# --- Riven/Docker requirements ---" + echo "lxc.apparmor.profile: unconfined" + echo "lxc.cgroup2.devices.allow: a" + echo "lxc.mount.auto: proc:rw sys:rw" +} >> "$CONF" + +# Pass /dev/fuse +{ + echo "# Pass FUSE" + echo "lxc.cgroup2.devices.allow: c 10:229 rwm" + echo "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file,optional 0 0" +} >> "$CONF" + +# Pass GPU (optional) +if [[ "${GPU}" == "yes" ]]; then + { + echo "# Pass GPU (DRI)" + echo "lxc.cgroup2.devices.allow: c 226:* rwm" + echo "lxc.mount.entry: /dev/dri dev/dri none bind,create=dir,optional 0 0" + } >> "$CONF" +fi + +# Bind-mount host path into CT as /srv/riven (recommended) +if [[ -n "$HOST_RIVEN_PATH" ]]; then + if [[ ! -d "$HOST_RIVEN_PATH" ]]; then + echo "Creating host path: $HOST_RIVEN_PATH" + mkdir -p "$HOST_RIVEN_PATH" + fi + echo "# Bind host storage into CT" + echo "mp0: ${HOST_RIVEN_PATH},mp=/srv/riven" >> "$CONF" +fi + +echo "Restarting CT to apply config changes..." +pct stop "$CTID" +pct start "$CTID" + +echo "CT $CTID created and configured." diff --git a/proxmox/lxc/riven-install.sh b/proxmox/lxc/riven-install.sh new file mode 100644 index 0000000..b4c777c --- /dev/null +++ b/proxmox/lxc/riven-install.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "== Riven Docker Install (inside LXC) ==" + +ROOT_DIR="/srv/riven" +INSTALL_DIR="$ROOT_DIR/app" +DATA_DIR="$ROOT_DIR/data" +BACKEND_DIR="$ROOT_DIR/backend" +MOUNT_DIR="$ROOT_DIR/mount" +MEDIA_DIR="$ROOT_DIR/media" + +RIVEN_UID=1000 +RIVEN_GID=1000 + +# ---------------------------- +# Directory setup +# ---------------------------- +mkdir -p "$INSTALL_DIR" "$DATA_DIR" "$BACKEND_DIR" "$MOUNT_DIR" +mkdir -p "$MEDIA_DIR/jellyfin" "$MEDIA_DIR/plex" "$MEDIA_DIR/emby" +chown -R "$RIVEN_UID:$RIVEN_GID" "$ROOT_DIR" + +# ---------------------------- +# Ensure bind + rshared mount +# ---------------------------- +if ! mountpoint -q "$MOUNT_DIR"; then + mount --bind "$MOUNT_DIR" "$MOUNT_DIR" +fi + +mount --make-rshared "$MOUNT_DIR" + +PROP="$(findmnt -T "$MOUNT_DIR" -o PROPAGATION -n || true)" +if [[ "$PROP" != "shared" && "$PROP" != "rshared" ]]; then + echo "ERROR: $MOUNT_DIR is not shared (got: $PROP)" + exit 1 +fi + +# ---------------------------- +# Persist rshared mount on boot +# ---------------------------- +cat >/etc/systemd/system/riven-bind-shared.service < .env </dev/null || true - ${APT_STD} adduser "$(id -u -n)" video || true - ${APT_STD} adduser "$(id -u -n)" render || true - fi - msg_ok "Set up Emby hardware acceleration packages" - fi - - local LATEST JSON_FILE - JSON_FILE="/tmp/emby-releases.json" - msg_info "Determining latest Emby release" - if ! curl -fsSL https://api.github.com/repos/MediaBrowser/Emby.Releases/releases/latest -o "$JSON_FILE"; then - msg_error "Failed to query Emby releases API; skipping Emby installation" - rm -f "$JSON_FILE" - return 1 - fi - # Use jq to parse the tag_name from the JSON payload. - LATEST="$(jq -r '.tag_name // empty' "$JSON_FILE" 2>/dev/null || true)" - rm -f "$JSON_FILE" - if [[ -z "${LATEST:-}" ]]; then - msg_error "Could not determine latest Emby release tag; skipping Emby installation" - return 1 - fi - - local DEB_PATH="/tmp/emby-server-deb_${LATEST}_amd64.deb" - msg_info "Downloading Emby Media Server (${LATEST})" - if ! curl -fsSL "https://github.com/MediaBrowser/Emby.Releases/releases/download/${LATEST}/emby-server-deb_${LATEST}_amd64.deb" \ - -o "${DEB_PATH}"; then - msg_error "Failed to download Emby .deb; skipping Emby installation" - return 1 - fi - - msg_info "Installing Emby Media Server (${LATEST})" - if ! ${APT_STD} dpkg -i "${DEB_PATH}"; then - msg_error "dpkg reported issues while installing Emby; attempting to fix dependencies" - if ! ${APT_STD} apt-get install -f -y; then - msg_error "Failed to resolve Emby dependencies" - rm -f "${DEB_PATH}" - return 1 - fi - fi - rm -f "${DEB_PATH}" - - # Emby and Jellyfin both default to port 8096. To avoid a conflict when - # both are installed in the same container, move Emby to 8097. - # Emby's config is typically stored under /var/lib/emby/config/system.xml, - # but it may not exist immediately. Give it a short window to appear. - local EMBY_CONFIG="/var/lib/emby/config/system.xml" - local wait_secs=0 - if systemctl list-unit-files | grep -q '^emby-server\.service'; then - systemctl start emby-server 2>/dev/null || true - fi - while [ "$wait_secs" -lt 10 ] && [ ! -f "$EMBY_CONFIG" ]; do - sleep 1 - wait_secs=$((wait_secs + 1)) - done - if [ -f "$EMBY_CONFIG" ]; then - msg_info "Reconfiguring Emby HTTP port to 8097 to avoid Jellyfin conflict" - sed -i \ - -e 's#8096#8097#' \ - -e 's#8096#8097#' \ - "$EMBY_CONFIG" 2>/dev/null || true - systemctl restart emby-server 2>/dev/null || true - fi - - # Adjust ssl-cert/render groups for Emby (best-effort). - if [[ "${CTTYPE:-1}" == "0" ]]; then - sed -i -e 's/^ssl-cert:x:104:emby$/render:x:104:root,emby/' \ - -e 's/^render:x:108:root$/ssl-cert:x:108:emby/' /etc/group 2>/dev/null || true - else - sed -i -e 's/^ssl-cert:x:104:emby$/render:x:104:emby/' \ - -e 's/^render:x:108:$/ssl-cert:x:108:/' /etc/group 2>/dev/null || true - fi - - # Ensure Emby can read Riven's VFS by joining the riven group if it exists. - if getent group riven >/dev/null 2>&1; then - usermod -aG riven emby 2>/dev/null || true - fi - - msg_ok "Installed Emby Media Server (${LATEST})" -} - diff --git a/proxmox/media-jellyfin.sh b/proxmox/media-jellyfin.sh deleted file mode 100644 index 3b03af1..0000000 --- a/proxmox/media-jellyfin.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -# Jellyfin Media Server install helper for the Riven LXC. -# -# This file is intended to be sourced by proxmox/riven-install.sh via: -# source /dev/stdin <<<"$(curl -fsSL https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/media-jellyfin.sh)" -# -# It assumes the following are already available in the environment: -# - msg_info/msg_ok/msg_error functions (from tteck install.func) -# - CTTYPE, PCT_OSTYPE, STD (optional), etc. -# -# It MUST NOT call exit; instead it should return non-zero on error so the -# caller can decide whether to continue. - -install_jellyfin_media_server() { - local APT_STD="${STD:-}" - local INSTALL_SCRIPT="/tmp/jellyfin-install-debuntu.sh" - - # Ensure curl is available (normally installed earlier, but be safe). - if ! command -v curl >/dev/null 2>&1; then - msg_info "Installing curl for Jellyfin installer" - if ! ${APT_STD} apt-get install -y curl; then - msg_error "Failed to install curl; skipping Jellyfin installation" - return 1 - fi - fi - - msg_info "Downloading official Jellyfin installer script" - if ! curl -fsSL https://repo.jellyfin.org/install-debuntu.sh -o "${INSTALL_SCRIPT}"; then - msg_error "Failed to download Jellyfin installer script; skipping Jellyfin installation" - return 1 - fi - - msg_info "Running official Jellyfin installer script" - if ! SKIP_CONFIRM=true bash "${INSTALL_SCRIPT}" >/dev/null 2>&1; then - msg_error "Jellyfin installer script failed; skipping Jellyfin installation" - rm -f "${INSTALL_SCRIPT}" - return 1 - fi - rm -f "${INSTALL_SCRIPT}" - - # Best-effort hardware acceleration tweaks (optional). - msg_info "Setting up Jellyfin hardware acceleration packages" - if ! ${APT_STD} apt-get -y install va-driver-all ocl-icd-libopencl1 intel-opencl-icd vainfo intel-gpu-tools; then - msg_error "Failed to install Jellyfin GPU/VAAPI packages (continuing without hardware acceleration)" - else - if [[ "${CTTYPE:-1}" == "0" && -d /dev/dri ]]; then - chgrp video /dev/dri || true - chmod 755 /dev/dri || true - chmod 660 /dev/dri/* 2>/dev/null || true - ${APT_STD} adduser "$(id -u -n)" video || true - ${APT_STD} adduser "$(id -u -n)" render || true - fi - msg_ok "Set up Jellyfin hardware acceleration packages" - fi - - # Ensure permissions are sane; installer already does this, so best-effort. - chown -R jellyfin:adm /etc/jellyfin 2>/dev/null || true - systemctl restart jellyfin 2>/dev/null || true - - # Ensure Jellyfin can read Riven's VFS by joining the riven group if it exists. - if getent group riven >/dev/null 2>&1; then - usermod -aG riven jellyfin 2>/dev/null || true - fi - - msg_ok "Installed Jellyfin Media Server" -} - diff --git a/proxmox/media-plex.sh b/proxmox/media-plex.sh deleted file mode 100644 index 21913aa..0000000 --- a/proxmox/media-plex.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash - -# Plex Media Server install helper for the Riven LXC. -# -# This file is intended to be sourced by proxmox/riven-install.sh via: -# source /dev/stdin <<<"$(curl -fsSL https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/media-plex.sh)" -# -# It assumes the following are already available in the environment: -# - msg_info/msg_ok/msg_error functions (from tteck install.func) -# - CTTYPE, STD (optional), etc. -# -# It MUST NOT call exit; instead it should return non-zero on error so the -# caller can decide whether to continue. - -install_plex_media_server() { - local APT_STD="${STD:-}" - - msg_info "Installing Plex Media Server dependencies" - if ! ${APT_STD} apt-get install -y curl sudo mc gpg; then - msg_error "Failed to install Plex dependencies; skipping Plex installation" - return 1 - fi - msg_ok "Installed Plex Media Server dependencies" - - msg_info "Setting up Plex hardware acceleration packages" - if ! ${APT_STD} apt-get -y install va-driver-all ocl-icd-libopencl1 intel-opencl-icd vainfo intel-gpu-tools; then - msg_error "Failed to install Plex GPU/VAAPI packages (continuing without hardware acceleration)" - else - if [[ "${CTTYPE:-1}" == "0" && -d /dev/dri ]]; then - chgrp video /dev/dri || true - chmod 755 /dev/dri || true - chmod 660 /dev/dri/* 2>/dev/null || true - ${APT_STD} adduser "$(id -u -n)" video || true - ${APT_STD} adduser "$(id -u -n)" render || true - fi - msg_ok "Set up Plex hardware acceleration packages" - fi - - msg_info "Setting up Plex Media Server repository" - if ! curl -fsSL https://downloads.plex.tv/plex-keys/PlexSign.key \ - >/usr/share/keyrings/PlexSign.asc; then - msg_error "Failed to download Plex signing key; skipping Plex installation" - return 1 - fi - if ! echo "deb [signed-by=/usr/share/keyrings/PlexSign.asc] https://downloads.plex.tv/repo/deb/ public main" \ - >/etc/apt/sources.list.d/plexmediaserver.list; then - msg_error "Failed to configure Plex apt source; skipping Plex installation" - return 1 - fi - msg_ok "Configured Plex Media Server repository" - - msg_info "Installing Plex Media Server" - if ! ${APT_STD} apt-get update; then - msg_error "apt-get update failed before Plex installation; skipping Plex" - return 1 - fi - if ! ${APT_STD} apt-get -o Dpkg::Options::="--force-confold" install -y plexmediaserver; then - msg_error "Failed to install Plex Media Server package" - return 1 - fi - - # Adjust ssl-cert/render groups for Plex (best-effort, do not fail install). - if [[ "${CTTYPE:-1}" == "0" ]]; then - sed -i -e 's/^ssl-cert:x:104:plex$/render:x:104:root,plex/' \ - -e 's/^render:x:108:root$/ssl-cert:x:108:plex/' /etc/group 2>/dev/null || true - else - sed -i -e 's/^ssl-cert:x:104:plex$/render:x:104:plex/' \ - -e 's/^render:x:108:$/ssl-cert:x:108:/' /etc/group 2>/dev/null || true - fi - - # Ensure Plex can read Riven's VFS by joining the riven group if it exists. - if getent group riven >/dev/null 2>&1; then - usermod -aG riven plex 2>/dev/null || true - fi - - msg_ok "Installed Plex Media Server" -} - diff --git a/proxmox/riven-install.sh b/proxmox/riven-install.sh deleted file mode 100644 index 1f75014..0000000 --- a/proxmox/riven-install.sh +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env bash - -# Baremetal Riven installer for Debian LXC (unprivileged) -# - Installs system dependencies (Python, Node, Postgres, FUSE, build tools, ffmpeg, etc.) -# - Configures FUSE and Python capabilities for RivenVFS -# - Sets up local PostgreSQL -# - Installs Riven backend (Python/uv) and frontend (Node/pnpm) -# - Creates env config in /etc/riven and systemd services for both components - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -export DEBIAN_FRONTEND=noninteractive - -# Determine whether to install the Riven frontend in this container. -# Default is "yes" unless overridden by the host helper via -# RIVEN_INSTALL_FRONTEND (values like yes/no/true/false/1/0). -INSTALL_FRONTEND_RAW="${RIVEN_INSTALL_FRONTEND:-yes}" -INSTALL_FRONTEND_RAW="$(echo "$INSTALL_FRONTEND_RAW" | tr '[:upper:]' '[:lower:]')" -if [[ "$INSTALL_FRONTEND_RAW" == "yes" || "$INSTALL_FRONTEND_RAW" == "true" || "$INSTALL_FRONTEND_RAW" == "1" ]]; then - INSTALL_FRONTEND="yes" -else - INSTALL_FRONTEND="no" -fi - -# ------------------------------------------------------------ -# Optional media server installers inside this Riven container -# -# The heavy install logic for Plex/Jellyfin/Emby lives in separate -# proxmox/media-*.sh scripts. We fetch and source those on demand so -# this main installer stays tidy. Errors in media server installs -# must never abort the core Riven installation. -# ------------------------------------------------------------ - -run_media_installer() { - local NAME="$1" URL="$2" FUNC="$3" - local SRC - - if ! SRC="$(curl -fsSL "$URL")"; then - msg_error "Failed to download ${NAME} installer script; skipping ${NAME} installation" - # Do not propagate non-zero status; media installs are optional. - return 0 - fi - - if ! source /dev/stdin <<<"$SRC"; then - msg_error "Failed to load ${NAME} installer script; skipping ${NAME} installation" - # Do not propagate non-zero status; media installs are optional. - return 0 - fi - - if ! "$FUNC"; then - msg_error "${NAME} installation encountered an error; continuing without ${NAME}" - # Do not propagate non-zero status; media installs are optional. - return 0 - fi - - # Record successful installation so the host helper can show - # accurate media server URLs in its completion message. - local MEDIA_FILE="/etc/riven/media-servers.txt" - local ID - ID=$(echo "$NAME" | tr '[:upper:] ' '[:lower:]-') - mkdir -p /etc/riven - if ! grep -qx "$ID" "$MEDIA_FILE" 2>/dev/null; then - printf '%s\n' "$ID" >>"$MEDIA_FILE" - fi - - return 0 -} - -install_selected_media_servers() { - # Use host-provided selections (RIVEN_MEDIA_*) to decide what to install. - # Default for all is "no", so if the user did not explicitly select a - # media server on the host, nothing is installed here. - local WANT_PLEX WANT_JELLYFIN WANT_EMBY - WANT_PLEX="${RIVEN_MEDIA_PLEX:-no}" - WANT_JELLYFIN="${RIVEN_MEDIA_JELLYFIN:-no}" - WANT_EMBY="${RIVEN_MEDIA_EMBY:-no}" - - # Normalize to lowercase for robustness. - WANT_PLEX=$(echo "$WANT_PLEX" | tr '[:upper:]' '[:lower:]') - WANT_JELLYFIN=$(echo "$WANT_JELLYFIN" | tr '[:upper:]' '[:lower:]') - WANT_EMBY=$(echo "$WANT_EMBY" | tr '[:upper:]' '[:lower:]') - - if [[ "$WANT_PLEX" != "yes" && "$WANT_JELLYFIN" != "yes" && "$WANT_EMBY" != "yes" ]]; then - msg_info "Skipping media server installation (none selected from host)" - rm -f /etc/riven/media-servers.txt 2>/dev/null || true - return - fi - - if [[ "$WANT_PLEX" == "yes" ]]; then - run_media_installer \ - "Plex" \ - "https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/media-plex.sh" \ - install_plex_media_server - fi - if [[ "$WANT_JELLYFIN" == "yes" ]]; then - run_media_installer \ - "Jellyfin" \ - "https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/media-jellyfin.sh" \ - install_jellyfin_media_server - fi - if [[ "$WANT_EMBY" == "yes" ]]; then - run_media_installer \ - "Emby" \ - "https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/media-emby.sh" \ - install_emby_media_server - fi -} - -msg_info "Installing Dependencies" -$STD apt-get update -$STD apt-get install -y \ - curl sudo mc git ffmpeg vim whiptail \ - python3 python3-venv python3-dev build-essential libffi-dev libpq-dev libfuse3-dev pkg-config \ - fuse3 libcap2-bin ca-certificates openssl \ - postgresql postgresql-contrib postgresql-client -msg_ok "Installed Dependencies" - -msg_info "Configuring FUSE" -echo 'user_allow_other' > /etc/fuse.conf -msg_ok "Configured FUSE" - -msg_info "Configuring Python capabilities for FUSE" -PY_BIN=$(command -v python3 || true) -if [ -n "$PY_BIN" ]; then - setcap cap_sys_admin+ep "$PY_BIN" 2>/dev/null || true -fi -msg_ok "Configured Python capabilities" - -if [ "$INSTALL_FRONTEND" = "yes" ]; then - msg_info "Installing Node.js (24.x) and pnpm" - curl -fsSL https://deb.nodesource.com/setup_24.x | bash - >/dev/null 2>&1 || { - msg_error "Failed to configure NodeSource repository for Node.js" - exit 1 - } - $STD apt-get install -y nodejs - npm install -g pnpm >/dev/null 2>&1 || { - msg_error "Failed to install pnpm globally" - exit 1 - } - msg_ok "Installed Node.js and pnpm" -else - msg_info "Skipping Node.js/pnpm install (frontend disabled)" - msg_ok "Frontend components will not be installed in this container" -fi - -msg_info "Configuring PostgreSQL" -$STD systemctl enable postgresql -$STD systemctl start postgresql -if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='riven'" | grep -q 1; then - sudo -u postgres psql -c "CREATE DATABASE riven;" >/dev/null 2>&1 || true -fi -sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'postgres';" >/dev/null 2>&1 || true -msg_ok "Configured PostgreSQL" - -msg_info "Creating Riven user and directories" -if ! id -u riven >/dev/null 2>&1; then - useradd -r -d /riven -s /usr/sbin/nologin riven || true -fi - -# Core application directories -mkdir -p /riven /riven/data /mount /mnt/riven /etc/riven -if [ "$INSTALL_FRONTEND" = "yes" ]; then - mkdir -p /opt/riven-frontend -fi -chown -R riven:riven /riven /riven/data /mount /mnt/riven -if [ "$INSTALL_FRONTEND" = "yes" ]; then - chown -R riven:riven /opt/riven-frontend -fi - -# Make the library-related mountpoints world-readable so media from other -# containers (e.g. Plex/Jellyfin/Emby) can be shared via /mnt/riven. -chmod 755 /riven /mount /mnt/riven || true - -# Keep internal data more restricted; only the riven user should need this. -chmod 700 /riven/data || true - -# Cache directory for RivenVFS (not shared across LXCs) -mkdir -p /dev/shm/riven-cache -chown riven:riven /dev/shm/riven-cache || true -chmod 700 /dev/shm/riven-cache || true -msg_ok "Created Riven user and directories" - -msg_info "Installing uv package manager" -curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1 || true -export PATH="${HOME}/.local/bin:$PATH" -UV_BIN="${HOME}/.local/bin/uv" -if [ ! -x "$UV_BIN" ]; then - msg_error "uv was not installed correctly" - exit 1 -fi -install -m 755 "$UV_BIN" /usr/local/bin/uv >/dev/null 2>&1 || true -UV_BIN="/usr/local/bin/uv" -msg_ok "Installed uv" - -msg_info "Installing Riven backend" -if [ ! -d /riven/src ]; then - git clone https://github.com/rivenmedia/riven.git /riven/src >/dev/null 2>&1 || { - msg_error "Failed to clone Riven backend repository" - exit 1 - } -else - cd /riven/src - git pull --rebase >/dev/null 2>&1 || true -fi -chown -R riven:riven /riven/src || true -cd /riven/src -# Ensure project virtual environment exists -if [ ! -d .venv ]; then - sudo -u riven -H "$UV_BIN" venv >/dev/null 2>&1 || { - msg_error "Failed to create Python virtual environment with uv" - exit 1 - } -fi - -VENV_PY_BIN="/riven/src/.venv/bin/python3" -if [ -x "$VENV_PY_BIN" ]; then - setcap cap_sys_admin+ep "$VENV_PY_BIN" 2>/dev/null || true -fi - -sudo -u riven -H "$UV_BIN" sync --no-dev --frozen >/dev/null 2>&1 || \ - sudo -u riven -H "$UV_BIN" sync --no-dev >/dev/null 2>&1 || { - msg_error "Failed to install Riven backend dependencies with uv" - exit 1 -} -chown -R riven:riven /riven -msg_ok "Installed Riven backend" - -msg_info "Configuring Riven backend environment" - -BACKEND_ENV="/etc/riven/backend.env" -FRONTEND_ENV="/etc/riven/frontend.env" -mkdir -p /etc/riven - -# Reuse existing API key if present to keep backend and frontend in sync -if [ -f "$BACKEND_ENV" ]; then - RIVEN_API_KEY=$(grep '^RIVEN_API_KEY=' "$BACKEND_ENV" | head -n1 | cut -d= -f2- || true) -fi -if [ -z "${RIVEN_API_KEY:-}" ]; then - RIVEN_API_KEY=$(openssl rand -hex 16) -fi - -if [ ! -f "$BACKEND_ENV" ]; then - cat <"$BACKEND_ENV" -RIVEN_API_KEY=$RIVEN_API_KEY -RIVEN_DATABASE_HOST=postgresql+psycopg2://postgres:postgres@127.0.0.1/riven -RIVEN_FILESYSTEM_MOUNT_PATH=/mount -RIVEN_UPDATERS_LIBRARY_PATH=/mnt/riven -RIVEN_FILESYSTEM_CACHE_DIR=/dev/shm/riven-cache -EOF - chown riven:riven "$BACKEND_ENV" - chmod 600 "$BACKEND_ENV" -fi -msg_ok "Configured Riven backend environment" - -msg_info "Creating systemd service for Riven backend" -cat <<'EOF' >/etc/systemd/system/riven-backend.service -[Unit] -Description=Riven Backend -After=network-online.target postgresql.service -Wants=network-online.target - -[Service] -Type=simple -User=riven -Group=riven -WorkingDirectory=/riven/src -EnvironmentFile=/etc/riven/backend.env -Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin -ExecStart=/usr/local/bin/uv run python src/main.py -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF - -systemctl daemon-reload -$STD systemctl enable riven-backend.service -$STD systemctl restart riven-backend.service -msg_ok "Created systemd service for Riven backend" - -if [ "$INSTALL_FRONTEND" = "yes" ]; then - msg_info "Installing Riven frontend" - if [ ! -d /opt/riven-frontend/.git ]; then - rm -rf /opt/riven-frontend - git clone https://github.com/rivenmedia/riven-frontend.git /opt/riven-frontend >/dev/null 2>&1 || { - msg_error "Failed to clone Riven frontend repository" - exit 1 - } - else - cd /opt/riven-frontend - git pull --rebase >/dev/null 2>&1 || true - fi - cd /opt/riven-frontend - if command -v pnpm >/dev/null 2>&1; then - if ! pnpm install >/dev/null 2>&1; then - msg_error "pnpm install failed while installing Riven frontend" - exit 1 - fi - if ! pnpm run build >/dev/null 2>&1; then - msg_error "pnpm run build failed while building Riven frontend" - exit 1 - fi - pnpm prune --prod >/dev/null 2>&1 || true - else - msg_error "pnpm is not available; cannot build Riven frontend" - exit 1 - fi - chown -R riven:riven /opt/riven-frontend - msg_ok "Installed Riven frontend" - - msg_info "Configuring Riven frontend environment" - AUTH_SECRET=$(openssl rand -base64 32) - - # If the host script provided a specific origin (e.g. a reverse proxy URL), use it. - # Otherwise, fall back to auto-detecting the CT's primary IPv4 and using :3000. - if [ -n "${RIVEN_FRONTEND_ORIGIN:-}" ]; then - FRONTEND_ORIGIN_DEFAULT="$RIVEN_FRONTEND_ORIGIN" - else - CT_IP=$(ip -4 -o addr show scope global 2>/dev/null | awk 'NR==1{print $4}' | cut -d/ -f1) - if [ -z "$CT_IP" ]; then - CT_IP="127.0.0.1" - fi - FRONTEND_ORIGIN_DEFAULT="http://$CT_IP:3000" - fi - - if [ ! -f "$FRONTEND_ENV" ]; then - cat <"$FRONTEND_ENV" -DATABASE_URL=/riven/data/riven.db -BACKEND_URL=http://127.0.0.1:8080 -BACKEND_API_KEY=$RIVEN_API_KEY -AUTH_SECRET=$AUTH_SECRET -ORIGIN=$FRONTEND_ORIGIN_DEFAULT -EOF - chown root:root "$FRONTEND_ENV" - chmod 600 "$FRONTEND_ENV" - fi - msg_ok "Configured Riven frontend environment" - - msg_info "Creating systemd service for Riven frontend" - cat <<'EOF' >/etc/systemd/system/riven-frontend.service -[Unit] -Description=Riven Frontend -After=network-online.target riven-backend.service -Wants=network-online.target - -[Service] -Type=simple -User=riven -Group=riven -WorkingDirectory=/opt/riven-frontend -EnvironmentFile=/etc/riven/frontend.env -ExecStart=ORIGIN=${ORIGIN} /usr/bin/node /opt/riven-frontend/build -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF - - systemctl daemon-reload - $STD systemctl enable riven-frontend.service - $STD systemctl restart riven-frontend.service - msg_ok "Created systemd service for Riven frontend" -else - msg_info "Skipping Riven frontend installation (disabled via installer)" - msg_ok "Only the Riven backend API was installed in this container" -fi - -motd_ssh -customize - -install_selected_media_servers - -msg_info "Cleaning up" -$STD apt-get -y autoremove -$STD apt-get -y autoclean -msg_ok "Cleaned" diff --git a/proxmox/riven.sh b/proxmox/riven.sh deleted file mode 100644 index 1ffd908..0000000 --- a/proxmox/riven.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash - -# Proxmox helper script to create a Riven LXC (Debian 12, unprivileged) - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -BUILD_FUNC_LOCAL="${SCRIPT_DIR}/build.func" -if [ -f "$BUILD_FUNC_LOCAL" ]; then - source "$BUILD_FUNC_LOCAL" -else - # Fallback to remote build.func when running directly via curl from GitHub - source <(curl -s https://raw.githubusercontent.com/rivenmedia/distributables/main/proxmox/build.func) -fi - -function header_info { -clear -cat <<'EOF' -.______ __ ____ ____ _______ .__ __. -| _ \ | | \ \ / / | ____|| \ | | -| |_) | | | \ \/ / | |__ | \| | -| / | | \ / | __| | . ` | -| |\ \----.| | \ / | |____ | |\ | -| _| `._____||__| \__/ |_______||__| \__| - -Riven LXC Helper -EOF -} - -header_info -echo -e "Loading..." - -APP="Riven" -var_disk="40" -var_cpu="4" -var_ram="8192" -var_os="debian" -var_version="12" - -variables -color -catch_errors - -function default_settings() { - CT_TYPE="1" - PW="" - CT_ID=$NEXTID - HN=$NSAPP - DISK_SIZE="$var_disk" - CORE_COUNT="$var_cpu" - RAM_SIZE="$var_ram" - BRG="vmbr0" - NET="dhcp" - GATE="" - APT_CACHER="" - APT_CACHER_IP="" - DISABLEIP6="no" - MTU="" - SD="" - NS="" - MAC="" - VLAN="" - SSH="no" - VERB="no" - RIVEN_INSTALL_FRONTEND="yes" - RIVEN_FRONTEND_ORIGIN="" - echo_default -} - -function update_script() { - msg_error "No ${APP} update script is available yet." - exit 1 -} - -start -build_container -description - -RIVEN_CT_ID="${CTID:-}" -if [ -z "$RIVEN_CT_ID" ]; then - RIVEN_CT_ID="" -fi - -RIVEN_CT_IP="" -if command -v pct >/dev/null 2>&1 && [ -n "${CTID:-}" ]; then - RIVEN_CT_IP=$(pct exec "$CTID" ip a s dev eth0 | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1) -fi -if [ -z "$RIVEN_CT_IP" ]; then - RIVEN_CT_IP="" -fi - -msg_ok "Completed Successfully!\n" - -echo -e "Riven container ID: ${BL}${RIVEN_CT_ID}${CL}" -echo -e "Riven container IP: ${BL}${RIVEN_CT_IP}${CL}\n" - -echo -e "${APP} backend (API) URL:" -echo -e " ${BL}http://${RIVEN_CT_IP}:8080/scalar${CL}\n" - -if [ "${RIVEN_INSTALL_FRONTEND:-yes}" != "no" ]; then - echo -e "${APP} frontend (web UI) URL:" - echo -e " ${BL}http://${RIVEN_CT_IP}:3000${CL}\n" -else - echo -e "${APP} frontend was ${RD}not installed${CL} in this container." - echo -e "You can host it elsewhere and point it at the backend URL above.\n" -fi - -# If the in-container installer recorded any media servers, show their -# default URLs here. The file is managed by proxmox/riven-install.sh. -MEDIA_SERVERS="" -if command -v pct >/dev/null 2>&1 && [ -n "${CTID:-}" ]; then - MEDIA_SERVERS="$(pct exec "$CTID" -- bash -c 'if [ -f /etc/riven/media-servers.txt ]; then cat /etc/riven/media-servers.txt; fi' 2>/dev/null || true)" -fi - -if [ -n "$MEDIA_SERVERS" ]; then - MEDIA_SERVERS_ONELINE=$(printf '%s' "$MEDIA_SERVERS" | tr '\n' ' ' | sed -e 's/[[:space:]]\+$//') - echo -e "Optional media servers installed in this container: ${BL}${MEDIA_SERVERS_ONELINE}${CL}\n" - while IFS= read -r srv; do - case "$srv" in - plex) - echo -e " Plex: ${BL}http://${RIVEN_CT_IP}:32400/web${CL}" - ;; - jellyfin) - echo -e " Jellyfin: ${BL}http://${RIVEN_CT_IP}:8096${CL}" - ;; - emby) - echo -e " Emby: ${BL}http://${RIVEN_CT_IP}:8097${CL}" - ;; - *) - ;; - esac - done <<<"$MEDIA_SERVERS" - echo -else - echo -e "No optional media servers were selected for this container.\n" -fi - -echo -e "Backend settings file inside the container:" -echo -e " ${BL}/riven/src/data/settings.json${CL}\n" diff --git a/ubuntu/docker-compose.media.yml b/ubuntu/docker-compose.media.yml new file mode 100644 index 0000000..19436b2 --- /dev/null +++ b/ubuntu/docker-compose.media.yml @@ -0,0 +1,45 @@ +services: + jellyfin: + image: jellyfin/jellyfin + container_name: jellyfin + profiles: ["jellyfin"] + restart: unless-stopped + ports: + - "8096:8096" + volumes: + - /mnt/riven/mount:/media:ro + - /mnt/jellyfin:/config + networks: [media] + + plex: + image: lscr.io/linuxserver/plex + container_name: plex + profiles: ["plex"] + restart: unless-stopped + ports: + - "32400:32400" + environment: + PUID: 1000 + PGID: 1000 + VERSION: docker + volumes: + - /mnt/riven/mount:/media:ro + - /mnt/plex:/config + networks: [media] + + emby: + image: emby/embyserver + container_name: emby + profiles: ["emby"] + restart: unless-stopped + ports: + - "8097:8096" + volumes: + - /mnt/riven/mount:/media:ro + - /mnt/emby:/config + networks: [media] + +networks: + media: + name: media + driver: bridge diff --git a/ubuntu/docker-compose.yml b/ubuntu/docker-compose.yml new file mode 100644 index 0000000..086989f --- /dev/null +++ b/ubuntu/docker-compose.yml @@ -0,0 +1,153 @@ +services: + riven-db: + image: postgres:17-alpine + container_name: riven-db + restart: unless-stopped + env_file: [.env] + environment: + PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - riven-pg-data:/var/lib/postgresql/data/pgdata + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - media + + riven: + image: spoked/riven:dev + container_name: riven + restart: unless-stopped + shm_size: 1024m + ports: + - "8080:8080" + tty: true + cap_add: + - SYS_ADMIN + security_opt: + - apparmor:unconfined + devices: + - /dev/fuse + env_file: [.env] + environment: + # ========================= + # CORE / RUNTIME + # ========================= + TZ: ${TZ} + PUID: 1000 + PGID: 1000 + RIVEN_FORCE_ENV: "true" + RIVEN_API_KEY: ${BACKEND_API_KEY} + + # ========================= + # DATABASE + # ========================= + RIVEN_DATABASE_HOST: postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@riven-db:5432/${POSTGRES_DB} + + # ========================= + # FILESYSTEM + # ========================= + RIVEN_FILESYSTEM_MOUNT_PATH: /mount + RIVEN_FILESYSTEM_CACHE_DIR: /dev/shm/riven-cache + + # ========================= + # UPDATERS — CORE + # ========================= + RIVEN_UPDATERS_LIBRARY_PATH: ${RIVEN_UPDATERS_LIBRARY_PATH} + RIVEN_UPDATERS_UPDATER_INTERVAL: ${RIVEN_UPDATERS_UPDATER_INTERVAL} + + # ========================= + # UPDATERS — JELLYFIN + # ========================= + RIVEN_UPDATERS_JELLYFIN_ENABLED: ${RIVEN_UPDATERS_JELLYFIN_ENABLED} + RIVEN_UPDATERS_JELLYFIN_URL: ${RIVEN_UPDATERS_JELLYFIN_URL} + RIVEN_UPDATERS_JELLYFIN_API_KEY: ${RIVEN_UPDATERS_JELLYFIN_API_KEY} + + # ========================= + # UPDATERS — PLEX + # ========================= + RIVEN_UPDATERS_PLEX_ENABLED: ${RIVEN_UPDATERS_PLEX_ENABLED} + RIVEN_UPDATERS_PLEX_URL: ${RIVEN_UPDATERS_PLEX_URL} + RIVEN_UPDATERS_PLEX_TOKEN: ${RIVEN_UPDATERS_PLEX_TOKEN} + + # ========================= + # UPDATERS — EMBY + # ========================= + RIVEN_UPDATERS_EMBY_ENABLED: ${RIVEN_UPDATERS_EMBY_ENABLED} + RIVEN_UPDATERS_EMBY_URL: ${RIVEN_UPDATERS_EMBY_URL} + RIVEN_UPDATERS_EMBY_API_KEY: ${RIVEN_UPDATERS_EMBY_API_KEY} + + # ========================= + # DOWNLOADERS + # ========================= + RIVEN_DOWNLOADERS_REAL_DEBRID_ENABLED: ${RIVEN_DOWNLOADERS_REAL_DEBRID_ENABLED} + RIVEN_DOWNLOADERS_REAL_DEBRID_API_KEY: ${RIVEN_DOWNLOADERS_REAL_DEBRID_API_KEY} + + RIVEN_DOWNLOADERS_ALL_DEBRID_ENABLED: ${RIVEN_DOWNLOADERS_ALL_DEBRID_ENABLED} + RIVEN_DOWNLOADERS_ALL_DEBRID_API_KEY: ${RIVEN_DOWNLOADERS_ALL_DEBRID_API_KEY} + + RIVEN_DOWNLOADERS_DEBRID_LINK_ENABLED: ${RIVEN_DOWNLOADERS_DEBRID_LINK_ENABLED} + RIVEN_DOWNLOADERS_DEBRID_LINK_API_KEY: ${RIVEN_DOWNLOADERS_DEBRID_LINK_API_KEY} + + # ========================= + # SCRAPERS + # ========================= + RIVEN_SCRAPING_TORRENTIO_ENABLED: ${RIVEN_SCRAPING_TORRENTIO_ENABLED} + + RIVEN_SCRAPING_PROWLARR_ENABLED: ${RIVEN_SCRAPING_PROWLARR_ENABLED} + RIVEN_SCRAPING_PROWLARR_URL: ${RIVEN_SCRAPING_PROWLARR_URL} + RIVEN_SCRAPING_PROWLARR_API_KEY: ${RIVEN_SCRAPING_PROWLARR_API_KEY} + + RIVEN_SCRAPING_ZILEAN_ENABLED: ${RIVEN_SCRAPING_ZILEAN_ENABLED} + RIVEN_SCRAPING_ZILEAN_URL: ${RIVEN_SCRAPING_ZILEAN_URL} + + RIVEN_SCRAPING_COMET_ENABLED: ${RIVEN_SCRAPING_COMET_ENABLED} + RIVEN_SCRAPING_COMET_URL: ${RIVEN_SCRAPING_COMET_URL} + + RIVEN_SCRAPING_JACKETT_ENABLED: ${RIVEN_SCRAPING_JACKETT_ENABLED} + RIVEN_SCRAPING_JACKETT_URL: ${RIVEN_SCRAPING_JACKETT_URL} + RIVEN_SCRAPING_JACKETT_API_KEY: ${RIVEN_SCRAPING_JACKETT_API_KEY} + + volumes: + - /mnt/riven/backend:/riven/data + - /mnt/riven/mount:/mount:rshared,z + + depends_on: + riven-db: + condition: service_healthy + + networks: + - media + + riven-frontend: + image: spoked/riven-frontend:dev + container_name: riven-frontend + restart: unless-stopped + ports: + - "3000:3000" + env_file: [.env] + environment: + TZ: ${TZ} + BACKEND_URL: http://riven:8080 + BACKEND_API_KEY: ${BACKEND_API_KEY} + AUTH_SECRET: ${AUTH_SECRET} + ORIGIN: ${ORIGIN} + volumes: + - riven-frontend-data:/riven/data + + networks: + - media + +volumes: + riven-frontend-data: + riven-pg-data: + +networks: + media: + name: media + driver: bridge diff --git a/ubuntu/install.sh b/ubuntu/install.sh new file mode 100644 index 0000000..9fa96d9 --- /dev/null +++ b/ubuntu/install.sh @@ -0,0 +1,1050 @@ +#!/usr/bin/env bash +set -euo pipefail + +DEBUG_MODE=false +if [[ "${1:-}" == "--debug" ]]; then + DEBUG_MODE=true + shift +fi + +############################################ +# CONSTANTS +############################################ +INSTALL_DIR="/opt/riven" +BACKEND_PATH="/mnt/riven/backend" +MOUNT_PATH="/mnt/riven/mount" +LOG_DIR="/tmp/logs/riven" + +MEDIA_COMPOSE_URL="https://raw.githubusercontent.com/AquaHorizonGaming/distributables/main/ubuntu/docker-compose.media.yml" +RIVEN_COMPOSE_URL="https://raw.githubusercontent.com/AquaHorizonGaming/distributables/main/ubuntu/docker-compose.yml" + +DEFAULT_ORIGIN="http://localhost:3000" + +INSTALL_VERSION="v0.6.8" + +############################################ +# HELPERS +############################################ +banner(){ echo -e "\n========================================\n $1\n========================================"; } +ok() { printf "✔ %s\n" "$1"; } +warn() { printf "⚠ %s\n" "$1"; } +fail() { printf "✖ %s\n" "$1"; exit 1; } + +# Capture whether the original stdout is a terminal before logging redirection. +exec 3>&1 +ORIGINAL_STDOUT_IS_TTY=false +[[ -t 3 ]] && ORIGINAL_STDOUT_IS_TTY=true + +run_docker_compose_up_detached() { + local -a compose_cmd=("$@") + + if [[ "$ORIGINAL_STDOUT_IS_TTY" == "true" ]]; then + # Preserve TTY behavior so pull progress updates in-place. + "${compose_cmd[@]}" up -d --pull always 1>&3 2>&3 + else + # Keep normal output in CI / non-interactive shells. + "${compose_cmd[@]}" up -d --pull always + fi +} + +############################################ +# REQUIRED NON-EMPTY (SILENT) +# (keep for non-secret values if needed) +############################################ +require_non_empty() { + local prompt="$1" val + while true; do + IFS= read -r -p "$prompt: " val + val="$(printf '%s' "$val" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [[ -n "$val" ]] && { printf '%s' "$val"; return; } + warn "Value required" + done +} + +############################################ +# REQUIRED NON-EMPTY (MASKED ****) +# For API keys / tokens / secrets +############################################ +read_masked_non_empty() { + local prompt="$1" + local val="" char + + while true; do + val="" + printf "%s: " "$prompt" + + while IFS= read -r -s -n1 char; do + [[ $char == $'\n' ]] && break + + # Handle backspace + if [[ $char == $'\177' ]]; then + if [[ -n "$val" ]]; then + val="${val%?}" + printf '\b \b' + fi + continue + fi + + val+="$char" + printf '*' + done + + echo + + # Trim whitespace + val="$(printf '%s' "$val" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + + [[ -n "$val" ]] && { printf '%s' "$val"; return; } + warn "Value required" + done +} + +############################################ +# URL VALIDATION +############################################ +require_url() { + local prompt="$1" val + while true; do + IFS= read -r -p "$prompt: " val + val="$(printf '%s' "$val" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [[ "$val" =~ ^https?:// ]] && { printf '%s' "$val"; return; } + warn "Must include http:// or https://" >&2 + done +} + +sanitize() { + printf "%s" "$1" | tr -d '\r\n' +} + +############################################ +# OS CHECK (Ubuntu only, WSL warned) +############################################ +banner "OS Check" + +require_ubuntu() { + # Must be Linux + if [[ "$(uname -s)" != "Linux" ]]; then + fail "This installer must be run on Ubuntu Linux. Detected: $(uname -s)" + fi + + # Detect WSL + if grep -qi microsoft /proc/version 2>/dev/null; then + warn "WSL detected — this is not recommended" + read -rp "Continue anyway? [y/N]: " yn + [[ "${yn:-}" =~ ^[Yy]$ ]] || exit 1 + fi + + # Must have os-release + if [[ ! -f /etc/os-release ]]; then + fail "Cannot determine OS (missing /etc/os-release)" + fi + + # Must be Ubuntu + . /etc/os-release + + if [[ "${ID:-}" != "ubuntu" ]]; then + fail "Unsupported OS: ${PRETTY_NAME:-unknown}. Ubuntu required." + fi + + ok "Ubuntu detected (${PRETTY_NAME})" +} + +require_ubuntu + +############################################ +# ROOT CHECK +############################################ +[[ "$(id -u)" -eq 0 ]] || fail "Run with sudo" + +############################################ +# INSTALLER VERSION +############################################ +banner "Version" + +print_installer_version() { + : "${INSTALL_VERSION:=unknown}" + ok "Installer version: ${INSTALL_VERSION}" +} + +print_installer_version + + +############################################ +# LOGGING MODULE +############################################ +banner "Logging" + +LOG_FILE="$LOG_DIR/install-$(date +%Y%m%d-%H%M%S).log" + +mkdir -p "$LOG_DIR" +touch "$LOG_FILE" + +# Mirror stdout + stderr to terminal AND log +exec > >(tee -a "$LOG_FILE") 2>&1 + +log() { echo "[INFO] $*"; } +log_warn() { echo "[WARN] $*"; } +log_error() { echo "[ERROR] $*"; } +log_section(){ echo -e "\n========== $* ==========\n"; } + +if [[ "$DEBUG_MODE" == "true" ]]; then + export PS4='+ [${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}] ' + set -x + debug "Debug mode enabled" + debug "original_stdout_is_tty=$ORIGINAL_STDOUT_IS_TTY" +fi + +trap 'rc=$?; cmd=${BASH_COMMAND:-unknown}; log_error "Installer exited unexpectedly at line $LINENO (rc=$rc, cmd: $cmd)"; debug "failure_context rc=$rc cmd=$cmd"; exit $rc' ERR + +log "Logging initialized" +log "Log file: $LOG_FILE" + +############################################ +# TIMEZONE (INSTALLER SAFE) +############################################ +banner "Timezone" +debug_step "timezone detection and configuration" + +detect_timezone() { + timedatectl show --property=Timezone --value 2>/dev/null \ + || cat /etc/timezone 2>/dev/null \ + || echo UTC +} + +TZ_DETECTED="$(detect_timezone)" +read -rp "Timezone [$TZ_DETECTED]: " TZ_INPUT +TZ_SELECTED="${TZ_INPUT:-$TZ_DETECTED}" + +if [[ ! -f "/usr/share/zoneinfo/$TZ_SELECTED" ]]; then + fail "Invalid timezone: $TZ_SELECTED" +fi + +ln -sf "/usr/share/zoneinfo/$TZ_SELECTED" /etc/localtime +echo "$TZ_SELECTED" > /etc/timezone + +ok "Timezone set: $TZ_SELECTED" + +############################################ +# SYSTEM DEPS +############################################ +banner "System Dependencies" +debug_step "system dependency verification" + +dpkg -s ca-certificates curl gnupg lsb-release openssl fuse3 >/dev/null 2>&1 \ + && ok "System dependencies already installed" \ + || { + apt-get update || fail "apt update failed" + apt-get install -y ca-certificates curl gnupg lsb-release openssl fuse3 \ + || fail "dependency install failed" + ok "System dependencies installed" + } + + +############################################ +# USER / UID / GID DETECTION +############################################ +banner "UserDetect" + +detect_uid_gid() { + # Prefer the sudo user if present + if [[ -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then + TARGET_UID="$(id -u "$SUDO_USER")" + TARGET_GID="$(id -g "$SUDO_USER")" + return + fi + + # Fallback: first non-root user with UID >= 1000 + local user + user="$(awk -F: '$3>=1000 && $3<65534 {print $1; exit}' /etc/passwd)" + + if [[ -n "$user" ]]; then + TARGET_UID="$(id -u "$user")" + TARGET_GID="$(id -g "$user")" + return + fi + + # Absolute fallback + TARGET_UID=1000 + TARGET_GID=1000 +} + +detect_uid_gid + +ok "Detected user ownership: UID=$TARGET_UID GID=$TARGET_GID" + +############################################ +# DOCKER +############################################ +banner "Docker" +debug_step "docker installation check" + +if command -v docker >/dev/null 2>&1; then + ok "Docker already installed" +else + echo "[*] Installing Docker — this may take several minutes depending on your connection..." + curl -fsSL https://get.docker.com | sh + systemctl enable --now docker + ok "Docker installed" +fi + +############################################ +# DOCKER GROUP / USER PERMISSIONS +############################################ +banner "DockerGroup" + +setup_docker_group() { + # Ensure docker group exists + if ! getent group docker >/dev/null 2>&1; then + groupadd docker || fail "Failed to create docker group" + ok "Docker group created" + else + ok "Docker group already exists" + fi + + # Determine target user + local user="" + if [[ -n "${SUDO_USER:-}" && "$SUDO_USER" != "root" ]]; then + user="$SUDO_USER" + else + user="$(awk -F: '$3>=1000 && $3<65534 {print $1; exit}' /etc/passwd)" + fi + + if [[ -z "$user" ]]; then + warn "No non-root user found to add to docker group" + return + fi + + # Add user to docker group if not already a member + if id -nG "$user" | grep -qw docker; then + ok "User '$user' already in docker group" + else + usermod -aG docker "$user" || fail "Failed to add $user to docker group" + ok "User '$user' added to docker group" + warn "Log out and back in for Docker permissions to apply" + fi +} + +setup_docker_group + +############################################ +# FILESYSTEM +############################################ +banner "Filesystem" + +mkdir -p "$BACKEND_PATH" "$MOUNT_PATH" "$INSTALL_DIR" + +chown "$TARGET_UID:$TARGET_GID" "$BACKEND_PATH" "$MOUNT_PATH" \ + || fail "Failed to chown backend or mount path" + +chown "$TARGET_UID:$TARGET_GID" "$INSTALL_DIR" \ + || fail "Failed to chown install dir" + +ok "Filesystem ready (owner: $TARGET_UID:$TARGET_GID)" + + +############################################ +# RIVEN rshared MOUNT MODULE (REQUIRED) +############################################ +ensure_riven_rshared_mount() { + local MOUNT_PATH="/mnt/riven/mount" + local SERVICE_NAME="riven-bind-shared.service" + + banner "Ensuring rshared mount for Riven" + + mkdir -p "$MOUNT_PATH" + + # If already shared, do nothing + if findmnt -no PROPAGATION "$MOUNT_PATH" 2>/dev/null | grep -q shared; then + ok "Mount already rshared" + return + fi + + warn "Mount is not rshared — installing systemd unit" + + cat >/etc/systemd/system/$SERVICE_NAME <" + + RIVEN_SCRAPING_COMET_URL="$(require_url "Enter Comet base URL")" + + log "Comet enabled" + ;; + 4) + RIVEN_SCRAPING_JACKETT_ENABLED=true + VALID_SELECTION=true + + echo "" + echo "Jackett configuration" + echo "Example: http://localhost:9117" + echo "API Key: Jackett Web UI → Top-right corner" + + RIVEN_SCRAPING_JACKETT_URL="$(require_url "Enter Jackett URL")" + RIVEN_SCRAPING_JACKETT_API_KEY="$(read_masked_non_empty "Enter Jackett API Key")" + + log "Jackett enabled" + ;; + 5) + RIVEN_SCRAPING_ZILEAN_ENABLED=true + VALID_SELECTION=true + + echo "" + echo "Zilean configuration" + echo "Examples:" + echo " • https://zilean.example.com" + echo " • http://localhost:" + + RIVEN_SCRAPING_ZILEAN_URL="$(require_url "Enter Zilean base URL")" + + log "Zilean enabled" + ;; + *) + warn "Invalid scraper option ignored: $sel" + ;; + esac +done + +if [[ "$VALID_SELECTION" != "true" ]]; then + fail "At least one scraper must be selected" +fi + +echo "" +echo "Enabled scrapers:" +[[ "$RIVEN_SCRAPING_TORRENTIO_ENABLED" == "true" ]] && echo " • Torrentio" +[[ "$RIVEN_SCRAPING_PROWLARR_ENABLED" == "true" ]] && echo " • Prowlarr" +[[ "$RIVEN_SCRAPING_COMET_ENABLED" == "true" ]] && echo " • Comet" +[[ "$RIVEN_SCRAPING_JACKETT_ENABLED" == "true" ]] && echo " • Jackett" +[[ "$RIVEN_SCRAPING_ZILEAN_ENABLED" == "true" ]] && echo " • Zilean" +echo "" + +############################################ +# SECRETS +############################################ +POSTGRES_PASSWORD="$(openssl rand -hex 24)" +AUTH_SECRET="$(openssl rand -base64 32)" + + +############################################ +# RIVEN API KEY MODULE +# Order: Generate → Validate → Continue +############################################ + +# ------------------------------------------ +# PART 1: Generate API key +# ------------------------------------------ +# Generate backend API key safely under pipefail +set +o pipefail +BACKEND_API_KEY="$(tr -dc 'A-Za-z0-9' .env < .env.fixed + +mv .env.fixed .env + +ok ".env repaired and sanitized" + + +############################################ +# START RIVEN +############################################ +banner "Starting Riven" +run_docker_compose_up_detached docker compose +ok "Riven started" + +banner "INSTALL COMPLETE" + +############################################ +# INSTALL SUMMARY MODULE +############################################ +banner "Riven Installation Summary" + +echo "📁 Paths" +echo " • Install Dir: $INSTALL_DIR" +echo " • Backend Path: $BACKEND_PATH" +echo " • Mount Path: $MOUNT_PATH" +echo + +echo "👤 Ownership" +echo " • UID:GID $TARGET_UID:$TARGET_GID" +echo + +echo "🌍 Frontend" +echo " • ORIGIN: $ORIGIN" +echo + +echo "🎬 Media Server" +echo " • Selected: $MEDIA_PROFILE" +echo " • URL: http://$SERVER_IP:$MEDIA_PORT" +echo " • Updater Enabled: $( + case "$MEDIA_PROFILE" in + jellyfin) echo "$RIVEN_UPDATERS_JELLYFIN_ENABLED" ;; + plex) echo "$RIVEN_UPDATERS_PLEX_ENABLED" ;; + emby) echo "$RIVEN_UPDATERS_EMBY_ENABLED" ;; + esac +)" +echo + +echo "⬇️ Downloader" +if [[ "$RIVEN_DOWNLOADERS_REAL_DEBRID_ENABLED" == "true" ]]; then + echo " • Real-Debrid (enabled)" +elif [[ "$RIVEN_DOWNLOADERS_ALL_DEBRID_ENABLED" == "true" ]]; then + echo " • All-Debrid (enabled)" +elif [[ "$RIVEN_DOWNLOADERS_DEBRID_LINK_ENABLED" == "true" ]]; then + echo " • Debrid-Link (enabled)" +else + echo " • NONE (❌ invalid state)" +fi +echo + +echo "🔍 Scraper" +if [[ "$RIVEN_SCRAPING_TORRENTIO_ENABLED" == "true" ]]; then + echo " • Torrentio" +elif [[ "$RIVEN_SCRAPING_PROWLARR_ENABLED" == "true" ]]; then + echo " • Prowlarr ($RIVEN_SCRAPING_PROWLARR_URL)" +else + echo " • NONE (❌ invalid state)" +fi +echo + +echo "🗄️ Database" +echo " • Postgres DB: riven" +echo " • User: postgres" +echo +echo " • POSTGRES PASSWORD: $POSTGRES_PASSWORD" +echo " • BACKEND API KEY: $BACKEND_API_KEY" +echo " • AUTH SECRET: $AUTH_SECRET" + + + +echo "🐳 Docker" +echo " • Media Compose: $INSTALL_DIR/docker-compose.media.yml" +echo " • Riven Compose: $INSTALL_DIR/docker-compose.yml" +echo " • Media Profile: $MEDIA_PROFILE" +echo + +echo "📦 Environment" +echo " • .env Location: $INSTALL_DIR/.env" +echo " • Permissions: 600" +echo + +echo "🎥 Media Server" +echo "➡️ Open your media server in a browser:" +echo "👉 http://$SERVER_IP:$MEDIA_PORT" + +echo "🧠 Notes" +echo " • rshared mount enforced via systemd" +echo " • Media server started first" +echo " • Riven started after config complete" +echo + +ok "Riven is ready 🚀" diff --git a/ubuntu/readme.md b/ubuntu/readme.md new file mode 100644 index 0000000..f7efb11 --- /dev/null +++ b/ubuntu/readme.md @@ -0,0 +1,272 @@ +## 📚 Table of Contents + +- [Install Riven](#installer) +- [Recover Riven Mount & Restart Media](#remount) +- [Uninstall Riven](#uninstaller) +- [Update Riven](#updater) + +--- + +## ▶️ How to run the installer (Ubuntu Script) + +Run this command on Ubuntu: + + sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/riven-scripts/main/ubuntu/install.sh)" + + +# 🔁 Riven Ubuntu Installer +# Riven Ubuntu Installer + +This installer deploys **Riven** on Ubuntu using Docker and Docker Compose with a fully interactive guided setup. + +--- + +## SUPPORTED SYSTEMS + +- Ubuntu Server +- Ubuntu Desktop +- Virtual Machines +- Headless servers +- Advanced WSL setups + +--- + +## WHAT THIS SCRIPT DOES + +### SYSTEM & DOCKER +- Installs Docker and Docker Compose ONLY if missing +- Configures Docker to use IPv4 only (IPv6 disabled inside Docker only) +- Sets reliable DNS defaults for containers + +--- + +### FILESYSTEM & MOUNTS + +Creates and manages the following paths: + + /opt/riven + ├─ docker-compose.yml + └─ .env + + /mnt/riven/backend + ├─ Riven backend data + └─ settings.json (auto-generated) + + /mnt/riven/mount + └─ Media library (movies, TV, anime) + +- Configures a systemd mount service +- Ensures /mnt/riven/mount is bind-mounted as rshared +- This behavior is REQUIRED for Riven to function + +--- + +## RIVEN DEPLOYMENT (AUTOMATED) + +The installer performs a fully interactive configuration. + +### DURING INSTALL YOU WILL BE PROMPTED TO: +- Select a Downloader (e.g. Real-Debrid) +- Select a Scraper +- Select a Media Server: + - Plex + - Jellyfin + - Emby +- Enter required API keys / tokens + +### THE SCRIPT WILL: +- Generate a secure `.env` file +- Download the `docker-compose.yml` +- Pull all required container images +- Start containers with retry logic +- Verify that all services are running +- Use the `.env` file to pass configuration into the containers (mapped to `settings.json`) + +NO manual configuration is required after install. + +--- + +## IMPORTANT CHANGE + +OLD BEHAVIOR: +- Manual editing of /mnt/riven/backend/settings.json was required + +NEW BEHAVIOR: +- All configuration is handled during the installer +- Riven starts fully configured +- Scraping and media integration work immediately + +--- + +## ACCESSING THE FRONTEND + +After installation completes, the script prints: + + http://:3000 + Or + http:// + +--- + +## IMPORTANT PATHS + +- Docker Compose: /opt/riven/docker-compose.yml +- Environment file: /opt/riven/.env +- Backend config: /mnt/riven/backend/settings.json +- Media library: /mnt/riven/mount + +--- + +## TROUBLESHOOTING + +Check running containers: + + docker ps + +Restart everything: + + cd /opt/riven + docker compose down + docker compose up -d + +View backend logs: + + docker logs riven + +--- + + +## 🔁 Riven Mount Recovery & Media Restart Tool + +This utility safely **resets the Riven mount and restarts media services** without reinstalling or reconfiguring anything. + +It is designed for situations where: +- The Riven mount becomes stale +- Media servers see empty libraries +- FUSE/bind mounts fail to release cleanly +- You need to safely cycle storage without rebooting + +--- + +### ▶️ Run this command on Ubuntu + + sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/riven-scripts/main/ubuntu/riven-remount-cycle.sh)" + +--- + +### WHAT THIS SCRIPT DOES + +- Stops the **Riven Docker container** +- Stops the selected **media server container** +- Actively unmounts the Riven mount path +- Verifies the mount is **fully released** +- Re-attempts unmounting until the kernel confirms it is gone +- Restarts the Riven container +- Waits for the mount to become available +- Starts the media server +- Restarts the media server **after mount stabilization** + +--- + +### INTERACTIVE PROMPTS + +During execution, you will be asked to: + +- Confirm or change the mount path + - Default: `/mnt/riven/mount` +- Select your media server: + - Plex + - Jellyfin + - Emby + - Custom container name + +No configuration files are modified. + +--- + +### IMPORTANT NOTES + +- This script **must be run with sudo** +- Safe to run multiple times +- Does **not** remove data +- Does **not** change `.env` or settings +- Does **not** reinstall containers +- Designed for production systems + +--- + +### WHEN TO USE THIS + +Use this tool if: +- Media libraries disappear unexpectedly +- Riven appears running but media sees no files +- Mounts do not release after stopping containers +- You want a clean mount reset without rebooting + +--- + +### WHEN NOT TO USE THIS + +Do **not** use this script to: +- Install Riven +- Update Riven +- Change configuration +- Replace the installer or updater + +--- + +## ✔️ SAFE RECOVERY COMPLETE + +If the script completes without errors: +- The mount is healthy +- Media servers are correctly attached +- No further action is required + +--- + + +## 🗑️ Riven Ubuntu Uninstaller + +This command **completely removes Riven and all related components** installed by the Riven Ubuntu installer. + +--- + +### ▶️ Run this command on Ubuntu + + sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/riven-scripts/main/ubuntu/riven-uninstall.sh)" + +--- + +### ⚠️ What this removes + +- Riven containers +- Media containers (Jellyfin / Plex / Emby) +- Docker volumes created by Riven +- Riven systemd mount service +- `/opt/riven` +- `/mnt/riven/backend` +- `/mnt/riven/mount` +- `/mnt/riven` (if empty) +- Riven installer logs + +> Docker itself is **preserved by default** (you will be prompted). + +--- + + +## 🔁 Riven Ubuntu Updater + +### ▶️ Run this command on Ubuntu + + sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/AquaHorizonGaming/riven-scripts/main/ubuntu/riven-update.sh)" + +This command updates **Riven** to the latest available Docker images and optionally updates the configured **media server**. + +The updater is **safe by default** and does **not** remove: +- Volumes +- Bind mounts +- Configuration files +- `.env` +- Media libraries + +--- \ No newline at end of file diff --git a/ubuntu/riven-remount-cycle.sh b/ubuntu/riven-remount-cycle.sh new file mode 100644 index 0000000..7db9c1d --- /dev/null +++ b/ubuntu/riven-remount-cycle.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################ +# REQUIRE ROOT +############################################ +if [[ "$EUID" -ne 0 ]]; then + echo "❌ Must be run as root" + exit 1 +fi + +############################################ +# DEFAULTS +############################################ +RIVEN_CONTAINER="riven" +DEFAULT_MOUNT="/mnt/riven/mount" + +UNMOUNT_RETRIES=3 +WAIT_BETWEEN=2 +REMOUNT_WAIT=30 +WAIT_TIME=5 +VERSION=3.0 + +############################################ +# OUTPUT HELPERS +############################################ +section() { echo -e "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; echo "▶ $1"; } +ok() { echo "✔ $1"; } +warn() { echo "⚠ $1"; } +fail() { echo "✖ $1"; exit 1; } + +is_mounted() { + findmnt -T "$MOUNT_PATH" >/dev/null 2>&1 +} + +############################################ +# VERSION MODULE +############################################ +show_version() { + section "Riven Bind-Remount Cycle" + ok "Version: v$VERSION" +} + +show_version + +############################################ +# MOUNT PATH PROMPT +############################################ +section "Mount Configuration" +read -rp "Mount path [${DEFAULT_MOUNT}]: " MOUNT_PATH +MOUNT_PATH="${MOUNT_PATH:-$DEFAULT_MOUNT}" +ok "Using mount path: $MOUNT_PATH" + +############################################ +# MEDIA RUNTIME SELECTION +############################################ +section "Media Server Runtime" +echo "1) Docker" +echo "2) Systemd service" +read -rp "Choice [1-2]: " MEDIA_RUNTIME + +case "$MEDIA_RUNTIME" in + 1) + MEDIA_MODE="docker" + ok "Media server will be controlled via Docker" + ;; + 2) + MEDIA_MODE="systemd" + ok "Media server will be controlled via systemd" + ;; + *) + fail "Invalid runtime selection" + ;; +esac + +############################################ +# MEDIA SERVER SELECTION +############################################ +section "Media Server Selection" +echo "1) Plex" +echo "2) Jellyfin" +echo "3) Emby" +echo "4) Custom name" +read -rp "Choice [1-4]: " MEDIA_CHOICE + +case "$MEDIA_CHOICE" in + 1) MEDIA_NAME="plex" ;; + 2) MEDIA_NAME="jellyfin" ;; + 3) MEDIA_NAME="emby" ;; + 4) read -rp "Enter media name: " MEDIA_NAME ;; + *) fail "Invalid selection" ;; +esac + +ok "Selected media server: $MEDIA_NAME" + +############################################ +# MEDIA TARGET RESOLUTION +############################################ +if [[ "$MEDIA_MODE" == "docker" ]]; then + MEDIA_CONTAINER="$MEDIA_NAME" + ok "Using Docker container: $MEDIA_CONTAINER" +else + case "$MEDIA_NAME" in + plex) MEDIA_SERVICE="plexmediaserver" ;; + jellyfin) MEDIA_SERVICE="jellyfin" ;; + emby) MEDIA_SERVICE="emby-server" ;; + *) + read -rp "Enter systemd service name: " MEDIA_SERVICE + ;; + esac + ok "Using systemd service: $MEDIA_SERVICE" +fi + +############################################ +# STOP SERVICES +############################################ +section "Stopping Services" + +docker stop "$RIVEN_CONTAINER" >/dev/null 2>&1 || true +ok "Riven container stopped" + +if [[ "$MEDIA_MODE" == "docker" ]]; then + docker stop "$MEDIA_CONTAINER" >/dev/null 2>&1 || true + ok "Media container stopped" +else + systemctl stop "$MEDIA_SERVICE" + ok "Media service stopped" +fi + +############################################ +# UNMOUNT +############################################ +section "Unmounting Mount Path" + +for attempt in $(seq 1 $UNMOUNT_RETRIES); do + if ! is_mounted; then + ok "Mount is already unmounted" + break + fi + + warn "Unmount attempt $attempt" + + if umount "$MOUNT_PATH" 2>&1 | grep -q "not mounted"; then + ok "Mount was already unmounted" + break + fi + + sleep "$WAIT_BETWEEN" + + if ! is_mounted; then + ok "Mount successfully unmounted" + break + fi +done + +############################################ +# REMOUNT (BIND + RSHARED) +############################################ +section "Re-establishing Mount" + +mount --bind "$MOUNT_PATH" "$MOUNT_PATH" +ok "Bind mount created" + +mount --make-rshared "$MOUNT_PATH" +ok "Mount marked as rshared" + +############################################ +# VERIFY PROPAGATION +############################################ +section "Verifying Propagation" + +findmnt -T "$MOUNT_PATH" -o TARGET,PROPAGATION + +PROP=$(findmnt -T "$MOUNT_PATH" -o PROPAGATION -n) +if [[ "$PROP" != "shared" && "$PROP" != "rshared" ]]; then + fail "Propagation incorrect: $PROP" +fi + +ok "Propagation verified: $PROP" + +############################################ +# START SERVICES +############################################ +section "Starting Services" + +# 1. Start media server +if [[ "$MEDIA_MODE" == "docker" ]]; then + docker start "$MEDIA_CONTAINER" >/dev/null + ok "Media container started" +else + systemctl start "$MEDIA_SERVICE" + ok "Media service started" +fi + +# 2. Wait before starting Riven +sleep 5 + +# 3. Start Riven +docker start "$RIVEN_CONTAINER" >/dev/null +ok "Riven container started" + +# 4. Wait for mount propagation +sleep 30 + +# 5. Restart media server (post-mount) +if [[ "$MEDIA_MODE" == "docker" ]]; then + docker restart "$MEDIA_CONTAINER" >/dev/null + ok "Media container restarted (post-mount)" +else + systemctl restart "$MEDIA_SERVICE" + ok "Media service restarted (post-mount)" +fi + + +############################################ +# DONE +############################################ +section "Complete" +ok "Riven bind-remount cycle finished successfully" diff --git a/ubuntu/riven-uninstall.sh b/ubuntu/riven-uninstall.sh new file mode 100644 index 0000000..c892462 --- /dev/null +++ b/ubuntu/riven-uninstall.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################ +# CONSTANTS (MUST MATCH INSTALLER) +############################################ +INSTALL_DIR="/opt/riven" +RIVEN_ROOT="/mnt/riven" +BACKEND_PATH="$RIVEN_ROOT/backend" +MOUNT_PATH="$RIVEN_ROOT/mount" +LOG_DIR="/tmp/logs/riven" + +SERVICE_NAME="riven-bind-shared.service" + +############################################ +# HELPERS +############################################ +banner(){ echo -e "\n========================================\n $1\n========================================"; } +ok(){ echo "[✔] $1"; } +warn(){ echo "[!] $1"; } +fail(){ echo "[✖] $1"; exit 1; } + +############################################ +# ROOT CHECK +############################################ +[[ "$(id -u)" -eq 0 ]] || fail "Run with sudo" + +############################################ +# CONFIRMATION +############################################ +banner "RIVEN UNINSTALLER" + +echo "⚠️ WARNING" +echo "This will COMPLETELY REMOVE:" +echo " • Riven containers" +echo " • Media containers" +echo " • systemd rshared mount unit" +echo " • $INSTALL_DIR" +echo " • $RIVEN_ROOT (backend + mount)" +echo " • Logs in $LOG_DIR" +echo +read -rp "Type UNINSTALL to continue: " CONFIRM +[[ "$CONFIRM" == "UNINSTALL" ]] || fail "Aborted by user" + +############################################ +# STOP CONTAINERS +############################################ +banner "Stopping Containers" + +if command -v docker >/dev/null; then + if [[ -d "$INSTALL_DIR" ]]; then + cd "$INSTALL_DIR" + + [[ -f docker-compose.yml ]] \ + && docker compose down --volumes --remove-orphans || true + + [[ -f docker-compose.media.yml ]] \ + && docker compose -f docker-compose.media.yml down --volumes --remove-orphans || true + fi +else + warn "Docker not installed — skipping container shutdown" +fi + +ok "Containers stopped" + +############################################ +# REMOVE SYSTEMD MOUNT UNIT +############################################ +banner "Removing rshared mount service" + +if systemctl list-unit-files | grep -q "$SERVICE_NAME"; then + systemctl disable --now "$SERVICE_NAME" || true + rm -f "/etc/systemd/system/$SERVICE_NAME" + systemctl daemon-reexec + systemctl daemon-reload + ok "systemd mount unit removed" +else + warn "No rshared mount service found" +fi + +############################################ +# UNMOUNT RIVEN MOUNT (SAFE) +############################################ +banner "Unmounting Riven mount" + +if mountpoint -q "$MOUNT_PATH"; then + umount -R "$MOUNT_PATH" || warn "Failed to fully unmount $MOUNT_PATH" + ok "Unmounted $MOUNT_PATH" +else + warn "$MOUNT_PATH is not mounted" +fi + +############################################ +# REMOVE BACKEND + MOUNT PATHS +############################################ +banner "Removing Riven filesystem" + +rm -rf "$BACKEND_PATH" +ok "Removed $BACKEND_PATH" + +rm -rf "$MOUNT_PATH" +ok "Removed $MOUNT_PATH" + +############################################ +# REMOVE /mnt/riven IF EMPTY +############################################ +if [[ -d "$RIVEN_ROOT" ]] && [[ -z "$(ls -A "$RIVEN_ROOT")" ]]; then + rmdir "$RIVEN_ROOT" + ok "Removed empty $RIVEN_ROOT" +else + warn "$RIVEN_ROOT not empty — leaving in place" +fi + +############################################ +# REMOVE INSTALL DIR + LOGS +############################################ +banner "Removing install artifacts" + +rm -rf "$INSTALL_DIR" +ok "Removed $INSTALL_DIR" + +rm -rf "$LOG_DIR" +ok "Removed logs" + +############################################ +# DOCKER CLEANUP (SAFE) +############################################ +banner "Docker Cleanup" + +if command -v docker >/dev/null; then + docker network prune -f || true + docker volume prune -f || true + ok "Docker cleanup complete" +else + warn "Docker not installed — skipping cleanup" +fi + +############################################ +# OPTIONAL: DOCKER REMOVAL +############################################ +banner "Optional Docker Removal" + +read -rp "Remove Docker entirely? (y/N): " REMOVE_DOCKER +if [[ "${REMOVE_DOCKER,,}" == "y" ]]; then + apt-get purge -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || true + apt-get autoremove -y + rm -rf /var/lib/docker /var/lib/containerd + ok "Docker fully removed" +else + ok "Docker preserved" +fi + +############################################ +# FINAL SUMMARY +############################################ +banner "UNINSTALL COMPLETE" + +echo "✔ Riven fully removed" +echo "✔ /mnt/riven cleaned" +echo "✔ systemd mount removed" +echo "✔ Containers removed" +echo +echo "System restored to pre-install state." + +ok "Cleanup complete 🧹" \ No newline at end of file diff --git a/ubuntu/riven-update.sh b/ubuntu/riven-update.sh new file mode 100644 index 0000000..63cbde3 --- /dev/null +++ b/ubuntu/riven-update.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################ +# CONSTANTS +############################################ +INSTALL_DIR="/opt/riven" +MEDIA_COMPOSE="docker-compose.media.yml" +RIVEN_COMPOSE="docker-compose.yml" + +############################################ +# HELPERS +############################################ +banner(){ echo -e "\n========================================\n $1\n========================================"; } +ok(){ echo "[✔] $1"; } +warn(){ echo "[!] $1"; } +fail(){ echo "[✖] $1"; exit 1; } + +############################################ +# ROOT CHECK +############################################ +[[ "$(id -u)" -eq 0 ]] || fail "Run with sudo" + +############################################ +# DOCKER CHECK +############################################ +banner "Docker Check" + +command -v docker >/dev/null \ + || fail "Docker is not installed" + +docker info >/dev/null 2>&1 \ + || fail "Docker is installed but not running" + +docker compose version >/dev/null 2>&1 \ + || fail "Docker Compose plugin is missing" + +ok "Docker and Docker Compose are available" + +############################################ +# PRE-FLIGHT CHECKS +############################################ +banner "Riven Update" + +[[ -d "$INSTALL_DIR" ]] \ + || fail "Riven is not installed ($INSTALL_DIR missing)" + +cd "$INSTALL_DIR" + +[[ -f "$RIVEN_COMPOSE" ]] \ + || fail "Missing $RIVEN_COMPOSE" + +[[ -f ".env" ]] \ + || fail "Missing .env file" + +ok "Riven installation detected" + +############################################ +# MEDIA SERVER PROMPT +############################################ +banner "Media Server Update" + +UPDATE_MEDIA=false +read -rp "Update media server containers too? (y/N): " ANSWER +[[ "${ANSWER,,}" == "y" ]] && UPDATE_MEDIA=true + +############################################ +# UPDATE RIVEN +############################################ +banner "Updating Riven" + +docker compose pull +docker compose up -d + +ok "Riven updated successfully" + +############################################ +# UPDATE MEDIA SERVER (OPTIONAL) +############################################ +if [[ "$UPDATE_MEDIA" == "true" ]]; then + banner "Updating Media Server" + + if [[ -f "$MEDIA_COMPOSE" ]]; then + docker compose -f "$MEDIA_COMPOSE" pull + docker compose -f "$MEDIA_COMPOSE" up -d + ok "Media server updated" + else + warn "Media compose file not found — skipping media update" + fi +else + ok "Media server update skipped" +fi + +############################################ +# OPTIONAL IMAGE CLEANUP +############################################ +banner "Optional Docker Image Cleanup" + +read -rp "Prune unused Docker images? (y/N): " PRUNE +if [[ "${PRUNE,,}" == "y" ]]; then + docker image prune + ok "Unused images pruned" +else + ok "Image cleanup skipped" +fi + +############################################ +# SUMMARY +############################################ +banner "UPDATE COMPLETE" + +echo "✔ Riven updated" +[[ "$UPDATE_MEDIA" == "true" ]] \ + && echo "✔ Media server updated" \ + || echo "• Media server unchanged" + +echo +echo "No volumes, mounts, or configuration files were modified." + +ok "Update finished 🚀" \ No newline at end of file