diff --git a/README.md b/README.md index e69de29..b17774d 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,49 @@ +# Torrent Mover + +A automated torrent management system for handling completed downloads and seeding management. + +## Features +- Automatic file organization by category (Movies, Games, Apps, Books) +- Seeding management based on ratio/time +- Archive extraction support (RAR, ZIP, 7z) +- Parallel processing +- Checksum verification +- Storage capacity monitoring + +## Installation +```bash +sudo ./install.sh + + + +torrent-mover [options] +Options: + --dry-run Simulate operations + --interactive Confirm before processing + --cache-warmup Pre-generate checksums + + + +Git Hosting +Repository URL: http://192.168.0.236:3000/masterdraco/torrent + +Security +Configuration files stored in /etc/torrent + +Processed logs in /var/log/torrent_* + +Copy + +3. **Repository Structure**: +```bash +. +├── etc +│ └── torrent +│ └── mover.conf +├── usr +│ └── local +│ └── bin +│ └── torrent-mover +├── install.sh +└── README.md + diff --git a/etc/torrent/mover.conf b/etc/torrent/mover.conf new file mode 100644 index 0000000..2d3d225 --- /dev/null +++ b/etc/torrent/mover.conf @@ -0,0 +1,31 @@ +# Transmission credentials +TRANSMISSION_IP="192.168.5.19" +TRANSMISSION_PORT="9091" +TRANSMISSION_USER="" +TRANSMISSION_PASSWORD="" + +# Seeding criteria +SEED_RATIO="2.5" +SEED_TIME="2880" # Minutes + +# Directory mappings +DIR_GAMES_DST="/mnt/dsnas1/Games" +DIR_APPS_DST="/mnt/dsnas1/Apps" +DIR_MOVIES_DST="/mnt/dsnas1/Movies" +DIR_BOOKS_DST="/mnt/dsnas1/Books" +DEFAULT_DST="/mnt/dsnas1/Other" + +# Storage directories (comma-separated) +STORAGE_DIRS="/mnt/dsnas/Movies" + +# Performance settings +PARALLEL_THREADS="16" # Match CPU core count +PARALLEL_PROCESSING=1 + +# Operation mode +COPY_MODE="copy" # move|copy +PROCESSED_LOG="/var/log/torrent_processed.log" +CHECKSUM_DB="/var/lib/torrent/checksums.db" + +# System settings +LOG_FILE="/var/log/torrent_mover.log" diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..cbbb609 --- /dev/null +++ b/install.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +# Git repository configuration +GIT_REPO="http://192.168.0.236:3000/masterdraco/torrent" +REPO_CREDENTIALS="masterdraco:mlvfnj78" + +# Check root privileges +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +# Install dependencies +echo "Checking dependencies..." +declare -A PKGS=( + [transmission-cli]="transmission-remote" + [unrar]="unrar" + [unzip]="unzip" + [p7zip-full]="7z" + [parallel]="parallel" + [bc]="bc" + [git]="git" +) + +for pkg in "${!PKGS[@]}"; do + if ! command -v "${PKGS[$pkg]}" &> /dev/null; then + echo "Installing $pkg..." + apt-get update + apt-get install -y "$pkg" + fi +done + +# Create directory structure +echo "Creating directory structure..." +mkdir -p /etc/torrent +mkdir -p /usr/local/bin + +# Install files +echo "Installing files..." +cp -v etc/torrent/mover.conf /etc/torrent/ +cp -v usr/local/bin/torrent-mover /usr/local/bin/ +chmod +x /usr/local/bin/torrent-mover + +# Set permissions +echo "Setting permissions..." +chmod 600 /etc/torrent/mover.conf +chown root:root /etc/torrent/mover.conf + +# Initialize Git repository +if [ ! -d .git ]; then + echo "Initializing Git repository..." + git init + git config user.name "torrent-mover-installer" + git config user.email "installer@localhost" + git remote add origin "http://$REPO_CREDENTIALS@192.168.0.236:3000/masterdraco/torrent.git" +fi + +# Add files to Git +git add . +git commit -m "Initial installation commit" || true + +# Push to remote repository +echo "Pushing to Git repository..." +git push -u origin master + +echo "" +echo "Installation complete!" +echo "Configuration file: /etc/torrent/mover.conf" +echo "Main script: /usr/local/bin/torrent-mover" diff --git a/usr/local/bin/torrent-mover b/usr/local/bin/torrent-mover new file mode 100755 index 0000000..968f6e3 --- /dev/null +++ b/usr/local/bin/torrent-mover @@ -0,0 +1,300 @@ +#!/bin/bash +# Torrent Mover v5.1 - Numeric Handling Fix + +set -o errexit +set -o nounset +set -o pipefail + +# Load configuration +CONFIG_FILE="/etc/torrent/mover.conf" +source "${CONFIG_FILE}" + +# Runtime flags +DRY_RUN=0 +INTERACTIVE=0 +CACHE_WARMUP=0 + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Initialize storage directories +STORAGE_DIRS_ARRAY=() +if [[ -n "${STORAGE_DIRS}" ]]; then + IFS=',' read -ra STORAGE_DIRS_ARRAY <<< "${STORAGE_DIRS}" +fi + +# Logging functions +log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%F %T') - $*" | tee -a "${LOG_FILE}"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%F %T') - $*" | tee -a "${LOG_FILE}"; } +log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%F %T') - $*" | tee -a "${LOG_FILE}"; } + +# Dependency check +check_dependencies() { + local deps=("transmission-remote" "unrar" "unzip" "7z" "parallel" "bc") + for dep in "${deps[@]}"; do + if ! command -v "${dep}" >/dev/null 2>&1; then + log_error "Missing dependency: ${dep}" + exit 1 + fi + done +} + +# Disk usage monitoring +declare -A CHECKED_MOUNTS=() + +check_disk_usage() { + local dir="$1" + [[ -z "${dir}" ]] && return + + if ! df -P "${dir}" &>/dev/null; then + log_warn "Directory not found: ${dir}" + return + fi + + local mount_point=$(df -P "${dir}" | awk 'NR==2 {print $6}') + [[ -z "${mount_point}" ]] && return + + if [[ -z "${CHECKED_MOUNTS["${mount_point}"]+x}" ]]; then + local usage=$(df -P "${dir}" | awk 'NR==2 {sub(/%/, "", $5); print $5}') + if (( usage >= 90 )); then + log_warn "Storage warning: ${mount_point} at ${usage}% capacity" + fi + CHECKED_MOUNTS["${mount_point}"]=1 + fi +} + +# Checksum database +init_checksum_db() { + mkdir -p "$(dirname "${CHECKSUM_DB}")" + touch "${CHECKSUM_DB}" + chmod 600 "${CHECKSUM_DB}" +} + +record_checksums() { + log_info "Generating checksums with ${PARALLEL_THREADS:-$(nproc)} threads" + find "$@" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -print0 | \ + parallel -0 -j ${PARALLEL_THREADS:-$(nproc)} md5sum | \ + sort > "${CHECKSUM_DB}.tmp" + mv "${CHECKSUM_DB}.tmp" "${CHECKSUM_DB}" +} + +file_metadata() { + find "$1" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -printf "%s %T@ %p\n" | \ + sort | \ + md5sum | \ + awk '{print $1}' +} + +files_need_processing() { + local src="$1" shift + local targets=("$@") + + [[ ! -d "${src}" ]] && return 1 + + local src_meta=$(file_metadata "${src}") + + for target in "${targets[@]}"; do + [[ ! -d "${target}" ]] && continue + local target_meta=$(file_metadata "${target}") + [[ "${src_meta}" == "${target_meta}" ]] && return 1 + done + + local src_checksums=$(find "${src}" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -exec md5sum {} \; | sort) + + for target in "${targets[@]}"; do + [[ ! -d "${target}" ]] && continue + local target_checksums=$(find "${target}" -type f \( -iname "*.nfo" -o -iname "*.sfv" \) -prune -o -type f -exec md5sum {} \; | sort) + diff <(echo "${src_checksums}") <(echo "${target_checksums}") >/dev/null && return 1 + done + + return 0 +} + +warm_cache() { + log_info "Starting cache warmup for Movies..." + local targets=("${DIR_MOVIES_DST}" "${STORAGE_DIRS_ARRAY[@]}") + record_checksums "${targets[@]}" + log_info "Cache warmup completed. Checksums stored in ${CHECKSUM_DB}" +} + +is_processed() { + grep -q "^${1}$" "${PROCESSED_LOG}" 2>/dev/null +} + +mark_processed() { + echo "${1}" >> "${PROCESSED_LOG}" +} + +get_destination() { + case "${1}" in + *Games*) echo "${DIR_GAMES_DST}";; + *Apps*) echo "${DIR_APPS_DST}";; + *Movies*) echo "${DIR_MOVIES_DST}";; + *Books*) echo "${DIR_BOOKS_DST}";; + *) echo "${DEFAULT_DST}";; + esac +} + +handle_archives() { + local src="$1" dst="$2" + find "${src}" -type f \( -iname "*.rar" -o -iname "*.zip" -o -iname "*.7z" \) | \ + while read -r arch; do + log_info "Extracting ${arch##*/}" + case "${arch##*.}" in + rar) unrar x -o- "${arch}" "${dst}";; + zip) unzip -o "${arch}" -d "${dst}";; + 7z) 7z x "${arch}" -o"${dst}";; + esac + done +} + +move_files() { + if (( PARALLEL_PROCESSING )); then + parallel -j ${PARALLEL_THREADS:-$(nproc)} mv {} "${1}" ::: "${2}"/* + else + mv "${2}"/* "${1}" + fi +} + +copy_files() { + if (( PARALLEL_PROCESSING )); then + parallel -j ${PARALLEL_THREADS:-$(nproc)} cp -r {} "${1}" ::: "${2}"/* + else + cp -r "${2}"/* "${1}" + fi +} + +process_copy() { + local id="$1" hash="$2" src="$3" dst="$4" + + if (( DRY_RUN )); then + log_info "[DRY RUN] Would process torrent ${id}:" + log_info " - Copy files from ${src} to ${dst}" + return + fi + + mkdir -p "${dst}" + handle_archives "${src}" "${dst}" + + case "${COPY_MODE}" in + move) move_files "${dst}" "${src}";; + copy) copy_files "${dst}" "${src}";; + esac +} + +process_removal() { + local id="$1" + + if (( DRY_RUN )); then + log_info "[DRY RUN] Would remove torrent ${id}" + return + fi + + transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ + -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" \ + -t "${id}" --remove-and-delete +} + +main() { + check_dependencies + init_checksum_db + + if (( CACHE_WARMUP )); then + warm_cache + exit 0 + fi + + log_info "Starting processing" + declare -A warned_dirs=() + + transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ + -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -l | \ + awk 'NR>1 && $1 ~ /^[0-9]+$/ {print $1}' | \ + while read -r id; do + + local info=$(transmission-remote "${TRANSMISSION_IP}:${TRANSMISSION_PORT}" \ + -n "${TRANSMISSION_USER}:${TRANSMISSION_PASSWORD}" -t "${id}" -i) + local hash=$(grep "Hash:" <<< "${info}" | awk '{print $2}') + + # Sanitize numeric values with fallbacks + local ratio=$(grep "Ratio:" <<< "${info}" | awk '{print $2 == "None" ? 0 : $2}' | tr -cd '0-9.') + ratio=${ratio:-0} # Handle empty values + + local time=$(grep "Seeding Time:" <<< "${info}" | awk '{print $3 == "None" ? 0 : $3}' | tr -cd '0-9.') + time=${time:-0} # Handle empty values + + local percent_done=$(grep "Percent Done:" <<< "${info}" | awk '{gsub(/%/, ""); print $3 == "None" ? 0 : $3}') + percent_done=${percent_done:-0} # Handle empty values + + local dir=$(grep "Location:" <<< "${info}" | cut -d' ' -f4-) + local dst=$(get_destination "${dir}") + + # Initialize warning tracking + [[ -z "${warned_dirs["${dir}"]+x}" ]] && warned_dirs["${dir}"]=0 + + # 1. Handle completed downloads + if (( $(bc <<< "${percent_done} >= 100") )) && ! is_processed "${hash}"; then + log_info "Processing completed torrent ${id} (${percent_done}% done)" + + if [[ "${dst}" == "${DEFAULT_DST}" ]] && (( warned_dirs["${dir}"] == 0 )); then + log_warn "Using default destination for: ${dir}" + warned_dirs["${dir}"]=1 + fi + + # Determine check targets + local targets=("${dst}") + if [[ "${dst}" == "${DIR_MOVIES_DST}" ]]; then + targets+=("${STORAGE_DIRS_ARRAY[@]}") + fi + + if ! files_need_processing "${dir}" "${targets[@]}"; then + log_info "Skipping copy - files already exist in:" + for target in "${targets[@]}"; do + [[ -d "${target}" ]] && log_info " - ${target}" + done + else + process_copy "${id}" "${hash}" "${dir}" "${dst}" + fi + + mark_processed "${hash}" + fi + + # 2. Handle seeding criteria + if (( $(bc <<< "${ratio} >= ${SEED_RATIO}") )) || (( $(bc <<< "${time} >= ${SEED_TIME}") )); then + log_info "Removing torrent ${id} (Ratio: ${ratio}, Time: ${time})" + process_removal "${id}" + fi + done + + # Final disk checks + check_disk_usage "${DIR_GAMES_DST}" + check_disk_usage "${DIR_APPS_DST}" + check_disk_usage "${DIR_MOVIES_DST}" + check_disk_usage "${DIR_BOOKS_DST}" + check_disk_usage "${DEFAULT_DST}" +} + +# Argument handling +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + --interactive) INTERACTIVE=1; shift ;; + --cache-warmup) CACHE_WARMUP=1; shift ;; + --help) + echo "Usage: $0 [--dry-run] [--interactive] [--cache-warmup]" + exit 0 + ;; + *) echo "Invalid option: $1"; exit 1 ;; + esac +done + +# Execution +if (( INTERACTIVE )); then + read -rp "Confirm processing? (y/n) " choice + [[ "${choice}" =~ ^[Yy]$ ]] || exit 0 +fi + +main