package templates import ( "encoding/json" "fmt" "html/template" "net/url" "os" "slices" "strconv" "strings" "time" ) func dict(values ...any) (map[string]any, error) { if len(values)%2 != 0 { return nil, fmt.Errorf("dict expects even number of values, got %d", len(values)) } out := make(map[string]any, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, fmt.Errorf("dict key at index %d is not a string", i) } out[key] = values[i+1] } return out, nil } func jsonAttr(v any) (template.HTMLAttr, error) { b, err := json.Marshal(v) if err != nil { return "", fmt.Errorf("json marshal: %w", err) } return template.HTMLAttr(b), nil } func genresParams(genres []int) string { if len(genres) == 0 { return "" } var b strings.Builder for i, g := range genres { if i > 0 { b.WriteByte('&') } b.WriteString("genres=") b.WriteString(strconv.Itoa(g)) } return b.String() } func browseURL(v map[string]any, overrides map[string]any) (string, error) { values := url.Values{} setQueryValue(values, "q", stringValue(v["Query"])) setQueryValue(values, "type", stringValue(v["Type"])) setQueryValue(values, "status", stringValue(v["Status"])) setQueryValue(values, "order_by", stringValue(v["OrderBy"])) setQueryValue(values, "sort", stringValue(v["Sort"])) setQueryValue(values, "studio", stringValue(v["Studio"])) if sfw, ok := v["SFW"]; ok && !boolValue(sfw) { values.Set("sfw", "false") } for _, genre := range intSliceValue(v["Genres"]) { values.Add("genres", strconv.Itoa(genre)) } setQueryValue(values, "page", stringValue(v["Page"])) for key, raw := range overrides { switch key { case "genres": values.Del("genres") for _, genre := range intSliceValue(raw) { values.Add("genres", strconv.Itoa(genre)) } case "sfw": values.Set("sfw", strconv.FormatBool(boolValue(raw))) default: setQueryValue(values, key, stringValue(raw)) } } encoded := values.Encode() if encoded == "" { return "/browse", nil } return "/browse?" + encoded, nil } func setQueryValue(values url.Values, key string, value string) { if value == "" { values.Del(key) return } values.Set(key, value) } func stringValue(v any) string { switch value := v.(type) { case nil: return "" case string: return value case int: if value == 0 { return "" } return strconv.Itoa(value) case int64: if value == 0 { return "" } return strconv.FormatInt(value, 10) case float64: if value == 0 { return "" } return strconv.Itoa(int(value)) default: return fmt.Sprint(v) } } func boolValue(v any) bool { switch value := v.(type) { case bool: return value case string: return value == "true" default: return false } } func intSliceValue(v any) []int { switch value := v.(type) { case []int: return value case []int64: out := make([]int, 0, len(value)) for _, item := range value { out = append(out, int(item)) } return out case []any: out := make([]int, 0, len(value)) for _, item := range value { n := toInt(item) if n > 0 { out = append(out, n) } } return out default: return nil } } func hasGenre(id int, genres []int) bool { return slices.Contains(genres, id) } func div(a, b float64) float64 { if b == 0 { return 0 } return a / b } func ceilDiv(a, b int) int { if b == 0 { return 0 } return (a + b - 1) / b } func idiv(a, b int) int { if b == 0 { return 0 } return a / b } func atoi(v any) int { s, ok := v.(string) if !ok { return 0 } n, err := strconv.Atoi(s) if err != nil { return 0 } return n } func seq(v any) []int { count := 0 switch n := v.(type) { case int: count = n case int64: if n > int64(^uint(0)>>1) { count = 0 } else { count = int(n) } } if count <= 0 { return []int{} } res := make([]int, count) for i := 0; i < count; i++ { res[i] = i } return res } func toInt(v any) int { switch n := v.(type) { case int: return n case int64: if n > int64(^uint(0)>>1) { return 0 } return int(n) case float64: if n > float64(int(^uint(0)>>1)) { return 0 } return int(n) case string: i, err := strconv.Atoi(n) if err != nil { return 0 } return i default: return 0 } } func percent(current, total float64) float64 { if total == 0 { return 0 } return (current / total) * 100 } func formatDate(dateStr string) string { t, err := time.Parse(time.RFC3339, dateStr) if err != nil { t, err = time.Parse("2006-01-02T15:04:05+00:00", dateStr) if err != nil { return dateStr } } return t.Format("Jan 2, 2006") } func nextSort(sort string) string { if sort == "asc" { return "desc" } return "asc" } func posterURL(webp, jpg any, width, height int) string { if webp != nil { if s, ok := webp.(string); ok && s != "" { return s } } if jpg != nil { if s, ok := jpg.(string); ok && s != "" { return s } } return fmt.Sprintf("https://placehold.co/%dx%d?text=No+Image", width, height) } func episodeRangeStart(epNum, step int) int { if epNum < 1 || step < 1 { return 1 } return ((epNum-1)/step)*step + 1 } func assetURL(assetPath string) string { info, err := os.Stat(strings.TrimPrefix(assetPath, "/")) if err != nil { return assetPath } values := url.Values{} values.Set("v", fmt.Sprintf("%d-%d", info.ModTime().Unix(), info.Size())) return assetPath + "?" + values.Encode() }