Загрузить файлы в «/»

This commit is contained in:
2026-06-15 00:00:55 +03:00
parent 9ca1e0f8b4
commit 371f8cfc0c

420
torrent_sort.sh Normal file
View File

@ -0,0 +1,420 @@
#!/bin/bash
# --- НАСТРОЙКИ ---
BASE_LOG_DIR="/data/torrents"
mkdir -p "$BASE_LOG_DIR"
LOG_FILE="$BASE_LOG_DIR/process.log"
ERROR_LOG="$BASE_LOG_DIR/error.log"
COLLISION_LOG="$BASE_LOG_DIR/collision.log"
# Куда складывать готовое (в подпапки)
ANIME_DEST="/data/media/anime"
# Куда складывать, если скрипт не смог распарсить имя
ANIME_UNSORTED="/data/media/anime/000_anime_unsorted"
# --- ФУНКЦИИ ЛОГИРОВАНИЯ ---
timestamp() { date '+%Y-%m-%d %H:%M:%S'; }
log_info() {
echo "[$(timestamp)] [INFO] $1" >> "$LOG_FILE"
}
log_error() {
echo "[$(timestamp)] [ERROR] $1" >> "$ERROR_LOG"
# Дублируем ошибку в основной лог для контекста
echo "[$(timestamp)] [ERROR] $1" >> "$LOG_FILE"
}
log_collision() {
echo "[$(timestamp)] [SKIP] Target exists: '$1' | Source was: '$2'" >> "$COLLISION_LOG"
echo "[$(timestamp)] [SKIP] Collision: $1" >> "$LOG_FILE"
}
# Логируем новую задачу
log_info "----------------------------------------------"
log_info "Torrent: $1"
log_info "Category: $2"
REAL_PATH=$3
# Проверка на существование пути
if [ ! -e "$REAL_PATH" ]; then
log_error "Path does not exist: $REAL_PATH"
exit 1
fi
# ==========================================
# БЛОК ОБРАБОТКИ АНИМЕ
# ==========================================
if [[ $2 == 'Anime' ]]; then
log_info "Processing Anime category..."
# Создаем папки назначения, если их нет
mkdir -p "$ANIME_DEST"
mkdir -p "$ANIME_UNSORTED"
# Ищем видеофайлы (mkv, mp4, avi и т.д.)
# Используем while read, чтобы корректно работать с пробелами в путях
# Ищем все видеофайлы
find "$REAL_PATH" -type f | grep -iE "\.(mkv|mp4|avi|webm)$" | while read -r FILE_PATH; do
FILENAME=$(basename "$FILE_PATH")
EXT="${FILENAME##*.}"
# 1. НОРМАЛИЗАЦИЯ: Заменяем _ на пробелы
NORMALIZED_NAME="${FILENAME//_/ }"
# 2. УДАЛЕНИЕ РАСШИРЕНИЯ (чтобы не мешало парсить хвост)
BASE_NAME="${NORMALIZED_NAME%.*}"
# 3. Trim (Убираем пробелы/точки в начале и конце сразу, чтобы case работал четко)
BASE_NAME=$(echo "$BASE_NAME" | sed 's/^[. ,]*//;s/[. ,]*$//')
# Переменные по умолчанию
MODE="Unsorted" # По умолчанию считаем фильмом, если не доказано обратное (сериал)
TITLE=""
EPISODE=""
# =======================================================
# ГЛАВНЫЙ БЛОК ПАРСИНГА ПО ГРУППАМ (CASE)
# =======================================================
case "$BASE_NAME" in
# === ГРУППА 1: Стандартные [Group] ===
*"[SubsPlease]"*|*"[Erai-raws]"*|*"[HorribleSubs]"*|*"[Anime Time]"*)
# 1. Удаляем только тег группы в начале
TEMP_NAME="${BASE_NAME#*]}"
TEMP_NAME=$(echo "$TEMP_NAME" | sed 's/^[ ]*//')
# 2. РАЗБИВАЕМ ПО ПОСЛЕДНЕМУ ТИРЕ
# ^(.+) -> Группа 1: Название (включая годы, скобки и т.д.)
# [[:space:]]-[[:space:]] -> Разделитель
# (.+) -> Группа 2: "Грязный" номер эпизода + тех. инфа
if [[ "$TEMP_NAME" =~ ^(.+)[[:space:]]-[[:space:]](.+)$ ]]; then
RAW_TITLE="${BASH_REMATCH[1]}" # Spice and Wolf (2024)
RAW_TAIL="${BASH_REMATCH[2]}" # 01 (1080p) [Hash]
# 3. ЧИСТИМ ТОЛЬКО ХВОСТ
# Здесь мы можем быть агрессивными, так как год остался в RAW_TITLE
# Удаляем всё, что начинается с ( или [
CLEAN_TAIL=$(echo "$RAW_TAIL" | sed -E 's/[ ]*(\(|\[).+$//')
# Trim пробелов
CLEAN_TAIL=$(echo "$CLEAN_TAIL" | sed 's/^[. ,]*//;s/[. ,]*$//')
# 4. АНАЛИЗИРУЕМ ЧИСТЫЙ ХВОСТ
# Вариант А: Это цифры (01, 01.5, 01v2)
if [[ "$CLEAN_TAIL" =~ ^[0-9]+(\.[0-9]+)?[vV]?[0-9]*$ ]]; then
MODE="Episode"
TITLE="$RAW_TITLE"
EPISODE="$CLEAN_TAIL"
# Вариант Б: Это Спешл (OVA, SP, Special)
elif [[ "$CLEAN_TAIL" =~ ^([sS]pecial|[sS][pP]|[oO][vV][aA]|[oO][aA][dD]) ]]; then
MODE="Episode"
TITLE="$RAW_TITLE"
EPISODE="$CLEAN_TAIL"
# Вариант В: Это просто текст (значит это Фильм с подзаголовком)
# Пример: Blue Lock - Episode Nagi
else
MODE="Movie"
# Для фильма собираем название обратно: Заголовок + Очищенный подзаголовок
TITLE="$RAW_TITLE - $CLEAN_TAIL"
fi
else
# Тире нет вообще -> Это Фильм
MODE="Movie"
# Тут придется чистить аккуратно (удаляем только хеш и разрешение)
# Удаляем [Hash] в конце (8 символов)
TITLE=$(echo "$TEMP_NAME" | sed -E 's/[ ]*\[[a-fA-F0-9]{8}\]$//')
# Удаляем (1080p/720p/etc)
TITLE=$(echo "$TITLE" | sed -E 's/[ ]*\([0-9]{3,4}[pP]\).*$//')
# Удаляем (BD/Web)
TITLE=$(echo "$TITLE" | sed -E 's/[ ]*\((BD|Web|WEB|Blu-ray).+$//')
fi
;;
# # === ГРУППА 1: Стандартные (SubsPlease, Erai-raws, HorribleSubs) ===
# # Формат: [Group] Title - 01 (1080p)...
# *"[SubsPlease]"*|*"[Erai-raws]"*|*"[HorribleSubs]"*)
# # 1. УДАЛЕНИЕ ГРУППЫ (NATIVE BASH)
# # Удаляем всё от начала до первой закрывающей скобки ]
# TEMP_NAME="${BASE_NAME#*]}"
# # Удаляем пробел в начале, если остался
# TEMP_NAME=$(echo "$TEMP_NAME" | sed 's/^[ ]*//')
# # Удаляем техническую инфу в конце (1080p, Hash) ПЕРЕД анализом
# TEMP_NAME=$(echo "$TEMP_NAME" | sed -E 's/[ ]*(\(|\[)[0-9a-zA-Z].*$//')
# # Trim на всякий случай
# TEMP_NAME=$(echo "$TEMP_NAME" | sed 's/^[. ,]*//;s/[. ,]*$//')
# # Мы разбиваем строку по ПОСЛЕДНЕМУ " - "
# # Группа 1: Всё до тире (Название)
# # Группа 2: Всё после тире (Потенциальный номер)
# if [[ "$TEMP_NAME" =~ ^(.+)[[:space:]]-[[:space:]](.+)$ ]]; then
# CANDIDATE_TITLE="${BASH_REMATCH[1]}"
# CANDIDATE_EP="${BASH_REMATCH[2]}"
# # --- АНАЛИЗ ТОГО, ЧТО ПОСЛЕ ТИРЕ ---
# # 1. СТАНДАРТ: Начинается с цифры (01, 01.5, 01v2)
# if [[ "$CANDIDATE_EP" =~ ^[0-9]+(\.[0-9]+)?[vV]?[0-9]*$ ]]; then
# MODE="Episode"
# TITLE="$CANDIDATE_TITLE"
# EPISODE="$CANDIDATE_EP"
# # 2. СПЕШЛЫ: Содержит ключевые слова (Special, SP, OVA, OAD)
# # [sS]pecial - регистронезависимый поиск первой буквы
# # Поддерживает: "Special 01", "SP1", "OVA", "Egghead SP1"
# elif [[ "$CANDIDATE_EP" =~ ([sS]pecial|[sS][pP]|[oO][vV][aA]|[oO][aA][dD]) ]]; then
# MODE="Episode"
# TITLE="$CANDIDATE_TITLE"
# EPISODE="$CANDIDATE_EP"
# else
# # После тире текст без цифр и без ключевых слов (напр. "The Last Attack")
# # Значит это подзаголовок фильма
# MODE="Movie"
# TITLE=$(echo "$TEMP_NAME" | sed -E 's/[ ]*\(.+//')
# fi
# else
# # Тире вообще нет
# MODE="Movie"
# TITLE=$(echo "$TEMP_NAME" | sed -E 's/[ ]*\(.+//')
# fi
# ;;
# === ГРУППА 2: Hi10 и подобные (в круглых скобках) ===
# Формат: (Hi10) Title - 01 (Quality)...
# Или: (Hi10) Title - ED01 ...
# *"(Hi10)"*)
#
# # 1. Удаляем тег группы (xxxx)
# TEMP_NAME=$(echo "$BASE_NAME" | sed -E 's/^\(.+?\)[ ]*//')
#
# # 2. Паттерн сериала (тут разрешаем буквы в номере, т.к. Hi10 использует ED, NCOP)
# if [[ "$TEMP_NAME" =~ ^(.+)[[:space:]]-[[:space:]]([a-zA-Z0-9]+) ]]; then
# MODE="Episode"
# TITLE="${BASH_REMATCH[1]}"
# EPISODE="${BASH_REMATCH[2]}"
# else
# # Фильм
# # Удаляем тех инфу, начинающуюся с (BD... или (1080p...
# MODE="Movie"
# TITLE=$(echo "$TEMP_NAME" | sed -E 's/[ ]*\((BD|1080p|720p).+//')
# fi
# ;;
# === ГРУППА 3: Китайские/Нумерованные [01] ===
# "["[0-9][0-9]"]"*|"["[0-9][0-9][0-9]"]"*)
# # Регулярка вытаскивает номер из скобок
# if [[ "$BASE_NAME" =~ ^\[([0-9]+)\][[:space:]]*(.+) ]]; then
# MODE="Episode"
# EPISODE="${BASH_REMATCH[1]}"
# TITLE="${BASH_REMATCH[2]}"
# fi
# ;;
# === FALLBACK: Если группа неизвестна ===
*)
MODE="Unsorted"
log_error "Unknown Group/Format for '$FILENAME'. Logic set to Unsorted."
;;
esac
# =======================================================
# ОБРАБОТКА РЕЗУЛЬТАТОВ
# =======================================================
# Если мы так и не поняли, что это (Unsorted) - сразу выход
if [[ "$MODE" == "Unsorted" ]]; then
ln "$FILE_PATH" "$ANIME_UNSORTED/$FILENAME" 2>> "$ERROR_LOG"
continue
fi
# =======================================================
# КОНЕЦ ПАРСИНГА. ДАЛЕЕ ОБЩАЯ ОБРАБОТКА
# =======================================================
# 1. TITLE CASE (Нормализация регистра для Windows)
TITLE=$(echo "$TITLE" | tr '[:upper:]' '[:lower:]')
TITLE=$(echo "$TITLE" | awk '{for(i=1;i<=NF;i++){ $i=toupper(substr($i,1,1)) substr($i,2) }}1')
# 2. ЧИСТКА ОТ СПЕЦСИМВОЛОВ WINDOWS
TITLE=$(echo "$TITLE" | tr -d ':?"<>|*\\/')
# Trim (пробелы по краям)
TITLE=$(echo "$TITLE" | sed 's/^[. ,]*//;s/[. ,]*$//')
# 3. SANITY CHECK (ПРОВЕРКА НА ОШИБКИ)
if [[ -z "$TITLE" ]] || [[ ${#TITLE} -lt 2 ]]; then
log_error "Parsed Title is empty/short ('$TITLE') for '$FILENAME'. Moving to Unsorted."
ln "$FILE_PATH" "$ANIME_UNSORTED/$FILENAME" 2>> "$ERROR_LOG"
continue
fi
# 4. СБОРКА ИМЕНИ
if [[ "$MODE" == "Episode" ]]; then
FINAL_FILENAME="$TITLE - $EPISODE.$EXT"
else
FINAL_FILENAME="$TITLE.$EXT"
fi
# 5. СОЗДАНИЕ ПАПОК И ССЫЛОК
TARGET_DIR="$ANIME_DEST/$TITLE"
TARGET_FILE="$TARGET_DIR/$FINAL_FILENAME"
INFO_TXT="$TARGET_DIR/info.txt"
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
echo "=== Anime Info: $TITLE ===" > "$INFO_TXT"
fi
# if [ ! -f "$TARGET_FILE" ]; then
# if ln "$FILE_PATH" "$TARGET_FILE" 2>/tmp/ln_error.$$; then
# ln "$FILE_PATH" "$TARGET_FILE"
# if [ $? -eq 0 ]; then
# log_info "SUCCESS ($MODE): '$FINAL_FILENAME'"
# {
# echo "------------------------------------------------"
# echo "Date Processed: $(date '+%Y-%m-%d %H:%M:%S')"
# echo "Type: $MODE"
# echo "Episode: ${EPISODE:-N/A}"
# echo "Linked File: $FINAL_FILENAME"
# echo "Source File: $FILENAME"
# echo "Source Torrent: $1"
# } >> "$INFO_TXT"
# else
# ERR_MSG=$(cat /tmp/ln_error.$$)
# rm -f /tmp/ln_error.$$
# log_error "Link failed for '$FINAL_FILENAME' | Source='$FILE_PATH' | Target='$TARGET_FILE' | Error='$ERR_MSG'"
# # log_error "Link failed for '$FINAL_FILENAME'. Filesystem error?"
# fi
# else
# # Записываем в отдельный лог коллизий
# log_collision "$FINAL_FILENAME" "$FILENAME"
# fi
# if [ ! -f "$TARGET_FILE" ]; then
# TMP_ERR="/tmp/torrent_sort_ln_$$.err"
# if ln "$FILE_PATH" "$TARGET_FILE" 2>"$TMP_ERR"; then
# log_info "SUCCESS ($MODE): '$FINAL_FILENAME'"
# {
# echo "------------------------------------------------"
# echo "Date Processed: $(date '+%Y-%m-%d %H:%M:%S')"
# echo "Type: $MODE"
# echo "Episode: ${EPISODE:-N/A}"
# echo "Linked File: $FINAL_FILENAME"
# echo "Source File: $FILENAME"
# echo "Source Torrent: $1"
# } >> "$INFO_TXT"
# else
# LN_ERROR=$(cat "$TMP_ERR" 2>/dev/null)
# SRC_STAT=$(stat -c 'inode=%i uid=%u gid=%g perms=%a owner=%U group=%G' "$FILE_PATH" 2>/dev/null)
# DST_DIR_STAT=$(stat -c 'inode=%i uid=%u gid=%g perms=%a owner=%U group=%G' "$TARGET_DIR" 2>/dev/null)
# log_error "Link failed for '$FINAL_FILENAME'
#Source='$FILE_PATH'
#Target='$TARGET_FILE'
#ln_error='$LN_ERROR'
#source_stat='$SRC_STAT'
#target_dir_stat='$DST_DIR_STAT'"
# fi
# rm -f "$TMP_ERR"
# else
#
# log_collision "$FINAL_FILENAME" "$FILENAME"
# fi
if [ ! -f "$TARGET_FILE" ]; then
if [ ! -w "$TARGET_DIR" ]; then
DST_DIR_STAT=$(stat -c 'inode=%i uid=%u gid=%g perms=%a owner=%U group=%G' "$TARGET_DIR" 2>/dev/null)
log_error "Target directory is not writable
Source='$FILE_PATH'
Target='$TARGET_FILE'
target_dir_stat='$DST_DIR_STAT'
current_user='$(id -un)'
current_group='$(id -gn)'
current_uid='$(id -u)'
current_gid='$(id -g)'"
continue
fi
TMP_ERR="/tmp/torrent_sort_ln_$$.err"
if ln "$FILE_PATH" "$TARGET_FILE" 2>"$TMP_ERR"; then
log_info "SUCCESS ($MODE): '$FINAL_FILENAME'"
{
echo "------------------------------------------------"
echo "Date Processed: $(date '+%Y-%m-%d %H:%M:%S')"
echo "Type: $MODE"
echo "Episode: ${EPISODE:-N/A}"
echo "Linked File: $FINAL_FILENAME"
echo "Source File: $FILENAME"
echo "Source Torrent: $1"
} >> "$INFO_TXT"
else
LN_ERROR=$(cat "$TMP_ERR" 2>/dev/null)
SRC_STAT=$(stat -c 'inode=%i uid=%u gid=%g perms=%a owner=%U group=%G' "$FILE_PATH" 2>/dev/null)
DST_DIR_STAT=$(stat -c 'inode=%i uid=%u gid=%g perms=%a owner=%U group=%G' "$TARGET_DIR" 2>/dev/null)
log_error "Link failed for '$FINAL_FILENAME'
Source='$FILE_PATH'
Target='$TARGET_FILE'
ln_error='$LN_ERROR'
source_stat='$SRC_STAT'
target_dir_stat='$DST_DIR_STAT'
current_user='$(id -un)'
current_group='$(id -gn)'
current_uid='$(id -u)'
current_gid='$(id -g)'"
fi
rm -f "$TMP_ERR"
else
log_collision "$FINAL_FILENAME" "$FILENAME"
fi
done
fi