WebP bridge is working again

This commit is contained in:
Eduard Urbach 2017-07-08 15:40:13 +02:00
parent 0a51b64e88
commit c729d9d3ba
10 changed files with 84 additions and 77 deletions

View File

@ -1,12 +1,9 @@
package main package main
import ( import (
"errors"
"net/http"
"strings" "strings"
"github.com/aerogo/aero" "github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components/js" "github.com/animenotifier/notify.moe/components/js"
) )
@ -53,44 +50,12 @@ func init() {
// Avatars // Avatars
app.Get("/images/avatars/large/:file", func(ctx *aero.Context) string { app.Get("/images/avatars/large/:file", func(ctx *aero.Context) string {
file := strings.TrimSuffix(ctx.Get("file"), ".webp") return ctx.File("images/avatars/large/" + ctx.Get("file"))
if ctx.CanUseWebP() {
return ctx.File("images/avatars/large/webp/" + file + ".webp")
}
original := arn.FindFileWithExtension(
file,
"images/avatars/large/original/",
arn.OriginalImageExtensions,
)
if original == "" {
return ctx.Error(http.StatusNotFound, "Avatar not found", errors.New("Image not found: "+file))
}
return ctx.File(original)
}) })
// Avatars // Avatars
app.Get("/images/avatars/small/:file", func(ctx *aero.Context) string { app.Get("/images/avatars/small/:file", func(ctx *aero.Context) string {
file := strings.TrimSuffix(ctx.Get("file"), ".webp") return ctx.File("images/avatars/large/" + ctx.Get("file"))
if ctx.CanUseWebP() {
return ctx.File("images/avatars/small/webp/" + file + ".webp")
}
original := arn.FindFileWithExtension(
file,
"images/avatars/small/original/",
arn.OriginalImageExtensions,
)
if original == "" {
return ctx.Error(http.StatusNotFound, "Avatar not found", errors.New("Image not found: "+file))
}
return ctx.File(original)
}) })
// Elements // Elements

View File

@ -15,7 +15,7 @@ func main() {
// Filter out active users with an avatar // Filter out active users with an avatar
users, err := arn.FilterUsers(func(user *arn.User) bool { users, err := arn.FilterUsers(func(user *arn.User) bool {
return user.IsActive() && user.Avatar != "" return user.IsActive() && user.AvatarExtension != ""
}) })
if err != nil { if err != nil {

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"image" "image"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/animenotifier/arn" "github.com/animenotifier/arn"
@ -21,6 +22,20 @@ type Avatar struct {
Format string Format string
} }
// Extension ...
func (avatar *Avatar) Extension() string {
switch avatar.Format {
case "jpg", "jpeg":
return ".jpg"
case "png":
return ".png"
case "gif":
return ".gif"
default:
return ""
}
}
// String returns a text representation of the format, width and height. // String returns a text representation of the format, width and height.
func (avatar *Avatar) String() string { func (avatar *Avatar) String() string {
return fmt.Sprint(avatar.Format, " | ", avatar.Image.Bounds().Dx(), "x", avatar.Image.Bounds().Dy()) return fmt.Sprint(avatar.Format, " | ", avatar.Image.Bounds().Dx(), "x", avatar.Image.Bounds().Dy())
@ -29,17 +44,23 @@ func (avatar *Avatar) String() string {
// AvatarFromURL downloads and decodes the image from an URL and creates an Avatar. // AvatarFromURL downloads and decodes the image from an URL and creates an Avatar.
func AvatarFromURL(url string, user *arn.User) *Avatar { func AvatarFromURL(url string, user *arn.User) *Avatar {
// Download // Download
response, data, networkErr := gorequest.New().Get(url).EndBytes() response, data, networkErrs := gorequest.New().Get(url).EndBytes()
// Retry after 5 seconds if service unavailable
if response.StatusCode == http.StatusServiceUnavailable {
time.Sleep(5 * time.Second)
response, data, networkErr = gorequest.New().Get(url).EndBytes()
}
// Network errors // Network errors
if networkErr != nil { if len(networkErrs) > 0 {
netLog.Error(user.Nick, url, networkErr) netLog.Error(user.Nick, url, networkErrs[0])
return nil
}
// Retry HTTP only version after 5 seconds if service unavailable
if response == nil || response.StatusCode == http.StatusServiceUnavailable {
time.Sleep(5 * time.Second)
response, data, networkErrs = gorequest.New().Get(strings.Replace(url, "https://", "http://", 1)).EndBytes()
}
// Network errors on 2nd try
if len(networkErrs) > 0 {
netLog.Error(user.Nick, url, networkErrs[0])
return nil return nil
} }

View File

@ -20,16 +20,9 @@ type AvatarOriginalFileOutput struct {
// SaveAvatar writes the original avatar to the file system. // SaveAvatar writes the original avatar to the file system.
func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error { func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error {
// Determine file extension // Determine file extension
extension := "" extension := avatar.Extension()
switch avatar.Format { if extension == "" {
case "jpg", "jpeg":
extension = ".jpg"
case "png":
extension = ".png"
case "gif":
extension = ".gif"
default:
return errors.New("Unknown format: " + avatar.Format) return errors.New("Unknown format: " + avatar.Format)
} }
@ -58,6 +51,9 @@ func (output *AvatarOriginalFileOutput) SaveAvatar(avatar *Avatar) error {
data = buffer.Bytes() data = buffer.Bytes()
} }
// Set user avatar
avatar.User.AvatarExtension = extension
// Write to file // Write to file
fileName := output.Directory + avatar.User.ID + extension fileName := output.Directory + avatar.User.ID + extension
return ioutil.WriteFile(fileName, data, 0644) return ioutil.WriteFile(fileName, data, 0644)

View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/animenotifier/arn" "github.com/animenotifier/arn"
@ -26,6 +27,7 @@ func (source *Gravatar) GetAvatar(user *arn.User) *Avatar {
// Build URL // Build URL
gravatarURL := gravatar.Url(user.Email) + "?s=" + fmt.Sprint(arn.AvatarMaxSize) + "&d=404&r=" + source.Rating gravatarURL := gravatar.Url(user.Email) + "?s=" + fmt.Sprint(arn.AvatarMaxSize) + "&d=404&r=" + source.Rating
gravatarURL = strings.Replace(gravatarURL, "http://", "https://", 1)
// Wait for request limiter to allow us to send a request // Wait for request limiter to allow us to send a request
<-source.RequestLimiter.C <-source.RequestLimiter.C

View File

@ -58,26 +58,26 @@ func main() {
avatarOutputs = []AvatarOutput{ avatarOutputs = []AvatarOutput{
// Original - Large // Original - Large
&AvatarOriginalFileOutput{ &AvatarOriginalFileOutput{
Directory: "images/avatars/large/original/", Directory: "images/avatars/large/",
Size: arn.AvatarMaxSize, Size: arn.AvatarMaxSize,
}, },
// Original - Small // Original - Small
&AvatarOriginalFileOutput{ &AvatarOriginalFileOutput{
Directory: "images/avatars/small/original/", Directory: "images/avatars/small/",
Size: arn.AvatarSmallSize, Size: arn.AvatarSmallSize,
}, },
// WebP - Large // WebP - Large
&AvatarWebPFileOutput{ &AvatarWebPFileOutput{
Directory: "images/avatars/large/webp/", Directory: "images/avatars/large/",
Size: arn.AvatarMaxSize, Size: arn.AvatarMaxSize,
Quality: webPQuality, Quality: webPQuality,
}, },
// WebP - Small // WebP - Small
&AvatarWebPFileOutput{ &AvatarWebPFileOutput{
Directory: "images/avatars/small/webp/", Directory: "images/avatars/small/",
Size: arn.AvatarSmallSize, Size: arn.AvatarSmallSize,
Quality: webPQuality, Quality: webPQuality,
}, },
@ -87,20 +87,12 @@ func main() {
return return
} }
// Stream of all users
users, _ := arn.FilterUsers(func(user *arn.User) bool {
return true
})
// Log user count
println(len(users), "users")
// Worker queue // Worker queue
usersQueue := make(chan *arn.User) usersQueue := make(chan *arn.User, 512)
StartWorkers(usersQueue, Work) StartWorkers(usersQueue, Work)
// We'll send each user to one of the worker threads // We'll send each user to one of the worker threads
for _, user := range users { for user := range arn.MustStreamUsers() {
usersQueue <- user usersQueue <- user
} }
@ -120,7 +112,7 @@ func StartWorkers(queue chan *arn.User, work func(*arn.User)) {
// Work handles a single user. // Work handles a single user.
func Work(user *arn.User) { func Work(user *arn.User) {
user.Avatar = "" user.AvatarExtension = ""
for _, source := range avatarSources { for _, source := range avatarSources {
avatar := source.GetAvatar(user) avatar := source.GetAvatar(user)
@ -139,10 +131,19 @@ func Work(user *arn.User) {
} }
fmt.Println(color.GreenString("✔"), reflect.TypeOf(source).Elem().Name(), "|", user.Nick, "|", avatar) fmt.Println(color.GreenString("✔"), reflect.TypeOf(source).Elem().Name(), "|", user.Nick, "|", avatar)
user.Avatar = "/+" + user.Nick + "/avatar"
break break
} }
// Since this a very long running job, refresh user data before saving it.
avatarExt := user.AvatarExtension
user, err := arn.GetUser(user.ID)
if err != nil {
avatarLog.Error("Can't refresh user info:", user.ID, user.Nick)
return
}
// Save avatar data // Save avatar data
user.AvatarExtension = avatarExt
user.Save() user.Save()
} }

View File

@ -4,7 +4,7 @@ component Avatar(user *arn.User)
component AvatarNoLink(user *arn.User) component AvatarNoLink(user *arn.User)
if user.HasAvatar() if user.HasAvatar()
img.user-image.lazy(data-src=user.SmallAvatar(), alt=user.Nick) img.user-image.lazy(data-src=user.SmallAvatar(), data-webp="true", alt=user.Nick)
else else
SVGAvatar SVGAvatar

View File

@ -1,6 +1,6 @@
component ProfileImage(user *arn.User) component ProfileImage(user *arn.User)
if user.HasAvatar() if user.HasAvatar()
img.profile-image(src=user.LargeAvatar(), alt="Profile image") img.profile-image.lazy(data-src=user.LargeAvatar(), data-webp="true", alt="Profile image")
else else
svg.profile-image(viewBox="0 0 50 50", alt="Profile image") svg.profile-image(viewBox="0 0 50 50", alt="Profile image")
circle.head(cx="25", cy="19", r="10") circle.head(cx="25", cy="19", r="10")

View File

@ -1,7 +1,7 @@
import { Application } from "./Application" import { Application } from "./Application"
import { Diff } from "./Diff" import { Diff } from "./Diff"
import { displayAiringDate, displayDate } from "./DateView" import { displayAiringDate, displayDate } from "./DateView"
import { findAll, delay } from "./Utils" import { findAll, delay, canUseWebP } from "./Utils"
import { MutationQueue } from "./MutationQueue" import { MutationQueue } from "./MutationQueue"
import * as actions from "./Actions" import * as actions from "./Actions"
@ -9,6 +9,7 @@ export class AnimeNotifier {
app: Application app: Application
user: HTMLElement user: HTMLElement
title: string title: string
webpEnabled: boolean
visibilityObserver: IntersectionObserver visibilityObserver: IntersectionObserver
imageFound: MutationQueue imageFound: MutationQueue
@ -80,6 +81,9 @@ export class AnimeNotifier {
document.documentElement.classList.add("osx") document.documentElement.classList.add("osx")
} }
// Check for WebP support
this.webpEnabled = canUseWebP()
// Initiate the elements we need // Initiate the elements we need
this.user = this.app.find("user") this.user = this.app.find("user")
this.app.content = this.app.find("content") this.app.content = this.app.find("content")
@ -207,7 +211,13 @@ export class AnimeNotifier {
lazyLoadImage(img: HTMLImageElement) { lazyLoadImage(img: HTMLImageElement) {
// Once the image becomes visible, load it // Once the image becomes visible, load it
img["became visible"] = () => { img["became visible"] = () => {
img.src = img.dataset.src // Replace URL with WebP if supported
if(this.webpEnabled && img.dataset.webp) {
let dot = img.dataset.src.lastIndexOf(".")
img.src = img.dataset.src.substring(0, dot) + ".webp"
} else {
img.src = img.dataset.src
}
if(img.naturalWidth === 0) { if(img.naturalWidth === 0) {
img.onload = () => { img.onload = () => {

View File

@ -11,5 +11,17 @@ export function delay<T>(millis: number, value?: T): Promise<T> {
} }
export function plural(count: number, singular: string): string { export function plural(count: number, singular: string): string {
return (count === 1 || count === -1) ? (count + ' ' + singular) : (count + ' ' + singular + 's') return (count === 1 || count === -1) ? (count + " " + singular) : (count + " " + singular + "s")
}
export function canUseWebP(): boolean {
let canvas = document.createElement("canvas")
if(!!(canvas.getContext && canvas.getContext("2d"))) {
// WebP representation possible
return canvas.toDataURL("image/webp").indexOf("data:image/webp") === 0
} else {
// In very old browsers (IE 8) canvas is not supported
return false
}
} }