Files
seanime-docker/seanime-2.9.10/internal/util/strings.go
2025-09-20 14:08:38 +01:00

274 lines
5.9 KiB
Go

package util
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"math/big"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"unicode"
"github.com/dustin/go-humanize"
)
func Bytes(size uint64) string {
switch runtime.GOOS {
case "darwin":
return humanize.Bytes(size)
default:
return humanize.IBytes(size)
}
}
func Decode(s string) string {
decoded, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return ""
}
return string(decoded)
}
func GenerateCryptoID() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
panic(err)
}
return hex.EncodeToString(bytes)
}
func IsMostlyLatinString(str string) bool {
if len(str) <= 0 {
return false
}
latinLength := 0
nonLatinLength := 0
for _, r := range str {
if isLatinRune(r) {
latinLength++
} else {
nonLatinLength++
}
}
return latinLength > nonLatinLength
}
func isLatinRune(r rune) bool {
return unicode.In(r, unicode.Latin)
}
// ToHumanReadableSpeed converts an integer representing bytes per second to a human-readable format using binary notation
func ToHumanReadableSpeed(bytesPerSecond int) string {
if bytesPerSecond <= 0 {
return `0 KiB/s`
}
const unit = 1024
if bytesPerSecond < unit {
return fmt.Sprintf("%d B/s", bytesPerSecond)
}
div, exp := int64(unit), 0
for n := int64(bytesPerSecond) / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB/s", float64(bytesPerSecond)/float64(div), "KMGTPE"[exp])
}
func StringSizeToBytes(str string) (int64, error) {
// Regular expression to extract size and unit
re := regexp.MustCompile(`(?i)^(\d+(\.\d+)?)\s*([KMGT]?i?B)$`)
match := re.FindStringSubmatch(strings.TrimSpace(str))
if match == nil {
return 0, fmt.Errorf("invalid size format: %s", str)
}
// Extract the numeric part and convert to float64
size, err := strconv.ParseFloat(match[1], 64)
if err != nil {
return 0, fmt.Errorf("failed to parse size: %s", err)
}
// Extract the unit and convert to lowercase
unit := strings.ToLower(match[3])
// Map units to their respective multipliers
unitMultipliers := map[string]int64{
"b": 1,
"bi": 1,
"kb": 1024,
"kib": 1024,
"mb": 1024 * 1024,
"mib": 1024 * 1024,
"gb": 1024 * 1024 * 1024,
"gib": 1024 * 1024 * 1024,
"tb": 1024 * 1024 * 1024 * 1024,
"tib": 1024 * 1024 * 1024 * 1024,
}
// Apply the multiplier based on the unit
multiplier, ok := unitMultipliers[unit]
if !ok {
return 0, fmt.Errorf("invalid unit: %s", unit)
}
// Calculate the total bytes
bytes := int64(size * float64(multiplier))
return bytes, nil
}
// FormatETA formats an ETA (in seconds) into a human-readable string
func FormatETA(etaInSeconds int) string {
const noETA = 8640000
if etaInSeconds == noETA {
return "No ETA"
}
etaDuration := time.Duration(etaInSeconds) * time.Second
hours := int(etaDuration.Hours())
minutes := int(etaDuration.Minutes()) % 60
seconds := int(etaDuration.Seconds()) % 60
switch {
case hours > 0:
return fmt.Sprintf("%d hours left", hours)
case minutes > 0:
return fmt.Sprintf("%d minutes left", minutes)
case seconds < 0:
return "No ETA"
default:
return fmt.Sprintf("%d seconds left", seconds)
}
}
func Pluralize(count int, singular, plural string) string {
if count == 1 {
return singular
}
return plural
}
// NormalizePath normalizes a path by converting it to lowercase and replacing backslashes with forward slashes
// Warning: Do not use the returned string for anything filesystem related, only for comparison
func NormalizePath(path string) (ret string) {
return strings.ToLower(filepath.ToSlash(path))
}
func Base64EncodeStr(str string) string {
return base64.StdEncoding.EncodeToString([]byte(str))
}
func Base64DecodeStr(str string) (string, error) {
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", err
}
return string(decoded), nil
}
func IsBase64(s string) bool {
// 1. Check if string is empty
if len(s) == 0 {
return false
}
// 2. Check if length is valid (must be multiple of 4)
if len(s)%4 != 0 {
return false
}
// 3. Check for valid padding
padding := strings.Count(s, "=")
if padding > 2 {
return false
}
// 4. Check if padding is at the end only
if padding > 0 && !strings.HasSuffix(s, strings.Repeat("=", padding)) {
return false
}
// 5. Check if string contains only valid base64 characters
validChars := regexp.MustCompile("^[A-Za-z0-9+/]*=*$")
if !validChars.MatchString(s) {
return false
}
// 6. Try to decode - this is the final verification
_, err := base64.StdEncoding.DecodeString(s)
return err == nil
}
var snakecaseSplitRegex = regexp.MustCompile(`[\W_]+`)
func Snakecase(str string) string {
var result strings.Builder
// split at any non word character and underscore
words := snakecaseSplitRegex.Split(str, -1)
for _, word := range words {
if word == "" {
continue
}
if result.Len() > 0 {
result.WriteString("_")
}
for i, c := range word {
if unicode.IsUpper(c) && i > 0 &&
// is not a following uppercase character
!unicode.IsUpper(rune(word[i-1])) {
result.WriteString("_")
}
result.WriteRune(c)
}
}
return strings.ToLower(result.String())
}
// randomStringWithAlphabet generates a cryptographically random string
// with the specified length and characters set.
//
// It panics if for some reason rand.Int returns a non-nil error.
func RandomStringWithAlphabet(length int, alphabet string) string {
b := make([]byte, length)
max := big.NewInt(int64(len(alphabet)))
for i := range b {
n, err := rand.Int(rand.Reader, max)
if err != nil {
panic(err)
}
b[i] = alphabet[n.Int64()]
}
return string(b)
}
func FileExt(str string) string {
lastDotIndex := strings.LastIndex(str, ".")
if lastDotIndex == -1 {
return ""
}
return str[lastDotIndex:]
}
func HashSHA256Hex(s string) string {
h := sha256.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}