master
Sergey Marinkevich 7 months ago
parent 3d0f3f935d
commit b4623d21cf

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,139 +1,56 @@
#!/bin/bash
#==============================================================================
# Модуль действий: выполнение mkvmerge, создание симлинков и т.д.
#==============================================================================
# Функция для склейки видео и аудио
# $1: Исходный видеофайл
# $2: Исходный аудиофайл
# $3: Выходной файл
action_merge_mkv() {
local video_file="$1"
local audio_file="$2"
local output_file="$3"
echo "---"
echo "ИСХОДНОЕ ВИДЕО : $video_file"
echo "ИСХОДНОЕ АУДИО : $audio_file"
echo "РЕЗУЛЬТАТ : $output_file"
# Это основная команда. Вы можете настроить ее под себя.
# --language 0:und - язык первой дорожки (видео) - неопределенный
# --language 1:rus - язык второй дорожки (аудио) - русский
# Вы можете добавить больше опций, например, --track-name, --default-track-flag
mkvmerge \
-o "$output_file" \
--language 0:und "$video_file" \
--language 1:rus "$audio_file"
if [ $? -eq 0 ]; then
echo "УСПЕХ: Файл создан."
else
echo "ОШИБКА: mkvmerge завершился с ошибкой."
fi
echo
}
# Функция для создания символической ссылки
# $1: Исходный видеофайл
# $2: Имя ссылки (выходной файл)
action_create_symlink() {
local video_file="$1"
local output_file="$2"
echo "---"
echo "ИСТОЧНИК: $video_file"
echo "ССЫЛКА : $output_file"
# Создаем символическую ссылку
ln -s "$video_file" "$output_file"
if [ $? -eq 0 ]; then
echo "УСПЕХ: Ссылка создана."
else
echo "ОШИБКА: Не удалось создать ссылку."
fi
echo
}
# ... (action_merge_mkv и action_create_symlink без изменений) ...
# Главная функция запуска обработки
run_processing() {
# 1. Готовим списки файлов
if ! logic_prepare_file_lists; then
return 1
fi
if ! logic_prepare_file_lists; then return 1; fi
if [ ${#FILE_TRIPLETS[@]} -eq 0 ]; then
ui_show_message "Запуск" "Нет файлов для обработки."
return
fi
# 2. Спрашиваем, что делать
local action_to_perform
action_to_perform=$(ui_select_action)
if [[ -z "$action_to_perform" ]]; then
ui_show_message "Отмена" "Операция отменена пользователем."
return
fi
# 3. Финальное подтверждение
local confirmation_text="Вы уверены, что хотите выполнить '${action_to_perform}' для ${#FILE_TRIPLETS[@]} файлов?\n\n"
# Больше не спрашиваем, а используем CURRENT_ACTION
local confirmation_text="Вы уверены, что хотите выполнить '${CURRENT_ACTION}' для ${#FILE_TRIPLETS[@]} файлов?\n\n"
confirmation_text+="Результаты будут сохранены в:\n$OUTPUT_BASE_DIR"
if ! ui_confirm "Финальное подтверждение" "$confirmation_text"; then
ui_show_message "Отмена" "Операция отменена пользователем."
return
fi
# 4. Создаем выходной каталог, если его нет
if [ ! -d "$OUTPUT_BASE_DIR" ]; then
if ui_confirm "Создание каталога" "Каталог '$OUTPUT_BASE_DIR' не существует. Создать его?"; then
mkdir -p "$OUTPUT_BASE_DIR"
if [ $? -ne 0 ]; then
ui_show_message "Ошибка" "Не удалось создать каталог '$OUTPUT_BASE_DIR'."
return
fi
if [ $? -ne 0 ]; then ui_show_message "Ошибка" "Не удалось создать каталог."; return; fi
else
ui_show_message "Отмена" "Операция отменена, так как выходной каталог не существует."
return
ui_show_message "Отмена" "Операция отменена."; return
fi
fi
# 5. Выполнение операции с прогресс-баром
# Выполнение операции с прогресс-баром
local total_files=${#FILE_TRIPLETS[@]}
local current_file=0
(
for triplet in "${FILE_TRIPLETS[@]}"; do
# Рассчитываем прогресс
local progress=$(( 100 * current_file / total_files ))
progress=$(( 100 * current_file / total_files ))
echo "$progress"
echo -e "XXX\n$((current_file + 1)) / $total_files\n$(basename "$triplet" | cut -f1 -d$'\t')\nXXX"
# Обновляем текст в прогресс-баре
echo -e "XXX\n$((current_file + 1)) / $total_files\nОбработка: $(basename "$triplet" | cut -f1 -d$'\t')\nXXX"
# Разбираем триплет
IFS=$'\t' read -r video audio output <<< "$triplet"
# Выполняем выбранное действие
case "$action_to_perform" in
# Выполняем заранее выбранное действие
case "$CURRENT_ACTION" in
"MERGE")
action_merge_mkv "$video" "$audio" "$output"
;;
action_merge_mkv "$video" "$audio" "$output" ;;
"SYMLINK")
action_create_symlink "$video" "$output"
;;
# Для симлинка аудио-файл не нужен
action_create_symlink "$video" "$output" ;;
esac
((current_file++))
done
# Для завершения прогресс-бара
echo "100"
echo -e "XXX\nГотово!\nНажмите Enter для выхода\nXXX"
sleep 2
) | dialog $DIALOG_OPTS --title "Выполнение..." --gauge "Подготовка..." 10 70 0
echo -e "XXX\nГотово!\nНажмите Enter\nXXX"; sleep 2
) | dialog "${DIALOG_OPTS[@]}" --title "Выполнение..." --gauge "Подготовка..." 10 70 0
clear
}

@ -4,104 +4,120 @@
# Модуль основной логики: поиск файлов, сопоставление, генерация имён
#==============================================================================
# Глобальная переменная для хранения подготовленных данных
# Формат: "видеоайл\tаудиоайл\tвыходной_файл" для каждой строки
declare -a FILE_TRIPLETS
# Основная функция, которая находит и сопоставляет файлы
# Возвращает 0 в случае успеха, 1 в случае ошибки
# Функция стала "умной" и проверяет CURRENT_ACTION
logic_prepare_file_lists() {
# Очищаем предыдущие результаты
FILE_TRIPLETS=()
# Проверка, заданы ли все необходимые пути
if [[ -z "$VIDEO_SRC_DIR" || -z "$AUDIO_SRC_DIR" || -z "$OUTPUT_BASE_DIR" || -z "$OUTPUT_SERIES_NAME" ]]; then
ui_show_message "Ошибка" "Не все обязательные параметры заданы (каталоги видео, аудио, вывода и имя сериала)."
return 1
local common_params_ok=true
if [[ -z "$VIDEO_SRC_DIR" || -z "$OUTPUT_BASE_DIR" || -z "$OUTPUT_SERIES_NAME" ]]; then
common_params_ok=false
fi
# 1. Найти видеофайлы
# Использование `find ... -print0` и `mapfile` для безопасной обработки имён с пробелами
# 1. Находим видеофайлы (это нужно для обоих режимов)
local video_list_raw
if $common_params_ok; then
mapfile -d '' video_list_raw < <(find "$VIDEO_SRC_DIR" -maxdepth 1 -name "$VIDEO_FILE_PATTERN" -print0 | sort -z)
fi
# РЕЖИМ: СКЛЕЙКА
if [ "$CURRENT_ACTION" == "MERGE" ]; then
if ! $common_params_ok || [[ -z "$AUDIO_SRC_DIR" ]]; then
ui_show_message "Ошибка" "Для склейки должны быть заданы все каталоги (видео, аудио, вывод) и имя сериала."
return 1
fi
if [ ${#video_list_raw[@]} -eq 0 ]; then
ui_show_message "Ошибка" "Видеофайлы по шаблону '$VIDEO_FILE_PATTERN' в каталоге '$VIDEO_SRC_DIR' не найдены."
ui_show_message "Ошибка" "Видеофайлы по шаблону '$VIDEO_FILE_PATTERN' не найдены."
return 1
fi
# 2. Найти аудиофайлы
# maxdepth не используется, чтобы искать в подкаталогах, как в примерах
local audio_list_raw
mapfile -d '' audio_list_raw < <(find "$AUDIO_SRC_DIR" -name "$AUDIO_FILE_PATTERN" -print0 | sort -z)
if [ ${#audio_list_raw[@]} -eq 0 ]; then
ui_show_message "Ошибка" "Аудиофайлы по шаблону '$AUDIO_FILE_PATTERN' в каталоге '$AUDIO_SRC_DIR' не найдены."
ui_show_message "Ошибка" "Аудиофайлы по шаблону '$AUDIO_FILE_PATTERN' не найдены."
return 1
fi
# 3. Проверить совпадение количества файлов
if [ ${#video_list_raw[@]} -ne ${#audio_list_raw[@]} ]; then
local msg="Количество найденных файлов не совпадает!\n\n"
msg+="Видео: ${#video_list_raw[@]}\n"
msg+="Аудио: ${#audio_list_raw[@]}\n\n"
msg+="Проверьте каталоги и шаблоны поиска."
ui_show_message "Ошибка сопоставления" "$msg"
ui_show_message "Ошибка" "Количество видео (${#video_list_raw[@]}) и аудио (${#audio_list_raw[@]}) не совпадает."
return 1
fi
# 4. Сгенерировать выходные имена и собрать триплеты
local i
# Собираем триплеты
for i in "${!video_list_raw[@]}"; do
local video_file="${video_list_raw[$i]}"
local audio_file="${audio_list_raw[$i]}"
local output_path
output_path=$(logic_generate_output_name "$video_file") || return 1
FILE_TRIPLETS+=("$video_file"$'\t'"$audio_file"$'\t'"$output_path")
done
# РЕЖИМ: СИМЛИНКИ
elif [ "$CURRENT_ACTION" == "SYMLINK" ]; then
if ! $common_params_ok; then
ui_show_message "Ошибка" "Для симлинков должны быть заданы каталог видео, каталог вывода и имя сериала."
return 1
fi
if [ ${#video_list_raw[@]} -eq 0 ]; then
ui_show_message "Ошибка" "Видеофайлы по шаблону '$VIDEO_FILE_PATTERN' не найдены."
return 1
fi
# Собираем пары (аудио-поле оставляем пустым)
for video_file in "${video_list_raw[@]}"; do
local output_path
output_path=$(logic_generate_output_name "$video_file") || return 1
FILE_TRIPLETS+=("$video_file"$'\t\t'"$output_path")
done
fi
return 0
}
# Вспомогательная функция для генерации имени, чтобы не дублировать код
logic_generate_output_name() {
local video_file="$1"
local video_basename
video_basename=$(basename "$video_file")
# Извлекаем номер эпизода с помощью regex
if [[ "$video_basename" =~ $EPISODE_NUMBER_REGEX ]]; then
local episode_num="${BASH_REMATCH[1]}"
# Приводим к формату с ведущим нулём, если нужно (например, 1 -> 01)
episode_num=$(printf "%02d" "$((10#$episode_num))")
else
ui_show_message "Ошибка Regex" "Не удалось извлечь номер эпизода из файла:\n$video_basename\n\nС помощью регулярного выражения:\n$EPISODE_NUMBER_REGEX"
return 1
fi
local episode_num
episode_num=$(printf "%02d" "$((10#${BASH_REMATCH[1]}))")
# Собираем имя выходного файла из шаблона
local output_name="$OUTPUT_FILENAME_TEMPLATE"
output_name="${output_name/\{SERIES_NAME\}/$OUTPUT_SERIES_NAME}"
output_name="${output_name/\{SEASON\}/$SEASON_NUMBER}"
output_name="${output_name/\{EPISODE\}/$episode_num}"
local output_path="${OUTPUT_BASE_DIR}/${output_name}"
# Сохраняем триплет в массив
FILE_TRIPLETS+=("$video_file"$'\t'"$audio_file"$'\t'"$output_path")
done
echo "${OUTPUT_BASE_DIR}/${output_name}"
return 0
}
# Функция для отображения предпросмотра
logic_show_preview() {
if ! logic_prepare_file_lists; then
# Сообщение об ошибке уже было показано внутри logic_prepare_file_lists
else
ui_show_message "Ошибка Regex" "Не удалось извлечь номер эпизода из файла:\n$video_basename\n\nС помощью регулярного выражения:\n$EPISODE_NUMBER_REGEX"
return 1
fi
}
# Предпросмотр тоже адаптируется к режиму
logic_show_preview() {
if ! logic_prepare_file_lists; then return 1; fi
if [ ${#FILE_TRIPLETS[@]} -eq 0 ]; then
ui_show_message "Предпросмотр" "Нет файлов для обработки."
ui_show_message "Предпросмотр" "Нет файлов для обработки по текущим настройкам."
return
fi
local preview_text="Будут обработаны следующие файлы (${#FILE_TRIPLETS[@]} шт.):\n\n"
local preview_text="РЕЖИМ: $CURRENT_ACTION\nБудут обработаны следующие файлы (${#FILE_TRIPLETS[@]} шт.):\n\n"
local count=1
for triplet in "${FILE_TRIPLETS[@]}"; do
IFS=$'\t' read -r video audio output <<< "$triplet"
preview_text+="$(printf "%02d" $count). \n"
if [ "$CURRENT_ACTION" == "MERGE" ]; then
preview_text+=" \ZbВИДЕО:\Zn $(basename "$video")\n"
preview_text+=" \ZbАУДИО:\Zn $(basename "$audio")\n"
preview_text+=" \Zb-> ВЫВОД:\Zn $(basename "$output")\n\n"
else # SYMLINK
preview_text+=" \ZbИСТОЧНИК:\Zn $(basename "$video")\n"
preview_text+=" \Zb-> ССЫЛКА:\Zn $(basename "$output")\n\n"
fi
((count++))
done

@ -2,15 +2,14 @@
#==============================================================================
# Модуль интерфейса (TUI) на основе 'dialog'
# ИСПРАВЛЕНА ПЕРЕДАЧА АРГУМЕНТОВ ЧЕРЕЗ МАССИВ
# ИСПРАВЛЕНА ВЫСОТА ДИНАМИЧЕСКОГО МЕНЮ
#==============================================================================
# Используем МАССИВ для опций, чтобы избежать проблем с кавычками и пробелами.
DIALOG_OPTS=(--colors --backtitle "Универсальный обработчик медиа")
# Функция-обертка для вызова dialog и корректного возврата результата.
# $1: Исходное значение (для возврата при отмене)
# $2, $3, ...: Команда dialog и ее аргументы
# ... (эта функция остается без изменений)
_call_dialog() {
local original_value="$1"
shift
@ -26,15 +25,30 @@ _call_dialog() {
fi
}
# Показать начальное меню выбора действия
# ... (эта функция остается без изменений)
ui_select_initial_action() {
_call_dialog "" \
dialog "${DIALOG_OPTS[@]}" --title "Выбор действия" \
--cancel-label "Выход" \
--menu "Выберите, что вы хотите сделать:" 15 70 2 \
"MERGE" "Склеить видео и аудио (mkvmerge)" \
"SYMLINK" "Создать симлинки с новыми именами"
}
# Показать главное меню
ui_main_menu() {
# Передаем массив опций как "${DIALOG_OPTS[@]}"
# --- КЛЮЧЕВОЕ ИЗМЕНЕНИЕ ЗДЕСЬ ---
# Заменяем фиксированную высоту меню "15" на "0".
# "0" говорит dialog автоматически рассчитать высоту на основе количества пунктов.
_call_dialog "" \
dialog "${DIALOG_OPTS[@]}" --title "Главное меню" \
--menu "Выберите опцию для редактирования или действия:" 20 85 15 "${@}"
--cancel-label "Назад" \
--menu "Выберите опцию для редактирования или действия:" 20 85 0 "${@}"
}
# Получить путь к каталогу
# ... (эта функция остается без изменений)
ui_get_directory() {
local title="$1"
local current_path="$2"
@ -47,6 +61,7 @@ ui_get_directory() {
}
# Получить текстовый ввод от пользователя
# ... (остальные функции остаются без изменений)
ui_get_input() {
local title="$1"
local current_value="$2"
@ -55,31 +70,15 @@ ui_get_input() {
dialog "${DIALOG_OPTS[@]}" --title "$title" --inputbox "Введите новое значение:" 10 70 "$current_value"
}
# Показать меню выбора действия перед запуском
ui_select_action() {
_call_dialog "" \
dialog "${DIALOG_OPTS[@]}" --title "Выбор действия" \
--menu "Какую операцию выполнить с найденными файлами?" 15 70 2 \
"MERGE" "Склеить видео и аудио с помощью mkvmerge" \
"SYMLINK" "Создать символические ссылки на видеофайлы"
}
# --- Функции без возврата значения ---
# Показать информационное сообщение
ui_show_message() {
local title="$1"
local text="$2"
# Здесь тоже исправляем на массив
dialog "${DIALOG_OPTS[@]}" --title "$title" --msgbox "$text" 20 70
}
# Показать окно с выбором Да/Нет
ui_confirm() {
local title="$1"
local text="$2"
# Здесь тоже исправляем на массив
dialog "${DIALOG_OPTS[@]}" --title "$title" --yesno "$text" 10 70
return $?
}

@ -5,65 +5,109 @@
#==============================================================================
# --- Подключение модулей ---
# Убедимся, что скрипты-модули находятся в том же каталоге
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
source "$SCRIPT_DIR/core_ui.sh"
source "$SCRIPT_DIR/core_logic.sh"
source "$SCRIPT_DIR/core_actions.sh"
# --- Конфигурация по умолчанию (можно менять) ---
# Эти пути будут предлагаться по умолчанию в диалогах выбора
# --- Конфигурация по умолчанию ---
DEFAULT_ROOT_PATH="/var/www/nextcloud/data/grayhook/files/"
DEFAULT_TORRENTS_DIR="${DEFAULT_ROOT_PATH}/Torrents"
DEFAULT_ARCHIVE_DIR="${DEFAULT_ROOT_PATH}/Archive/Anime"
# --- Переменные состояния (будут меняться через интерфейс) ---
# --- Переменные состояния ---
# ... (все переменные состояния остаются без изменений)
VIDEO_SRC_DIR=""
AUDIO_SRC_DIR=""
OUTPUT_BASE_DIR=""
OUTPUT_SERIES_NAME=""
SEASON_NUMBER="01"
# Паттерны для поиска файлов (можно использовать find-совместимые wildcards)
VIDEO_FILE_PATTERN="*.mkv"
AUDIO_FILE_PATTERN="*.mka"
# Регулярное выражение для извлечения номера серии из ИМЕНИ ВИДЕОФАЙЛА
# Использует синтаксис ERE (sed -E). Группа захвата (в скобках) должна поймать номер.
EPISODE_NUMBER_REGEX='.* ([0-9]{2}) .*'
# Шаблон для имени выходного файла. Заполнители будут заменены.
# {SERIES_NAME} - Имя сериала
# {SEASON} - Номер сезона (с ведущим нулём)
# {EPISODE} - Номер эпизода (с ведущим нулём, извлечённый regex'ом)
OUTPUT_FILENAME_TEMPLATE="{SERIES_NAME} s{SEASON}e{EPISODE}.mkv"
CURRENT_ACTION=""
# --- НОВАЯ ВСПОМОГАТЕЛЬНАЯ ФУНКЦИЯ ---
# Обрезает путь для красивого отображения в меню, оставляя конец строки.
# $1: Полный путь
# $2: Максимальная длина отображения
# $3: Текст, если путь не задан
truncate_path_for_display() {
local full_path="$1"
local max_len="$2"
local placeholder="$3"
if [[ -z "$full_path" ]]; then
echo "$placeholder"
return
fi
if [ ${#full_path} -gt "$max_len" ]; then
# Вычисляем, сколько символов с конца нужно оставить
# -3 нужно, чтобы вместить "..." в начале
local trim_len=$((max_len - 3))
echo "...${full_path: -$trim_len}"
else
echo "$full_path"
fi
}
# --- Главный цикл программы ---
main() {
# 1. ВНЕШНИЙ ЦИКЛ: ВЫБОР ДЕЙСТВИЯ
while true; do
CURRENT_ACTION=$(ui_select_initial_action)
if [[ -z "$CURRENT_ACTION" ]]; then
clear
echo "Выход."
break
fi
# 2. ВНУТРЕННИЙ ЦИКЛ: НАСТРОЙКА И ЗАПУСК
while true; do
# Формируем пункты меню с текущими значениями
menu_items=(
"V" "Видео каталог : ${VIDEO_SRC_DIR:-_не задан_}"
"A" "Аудио каталог : ${AUDIO_SRC_DIR:-_не задан_}"
"O" "Выходной каталог : ${OUTPUT_BASE_DIR:-_не задан_}"
# --- ИЗМЕНЕНИЯ ЗДЕСЬ ---
# Готовим пути для отображения с помощью новой функции
local max_display_len=45 # Можете поменять это значение под свой экран
local display_video_path=$(truncate_path_for_display "$VIDEO_SRC_DIR" "$max_display_len" "_не задан_")
local display_audio_path=$(truncate_path_for_display "$AUDIO_SRC_DIR" "$max_display_len" "_не задан_")
local display_output_path=$(truncate_path_for_display "$OUTPUT_BASE_DIR" "$max_display_len" "_не задан_")
# Формируем пункты меню ДИНАМИЧЕСКИ
local menu_items=()
menu_items+=("V" "Видео каталог : $display_video_path")
if [ "$CURRENT_ACTION" == "MERGE" ]; then
menu_items+=("A" "Аудио каталог : $display_audio_path")
fi
menu_items+=(
"O" "Выходной каталог : $display_output_path"
"N" "Имя для Plex : ${OUTPUT_SERIES_NAME:-_не задано_}"
"S" "Номер сезона : $SEASON_NUMBER"
"" "--- Шаблоны и Regex ---"
"P" "Шаблон видеофайлов : $VIDEO_FILE_PATTERN"
"U" "Шаблон аудиофайлов : $AUDIO_FILE_PATTERN"
)
if [ "$CURRENT_ACTION" == "MERGE" ]; then
menu_items+=("U" "Шаблон аудиофайлов : $AUDIO_FILE_PATTERN")
fi
menu_items+=(
"R" "Regex номера серии : $EPISODE_NUMBER_REGEX"
"T" "Шаблон имени вывода : $OUTPUT_FILENAME_TEMPLATE"
"" "--- Действия ---"
"VIEW" "Предпросмотр сопоставления файлов"
"RUN" "ЗАПУСТИТЬ обработку"
"BACK" "<- Назад к выбору действия"
)
choice=$(ui_main_menu "${menu_items[@]}")
# Выход из скрипта по кнопке Cancel или Esc
if [[ -z "$choice" ]]; then
clear
echo "Выход."
# ... (остальная часть цикла main остается без изменений) ...
if [[ "$choice" == "BACK" || -z "$choice" ]]; then
break
fi
@ -77,26 +121,16 @@ main() {
U) AUDIO_FILE_PATTERN=$(ui_get_input "Введите шаблон для поиска аудио (*.mka, *.ac3)" "$AUDIO_FILE_PATTERN") ;;
R) EPISODE_NUMBER_REGEX=$(ui_get_input "Введите ERE-regex для номера серии" "$EPISODE_NUMBER_REGEX") ;;
T) OUTPUT_FILENAME_TEMPLATE=$(ui_get_input "Введите шаблон имени выходного файла" "$OUTPUT_FILENAME_TEMPLATE") ;;
VIEW)
# Вызываем предпросмотр
logic_show_preview
;;
RUN)
# Вызываем меню выбора действия и запускаем обработку
run_processing
;;
VIEW) logic_show_preview ;;
RUN) run_processing ;;
esac
done
done
}
# --- Точка входа ---
# Проверка наличия dialog
if ! command -v dialog &> /dev/null; then
echo "Команда 'dialog' не найдена. Пожалуйста, установите ее."
echo "sudo apt-get install dialog (Debian/Ubuntu)"
exit 1
fi
main

Loading…
Cancel
Save