263 lines
5.8 KiB
Go

package arn
import (
"errors"
"fmt"
"sort"
"github.com/aerogo/nano"
)
// Character represents an anime or manga character.
type Character struct {
Name CharacterName `json:"name" editable:"true"`
Image CharacterImage `json:"image"`
MainQuoteID string `json:"mainQuoteId" editable:"true"`
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 ...
func GetCharacter(id string) (*Character, error) {
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
}