feat: initial commit — unarr CLI
Search, inspect, stream, and download torrents from the terminal. Replaces the entire *arr stack with a single binary.
This commit is contained in:
commit
29cf0a0126
85 changed files with 10178 additions and 0 deletions
191
internal/ui/format.go
Normal file
191
internal/ui/format.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FormatSize converts bytes to human-readable format.
|
||||
func FormatSize(sizeBytes *int64) string {
|
||||
if sizeBytes == nil {
|
||||
return "?"
|
||||
}
|
||||
return FormatBytes(*sizeBytes)
|
||||
}
|
||||
|
||||
// FormatBytes converts bytes to human-readable format.
|
||||
func FormatBytes(b int64) string {
|
||||
if b == 0 {
|
||||
return "0 B"
|
||||
}
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
val := float64(b) / float64(div)
|
||||
units := []string{"KB", "MB", "GB", "TB"}
|
||||
if exp >= len(units) {
|
||||
exp = len(units) - 1
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", val, units[exp])
|
||||
}
|
||||
|
||||
// QualityIndicator returns a colored emoji for quality score (0-100 scale).
|
||||
func QualityIndicator(score *int) string {
|
||||
if score == nil {
|
||||
return " "
|
||||
}
|
||||
s := *score
|
||||
switch {
|
||||
case s >= 70:
|
||||
return "🟢"
|
||||
case s >= 40:
|
||||
return "🟡"
|
||||
default:
|
||||
return "🔴"
|
||||
}
|
||||
}
|
||||
|
||||
// SeedHealthIndicator returns a colored emoji for seed count.
|
||||
func SeedHealthIndicator(seeds int) string {
|
||||
switch {
|
||||
case seeds > 100:
|
||||
return "🟢"
|
||||
case seeds >= 10:
|
||||
return "🟡"
|
||||
default:
|
||||
return "🔴"
|
||||
}
|
||||
}
|
||||
|
||||
// FormatRating returns a display string for a rating.
|
||||
func FormatRating(rating *string) string {
|
||||
if rating == nil {
|
||||
return "-"
|
||||
}
|
||||
return *rating
|
||||
}
|
||||
|
||||
// FormatYear returns a display string for a year.
|
||||
func FormatYear(year *int) string {
|
||||
if year == nil {
|
||||
return "-"
|
||||
}
|
||||
return strconv.Itoa(*year)
|
||||
}
|
||||
|
||||
// FormatContentType returns a short display for content type.
|
||||
func FormatContentType(ct string) string {
|
||||
switch strings.ToLower(ct) {
|
||||
case "movie":
|
||||
return "Movie"
|
||||
case "show":
|
||||
return "Show"
|
||||
default:
|
||||
return ct
|
||||
}
|
||||
}
|
||||
|
||||
// Ptr returns a pointer to a value. Useful for tests.
|
||||
func Ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
// TruncateString truncates a string to maxLen with ellipsis.
|
||||
func TruncateString(s string, maxLen int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxLen {
|
||||
return s
|
||||
}
|
||||
if maxLen <= 3 {
|
||||
return string(runes[:maxLen])
|
||||
}
|
||||
return string(runes[:maxLen-3]) + "..."
|
||||
}
|
||||
|
||||
// FormatLanguages joins language codes.
|
||||
func FormatLanguages(langs []string) string {
|
||||
if len(langs) == 0 {
|
||||
return "-"
|
||||
}
|
||||
return strings.Join(langs, ", ")
|
||||
}
|
||||
|
||||
// FormatSeedRatio returns a display for seed/leech ratio.
|
||||
func FormatSeedRatio(seeders, leechers int) string {
|
||||
if leechers == 0 {
|
||||
if seeders == 0 {
|
||||
return "0:0"
|
||||
}
|
||||
return fmt.Sprintf("%d:0", seeders)
|
||||
}
|
||||
ratio := float64(seeders) / float64(leechers)
|
||||
return fmt.Sprintf("%.0f:1", math.Round(ratio))
|
||||
}
|
||||
|
||||
// FormatTimeAgo returns a human-readable "time ago" string.
|
||||
func FormatTimeAgo(t string) string {
|
||||
parsed, err := time.Parse(time.RFC3339, t)
|
||||
if err != nil {
|
||||
return t
|
||||
}
|
||||
diff := time.Since(parsed)
|
||||
switch {
|
||||
case diff < time.Minute:
|
||||
return "just now"
|
||||
case diff < time.Hour:
|
||||
m := int(diff.Minutes())
|
||||
return fmt.Sprintf("%dm ago", m)
|
||||
case diff < 24*time.Hour:
|
||||
h := int(diff.Hours())
|
||||
return fmt.Sprintf("%dh ago", h)
|
||||
case diff < 30*24*time.Hour:
|
||||
d := int(diff.Hours() / 24)
|
||||
return fmt.Sprintf("%dd ago", d)
|
||||
default:
|
||||
m := int(diff.Hours() / 24 / 30)
|
||||
return fmt.Sprintf("%dmo ago", m)
|
||||
}
|
||||
}
|
||||
|
||||
// FormatNumber formats a number with thousands separator.
|
||||
func FormatNumber(n int) string {
|
||||
negative := n < 0
|
||||
if negative {
|
||||
n = -n
|
||||
}
|
||||
s := strconv.Itoa(n)
|
||||
if len(s) <= 3 {
|
||||
if negative {
|
||||
return "-" + s
|
||||
}
|
||||
return s
|
||||
}
|
||||
var result []byte
|
||||
for i, c := range s {
|
||||
if i > 0 && (len(s)-i)%3 == 0 {
|
||||
result = append(result, ',')
|
||||
}
|
||||
result = append(result, byte(c))
|
||||
}
|
||||
if negative {
|
||||
return "-" + string(result)
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// StringOrDash returns the string value or "-" if nil.
|
||||
func StringOrDash(s *string) string {
|
||||
if s == nil {
|
||||
return "-"
|
||||
}
|
||||
return *s
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue