2019-06-03 09:32:43 +00:00
|
|
|
package arn
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/aerogo/nano"
|
|
|
|
)
|
|
|
|
|
2019-11-18 05:01:13 +00:00
|
|
|
// CharacterID represents a character ID.
|
|
|
|
type CharacterID = ID
|
|
|
|
|
2019-06-03 09:32:43 +00:00
|
|
|
// Character represents an anime or manga character.
|
|
|
|
type Character struct {
|
|
|
|
Name CharacterName `json:"name" editable:"true"`
|
2019-06-04 03:16:27 +00:00
|
|
|
Image Image `json:"image"`
|
2019-11-18 05:01:13 +00:00
|
|
|
MainQuoteID QuoteID `json:"mainQuoteId" editable:"true"`
|
2019-06-03 09:32:43 +00:00
|
|
|
Description string `json:"description" editable:"true" type:"textarea"`
|
|
|
|
Spoilers []Spoiler `json:"spoilers" editable:"true"`
|
|
|
|
Attributes []*CharacterAttribute `json:"attributes" editable:"true"`
|
|
|
|
|
|
|
|
hasID
|
|
|
|
hasPosts
|
|
|
|
hasMappings
|
|
|
|
hasCreator
|
|
|
|
hasEditor
|
|
|
|
hasLikes
|
|
|
|
hasDraft
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCharacter creates a new character.
|
|
|
|
func NewCharacter() *Character {
|
|
|
|
return &Character{
|
|
|
|
hasID: hasID{
|
|
|
|
ID: GenerateID("Character"),
|
|
|
|
},
|
|
|
|
hasCreator: hasCreator{
|
|
|
|
Created: DateTimeUTC(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Link ...
|
|
|
|
func (character *Character) Link() string {
|
|
|
|
return "/character/" + character.ID
|
|
|
|
}
|
|
|
|
|
|
|
|
// TitleByUser returns the preferred title for the given user.
|
|
|
|
func (character *Character) TitleByUser(user *User) string {
|
|
|
|
return character.Name.ByUser(user)
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns the canonical name of the character.
|
|
|
|
func (character *Character) String() string {
|
|
|
|
return character.Name.Canonical
|
|
|
|
}
|
|
|
|
|
|
|
|
// TypeName returns the type name.
|
|
|
|
func (character *Character) TypeName() string {
|
|
|
|
return "Character"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Self returns the object itself.
|
|
|
|
func (character *Character) Self() Loggable {
|
|
|
|
return character
|
|
|
|
}
|
|
|
|
|
|
|
|
// MainQuote ...
|
|
|
|
func (character *Character) MainQuote() *Quote {
|
|
|
|
quote, _ := GetQuote(character.MainQuoteID)
|
|
|
|
return quote
|
|
|
|
}
|
|
|
|
|
|
|
|
// AverageColor returns the average color of the image.
|
|
|
|
func (character *Character) AverageColor() string {
|
|
|
|
color := character.Image.AverageColor
|
|
|
|
|
|
|
|
if color.Hue == 0 && color.Saturation == 0 && color.Lightness == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return color.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ImageLink ...
|
|
|
|
func (character *Character) ImageLink(size string) string {
|
|
|
|
extension := ".jpg"
|
|
|
|
|
|
|
|
if size == "original" {
|
|
|
|
extension = character.Image.Extension
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprintf("//%s/images/characters/%s/%s%s?%v", MediaHost, size, character.ID, extension, character.Image.LastModified)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Publish publishes the character draft.
|
|
|
|
func (character *Character) Publish() error {
|
|
|
|
// No name
|
|
|
|
if character.Name.Canonical == "" {
|
|
|
|
return errors.New("No canonical character name")
|
|
|
|
}
|
|
|
|
|
|
|
|
// No image
|
|
|
|
if !character.HasImage() {
|
|
|
|
return errors.New("No character image")
|
|
|
|
}
|
|
|
|
|
|
|
|
return publish(character)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unpublish turns the character into a draft.
|
|
|
|
func (character *Character) Unpublish() error {
|
|
|
|
return unpublish(character)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Anime returns a list of all anime the character appears in.
|
|
|
|
func (character *Character) Anime() []*Anime {
|
|
|
|
var results []*Anime
|
|
|
|
|
|
|
|
for animeCharacters := range StreamAnimeCharacters() {
|
|
|
|
if animeCharacters.Contains(character.ID) {
|
|
|
|
anime, err := GetAnime(animeCharacters.AnimeID)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
results = append(results, anime)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return results
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCharacter ...
|
2019-11-18 06:13:51 +00:00
|
|
|
func GetCharacter(id CharacterID) (*Character, error) {
|
2019-06-03 09:32:43 +00:00
|
|
|
obj, err := DB.Get("Character", id)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj.(*Character), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge deletes the character and moves all existing references to the new character.
|
|
|
|
func (character *Character) Merge(target *Character) {
|
|
|
|
// Check anime characters
|
|
|
|
for list := range StreamAnimeCharacters() {
|
|
|
|
for _, animeCharacter := range list.Items {
|
|
|
|
if animeCharacter.CharacterID == character.ID {
|
|
|
|
animeCharacter.CharacterID = target.ID
|
|
|
|
list.Save()
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check quotes
|
|
|
|
for quote := range StreamQuotes() {
|
|
|
|
if quote.CharacterID == character.ID {
|
|
|
|
quote.CharacterID = target.ID
|
|
|
|
quote.Save()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check log
|
|
|
|
for entry := range StreamEditLogEntries() {
|
|
|
|
if entry.ObjectType != "Character" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if entry.ObjectID == character.ID {
|
|
|
|
// Delete log entries for the old character
|
|
|
|
DB.Delete("EditLogEntry", entry.ID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge likes
|
|
|
|
for _, userID := range character.Likes {
|
|
|
|
if !Contains(target.Likes, userID) {
|
|
|
|
target.Likes = append(target.Likes, userID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
target.Save()
|
|
|
|
|
|
|
|
// Delete image files
|
|
|
|
character.DeleteImages()
|
|
|
|
|
|
|
|
// Delete character
|
|
|
|
DB.Delete("Character", character.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteImages deletes all images for the character.
|
|
|
|
func (character *Character) DeleteImages() {
|
|
|
|
deleteImages("characters", character.ID, character.Image.Extension)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Quotes returns the list of quotes for this character.
|
|
|
|
func (character *Character) Quotes() []*Quote {
|
|
|
|
return FilterQuotes(func(quote *Quote) bool {
|
|
|
|
return !quote.IsDraft && quote.CharacterID == character.ID
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// SortCharactersByLikes sorts the given slice of characters by the amount of likes.
|
|
|
|
func SortCharactersByLikes(characters []*Character) {
|
|
|
|
sort.Slice(characters, func(i, j int) bool {
|
|
|
|
aLikes := len(characters[i].Likes)
|
|
|
|
bLikes := len(characters[j].Likes)
|
|
|
|
|
|
|
|
if aLikes == bLikes {
|
|
|
|
return characters[i].Name.Canonical < characters[j].Name.Canonical
|
|
|
|
}
|
|
|
|
|
|
|
|
return aLikes > bLikes
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// StreamCharacters returns a stream of all characters.
|
|
|
|
func StreamCharacters() <-chan *Character {
|
|
|
|
channel := make(chan *Character, nano.ChannelBufferSize)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for obj := range DB.All("Character") {
|
|
|
|
channel <- obj.(*Character)
|
|
|
|
}
|
|
|
|
|
|
|
|
close(channel)
|
|
|
|
}()
|
|
|
|
|
|
|
|
return channel
|
|
|
|
}
|
|
|
|
|
|
|
|
// FilterCharacters filters all characters by a custom function.
|
|
|
|
func FilterCharacters(filter func(*Character) bool) []*Character {
|
|
|
|
var filtered []*Character
|
|
|
|
|
|
|
|
channel := DB.All("Character")
|
|
|
|
|
|
|
|
for obj := range channel {
|
|
|
|
realObject := obj.(*Character)
|
|
|
|
|
|
|
|
if filter(realObject) {
|
|
|
|
filtered = append(filtered, realObject)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return filtered
|
|
|
|
}
|
|
|
|
|
|
|
|
// AllCharacters returns a slice of all characters.
|
|
|
|
func AllCharacters() []*Character {
|
|
|
|
all := make([]*Character, 0, DB.Collection("Character").Count())
|
|
|
|
|
|
|
|
stream := StreamCharacters()
|
|
|
|
|
|
|
|
for obj := range stream {
|
|
|
|
all = append(all, obj)
|
|
|
|
}
|
|
|
|
|
|
|
|
return all
|
|
|
|
}
|