package api import ( "errors" "fmt" "math" "path/filepath" "reflect" "strings" "reichard.io/antholume/database" "reichard.io/antholume/graph" "reichard.io/antholume/metadata" ) // getTimeZones returns a string slice of IANA timezones. func getTimeZones() []string { return []string{ "Africa/Cairo", "Africa/Johannesburg", "Africa/Lagos", "Africa/Nairobi", "America/Adak", "America/Anchorage", "America/Buenos_Aires", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Mexico_City", "America/New_York", "America/Nuuk", "America/Phoenix", "America/Puerto_Rico", "America/Sao_Paulo", "America/St_Johns", "America/Toronto", "Asia/Dubai", "Asia/Hong_Kong", "Asia/Kolkata", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Tokyo", "Atlantic/Azores", "Australia/Melbourne", "Australia/Sydney", "Europe/Berlin", "Europe/London", "Europe/Moscow", "Europe/Paris", "Pacific/Auckland", "Pacific/Honolulu", } } // niceSeconds takes in an int (in seconds) and returns a string readable // representation. For example 1928371 -> "22d 7h 39m 31s". func niceSeconds(input int64) (result string) { if input == 0 { return "N/A" } days := math.Floor(float64(input) / 60 / 60 / 24) seconds := input % (60 * 60 * 24) hours := math.Floor(float64(seconds) / 60 / 60) seconds = input % (60 * 60) minutes := math.Floor(float64(seconds) / 60) seconds = input % 60 if days > 0 { result += fmt.Sprintf("%dd ", int(days)) } if hours > 0 { result += fmt.Sprintf("%dh ", int(hours)) } if minutes > 0 { result += fmt.Sprintf("%dm ", int(minutes)) } if seconds > 0 { result += fmt.Sprintf("%ds", int(seconds)) } return } // niceNumbers takes in an int and returns a string representation. For example // 19823 -> "19.8k". func niceNumbers(input int64) string { if input == 0 { return "0" } abbreviations := []string{"", "k", "M", "B", "T"} abbrevIndex := int(math.Log10(float64(input)) / 3) scaledNumber := float64(input) / math.Pow(10, float64(abbrevIndex*3)) if scaledNumber >= 100 { return fmt.Sprintf("%.0f%s", scaledNumber, abbreviations[abbrevIndex]) } else if scaledNumber >= 10 { return fmt.Sprintf("%.1f%s", scaledNumber, abbreviations[abbrevIndex]) } else { return fmt.Sprintf("%.2f%s", scaledNumber, abbreviations[abbrevIndex]) } } // getSVGGraphData builds SVGGraphData from the provided stats, width and height. // It is used exclusively in templates to generate the daily read stats graph. func getSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData { var intData []int64 for _, item := range inputData { intData = append(intData, item.MinutesRead) } return graph.GetSVGGraphData(intData, svgWidth, svgHeight) } // dict returns a map[string]any dict. Each pair of two is a key & value // respectively. It's primarily utilized in templates. func dict(values ...any) (map[string]any, error) { if len(values)%2 != 0 { return nil, errors.New("invalid dict call") } dict := make(map[string]any, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, errors.New("dict keys must be strings") } dict[key] = values[i+1] } return dict, nil } // fields returns a map[string]any of the provided struct. It's primarily // utilized in templates. func fields(value any) (map[string]any, error) { v := reflect.Indirect(reflect.ValueOf(value)) if v.Kind() != reflect.Struct { return nil, fmt.Errorf("%T is not a struct", value) } m := make(map[string]any) t := v.Type() for i := 0; i < t.NumField(); i++ { sv := t.Field(i) m[sv.Name] = v.Field(i).Interface() } return m, nil } // slice returns a slice of the provided arguments. It's primarily utilized in // templates. func slice(elements ...any) []any { return elements } // deriveBaseFileName builds the base filename for a given MetadataInfo object. func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string { // Derive New FileName var newFileName string if *metadataInfo.Author != "" { newFileName = newFileName + *metadataInfo.Author } else { newFileName = newFileName + "Unknown" } if *metadataInfo.Title != "" { newFileName = newFileName + " - " + *metadataInfo.Title } else { newFileName = newFileName + " - Unknown" } // Remove Slashes fileName := strings.ReplaceAll(newFileName, "/", "") return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type)) } // importStatusPriority returns the order priority for import status in the UI. func importStatusPriority(status importStatus) int { switch status { case importFailed: return 1 case importExists: return 2 default: return 3 } }