Skip to content

Commit

Permalink
feat: Updates via Heresphere API (#952)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
toshski authored Oct 2, 2022
1 parent 61ed589 commit b7e6781
Show file tree
Hide file tree
Showing 13 changed files with 498 additions and 96 deletions.
11 changes: 7 additions & 4 deletions pkg/api/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,17 @@ func (i FilesResource) removeFile(req *restful.Request, resp *restful.Response)
if err != nil {
return
}
scene := removeFileByFileId(uint(fileId))
resp.WriteHeaderAndEntity(http.StatusOK, scene)
}
func removeFileByFileId(fileId uint) models.Scene {

var scene models.Scene
var file models.File
db, _ := models.GetDB()
defer db.Close()

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

deleted := false
Expand All @@ -331,7 +335,7 @@ func (i FilesResource) removeFile(req *restful.Request, resp *restful.Response)
case "putio":
id, err := strconv.ParseInt(file.Path, 10, 64)
if err != nil {
return
return scene
}
client := file.Volume.GetPutIOClient()
err = client.Files.Delete(context.Background(), id)
Expand All @@ -352,6 +356,5 @@ func (i FilesResource) removeFile(req *restful.Request, resp *restful.Response)
} else {
log.Errorf("Error deleting file ", err)
}

resp.WriteHeaderAndEntity(http.StatusOK, scene)
return scene
}
228 changes: 213 additions & 15 deletions pkg/api/heresphere.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package api

import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"

"github.com/dustin/go-humanize"
"github.com/emicklei/go-restful"
restfulspec "github.com/emicklei/go-restful-openapi"
"github.com/markphelps/optional"
"github.com/xbapps/xbvr/pkg/config"
"github.com/xbapps/xbvr/pkg/models"
"github.com/xbapps/xbvr/pkg/tasks"
"golang.org/x/crypto/bcrypt"
)

Expand Down Expand Up @@ -47,6 +52,10 @@ type HeresphereVideo struct {
Scripts []HeresphereScript `json:"scripts,omitempty"`
Tags []HeresphereTag `json:"tags,omitempty"`
Media []HeresphereMedia `json:"media"`
WriteFavorite bool `json:"writeFavorite"`
WriteRating bool `json:"writeRating"`
WriteTags bool `json:"writeTags"`
WriteHSP bool `json:"writeHSP"`
}

type HeresphereScript struct {
Expand All @@ -55,10 +64,10 @@ type HeresphereScript struct {
}

type HeresphereTag struct {
Name string `json:"name"`
StartMilliseconds int `json:"start,omitempty"`
EndMilliseconds int `json:"end,omitempty"`
Track *int `json:"track,omitempty"`
Name string `json:"name"`
StartMilliseconds float64 `json:"start,omitempty"`
EndMilliseconds float64 `json:"end,omitempty"`
Track *int `json:"track,omitempty"`
}

type HeresphereMedia struct {
Expand All @@ -75,8 +84,13 @@ type HeresphereSource struct {
}

type HereSphereAuthRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username"`
Password string `json:"password"`
Rating *float64 `json:"rating"`
IsFavorite *bool `json:"isFavorite"`
Hsp *string `json:"hsp"`
Tags *[]HeresphereTag `json:"tags"`
DeleteFiles *bool `json:"deleteFile"`
}

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

var requestData HereSphereAuthRequest
if err := json.NewDecoder(req.Request.Body).Decode(&requestData); err != nil {
log.Errorf("Error decoding heresphere api POST request: %v", err)
return
}

db, _ := models.GetDB()
defer db.Close()

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

resp.WriteHeaderAndEntity(http.StatusOK, video)
}
Expand All @@ -208,6 +231,12 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
return
}

var requestData HereSphereAuthRequest
if err := json.NewDecoder(req.Request.Body).Decode(&requestData); err != nil {
log.Errorf("Error decoding heresphere api POST request: %v", err)
return
}

sceneID := req.PathParameter("scene-id")
if sceneID == "" {
return
Expand All @@ -220,12 +249,22 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
err := db.Preload("Cast").
Preload("Tags").
Preload("Cuepoints").
Preload("Files").
Where("id = ?", sceneID).First(&scene).Error
if err != nil {
log.Error(err)
return
}

var videoFiles []models.File
videoFiles, err = scene.GetVideoFiles()
if err != nil {
log.Error(err)
return
}

ProcessHeresphereUpdates(&scene, requestData, videoFiles[0])

features := make(map[string]bool, 30)
addFeatureTag := func(feature string) {
if !features[feature] {
Expand All @@ -235,12 +274,6 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf

var media []HeresphereMedia

var videoFiles []models.File
videoFiles, err = scene.GetVideoFiles()
if err != nil {
log.Error(err)
return
}
videoLength := float64(scene.Duration)

for i, file := range videoFiles {
Expand Down Expand Up @@ -302,8 +335,8 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
}
tags = append(tags, HeresphereTag{
Name: scene.Cuepoints[i].Name,
StartMilliseconds: start,
EndMilliseconds: end,
StartMilliseconds: float64(start),
EndMilliseconds: float64(end),
Track: &track,
})
}
Expand Down Expand Up @@ -475,15 +508,180 @@ func (i HeresphereResource) getHeresphereScene(req *restful.Request, resp *restf
Scripts: heresphereScriptFiles,
Tags: tags,
Media: media,
WriteFavorite: config.Config.Interfaces.Heresphere.AllowFavoriteUpdates,
WriteRating: config.Config.Interfaces.Heresphere.AllowRatingUpdates,
WriteTags: config.Config.Interfaces.Heresphere.AllowTagUpdates || config.Config.Interfaces.Heresphere.AllowCuepointUpdates || config.Config.Interfaces.Heresphere.AllowWatchlistUpdates,
WriteHSP: config.Config.Interfaces.Heresphere.AllowHspData,
}

if scene.HasVideoPreview {
video.ThumbnailVideo = fmt.Sprintf("http://%v/api/dms/preview/%v", req.Request.Host, scene.SceneID)
}

resp.WriteHeaderAndEntity(http.StatusOK, video)
}

var lockHeresphereUpdates sync.Mutex

func ProcessHeresphereUpdates(scene *models.Scene, requestData HereSphereAuthRequest, videoFile models.File) {

db, _ := models.GetDB()
defer db.Close()

updateReqd := false
if requestData.IsFavorite != nil && *requestData.IsFavorite != scene.Favourite && config.Config.Interfaces.Heresphere.AllowFavoriteUpdates {
scene.Favourite = *requestData.IsFavorite
updateReqd = true
}
if requestData.Rating != nil && *requestData.Rating != scene.StarRating && config.Config.Interfaces.Heresphere.AllowRatingUpdates {
scene.StarRating = *requestData.Rating
updateReqd = true
}

if requestData.Tags != nil && (config.Config.Interfaces.Heresphere.AllowTagUpdates || config.Config.Interfaces.Heresphere.AllowCuepointUpdates || config.Config.Interfaces.Heresphere.AllowWatchlistUpdates) {
// need lock, heresphere can send a second post too soon
lockHeresphereUpdates.Lock()
defer lockHeresphereUpdates.Unlock()
}
if requestData.Tags != nil && config.Config.Interfaces.Heresphere.AllowTagUpdates {
var newTags []string

// need to reread the tags, to handle muti threading issues and the scene record may have changed
// just preload the tags, preload all associations and the scene, does not reread the tags?, so just get them and update the scene
var tmp models.Scene
db.Preload("Tags").Where("id = ?", scene.ID).First(&tmp)
scene.Tags = tmp.Tags

for _, tag := range *requestData.Tags {
if strings.HasPrefix(strings.ToLower(tag.Name), "category:") {
newTags = append(newTags, tag.Name[9:])
}
}
ProcessTagChanges(scene, &newTags, db)
updateReqd = true
}

if requestData.Tags != nil && config.Config.Interfaces.Heresphere.AllowWatchlistUpdates {
// need to reread the tags, to handle muti threading issues and the scene record may have changed
// just preload the tags, preload all associations and the scene, does not reread the tags?, so just get them and update the scene
var tmp models.Scene
db.Preload("Tags").Where("id = ?", scene.ID).First(&tmp)
scene.Tags = tmp.Tags

watchlist := false
for _, tag := range *requestData.Tags {
if strings.HasPrefix(strings.ToLower(tag.Name), "feature:watchlist") {
watchlist = true
}
}
if scene.Watchlist != watchlist {
scene.Watchlist = watchlist
updateReqd = true
}
}

if requestData.Tags != nil && config.Config.Interfaces.Heresphere.AllowCuepointUpdates {
// need to reread the cuepoints, to handle muti threading issues and the scene record may have changed
// just preload the cuepoint, preload all associations and the scene, does not reread the cuepoint?, so just get them and update the scene
var tmp models.Scene
db.Preload("Cuepoints").Where("id = ?", scene.ID).First(&tmp)
scene.Cuepoints = tmp.Cuepoints

var replacementCuepoints []models.SceneCuepoint
endpos := findEndPos(requestData)
firstTrack := findTheMainTrack(requestData)
for _, tag := range *requestData.Tags {
if !strings.Contains(tag.Name, ":") {
if *tag.Track == firstTrack {
replacementCuepoints = append(replacementCuepoints, models.SceneCuepoint{SceneID: scene.ID, TimeStart: float64(tag.StartMilliseconds) / 1000, Name: tag.Name})
} else {
//allow for multi track, merge into the main cuepoint name
if tag.StartMilliseconds > 0 || tag.EndMilliseconds < endpos {
for idx, newtag := range replacementCuepoints {
// allow 5 seconds lewway to align manually entered tags
if math.Abs((newtag.TimeStart)-tag.StartMilliseconds/1000) < 5 {
replacementCuepoints[idx].Name = tag.Name + "-" + replacementCuepoints[idx].Name
}
}
}
}
}
}
db.Model(&scene).Association("Cuepoints").Replace(&replacementCuepoints)

updateReqd = true
}

if requestData.DeleteFiles != nil && config.Config.Interfaces.Heresphere.AllowFileDeletes {
for _, sceneFile := range scene.Files {
removeFileByFileId(sceneFile.ID)
}
}

if requestData.Hsp != nil && config.Config.Interfaces.Heresphere.AllowHspData {
hspContent, err := base64.StdEncoding.DecodeString(*requestData.Hsp)
if err != nil {
log.Error("Error decoding heresphere hsp data %v", err)
}

fName := filepath.Join(scene.Files[0].Path, strings.TrimSuffix(scene.Files[0].Filename, filepath.Ext(videoFile.Filename))+".hsp")
ioutil.WriteFile(fName, hspContent, 0644)

tasks.ScanLocalHspFile(fName, videoFile.VolumeID, scene.ID)
}

if updateReqd {
scene.Save()
}
}
func findTheMainTrack(requestData HereSphereAuthRequest) int {
// 99% of the time we want Track 0, but the user may have deleted and added whole track

// find the max duration
endpos := findEndPos(requestData)
for _, tag := range *requestData.Tags {
if endpos < tag.EndMilliseconds {
endpos = tag.EndMilliseconds
}
}

// find the best track
likelyTrack := 9999
alternateTrack := 9999

for _, tag := range *requestData.Tags {
if (tag.StartMilliseconds > 0 || tag.EndMilliseconds < endpos) && !strings.Contains(tag.Name, ":") {
return *tag.Track
}

if (tag.StartMilliseconds > 0 || tag.EndMilliseconds < endpos) && likelyTrack > *tag.Track {
likelyTrack = *tag.Track
}
if !strings.Contains(tag.Name, ":") && alternateTrack > *tag.Track {
alternateTrack = *tag.Track
}
}

if likelyTrack < 9999 {
return likelyTrack
}

if alternateTrack < 9999 {
return likelyTrack
}

return -1
}
func findEndPos(requestData HereSphereAuthRequest) float64 {
// find the max duration
endpos := float64(0)
for _, tag := range *requestData.Tags {
if endpos < tag.EndMilliseconds {
endpos = tag.EndMilliseconds
}
}
return endpos
}

func (i HeresphereResource) getHeresphereLibrary(req *restful.Request, resp *restful.Response) {
if !config.Config.Interfaces.DeoVR.Enabled {
return
Expand Down
Loading

0 comments on commit b7e6781

Please sign in to comment.