package arn import ( "errors" "flag" "fmt" "os" "path" "reflect" "regexp" "strconv" "strings" "time" "github.com/aerogo/aero" "github.com/aerogo/mirror" "github.com/akyoto/color" "github.com/animenotifier/kitsu" "github.com/animenotifier/mal" jsoniter "github.com/json-iterator/go" shortid "github.com/ventu-io/go-shortid" ) var ( // MediaHost is the host we use to link image files. MediaHost = "media.notify.moe" // Regular expressions stripTagsRegex = regexp.MustCompile(`<[^>]*>`) sourceRegex = regexp.MustCompile(`\(Source: (.*?)\)`) writtenByRegex = regexp.MustCompile(`\[Written by (.*?)\]`) ) // GenerateID generates a unique ID for a given collection. func GenerateID(collection string) string { id, _ := shortid.Generate() // Retry until we find an unused ID retry := 0 for { _, err := DB.Get(collection, id) if err != nil && strings.Contains(err.Error(), "not found") { return id } retry++ if retry > 10 { panic(errors.New("Can't generate unique ID")) } id, _ = shortid.Generate() } } // GetUserFromContext returns the logged in user for the given context. func GetUserFromContext(ctx aero.Context) *User { if !ctx.HasSession() { return nil } userID := ctx.Session().GetString("userId") if userID == "" { return nil } user, err := GetUser(userID) if err != nil { return nil } return user } // GetObjectTitle ... func GetObjectTitle(typeName string, id string) string { obj, err := DB.Get(typeName, id) if err != nil { return fmt.Sprintf("", id) } return fmt.Sprint(obj) } // GetObjectLink ... func GetObjectLink(typeName string, id string) string { obj, err := DB.Get(typeName, id) if err != nil { return fmt.Sprintf("", id) } linkable, ok := obj.(Linkable) if ok { return linkable.Link() } return "/" + strings.ToLower(typeName) + "/" + id } // FilterIDTags returns all IDs of the given type in the tag list. func FilterIDTags(tags []string, idType string) []string { var idList []string prefix := idType + ":" for _, tag := range tags { if strings.HasPrefix(tag, prefix) { id := strings.TrimPrefix(tag, prefix) idList = append(idList, id) } } return idList } // AgeInYears returns the person's age in years. func AgeInYears(birthDayString string) int { birthDay, err := time.Parse("2006-01-02", birthDayString) if err != nil { return 0 } now := time.Now() years := now.Year() - birthDay.Year() if now.YearDay() < birthDay.YearDay() { years-- } return years } // JSON turns the object into a JSON string. func JSON(obj interface{}) string { data, err := jsoniter.Marshal(obj) if err == nil { return string(data) } return err.Error() } // SetObjectProperties updates the object with the given map[string]interface{} func SetObjectProperties(rootObj interface{}, updates map[string]interface{}) error { for key, value := range updates { field, _, v, err := mirror.GetField(rootObj, key) if err != nil { return err } // Is somebody attempting to edit fields that aren't editable? if field.Tag.Get("editable") != "true" { return errors.New("Field " + key + " is not editable") } newValue := reflect.ValueOf(value) // Implement special data type cases here if v.Kind() == reflect.Int { x := int64(newValue.Float()) if !v.OverflowInt(x) { v.SetInt(x) } } else { v.Set(newValue) } } return nil } // GetGenreIDByName ... func GetGenreIDByName(genre string) string { genre = strings.Replace(genre, "-", "", -1) genre = strings.Replace(genre, " ", "", -1) genre = strings.ToLower(genre) return genre } // FixAnimeDescription ... func FixAnimeDescription(description string) string { description = stripTagsRegex.ReplaceAllString(description, "") description = sourceRegex.ReplaceAllString(description, "") description = writtenByRegex.ReplaceAllString(description, "") return strings.TrimSpace(description) } // FixGender ... func FixGender(gender string) string { if gender != "male" && gender != "female" { return "" } return gender } // DateToSeason returns the season of the year for the given date. func DateToSeason(date time.Time) string { month := date.Month() if month >= 4 && month <= 6 { return "spring" } if month >= 7 && month <= 9 { return "summer" } if month >= 10 && month <= 12 { return "autumn" } if month >= 1 && month < 4 { return "winter" } return "" } // BroadcastEvent sends the given event to the event streams of all users. func BroadcastEvent(event *aero.Event) { for user := range StreamUsers() { user.BroadcastEvent(event) } } // AnimeRatingStars displays the rating in Unicode stars. func AnimeRatingStars(rating float64) string { stars := int(rating/20 + 0.5) return strings.Repeat("★", stars) + strings.Repeat("☆", 5-stars) } // EpisodesToString shows a question mark if the episode count is zero. func EpisodesToString(episodes int) string { if episodes == 0 { return "?" } return fmt.Sprint(episodes) } // EpisodeCountMax is used for the max value of number input on episodes. func EpisodeCountMax(episodes int) string { if episodes == 0 { return "" } return strconv.Itoa(episodes) } // DateTimeUTC returns the current UTC time in RFC3339 format. func DateTimeUTC() string { return time.Now().UTC().Format(time.RFC3339) } // OverallRatingName returns Overall in general, but Hype when episodes watched is zero. func OverallRatingName(episodes int) string { if episodes == 0 { return "Hype" } return "Overall" } // IsIPv6 tells you whether the given address is IPv6 encoded. func IsIPv6(ip string) bool { for i := 0; i < len(ip); i++ { if ip[i] == ':' { return true } } return false } // MyAnimeListStatusToARNStatus ... func MyAnimeListStatusToARNStatus(status int) string { switch status { case mal.AnimeListStatusCompleted: return AnimeListStatusCompleted case mal.AnimeListStatusWatching: return AnimeListStatusWatching case mal.AnimeListStatusPlanned: return AnimeListStatusPlanned case mal.AnimeListStatusHold: return AnimeListStatusHold case mal.AnimeListStatusDropped: return AnimeListStatusDropped default: return "" } } // KitsuStatusToARNStatus ... func KitsuStatusToARNStatus(status string) string { switch status { case kitsu.AnimeListStatusCompleted: return AnimeListStatusCompleted case kitsu.AnimeListStatusWatching: return AnimeListStatusWatching case kitsu.AnimeListStatusPlanned: return AnimeListStatusPlanned case kitsu.AnimeListStatusHold: return AnimeListStatusHold case kitsu.AnimeListStatusDropped: return AnimeListStatusDropped default: return "" } } // ListItemStatusName ... func ListItemStatusName(status string) string { switch status { case AnimeListStatusWatching: return "Watching" case AnimeListStatusCompleted: return "Completed" case AnimeListStatusPlanned: return "Planned" case AnimeListStatusHold: return "On hold" case AnimeListStatusDropped: return "Dropped" default: return "" } } // IsTest returns true if the program is currently running in the "go test" tool. func IsTest() bool { return flag.Lookup("test.v") != nil } // PanicOnError will panic if the error is not nil. func PanicOnError(err error) { if err != nil { panic(err) } } // deleteImages deletes images in the given folder. func deleteImages(folderName string, id string, originalExtension string) { if originalExtension == "" { return } err := os.Remove(path.Join(Root, "images", folderName, "original", id+originalExtension)) if err != nil { // Don't return the error. // It's too late to stop the process at this point. // Instead, log the error. color.Red(err.Error()) } os.Remove(path.Join(Root, "images", folderName, "large", id+".jpg")) os.Remove(path.Join(Root, "images", folderName, "large", id+"@2.jpg")) os.Remove(path.Join(Root, "images", folderName, "large", id+".webp")) os.Remove(path.Join(Root, "images", folderName, "large", id+"@2.webp")) os.Remove(path.Join(Root, "images", folderName, "medium", id+".jpg")) os.Remove(path.Join(Root, "images", folderName, "medium", id+"@2.jpg")) os.Remove(path.Join(Root, "images", folderName, "medium", id+".webp")) os.Remove(path.Join(Root, "images", folderName, "medium", id+"@2.webp")) os.Remove(path.Join(Root, "images", folderName, "small", id+".jpg")) os.Remove(path.Join(Root, "images", folderName, "small", id+"@2.jpg")) os.Remove(path.Join(Root, "images", folderName, "small", id+".webp")) os.Remove(path.Join(Root, "images", folderName, "small", id+"@2.webp")) }