#!/bin/bash # Qar Install Script # Detects the OS, adds required repositories, and installs Qar. # # Usage: # curl -fsSL https://get.qar.lol | sudo bash # # Supports: Debian 12+, Ubuntu 22.04+, Fedora 40+, RHEL 9+, Rocky 9+, AlmaLinux 9+ set -euo pipefail # Where Qar's packages come from. # # Overridable so a fork, a mirror or a staging repository can be installed from # without editing this script. QAR_REPO_URL="${QAR_REPO_URL:-https://repo.qar.lol}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # Fedora releases older than this do not carry a Jellyfin build in RPM Fusion # that Qar can use. MIN_FEDORA=41 log() { echo -e "${GREEN}[qar]${NC} $*"; } warn() { echo -e "${YELLOW}[qar]${NC} $*"; } err() { echo -e "${RED}[qar]${NC} $*" >&2; } # Which Jellyfin suite to use, asked of the repository rather than assumed. # # This used to be a hardcoded list, and a hardcoded list is wrong in both # directions at once: Ubuntu 26.04 "resolute" is published by Jellyfin but was # not on the list, while "oracular" was on the list and has since been dropped. # The old fallback then made it worse by pairing a Debian codename with the # Ubuntu path -- repo.jellyfin.org/ubuntu bookworm, a URL that has never # existed -- which broke apt for everything else on the machine. # # Asking costs one HEAD request and cannot go stale. jellyfin_suite() { local distro="$1" want fallbacks c want=$(. /etc/os-release && echo "${VERSION_CODENAME:-}") if [ -n "$want" ] && curl -fsI -m 10 "https://repo.jellyfin.org/$distro/dists/$want/Release" >/dev/null 2>&1; then echo "$want" return 0 fi # This release is not published. Fall back to the newest one that is, for # THIS distribution -- never across to the other one's codenames. case "$distro" in ubuntu) fallbacks="noble jammy" ;; *) fallbacks="trixie bookworm bullseye" ;; esac for c in $fallbacks; do if curl -fsI -m 10 "https://repo.jellyfin.org/$distro/dists/$c/Release" >/dev/null 2>&1; then echo "$c" return 0 fi done return 0 # nothing usable; the caller decides what to do about it } # Require root if [ "$(id -u)" -ne 0 ]; then err "This script must be run as root (or with sudo)" exit 1 fi echo "" echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ Qar Media System Installer ║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}" echo "" # Detect OS detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS_ID="$ID" OS_VERSION_ID="${VERSION_ID:-}" OS_ID_LIKE="${ID_LIKE:-}" else err "Cannot detect operating system (/etc/os-release not found)" exit 1 fi # Determine the package family case "$OS_ID" in debian|ubuntu|linuxmint|pop) PKG_FAMILY="deb" ;; fedora) PKG_FAMILY="rpm" RPM_VARIANT="fedora" ;; rocky|almalinux|centos|rhel|ol) PKG_FAMILY="rpm" RPM_VARIANT="el" ;; *) # Try ID_LIKE as fallback if echo "$OS_ID_LIKE" | grep -q "debian\|ubuntu"; then PKG_FAMILY="deb" elif echo "$OS_ID_LIKE" | grep -q "fedora\|rhel\|centos"; then PKG_FAMILY="rpm" if echo "$OS_ID_LIKE" | grep -q "fedora" && [ "$OS_ID" != "fedora" ]; then RPM_VARIANT="el" else RPM_VARIANT="fedora" fi else err "Unsupported distribution: $OS_ID" err "Qar supports Debian/Ubuntu, Fedora, RHEL/Rocky/AlmaLinux" exit 1 fi ;; esac log "Detected: $PRETTY_NAME ($PKG_FAMILY)" } # Open the ports a TV or phone on the LAN needs to reach Jellyfin, plus the # Qar web interface. QBittorrent's WebUI is deliberately left closed -- it is # bound to loopback and reached through Qar. configure_firewall() { command -v firewall-cmd >/dev/null 2>&1 || return 0 firewall-cmd --state &>/dev/null || return 0 log "Opening firewall ports for Jellyfin and the Qar web interface..." if firewall-cmd --get-services 2>/dev/null | grep -qw jellyfin; then firewall-cmd --permanent --add-service=jellyfin >/dev/null 2>&1 || true else # No jellyfin-firewalld on this system; open the ports directly. firewall-cmd --permanent --add-port=8096/tcp >/dev/null 2>&1 || true firewall-cmd --permanent --add-port=8920/tcp >/dev/null 2>&1 || true firewall-cmd --permanent --add-port=1900/udp >/dev/null 2>&1 || true firewall-cmd --permanent --add-port=7359/udp >/dev/null 2>&1 || true fi firewall-cmd --permanent --add-port=3000/tcp >/dev/null 2>&1 || true # 3001 is the backend. Jellyfin hands its address straight to a TV or phone # for the download-progress video, so playback devices must be able to reach # it, not just this machine. firewall-cmd --permanent --add-port=3001/tcp >/dev/null 2>&1 || true firewall-cmd --reload >/dev/null 2>&1 || true } # Jellyfin runs as its own user and has to read the library Qar writes. grant_jellyfin_media_access() { getent passwd jellyfin >/dev/null 2>&1 || return 0 getent group qar >/dev/null 2>&1 || return 0 usermod -aG qar jellyfin 2>/dev/null || true # Group traversal down to the library. chmod 755 /qar /qar/content 2>/dev/null || true if systemctl is-active jellyfin &>/dev/null; then systemctl restart jellyfin 2>/dev/null || true fi } # --- Debian/Ubuntu --- install_deb() { log "Updating package lists..." apt-get update -qq # Install prerequisites log "Installing prerequisites..." apt-get install -y -qq curl gnupg apt-transport-https > /dev/null # Add Jellyfin repository if ! [ -f /usr/share/keyrings/jellyfin.gpg ]; then log "Adding Jellyfin repository..." local repo_distro case "$OS_ID" in ubuntu|linuxmint|pop) repo_distro="ubuntu" ;; *) repo_distro="debian" ;; esac local codename codename=$(jellyfin_suite "$repo_distro") if [ -z "$codename" ]; then # Adding a source that 404s does not degrade gracefully: apt-get update # fails outright, and every other repository on the machine -- including # Qar's -- stops working with it. Better to install without Jellyfin and # say so than to leave a box where apt itself is broken. warn "Jellyfin publishes nothing for $repo_distro/$(. /etc/os-release && echo "$VERSION_CODENAME")." warn "Skipping the Jellyfin repository; install Jellyfin separately." else curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg echo "deb [signed-by=/usr/share/keyrings/jellyfin.gpg] https://repo.jellyfin.org/$repo_distro $codename main" \ > /etc/apt/sources.list.d/jellyfin.list fi else log "Jellyfin repository already configured" fi # Add Qar repository # # The source list is checked as well as the key, not just the key. Qar's # packages moved hosts, and a machine installed before the move has a key on # disk but a source pointing somewhere that no longer publishes -- so keying # this on the key alone would leave exactly the machines that need migrating # as the ones that never get it. Re-running the installer moves them over. if ! grep -qsF "$QAR_REPO_URL/deb" /etc/apt/sources.list.d/qar.list; then log "Adding Qar repository ($QAR_REPO_URL)..." curl -fsSL "$QAR_REPO_URL/KEY.gpg" | gpg --dearmor --yes -o /usr/share/keyrings/qar.gpg echo "deb [signed-by=/usr/share/keyrings/qar.gpg] $QAR_REPO_URL/deb stable main" \ > /etc/apt/sources.list.d/qar.list else log "Qar repository already configured" fi # Install Qar (pulls in all deps except Jellyfin) log "Updating package lists..." apt-get update -qq log "Installing Qar and dependencies..." DEBIAN_FRONTEND=noninteractive apt-get install -y qar # Install Jellyfin from its official repo (added above) if ! dpkg -l jellyfin-server 2>/dev/null | grep -q '^ii'; then log "Installing Jellyfin..." DEBIAN_FRONTEND=noninteractive apt-get install -y jellyfin else log "Jellyfin already installed" fi grant_jellyfin_media_access } # --- Fedora --- install_rpm_fedora() { local fedora_version fedora_version=$(rpm -E %fedora) if [ "$fedora_version" -lt "$MIN_FEDORA" ] 2>/dev/null; then err "Fedora $fedora_version is not supported. Qar requires Fedora $MIN_FEDORA or newer." err "Older releases do not carry a Jellyfin build compatible with Qar." exit 1 fi # RPM Fusion supplies both a full ffmpeg and Jellyfin itself on Fedora. if ! rpm -q rpmfusion-free-release &>/dev/null; then log "Adding RPM Fusion repository..." dnf install -y "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-${fedora_version}.noarch.rpm" else log "RPM Fusion already configured" fi setup_qar_rpm_repo log "Installing Qar and dependencies..." dnf install -y qar # Jellyfin from RPM Fusion. jellyfin-firewalld ships the service definition # so a TV on the LAN can reach the server. if rpm -q jellyfin-server &>/dev/null; then log "Jellyfin already installed" else log "Installing Jellyfin from RPM Fusion..." dnf install -y jellyfin jellyfin-firewalld fi systemctl enable --now jellyfin 2>/dev/null || true configure_firewall grant_jellyfin_media_access } # --- RHEL / Rocky / AlmaLinux (EL9+) --- install_rpm_el() { local el_version el_version=$(. /etc/os-release && echo "${VERSION_ID%%.*}") # Enable Node.js 20 module stream (EL9 defaults to Node 16 which is too old) log "Enabling Node.js 20 module stream..." dnf module reset nodejs -y 2>/dev/null || true dnf module enable nodejs:20 -y # EPEL (needed for qbittorrent-nox, tor) if ! rpm -q epel-release &>/dev/null; then log "Installing EPEL repository..." dnf install -y epel-release else log "EPEL already configured" fi # Enable CRB/PowerTools (needed for RPM Fusion deps) log "Enabling CRB repository..." /usr/bin/crb enable 2>/dev/null || dnf config-manager --set-enabled crb 2>/dev/null || true # RPM Fusion (needed for ffmpeg) if ! rpm -q rpmfusion-free-release &>/dev/null; then log "Adding RPM Fusion repository..." dnf install -y --nogpgcheck "https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${el_version}.noarch.rpm" else log "RPM Fusion already configured" fi # Add Qar repository setup_qar_rpm_repo # Install Qar log "Installing Qar and dependencies..." dnf install -y qar # Install Jellyfin via portable tarball (RPM Fusion jellyfin package # requires ffmpeg >= 7.1 which is not available on EL9) install_jellyfin_portable configure_firewall grant_jellyfin_media_access } # Add Qar DNF/YUM repository setup_qar_rpm_repo() { if [ -f /etc/yum.repos.d/qar.repo ]; then log "Qar repository already configured" return fi log "Adding Qar repository ($QAR_REPO_URL)..." rpm --import "$QAR_REPO_URL/KEY.gpg" # gpgcheck covers each .rpm, repo_gpgcheck covers the metadata listing them. # Both matter: the first stops an unsigned package being installed, the # second stops a tampered index pointing at a different one. cat > /etc/yum.repos.d/qar.repo << EOF [qar] name=Qar - Self-hosted media management baseurl=$QAR_REPO_URL/rpm enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=$QAR_REPO_URL/KEY.gpg EOF } # Install Jellyfin from portable tarball (for RPM systems) install_jellyfin_portable() { # Skip if Jellyfin is already installed if systemctl is-active jellyfin &>/dev/null; then log "Jellyfin is already running" return fi if [ -f /opt/qar/install-jellyfin.sh ]; then log "Installing Jellyfin via portable installer..." bash /opt/qar/install-jellyfin.sh else warn "Jellyfin installer not found at /opt/qar/install-jellyfin.sh" warn "Install Jellyfin manually: https://jellyfin.org/downloads/" fi } # Detect and install detect_os case "$PKG_FAMILY" in deb) install_deb ;; rpm) case "${RPM_VARIANT:-fedora}" in fedora) install_rpm_fedora ;; el) install_rpm_el ;; esac ;; esac echo "" echo -e "${GREEN}╔══════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ Qar installed successfully! ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════╝${NC}" echo "" echo -e " Finish setup in your browser:" echo "" echo -e " ${CYAN}http://localhost:3000/setup${NC}" echo "" echo " It walks you through choosing a VPN (or not), connecting" echo " Jellyfin, and adding your first movie. Nothing downloads" echo " until that is done." echo ""