package arn import ( "errors" "fmt" "io" "os" "path/filepath" "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 AnimeID `json:"mainAnimeId" editable:"true"` ExtraAnimeIDs []AnimeID `json:"extraAnimeIds" editable:"true"` VideoEditorIDs []UserID `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 } // VideoLink returns the permalink for the video file. func (amv *AMV) VideoLink() string { return fmt.Sprintf("https://notify.moe/videos/amvs/%s", amv.File) } // TitleByUser returns the preferred title for the given user. func (amv *AMV) TitleByUser(user *User) string { return amv.Title.ByUser(user) } // SetVideoReader sets the bytes for the video file by reading them from the reader. func (amv *AMV) SetVideoReader(reader io.Reader) error { fileName := amv.ID + ".webm" file, err := os.Create(filepath.Join(Root, "videos", "amvs", fileName)) if err != nil { return err } defer file.Close() _, err = io.Copy(file, reader) if err != nil { return err } // Run mkclean // optimizedFile := filePath + ".optimized" // defer os.Remove(optimizedFile) // 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 // } // Refresh video file info // info, err := video.GetInfo(optimizedFile) // if err != nil { // return err // } // // Is our storage server available? // if Spaces == nil { // return errors.New("File storage client has not been initialized") // } // // Make sure the file is public // userMetaData := map[string]string{ // "x-amz-acl": "public-read", // } // // Upload the file to our storage server // _, err = Spaces.FPutObject(context.TODO(), "arn", , optimizedFile, minio.PutObjectOptions{ // ContentType: "video/webm", // UserMetadata: userMetaData, // }) // Refresh video file info info, err := video.GetInfo(fileName) if err != nil { return err } amv.Info = *info amv.File = fileName 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 := make([]*Anime, 0, len(amv.ExtraAnimeIDs)) 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") } // Check that the file name exists if amv.File == "" { return errors.New("You need to upload a WebM file for this AMV") } // No file uploaded _, err := os.Stat(filepath.Join(Root, "videos", "amvs", amv.File)) if err != nil { 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 ID) (*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 }