initial commit
This commit is contained in:
parent
f28e165b8a
commit
42b2977fca
49
README.md
49
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
|
||||||
|
|
31
etc/torrent/mover.conf
Normal file
31
etc/torrent/mover.conf
Normal file
@ -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"
|
70
install.sh
Normal file
70
install.sh
Normal file
@ -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"
|
300
usr/local/bin/torrent-mover
Executable file
300
usr/local/bin/torrent-mover
Executable file
@ -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
|
Loading…
x
Reference in New Issue
Block a user