WebP bridge is working again
This commit is contained in:
parent
0a51b64e88
commit
c729d9d3ba
39
assets.go
39
assets.go
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
@ -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"] = () => {
|
||||||
|
// 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
|
img.src = img.dataset.src
|
||||||
|
}
|
||||||
|
|
||||||
if(img.naturalWidth === 0) {
|
if(img.naturalWidth === 0) {
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user