1
1

Добавить функциональность репортинга прогресса обработки аудиокниг и экран конфигурации

This commit is contained in:
Dmitriy Fofanov
2025-09-30 14:04:38 +03:00
parent 72a66f1664
commit e4ba057abb
3 changed files with 317 additions and 120 deletions

View File

@@ -60,6 +60,10 @@ func main() {
rutrackerRepo, rutrackerRepo,
logger, logger,
) )
// Подключаем репортер прогресса к TUI
processUseCase.SetProgressReporter(func(s entities.ProcessingStatus) {
tuiManager.SendStatusUpdate(s)
})
// Создание процессора для обработки команд // Создание процессора для обработки команд
processor := &ApplicationProcessor{ processor := &ApplicationProcessor{
@@ -102,36 +106,14 @@ func (p *ApplicationProcessor) StartProcessing() {
p.logger.Info("Запуск обработки аудиокниг...") p.logger.Info("Запуск обработки аудиокниг...")
} }
// Обновим UI статус // Запускаем обработку (репортинг прогресса выполняет use case)
p.tuiManager.SendStatusUpdate(entities.ProcessingStatus{
Current: 0,
Total: 1,
Status: "Начинаем обработку...",
Error: nil,
})
// Запускаем обработку
if err := p.processUseCase.Execute(p.config); err != nil { if err := p.processUseCase.Execute(p.config); err != nil {
if p.logger != nil { if p.logger != nil {
p.logger.Error("Ошибка обработки: %v", err) p.logger.Error("Ошибка обработки: %v", err)
} }
p.tuiManager.SendStatusUpdate(entities.ProcessingStatus{
Current: 0,
Total: 1,
Status: "Ошибка обработки",
Error: err,
})
return return
} }
// Успешное завершение
p.tuiManager.SendStatusUpdate(entities.ProcessingStatus{
Current: 1,
Total: 1,
Status: "Обработка завершена успешно!",
Error: nil,
})
if p.logger != nil { if p.logger != nil {
p.logger.Success("Обработка аудиокниг завершена успешно") p.logger.Success("Обработка аудиокниг завершена успешно")
} }

View File

@@ -16,6 +16,8 @@ type ProcessAudioBooksUseCase struct {
logger repositories.Logger logger repositories.Logger
audioBookSvc *services.AudioBookService audioBookSvc *services.AudioBookService
metadataSvc *services.MetadataService metadataSvc *services.MetadataService
// Репортер прогресса
progress func(entities.ProcessingStatus)
} }
// NewProcessAudioBooksUseCase создает новый use case // NewProcessAudioBooksUseCase создает новый use case
@@ -33,6 +35,11 @@ func NewProcessAudioBooksUseCase(
} }
} }
// SetProgressReporter задает функцию репортинга прогресса
func (uc *ProcessAudioBooksUseCase) SetProgressReporter(f func(entities.ProcessingStatus)) {
uc.progress = f
}
// Execute выполняет обработку аудиокниг // Execute выполняет обработку аудиокниг
func (uc *ProcessAudioBooksUseCase) Execute(config *entities.Config) error { func (uc *ProcessAudioBooksUseCase) Execute(config *entities.Config) error {
uc.logger.Info("🚀 Начало процесса обработки аудиокниг") uc.logger.Info("🚀 Начало процесса обработки аудиокниг")
@@ -42,35 +49,58 @@ func (uc *ProcessAudioBooksUseCase) Execute(config *entities.Config) error {
audioBooks, err := uc.audioBookRepo.ScanDirectory(config.Scanner.SourceDirectory) audioBooks, err := uc.audioBookRepo.ScanDirectory(config.Scanner.SourceDirectory)
if err != nil { if err != nil {
uc.logger.Error("❌ Критическая ошибка сканирования: %v", err) 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) 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("⚠️ Аудиокниги не найдены") uc.logger.Warning("⚠️ Аудиокниги не найдены")
if uc.progress != nil {
uc.progress(entities.ProcessingStatus{Current: 0, Total: 0, Status: "Книги не найдены"})
}
return nil return nil
} }
// Сообщаем общее количество до начала обработки
if uc.progress != nil {
uc.progress(entities.ProcessingStatus{Current: 0, Total: total, Status: "Начинаем обработку"})
}
// Авторизация в RuTracker // Авторизация в RuTracker
if err := uc.rutrackerRepo.Login(); err != nil { if err := uc.rutrackerRepo.Login(); err != nil {
uc.logger.Error("❌ Ошибка авторизации в RuTracker: %v", err) 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) return fmt.Errorf("ошибка авторизации: %w", err)
} }
uc.logger.Success("✅ Авторизация в RuTracker успешна") uc.logger.Success("✅ Авторизация в RuTracker успешна")
// Обработка каждой аудиокниги // Обработка каждой аудиокниги
for i, book := range audioBooks { 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 { if err := uc.processAudioBook(book); err != nil {
uc.logger.Error("❌ Ошибка обработки книги \"%s\": %v", book.Title, err) 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) uc.logger.Success("✅ Книга \"%s\" обработана", book.Title)
} }
if uc.progress != nil {
uc.progress(entities.ProcessingStatus{Current: total, Total: total, Status: "Обработка завершена"})
}
uc.logger.Success("🎉 Обработка завершена успешно!") uc.logger.Success("🎉 Обработка завершена успешно!")
return nil return nil
} }

View File

@@ -2,6 +2,10 @@ package tui
import ( import (
"fmt" "fmt"
"math"
"os"
"path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
@@ -9,8 +13,36 @@ import (
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "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 интерфейсом // Manager управляет TUI интерфейсом
type Manager struct { type Manager struct {
app *tview.Application app *tview.Application
@@ -25,19 +57,17 @@ type Manager struct {
// Компоненты главного меню // Компоненты главного меню
menuList *tview.List menuList *tview.List
infoPanel *tview.TextView
// Компоненты обработки // Компоненты обработки
logView *tview.TextView logView *tview.TextView
progressBar *tview.TextView progressBar *tview.TextView
progressText *tview.TextView
// Компоненты настроек // Компоненты конфигурации
settingsForm *tview.Form configForm *tview.Form
// Компоненты результатов // Счетчики прогресса
resultsTable *tview.Table totalFolders int
detailView *tview.TextView processedFolders int
// Состояние // Состояние
logBuffer []string logBuffer []string
@@ -73,6 +103,7 @@ func (m *Manager) Initialize() {
m.initializeComponents() m.initializeComponents()
m.setupMainMenu() m.setupMainMenu()
m.setupProcessingScreen() m.setupProcessingScreen()
m.setupConfigScreen()
m.setupSettingsScreen() m.setupSettingsScreen()
m.setupResultsScreen() m.setupResultsScreen()
m.setupPages() m.setupPages()
@@ -126,56 +157,38 @@ func (m *Manager) initializeComponents() {
// Создаем систему страниц заранее // Создаем систему страниц заранее
m.pages = tview.NewPages() m.pages = tview.NewPages()
// Создаем главный контейнер: Header | Pages | Status | Footer // Создаем главный контейнер: Header | Pages | Footer
m.mainFlex = tview.NewFlex(). m.mainFlex = tview.NewFlex().
SetDirection(tview.FlexRow). SetDirection(tview.FlexRow).
AddItem(m.headerBar, 3, 0, false). AddItem(m.headerBar, 3, 0, false).
AddItem(m.pages, 0, 1, true). AddItem(m.pages, 0, 1, true).
AddItem(m.statusBar, 1, 0, false).
AddItem(m.footerBar, 1, 0, false) AddItem(m.footerBar, 1, 0, false)
} }
// setupMainMenu настраивает главное меню // setupMainMenu настраивает главное меню
func (m *Manager) setupMainMenu() { func (m *Manager) setupMainMenu() {
m.menuList = tview.NewList(). m.menuList = tview.NewList().
AddItem("🚀 Начать обработку аудиокниг", "Начать сканирование и обработку аудиокниг", '1', func() { AddItem("🚀 Обработка аудиокниг", "Начать обработку аудиокниг в указанных папках", '1', func() {
m.switchToProcessing() m.switchToProcessing()
if m.onStartProcessing != nil { if m.onStartProcessing != nil {
go m.onStartProcessing() go m.onStartProcessing()
} }
}). }).
AddItem("⚙️ Настройки", "Настройка параметров приложения", '2', func() { AddItem("⚙️ Конфигурация", "Настройка параметров приложения", '2', func() {
m.switchToSettings() m.switchToConfig()
}).
AddItem("📊 Результаты", "Просмотр результатов обработки", '3', func() {
m.switchToResults()
}). }).
AddItem("❌ Выход", "Завершить работу приложения", 'q', func() { AddItem("❌ Выход", "Завершить работу приложения", 'q', func() {
m.app.Stop() m.app.Stop()
}) })
m.menuList.SetBorder(true).SetTitle(" Главное меню ") m.menuList.SetBorder(true).SetTitle(" Выберите задачу ")
m.infoPanel = tview.NewTextView(). // Увеличиваем размер элементов списка
SetDynamicColors(true). m.menuList.SetSelectedBackgroundColor(tcell.ColorBlue)
SetWrap(true). m.menuList.SetSelectedTextColor(tcell.ColorWhite)
SetText(`[yellow]AudioBook Catalyst[-]
Приложение для автоматической обработки аудиокниг: // Создаем страницу с меню на весь экран
• Сканирование папок с MP3 файлами m.pages.AddPage("main", m.menuList, true, true)
• Поиск метаданных на 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)
} }
// setupProcessingScreen настраивает экран обработки // setupProcessingScreen настраивает экран обработки
@@ -191,59 +204,59 @@ func (m *Manager) setupProcessingScreen() {
SetTextAlign(tview.AlignCenter) SetTextAlign(tview.AlignCenter)
m.progressBar.SetBorder(true).SetTitle(" Прогресс ") m.progressBar.SetBorder(true).SetTitle(" Прогресс ")
m.progressText = tview.NewTextView(). // Горизонтальный прогресс ниже логов
SetDynamicColors(true).
SetTextAlign(tview.AlignCenter).
SetText("[white]Ожидание начала обработки...[-]")
progressFlex := tview.NewFlex(). progressFlex := tview.NewFlex().
SetDirection(tview.FlexRow). SetDirection(tview.FlexRow).
AddItem(m.progressBar, 3, 0, false). AddItem(m.progressBar, 3, 0, false)
AddItem(m.progressText, 1, 0, false)
processingFlex := tview.NewFlex(). processingFlex := tview.NewFlex().
SetDirection(tview.FlexRow). 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) m.pages.AddPage("processing", processingFlex, true, false)
} }
// setupSettingsScreen настраивает экран настроек // setupConfigScreen настраивает экран конфигурации
func (m *Manager) setupSettingsScreen() { func (m *Manager) setupConfigScreen() {
m.settingsForm = tview.NewForm(). m.configForm = tview.NewForm().
AddInputField("Исходная директория:", "", 50, nil, nil). AddInputField("Исходная папка:", "", 50, nil, nil).
AddInputField("Целевая директория:", "", 50, nil, nil). AddInputField("Целевая папка:", "", 50, nil, nil).
AddInputField("Имя пользователя RuTracker:", "", 30, nil, nil). AddInputField("URL RuTracker:", "", 50, nil, nil).
AddPasswordField("Пароль RuTracker:", "", 30, '*', nil). AddInputField("User Agent:", "", 50, nil, nil).
AddDropDown("Уровень логирования:", []string{"info", "debug"}, 0, 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() { AddButton("Сохранить", func() {
// TODO: Сохранить настройки m.saveConfig()
}). }).
AddButton("Отмена", func() { AddButton("Отмена", func() {
m.switchToMain() m.switchToMain()
}) })
m.settingsForm.SetBorder(true).SetTitle(" Настройки ") m.configForm.SetBorder(true).SetTitle(" Конфигурация ")
m.pages.AddPage("settings", m.settingsForm, true, false) m.pages.AddPage("config", m.configForm, true, false)
}
// setupSettingsScreen настраивает экран настроек
func (m *Manager) setupSettingsScreen() {
// Убираем экран настроек, так как он не нужен в упрощенном интерфейсе
} }
// setupResultsScreen настраивает экран результатов // setupResultsScreen настраивает экран результатов
func (m *Manager) 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 настраивает систему страниц // setupPages настраивает систему страниц
@@ -262,12 +275,7 @@ func (m *Manager) setupPages() {
case tcell.KeyF2: case tcell.KeyF2:
m.switchToProcessing() m.switchToProcessing()
return nil return nil
case tcell.KeyF3:
m.switchToSettings()
return nil
case tcell.KeyF4:
m.switchToResults()
return nil
} }
return event return event
}) })
@@ -289,18 +297,68 @@ func (m *Manager) handleUpdates() {
} }
} }
// updateProgress обновляет прогресс // SetTotalFolders устанавливает общее количество папок для обработки
func (m *Manager) updateProgress(status entities.ProcessingStatus) { func (m *Manager) SetTotalFolders(total int) {
if status.Total > 0 { m.totalFolders = total
progress := float64(status.Current) / float64(status.Total) * 100 m.processedFolders = 0
m.progressBar.SetText(fmt.Sprintf("[green]%.1f%% (%d/%d)[-]", progress, status.Current, status.Total)) m.updateProgressDisplay()
} }
if status.Error != nil { // IncrementProgress увеличивает счетчик обработанных папок
m.progressText.SetText(fmt.Sprintf("[red]Ошибка: %s[-]", status.Error.Error())) func (m *Manager) IncrementProgress() {
} else { m.processedFolders++
m.progressText.SetText(fmt.Sprintf("[white]%s[-]", status.Status)) 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 добавляет сообщение в лог // addLogMessage добавляет сообщение в лог
@@ -317,6 +375,96 @@ func (m *Manager) addLogMessage(message string) {
m.logView.ScrollToEnd() 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() { func (m *Manager) switchToMain() {
m.currentScreen = entities.ScreenMainMenu m.currentScreen = entities.ScreenMainMenu
@@ -330,14 +478,51 @@ func (m *Manager) switchToProcessing() {
m.app.SetFocus(m.logView) m.app.SetFocus(m.logView)
} }
func (m *Manager) switchToSettings() { func (m *Manager) switchToConfig() {
m.loadConfig()
m.currentScreen = entities.ScreenSettings m.currentScreen = entities.ScreenSettings
m.pages.SwitchToPage("settings") m.pages.SwitchToPage("config")
m.app.SetFocus(m.settingsForm) m.app.SetFocus(m.configForm)
} }
func (m *Manager) switchToResults() { // CountAudiobookFolders подсчитывает количество книг (папок верхнего уровня с аудиофайлами)
m.currentScreen = entities.ScreenResults func (m *Manager) CountAudiobookFolders(sourceDir string) int {
m.pages.SwitchToPage("results") count := 0
m.app.SetFocus(m.resultsTable)
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
} }