Добавить функциональность репортинга прогресса обработки аудиокниг и экран конфигурации
This commit is contained in:
28
cmd/main.go
28
cmd/main.go
@@ -60,6 +60,10 @@ func main() {
|
||||
rutrackerRepo,
|
||||
logger,
|
||||
)
|
||||
// Подключаем репортер прогресса к TUI
|
||||
processUseCase.SetProgressReporter(func(s entities.ProcessingStatus) {
|
||||
tuiManager.SendStatusUpdate(s)
|
||||
})
|
||||
|
||||
// Создание процессора для обработки команд
|
||||
processor := &ApplicationProcessor{
|
||||
@@ -102,36 +106,14 @@ func (p *ApplicationProcessor) StartProcessing() {
|
||||
p.logger.Info("Запуск обработки аудиокниг...")
|
||||
}
|
||||
|
||||
// Обновим UI статус
|
||||
p.tuiManager.SendStatusUpdate(entities.ProcessingStatus{
|
||||
Current: 0,
|
||||
Total: 1,
|
||||
Status: "Начинаем обработку...",
|
||||
Error: nil,
|
||||
})
|
||||
|
||||
// Запускаем обработку
|
||||
// Запускаем обработку (репортинг прогресса выполняет use case)
|
||||
if err := p.processUseCase.Execute(p.config); err != nil {
|
||||
if p.logger != nil {
|
||||
p.logger.Error("Ошибка обработки: %v", err)
|
||||
}
|
||||
p.tuiManager.SendStatusUpdate(entities.ProcessingStatus{
|
||||
Current: 0,
|
||||
Total: 1,
|
||||
Status: "Ошибка обработки",
|
||||
Error: err,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Успешное завершение
|
||||
p.tuiManager.SendStatusUpdate(entities.ProcessingStatus{
|
||||
Current: 1,
|
||||
Total: 1,
|
||||
Status: "Обработка завершена успешно!",
|
||||
Error: nil,
|
||||
})
|
||||
|
||||
if p.logger != nil {
|
||||
p.logger.Success("Обработка аудиокниг завершена успешно")
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ type ProcessAudioBooksUseCase struct {
|
||||
logger repositories.Logger
|
||||
audioBookSvc *services.AudioBookService
|
||||
metadataSvc *services.MetadataService
|
||||
// Репортер прогресса
|
||||
progress func(entities.ProcessingStatus)
|
||||
}
|
||||
|
||||
// NewProcessAudioBooksUseCase создает новый use case
|
||||
@@ -33,6 +35,11 @@ func NewProcessAudioBooksUseCase(
|
||||
}
|
||||
}
|
||||
|
||||
// SetProgressReporter задает функцию репортинга прогресса
|
||||
func (uc *ProcessAudioBooksUseCase) SetProgressReporter(f func(entities.ProcessingStatus)) {
|
||||
uc.progress = f
|
||||
}
|
||||
|
||||
// Execute выполняет обработку аудиокниг
|
||||
func (uc *ProcessAudioBooksUseCase) Execute(config *entities.Config) error {
|
||||
uc.logger.Info("🚀 Начало процесса обработки аудиокниг")
|
||||
@@ -42,35 +49,58 @@ func (uc *ProcessAudioBooksUseCase) Execute(config *entities.Config) error {
|
||||
audioBooks, err := uc.audioBookRepo.ScanDirectory(config.Scanner.SourceDirectory)
|
||||
if err != nil {
|
||||
uc.logger.Error("❌ Критическая ошибка сканирования: %v", err)
|
||||
if uc.progress != nil {
|
||||
uc.progress(entities.ProcessingStatus{Current: 0, Total: 0, Status: "Ошибка сканирования", Error: err})
|
||||
}
|
||||
return fmt.Errorf("ошибка сканирования: %w", err)
|
||||
}
|
||||
|
||||
uc.logger.Info("📊 Найдено %d аудиокниг", len(audioBooks))
|
||||
total := len(audioBooks)
|
||||
uc.logger.Info("📊 Найдено %d аудиокниг", total)
|
||||
|
||||
if len(audioBooks) == 0 {
|
||||
if total == 0 {
|
||||
uc.logger.Warning("⚠️ Аудиокниги не найдены")
|
||||
if uc.progress != nil {
|
||||
uc.progress(entities.ProcessingStatus{Current: 0, Total: 0, Status: "Книги не найдены"})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Сообщаем общее количество до начала обработки
|
||||
if uc.progress != nil {
|
||||
uc.progress(entities.ProcessingStatus{Current: 0, Total: total, Status: "Начинаем обработку"})
|
||||
}
|
||||
|
||||
// Авторизация в RuTracker
|
||||
if err := uc.rutrackerRepo.Login(); err != nil {
|
||||
uc.logger.Error("❌ Ошибка авторизации в RuTracker: %v", err)
|
||||
if uc.progress != nil {
|
||||
uc.progress(entities.ProcessingStatus{Current: 0, Total: total, Status: "Ошибка авторизации", Error: err})
|
||||
}
|
||||
return fmt.Errorf("ошибка авторизации: %w", err)
|
||||
}
|
||||
uc.logger.Success("✅ Авторизация в RuTracker успешна")
|
||||
|
||||
// Обработка каждой аудиокниги
|
||||
for i, book := range audioBooks {
|
||||
uc.logger.Info("📋 Обработка книги %d/%d: \"%s\"", i+1, len(audioBooks), book.Title)
|
||||
uc.logger.Info("📋 Обработка книги %d/%d: \"%s\"", i+1, total, book.Title)
|
||||
|
||||
if err := uc.processAudioBook(book); err != nil {
|
||||
uc.logger.Error("❌ Ошибка обработки книги \"%s\": %v", book.Title, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Увеличиваем прогресс после завершения обработки книги (успешно или нет)
|
||||
if uc.progress != nil {
|
||||
uc.progress(entities.ProcessingStatus{Current: i + 1, Total: total, Status: fmt.Sprintf("Готово %d/%d", i+1, total)})
|
||||
}
|
||||
|
||||
uc.logger.Success("✅ Книга \"%s\" обработана", book.Title)
|
||||
}
|
||||
|
||||
if uc.progress != nil {
|
||||
uc.progress(entities.ProcessingStatus{Current: total, Total: total, Status: "Обработка завершена"})
|
||||
}
|
||||
|
||||
uc.logger.Success("🎉 Обработка завершена успешно!")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -9,8 +13,36 @@ import (
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ConfigData структура для хранения данных конфигурации
|
||||
type ConfigData struct {
|
||||
Scanner struct {
|
||||
SourceDirectory string `yaml:"source_directory"`
|
||||
TargetDirectory string `yaml:"target_directory"`
|
||||
} `yaml:"scanner"`
|
||||
RuTracker struct {
|
||||
BaseURL string `yaml:"base_url"`
|
||||
UserAgent string `yaml:"user_agent"`
|
||||
RequestDelay int `yaml:"request_delay"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
} `yaml:"rutracker"`
|
||||
Processing struct {
|
||||
ParallelWorkers int `yaml:"parallel_workers"`
|
||||
TimeoutSeconds int `yaml:"timeout_seconds"`
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
} `yaml:"processing"`
|
||||
Output struct {
|
||||
LogLevel string `yaml:"log_level"`
|
||||
ProgressBar bool `yaml:"progress_bar"`
|
||||
LogToFile bool `yaml:"log_to_file"`
|
||||
LogFileName string `yaml:"log_file_name"`
|
||||
LogMaxSizeMB int `yaml:"log_max_size_mb"`
|
||||
} `yaml:"output"`
|
||||
}
|
||||
|
||||
// Manager управляет TUI интерфейсом
|
||||
type Manager struct {
|
||||
app *tview.Application
|
||||
@@ -25,19 +57,17 @@ type Manager struct {
|
||||
|
||||
// Компоненты главного меню
|
||||
menuList *tview.List
|
||||
infoPanel *tview.TextView
|
||||
|
||||
// Компоненты обработки
|
||||
logView *tview.TextView
|
||||
progressBar *tview.TextView
|
||||
progressText *tview.TextView
|
||||
|
||||
// Компоненты настроек
|
||||
settingsForm *tview.Form
|
||||
// Компоненты конфигурации
|
||||
configForm *tview.Form
|
||||
|
||||
// Компоненты результатов
|
||||
resultsTable *tview.Table
|
||||
detailView *tview.TextView
|
||||
// Счетчики прогресса
|
||||
totalFolders int
|
||||
processedFolders int
|
||||
|
||||
// Состояние
|
||||
logBuffer []string
|
||||
@@ -73,6 +103,7 @@ func (m *Manager) Initialize() {
|
||||
m.initializeComponents()
|
||||
m.setupMainMenu()
|
||||
m.setupProcessingScreen()
|
||||
m.setupConfigScreen()
|
||||
m.setupSettingsScreen()
|
||||
m.setupResultsScreen()
|
||||
m.setupPages()
|
||||
@@ -126,56 +157,38 @@ func (m *Manager) initializeComponents() {
|
||||
// Создаем систему страниц заранее
|
||||
m.pages = tview.NewPages()
|
||||
|
||||
// Создаем главный контейнер: Header | Pages | Status | Footer
|
||||
// Создаем главный контейнер: Header | Pages | Footer
|
||||
m.mainFlex = tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(m.headerBar, 3, 0, false).
|
||||
AddItem(m.pages, 0, 1, true).
|
||||
AddItem(m.statusBar, 1, 0, false).
|
||||
AddItem(m.footerBar, 1, 0, false)
|
||||
}
|
||||
|
||||
// setupMainMenu настраивает главное меню
|
||||
func (m *Manager) setupMainMenu() {
|
||||
m.menuList = tview.NewList().
|
||||
AddItem("🚀 Начать обработку аудиокниг", "Начать сканирование и обработку аудиокниг", '1', func() {
|
||||
AddItem("🚀 Обработка аудиокниг", "Начать обработку аудиокниг в указанных папках", '1', func() {
|
||||
m.switchToProcessing()
|
||||
if m.onStartProcessing != nil {
|
||||
go m.onStartProcessing()
|
||||
}
|
||||
}).
|
||||
AddItem("⚙️ Настройки", "Настройка параметров приложения", '2', func() {
|
||||
m.switchToSettings()
|
||||
}).
|
||||
AddItem("📊 Результаты", "Просмотр результатов обработки", '3', func() {
|
||||
m.switchToResults()
|
||||
AddItem("⚙️ Конфигурация", "Настройка параметров приложения", '2', func() {
|
||||
m.switchToConfig()
|
||||
}).
|
||||
AddItem("❌ Выход", "Завершить работу приложения", 'q', func() {
|
||||
m.app.Stop()
|
||||
})
|
||||
|
||||
m.menuList.SetBorder(true).SetTitle(" Главное меню ")
|
||||
m.menuList.SetBorder(true).SetTitle(" Выберите задачу ")
|
||||
|
||||
m.infoPanel = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetWrap(true).
|
||||
SetText(`[yellow]AudioBook Catalyst[-]
|
||||
// Увеличиваем размер элементов списка
|
||||
m.menuList.SetSelectedBackgroundColor(tcell.ColorBlue)
|
||||
m.menuList.SetSelectedTextColor(tcell.ColorWhite)
|
||||
|
||||
Приложение для автоматической обработки аудиокниг:
|
||||
• Сканирование папок с MP3 файлами
|
||||
• Поиск метаданных на RuTracker
|
||||
• Создание файлов metadata.json
|
||||
• Загрузка обложек
|
||||
|
||||
[grey]Используйте клавиши 1-4 или мышь для навигации[-]`)
|
||||
|
||||
m.infoPanel.SetBorder(true).SetTitle(" Информация ")
|
||||
|
||||
mainMenuFlex := tview.NewFlex().
|
||||
AddItem(m.menuList, 0, 1, true).
|
||||
AddItem(m.infoPanel, 0, 1, false)
|
||||
|
||||
m.pages.AddPage("main", mainMenuFlex, true, true)
|
||||
// Создаем страницу с меню на весь экран
|
||||
m.pages.AddPage("main", m.menuList, true, true)
|
||||
}
|
||||
|
||||
// setupProcessingScreen настраивает экран обработки
|
||||
@@ -191,59 +204,59 @@ func (m *Manager) setupProcessingScreen() {
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
m.progressBar.SetBorder(true).SetTitle(" Прогресс ")
|
||||
|
||||
m.progressText = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetText("[white]Ожидание начала обработки...[-]")
|
||||
|
||||
// Горизонтальный прогресс ниже логов
|
||||
progressFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(m.progressBar, 3, 0, false).
|
||||
AddItem(m.progressText, 1, 0, false)
|
||||
AddItem(m.progressBar, 3, 0, false)
|
||||
|
||||
processingFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(progressFlex, 4, 0, false).
|
||||
AddItem(m.logView, 0, 1, true)
|
||||
AddItem(m.logView, 0, 1, true).
|
||||
AddItem(progressFlex, 4, 0, false)
|
||||
|
||||
// Инициализируем прогресс-бар текстом по умолчанию
|
||||
m.updateProgressDisplay()
|
||||
|
||||
m.pages.AddPage("processing", processingFlex, true, false)
|
||||
}
|
||||
|
||||
// setupSettingsScreen настраивает экран настроек
|
||||
func (m *Manager) setupSettingsScreen() {
|
||||
m.settingsForm = tview.NewForm().
|
||||
AddInputField("Исходная директория:", "", 50, nil, nil).
|
||||
AddInputField("Целевая директория:", "", 50, nil, nil).
|
||||
AddInputField("Имя пользователя RuTracker:", "", 30, nil, nil).
|
||||
AddPasswordField("Пароль RuTracker:", "", 30, '*', nil).
|
||||
AddDropDown("Уровень логирования:", []string{"info", "debug"}, 0, nil).
|
||||
// setupConfigScreen настраивает экран конфигурации
|
||||
func (m *Manager) setupConfigScreen() {
|
||||
m.configForm = tview.NewForm().
|
||||
AddInputField("Исходная папка:", "", 50, nil, nil).
|
||||
AddInputField("Целевая папка:", "", 50, nil, nil).
|
||||
AddInputField("URL RuTracker:", "", 50, nil, nil).
|
||||
AddInputField("User Agent:", "", 50, nil, nil).
|
||||
AddInputField("Задержка запросов (мс):", "", 20, nil, nil).
|
||||
AddInputField("Имя пользователя:", "", 30, nil, nil).
|
||||
AddPasswordField("Пароль:", "", 30, '*', nil).
|
||||
AddInputField("Параллельные воркеры:", "", 10, nil, nil).
|
||||
AddInputField("Таймаут (сек):", "", 10, nil, nil).
|
||||
AddInputField("Попытки повтора:", "", 10, nil, nil).
|
||||
AddDropDown("Уровень логирования:", []string{"debug", "info", "warn", "error"}, 0, nil).
|
||||
AddCheckbox("Индикатор прогресса:", true, nil).
|
||||
AddCheckbox("Логирование в файл:", true, nil).
|
||||
AddInputField("Имя лог файла:", "", 30, nil, nil).
|
||||
AddInputField("Макс размер лога (МБ):", "", 10, nil, nil).
|
||||
AddButton("Сохранить", func() {
|
||||
// TODO: Сохранить настройки
|
||||
m.saveConfig()
|
||||
}).
|
||||
AddButton("Отмена", func() {
|
||||
m.switchToMain()
|
||||
})
|
||||
|
||||
m.settingsForm.SetBorder(true).SetTitle(" Настройки ")
|
||||
m.pages.AddPage("settings", m.settingsForm, true, false)
|
||||
m.configForm.SetBorder(true).SetTitle(" Конфигурация ")
|
||||
m.pages.AddPage("config", m.configForm, true, false)
|
||||
}
|
||||
|
||||
// setupSettingsScreen настраивает экран настроек
|
||||
func (m *Manager) setupSettingsScreen() {
|
||||
// Убираем экран настроек, так как он не нужен в упрощенном интерфейсе
|
||||
}
|
||||
|
||||
// setupResultsScreen настраивает экран результатов
|
||||
func (m *Manager) setupResultsScreen() {
|
||||
m.resultsTable = tview.NewTable().
|
||||
SetSelectable(true, false)
|
||||
m.resultsTable.SetBorder(true).SetTitle(" Обработанные аудиокниги ")
|
||||
|
||||
m.detailView = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetWrap(true)
|
||||
m.detailView.SetBorder(true).SetTitle(" Детали ")
|
||||
|
||||
resultsFlex := tview.NewFlex().
|
||||
AddItem(m.resultsTable, 0, 2, true).
|
||||
AddItem(m.detailView, 0, 1, false)
|
||||
|
||||
m.pages.AddPage("results", resultsFlex, true, false)
|
||||
// Убираем экран результатов, так как он не нужен в упрощенном интерфейсе
|
||||
}
|
||||
|
||||
// setupPages настраивает систему страниц
|
||||
@@ -262,12 +275,7 @@ func (m *Manager) setupPages() {
|
||||
case tcell.KeyF2:
|
||||
m.switchToProcessing()
|
||||
return nil
|
||||
case tcell.KeyF3:
|
||||
m.switchToSettings()
|
||||
return nil
|
||||
case tcell.KeyF4:
|
||||
m.switchToResults()
|
||||
return nil
|
||||
|
||||
}
|
||||
return event
|
||||
})
|
||||
@@ -289,18 +297,68 @@ func (m *Manager) handleUpdates() {
|
||||
}
|
||||
}
|
||||
|
||||
// updateProgress обновляет прогресс
|
||||
func (m *Manager) updateProgress(status entities.ProcessingStatus) {
|
||||
if status.Total > 0 {
|
||||
progress := float64(status.Current) / float64(status.Total) * 100
|
||||
m.progressBar.SetText(fmt.Sprintf("[green]%.1f%% (%d/%d)[-]", progress, status.Current, status.Total))
|
||||
// SetTotalFolders устанавливает общее количество папок для обработки
|
||||
func (m *Manager) SetTotalFolders(total int) {
|
||||
m.totalFolders = total
|
||||
m.processedFolders = 0
|
||||
m.updateProgressDisplay()
|
||||
}
|
||||
|
||||
if status.Error != nil {
|
||||
m.progressText.SetText(fmt.Sprintf("[red]Ошибка: %s[-]", status.Error.Error()))
|
||||
} else {
|
||||
m.progressText.SetText(fmt.Sprintf("[white]%s[-]", status.Status))
|
||||
// IncrementProgress увеличивает счетчик обработанных папок
|
||||
func (m *Manager) IncrementProgress() {
|
||||
m.processedFolders++
|
||||
m.updateProgressDisplay()
|
||||
}
|
||||
|
||||
// updateProgressDisplay обновляет отображение прогресса
|
||||
func (m *Manager) updateProgressDisplay() {
|
||||
if m.totalFolders > 0 {
|
||||
progress := int(math.Round(float64(m.processedFolders) * 100.0 / float64(m.totalFolders)))
|
||||
if progress < 0 {
|
||||
progress = 0
|
||||
}
|
||||
if progress > 100 {
|
||||
progress = 100
|
||||
}
|
||||
bar := m.renderProgressBar()
|
||||
m.progressBar.SetText(fmt.Sprintf("[cyan::b]Прогресс:[-] %s [green]%d%%[-] [gray](%d/%d книг)[-]", bar, progress, m.processedFolders, m.totalFolders))
|
||||
} else {
|
||||
m.progressBar.SetText("[yellow]Подсчет книг...[-]")
|
||||
}
|
||||
}
|
||||
|
||||
// renderProgressBar рисует горизонтальный прогресс-бар фиксированной ширины (■ — обработано, □ — ожидает)
|
||||
func (m *Manager) renderProgressBar() string {
|
||||
const width = 30
|
||||
if m.totalFolders <= 0 {
|
||||
return strings.Repeat("[white]□[-]", width)
|
||||
}
|
||||
filled := int(math.Round(float64(m.processedFolders) / float64(m.totalFolders) * float64(width)))
|
||||
if filled < 0 {
|
||||
filled = 0
|
||||
}
|
||||
if filled > width {
|
||||
filled = width
|
||||
}
|
||||
var b strings.Builder
|
||||
for i := 0; i < filled; i++ {
|
||||
b.WriteString("[green]■[-]")
|
||||
}
|
||||
for i := filled; i < width; i++ {
|
||||
b.WriteString("[white]□[-]")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// updateProgress обновляет прогресс (без вывода служебных статусов под баром)
|
||||
func (m *Manager) updateProgress(status entities.ProcessingStatus) {
|
||||
if status.Total > 0 {
|
||||
m.totalFolders = status.Total
|
||||
}
|
||||
if status.Current >= 0 {
|
||||
m.processedFolders = status.Current
|
||||
}
|
||||
m.updateProgressDisplay()
|
||||
}
|
||||
|
||||
// addLogMessage добавляет сообщение в лог
|
||||
@@ -317,6 +375,96 @@ func (m *Manager) addLogMessage(message string) {
|
||||
m.logView.ScrollToEnd()
|
||||
}
|
||||
|
||||
// loadConfig загружает конфигурацию из файла и заполняет форму
|
||||
func (m *Manager) loadConfig() {
|
||||
data, err := os.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var config ConfigData
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Заполняем поля формы данными из конфигурации
|
||||
m.configForm.GetFormItemByLabel("Исходная папка:").(*tview.InputField).SetText(config.Scanner.SourceDirectory)
|
||||
m.configForm.GetFormItemByLabel("Целевая папка:").(*tview.InputField).SetText(config.Scanner.TargetDirectory)
|
||||
m.configForm.GetFormItemByLabel("URL RuTracker:").(*tview.InputField).SetText(config.RuTracker.BaseURL)
|
||||
m.configForm.GetFormItemByLabel("User Agent:").(*tview.InputField).SetText(config.RuTracker.UserAgent)
|
||||
m.configForm.GetFormItemByLabel("Задержка запросов (мс):").(*tview.InputField).SetText(strconv.Itoa(config.RuTracker.RequestDelay))
|
||||
m.configForm.GetFormItemByLabel("Имя пользователя:").(*tview.InputField).SetText(config.RuTracker.Username)
|
||||
m.configForm.GetFormItemByLabel("Пароль:").(*tview.InputField).SetText(config.RuTracker.Password)
|
||||
m.configForm.GetFormItemByLabel("Параллельные воркеры:").(*tview.InputField).SetText(strconv.Itoa(config.Processing.ParallelWorkers))
|
||||
m.configForm.GetFormItemByLabel("Таймаут (сек):").(*tview.InputField).SetText(strconv.Itoa(config.Processing.TimeoutSeconds))
|
||||
m.configForm.GetFormItemByLabel("Попытки повтора:").(*tview.InputField).SetText(strconv.Itoa(config.Processing.RetryAttempts))
|
||||
|
||||
// Устанавливаем уровень логирования
|
||||
logLevels := []string{"debug", "info", "warn", "error"}
|
||||
for i, level := range logLevels {
|
||||
if level == config.Output.LogLevel {
|
||||
m.configForm.GetFormItemByLabel("Уровень логирования:").(*tview.DropDown).SetCurrentOption(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
m.configForm.GetFormItemByLabel("Индикатор прогресса:").(*tview.Checkbox).SetChecked(config.Output.ProgressBar)
|
||||
m.configForm.GetFormItemByLabel("Логирование в файл:").(*tview.Checkbox).SetChecked(config.Output.LogToFile)
|
||||
m.configForm.GetFormItemByLabel("Имя лог файла:").(*tview.InputField).SetText(config.Output.LogFileName)
|
||||
m.configForm.GetFormItemByLabel("Макс размер лога (МБ):").(*tview.InputField).SetText(strconv.Itoa(config.Output.LogMaxSizeMB))
|
||||
}
|
||||
|
||||
// saveConfig сохраняет конфигурацию из формы в файл
|
||||
func (m *Manager) saveConfig() {
|
||||
var config ConfigData
|
||||
|
||||
// Получаем данные из формы
|
||||
config.Scanner.SourceDirectory = m.configForm.GetFormItemByLabel("Исходная папка:").(*tview.InputField).GetText()
|
||||
config.Scanner.TargetDirectory = m.configForm.GetFormItemByLabel("Целевая папка:").(*tview.InputField).GetText()
|
||||
config.RuTracker.BaseURL = m.configForm.GetFormItemByLabel("URL RuTracker:").(*tview.InputField).GetText()
|
||||
config.RuTracker.UserAgent = m.configForm.GetFormItemByLabel("User Agent:").(*tview.InputField).GetText()
|
||||
|
||||
if delay, err := strconv.Atoi(m.configForm.GetFormItemByLabel("Задержка запросов (мс):").(*tview.InputField).GetText()); err == nil {
|
||||
config.RuTracker.RequestDelay = delay
|
||||
}
|
||||
|
||||
config.RuTracker.Username = m.configForm.GetFormItemByLabel("Имя пользователя:").(*tview.InputField).GetText()
|
||||
config.RuTracker.Password = m.configForm.GetFormItemByLabel("Пароль:").(*tview.InputField).GetText()
|
||||
|
||||
if workers, err := strconv.Atoi(m.configForm.GetFormItemByLabel("Параллельные воркеры:").(*tview.InputField).GetText()); err == nil {
|
||||
config.Processing.ParallelWorkers = workers
|
||||
}
|
||||
|
||||
if timeout, err := strconv.Atoi(m.configForm.GetFormItemByLabel("Таймаут (сек):").(*tview.InputField).GetText()); err == nil {
|
||||
config.Processing.TimeoutSeconds = timeout
|
||||
}
|
||||
|
||||
if retries, err := strconv.Atoi(m.configForm.GetFormItemByLabel("Попытки повтора:").(*tview.InputField).GetText()); err == nil {
|
||||
config.Processing.RetryAttempts = retries
|
||||
}
|
||||
|
||||
// Получаем уровень логирования
|
||||
_, logLevel := m.configForm.GetFormItemByLabel("Уровень логирования:").(*tview.DropDown).GetCurrentOption()
|
||||
config.Output.LogLevel = logLevel
|
||||
|
||||
config.Output.ProgressBar = m.configForm.GetFormItemByLabel("Индикатор прогресса:").(*tview.Checkbox).IsChecked()
|
||||
config.Output.LogToFile = m.configForm.GetFormItemByLabel("Логирование в файл:").(*tview.Checkbox).IsChecked()
|
||||
config.Output.LogFileName = m.configForm.GetFormItemByLabel("Имя лог файла:").(*tview.InputField).GetText()
|
||||
|
||||
if maxSize, err := strconv.Atoi(m.configForm.GetFormItemByLabel("Макс размер лога (МБ):").(*tview.InputField).GetText()); err == nil {
|
||||
config.Output.LogMaxSizeMB = maxSize
|
||||
}
|
||||
|
||||
// Сохраняем в файл
|
||||
data, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
os.WriteFile("config.yaml", data, 0644)
|
||||
m.switchToMain()
|
||||
}
|
||||
|
||||
// Методы переключения страниц
|
||||
func (m *Manager) switchToMain() {
|
||||
m.currentScreen = entities.ScreenMainMenu
|
||||
@@ -330,14 +478,51 @@ func (m *Manager) switchToProcessing() {
|
||||
m.app.SetFocus(m.logView)
|
||||
}
|
||||
|
||||
func (m *Manager) switchToSettings() {
|
||||
func (m *Manager) switchToConfig() {
|
||||
m.loadConfig()
|
||||
m.currentScreen = entities.ScreenSettings
|
||||
m.pages.SwitchToPage("settings")
|
||||
m.app.SetFocus(m.settingsForm)
|
||||
m.pages.SwitchToPage("config")
|
||||
m.app.SetFocus(m.configForm)
|
||||
}
|
||||
|
||||
func (m *Manager) switchToResults() {
|
||||
m.currentScreen = entities.ScreenResults
|
||||
m.pages.SwitchToPage("results")
|
||||
m.app.SetFocus(m.resultsTable)
|
||||
// CountAudiobookFolders подсчитывает количество книг (папок верхнего уровня с аудиофайлами)
|
||||
func (m *Manager) CountAudiobookFolders(sourceDir string) int {
|
||||
count := 0
|
||||
|
||||
entries, err := os.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
bookDir := filepath.Join(sourceDir, entry.Name())
|
||||
// Считаем книгой папку, в которой есть хотя бы один mp3 во вложенности или metadata.json
|
||||
if folderHasBookContent(bookDir) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// folderHasBookContent проверяет, есть ли в папке аудиокнига (mp3 или metadata.json) на любом уровне вложенности
|
||||
func folderHasBookContent(dir string) bool {
|
||||
found := false
|
||||
_ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil || found {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
name := strings.ToLower(d.Name())
|
||||
if strings.HasSuffix(name, ".mp3") || name == "metadata.json" {
|
||||
found = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user