Use a CDN for faster AMV delivery
This commit is contained in:
parent
1b0bb6fdbd
commit
bc4e67f718
70
arn/AMV.go
70
arn/AMV.go
@ -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()
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshInfo refreshes the information about the video file.
|
// Is our storage server available?
|
||||||
func (amv *AMV) RefreshInfo() error {
|
if Spaces == nil {
|
||||||
if amv.File == "" {
|
return errors.New("File storage client has not been initialized")
|
||||||
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))
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
30
arn/Spaces.go
Normal 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
@ -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")
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user