Use a CDN for faster AMV delivery

This commit is contained in:
Eduard Urbach 2019-08-28 10:07:50 +09:00
parent 1b0bb6fdbd
commit bc4e67f718
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
9 changed files with 91 additions and 90 deletions

View File

@ -3,6 +3,7 @@ package arn
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
@ -10,6 +11,7 @@ import (
"github.com/aerogo/nano" "github.com/aerogo/nano"
"github.com/animenotifier/notify.moe/arn/video" "github.com/animenotifier/notify.moe/arn/video"
"github.com/minio/minio-go/v6"
) )
// AMV is an anime music video. // AMV is an anime music video.
@ -36,16 +38,37 @@ func (amv *AMV) Link() string {
return "/amv/" + amv.ID return "/amv/" + amv.ID
} }
// VideoLink returns the permalink for the video file.
func (amv *AMV) VideoLink() string {
domain := "arn.sfo2.cdn"
if amv.IsDraft {
domain = "arn.sfo2"
}
return fmt.Sprintf("https://%s.digitaloceanspaces.com/videos/amvs/%s", domain, amv.File)
}
// TitleByUser returns the preferred title for the given user. // TitleByUser returns the preferred title for the given user.
func (amv *AMV) TitleByUser(user *User) string { func (amv *AMV) TitleByUser(user *User) string {
return amv.Title.ByUser(user) return amv.Title.ByUser(user)
} }
// SetVideoBytes sets the bytes for the video file. // SetVideoReader sets the bytes for the video file by reading them from the reader.
func (amv *AMV) SetVideoBytes(data []byte) error { func (amv *AMV) SetVideoReader(reader io.Reader) error {
fileName := amv.ID + ".webm" fileName := amv.ID + ".webm"
filePath := path.Join(Root, "videos", "amvs", fileName) pattern := amv.ID + ".*.webm"
err := ioutil.WriteFile(filePath, data, 0644) file, err := ioutil.TempFile("", pattern)
if err != nil {
return err
}
filePath := file.Name()
defer os.Remove(filePath)
// Write file contents
_, err = io.Copy(file, reader)
if err != nil { if err != nil {
return err return err
@ -53,6 +76,7 @@ func (amv *AMV) SetVideoBytes(data []byte) error {
// Run mkclean // Run mkclean
optimizedFile := filePath + ".optimized" optimizedFile := filePath + ".optimized"
defer os.Remove(optimizedFile)
cmd := exec.Command( cmd := exec.Command(
"mkclean", "mkclean",
@ -79,37 +103,35 @@ func (amv *AMV) SetVideoBytes(data []byte) error {
return err 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 // Refresh video file info
amv.File = fileName info, err := video.GetInfo(optimizedFile)
return amv.RefreshInfo()
}
// RefreshInfo refreshes the information about the video file. if err != nil {
func (amv *AMV) RefreshInfo() error { return err
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)) // 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("arn", fmt.Sprintf("videos/amvs/%s.webm", amv.ID), optimizedFile, minio.PutObjectOptions{
ContentType: "video/webm",
UserMetadata: userMetaData,
})
if err != nil { if err != nil {
return err return err
} }
amv.Info = *info amv.Info = *info
amv.File = fileName
return nil return nil
} }

View File

@ -3,8 +3,6 @@ package arn
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"path"
"reflect" "reflect"
"github.com/aerogo/aero" "github.com/aerogo/aero"
@ -104,8 +102,8 @@ func (amv *AMV) Delete() error {
} }
// Remove file // Remove file
if amv.File != "" { if amv.File != "" && Spaces != nil {
err := os.Remove(path.Join(Root, "videos", "amvs", amv.File)) err := Spaces.RemoveObject("arn", fmt.Sprintf("videos/amvs/%s", amv.File))
if err != nil { if err != nil {
return err return err

View File

@ -117,4 +117,7 @@ func init() {
// Set Anilist API keys // Set Anilist API keys
anilist.APIKeyID = APIKeys.AniList.ID anilist.APIKeyID = APIKeys.AniList.ID
anilist.APIKeySecret = APIKeys.AniList.Secret anilist.APIKeySecret = APIKeys.AniList.Secret
// Initialize file storage
initSpaces()
} }

30
arn/Spaces.go Normal file
View File

@ -0,0 +1,30 @@
package arn
import (
"log"
"github.com/minio/minio-go/v6"
)
// Spaces represents our file storage server.
var Spaces *minio.Client
// initSpaces starts our file storage client.
func initSpaces() {
if APIKeys.S3.ID == "" || APIKeys.S3.Secret == "" {
return
}
go func() {
var err error
endpoint := "sfo2.digitaloceanspaces.com"
ssl := true
// Initiate a client using DigitalOcean Spaces.
Spaces, err = minio.New(endpoint, APIKeys.S3.ID, APIKeys.S3.Secret, ssl)
if err != nil {
log.Fatal(err)
}
}()
}

View File

@ -11,7 +11,7 @@ component AMVMini(amv *arn.AMV, user *arn.User)
component AMVVideo(amv *arn.AMV) component AMVVideo(amv *arn.AMV)
.video-container(id=amv.ID, data-api="/api/amv/" + amv.ID) .video-container(id=amv.ID, data-api="/api/amv/" + amv.ID)
video.video.lazy.action(data-action="toggleFullscreen", data-trigger="dblclick", data-id=amv.ID) video.video.lazy.action(data-action="toggleFullscreen", data-trigger="dblclick", data-id=amv.ID)
source(data-src="https://notify.moe/videos/amvs/" + amv.File, data-type="video/webm") source(data-src=amv.VideoLink(), data-type="video/webm")
//- button.media-play-button //- button.media-play-button
//- RawIcon("play") //- RawIcon("play")

View File

@ -2,7 +2,6 @@ package episode
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"github.com/aerogo/aero" "github.com/aerogo/aero"
@ -12,27 +11,6 @@ import (
minio "github.com/minio/minio-go/v6" minio "github.com/minio/minio-go/v6"
) )
var spaces *minio.Client
func init() {
if arn.APIKeys.S3.ID == "" || arn.APIKeys.S3.Secret == "" {
return
}
go func() {
var err error
endpoint := "sfo2.digitaloceanspaces.com"
ssl := true
// Initiate a client using DigitalOcean Spaces.
spaces, err = minio.New(endpoint, arn.APIKeys.S3.ID, arn.APIKeys.S3.Secret, ssl)
if err != nil {
log.Fatal(err)
}
}()
}
// Get renders the anime episode. // Get renders the anime episode.
func Get(ctx aero.Context) error { func Get(ctx aero.Context) error {
user := utils.GetUser(ctx) user := utils.GetUser(ctx)
@ -60,8 +38,8 @@ func Get(ctx aero.Context) error {
// Does the episode exist? // Does the episode exist?
uploaded := false uploaded := false
if spaces != nil { if arn.Spaces != nil {
stat, err := spaces.StatObject("arn", fmt.Sprintf("videos/anime/%s/%d.webm", anime.ID, episodeNumber), minio.StatObjectOptions{}) stat, err := arn.Spaces.StatObject("arn", fmt.Sprintf("videos/anime/%s/%d.webm", anime.ID, episodeNumber), minio.StatObjectOptions{})
uploaded = (err == nil) && (stat.Size > 0) uploaded = (err == nil) && (stat.Size > 0)
} }

View File

@ -29,7 +29,7 @@ func Subtitles(ctx aero.Context) error {
ctx.Response().SetHeader("Access-Control-Allow-Origin", "*") ctx.Response().SetHeader("Access-Control-Allow-Origin", "*")
ctx.Response().SetHeader("Content-Type", "text/vtt; charset=utf-8") ctx.Response().SetHeader("Content-Type", "text/vtt; charset=utf-8")
obj, err := spaces.GetObject("arn", fmt.Sprintf("videos/anime/%s/%d.%s.vtt", anime.ID, episodeNumber, language), minio.GetObjectOptions{}) obj, err := arn.Spaces.GetObject("arn", fmt.Sprintf("videos/anime/%s/%d.%s.vtt", anime.ID, episodeNumber, language), minio.GetObjectOptions{})
if err != nil { if err != nil {
return ctx.Error(http.StatusInternalServerError, err) return ctx.Error(http.StatusInternalServerError, err)

View File

@ -25,20 +25,17 @@ func AMVFile(ctx aero.Context) error {
} }
// Retrieve file from post body // Retrieve file from post body
data, err := ctx.Request().Body().Bytes() reader := ctx.Request().Body().Reader()
defer reader.Close()
if err != nil { // Set amv video file
return ctx.Error(http.StatusInternalServerError, "Reading request body failed", err) err = amv.SetVideoReader(reader)
}
// Set amv image file
err = amv.SetVideoBytes(data)
if err != nil { if err != nil {
return ctx.Error(http.StatusInternalServerError, "Invalid video format", err) return ctx.Error(http.StatusInternalServerError, "Invalid video format", err)
} }
// Save image information // Save video information
amv.Save() amv.Save()
// Write log entry // Write log entry

View File

@ -1,27 +0,0 @@
package main
import (
"github.com/akyoto/color"
"github.com/animenotifier/notify.moe/arn"
"github.com/animenotifier/notify.moe/arn/stringutils"
)
func main() {
color.Yellow("Updating AMV info")
defer color.Green("Finished.")
defer arn.Node.Close()
for amv := range arn.StreamAMVs() {
err := amv.RefreshInfo()
if err != nil {
color.Red(err.Error())
continue
}
color.Green(amv.Title.Canonical)
stringutils.PrettyPrint(amv.Info)
amv.Save()
}
}