Added arn to the main repository
This commit is contained in:
parent
cf258573a8
commit
29a48d94a5
267
arn/AMV.go
Normal file
267
arn/AMV.go
Normal file
@ -0,0 +1,267 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
||||
"github.com/aerogo/nano"
|
||||
"github.com/animenotifier/notify.moe/arn/video"
|
||||
)
|
||||
|
||||
// AMV is an anime music video.
|
||||
type AMV struct {
|
||||
File string `json:"file" editable:"true" type:"upload" filetype:"video" endpoint:"/api/upload/amv/:id/file"`
|
||||
Title AMVTitle `json:"title" editable:"true"`
|
||||
MainAnimeID string `json:"mainAnimeId" editable:"true"`
|
||||
ExtraAnimeIDs []string `json:"extraAnimeIds" editable:"true"`
|
||||
VideoEditorIDs []string `json:"videoEditorIds" editable:"true"`
|
||||
Links []Link `json:"links" editable:"true"`
|
||||
Tags []string `json:"tags" editable:"true"`
|
||||
Info video.Info `json:"info"`
|
||||
|
||||
hasID
|
||||
hasPosts
|
||||
hasCreator
|
||||
hasEditor
|
||||
hasLikes
|
||||
hasDraft
|
||||
}
|
||||
|
||||
// Link returns the permalink for the AMV.
|
||||
func (amv *AMV) Link() string {
|
||||
return "/amv/" + amv.ID
|
||||
}
|
||||
|
||||
// TitleByUser returns the preferred title for the given user.
|
||||
func (amv *AMV) TitleByUser(user *User) string {
|
||||
return amv.Title.ByUser(user)
|
||||
}
|
||||
|
||||
// SetVideoBytes sets the bytes for the video file.
|
||||
func (amv *AMV) SetVideoBytes(data []byte) error {
|
||||
fileName := amv.ID + ".webm"
|
||||
filePath := path.Join(Root, "videos", "amvs", fileName)
|
||||
err := ioutil.WriteFile(filePath, data, 0644)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run mkclean
|
||||
optimizedFile := filePath + ".optimized"
|
||||
|
||||
cmd := exec.Command(
|
||||
"mkclean",
|
||||
"--doctype", "4",
|
||||
"--keep-cues",
|
||||
"--optimize",
|
||||
filePath,
|
||||
optimizedFile,
|
||||
)
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
err = cmd.Start()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now delete the original file and replace it with the optimized file
|
||||
err = os.Remove(filePath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Rename(optimizedFile, filePath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Refresh video file info
|
||||
amv.File = fileName
|
||||
return amv.RefreshInfo()
|
||||
}
|
||||
|
||||
// RefreshInfo refreshes the information about the video file.
|
||||
func (amv *AMV) RefreshInfo() error {
|
||||
if amv.File == "" {
|
||||
return fmt.Errorf("Video file has not been uploaded yet for AMV %s", amv.ID)
|
||||
}
|
||||
|
||||
info, err := video.GetInfo(path.Join(Root, "videos", "amvs", amv.File))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
amv.Info = *info
|
||||
return nil
|
||||
}
|
||||
|
||||
// MainAnime returns main anime for the AMV, if available.
|
||||
func (amv *AMV) MainAnime() *Anime {
|
||||
mainAnime, _ := GetAnime(amv.MainAnimeID)
|
||||
return mainAnime
|
||||
}
|
||||
|
||||
// ExtraAnime returns main anime for the AMV, if available.
|
||||
func (amv *AMV) ExtraAnime() []*Anime {
|
||||
objects := DB.GetMany("Anime", amv.ExtraAnimeIDs)
|
||||
animes := []*Anime{}
|
||||
|
||||
for _, obj := range objects {
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
animes = append(animes, obj.(*Anime))
|
||||
}
|
||||
|
||||
return animes
|
||||
}
|
||||
|
||||
// VideoEditors returns a slice of all the users involved in creating the AMV.
|
||||
func (amv *AMV) VideoEditors() []*User {
|
||||
objects := DB.GetMany("User", amv.VideoEditorIDs)
|
||||
editors := []*User{}
|
||||
|
||||
for _, obj := range objects {
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
editors = append(editors, obj.(*User))
|
||||
}
|
||||
|
||||
return editors
|
||||
}
|
||||
|
||||
// Publish turns the draft into a published object.
|
||||
func (amv *AMV) Publish() error {
|
||||
// No title
|
||||
if amv.Title.String() == "" {
|
||||
return errors.New("AMV doesn't have a title")
|
||||
}
|
||||
|
||||
// No anime found
|
||||
if amv.MainAnimeID == "" && len(amv.ExtraAnimeIDs) == 0 {
|
||||
return errors.New("Need to specify at least one anime")
|
||||
}
|
||||
|
||||
// No file uploaded
|
||||
if amv.File == "" {
|
||||
return errors.New("You need to upload a WebM file for this AMV")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(path.Join(Root, "videos", "amvs", amv.File)); os.IsNotExist(err) {
|
||||
return errors.New("You need to upload a WebM file for this AMV")
|
||||
}
|
||||
|
||||
return publish(amv)
|
||||
}
|
||||
|
||||
// Unpublish turns the object back into a draft.
|
||||
func (amv *AMV) Unpublish() error {
|
||||
return unpublish(amv)
|
||||
}
|
||||
|
||||
// OnLike is called when the AMV receives a like.
|
||||
func (amv *AMV) OnLike(likedBy *User) {
|
||||
if likedBy.ID == amv.CreatedBy {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
amv.Creator().SendNotification(&PushNotification{
|
||||
Title: likedBy.Nick + " liked your AMV " + amv.Title.ByUser(amv.Creator()),
|
||||
Message: likedBy.Nick + " liked your AMV " + amv.Title.ByUser(amv.Creator()) + ".",
|
||||
Icon: "https:" + likedBy.AvatarLink("large"),
|
||||
Link: "https://notify.moe" + likedBy.Link(),
|
||||
Type: NotificationTypeLike,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
// String implements the default string serialization.
|
||||
func (amv *AMV) String() string {
|
||||
return amv.Title.ByUser(nil)
|
||||
}
|
||||
|
||||
// TypeName returns the type name.
|
||||
func (amv *AMV) TypeName() string {
|
||||
return "AMV"
|
||||
}
|
||||
|
||||
// Self returns the object itself.
|
||||
func (amv *AMV) Self() Loggable {
|
||||
return amv
|
||||
}
|
||||
|
||||
// GetAMV returns the AMV with the given ID.
|
||||
func GetAMV(id string) (*AMV, error) {
|
||||
obj, err := DB.Get("AMV", id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj.(*AMV), nil
|
||||
}
|
||||
|
||||
// StreamAMVs returns a stream of all AMVs.
|
||||
func StreamAMVs() <-chan *AMV {
|
||||
channel := make(chan *AMV, nano.ChannelBufferSize)
|
||||
|
||||
go func() {
|
||||
for obj := range DB.All("AMV") {
|
||||
channel <- obj.(*AMV)
|
||||
}
|
||||
|
||||
close(channel)
|
||||
}()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// AllAMVs returns a slice of all AMVs.
|
||||
func AllAMVs() []*AMV {
|
||||
all := make([]*AMV, 0, DB.Collection("AMV").Count())
|
||||
|
||||
stream := StreamAMVs()
|
||||
|
||||
for obj := range stream {
|
||||
all = append(all, obj)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// FilterAMVs filters all AMVs by a custom function.
|
||||
func FilterAMVs(filter func(*AMV) bool) []*AMV {
|
||||
var filtered []*AMV
|
||||
|
||||
for obj := range DB.All("AMV") {
|
||||
realObject := obj.(*AMV)
|
||||
|
||||
if filter(realObject) {
|
||||
filtered = append(filtered, realObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
139
arn/AMVAPI.go
Normal file
139
arn/AMVAPI.go
Normal file
@ -0,0 +1,139 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/api"
|
||||
)
|
||||
|
||||
// Force interface implementations
|
||||
var (
|
||||
_ Publishable = (*AMV)(nil)
|
||||
_ Likeable = (*AMV)(nil)
|
||||
_ LikeEventReceiver = (*AMV)(nil)
|
||||
_ PostParent = (*AMV)(nil)
|
||||
_ fmt.Stringer = (*AMV)(nil)
|
||||
_ api.Newable = (*AMV)(nil)
|
||||
_ api.Editable = (*AMV)(nil)
|
||||
_ api.Deletable = (*AMV)(nil)
|
||||
_ api.ArrayEventListener = (*AMV)(nil)
|
||||
)
|
||||
|
||||
// Actions
|
||||
func init() {
|
||||
API.RegisterActions("AMV", []*api.Action{
|
||||
// Publish
|
||||
PublishAction(),
|
||||
|
||||
// Unpublish
|
||||
UnpublishAction(),
|
||||
|
||||
// Like
|
||||
LikeAction(),
|
||||
|
||||
// Unlike
|
||||
UnlikeAction(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create sets the data for a new AMV with data we received from the API request.
|
||||
func (amv *AMV) Create(ctx aero.Context) error {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
if user == nil {
|
||||
return errors.New("Not logged in")
|
||||
}
|
||||
|
||||
amv.ID = GenerateID("AMV")
|
||||
amv.Created = DateTimeUTC()
|
||||
amv.CreatedBy = user.ID
|
||||
|
||||
// Write log entry
|
||||
logEntry := NewEditLogEntry(user.ID, "create", "AMV", amv.ID, "", "", "")
|
||||
logEntry.Save()
|
||||
|
||||
return amv.Unpublish()
|
||||
}
|
||||
|
||||
// Edit creates an edit log entry.
|
||||
func (amv *AMV) Edit(ctx aero.Context, key string, value reflect.Value, newValue reflect.Value) (consumed bool, err error) {
|
||||
return edit(amv, ctx, key, value, newValue)
|
||||
}
|
||||
|
||||
// OnAppend saves a log entry.
|
||||
func (amv *AMV) OnAppend(ctx aero.Context, key string, index int, obj interface{}) {
|
||||
onAppend(amv, ctx, key, index, obj)
|
||||
}
|
||||
|
||||
// OnRemove saves a log entry.
|
||||
func (amv *AMV) OnRemove(ctx aero.Context, key string, index int, obj interface{}) {
|
||||
onRemove(amv, ctx, key, index, obj)
|
||||
}
|
||||
|
||||
// DeleteInContext deletes the amv in the given context.
|
||||
func (amv *AMV) DeleteInContext(ctx aero.Context) error {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
// Write log entry
|
||||
logEntry := NewEditLogEntry(user.ID, "delete", "AMV", amv.ID, "", fmt.Sprint(amv), "")
|
||||
logEntry.Save()
|
||||
|
||||
return amv.Delete()
|
||||
}
|
||||
|
||||
// Delete deletes the object from the database.
|
||||
func (amv *AMV) Delete() error {
|
||||
if amv.IsDraft {
|
||||
draftIndex := amv.Creator().DraftIndex()
|
||||
draftIndex.AMVID = ""
|
||||
draftIndex.Save()
|
||||
}
|
||||
|
||||
// Remove posts
|
||||
for _, post := range amv.Posts() {
|
||||
err := post.Delete()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove file
|
||||
if amv.File != "" {
|
||||
err := os.Remove(path.Join(Root, "videos", "amvs", amv.File))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
DB.Delete("AMV", amv.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authorize returns an error if the given API POST request is not authorized.
|
||||
func (amv *AMV) Authorize(ctx aero.Context, action string) error {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
if user == nil {
|
||||
return errors.New("Not logged in")
|
||||
}
|
||||
|
||||
if action == "delete" {
|
||||
if user.Role != "editor" && user.Role != "admin" {
|
||||
return errors.New("Insufficient permissions")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the amv object in the database.
|
||||
func (amv *AMV) Save() {
|
||||
DB.Set("AMV", amv.ID, amv)
|
||||
}
|
32
arn/AMVTitle.go
Normal file
32
arn/AMVTitle.go
Normal file
@ -0,0 +1,32 @@
|
||||
package arn
|
||||
|
||||
// AMVTitle is the same as a soundtrack title.
|
||||
type AMVTitle SoundTrackTitle
|
||||
|
||||
// String is the default representation of the title.
|
||||
func (title *AMVTitle) String() string {
|
||||
return title.ByUser(nil)
|
||||
}
|
||||
|
||||
// ByUser returns the preferred title for the given user.
|
||||
func (title *AMVTitle) ByUser(user *User) string {
|
||||
if user == nil {
|
||||
if title.Canonical != "" {
|
||||
return title.Canonical
|
||||
}
|
||||
|
||||
return title.Native
|
||||
}
|
||||
|
||||
switch user.Settings().TitleLanguage {
|
||||
case "japanese":
|
||||
if title.Native == "" {
|
||||
return title.Canonical
|
||||
}
|
||||
|
||||
return title.Native
|
||||
|
||||
default:
|
||||
return title.ByUser(nil)
|
||||
}
|
||||
}
|
121
arn/APIKeys.go
Normal file
121
arn/APIKeys.go
Normal file
@ -0,0 +1,121 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/animenotifier/anilist"
|
||||
"github.com/animenotifier/osu"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// Root is the full path to the root directory of notify.moe repository.
|
||||
var Root = os.Getenv("ARN_ROOT")
|
||||
|
||||
// APIKeys are global API keys for several services
|
||||
var APIKeys APIKeysData
|
||||
|
||||
// APIKeysData ...
|
||||
type APIKeysData struct {
|
||||
Google struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"google"`
|
||||
|
||||
Facebook struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"facebook"`
|
||||
|
||||
Twitter struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"twitter"`
|
||||
|
||||
Discord struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
Token string `json:"token"`
|
||||
} `json:"discord"`
|
||||
|
||||
SoundCloud struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"soundcloud"`
|
||||
|
||||
GoogleAPI struct {
|
||||
Key string `json:"key"`
|
||||
} `json:"googleAPI"`
|
||||
|
||||
IPInfoDB struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"ipInfoDB"`
|
||||
|
||||
AniList struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"anilist"`
|
||||
|
||||
Osu struct {
|
||||
Secret string `json:"secret"`
|
||||
} `json:"osu"`
|
||||
|
||||
PayPal struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"paypal"`
|
||||
|
||||
VAPID struct {
|
||||
Subject string `json:"subject"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
} `json:"vapid"`
|
||||
|
||||
SMTP struct {
|
||||
Server string `json:"server"`
|
||||
Address string `json:"address"`
|
||||
Password string `json:"password"`
|
||||
} `json:"smtp"`
|
||||
|
||||
S3 struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
} `json:"s3"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Path for API keys
|
||||
apiKeysPath := path.Join(Root, "security/api-keys.json")
|
||||
|
||||
// If the API keys file is not available, create a symlink to the default API keys
|
||||
if _, err := os.Stat(apiKeysPath); os.IsNotExist(err) {
|
||||
defaultAPIKeysPath := path.Join(Root, "security/default/api-keys.json")
|
||||
err := os.Link(defaultAPIKeysPath, apiKeysPath)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load API keys
|
||||
data, err := ioutil.ReadFile(apiKeysPath)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
err = jsoniter.Unmarshal(data, &APIKeys)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set Osu API key
|
||||
osu.APIKey = APIKeys.Osu.Secret
|
||||
|
||||
// Set Anilist API keys
|
||||
anilist.APIKeyID = APIKeys.AniList.ID
|
||||
anilist.APIKeySecret = APIKeys.AniList.Secret
|
||||
}
|
81
arn/Activity.go
Normal file
81
arn/Activity.go
Normal file
@ -0,0 +1,81 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aerogo/nano"
|
||||
)
|
||||
|
||||
// Activity is a user activity that appears in the follower's feeds.
|
||||
type Activity interface {
|
||||
Creator() *User
|
||||
TypeName() string
|
||||
GetID() string
|
||||
GetCreated() string
|
||||
GetCreatedBy() UserID
|
||||
GetCreatedTime() time.Time
|
||||
}
|
||||
|
||||
// SortActivitiesLatestFirst puts the latest entries on top.
|
||||
func SortActivitiesLatestFirst(entries []Activity) {
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].GetCreated() > entries[j].GetCreated()
|
||||
})
|
||||
}
|
||||
|
||||
// StreamActivities returns a stream of all activities.
|
||||
func StreamActivities() <-chan Activity {
|
||||
channel := make(chan Activity, nano.ChannelBufferSize)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
for obj := range DB.All("ActivityCreate") {
|
||||
channel <- obj.(Activity)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for obj := range DB.All("ActivityConsumeAnime") {
|
||||
channel <- obj.(Activity)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(channel)
|
||||
}()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// AllActivities returns a slice of all activities.
|
||||
func AllActivities() []Activity {
|
||||
all := make([]Activity, 0, DB.Collection("ActivityCreate").Count()+DB.Collection("ActivityConsumeAnime").Count())
|
||||
stream := StreamActivities()
|
||||
|
||||
for obj := range stream {
|
||||
all = append(all, obj)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// FilterActivities filters all Activities by a custom function.
|
||||
func FilterActivities(filter func(Activity) bool) []Activity {
|
||||
var filtered []Activity
|
||||
|
||||
for obj := range StreamActivities() {
|
||||
if filter(obj) {
|
||||
filtered = append(filtered, obj)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
97
arn/ActivityConsumeAnime.go
Normal file
97
arn/ActivityConsumeAnime.go
Normal file
@ -0,0 +1,97 @@
|
||||
package arn
|
||||
|
||||
import "sort"
|
||||
|
||||
// ActivityConsumeAnime is a user activity that consumes anime.
|
||||
type ActivityConsumeAnime struct {
|
||||
AnimeID string `json:"animeId"`
|
||||
FromEpisode int `json:"fromEpisode"`
|
||||
ToEpisode int `json:"toEpisode"`
|
||||
|
||||
hasID
|
||||
hasCreator
|
||||
hasLikes
|
||||
}
|
||||
|
||||
// NewActivityConsumeAnime creates a new activity.
|
||||
func NewActivityConsumeAnime(animeID string, fromEpisode int, toEpisode int, userID UserID) *ActivityConsumeAnime {
|
||||
return &ActivityConsumeAnime{
|
||||
hasID: hasID{
|
||||
ID: GenerateID("ActivityConsumeAnime"),
|
||||
},
|
||||
hasCreator: hasCreator{
|
||||
Created: DateTimeUTC(),
|
||||
CreatedBy: userID,
|
||||
},
|
||||
AnimeID: animeID,
|
||||
FromEpisode: fromEpisode,
|
||||
ToEpisode: toEpisode,
|
||||
}
|
||||
}
|
||||
|
||||
// Anime returns the anime.
|
||||
func (activity *ActivityConsumeAnime) Anime() *Anime {
|
||||
anime, _ := GetAnime(activity.AnimeID)
|
||||
return anime
|
||||
}
|
||||
|
||||
// TypeName returns the type name.
|
||||
func (activity *ActivityConsumeAnime) TypeName() string {
|
||||
return "ActivityConsumeAnime"
|
||||
}
|
||||
|
||||
// Self returns the object itself.
|
||||
func (activity *ActivityConsumeAnime) Self() Loggable {
|
||||
return activity
|
||||
}
|
||||
|
||||
// LastActivityConsumeAnime returns the last activity for the given anime.
|
||||
func (user *User) LastActivityConsumeAnime(animeID string) *ActivityConsumeAnime {
|
||||
activities := FilterActivitiesConsumeAnime(func(activity *ActivityConsumeAnime) bool {
|
||||
return activity.AnimeID == animeID && activity.CreatedBy == user.ID
|
||||
})
|
||||
|
||||
if len(activities) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(activities, func(i, j int) bool {
|
||||
return activities[i].Created > activities[j].Created
|
||||
})
|
||||
|
||||
return activities[0]
|
||||
}
|
||||
|
||||
// FilterActivitiesConsumeAnime filters all anime consumption activities by a custom function.
|
||||
func FilterActivitiesConsumeAnime(filter func(*ActivityConsumeAnime) bool) []*ActivityConsumeAnime {
|
||||
var filtered []*ActivityConsumeAnime
|
||||
|
||||
for obj := range DB.All("ActivityConsumeAnime") {
|
||||
realObject := obj.(*ActivityConsumeAnime)
|
||||
|
||||
if filter(realObject) {
|
||||
filtered = append(filtered, realObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// // OnLike is called when the activity receives a like.
|
||||
// func (activity *Activity) OnLike(likedBy *User) {
|
||||
// if likedBy.ID == activity.CreatedBy {
|
||||
// return
|
||||
// }
|
||||
|
||||
// go func() {
|
||||
// notifyUser := activity.Creator()
|
||||
|
||||
// notifyUser.SendNotification(&PushNotification{
|
||||
// Title: likedBy.Nick + " liked your activity",
|
||||
// Message: activity.TextByUser(notifyUser),
|
||||
// Icon: "https:" + likedBy.AvatarLink("large"),
|
||||
// Link: "https://notify.moe" + activity.Link(),
|
||||
// Type: NotificationTypeLike,
|
||||
// })
|
||||
// }()
|
||||
// }
|
80
arn/ActivityConsumeAnimeAPI.go
Normal file
80
arn/ActivityConsumeAnimeAPI.go
Normal file
@ -0,0 +1,80 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/api"
|
||||
)
|
||||
|
||||
// Force interface implementations
|
||||
var (
|
||||
_ Activity = (*ActivityConsumeAnime)(nil)
|
||||
_ api.Deletable = (*ActivityConsumeAnime)(nil)
|
||||
_ api.Savable = (*ActivityConsumeAnime)(nil)
|
||||
)
|
||||
|
||||
// Authorize returns an error if the given API POST request is not authorized.
|
||||
func (activity *ActivityConsumeAnime) Authorize(ctx aero.Context, action string) error {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
if user == nil {
|
||||
return errors.New("Not logged in")
|
||||
}
|
||||
|
||||
if user.ID != activity.CreatedBy {
|
||||
return errors.New("Can't modify activities from other users")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the activity object in the database.
|
||||
func (activity *ActivityConsumeAnime) Save() {
|
||||
DB.Set("ActivityConsumeAnime", activity.ID, activity)
|
||||
}
|
||||
|
||||
// DeleteInContext deletes the activity in the given context.
|
||||
func (activity *ActivityConsumeAnime) DeleteInContext(ctx aero.Context) error {
|
||||
return activity.Delete()
|
||||
}
|
||||
|
||||
// Delete deletes the object from the database.
|
||||
func (activity *ActivityConsumeAnime) Delete() error {
|
||||
DB.Delete("ActivityConsumeAnime", activity.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// // Force interface implementations
|
||||
// var (
|
||||
// _ Likeable = (*Activity)(nil)
|
||||
// _ LikeEventReceiver = (*Activity)(nil)
|
||||
// _ api.Deletable = (*Activity)(nil)
|
||||
// )
|
||||
|
||||
// // Actions
|
||||
// func init() {
|
||||
// API.RegisterActions("Activity", []*api.Action{
|
||||
// // Like
|
||||
// LikeAction(),
|
||||
|
||||
// // Unlike
|
||||
// UnlikeAction(),
|
||||
// })
|
||||
// }
|
||||
|
||||
// // Authorize returns an error if the given API request is not authorized.
|
||||
// func (activity *Activity) Authorize(ctx aero.Context, action string) error {
|
||||
// user := GetUserFromContext(ctx)
|
||||
|
||||
// if user == nil {
|
||||
// return errors.New("Not logged in")
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// // DeleteInContext deletes the activity in the given context.
|
||||
// func (activity *Activity) DeleteInContext(ctx aero.Context) error {
|
||||
// return activity.Delete()
|
||||
// }
|
63
arn/ActivityCreate.go
Normal file
63
arn/ActivityCreate.go
Normal file
@ -0,0 +1,63 @@
|
||||
package arn
|
||||
|
||||
import "github.com/aerogo/nano"
|
||||
|
||||
// ActivityCreate is a user activity that creates something.
|
||||
type ActivityCreate struct {
|
||||
ObjectType string `json:"objectType"`
|
||||
ObjectID string `json:"objectId"`
|
||||
|
||||
hasID
|
||||
hasCreator
|
||||
}
|
||||
|
||||
// NewActivityCreate creates a new activity.
|
||||
func NewActivityCreate(objectType string, objectID string, userID UserID) *ActivityCreate {
|
||||
return &ActivityCreate{
|
||||
hasID: hasID{
|
||||
ID: GenerateID("ActivityCreate"),
|
||||
},
|
||||
hasCreator: hasCreator{
|
||||
Created: DateTimeUTC(),
|
||||
CreatedBy: userID,
|
||||
},
|
||||
ObjectType: objectType,
|
||||
ObjectID: objectID,
|
||||
}
|
||||
}
|
||||
|
||||
// Object returns the object that was created.
|
||||
func (activity *ActivityCreate) Object() Likeable {
|
||||
obj, _ := DB.Get(activity.ObjectType, activity.ObjectID)
|
||||
return obj.(Likeable)
|
||||
}
|
||||
|
||||
// Postable casts the object to the Postable interface.
|
||||
func (activity *ActivityCreate) Postable() Postable {
|
||||
return activity.Object().(Postable)
|
||||
}
|
||||
|
||||
// TypeName returns the type name.
|
||||
func (activity *ActivityCreate) TypeName() string {
|
||||
return "ActivityCreate"
|
||||
}
|
||||
|
||||
// Self returns the object itself.
|
||||
func (activity *ActivityCreate) Self() Loggable {
|
||||
return activity
|
||||
}
|
||||
|
||||
// StreamActivityCreates returns a stream of all ActivityCreate objects.
|
||||
func StreamActivityCreates() <-chan *ActivityCreate {
|
||||
channel := make(chan *ActivityCreate, nano.ChannelBufferSize)
|
||||
|
||||
go func() {
|
||||
for obj := range DB.All("ActivityCreate") {
|
||||
channel <- obj.(*ActivityCreate)
|
||||
}
|
||||
|
||||
close(channel)
|
||||
}()
|
||||
|
||||
return channel
|
||||
}
|
22
arn/ActivityCreateAPI.go
Normal file
22
arn/ActivityCreateAPI.go
Normal file
@ -0,0 +1,22 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"github.com/aerogo/api"
|
||||
)
|
||||
|
||||
// Force interface implementations
|
||||
var (
|
||||
_ Activity = (*ActivityCreate)(nil)
|
||||
_ api.Savable = (*ActivityCreate)(nil)
|
||||
)
|
||||
|
||||
// Save saves the activity object in the database.
|
||||
func (activity *ActivityCreate) Save() {
|
||||
DB.Set("ActivityCreate", activity.ID, activity)
|
||||
}
|
||||
|
||||
// Delete deletes the activity object from the database.
|
||||
func (activity *ActivityCreate) Delete() error {
|
||||
DB.Delete("ActivityCreate", activity.ID)
|
||||
return nil
|
||||
}
|
43
arn/AiringDate.go
Normal file
43
arn/AiringDate.go
Normal file
@ -0,0 +1,43 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// AiringDate represents the airing date of an anime.
|
||||
type AiringDate struct {
|
||||
Start string `json:"start" editable:"true"`
|
||||
End string `json:"end" editable:"true"`
|
||||
}
|
||||
|
||||
// StartDateHuman returns the start date of the anime in human readable form.
|
||||
func (airing *AiringDate) StartDateHuman() string {
|
||||
t, _ := time.Parse(time.RFC3339, airing.Start)
|
||||
humanReadable := t.Format(time.RFC1123)
|
||||
|
||||
return humanReadable[:len("Thu, 25 May 2017")]
|
||||
}
|
||||
|
||||
// EndDateHuman returns the end date of the anime in human readable form.
|
||||
func (airing *AiringDate) EndDateHuman() string {
|
||||
t, _ := time.Parse(time.RFC3339, airing.End)
|
||||
humanReadable := t.Format(time.RFC1123)
|
||||
|
||||
return humanReadable[:len("Thu, 25 May 2017")]
|
||||
}
|
||||
|
||||
// StartTimeHuman returns the start time of the anime in human readable form.
|
||||
func (airing *AiringDate) StartTimeHuman() string {
|
||||
t, _ := time.Parse(time.RFC3339, airing.Start)
|
||||
humanReadable := t.Format(time.RFC1123)
|
||||
|
||||
return humanReadable[len("Thu, 25 May 2017 "):]
|
||||
}
|
||||
|
||||
// EndTimeHuman returns the end time of the anime in human readable form.
|
||||
func (airing *AiringDate) EndTimeHuman() string {
|
||||
t, _ := time.Parse(time.RFC3339, airing.End)
|
||||
humanReadable := t.Format(time.RFC1123)
|
||||
|
||||
return humanReadable[len("Thu, 25 May 2017 "):]
|
||||
}
|
78
arn/Analytics.go
Normal file
78
arn/Analytics.go
Normal file
@ -0,0 +1,78 @@
|
||||
package arn
|
||||
|
||||
import "github.com/aerogo/nano"
|
||||
|
||||
// Analytics stores user-related statistics.
|
||||
type Analytics struct {
|
||||
UserID string `json:"userId"`
|
||||
General GeneralAnalytics `json:"general"`
|
||||
Screen ScreenAnalytics `json:"screen"`
|
||||
System SystemAnalytics `json:"system"`
|
||||
Connection ConnectionAnalytics `json:"connection"`
|
||||
}
|
||||
|
||||
// GeneralAnalytics stores general information.
|
||||
type GeneralAnalytics struct {
|
||||
TimezoneOffset int `json:"timezoneOffset"`
|
||||
}
|
||||
|
||||
// ScreenAnalytics stores information about the device screen.
|
||||
type ScreenAnalytics struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
AvailableWidth int `json:"availableWidth"`
|
||||
AvailableHeight int `json:"availableHeight"`
|
||||
PixelRatio float64 `json:"pixelRatio"`
|
||||
}
|
||||
|
||||
// SystemAnalytics stores information about the CPU and OS.
|
||||
type SystemAnalytics struct {
|
||||
CPUCount int `json:"cpuCount"`
|
||||
Platform string `json:"platform"`
|
||||
}
|
||||
|
||||
// ConnectionAnalytics stores information about connection speed and ping.
|
||||
type ConnectionAnalytics struct {
|
||||
DownLink float64 `json:"downLink"`
|
||||
RoundTripTime float64 `json:"roundTripTime"`
|
||||
EffectiveType string `json:"effectiveType"`
|
||||
}
|
||||
|
||||
// GetAnalytics returns the analytics for the given user ID.
|
||||
func GetAnalytics(userID UserID) (*Analytics, error) {
|
||||
obj, err := DB.Get("Analytics", userID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj.(*Analytics), nil
|
||||
}
|
||||
|
||||
// StreamAnalytics returns a stream of all analytics.
|
||||
func StreamAnalytics() <-chan *Analytics {
|
||||
channel := make(chan *Analytics, nano.ChannelBufferSize)
|
||||
|
||||
go func() {
|
||||
for obj := range DB.All("Analytics") {
|
||||
channel <- obj.(*Analytics)
|
||||
}
|
||||
|
||||
close(channel)
|
||||
}()
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// AllAnalytics returns a slice of all analytics.
|
||||
func AllAnalytics() []*Analytics {
|
||||
all := make([]*Analytics, 0, DB.Collection("Analytics").Count())
|
||||
|
||||
stream := StreamAnalytics()
|
||||
|
||||
for obj := range stream {
|
||||
all = append(all, obj)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
41
arn/AnalyticsAPI.go
Normal file
41
arn/AnalyticsAPI.go
Normal file
@ -0,0 +1,41 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/api"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// Force interface implementations
|
||||
var (
|
||||
_ api.Newable = (*Analytics)(nil)
|
||||
)
|
||||
|
||||
// Authorize returns an error if the given API POST request is not authorized.
|
||||
func (analytics *Analytics) Authorize(ctx aero.Context, action string) error {
|
||||
return AuthorizeIfLoggedIn(ctx)
|
||||
}
|
||||
|
||||
// Create creates a new analytics object.
|
||||
func (analytics *Analytics) Create(ctx aero.Context) error {
|
||||
body, err := ctx.Request().Body().Bytes()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = jsoniter.Unmarshal(body, analytics)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
analytics.UserID = GetUserFromContext(ctx).ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the analytics in the database.
|
||||
func (analytics *Analytics) Save() {
|
||||
DB.Set("Analytics", analytics.UserID, analytics)
|
||||
}
|
95
arn/AniList.go
Normal file
95
arn/AniList.go
Normal file
@ -0,0 +1,95 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/animenotifier/anilist"
|
||||
)
|
||||
|
||||
// AniListAnimeFinder holds an internal map of ID to anime mappings
|
||||
// and is therefore very efficient to use when trying to find
|
||||
// anime by a given service and ID.
|
||||
type AniListAnimeFinder struct {
|
||||
idToAnime map[string]*Anime
|
||||
malIDToAnime map[string]*Anime
|
||||
}
|
||||
|
||||
// NewAniListAnimeFinder creates a new finder for Anilist anime.
|
||||
func NewAniListAnimeFinder() *AniListAnimeFinder {
|
||||
finder := &AniListAnimeFinder{
|
||||
idToAnime: map[string]*Anime{},
|
||||
malIDToAnime: map[string]*Anime{},
|
||||
}
|
||||
|
||||
for anime := range StreamAnime() {
|
||||
id := anime.GetMapping("anilist/anime")
|
||||
|
||||
if id != "" {
|
||||
finder.idToAnime[id] = anime
|
||||
}
|
||||
|
||||
malID := anime.GetMapping("myanimelist/anime")
|
||||
|
||||
if malID != "" {
|
||||
finder.malIDToAnime[malID] = anime
|
||||
}
|
||||
}
|
||||
|
||||
return finder
|
||||
}
|
||||
|
||||
// GetAnime tries to find an AniList anime in our anime database.
|
||||
func (finder *AniListAnimeFinder) GetAnime(id string, malID string) *Anime {
|
||||
animeByID, existsByID := finder.idToAnime[id]
|
||||
animeByMALID, existsByMALID := finder.malIDToAnime[malID]
|
||||
|
||||
// Add anilist mapping to the MAL mapped anime if it's missing
|
||||
if existsByMALID && animeByMALID.GetMapping("anilist/anime") != id {
|
||||
animeByMALID.SetMapping("anilist/anime", id)
|
||||
animeByMALID.Save()
|
||||
|
||||
finder.idToAnime[id] = animeByMALID
|
||||
}
|
||||
|
||||
// If both MAL ID and AniList ID are matched, but the matched anime are different,
|
||||
// while the MAL IDs are different as well,
|
||||
// then we're trusting the MAL ID matching more and deleting the incorrect mapping.
|
||||
if existsByID && existsByMALID && animeByID.ID != animeByMALID.ID && animeByID.GetMapping("myanimelist/anime") != animeByMALID.GetMapping("myanimelist/anime") {
|
||||
animeByID.RemoveMapping("anilist/anime")
|
||||
animeByID.Save()
|
||||
|
||||
delete(finder.idToAnime, id)
|
||||
|
||||
fmt.Println("MAL / Anilist mismatch:")
|
||||
fmt.Println(animeByID.ID, animeByID)
|
||||
fmt.Println(animeByMALID.ID, animeByMALID)
|
||||
}
|
||||
|
||||
if existsByID {
|
||||
return animeByID
|
||||
}
|
||||
|
||||
if existsByMALID {
|
||||
return animeByMALID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AniListAnimeListStatus returns the ARN version of the anime status.
|
||||
func AniListAnimeListStatus(item *anilist.AnimeListItem) string {
|
||||
switch item.Status {
|
||||
case "CURRENT", "REPEATING":
|
||||
return AnimeListStatusWatching
|
||||
case "COMPLETED":
|
||||
return AnimeListStatusCompleted
|
||||
case "PLANNING":
|
||||
return AnimeListStatusPlanned
|
||||
case "PAUSED":
|
||||
return AnimeListStatusHold
|
||||
case "DROPPED":
|
||||
return AnimeListStatusDropped
|
||||
default:
|
||||
return AnimeListStatusPlanned
|
||||
}
|
||||
}
|
19
arn/AniListMatch.go
Normal file
19
arn/AniListMatch.go
Normal file
@ -0,0 +1,19 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"github.com/animenotifier/anilist"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// AniListMatch ...
|
||||
type AniListMatch struct {
|
||||
AniListItem *anilist.AnimeListItem `json:"anilistItem"`
|
||||
ARNAnime *Anime `json:"arnAnime"`
|
||||
}
|
||||
|
||||
// JSON ...
|
||||
func (match *AniListMatch) JSON() string {
|
||||
b, err := jsoniter.Marshal(match)
|
||||
PanicOnError(err)
|
||||
return string(b)
|
||||
}
|
1015
arn/Anime.go
Normal file
1015
arn/Anime.go
Normal file
File diff suppressed because it is too large
Load Diff
210
arn/AnimeAPI.go
Normal file
210
arn/AnimeAPI.go
Normal file
@ -0,0 +1,210 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/api"
|
||||
"github.com/akyoto/color"
|
||||
)
|
||||
|
||||
// Force interface implementations
|
||||
var (
|
||||
_ fmt.Stringer = (*Anime)(nil)
|
||||
_ Likeable = (*Anime)(nil)
|
||||
_ PostParent = (*Anime)(nil)
|
||||
_ api.Deletable = (*Anime)(nil)
|
||||
_ api.Editable = (*Anime)(nil)
|
||||
_ api.CustomEditable = (*Anime)(nil)
|
||||
_ api.ArrayEventListener = (*Anime)(nil)
|
||||
)
|
||||
|
||||
// Actions
|
||||
func init() {
|
||||
API.RegisterActions("Anime", []*api.Action{
|
||||
// Like anime
|
||||
LikeAction(),
|
||||
|
||||
// Unlike anime
|
||||
UnlikeAction(),
|
||||
})
|
||||
}
|
||||
|
||||
// Edit creates an edit log entry.
|
||||
func (anime *Anime) Edit(ctx aero.Context, key string, value reflect.Value, newValue reflect.Value) (consumed bool, err error) {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
if key == "Status" {
|
||||
oldStatus := value.String()
|
||||
newStatus := newValue.String()
|
||||
|
||||
// Notify people who want to know about finished series
|
||||
if oldStatus == "current" && newStatus == "finished" {
|
||||
go func() {
|
||||
for _, user := range anime.UsersWatchingOrPlanned() {
|
||||
if !user.Settings().Notification.AnimeFinished {
|
||||
continue
|
||||
}
|
||||
|
||||
user.SendNotification(&PushNotification{
|
||||
Title: anime.Title.ByUser(user),
|
||||
Message: anime.Title.ByUser(user) + " has finished airing!",
|
||||
Icon: anime.ImageLink("medium"),
|
||||
Link: "https://notify.moe" + anime.Link(),
|
||||
Type: NotificationTypeAnimeFinished,
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Write log entry
|
||||
logEntry := NewEditLogEntry(user.ID, "edit", "Anime", anime.ID, key, fmt.Sprint(value.Interface()), fmt.Sprint(newValue.Interface()))
|
||||
logEntry.Save()
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// OnAppend saves a log entry.
|
||||
func (anime *Anime) OnAppend(ctx aero.Context, key string, index int, obj interface{}) {
|
||||
onAppend(anime, ctx, key, index, obj)
|
||||
}
|
||||
|
||||
// OnRemove saves a log entry.
|
||||
func (anime *Anime) OnRemove(ctx aero.Context, key string, index int, obj interface{}) {
|
||||
onRemove(anime, ctx, key, index, obj)
|
||||
}
|
||||
|
||||
// Authorize returns an error if the given API POST request is not authorized.
|
||||
func (anime *Anime) Authorize(ctx aero.Context, action string) error {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
if user == nil || (user.Role != "editor" && user.Role != "admin") {
|
||||
return errors.New("Not logged in or not authorized to edit this anime")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInContext deletes the anime in the given context.
|
||||
func (anime *Anime) DeleteInContext(ctx aero.Context) error {
|
||||
user := GetUserFromContext(ctx)
|
||||
|
||||
// Write log entry
|
||||
logEntry := NewEditLogEntry(user.ID, "delete", "Anime", anime.ID, "", fmt.Sprint(anime), "")
|
||||
logEntry.Save()
|
||||
|
||||
return anime.Delete()
|
||||
}
|
||||
|
||||
// Delete deletes the anime from the database.
|
||||
func (anime *Anime) Delete() error {
|
||||
// Delete anime characters
|
||||
DB.Delete("AnimeCharacters", anime.ID)
|
||||
|
||||
// Delete anime relations
|
||||
DB.Delete("AnimeRelations", anime.ID)
|
||||
|
||||
// Delete anime episodes
|
||||
DB.Delete("AnimeEpisodes", anime.ID)
|
||||
|
||||
// Delete anime list items
|
||||
for animeList := range StreamAnimeLists() {
|
||||
removed := animeList.Remove(anime.ID)
|
||||
|
||||
if removed {
|
||||
animeList.Save()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete anime ID from existing relations
|
||||
for relations := range StreamAnimeRelations() {
|
||||
removed := relations.Remove(anime.ID)
|
||||
|
||||
if removed {
|
||||
relations.Save()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete anime ID from quotes
|
||||
for quote := range StreamQuotes() {
|
||||
if quote.AnimeID == anime.ID {
|
||||
quote.AnimeID = ""
|
||||
quote.Save()
|
||||
}
|
||||
}
|
||||
|
||||
// Remove posts
|
||||
for _, post := range anime.Posts() {
|
||||
err := post.Delete()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete soundtrack tags
|
||||
for track := range StreamSoundTracks() {
|
||||
newTags := []string{}
|
||||
|
||||
for _, tag := range track.Tags {
|
||||
if strings.HasPrefix(tag, "anime:") {
|
||||
parts := strings.Split(tag, ":")
|
||||
id := parts[1]
|
||||
|
||||
if id == anime.ID {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
newTags = append(newTags, tag)
|
||||
}
|
||||
|
||||
if len(track.Tags) != len(newTags) {
|
||||
track.Tags = newTags
|
||||
track.Save()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete images on file system
|
||||
if anime.HasImage() {
|
||||
err := os.Remove(path.Join(Root, "images/anime/original/", anime.ID+anime.Image.Extension))
|
||||
|
||||
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/anime/large/", anime.ID+".jpg"))
|
||||
os.Remove(path.Join(Root, "images/anime/large/", anime.ID+"@2.jpg"))
|
||||
os.Remove(path.Join(Root, "images/anime/large/", anime.ID+".webp"))
|
||||
os.Remove(path.Join(Root, "images/anime/large/", anime.ID+"@2.webp"))
|
||||
|
||||
os.Remove(path.Join(Root, "images/anime/medium/", anime.ID+".jpg"))
|
||||
os.Remove(path.Join(Root, "images/anime/medium/", anime.ID+"@2.jpg"))
|
||||
os.Remove(path.Join(Root, "images/anime/medium/", anime.ID+".webp"))
|
||||
os.Remove(path.Join(Root, "images/anime/medium/", anime.ID+"@2.webp"))
|
||||
|
||||
os.Remove(path.Join(Root, "images/anime/small/", anime.ID+".jpg"))
|
||||
os.Remove(path.Join(Root, "images/anime/small/", anime.ID+"@2.jpg"))
|
||||
os.Remove(path.Join(Root, "images/anime/small/", anime.ID+".webp"))
|
||||
os.Remove(path.Join(Root, "images/anime/small/", anime.ID+"@2.webp"))
|
||||
}
|
||||
|
||||
// Delete the actual anime
|
||||
DB.Delete("Anime", anime.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the anime in the database.
|
||||
func (anime *Anime) Save() {
|
||||
DB.Set("Anime", anime.ID, anime)
|
||||
}
|
21
arn/AnimeCharacter.go
Normal file
21
arn/AnimeCharacter.go
Normal file
@ -0,0 +1,21 @@
|
||||
package arn
|
||||
|
||||
// Register a list of supported character roles.
|
||||
func init() {
|
||||
DataLists["anime-character-roles"] = []*Option{
|
||||
{"main", "Main character"},
|
||||
{"supporting", "Supporting character"},
|
||||
}
|
||||
}
|
||||
|
||||
// AnimeCharacter ...
|
||||
type AnimeCharacter struct {
|
||||
CharacterID string `json:"characterId" editable:"true"`
|
||||
Role string `json:"role" editable:"true" datalist:"anime-character-roles"`
|
||||
}
|
||||
|
||||
// Character ...
|
||||
func (char *AnimeCharacter) Character() *Character {
|
||||
character, _ := GetCharacter(char.CharacterID)
|
||||
return character
|
||||
}
|
17
arn/AnimeCharacterAPI.go
Normal file
17
arn/AnimeCharacterAPI.go
Normal file
@ -0,0 +1,17 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"github.com/aerogo/aero"
|
||||
"github.com/aerogo/api"
|
||||
)
|
||||
|
||||
// Force interface implementations
|
||||
var (
|
||||
_ api.Creatable = (*AnimeCharacter)(nil)
|
||||
)
|
||||
|
||||
// Create sets the data for new anime characters.
|
||||
func (character *AnimeCharacter) Create(ctx aero.Context) error {
|
||||
character.Role = "supporting"
|
||||
return nil
|
||||
}
|
134
arn/AnimeCharacters.go
Normal file
134
arn/AnimeCharacters.go
Normal file
@ -0,0 +1,134 @@
|
||||
package arn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/aerogo/nano"
|
||||
"github.com/akyoto/color"
|
||||
)
|
||||
|
||||
// AnimeCharacters is a list of characters for an anime.
|
||||
type AnimeCharacters struct {
|
||||
AnimeID string `json:"animeId" mainID:"true"`
|
||||
Items []*AnimeCharacter `json:"items" editable:"true"`
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// Anime returns the anime the characters refer to.
|
||||
func (characters *AnimeCharacters) Anime() *Anime {
|
||||
anime, _ := GetAnime(characters.AnimeID)
|
||||
return anime
|
||||
}
|
||||
|
||||
// Add adds an anime character to the list.
|
||||
func (characters *AnimeCharacters) Add(animeCharacter *AnimeCharacter) error {
|
||||
if animeCharacter.CharacterID == "" || animeCharacter.Role == "" {
|
||||
return errors.New("Empty ID or role")
|
||||
}
|
||||
|
||||
characters.Lock()
|
||||
characters.Items = append(characters.Items, animeCharacter)
|
||||
characters.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByMapping finds an anime character by the given mapping.
|
||||
func (characters *AnimeCharacters) FindByMapping(service string, serviceID string) *AnimeCharacter {
|
||||
characters.Lock()
|
||||
defer characters.Unlock()
|
||||
|
||||
for _, animeCharacter := range characters.Items {
|
||||
character := animeCharacter.Character()
|
||||
|
||||
if character == nil {
|
||||
color.Red("Anime %s has an incorrect Character ID inside AnimeCharacter: %s", characters.AnimeID, animeCharacter.CharacterID)
|
||||
continue
|
||||
}
|
||||
|
||||
if character.GetMapping(service) == serviceID {
|
||||
return animeCharacter
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Link returns the link for that object.
|
||||
func (characters *AnimeCharacters) Link() string {
|
||||
return "/anime/" + characters.AnimeID + "/characters"
|
||||
}
|
||||
|
||||
// String implements the default string serialization.
|
||||
func (characters *AnimeCharacters) String() string {
|
||||
return characters.Anime().String()
|
||||
}
|
||||
|
||||
// GetID returns the anime ID.
|
||||
func (characters *AnimeCharacters) GetID() string {
|
||||
return characters.AnimeID
|
||||
}
|
||||
|
||||
// TypeName returns the type name.
|
||||
func (characters *AnimeCharacters) TypeName() string {
|
||||
return "AnimeCharacters"
|
||||
}
|
||||
|
||||
// Self returns the object itself.
|
||||
func (characters *AnimeCharacters) Self() Loggable {
|
||||
return characters
|
||||
}
|
||||
|
||||
// Contains tells you whether the given character ID exists.
|
||||
func (characters *AnimeCharacters) Contains(characterID string) bool {
|
||||
characters.Lock()
|
||||