From 371f8cfc0cacf8d29ce9637a41131ab2bd4f6905 Mon Sep 17 00:00:00 2001 From: volandsz Date: Mon, 15 Jun 2026 00:00:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- torrent_sort.sh | 420 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 torrent_sort.sh diff --git a/torrent_sort.sh b/torrent_sort.sh new file mode 100644 index 0000000..81dd2ce --- /dev/null +++ b/torrent_sort.sh @@ -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