Skip to content

Commit b7e6781

Browse files
authored
feat: Updates via Heresphere API (#952)
* Heresphere Api Updates Handles isFavorites, Rating and DeleteFiles * Enable Option to Save HSP files * Handle Tag/Cuepoint changes * Enable delete of Unassigned Files via api * Sync Watchlist via Feature:Watchlist tag
1 parent 61ed589 commit b7e6781

File tree

13 files changed

+498
-96
lines changed

13 files changed

+498
-96
lines changed

pkg/api/files.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,17 @@ func (i FilesResource) removeFile(req *restful.Request, resp *restful.Response)
310310
if err != nil {
311311
return
312312
}
313+
scene := removeFileByFileId(uint(fileId))
314+
resp.WriteHeaderAndEntity(http.StatusOK, scene)
315+
}
316+
func removeFileByFileId(fileId uint) models.Scene {
313317

314318
var scene models.Scene
315319
var file models.File
316320
db, _ := models.GetDB()
317321
defer db.Close()
318322

319-
err = db.Preload("Volume").Where(&models.File{ID: uint(fileId)}).First(&file).Error
323+
err := db.Preload("Volume").Where(&models.File{ID: fileId}).First(&file).Error
320324
if err == nil {
321325

322326
deleted := false
@@ -331,7 +335,7 @@ func (i FilesResource) removeFile(req *restful.Request, resp *restful.Response)
331335
case "putio":
332336
id, err := strconv.ParseInt(file.Path, 10, 64)
333337
if err != nil {
334-
return
338+
return scene
335339
}
336340
client := file.Volume.GetPutIOClient()
337341
err = client.Files.Delete(context.Background(), id)
@@ -352,6 +356,5 @@ func (i FilesResource) removeFile(req *restful.Request, resp *restful.Response)
352356
} else {
353357
log.Errorf("Error deleting file ", err)
354358
}
355-
356-
resp.WriteHeaderAndEntity(http.StatusOK, scene)
359+
return scene
357360
}

pkg/api/heresphere.go

Lines changed: 213 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
package api
22

33
import (
4+
"encoding/base64"
45
"encoding/json"
56
"fmt"
7+
"io/ioutil"
68
"math"
79
"net/http"
10+
"path/filepath"
811
"sort"
912
"strconv"
1013
"strings"
14+
"sync"
1115

1216
"github.com/dustin/go-humanize"
1317
"github.com/emicklei/go-restful"
1418
restfulspec "github.com/emicklei/go-restful-openapi"
1519
"github.com/markphelps/optional"
1620
"github.com/xbapps/xbvr/pkg/config"
1721
"github.com/xbapps/xbvr/pkg/models"
22+
"github.com/xbapps/xbvr/pkg/tasks"
1823
"golang.org/x/crypto/bcrypt"
1924
)
2025

@@ -47,6 +52,10 @@ type HeresphereVideo struct {
4752
Scripts []HeresphereScript `json:"scripts,omitempty"`
4853
Tags []HeresphereTag `json:"tags,omitempty"`
4954
Media []HeresphereMedia `json:"media"`
55+
WriteFavorite bool `json:"writeFavorite"`
56+
WriteRating bool `json:"writeRating"`
57+
WriteTags bool `json:"writeTags"`
58+
WriteHSP bool `json:"writeHSP"`
5059
}
5160

5261
type HeresphereScript struct {
@@ -55,10 +64,10 @@ type HeresphereScript struct {
5564
}
5665

5766
type HeresphereTag struct {
58-
Name string `json:"name"`
59-
StartMilliseconds int `json:"start,omitempty"`
60-
EndMilliseconds int `json:"end,omitempty"`
61-
Track *int `json:"track,omitempty"`
67+
Name string `json:"name"`
68+
StartMilliseconds float64 `json:"start,omitempty"`
69+
EndMilliseconds float64 `json:"end,omitempty"`
70+
Track *int `json:"track,omitempty"`
6271
}
6372

6473
type HeresphereMedia struct {
@@ -75,8 +84,13 @@ type HeresphereSource struct {
7584
}
7685

7786
type HereSphereAuthRequest struct {
78-
Username string `json:"username"`
79-
Password string `json:"password"`
87+
Username string `json:"username"`
88+
Password string `json:"password"`
89+
Rating *float64 `json:"rating"`
90+
IsFavorite *bool `json:"isFavorite"`
91+
Hsp *string `json:"hsp"`
92+
Tags *[]HeresphereTag `json:"tags"`
93+
DeleteFiles *bool `json:"deleteFile"`
8094
}
8195

8296
func HeresphereAuthFilter(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
@@ -157,6 +171,12 @@ func (i HeresphereResource) getHeresphereFile(req *restful.Request, resp *restfu
157171
return
158172
}
159173

174+
var requestData HereSphereAuthRequest
175+
if err := json.NewDecoder(req.Request.Body).Decode(&requestData); err != nil {
176+
log.Errorf("Error decoding heresphere api POST request: %v", err)
177+
return
178+
}
179+
160180
db, _ := models.GetDB()
161181
defer db.Close()
162182

@@ -199,6 +219,9 @@ func (i HeresphereResource) getHeresphereFile(req *restful.Request, resp *restfu
199219
DurationMilliseconds: uint(file.VideoDuration * 1000),
200220
Media: media,
201221
}
222+
if requestData.DeleteFiles != nil && config.Config.Interfaces.Heresphere.AllowFileDeletes {
223+
removeFileByFileId(file.ID)
224+
}
202225

203226
resp.WriteHeaderAndEntity(http.StatusOK, video)
204227
}
@@ -208,6 +231,12 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
208231
return
209232
}
210233

234+
var requestData HereSphereAuthRequest
235+
if err := json.NewDecoder(req.Request.Body).Decode(&requestData); err != nil {
236+
log.Errorf("Error decoding heresphere api POST request: %v", err)
237+
return
238+
}
239+
211240
sceneID := req.PathParameter("scene-id")
212241
if sceneID == "" {
213242
return
@@ -220,12 +249,22 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
220249
err := db.Preload("Cast").
221250
Preload("Tags").
222251
Preload("Cuepoints").
252+
Preload("Files").
223253
Where("id = ?", sceneID).First(&scene).Error
224254
if err != nil {
225255
log.Error(err)
226256
return
227257
}
228258

259+
var videoFiles []models.File
260+
videoFiles, err = scene.GetVideoFiles()
261+
if err != nil {
262+
log.Error(err)
263+
return
264+
}
265+
266+
ProcessHeresphereUpdates(&scene, requestData, videoFiles[0])
267+
229268
features := make(map[string]bool, 30)
230269
addFeatureTag := func(feature string) {
231270
if !features[feature] {
@@ -235,12 +274,6 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
235274

236275
var media []HeresphereMedia
237276

238-
var videoFiles []models.File
239-
videoFiles, err = scene.GetVideoFiles()
240-
if err != nil {
241-
log.Error(err)
242-
return
243-
}
244277
videoLength := float64(scene.Duration)
245278

246279
for i, file := range videoFiles {
@@ -302,8 +335,8 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
302335
}
303336
tags = append(tags, HeresphereTag{
304337
Name: scene.Cuepoints[i].Name,
305-
StartMilliseconds: start,
306-
EndMilliseconds: end,
338+
StartMilliseconds: float64(start),
339+
EndMilliseconds: float64(end),
307340
Track: &track,
308341
})
309342
}
@@ -475,15 +508,180 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
475508
Scripts: heresphereScriptFiles,
476509
Tags: tags,
477510
Media: media,
511+
WriteFavorite: config.Config.Interfaces.Heresphere.AllowFavoriteUpdates,
512+
WriteRating: config.Config.Interfaces.Heresphere.AllowRatingUpdates,
513+
WriteTags: config.Config.Interfaces.Heresphere.AllowTagUpdates || config.Config.Interfaces.Heresphere.AllowCuepointUpdates || config.Config.Interfaces.Heresphere.AllowWatchlistUpdates,
514+
WriteHSP: config.Config.Interfaces.Heresphere.AllowHspData,
478515
}
479516

480517
if scene.HasVideoPreview {
481518
video.ThumbnailVideo = fmt.Sprintf("http://%v/api/dms/preview/%v", req.Request.Host, scene.SceneID)
482519
}
483-
484520
resp.WriteHeaderAndEntity(http.StatusOK, video)
485521
}
486522

523+
var lockHeresphereUpdates sync.Mutex
524+
525+
func ProcessHeresphereUpdates(scene *models.Scene, requestData HereSphereAuthRequest, videoFile models.File) {
526+
527+
db, _ := models.GetDB()
528+
defer db.Close()
529+
530+
updateReqd := false
531+
if requestData.IsFavorite != nil && *requestData.IsFavorite != scene.Favourite && config.Config.Interfaces.Heresphere.AllowFavoriteUpdates {
532+
scene.Favourite = *requestData.IsFavorite
533+
updateReqd = true
534+
}
535+
if requestData.Rating != nil && *requestData.Rating != scene.StarRating && config.Config.Interfaces.Heresphere.AllowRatingUpdates {
536+
scene.StarRating = *requestData.Rating
537+
updateReqd = true
538+
}
539+
540+
if requestData.Tags != nil && (config.Config.Interfaces.Heresphere.AllowTagUpdates || config.Config.Interfaces.Heresphere.AllowCuepointUpdates || config.Config.Interfaces.Heresphere.AllowWatchlistUpdates) {
541+
// need lock, heresphere can send a second post too soon
542+
lockHeresphereUpdates.Lock()
543+
defer lockHeresphereUpdates.Unlock()
544+
}
545+
if requestData.Tags != nil && config.Config.Interfaces.Heresphere.AllowTagUpdates {
546+
var newTags []string
547+
548+
// need to reread the tags, to handle muti threading issues and the scene record may have changed
549+
// just preload the tags, preload all associations and the scene, does not reread the tags?, so just get them and update the scene
550+
var tmp models.Scene
551+
db.Preload("Tags").Where("id = ?", scene.ID).First(&tmp)
552+
scene.Tags = tmp.Tags
553+
554+
for _, tag := range *requestData.Tags {
555+
if strings.HasPrefix(strings.ToLower(tag.Name), "category:") {
556+
newTags = append(newTags, tag.Name[9:])
557+
}
558+
}
559+
ProcessTagChanges(scene, &newTags, db)
560+
updateReqd = true
561+
}
562+
563+
if requestData.Tags != nil && config.Config.Interfaces.Heresphere.AllowWatchlistUpdates {
564+
// need to reread the tags, to handle muti threading issues and the scene record may have changed
565+
// just preload the tags, preload all associations and the scene, does not reread the tags?, so just get them and update the scene
566+
var tmp models.Scene
567+
db.Preload("Tags").Where("id = ?", scene.ID).First(&tmp)
568+
scene.Tags = tmp.Tags
569+
570+
watchlist := false
571+
for _, tag := range *requestData.Tags {
572+
if strings.HasPrefix(strings.ToLower(tag.Name), "feature:watchlist") {
573+
watchlist = true
574+
}
575+
}
576+
if scene.Watchlist != watchlist {
577+
scene.Watchlist = watchlist
578+
updateReqd = true
579+
}
580+
}
581+
582+
if requestData.Tags != nil && config.Config.Interfaces.Heresphere.AllowCuepointUpdates {
583+
// need to reread the cuepoints, to handle muti threading issues and the scene record may have changed
584+
// just preload the cuepoint, preload all associations and the scene, does not reread the cuepoint?, so just get them and update the scene
585+
var tmp models.Scene
586+
db.Preload("Cuepoints").Where("id = ?", scene.ID).First(&tmp)
587+
scene.Cuepoints = tmp.Cuepoints
588+
589+
var replacementCuepoints []models.SceneCuepoint
590+
endpos := findEndPos(requestData)
591+
firstTrack := findTheMainTrack(requestData)
592+
for _, tag := range *requestData.Tags {
593+
if !strings.Contains(tag.Name, ":") {
594+
if *tag.Track == firstTrack {
595+
replacementCuepoints = append(replacementCuepoints, models.SceneCuepoint{SceneID: scene.ID, TimeStart: float64(tag.StartMilliseconds) / 1000, Name: tag.Name})
596+
} else {
597+
//allow for multi track, merge into the main cuepoint name
598+
if tag.StartMilliseconds > 0 || tag.EndMilliseconds < endpos {
599+
for idx, newtag := range replacementCuepoints {
600+
// allow 5 seconds lewway to align manually entered tags
601+
if math.Abs((newtag.TimeStart)-tag.StartMilliseconds/1000) < 5 {
602+
replacementCuepoints[idx].Name = tag.Name + "-" + replacementCuepoints[idx].Name
603+
}
604+
}
605+
}
606+
}
607+
}
608+
}
609+
db.Model(&scene).Association("Cuepoints").Replace(&replacementCuepoints)
610+
611+
updateReqd = true
612+
}
613+
614+
if requestData.DeleteFiles != nil && config.Config.Interfaces.Heresphere.AllowFileDeletes {
615+
for _, sceneFile := range scene.Files {
616+
removeFileByFileId(sceneFile.ID)
617+
}
618+
}
619+
620+
if requestData.Hsp != nil && config.Config.Interfaces.Heresphere.AllowHspData {
621+
hspContent, err := base64.StdEncoding.DecodeString(*requestData.Hsp)
622+
if err != nil {
623+
log.Error("Error decoding heresphere hsp data %v", err)
624+
}
625+
626+
fName := filepath.Join(scene.Files[0].Path, strings.TrimSuffix(scene.Files[0].Filename, filepath.Ext(videoFile.Filename))+".hsp")
627+
ioutil.WriteFile(fName, hspContent, 0644)
628+
629+
tasks.ScanLocalHspFile(fName, videoFile.VolumeID, scene.ID)
630+
}
631+
632+
if updateReqd {
633+
scene.Save()
634+
}
635+
}
636+
func findTheMainTrack(requestData HereSphereAuthRequest) int {
637+
// 99% of the time we want Track 0, but the user may have deleted and added whole track
638+
639+
// find the max duration
640+
endpos := findEndPos(requestData)
641+
for _, tag := range *requestData.Tags {
642+
if endpos < tag.EndMilliseconds {
643+
endpos = tag.EndMilliseconds
644+
}
645+
}
646+
647+
// find the best track
648+
likelyTrack := 9999
649+
alternateTrack := 9999
650+
651+
for _, tag := range *requestData.Tags {
652+
if (tag.StartMilliseconds > 0 || tag.EndMilliseconds < endpos) && !strings.Contains(tag.Name, ":") {
653+
return *tag.Track
654+
}
655+
656+
if (tag.StartMilliseconds > 0 || tag.EndMilliseconds < endpos) && likelyTrack > *tag.Track {
657+
likelyTrack = *tag.Track
658+
}
659+
if !strings.Contains(tag.Name, ":") && alternateTrack > *tag.Track {
660+
alternateTrack = *tag.Track
661+
}
662+
}
663+
664+
if likelyTrack < 9999 {
665+
return likelyTrack
666+
}
667+
668+
if alternateTrack < 9999 {
669+
return likelyTrack
670+
}
671+
672+
return -1
673+
}
674+
func findEndPos(requestData HereSphereAuthRequest) float64 {
675+
// find the max duration
676+
endpos := float64(0)
677+
for _, tag := range *requestData.Tags {
678+
if endpos < tag.EndMilliseconds {
679+
endpos = tag.EndMilliseconds
680+
}
681+
}
682+
return endpos
683+
}
684+
487685
func (i HeresphereResource) getHeresphereLibrary(req *restful.Request, resp *restful.Response) {
488686
if !config.Config.Interfaces.DeoVR.Enabled {
489687
return

0 commit comments

Comments
 (0)