Skip to content

unRAID Mover and qBittorrent

When you use the unRAID cache drive for your /data/torrents share while torrents are actively seeding in qBittorrent, the unRAID mover cannot move files. This happens because the files are still in use, which would break the hard links.

Using the instructions below, you can move files using the qBittorrent API with the qBit-Mover script.


How the qBit-Mover Script Works

The qBit-Mover script doesn't move files itself. It only pauses and resumes torrents, and it can trigger the unRAID mover or Mover Tuning.

This guide explains two ways to use the mover script:

  • Option 1: Combined with the Mover Tuning Plugin (Recommended)
  • Option 2: Using User Scripts

Option 1

This option uses the Mover Tuning plugin to:

  1. Pause torrents within a specific age range that are on your cache drive
  2. Resume the torrents after the unRAID mover finishes

It also offers these features:

  • Automatically install and update the qbittorrent-api module (REQUIRED)
  • Automatically download the qBit-Mover script (REQUIRED)
  • qBit-Manage integration (OPTIONAL)

    • Stop qBit-Manage before qBit-Mover runs
    • Start qBit-Manage after qBit-Mover completes or after fclones finishes

      If qBit-Manage runs while files are moving from cache to your array, it may incorrectly mark your files as NoHL. We strongly recommend enabling this option to prevent this issue.

  • Automatically download fclones (OPTIONAL)

    • Run fclones (Replace copies with hardlinks)
  • Automatically set the correct unRAID User/Group and permissions

Option 2

This option runs the script from User Scripts to:

  1. Pause torrents within a specific age range that are on your cache drive
  2. Run/trigger the unRAID mover
  3. Resume the torrents after the unRAID mover finishes

Requirements

Important: Disable Pre-allocation in qBittorrent

Go to qBittorrent → Options → Downloads and disable this option:

Pre-allocate disk space for all files

When this option is enabled, it keeps the reserved space locked (in use) until you quit qBittorrent.

Tips & Info

Tip

  • Don't disable the mover from SettingsSchedulerMover Settings. Instead, you could set the mover to run once a month, one minute after you run the qBit Mover script. The mover shouldn't run because it's already running.
  • If you're also using Mover Tuning, don't disable the mover from running on a schedule—this could completely disable it.
  • We recommend using Mover Tuning. If you do, make sure Move files that are greater than this many days old matches the number of days you set in the qBit-Mover script or config.

Info

The screenshots below are EXAMPLES to show you how things should look and where to add data. They are NOT always 100% accurate reflections of the actual data or the exact values you need.

  • Always follow the recommendations in this guide.
  • If you have questions or aren't sure about something, click the chat badge to join the Discord Channel where you can ask questions directly.

Option 1: Mover Tuning

This option expects that you follow the guide's suggested paths as described in this section.

Install the following plugins:

  • Python 3 for unRAID (unRAID Plugin)
  • Mover Tuning (unRAID Plugin)

Install the following container (Optional but suggested):

  • qBit-Manage

For this option, you only need to download three files and place them in /mnt/user/appdata/qbt-mover/:

  • mover-tuning-start.sh - The script that runs before the mover starts.

    mover-tuning-start.sh - [Click to show/hide]
    #!/bin/bash
    set -euo pipefail # Exit on error, undefined variables, and pipe failures
    
    # =======================================
    # Script: qBittorrent Cache Mover - Start
    # Updated: 20251112
    # =======================================
    
    # Get the directory where the script is located
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    
    # Source the config from the same directory
    source "$SCRIPT_DIR/mover-tuning.cfg"
    
    readonly VENV_PATH="${QBIT_MOVER_PATH}.venv"
    readonly MOVER_SCRIPT="${QBIT_MOVER_PATH}mover.py"
    readonly MOVER_URL="https://raw.githubusercontent.com/StuffAnThings/qbit_manage/develop/scripts/mover.py"
    
    # Notification delay in seconds (helps ensure all notifications appear in Unraid)
    NOTIFICATION_DELAY=2
    
    # ================================
    # UTILITY FUNCTIONS
    # ================================
    log() {
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
    }
    
    error() {
        log "ERROR: $*" >&2
        exit 1
    }
    
    notify() {
        local subject="$1"
        local description="$2"
        local notify_cmd="/usr/local/emhttp/plugins/dynamix/scripts/notify"
    
        if [[ -x "$notify_cmd" ]]; then
            "$notify_cmd" -s "$subject" -d "$description"
            # Add delay after each notification to prevent dropping
            sleep "$NOTIFICATION_DELAY"
        fi
    }
    
    check_command() {
        command -v "$1" &> /dev/null
    }
    
    set_ownership() {
        chown -R nobody:users "$1" 2>/dev/null || log "⚠ Warning: Could not set ownership for $1"
    }
    
    # ================================
    # AUTO INSTALLER FUNCTION
    # ================================
    run_auto_installer() {
        log "========================================"
        log "Running qBit-Api and qBit-Mover Auto Installer"
        log "========================================"
    
        # Create QBIT_MOVER_PATH directory if needed
        if [[ ! -d "$QBIT_MOVER_PATH" ]]; then
            mkdir -p "$QBIT_MOVER_PATH" || error "Failed to create $QBIT_MOVER_PATH"
            set_ownership "$QBIT_MOVER_PATH"
            log "✓ Created $QBIT_MOVER_PATH"
        fi
    
        # Create virtual environment if needed
        if [[ ! -d "$VENV_PATH" ]]; then
            log "Creating virtual environment..."
            python3 -m venv "$VENV_PATH" || error "Failed to create virtual environment"
            set_ownership "$VENV_PATH"
            log "✓ Virtual environment created"
        else
            log "✓ Virtual environment exists"
        fi
    
        # Activate virtual environment
        # shellcheck source=/dev/null
        source "${VENV_PATH}/bin/activate" || error "Failed to activate virtual environment"
    
        # Upgrade pip if needed
        log "Checking pip version..."
        if pip3 install --upgrade pip --quiet 2>&1 | grep -q "Successfully installed"; then
            log "✓ Pip upgraded to $(pip3 --version | awk '{print $2}')"
            set_ownership "$VENV_PATH"
        else
            log "✓ Pip is up to date"
        fi
    
        # Install/upgrade qbittorrent-api
        if python3 -c "import qbittorrentapi" 2>/dev/null; then
            log "✓ qbittorrent-api installed ($(pip3 show qbittorrent-api 2>/dev/null | awk '/Version:/ {print $2}'))"
    
            # Check for updates
            if pip3 install --dry-run --upgrade qbittorrent-api 2>&1 | grep -q "Would install"; then
                log "Upgrading qbittorrent-api..."
                pip3 install qbittorrent-api --upgrade --quiet || log "⚠ Warning: Failed to upgrade qbittorrent-api"
                set_ownership "$VENV_PATH"
                log "✓ qbittorrent-api upgraded"
            else
                log "✓ qbittorrent-api is up to date"
            fi
        else
            log "Installing qbittorrent-api..."
            pip3 install qbittorrent-api --quiet || error "Failed to install qbittorrent-api"
            set_ownership "$VENV_PATH"
            log "✓ qbittorrent-api installed"
        fi
    
        deactivate
    
        # Download mover.py if needed
        if [[ ! -f "$MOVER_SCRIPT" ]]; then
            log "Downloading mover.py..."
            if curl -sSL "$MOVER_URL" -o "$MOVER_SCRIPT"; then
                chmod +x "$MOVER_SCRIPT"
                set_ownership "$MOVER_SCRIPT"
                log "✓ mover.py downloaded"
            else
                error "Failed to download mover.py"
            fi
        else
            log "✓ mover.py exists"
        fi
    
        log "========================================"
        log "Auto Installer completed"
        log "========================================"
    }
    
    # ================================
    # VALIDATION
    # ================================
    validate_config() {
        log "Validating configuration..."
    
        # Check required commands
        for cmd in python3 date curl; do
            check_command "$cmd" || error "$cmd is not installed"
        done
    
        # Validate docker if needed
        if [[ "$ENABLE_QBIT_MANAGE" == true ]]; then
            check_command docker || error "docker is required when ENABLE_QBIT_MANAGE=true"
        fi
    
        # Validate settings
        [[ "$DAYS_FROM" -ge 2 ]] || error "DAYS_FROM must be at least 2"
        [[ "$DAYS_TO" -ge "$DAYS_FROM" ]] || error "DAYS_TO must be >= DAYS_FROM"
        [[ -d "$CACHE_MOUNT" ]] || error "Cache mount does not exist: $CACHE_MOUNT"
    }
    
    # ================================
    # PROCESS QBITTORRENT INSTANCE
    # ================================
    process_qbit_instance() {
        local name="$1" host="$2" user="$3" password="$4"
    
        log "Processing $name..."
    
        # Determine Python command
        local python_cmd
        if [[ -f "${VENV_PATH}/bin/python3" ]]; then
            python_cmd="${VENV_PATH}/bin/python3"
        elif python3 -c "import qbittorrentapi" 2>/dev/null; then
            python_cmd="python3"
        else
            log "✗ qbittorrent-api not found for $name"
            return 1
        fi
    
        # Run mover script
        if $python_cmd "$MOVER_SCRIPT" \
            --pause \
            --host "$host" \
            --user "$user" \
            --password "$password" \
            --cache-mount "$CACHE_MOUNT" \
            --days_from "$DAYS_FROM" \
            --days_to "$DAYS_TO" 2>&1 | while IFS= read -r line; do
                log "  $line"
            done; then
            log "✓ Successfully processed $name"
            notify "$name" "Paused @ $(date +%H:%M:%S)"
            return 0
        else
            log "✗ Failed to process $name"
            return 1
        fi
    }
    
    # ================================
    # MAIN EXECUTION
    # ================================
    main() {
        local failed_instances=0
        readonly date_from=$(date --date="$DAYS_FROM day ago" +%F)
    
        log "========================================"
        log "qBittorrent Cache Mover Started"
        log "Date range: $DAYS_FROM-$DAYS_TO days (from $date_from)"
        log "========================================"
    
        # Run auto installer if enabled
        [[ "$ENABLE_AUTO_INSTALLER" == true ]] && run_auto_installer
    
        # Validate configuration
        validate_config
        [[ -f "$MOVER_SCRIPT" ]] || error "mover.py not found at: $MOVER_SCRIPT"
    
        # Stop qBit-Manage if enabled
        if [[ "$ENABLE_QBIT_MANAGE" == true ]]; then
            log "Stopping $QBIT_MANAGE_CONTAINER..."
            if docker stop "$QBIT_MANAGE_CONTAINER" &> /dev/null; then
                log "✓ Stopped qBit-Manage"
                notify "qBit-Manage" "Stopped @ $(date +%H:%M:%S)"
                sleep "$QBIT_MANAGE_WAIT"
            else
                log "⚠ Warning: Failed to stop $QBIT_MANAGE_CONTAINER"
            fi
        fi
    
        # Process instances
        process_qbit_instance "$QBIT_NAME_1" "$QBIT_HOST_1" "$QBIT_USER_1" "$QBIT_PASS_1" || ((failed_instances++))
    
        if [[ "$ENABLE_QBIT_2" == true ]]; then
            process_qbit_instance "$QBIT_NAME_2" "$QBIT_HOST_2" "$QBIT_USER_2" "$QBIT_PASS_2" || ((failed_instances++))
        fi
    
        # Summary
        log "========================================"
        if [[ $failed_instances -eq 0 ]]; then
            log "✓ All operations completed successfully"
            exit 0
        else
            log "⚠ Completed with $failed_instances failed instance(s)"
            exit 1
        fi
    }
    
    # Run main function
    main
    
  • mover-tuning-end.sh - The script that runs after the mover finishes.

    mover-tuning-end.sh - [Click to show/hide]
    #!/bin/bash
    set -euo pipefail # Exit on error, undefined variables, and pipe failures
    
    # =====================================
    # Script: qBittorrent Cache Mover - End
    # Updated: 20251112
    # =====================================
    
    # Get the directory where the script is located
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    
    # Source the config from the same directory
    source "$SCRIPT_DIR/mover-tuning.cfg"
    
    # Notification delay in seconds (helps ensure all notifications appear in Unraid)
    NOTIFICATION_DELAY=2
    
    # ================================
    # UTILITY FUNCTIONS
    # ================================
    log() {
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
    }
    
    error() {
        log "ERROR: $*" >&2
        exit 1
    }
    
    notify() {
        local subject="$1"
        local description="$2"
        local notify_cmd="/usr/local/emhttp/plugins/dynamix/scripts/notify"
    
        if [[ -x "$notify_cmd" ]]; then
            "$notify_cmd" -s "$subject" -d "$description"
            # Add delay after each notification to prevent dropping
            sleep "$NOTIFICATION_DELAY"
        fi
    }
    
    # ================================
    # AUTO-INSTALLER FUNCTIONS
    # ================================
    install_fclones_binary() {
        log "Installing/updating fclones binary..."
    
        # fclones configuration
        local FCLONES_BIN="/usr/local/bin/fclones"
        local BOOT_DIR="/boot/config/plugins/fclones/usr/bin"
        local GO_FILE="/boot/config/go"
        local DEFAULT_VERSION="0.35.0"
    
        # Current installed version
        local CURRENT_VERSION=""
        if [[ -x "$FCLONES_BIN" ]]; then
            CURRENT_VERSION=$($FCLONES_BIN --version 2>/dev/null | awk '{print $2}')
            log "✓ Found fclones version $CURRENT_VERSION"
        else
            log "✗ fclones not found"
        fi
    
        # Check for curl or wget
        local GITHUB_API_CMD
        if command -v curl >/dev/null 2>&1; then
            GITHUB_API_CMD="curl -s https://api.github.com/repos/pkolaczk/fclones/releases/latest"
        elif command -v wget >/dev/null 2>&1; then
            GITHUB_API_CMD="wget -qO- https://api.github.com/repos/pkolaczk/fclones/releases/latest"
        else
            log "✗ Neither curl nor wget is available"
            return 1
        fi
    
        # Fetch latest release from GitHub
        local LATEST_VERSION
        LATEST_VERSION=$($GITHUB_API_CMD | grep -Po '"tag_name": "\K.*?(?=")')
        if [[ -z "$LATEST_VERSION" ]]; then
            log "⚠ Could not fetch latest release, using default version $DEFAULT_VERSION"
            LATEST_VERSION="$DEFAULT_VERSION"
        else
            log "Latest fclones release: $LATEST_VERSION"
        fi
    
        # Compare and install if missing or outdated
        if [[ "$CURRENT_VERSION" != "$LATEST_VERSION" ]]; then
            log "Installing/updating fclones to $LATEST_VERSION..."
    
            local TMP_DIR
            TMP_DIR=$(mktemp -d)
    
            # Remove leading 'v' from filename
            local VERSION_NO_V="${LATEST_VERSION#v}"
            local DOWNLOAD_URL="https://github.com/pkolaczk/fclones/releases/download/$LATEST_VERSION/fclones-$VERSION_NO_V-linux-glibc-x86_64.tar.gz"
    
            wget -O "$TMP_DIR/fclones.tar.gz" "$DOWNLOAD_URL"
            if [[ $? -ne 0 ]]; then
                log "✗ Failed to download fclones from $DOWNLOAD_URL"
                rm -rf "$TMP_DIR"
                return 1
            fi
    
            tar -xzf "$TMP_DIR/fclones.tar.gz" -C "$TMP_DIR"
            mkdir -p "$BOOT_DIR"
            cp "$TMP_DIR/usr/bin/fclones" "$BOOT_DIR/fclones"
            chmod +x "$BOOT_DIR/fclones"
    
            # Copy to /usr/local/bin immediately
            cp "$BOOT_DIR/fclones" /usr/local/bin/fclones
            chmod +x /usr/local/bin/fclones
    
            # Add boot-time copy and PATH setup if not already in /boot/config/go
            if ! grep -q "fclones boot-time setup" "$GO_FILE"; then
                if [ ! -w "$GO_FILE" ]; then
                    log "✗ Cannot write to $GO_FILE. Please check permissions."
                    rm -rf "$TMP_DIR"
                    return 1
                fi
                echo "" >> "$GO_FILE"
                echo "# fclones boot-time setup" >> "$GO_FILE"
                echo "export PATH=/usr/local/bin:\$PATH" >> "$GO_FILE"
                echo "cp $BOOT_DIR/fclones /usr/local/bin/fclones" >> "$GO_FILE"
            fi
    
            rm -rf "$TMP_DIR"
            log "✓ fclones $VERSION_NO_V installed successfully"
            return 0
        else
            log "✓ fclones is up to date ($CURRENT_VERSION)"
            return 0
        fi
    }
    
    install_fclones_script() {
        log "Installing fclones.sh script..."
    
        local raw_script_url="https://gist.githubusercontent.com/BaukeZwart/b570ce6b6165c4f0b64c5b98d9d3af1e/raw"
        local script_path="${QBIT_MOVER_PATH}fclones.sh"
    
        # Create directory if needed
        mkdir -p "$QBIT_MOVER_PATH"
    
        # Download script
        if command -v curl &> /dev/null; then
            curl -fsSL "$raw_script_url" -o "$script_path" || {
                log "✗ Failed to download fclones.sh script"
                return 1
            }
        elif command -v wget &> /dev/null; then
            wget -q "$raw_script_url" -O "$script_path" || {
                log "✗ Failed to download fclones.sh script"
                return 1
            }
        else
            log "✗ Neither curl nor wget is available"
            return 1
        fi
    
        # Set permissions
        chmod +x "$script_path"
        chown nobody:users "$script_path" 2>/dev/null || \
            log "⚠ Warning: Could not set ownership to nobody:users"
    
        log "✓ fclones.sh script installed at $script_path"
        return 0
    }
    
    check_and_install_fclones() {
        log "Checking fclones installation..."
    
        local need_install=false
    
        # Check binary
        if ! command -v fclones &> /dev/null; then
            log "✗ fclones binary not found"
            if ! install_fclones_binary; then
                notify "fclones Auto-Installer" "Failed to install binary @ $(date +%H:%M:%S)"
                return 1
            fi
            need_install=true
        else
            # Check if update is needed
            if ! install_fclones_binary; then
                log "⚠ Failed to check/update fclones binary"
            fi
        fi
    
        # Check script
        if [[ ! -f "${QBIT_MOVER_PATH}fclones.sh" ]]; then
            log "✗ fclones.sh script not found"
            if ! install_fclones_script; then
                notify "fclones Auto-Installer" "Failed to install script @ $(date +%H:%M:%S)"
                return 1
            fi
            need_install=true
        else
            log "✓ fclones.sh script found"
        fi
    
        if [[ "$need_install" == true ]]; then
            log "✓ fclones installation completed"
            notify "fclones Auto-Installer" "Installation completed @ $(date +%H:%M:%S)"
        fi
    
        return 0
    }
    
    # ================================
    # VALIDATION
    # ================================
    validate_config() {
        log "Validating configuration..."
    
        # Check required commands
        local missing_cmds=()
        for cmd in python3 date; do
            command -v "$cmd" &> /dev/null || missing_cmds+=("$cmd")
        done
    
        if [[ ${#missing_cmds[@]} -gt 0 ]]; then
            error "Missing required commands: ${missing_cmds[*]}"
        fi
    
        # Check docker if needed
        if [[ "$ENABLE_QBIT_MANAGE" == true ]] && ! command -v docker &> /dev/null; then
            error "docker is required when ENABLE_QBIT_MANAGE=true"
        fi
    
        # Validate paths and values
        [[ "$DAYS_FROM" -ge 2 ]] || error "DAYS_FROM must be at least 2"
        [[ "$DAYS_TO" -ge "$DAYS_FROM" ]] || error "DAYS_TO must be >= DAYS_FROM"
        [[ -d "$CACHE_MOUNT" ]] || error "Cache mount not found: $CACHE_MOUNT"
        [[ -f "${QBIT_MOVER_PATH}mover.py" ]] || error "mover.py not found: ${QBIT_MOVER_PATH}mover.py"
    
        # Validate duplicate finder if enabled
        if [[ "$ENABLE_DUPLICATE_FINDER" == true ]]; then
            if [[ "$ENABLE_AUTO_INSTALLER" == true ]]; then
                check_and_install_fclones || return 1
            else
                [[ -f "${QBIT_MOVER_PATH}fclones.sh" ]] || \
                    error "Duplicate finder script not found: ${QBIT_MOVER_PATH}fclones.sh"
                [[ -x "${QBIT_MOVER_PATH}fclones.sh" ]] || \
                    error "Duplicate finder script not executable: ${QBIT_MOVER_PATH}fclones.sh"
            fi
        fi
    
        log "✓ Validation completed"
    }
    
    # ================================
    # PROCESS QBITTORRENT INSTANCE
    # ================================
    process_qbit_instance() {
        local name="$1"
        local host="$2"
        local user="$3"
        local password="$4"
    
        log "Processing $name..."
    
        # Determine Python command
        local python_cmd
        if [[ -f "${QBIT_MOVER_PATH}.venv/bin/python3" ]]; then
            python_cmd="${QBIT_MOVER_PATH}.venv/bin/python3"
            log "✓ Using virtual environment"
        elif python3 -c "import qbittorrentapi" 2>/dev/null; then
            python_cmd="python3"
            log "✓ Using system Python"
        else
            log "✗ qbittorrent-api not found"
            return 1
        fi
    
        # Execute mover script
        if "$python_cmd" "${QBIT_MOVER_PATH}mover.py" \
            --resume \
            --host "$host" \
            --user "$user" \
            --password "$password" \
            --days_from "$DAYS_FROM" \
            --days_to "$DAYS_TO"; then
            log "✓ Successfully resumed torrents for $name"
            notify "$name" "Resumed @ $(date +%H:%M:%S)"
            return 0
        else
            log "✗ Failed to resume torrents for $name"
            return 1
        fi
    }
    
    # ================================
    # MAIN EXECUTION
    # ================================
    main() {
        local failed_instances=0
        local date_str
        date_str=$(date --date="$DAYS_FROM day ago" +%F)
    
        log "========================================"
        log "Starting torrent resume process"
        log "Age range: $DAYS_FROM-$DAYS_TO days (from $date_str)"
        log "========================================"
    
        # Validate configuration
        validate_config || exit 1
    
        # Process primary instance
        process_qbit_instance "$QBIT_NAME_1" "$QBIT_HOST_1" "$QBIT_USER_1" "$QBIT_PASS_1" || \
            ((failed_instances++))
    
        # Process secondary instance if enabled
        if [[ "$ENABLE_QBIT_2" == true ]]; then
            log "Processing secondary instance..."
            process_qbit_instance "$QBIT_NAME_2" "$QBIT_HOST_2" "$QBIT_USER_2" "$QBIT_PASS_2" || \
                ((failed_instances++))
        else
            log "Secondary instance disabled"
        fi
    
        # Run duplicate finder if enabled
        if [[ "$ENABLE_DUPLICATE_FINDER" == true ]]; then
            log "Running duplicate finder..."
            if bash "${QBIT_MOVER_PATH}fclones.sh"; then
                log "✓ $DUPLICATE_FINDER_NAME completed"
                notify "$DUPLICATE_FINDER_NAME" "Completed @ $(date +%H:%M:%S)"
            else
                log "⚠ $DUPLICATE_FINDER_NAME failed"
                notify "$DUPLICATE_FINDER_NAME" "Failed @ $(date +%H:%M:%S)"
            fi
        fi
    
        # Start qBit-Manage if enabled
        if [[ "$ENABLE_QBIT_MANAGE" == true ]]; then
            log "Starting qBit-Manage container..."
            if docker start "$QBIT_MANAGE_CONTAINER" &> /dev/null; then
                log "✓ qBit-Manage started"
                notify "qBit-Manage" "Started @ $(date +%H:%M:%S)"
            else
                log "⚠ Failed to start qBit-Manage"
            fi
        fi
    
        # Summary
        log "========================================"
        if [[ $failed_instances -eq 0 ]]; then
            log "✓ All operations completed successfully"
            exit 0
        else
            log "⚠ Completed with $failed_instances failed instance(s)"
            exit 1
        fi
    }
    
    # Run main function
    main
    
  • mover-tuning.cfg - This config file holds all the user variables used by the other scripts.

    Read and edit the instructions inside the script.

    mover-tuning.cfg - [Click to show/hide]
    # =============================================
    # <----- mover-tuning start/end settings ----->
    # =============================================
    
    # Auto-installer configuration (Optional)
    readonly ENABLE_AUTO_INSTALLER=true  # Set to false to disable qBit-Api and qBit-Mover auto installer
    
    # qBit-Mover Settings
    # >>> NOTE: Setting "DAYS_FROM" below 2 days may not work properly <<<
    readonly DAYS_FROM=25  # How old torrents must be (in days) before they're paused and moved (Must be at least 2 days)
    readonly DAYS_TO=99    # Maximum age (days) for torrent selection
    readonly CACHE_MOUNT="/mnt/cache/"  # Cache mount point in Unraid
    readonly QBIT_MOVER_PATH="/mnt/user/appdata/qbt-mover/"  # Path to mover.py
    
    # Primary qBittorrent instance (REQUIRED)
    readonly QBIT_NAME_1="qBit-Movies"  # qBittorrent instance name
    readonly QBIT_HOST_1="192.168.2.200:8800"  # qBittorrent host:port
    readonly QBIT_USER_1="admin"  # qBittorrent username
    readonly QBIT_PASS_1="qbt1-password"  # qBittorrent password
    
    # Secondary qBittorrent instance (OPTIONAL)
    readonly ENABLE_QBIT_2=false  # Set to true to enable secondary instance
    readonly QBIT_NAME_2="qBit-TV"  # qBittorrent instance name
    readonly QBIT_HOST_2="192.168.2.200:8811"  # qBittorrent host:port
    readonly QBIT_USER_2="admin"  # qBittorrent username
    readonly QBIT_PASS_2="qbt2-password"  # qBittorrent password
    
    # qBit-Manage integration (OPTIONAL)
    readonly ENABLE_QBIT_MANAGE=true  # Set to false to disable qBit-Manage
    readonly QBIT_MANAGE_CONTAINER="qbit-manage"  # qBit-Manage Docker container name
    readonly QBIT_MANAGE_WAIT=5  # Wait time (seconds) after stopping qBit-Manage
    
    # Duplicate finder script (OPTIONAL)
    readonly ENABLE_DUPLICATE_FINDER=true # Set to false to disable duplicate finder
    readonly DUPLICATE_FINDER_NAME="Duplicate Finder" # Name for logging/notifications
    
    # =========================================
    # <----- fclones run script settings ----->
    # =========================================
    
    # Path to fclones executable
    #
    # OPTION 1: System-wide installation (Recommended)
    # If you installed fclones system-wide (e.g., via the mover-tuning-end script)
    # it will be available in your PATH.
    # In this case, simply use:
    # FCLONES_PATH="fclones"
    #
    # OPTION 2: Custom binary location
    # If you downloaded the fclones binary manually:
    #   1. Download from: https://github.com/pkolaczk/fclones/releases
    #   2. Extract the archive
    #   3. Make the binary executable: chmod +x fclones
    #   4. Place it in a directory of your choice
    #   5. Set the full path below, for example:
    # FCLONES_PATH="/mnt/user/apps/fclones/fclones"
    
    # To verify your installation, run: $FCLONES_PATH --version
    readonly FCLONES_PATH="fclones"
    
    # fclones arguments for the group command
    # Note: --cache, file paths, and --name patterns will be added automatically
    readonly FCLONES_ARGS="--one-fs --hidden --follow-links"
    
    # File patterns to match (space-separated)
    readonly FILE_PATTERNS="*.mkv *.mp4 *.avi"
    
    # Maximum number of files to display per folder in notifications
    # Set to 0 for unlimited, or any positive number to limit
    readonly MAX_FILES_DISPLAY=5
    
    # Display full filenames including file extensions in notifications
    # Set to true to show full filename with extension, false to show without extension
    readonly SHOW_FILE_EXTENSIONS=false
    
    # Maximum filename length in notifications before truncating
    # Filenames longer than this will be truncated with "..."
    readonly MAX_FILENAME_LENGTH=56
    
    # Log file settings
    LOG_PATH="/mnt/user/appdata/qbt-mover/fclones-logs"
    readonly LOG_RETENTION_DAYS=5
    
    # Search path pairs - Add or modify your folder pairs here
    # Format: folders["Display Name"]="source_path target_path"
    declare -A folders
    folders["Series"]="/mnt/user/data/torrents/tv/ /mnt/user/data/media/tv/"
    folders["Movies"]="/mnt/user/data/torrents/movies/ /mnt/user/data/media/movies/"
    # folders["Series-xseed"]="/mnt/user/data/torrents/tv/ /mnt/user/data/torrents/series_linkdir/"
    # folders["Movies-xseed"]="/mnt/user/data/torrents/movies/ /mnt/user/data/torrents/movies_linkdir/"
    

Permissions

Once you've downloaded all the scripts, make sure the permissions are correct and that the scripts are executable. You can do this from a terminal with the following command:

chown -R nobody:users /mnt/user/appdata/qbt-mover/
chmod -R a=,a+rX,u+w,g+w /mnt/user/appdata/qbt-mover/
chmod +x /mnt/user/appdata/qbt-mover/mover-tuning-start.sh
chmod +x /mnt/user/appdata/qbt-mover/mover-tuning-end.sh

Mover Tuning Settings

We'll only cover the Mover Tuning settings that are important for qBit-Mover, not every single setting.

In your unRAID Dashboard, go to the Settings tab and select Scheduler in the User Preferences section.

Mover Settings

In the Scheduler under Mover Settings, first set when the mover should run:

Mover Settings

  1. Choose how often you want the mover to run
  2. Choose when you want the mover to run
  3. Enable mover logging if you want to see what's being moved in the log files
  4. Click APPLY to save the settings

Mover Tuning - Plugin Settings

Mover Tuning - Plugin Settings

  1. This prevents the mover from running at the schedule set in Mover Settings. Setting this to "Yes" effectively disables the plugin schedule.
  2. Test Mode (dry run). Enable this the first few times to see what will happen without actually moving files.
  3. We've had reports that enabling this option prevents the mover from moving certain files, so we recommend keeping it disabled.

    Suggested: Disabled

  4. Enable Mover Tuning logging.

  5. Enable or disable notifications for this plugin. Notifications appear in the Unraid GUI.
  6. Default save path for Mover Tuning log files. You can choose a different location if you prefer.
  7. Select how many days old log files (*.log) and text files (*.txt) should be before Mover Tuning deletes them.
  8. Select how many days old list files (*.list) should be before Mover Tuning deletes them.
  9. Show advanced settings options. Make sure this is enabled.

    Suggested: Enabled

  10. If enabled, the mover will run during a parity check or rebuild.

  11. Set the priority for the mover process. Adjusting these options may help if other applications pause or buffer when the mover runs.
  12. Set the priority for the mover process. Adjusting these options may help if other applications pause or buffer when the mover runs.
  13. Path to a script that runs before the mover starts. (This is where we add the path to the mover-tuning-start.sh script.)

    Suggested: /mnt/user/appdata/qbt-mover/mover-tuning-start.sh

  14. Path to a script that runs after the mover finishes. (This is where we add the path to the mover-tuning-end.sh script.)

    Suggested: /mnt/user/appdata/qbt-mover/mover-tuning-end.sh

  15. Select "Yes" to follow plugin filters, or "No" to run the original mover (ignores plugin filters) from the button.

Mover Tuning - Filters

Mover Tuning - Filters

  1. When this percentage threshold is reached, the mover starts moving data off the cache pool.

    If your cache drive is large enough, you could set this to 75%, for example. This lets you seed longer from your cache drive and have all upgrades happen there before moving to your array.

  2. Set the percentage of disk space used on the cache drive after the mover completes. Setting this to 0% means the mover continues until all data is moved off the cache pool.

    Setting this to 50%, for example, frees up space to 50% of your cache capacity and lets you seed longer from your cache drive.

  3. Select whether you want to move files off the Primary (cache) based on their age in days.

    Suggested: Yes

  4. Select how many days old a file must be to move (up to 1 year). "Auto" moves from oldest to newest until the threshold is met.

    Suggested: Auto

  5. Show advanced filter options.

    Suggested: Yes

  6. Use CTIME (creation time) instead of MTIME (modification time) in the find command.

    Suggested: No

  7. Use ATIME (access time) instead of MTIME (modification time) in the find command.

    Suggested: No

  8. Select "Yes" if you want to move files based on their size in MB.

    Suggested: No

  9. Ignore all hidden files and directories.

    Suggested: No

  10. Set to "Yes" if you want to move all files from your cache to the array when the percentage below is exceeded.

  11. Set the percentage of disk space used on the Primary pool that triggers a move of all files from your cache to your array.

Mover Tuning - Options

Mover Tuning - Options

  1. Forces unRAID to switch to turbo write mode (reconstruct write) when the mover runs.

    Suggested: Personal Preference

  2. Choose the tool for moving files with plugin filters. You can select between Rsync or Move (Unraid's built-in file-moving utility).

    • Move - Use Unraid's built-in tool for moving, but continue using Rsync for syncing files.
    • Rsync - Use Rsync for both moving and syncing files.

    Suggested: Personal Preference and what works best for you. Some people report that Move didn't work but Rsync did.

  3. This removes the parent folder only of moved files. "Top Folder" removes the top-level folder of moved files on a share, including all subfolders if they're empty.

    Suggested: Top Folder

  4. Show advanced options.

    Suggested: Yes

  5. Moves files from shares to their Primary and/or Secondary storage if they're spread across Unattended Storage (disks/pools not assigned as a share's Primary or Secondary). May move older files from Primary→Secondary or Secondary→Primary if allowed.

    Suggested: No

  6. Click APPLY to save the settings


Option 2: User Scripts

Install the following plugins:

  • User Scripts
  • Python 3 for unRAID (unRAID Plugin)

qBit-Mover Script

Download the latest qBit-Mover script HERE.

Big thanks to bobokun, the developer of qBit Manage, for creating this script with all the requested changes.

Save the Script to Your Preferred Location

Place the qBit-Mover script somewhere easy to access and remember.

Suggested locations:

  • /mnt/user/appdata/qbt-mover/mover.py
  • /mnt/user/appdata/scripts/mover.py

Install the qbittorrent-api Module

The script needs the qbittorrent-api module to work, so you need to install it when your unRAID server starts or when the Array starts for the first time.

Choose one of the following three methods (select a tab) to install qbittorrent-api.

This method creates a Python virtual environment on your disk. You'll use this to run and store dependencies (qbittorrent-api) for this specific environment.

With this method, you only need to set this up once and it will remain after reboots (unlike the other methods).

First, choose a location for your new Python environment.

Info

In the next steps, you'll choose a location to store the script. Try to keep things organized.

Suggested locations:

  • /mnt/user/appdata/qbt-mover/.venv
  • /mnt/user/appdata/scripts/.venv

Run the following command in unRAID's terminal using the directory you chose:

python3 -m venv --clear /mnt/user/appdata/scripts/.venv

Now enter this new environment and install the dependency (qbittorrent-api):

source /mnt/user/appdata/scripts/.venv/bin/activate
pip3 install qbittorrent-api
deactivate # to exit the environment

Info

Replace /mnt/user/appdata/scripts/.venv with the path you chose.

This method installs the qbittorrent-api module when the Array starts for the first time.

In your unRAID Dashboard, go to the Settings tab and select User Scripts in the User Utilities section at the bottom.

User Scripts

At the bottom of the User Scripts page, click the ADD NEW SCRIPT button.

Add New Script

A popup will ask you to name the script. For this example, use Install qBittorrent-API and click OK.

Install qBittorrent API

Click the cogwheel next to the new script in the list and select Edit Script.

Select user script

Copy and paste the following into the new window that opens, then click SAVE CHANGES.

#!/bin/bash
pip3 install qbittorrent-api

Bash script

In the schedule list, select when the script should run and choose At First Array Start Only.

Set Run Time

Click Apply.

Finally, click RUN IN BACKGROUND or restart your unRAID server to install the qbittorrent-api module.

RUN IN BACKGROUND

This method installs the qbittorrent-api module when the unRAID server starts.

On your USB stick/key, go to /boot/config and open the go file with your text editor (VSCode or Notepad++).

Copy and paste the following command:

pip3 install qbittorrent-api

Restart your unRAID server or run the command above from the terminal.

Set Up the Scheduler

Set up the scheduler for when the mover should run.

In your unRAID Dashboard, go to the Settings tab and select User Scripts in the User Utilities section at the bottom.

User Scripts

At the bottom of the User Scripts page, click the ADD NEW SCRIPT button.

Add New Script

A popup will ask you to name the script. For this example, use qBittorrent Mover and click OK.

qBittorrent Mover

Click the cogwheel next to the new script in the list.

Select user script

Choose your method (select a tab) and copy/paste the script into the new window that opens, then click SAVE CHANGES.

Important: Replace placeholders

Replace ip with your unRAID server IP and port with your qBittorrent WebGUI port.

#!/bin/bash
/usr/local/emhttp/plugins/dynamix/scripts/notify -s "qBittorrent Mover" -d "qBittorrent Mover starting @ `date +%H:%M:%S`."
echo "executing script to pause torrents and run mover."
python3 /mnt/user/appdata/scripts/mover.py --host "ip:port" --user "your_user" --password "your_password" --cache-mount "/mnt/cache" --days_from 0 --days_to 2
echo "qbittorrent-mover completed and resumed all paused torrents."
/usr/local/emhttp/plugins/dynamix/scripts/notify -s "qBittorrent Mover" -d "qBittorrent Mover completed @ `date +%H:%M:%S`."
#!/bin/bash

/usr/local/emhttp/plugins/dynamix/scripts/notify -s "qBittorrent Mover" -d "qBittorrent Mover starting @ `date +%H:%M:%S`."
echo "executing script to pause torrents and run mover."
/mnt/user/appdata/scripts/.venv/bin/python3 /mnt/user/appdata/scripts/mover.py --host "ip:port" --user "your_user" --password "your_password" --cache-mount "/mnt/cache" --days_from 0 --days_to 2
echo "qbittorrent-mover completed and resumed all paused torrents."
/usr/local/emhttp/plugins/dynamix/scripts/notify -s "qBittorrent Mover" -d "qBittorrent Mover completed @ `date +%H:%M:%S`."

Update the script path

Replace /mnt/user/appdata/scripts/ in the script with the path you chose for the Python script (qBit-Mover script).

Script Parameters Explained

Parameter Description
--days_from Set the number of days to stop torrents from for the move
--days_to Set the number of days to stop torrents to for the move
--host Replace ip with your unRAID server IP and port with your qBittorrent WebGUI port
--user Your qBittorrent username (if you have authentication enabled)
--password Your qBittorrent password (if you have authentication enabled)
--cache-mount Cache mount point in Unraid. This filters for only torrents that exist on the cache mount. Use this option ONLY if you follow the TRaSH Guides folder structure. (For the default cache drive, set this to /mnt/cache)

Set the Schedule

Click the schedule dropdown to choose when the script should run. Select Custom.

Set Run Time

After changing to Custom, you'll see an extra text field on the right where you can set your schedule using cron syntax.

For this example, we'll run the script every day at 4 AM:

0 4 * * *

You can create your schedule using crontab guru.

Set Run Time



Questions or Suggestions?

If you have questions or suggestions, click the button below to join our Discord server.

Click For Support
Discord chat