#!/bin/sh # POSIX-only, /bin/sh compatible, ASCII-only # Dry-run by default; use --write to apply changes # Logs: $LOG_DIR, Plan: $PLAN_OUT set -u # Load env if [ -f "/etc/synomap/synomap.env" ]; then . /etc/synomap/synomap.env fi QB_URL="${QB_URL:-}"; QB_USER="${QB_USER:-}"; QB_PASS="${QB_PASS:-}" MAP_FILE="${MAP_FILE:-}"; TORR_ROOT="${TORR_ROOT:-/syno/torrents/completed}" QB_CATEGORIES="${QB_CATEGORIES:-sonarr,radarr}" QB_EXCLUDE_CATEGORIES="${QB_EXCLUDE_CATEGORIES:-hum}" TAG_DONE="${TAG_DONE:-SYNO}"; PLAN_OUT="${PLAN_OUT:-/root/synomap_plan.txt}" LOG_DIR="${LOG_DIR:-/var/log/synomap}"; LOCK_DIR="${LOCK_DIR:-/var/lock/synomap}" UMASK_VAL="${UMASK:-002}"; DIRECTION="${DIRECTION:-LIB_TO_TORRENTS}" QB_TIMEOUT="${QB_TIMEOUT:-10}" QB_FETCH_LIMIT="${QB_FETCH_LIMIT:-500}" QB_FETCH_FACTOR="${QB_FETCH_FACTOR:-20}" umask "$UMASK_VAL"; mkdir -p "$LOG_DIR" "$LOCK_DIR" 2>/dev/null || true DRY=1; WRITE=0; PLAN=""; LIMIT="" log(){ printf "%s %s\n" "$(date +%Y-%m-%dT%H:%M:%S)" "$*" | tee -a "$LOG_DIR/synomap.log"; } nok(){ log "[NOK] $*"; }; ok(){ log "[OK] $*"; }; note(){ log "[NOTE] $*"; } qb_cookie="" qb_login(){ qb_cookie="$(mktemp)" curl -m "$QB_TIMEOUT" -sS -c "$qb_cookie" \ --data-urlencode "username=$QB_USER" \ --data-urlencode "password=$QB_PASS" \ "$QB_URL/api/v2/auth/login" >/dev/null 2>&1 || return 1 return 0 } qb_api(){ m="$1"; shift; p="$1"; shift if [ "$m" = "GET" ]; then curl -m "$QB_TIMEOUT" -sS -b "$qb_cookie" "$QB_URL$p" 2>/dev/null else curl -m "$QB_TIMEOUT" -sS -b "$qb_cookie" -X POST "$QB_URL$p" "$@" 2>/dev/null fi } qb_info_many(){ lim="$QB_FETCH_LIMIT" if [ -n "$LIMIT" ]; then case "$LIMIT" in *[!0-9]*|"") : ;; *) lim=$((LIMIT * QB_FETCH_FACTOR));; esac fi qb_api GET "/api/v2/torrents/info?limit=$lim" } qb_set_location(){ qb_api POST "/api/v2/torrents/setLocation" --data "hashes=$1" --data-urlencode "location=$2"; } qb_add_tags(){ qb_api POST "/api/v2/torrents/addTags" --data "hashes=$1" --data-urlencode "tags=$2"; } qb_recheck(){ qb_api POST "/api/v2/torrents/recheck" --data "hashes=$1"; } poll_state(){ h="$1"; tm="${2:-120}"; i=0 while [ "$i" -lt "$tm" ]; do st="$(qb_api GET "/api/v2/torrents/info?hashes=$h" | tr -d '\r\n')" state="$(printf "%s" "$st" | sed -n 's/.*\"state\":\"\([^\"]*\)\".*/\1/p')" [ -n "$state" ] && log "[STATE] $h = $state" case "$state" in checking*|queuedForChecking) : ;; *) break ;; esac sleep 2; i=$((i+2)) done } while [ $# -gt 0 ]; do case "$1" in --write) DRY=0; WRITE=1; shift;; --dry-run) DRY=1; WRITE=0; shift;; --plan) PLAN="$2"; shift 2;; --limit) LIMIT="$2"; shift 2;; *) note "arg ignored: $1"; shift;; case_esac=true esac done # synomap_apply_from_plan.sh (r8) [ -n "$PLAN" ] && PLAN_IN="$PLAN" || PLAN_IN="$PLAN_OUT" [ -f "$PLAN_IN" ] || { nok "Plan missing: $PLAN_IN"; exit 0; } [ "$DIRECTION" = "LIB_TO_TORRENTS" ] || { nok "Unsupported DIRECTION=$DIRECTION"; exit 0; } ok "Login to qBittorrent" if ! qb_login; then nok "qB login failed"; exit 0; fi i=0 while IFS='|' read -r h srclib dsttor catg; do [ -z "$h" ] && continue i=$((i+1)); log "[STEP] $i hash=$h cat=$catg" if [ "$DRY" -eq 1 ]; then log "MKDIR -p $dsttor" log "HARDLINK from LIB $srclib -> TORR $dsttor (only this torrent)" log "qB:setLocation $h -> $dsttor ; addTags $TAG_DONE ; recheck" continue fi mkdir -p "$dsttor" || { nok "mkdir failed: $dsttor"; continue; } if [ -d "$srclib" ]; then find "$srclib" -type f -print0 | while IFS= read -r -d '' f; do rel="${f#$srclib/}"; out="$dsttor/$rel"; odir="$(dirname "$out")"; mkdir -p "$odir" || true ln "$f" "$out" 2>/dev/null || cp -p "$f" "$out" 2>/dev/null || nok "copy failed: $f" done elif [ -f "$srclib" ]; then odir="$(dirname "$dsttor")"; mkdir -p "$odir" || true ln "$srclib" "$dsttor" 2>/dev/null || cp -p "$srclib" "$dsttor" 2>/dev/null || nok "copy failed: $srclib" else nok "src not found: $srclib" fi qb_set_location "$h" "$dsttor" >/dev/null qb_add_tags "$h" "$TAG_DONE" >/dev/null qb_recheck "$h" >/dev/null 2>&1 || true poll_state "$h" 120 done < "$PLAN_IN" ok "Apply finished"; rm -f "$qb_cookie"; exit 0