Раскладка файлов после распознавния
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sanitizeComponent чистит один компонент пути (имя папки/файла): убирает
|
||||
// разделители, управляющие символы и неудобные для ФС/SMB знаки, схлопывает
|
||||
// пробелы и срезает точки/пробелы по краям. Кириллица и пробелы внутри
|
||||
// сохраняются. Результат гарантированно не содержит '/', '\\', ".." целиком
|
||||
// и не пуст (иначе ошибка у вызывающего).
|
||||
func sanitizeComponent(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r < 0x20 || r == 0x7f: // управляющие
|
||||
b.WriteByte(' ')
|
||||
case strings.ContainsRune(`/\:*?"<>|`, r): // разделители и недопустимые в SMB/NTFS
|
||||
b.WriteByte(' ')
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
out := strings.Join(strings.Fields(b.String()), " ") // схлопнуть пробелы
|
||||
out = strings.Trim(out, " .") // края: ни точек, ни пробелов
|
||||
return out
|
||||
}
|
||||
|
||||
// titleYear строит базу "Название (Год)" или "Название" при year == 0.
|
||||
func titleYear(title string, year int) (string, error) {
|
||||
t := sanitizeComponent(title)
|
||||
if t == "" {
|
||||
return "", fmt.Errorf("layout: пустое название после санитизации (%q)", title)
|
||||
}
|
||||
if year > 0 {
|
||||
return fmt.Sprintf("%s (%d)", t, year), nil
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// folderName добавляет provider-тег к базе: "Название (Год) [tmdbid-123]".
|
||||
func folderName(base, providerTag string) string {
|
||||
tag := sanitizeComponent(providerTag)
|
||||
if tag == "" {
|
||||
return base
|
||||
}
|
||||
return fmt.Sprintf("%s [%s]", base, tag)
|
||||
}
|
||||
|
||||
// seasonFolder — "Season 00" (спецвыпуски) / "Season 01" / ...
|
||||
func seasonFolder(season int) string {
|
||||
return fmt.Sprintf("Season %02d", season)
|
||||
}
|
||||
|
||||
// episodeStem — "Название (Год) S01E02"; при двойной серии episodeEnd>episode
|
||||
// даёт "S01E02-E03".
|
||||
func episodeStem(base string, season, episode, episodeEnd int) string {
|
||||
if episodeEnd > episode {
|
||||
return fmt.Sprintf("%s S%02dE%02d-E%02d", base, season, episode, episodeEnd)
|
||||
}
|
||||
return fmt.Sprintf("%s S%02dE%02d", base, season, episode)
|
||||
}
|
||||
|
||||
// subtitleSuffix — ".ru", ".ru.forced" и т.п. (флаги после языка).
|
||||
func subtitleSuffix(lang string, flags []string) string {
|
||||
var b strings.Builder
|
||||
if l := sanitizeComponent(lang); l != "" {
|
||||
b.WriteByte('.')
|
||||
b.WriteString(strings.ToLower(l))
|
||||
}
|
||||
for _, f := range flags {
|
||||
if f = sanitizeComponent(f); f != "" {
|
||||
b.WriteByte('.')
|
||||
b.WriteString(strings.ToLower(f))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ext возвращает расширение файла источника в нижнем регистре (с точкой).
|
||||
func ext(src string) string {
|
||||
return strings.ToLower(filepath.Ext(src))
|
||||
}
|
||||
|
||||
// underRoot сообщает, лежит ли clean-путь p строго под каталогом root
|
||||
// (после filepath.Clean у обоих). Защита от traversal: даже если имя
|
||||
// прошло санитизацию, итог обязан остаться внутри библиотеки.
|
||||
func underRoot(root, p string) bool {
|
||||
root = filepath.Clean(root)
|
||||
p = filepath.Clean(p)
|
||||
if p == root {
|
||||
return false // сам корень — не цель для файла
|
||||
}
|
||||
return strings.HasPrefix(p, root+string(filepath.Separator))
|
||||
}
|
||||
Reference in New Issue
Block a user