package utils import ( "crypto/rand" "encoding/json" "errors" "fmt" "io" "math/big" "net/http" "os" "strings" "time" "github.com/gabehf/koito/internal/mbz" "github.com/gabehf/koito/internal/models" "github.com/google/uuid" ) func IDFromString(s string) string { s = strings.ToLower(s) s = strings.ReplaceAll(s, " ", "-") return s } func ParseUUIDSlice(str []string) ([]uuid.UUID, error) { ret := make([]uuid.UUID, 0) for _, s := range str { parsed, err := uuid.Parse(s) if err != nil { continue } ret = append(ret, parsed) } return ret, nil } func FlattenArtistMbzIDs(artists []*models.Artist) []uuid.UUID { ids := make([]uuid.UUID, 0) for _, a := range artists { if a.MbzID == nil || *a.MbzID == uuid.Nil { continue } ids = append(ids, *a.MbzID) } return ids } func FlattenArtistNames(artists []*models.Artist) []string { names := make([]string, 0) for _, a := range artists { names = append(names, a.Aliases...) } return names } func FlattenSimpleArtistNames(artists []models.SimpleArtist) []string { names := make([]string, 0) for _, a := range artists { names = append(names, a.Name) } return names } func FlattenMbzArtistCreditNames(artists []mbz.MusicBrainzArtistCredit) []string { names := make([]string, len(artists)) for i, a := range artists { names[i] = a.Name } return names } func FlattenArtistIDs(artists []*models.Artist) []int32 { ids := make([]int32, len(artists)) for i, a := range artists { ids[i] = a.ID } return ids } // DateRange takes optional week, month, and year. If all are 0, it returns the zero time range. // If only year is provided, it returns the full year. // If both month and year are provided, it returns the start and end of that month. // If week and year are provided, it returns the start and end of that week. // If only week or month is provided without a year, it's considered invalid. func DateRange(week, month, year int) (time.Time, time.Time, error) { if week == 0 && month == 0 && year == 0 { // No filter applied return time.Time{}, time.Time{}, nil } if month != 0 && (month < 1 || month > 12) { return time.Time{}, time.Time{}, errors.New("invalid month") } if week != 0 && (week < 1 || week > 53) { return time.Time{}, time.Time{}, errors.New("invalid week") } if year < 1 { return time.Time{}, time.Time{}, errors.New("invalid year") } loc := time.Local if week != 0 { if month != 0 { return time.Time{}, time.Time{}, errors.New("cannot specify both week and month") } // Specific week start := time.Date(year, 1, 1, 0, 0, 0, 0, loc) start = start.AddDate(0, 0, (week-1)*7) end := start.AddDate(0, 0, 7) return start, end, nil } if month == 0 { // Whole year start := time.Date(year, 1, 1, 0, 0, 0, 0, loc) end := start.AddDate(1, 0, 0) return start, end, nil } // Specific month start := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc) end := start.AddDate(0, 1, 0) return start, end, nil } // CopyFile copies a file from src to dst. If src and dst files exist, and are // the same, then return success. Otherise, attempt to create a hard link // between the two files. If that fail, copy the file contents from src to dst. func CopyFile(src, dst string) (err error) { sfi, err := os.Stat(src) if err != nil { return } if !sfi.Mode().IsRegular() { // cannot copy non-regular files (e.g., directories, // symlinks, devices, etc.) return fmt.Errorf("non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String()) } dfi, err := os.Stat(dst) if err != nil { if !os.IsNotExist(err) { return } } else { if !(dfi.Mode().IsRegular()) { return fmt.Errorf("non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String()) } if os.SameFile(sfi, dfi) { return } } if err = os.Link(src, dst); err == nil { return } err = copyFileContents(src, dst) return } // copyFileContents copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents // of the source file. func copyFileContents(src, dst string) (err error) { in, err := os.Open(src) if err != nil { return } defer in.Close() out, err := os.Create(dst) if err != nil { return } defer func() { cerr := out.Close() if err == nil { err = cerr } }() if _, err = io.Copy(out, in); err != nil { return } err = out.Sync() return } // Returns the same slice, but with all strings that are equal (with strings.EqualFold) // included only once func UniqueIgnoringCase(s []string) []string { unique := []string{} for _, str := range s { isDuplicate := false for _, u := range unique { if strings.EqualFold(str, u) { isDuplicate = true break } } if !isDuplicate { unique = append(unique, str) } } return unique } // Removes duplicates in a string set func Unique(xs *[]string) { found := make(map[string]bool) j := 0 for i, x := range *xs { if !found[x] { found[x] = true (*xs)[j] = (*xs)[i] j++ } } *xs = (*xs)[:j] } // Returns the same slice, but with all entries that contain non ASCII characters removed func RemoveNonAscii(s []string) []string { filtered := []string{} for _, str := range s { isAscii := true for _, r := range str { if r > 127 { isAscii = false break } } if isAscii { filtered = append(filtered, str) } } return filtered } // Returns only items that are in one slice but not the other func RemoveInBoth(s, c []string) []string { result := []string{} set := make(map[string]struct{}) for _, str := range c { set[str] = struct{}{} } for _, str := range s { if _, exists := set[str]; !exists { result = append(result, str) } } return result } // MoveFirstMatchToFront moves the first string containing the substring to the front of the slice. func MoveFirstMatchToFront(slice []string, substring string) []string { for i, s := range slice { if strings.Contains(s, substring) { if i == 0 { return slice // already at the front } // Move the matching element to the front return append([]string{slice[i]}, append(slice[:i], slice[i+1:]...)...) } } // No match found, return unchanged return slice } // Taken with little modification from // https://gist.github.com/dopey/c69559607800d2f2f90b1b1ed4e550fb?permalink_comment_id=3527095#gistcomment-3527095 func GenerateRandomString(length int) (string, error) { const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" ret := make([]byte, length) for i := range length { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) if err != nil { return "", err } ret[i] = letters[num.Int64()] } return string(ret), nil } // Essentially the same as utils.WriteError(w, `{"error": "message"}`, code) func WriteError(w http.ResponseWriter, message string, code int) { http.Error(w, fmt.Sprintf(`{"error":"%s"}`, message), code) } // Sets content type and status code, and encodes data to json func WriteJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } // Returns true if more than one string is not empty func MoreThanOneString(s ...string) bool { count := 0 for _, str := range s { if str != "" { count++ } } return count > 1 }