1
1
Files
Dmitriy Fofanov 72a66f1664 Добавить тесты репозитория файловой системы и реализовать функциональность журналирования файлов.
- Реализовать тесты для поиска MP3-файлов и переименования/организации папок книг в репозитории файловой системы.
- Создать FileLogger для записи сообщений в файл с поддержкой различных уровней журналирования и управления размером файлов.
- Разработать репозиторий RuTracker для обработки поиска торрентов, получения метаданных и загрузки торрент-файлов.
- Добавить тесты для нормализации URL в репозиторий RuTracker.
- Реализовать адаптер логгера TUI для отображения логов в терминальном интерфейсе и, при необходимости, для записи логов в базовый логгер.
- Создать менеджер TUI для управления пользовательским интерфейсом приложения, включая главное меню, экран обработки, настройки и отображение результатов.
2025-09-29 20:40:05 +03:00

287 lines
8.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package filesystem
import (
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"audio-catalyst/internal/domain/entities"
)
// Repository реализация AudioBookRepository
type Repository struct{}
// NewRepository создает новый репозиторий файловой системы
func NewRepository() *Repository {
return &Repository{}
}
// ScanDirectory сканирует директорию на наличие аудиокниг
func (r *Repository) ScanDirectory(rootDir string) ([]entities.AudioBook, error) {
var audioBooks []entities.AudioBook
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
mp3Files, err := r.findMP3Files(path)
if err != nil {
return err
}
if len(mp3Files) > 0 {
book := entities.AudioBook{
Path: path,
Title: filepath.Base(path),
MP3Files: mp3Files,
CoverFile: r.findCoverFile(path),
Description: r.findDescriptionFile(path),
}
audioBooks = append(audioBooks, book)
}
return nil
})
return audioBooks, err
}
// SaveMetadata сохраняет метаданные аудиокниги
func (r *Repository) SaveMetadata(bookPath string, metadata *entities.AudioBookMetadata) error {
metadataFile := filepath.Join(bookPath, "metadata.json")
data, err := json.MarshalIndent(metadata, "", " ")
if err != nil {
return fmt.Errorf("ошибка сериализации метаданных: %w", err)
}
if err := os.WriteFile(metadataFile, data, 0644); err != nil {
return fmt.Errorf("ошибка записи метаданных: %w", err)
}
return nil
}
// DownloadCover загружает обложку по URL
func (r *Repository) DownloadCover(coverURL, bookPath string) error {
if coverURL == "" {
return fmt.Errorf("пустой URL обложки")
}
client := &http.Client{Timeout: 30 * time.Second}
req, err := http.NewRequest("GET", coverURL, nil)
if err != nil {
return fmt.Errorf("ошибка создания запроса: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
// Поставим корректный Referer, если URL — с rutracker.org
if u, err := url.Parse(coverURL); err == nil && strings.Contains(u.Host, "rutracker.org") {
req.Header.Set("Referer", "https://rutracker.org/forum/")
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("ошибка загрузки обложки: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP статус: %d", resp.StatusCode)
}
// Определяем расширение файла
fileExt := ".jpg"
if ct := resp.Header.Get("Content-Type"); ct != "" {
if exts, _ := mime.ExtensionsByType(ct); len(exts) > 0 {
fileExt = exts[0]
} else if strings.Contains(ct, "png") {
fileExt = ".png"
} else if strings.Contains(ct, "gif") {
fileExt = ".gif"
}
}
// Если в URL есть расширение — доверим ему
if u, err := url.Parse(coverURL); err == nil {
if ext := strings.ToLower(filepath.Ext(u.Path)); ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" {
fileExt = ext
}
}
coverFile := filepath.Join(bookPath, "cover"+fileExt)
out, err := os.Create(coverFile)
if err != nil {
return fmt.Errorf("ошибка создания файла: %w", err)
}
defer out.Close()
const maxSize = 10 * 1024 * 1024
limitedReader := io.LimitReader(resp.Body, maxSize)
if _, err = io.Copy(out, limitedReader); err != nil {
_ = os.Remove(coverFile)
return fmt.Errorf("ошибка записи файла: %w", err)
}
return nil
}
// findMP3Files находит MP3 файлы в директории
func (r *Repository) findMP3Files(dir string) ([]string, error) {
var mp3Files []string
files, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(strings.ToLower(file.Name()), ".mp3") {
mp3Files = append(mp3Files, filepath.Join(dir, file.Name()))
}
}
return mp3Files, nil
}
// findCoverFile находит файл обложки
func (r *Repository) findCoverFile(dir string) string {
coverNames := []string{"cover.jpg", "cover.png", "cover.jpeg", "folder.jpg", "folder.png"}
files, err := os.ReadDir(dir)
if err != nil {
return ""
}
for _, file := range files {
if !file.IsDir() {
fileName := strings.ToLower(file.Name())
for _, coverName := range coverNames {
if fileName == coverName {
return filepath.Join(dir, file.Name())
}
}
}
}
return ""
}
// findDescriptionFile находит файл описания
func (r *Repository) findDescriptionFile(dir string) string {
descNames := []string{"description.txt", "readme.txt", "info.txt"}
files, err := os.ReadDir(dir)
if err != nil {
return ""
}
for _, file := range files {
if !file.IsDir() {
fileName := strings.ToLower(file.Name())
for _, descName := range descNames {
if fileName == descName {
content, err := os.ReadFile(filepath.Join(dir, file.Name()))
if err == nil {
return string(content)
}
}
}
}
}
return ""
}
// RenameBookFolder переименовывает папку аудиокниги и возвращает новый путь
func (r *Repository) RenameBookFolder(oldPath, newBaseName string) (string, error) {
if oldPath == "" || newBaseName == "" {
return oldPath, fmt.Errorf("некорректные параметры переименования")
}
parent := filepath.Dir(oldPath)
// Заменим недопустимые для Windows символы
safeName := strings.NewReplacer(
"<", "(",
">", ")",
"|", "-",
"?", "",
"*", "",
"\"", "'",
":", " -",
"/", "-",
"\\", "-",
).Replace(newBaseName)
// Убираем хвосты RuTracker.org и повторяющиеся разделители в конце
reSuffix := regexp.MustCompile(`(?i)\s*(?:\s*(?:\||::|:|—|-)\s*)+RuTracker\.?org.*$`)
safeName = reSuffix.ReplaceAllString(safeName, "")
safeName = strings.TrimSpace(safeName)
safeName = regexp.MustCompile(`\s*(?:-\s*){2,}$`).ReplaceAllString(safeName, "") // " - -" в конце
if safeName == "" {
return oldPath, fmt.Errorf("пустое имя папки")
}
newPath := filepath.Join(parent, safeName)
if strings.EqualFold(oldPath, newPath) {
return oldPath, nil
}
if _, err := os.Stat(newPath); err == nil {
return oldPath, fmt.Errorf("папка уже существует: %s", newPath)
}
if err := os.Rename(oldPath, newPath); err != nil {
return oldPath, fmt.Errorf("ошибка переименования: %w", err)
}
return newPath, nil
}
// OrganizeBookFolder перемещает папку книги в organized/<Letter>/<Author>
func (r *Repository) OrganizeBookFolder(bookPath, authorFullName, targetRoot string) (string, error) {
if bookPath == "" || targetRoot == "" {
return bookPath, fmt.Errorf("некорректные параметры перемещения")
}
// Определяем букву (первая буква фамилии/имени)
author := strings.TrimSpace(authorFullName)
if author == "" {
author = "Unknown"
}
// Приводим к формату "Фамилия Имя" (если уже так — оставляем)
author = regexp.MustCompile(`\s+`).ReplaceAllString(author, " ")
letter := strings.ToUpper(string([]rune(author)[0]))
// Безопасные имена
safe := func(s string) string {
return strings.NewReplacer(
"<", "(", ">", ")", "|", "-", "?", "", "*", "", "\"", "'", ":", " -", "/", "-", "\\", "-",
).Replace(strings.TrimSpace(s))
}
letterDir := filepath.Join(targetRoot, safe(letter))
authorDir := filepath.Join(letterDir, safe(author))
if err := os.MkdirAll(authorDir, 0755); err != nil {
return bookPath, fmt.Errorf("ошибка создания каталогов: %w", err)
}
newBookPath := filepath.Join(authorDir, filepath.Base(bookPath))
if strings.EqualFold(bookPath, newBookPath) {
return bookPath, nil
}
if _, err := os.Stat(newBookPath); err == nil {
return bookPath, fmt.Errorf("папка уже существует: %s", newBookPath)
}
if err := os.Rename(bookPath, newBookPath); err != nil {
return bookPath, fmt.Errorf("ошибка перемещения: %w", err)
}
return newBookPath, nil
}