Skip to content

Commit 4cc29c4

Browse files
authored
Render heatmaps on scene thumbnails in DeoVR (#464)
1 parent 81ca73c commit 4cc29c4

File tree

12 files changed

+341
-53
lines changed

12 files changed

+341
-53
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/bregydoc/gtranslate v0.0.0-20200913051839-1bd07f6c1fc5
1919
github.com/creasty/defaults v1.5.1
2020
github.com/darwayne/go-timecode v1.1.0
21+
github.com/disintegration/imaging v1.6.0
2122
github.com/djherbis/times v1.2.0
2223
github.com/dsnet/compress v0.0.1 // indirect
2324
github.com/dustin/go-humanize v1.0.0

pkg/api/deovr.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,14 @@ func (i DeoVRResource) getDeoScene(req *restful.Request, resp *restful.Response)
337337
screenType = "sphere"
338338
}
339339

340-
var title = scene.Title
340+
title := scene.Title
341+
thumbnailURL := session.DeoRequestHost + "/img/700x/" + strings.Replace(scene.CoverURL, "://", ":/", -1)
341342

342343
if scene.IsScripted {
343344
title = scene.GetFunscriptTitle()
345+
if config.Config.Interfaces.DeoVR.RenderHeatmaps {
346+
thumbnailURL = session.DeoRequestHost + "/imghm/" + fmt.Sprint(scene.ID) + "/" + strings.Replace(scene.CoverURL, "://", ":/", -1)
347+
}
344348
}
345349

346350
deoScene := DeoScene{
@@ -354,7 +358,7 @@ func (i DeoVRResource) getDeoScene(req *restful.Request, resp *restful.Response)
354358
RatingAvg: scene.StarRating,
355359
FullVideoReady: true,
356360
FullAccess: true,
357-
ThumbnailURL: session.DeoRequestHost + "/img/700x/" + strings.Replace(scene.CoverURL, "://", ":/", -1),
361+
ThumbnailURL: thumbnailURL,
358362
StereoMode: stereoMode,
359363
Is3D: true,
360364
ScreenType: screenType,
@@ -426,10 +430,16 @@ func scenesToDeoList(req *restful.Request, scenes []models.Scene) []DeoListItem
426430

427431
list := make([]DeoListItem, 0)
428432
for i := range scenes {
433+
thumbnailURL := fmt.Sprintf("%v/img/700x/%v", session.DeoRequestHost, strings.Replace(scenes[i].CoverURL, "://", ":/", -1))
434+
435+
if config.Config.Interfaces.DeoVR.RenderHeatmaps && scenes[i].IsScripted {
436+
thumbnailURL = fmt.Sprintf("%v/imghm/%d/%v", session.DeoRequestHost, scenes[i].ID, strings.Replace(scenes[i].CoverURL, "://", ":/", -1))
437+
}
438+
429439
item := DeoListItem{
430440
Title: scenes[i].Title,
431441
VideoLength: scenes[i].Duration * 60,
432-
ThumbnailURL: fmt.Sprintf("%v/img/700x/%v", session.DeoRequestHost, strings.Replace(scenes[i].CoverURL, "://", ":/", -1)),
442+
ThumbnailURL: thumbnailURL,
433443
VideoURL: fmt.Sprintf("%v/deovr/%v", session.DeoRequestHost, scenes[i].ID),
434444
}
435445
list = append(list, item)

pkg/api/options.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ type RequestSaveOptionsDLNA struct {
5050
}
5151

5252
type RequestSaveOptionsDeoVR struct {
53-
Enabled bool `json:"enabled"`
54-
AuthEnabled bool `json:"auth_enabled"`
55-
Username string `json:"username"`
56-
Password string `json:"password"`
57-
RemoteEnabled bool `json:"remote_enabled"`
53+
Enabled bool `json:"enabled"`
54+
AuthEnabled bool `json:"auth_enabled"`
55+
Username string `json:"username"`
56+
Password string `json:"password"`
57+
RemoteEnabled bool `json:"remote_enabled"`
58+
RenderHeatmaps bool `json:"render_heatmaps"`
5859
}
5960

6061
type RequestSaveOptionsPreviews struct {
@@ -229,6 +230,7 @@ func (i ConfigResource) saveOptionsDeoVR(req *restful.Request, resp *restful.Res
229230

230231
config.Config.Interfaces.DeoVR.Enabled = r.Enabled
231232
config.Config.Interfaces.DeoVR.AuthEnabled = r.AuthEnabled
233+
config.Config.Interfaces.DeoVR.RenderHeatmaps = r.RenderHeatmaps
232234
config.Config.Interfaces.DeoVR.RemoteEnabled = r.RemoteEnabled
233235
config.Config.Interfaces.DeoVR.Username = r.Username
234236
if r.Password != config.Config.Interfaces.DeoVR.Password && r.Password != "" {

pkg/config/config.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ type ObjectConfig struct {
2929
AllowedIP []string `default:"[]" json:"allowedIp"`
3030
} `json:"dlna"`
3131
DeoVR struct {
32-
Enabled bool `default:"true" json:"enabled"`
33-
AuthEnabled bool `default:"false" json:"auth_enabled"`
34-
RemoteEnabled bool `default:"false" json:"remote_enabled"`
35-
Username string `default:"" json:"username"`
36-
Password string `default:"" json:"password"`
32+
Enabled bool `default:"true" json:"enabled"`
33+
AuthEnabled bool `default:"false" json:"auth_enabled"`
34+
RenderHeatmaps bool `default:"false" json:"render_heatmaps"`
35+
RemoteEnabled bool `default:"false" json:"remote_enabled"`
36+
Username string `default:"" json:"username"`
37+
Password string `default:"" json:"password"`
3738
} `json:"deovr"`
3839
} `json:"interfaces"`
3940
Library struct {

pkg/server/heatmapproxy.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package server
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"image"
7+
"image/draw"
8+
"image/jpeg"
9+
"image/png"
10+
"io"
11+
"io/ioutil"
12+
"net/http"
13+
"net/url"
14+
"os"
15+
"path/filepath"
16+
"strconv"
17+
"strings"
18+
19+
"github.com/disintegration/imaging"
20+
"github.com/xbapps/xbvr/pkg/common"
21+
"github.com/xbapps/xbvr/pkg/models"
22+
"willnorris.com/go/imageproxy"
23+
)
24+
25+
const thumbnailWidth = 700
26+
const thumbnailHeight = 420
27+
const heatmapHeight = 10
28+
const heatmapMargin = 3
29+
30+
type BufferResponseWriter struct {
31+
header http.Header
32+
statusCode int
33+
buf *bytes.Buffer
34+
}
35+
36+
func (myrw *BufferResponseWriter) Write(p []byte) (int, error) {
37+
return myrw.buf.Write(p)
38+
}
39+
40+
func (w *BufferResponseWriter) Header() http.Header {
41+
return w.header
42+
}
43+
44+
func (w *BufferResponseWriter) WriteHeader(statusCode int) {
45+
w.statusCode = statusCode
46+
}
47+
48+
type HeatmapThumbnailProxy struct {
49+
ImageProxy *imageproxy.Proxy
50+
Cache imageproxy.Cache
51+
}
52+
53+
func NewHeatmapThumbnailProxy(imageproxy *imageproxy.Proxy, cache imageproxy.Cache) *HeatmapThumbnailProxy {
54+
proxy := &HeatmapThumbnailProxy{
55+
ImageProxy: imageproxy,
56+
Cache: cache,
57+
}
58+
return proxy
59+
}
60+
61+
func getScriptFileId(urlpart string) (uint, error) {
62+
sceneId, err := strconv.Atoi(urlpart)
63+
if err != nil {
64+
return 0, err
65+
}
66+
67+
var scene models.Scene
68+
err = scene.GetIfExistByPK(uint(sceneId))
69+
if err != nil {
70+
return 0, err
71+
}
72+
scriptfiles, err := scene.GetScriptFiles()
73+
if err != nil || len(scriptfiles) < 1 {
74+
return 0, fmt.Errorf("scene %d has no script files", sceneId)
75+
}
76+
return scriptfiles[0].ID, nil
77+
}
78+
79+
func getHeatmapImageForScene(fileId uint) (image.Image, error) {
80+
81+
heatmapFilename := filepath.Join(common.ScriptHeatmapDir, fmt.Sprintf("heatmap-%d.png", fileId))
82+
heatmapFile, err := os.Open(heatmapFilename)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
heatmapImage, err := png.Decode(heatmapFile)
88+
heatmapFile.Close()
89+
if err != nil {
90+
return nil, err
91+
}
92+
93+
return heatmapImage, nil
94+
}
95+
96+
func createHeatmapThumbnail(out *bytes.Buffer, r io.Reader, heatmapImage image.Image) error {
97+
thumbnailImage, err := jpeg.Decode(r)
98+
99+
if err != nil {
100+
return err
101+
}
102+
103+
rect := thumbnailImage.Bounds()
104+
if rect.Dx() != thumbnailWidth || rect.Dy() != thumbnailHeight-heatmapHeight-heatmapMargin {
105+
thumbnailImage = imaging.Fill(thumbnailImage, thumbnailWidth, thumbnailHeight-heatmapHeight-heatmapMargin, imaging.Center, imaging.Linear)
106+
}
107+
heatmapImage = imaging.Resize(heatmapImage, thumbnailWidth, heatmapHeight, imaging.Linear)
108+
109+
canvas := image.NewNRGBA(image.Rect(0, 0, thumbnailWidth, thumbnailHeight))
110+
111+
drawRect := image.Rect(0, 0, thumbnailWidth, thumbnailHeight-heatmapHeight-heatmapMargin)
112+
draw.Draw(canvas, drawRect, thumbnailImage, image.Point{}, draw.Over)
113+
drawRect = image.Rect(0, thumbnailHeight-heatmapHeight, thumbnailWidth, thumbnailHeight)
114+
draw.Draw(canvas, drawRect, heatmapImage, image.Point{}, draw.Over)
115+
jpeg.Encode(out, canvas, &jpeg.Options{Quality: 90})
116+
return nil
117+
}
118+
119+
func (p *HeatmapThumbnailProxy) serveImageproxyResponse(w http.ResponseWriter, r *http.Request, imageURL string) {
120+
proxyURL := "/700x/" + imageURL
121+
r2 := new(http.Request)
122+
*r2 = *r
123+
r2.URL = new(url.URL)
124+
*r2.URL = *r.URL
125+
r2.URL.Path = proxyURL
126+
p.ImageProxy.ServeHTTP(w, r2)
127+
}
128+
129+
func (p *HeatmapThumbnailProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
130+
131+
parts := strings.SplitN(r.URL.Path, "/", 3)
132+
if len(parts) != 3 {
133+
http.NotFound(w, r)
134+
return
135+
}
136+
137+
imageURL := parts[2]
138+
fileId, err := getScriptFileId(parts[1])
139+
if err != nil {
140+
p.serveImageproxyResponse(w, r, imageURL)
141+
return
142+
}
143+
144+
cacheKey := fmt.Sprintf("%d:%s", fileId, imageURL)
145+
cachedContent, ok := p.Cache.Get(cacheKey)
146+
if ok {
147+
w.Header().Add("Content-Type", "image/jpeg")
148+
w.Header().Add("Content-Length", fmt.Sprint(len(cachedContent)))
149+
if _, err := io.Copy(w, bytes.NewReader(cachedContent)); err != nil {
150+
log.Printf("Failed to send out response: %v", err)
151+
}
152+
return
153+
}
154+
155+
heatmapImage, err := getHeatmapImageForScene(fileId)
156+
if err != nil {
157+
p.serveImageproxyResponse(w, r, imageURL)
158+
return
159+
}
160+
161+
proxyURL := fmt.Sprintf("/%dx%d,jpeg/%s", thumbnailWidth, thumbnailHeight-heatmapHeight-heatmapMargin, imageURL)
162+
r2 := new(http.Request)
163+
*r2 = *r
164+
r2.URL = new(url.URL)
165+
*r2.URL = *r.URL
166+
r2.URL.Path = proxyURL
167+
imageproxyResponseWriter := &BufferResponseWriter{
168+
header: http.Header{},
169+
buf: &bytes.Buffer{},
170+
}
171+
p.ImageProxy.ServeHTTP(imageproxyResponseWriter, r2)
172+
173+
respbody, err := ioutil.ReadAll(imageproxyResponseWriter.buf)
174+
if err == nil {
175+
var output bytes.Buffer
176+
err = createHeatmapThumbnail(&output, bytes.NewReader(respbody), heatmapImage)
177+
if err == nil {
178+
p.Cache.Set(cacheKey, output.Bytes())
179+
w.Header().Add("Content-Type", "image/jpeg")
180+
w.Header().Add("Content-Length", fmt.Sprint(len(output.Bytes())))
181+
if _, err := io.Copy(w, bytes.NewReader(output.Bytes())); err != nil {
182+
log.Printf("Failed to send out response: %v", err)
183+
}
184+
return
185+
}
186+
}
187+
if err != nil {
188+
log.Printf("%v", err)
189+
// serve original response
190+
if _, err := io.Copy(w, bytes.NewReader(respbody)); err != nil {
191+
log.Printf("Failed to send out response: %v", err)
192+
}
193+
}
194+
}

pkg/server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ func StartServer(version, commit, branch, date string) {
131131
p := imageproxy.NewProxy(nil, diskCache(filepath.Join(common.AppDir, "imageproxy")))
132132
p.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36"
133133
r.PathPrefix("/img/").Handler(http.StripPrefix("/img", p))
134+
hmp := NewHeatmapThumbnailProxy(p, diskCache(filepath.Join(common.AppDir, "heatmapthumbnailproxy")))
135+
r.PathPrefix("/imghm/").Handler(http.StripPrefix("/imghm", hmp))
134136
r.SkipClean(true)
135137

136138
r.PathPrefix("/").Handler(http.DefaultServeMux)

0 commit comments

Comments
 (0)