380 lines
8.4 KiB
Go
380 lines
8.4 KiB
Go
package arn
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"
|
|
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("<not found: %s>", 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("<not found: %s>", 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 := json.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"))
|
|
}
|