Files

83 lines
2.2 KiB
Go

package media
import (
"fmt"
"path/filepath"
"strings"
"unicode"
)
const (
maxSeriesBytes = 90
maxEpisodeBytes = 140
maxEpisodeIDBytes = 64
maxFilenameBytes = 255
)
// RelativeVideoPath は worker と web が共有する保存先の相対パスを返す。
func RelativeVideoPath(seriesTitle, episodeTitle, episodeID string) (string, error) {
series := sanitizeComponent(seriesTitle, maxSeriesBytes)
episode := sanitizeComponent(episodeTitle, maxEpisodeBytes)
id := sanitizeComponent(episodeID, -1)
if len(id) > maxEpisodeIDBytes {
return "", fmt.Errorf("エピソードIDはサニタイズ後%dバイト以下である必要があります: %dバイト", maxEpisodeIDBytes, len(id))
}
// A directory entry must fit within the NFS namlen limit. Keep the episode
// ID intact and use the remaining bytes for the episode title.
const separators = " [].mp4"
seriesInFilename := series
titleBytes := maxFilenameBytes - len(seriesInFilename) - len(id) - len(separators)
if titleBytes < 0 {
titleBytes = 0
}
episode = truncateComponent(episode, titleBytes, false)
return filepath.Join(series, seriesInFilename+" "+episode+" ["+id+"].mp4"), nil
}
func sanitizeComponent(value string, maxBytes int) string {
var result []rune
lastReplacement := false
for _, r := range value {
invalid := unicode.IsControl(r) || strings.ContainsRune(`<>:"/\|?*`, r)
if invalid {
if !lastReplacement {
result = append(result, '_')
lastReplacement = true
}
continue
}
if unicode.IsSpace(r) {
r = ' '
}
result = append(result, r)
lastReplacement = false
}
clean := strings.Trim(string(result), " .")
if clean == "" || clean == "." || clean == ".." {
return "名称未設定"
}
return truncateComponent(clean, maxBytes, true)
}
func truncateComponent(value string, maxBytes int, fallback bool) string {
if maxBytes < 0 {
return value
}
var limited []rune
bytes := 0
for _, r := range value {
size := len(string(r))
if bytes+size > maxBytes {
break
}
limited = append(limited, r)
bytes += size
}
clean := strings.Trim(string(limited), " .")
if fallback && (clean == "" || clean == "." || clean == "..") {
return "名称未設定"
}
return clean
}