Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow serving of interactive CSVs directly to Handy #3756

Merged
1 change: 1 addition & 0 deletions graphql/documents/data/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
}
handyKey
funscriptOffset
useStashHostedFunscript
}

fragment ConfigDLNAData on ConfigDLNAResult {
Expand Down
4 changes: 4 additions & 0 deletions graphql/schema/types/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ input ConfigInterfaceInput {
handyKey: String
"""Funscript Time Offset"""
funscriptOffset: Int
"""Whether to use Stash Hosted Funscript"""
useStashHostedFunscript: Boolean
"""True if we should not auto-open a browser window on startup"""
noBrowser: Boolean
"""True if we should send notifications to the desktop"""
Expand Down Expand Up @@ -425,6 +427,8 @@ type ConfigInterfaceResult {
handyKey: String
"""Funscript Time Offset"""
funscriptOffset: Int
"""Whether to use Stash Hosted Funscript"""
useStashHostedFunscript: Boolean
}

input ConfigDLNAInput {
Expand Down
4 changes: 4 additions & 0 deletions internal/api/resolver_mutation_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
c.Set(config.FunscriptOffset, *input.FunscriptOffset)
}

if input.UseStashHostedFunscript != nil {
c.Set(config.UseStashHostedFunscript, *input.UseStashHostedFunscript)
}

if err := c.Write(); err != nil {
return makeConfigInterfaceResult(), err
}
Expand Down
6 changes: 4 additions & 2 deletions internal/api/resolver_query_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
language := config.GetLanguage()
handyKey := config.GetHandyKey()
scriptOffset := config.GetFunscriptOffset()
useStashHostedFunscript := config.GetUseStashHostedFunscript()
imageLightboxOptions := config.GetImageLightboxOptions()
// FIXME - misnamed output field means we have redundant fields
disableDropdownCreate := config.GetDisableDropdownCreate()
Expand Down Expand Up @@ -190,8 +191,9 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
DisabledDropdownCreate: disableDropdownCreate,
DisableDropdownCreate: disableDropdownCreate,

HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
UseStashHostedFunscript: &useStashHostedFunscript,
}
}

Expand Down
15 changes: 15 additions & 0 deletions internal/api/routes_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (rs sceneRoutes) Routes() chi.Router {
r.Get("/vtt/thumbs", rs.VttThumbs)
r.Get("/vtt/sprite", rs.VttSprite)
r.Get("/funscript", rs.Funscript)
r.Get("/interactive_csv", rs.InteractiveCSV)
r.Get("/interactive_heatmap", rs.InteractiveHeatmap)
r.Get("/caption", rs.CaptionLang)

Expand Down Expand Up @@ -374,6 +375,20 @@ func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) {
utils.ServeStaticFile(w, r, filepath)
}

func (rs sceneRoutes) InteractiveCSV(w http.ResponseWriter, r *http.Request) {
s := r.Context().Value(sceneKey).(*models.Scene)
filepath := video.GetFunscriptPath(s.Path)

// TheHandy directly only accepts interactive CSVs
csvBytes, err := manager.ConvertFunscriptToCSV(filepath)

if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
utils.ServeStaticContent(w, r, csvBytes)
}

func (rs sceneRoutes) InteractiveHeatmap(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
Expand Down
10 changes: 8 additions & 2 deletions internal/manager/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,10 @@ const (
DisableDropdownCreateStudio = "disable_dropdown_create.studio"
DisableDropdownCreateTag = "disable_dropdown_create.tag"

HandyKey = "handy_key"
FunscriptOffset = "funscript_offset"
HandyKey = "handy_key"
FunscriptOffset = "funscript_offset"
UseStashHostedFunscript = "use_stash_hosted_funscript"
useStashHostedFunscriptDefault = false

DrawFunscriptHeatmapRange = "draw_funscript_heatmap_range"
drawFunscriptHeatmapRangeDefault = true
Expand Down Expand Up @@ -1260,6 +1262,10 @@ func (i *Instance) GetFunscriptOffset() int {
return i.getInt(FunscriptOffset)
}

func (i *Instance) GetUseStashHostedFunscript() bool {
return i.getBoolDefault(UseStashHostedFunscript, useStashHostedFunscriptDefault)
}

func (i *Instance) GetDeleteFileDefault() bool {
return i.getBool(DeleteFileDefault)
}
Expand Down
1 change: 1 addition & 0 deletions internal/manager/config/config_concurrency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func TestConcurrentConfigAccess(t *testing.T) {
i.Set(CSSEnabled, i.GetCSSEnabled())
i.Set(CSSEnabled, i.GetCustomLocalesEnabled())
i.Set(HandyKey, i.GetHandyKey())
i.Set(UseStashHostedFunscript, i.GetUseStashHostedFunscript())
i.Set(DLNAServerName, i.GetDLNAServerName())
i.Set(DLNADefaultEnabled, i.GetDLNADefaultEnabled())
i.Set(DLNADefaultIPWhitelist, i.GetDLNADefaultIPWhitelist())
Expand Down
61 changes: 61 additions & 0 deletions internal/manager/generator_interactive_heatmap_speed.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package manager

import (
"bytes"
"encoding/json"
"fmt"
"image"
Expand All @@ -11,6 +12,7 @@ import (
"sort"

"github.com/lucasb-eyer/go-colorful"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
)

Expand Down Expand Up @@ -365,3 +367,62 @@ func getSegmentColor(intensity float64) colorful.Color {

return c
}

func LoadFunscriptData(path string) (Script, error) {
data, err := os.ReadFile(path)
if err != nil {
return Script{}, err
}

var funscript Script
err = json.Unmarshal(data, &funscript)
if err != nil {
return Script{}, err
}

if funscript.Actions == nil {
return Script{}, fmt.Errorf("actions list missing in %s", path)
}

sort.SliceStable(funscript.Actions, func(i, j int) bool { return funscript.Actions[i].At < funscript.Actions[j].At })

return funscript, nil
}

func convertRange(value int, fromLow int, fromHigh int, toLow int, toHigh int) int {
return ((value-fromLow)*(toHigh-toLow))/(fromHigh-fromLow) + toLow
}

func ConvertFunscriptToCSV(funscriptPath string) ([]byte, error) {
funscript, err := LoadFunscriptData(funscriptPath)

if err != nil {
return nil, err
}

var buffer bytes.Buffer
for _, action := range funscript.Actions {
pos := action.Pos

if funscript.Inverted {
pos = convertRange(pos, 0, 100, 100, 0)
}

if funscript.Range > 0 {
pos = convertRange(pos, 0, funscript.Range, 0, 100)
}

buffer.WriteString(fmt.Sprintf("%d,%d\r\n", action.At, pos))
}
return buffer.Bytes(), nil
}

func ConvertFunscriptToCSVFile(funscriptPath string, csvPath string) error {
csvBytes, err := ConvertFunscriptToCSV(funscriptPath)

if err != nil {
return err
}

return fsutil.WriteFile(csvPath, csvBytes)
}
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,14 @@ export const SettingsInterfacePanel: React.FC = () => {
value={iface.funscriptOffset ?? undefined}
onChange={(v) => saveInterface({ funscriptOffset: v })}
/>

<BooleanSetting
id="use-stash-hosted-funscript"
headingID="config.ui.use_stash_hosted_funscript.heading"
subHeadingID="config.ui.use_stash_hosted_funscript.description"
checked={iface.useStashHostedFunscript ?? false}
onChange={(v) => saveInterface({ useStashHostedFunscript: v })}
/>
</SettingSection>
</>
);
Expand Down
22 changes: 19 additions & 3 deletions ui/v2.5/src/hooks/Interactive/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const InteractiveProvider: React.FC = ({ children }) => {
undefined
);
const [scriptOffset, setScriptOffset] = useState<number>(0);
const [useStashHostedFunscript, setUseStashHostedFunscript] =
useState<boolean>(false);
const [interactive] = useState<InteractiveAPI>(new InteractiveAPI("", 0));

const [initialised, setInitialised] = useState(false);
Expand Down Expand Up @@ -118,6 +120,9 @@ export const InteractiveProvider: React.FC = ({ children }) => {

setHandyKey(stashConfig.interface.handyKey ?? undefined);
setScriptOffset(stashConfig.interface.funscriptOffset ?? 0);
setUseStashHostedFunscript(
stashConfig.interface.useStashHostedFunscript ?? false
);
}, [stashConfig]);

useEffect(() => {
Expand All @@ -129,11 +134,19 @@ export const InteractiveProvider: React.FC = ({ children }) => {

interactive.handyKey = handyKey ?? "";
interactive.scriptOffset = scriptOffset;
interactive.useStashHostedFunscript = useStashHostedFunscript;

if (oldKey !== interactive.handyKey && interactive.handyKey) {
initialise();
}
}, [handyKey, scriptOffset, config, interactive, initialise]);
}, [
handyKey,
scriptOffset,
useStashHostedFunscript,
config,
interactive,
initialise,
]);

const sync = useCallback(async () => {
if (
Expand Down Expand Up @@ -163,14 +176,17 @@ export const InteractiveProvider: React.FC = ({ children }) => {

setState(ConnectionState.Uploading);
try {
await interactive.uploadScript(funscriptPath);
await interactive.uploadScript(
funscriptPath,
stashConfig?.general?.apiKey
);
setCurrentScript(funscriptPath);
setState(ConnectionState.Ready);
} catch (e) {
setState(ConnectionState.Error);
}
},
[interactive, currentScript]
[interactive, currentScript, stashConfig]
);

return (
Expand Down
37 changes: 29 additions & 8 deletions ui/v2.5/src/hooks/Interactive/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ export class Interactive {
_playing: boolean;
_scriptOffset: number;
_handy: Handy;
_useStashHostedFunscript: boolean;

constructor(handyKey: string, scriptOffset: number) {
this._handy = new Handy();
this._handy.connectionKey = handyKey;
this._scriptOffset = scriptOffset;
this._useStashHostedFunscript = false;
this._connected = false;
this._playing = false;
}
Expand All @@ -127,27 +129,46 @@ export class Interactive {
return this._handy.connectionKey;
}

set useStashHostedFunscript(useStashHostedFunscript: boolean) {
this._useStashHostedFunscript = useStashHostedFunscript;
}

get useStashHostedFunscript(): boolean {
return this._useStashHostedFunscript;
}

set scriptOffset(offset: number) {
this._scriptOffset = offset;
}

async uploadScript(funscriptPath: string) {
async uploadScript(funscriptPath: string, apiKey?: string) {
if (!(this._handy.connectionKey && funscriptPath)) {
return;
}

const csv = await fetch(funscriptPath)
.then((response) => response.json())
.then((json) => convertFunscriptToCSV(json));
const fileName = `${Math.round(Math.random() * 100000000)}.csv`;
const csvFile = new File([csv], fileName);
var funscriptUrl;

const tempURL = await uploadCsv(csvFile).then((response) => response.url);
if (this._useStashHostedFunscript) {
funscriptUrl = funscriptPath.replace("/funscript", "/interactive_csv");
if (typeof apiKey !== "undefined" && apiKey !== "") {
var url = new URL(funscriptUrl);
url.searchParams.append("apikey", apiKey);
funscriptUrl = url.toString();
}
} else {
const csv = await fetch(funscriptPath)
.then((response) => response.json())
.then((json) => convertFunscriptToCSV(json));
const fileName = `${Math.round(Math.random() * 100000000)}.csv`;
const csvFile = new File([csv], fileName);

funscriptUrl = await uploadCsv(csvFile).then((response) => response.url);
}

await this._handy.setMode(HandyMode.hssp);

this._connected = await this._handy
.setHsspSetup(tempURL)
.setHsspSetup(funscriptUrl)
.then((result) => result === HsspSetupResult.downloaded);
}

Expand Down
6 changes: 5 additions & 1 deletion ui/v2.5/src/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,11 @@
}
}
},
"title": "User Interface"
"title": "User Interface",
"use_stash_hosted_funscript": {
"description": "When enabled, funscripts will be served directly from Stash to your Handy device without using the third party Handy server. Requires that Stash be accessible from your Handy device.",
"heading": "Serve funscripts directly"
}
}
},
"configuration": "Configuration",
Expand Down
Loading