ive-connect 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/device-interface.d.ts +55 -15
- package/dist/core/device-interface.js +1 -0
- package/dist/core/device-manager.d.ts +24 -7
- package/dist/core/device-manager.js +75 -25
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.js +23 -0
- package/dist/core/script-loader.d.ts +35 -0
- package/dist/core/script-loader.js +172 -0
- package/dist/devices/autoblow/autoblow-device.d.ts +87 -0
- package/dist/devices/autoblow/autoblow-device.js +377 -0
- package/dist/devices/autoblow/types.d.ts +28 -0
- package/dist/devices/autoblow/types.js +2 -0
- package/dist/devices/buttplug/buttplug-api.d.ts +1 -0
- package/dist/devices/buttplug/buttplug-api.js +36 -0
- package/dist/devices/buttplug/buttplug-device.d.ts +7 -7
- package/dist/devices/buttplug/buttplug-device.js +26 -97
- package/dist/devices/buttplug/command-helpers.js +6 -3
- package/dist/devices/buttplug/types.d.ts +4 -7
- package/dist/devices/handy/handy-api.d.ts +82 -4
- package/dist/devices/handy/handy-api.js +290 -2
- package/dist/devices/handy/handy-device.d.ts +94 -9
- package/dist/devices/handy/handy-device.js +393 -129
- package/dist/devices/handy/types.d.ts +40 -7
- package/dist/devices/handy/types.js +12 -0
- package/dist/devices/index.d.ts +2 -0
- package/dist/devices/index.js +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +9 -3
- package/package.json +3 -2
|
@@ -16,6 +16,7 @@ export declare enum DeviceCapability {
|
|
|
16
16
|
VIBRATE = "vibrate",
|
|
17
17
|
ROTATE = "rotate",
|
|
18
18
|
LINEAR = "linear",
|
|
19
|
+
OSCILLATE = "oscillate",
|
|
19
20
|
STROKE = "stroke"
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
@@ -27,7 +28,7 @@ export interface DeviceInfo {
|
|
|
27
28
|
type: string;
|
|
28
29
|
firmware?: string;
|
|
29
30
|
hardware?: string;
|
|
30
|
-
[key: string]:
|
|
31
|
+
[key: string]: unknown;
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Device settings interface
|
|
@@ -37,22 +38,60 @@ export interface DeviceSettings {
|
|
|
37
38
|
id: string;
|
|
38
39
|
name: string;
|
|
39
40
|
enabled: boolean;
|
|
40
|
-
[key: string]:
|
|
41
|
+
[key: string]: unknown;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* Funscript action
|
|
45
|
+
*/
|
|
46
|
+
export interface FunscriptAction {
|
|
47
|
+
at: number;
|
|
48
|
+
pos: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Funscript format
|
|
52
|
+
*/
|
|
53
|
+
export interface Funscript {
|
|
54
|
+
actions: FunscriptAction[];
|
|
55
|
+
inverted?: boolean;
|
|
56
|
+
range?: number;
|
|
57
|
+
version?: string;
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Script data interface - input for loading scripts
|
|
44
63
|
*/
|
|
45
64
|
export interface ScriptData {
|
|
46
65
|
type: string;
|
|
47
66
|
url?: string;
|
|
48
|
-
content?:
|
|
67
|
+
content?: Funscript;
|
|
49
68
|
}
|
|
50
69
|
/**
|
|
51
70
|
* Script options interface
|
|
52
71
|
*/
|
|
53
|
-
export
|
|
72
|
+
export interface ScriptOptions {
|
|
54
73
|
invertScript?: boolean;
|
|
55
|
-
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Result from loading a script to a single device
|
|
77
|
+
*/
|
|
78
|
+
export interface DeviceScriptLoadResult {
|
|
79
|
+
success: boolean;
|
|
80
|
+
error?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Result from loading a script via DeviceManager
|
|
84
|
+
*/
|
|
85
|
+
export interface ScriptLoadResult {
|
|
86
|
+
/** The parsed and processed funscript content */
|
|
87
|
+
funscript: Funscript | null;
|
|
88
|
+
/** Whether the script was successfully fetched/parsed */
|
|
89
|
+
success: boolean;
|
|
90
|
+
/** Error message if fetching/parsing failed */
|
|
91
|
+
error?: string;
|
|
92
|
+
/** Per-device load results */
|
|
93
|
+
devices: Record<string, DeviceScriptLoadResult>;
|
|
94
|
+
}
|
|
56
95
|
/**
|
|
57
96
|
* Common interface for all haptic devices
|
|
58
97
|
*/
|
|
@@ -73,7 +112,7 @@ export interface HapticDevice {
|
|
|
73
112
|
* Connect to the device
|
|
74
113
|
* @param config Optional configuration
|
|
75
114
|
*/
|
|
76
|
-
connect(config?:
|
|
115
|
+
connect(config?: unknown): Promise<boolean>;
|
|
77
116
|
/**
|
|
78
117
|
* Disconnect from the device
|
|
79
118
|
*/
|
|
@@ -88,13 +127,14 @@ export interface HapticDevice {
|
|
|
88
127
|
*/
|
|
89
128
|
updateConfig(config: Partial<DeviceSettings>): Promise<boolean>;
|
|
90
129
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
130
|
+
* Prepare the device to play a script
|
|
131
|
+
* The funscript content is already parsed - device just needs to prepare it
|
|
132
|
+
* (e.g., upload to server for Handy, store in memory for Buttplug)
|
|
133
|
+
*
|
|
134
|
+
* @param funscript The parsed funscript content
|
|
135
|
+
* @param options Script options (e.g., inversion already applied)
|
|
93
136
|
*/
|
|
94
|
-
|
|
95
|
-
success: boolean;
|
|
96
|
-
scriptContent?: ScriptData;
|
|
97
|
-
}>;
|
|
137
|
+
prepareScript(funscript: Funscript, options?: ScriptOptions): Promise<DeviceScriptLoadResult>;
|
|
98
138
|
/**
|
|
99
139
|
* Play the loaded script at the specified time
|
|
100
140
|
* @param timeMs Current time in milliseconds
|
|
@@ -121,11 +161,11 @@ export interface HapticDevice {
|
|
|
121
161
|
* @param event Event name
|
|
122
162
|
* @param callback Callback function
|
|
123
163
|
*/
|
|
124
|
-
on(event: string, callback: (data:
|
|
164
|
+
on(event: string, callback: (data: unknown) => void): void;
|
|
125
165
|
/**
|
|
126
166
|
* Remove event listener
|
|
127
167
|
* @param event Event name
|
|
128
168
|
* @param callback Callback function
|
|
129
169
|
*/
|
|
130
|
-
off(event: string, callback: (data:
|
|
170
|
+
off(event: string, callback: (data: unknown) => void): void;
|
|
131
171
|
}
|
|
@@ -21,5 +21,6 @@ var DeviceCapability;
|
|
|
21
21
|
DeviceCapability["VIBRATE"] = "vibrate";
|
|
22
22
|
DeviceCapability["ROTATE"] = "rotate";
|
|
23
23
|
DeviceCapability["LINEAR"] = "linear";
|
|
24
|
+
DeviceCapability["OSCILLATE"] = "oscillate";
|
|
24
25
|
DeviceCapability["STROKE"] = "stroke";
|
|
25
26
|
})(DeviceCapability || (exports.DeviceCapability = DeviceCapability = {}));
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device Manager
|
|
3
3
|
*
|
|
4
|
-
* Central manager for all haptic devices
|
|
4
|
+
* Central manager for all haptic devices.
|
|
5
|
+
* Handles unified script loading and distribution to devices.
|
|
5
6
|
*/
|
|
6
7
|
import { EventEmitter } from "./events";
|
|
7
|
-
import { HapticDevice, ScriptData, ScriptOptions } from "./device-interface";
|
|
8
|
+
import { HapticDevice, ScriptData, ScriptOptions, ScriptLoadResult, Funscript } from "./device-interface";
|
|
8
9
|
/**
|
|
9
10
|
* Device Manager class
|
|
10
11
|
* Handles registration and control of multiple haptic devices
|
|
11
12
|
*/
|
|
12
13
|
export declare class DeviceManager extends EventEmitter {
|
|
13
14
|
private devices;
|
|
14
|
-
private
|
|
15
|
+
private currentFunscript;
|
|
16
|
+
private currentScriptOptions;
|
|
15
17
|
/**
|
|
16
18
|
* Register a device with the manager
|
|
17
19
|
* @param device Device to register
|
|
@@ -31,6 +33,10 @@ export declare class DeviceManager extends EventEmitter {
|
|
|
31
33
|
* @param deviceId Device ID to retrieve
|
|
32
34
|
*/
|
|
33
35
|
getDevice(deviceId: string): HapticDevice | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Get the currently loaded funscript
|
|
38
|
+
*/
|
|
39
|
+
getCurrentFunscript(): Funscript | null;
|
|
34
40
|
/**
|
|
35
41
|
* Connect to all registered devices
|
|
36
42
|
* @returns Object with success status for each device
|
|
@@ -42,12 +48,19 @@ export declare class DeviceManager extends EventEmitter {
|
|
|
42
48
|
*/
|
|
43
49
|
disconnectAll(): Promise<Record<string, boolean>>;
|
|
44
50
|
/**
|
|
45
|
-
* Load a script
|
|
46
|
-
*
|
|
51
|
+
* Load a script - fetches, parses, and prepares on all connected devices
|
|
52
|
+
*
|
|
53
|
+
* This is the main entry point for loading scripts. It:
|
|
54
|
+
* 1. Fetches and parses the script (once, centrally)
|
|
55
|
+
* 2. Applies any transformations (inversion, sorting)
|
|
56
|
+
* 3. Distributes to all connected devices
|
|
57
|
+
* 4. Returns the funscript along with per-device results
|
|
58
|
+
*
|
|
59
|
+
* @param scriptData Script data to load (URL or content)
|
|
47
60
|
* @param options Options for script loading (e.g., invertScript)
|
|
48
|
-
* @returns
|
|
61
|
+
* @returns ScriptLoadResult with funscript and per-device status
|
|
49
62
|
*/
|
|
50
|
-
|
|
63
|
+
loadScript(scriptData: ScriptData, options?: ScriptOptions): Promise<ScriptLoadResult>;
|
|
51
64
|
/**
|
|
52
65
|
* Start playback on all connected devices
|
|
53
66
|
* @param timeMs Current time in milliseconds
|
|
@@ -68,6 +81,10 @@ export declare class DeviceManager extends EventEmitter {
|
|
|
68
81
|
* @returns Object with success status for each device
|
|
69
82
|
*/
|
|
70
83
|
syncTimeAll(timeMs: number, filter?: number): Promise<Record<string, boolean>>;
|
|
84
|
+
/**
|
|
85
|
+
* Clear the currently loaded script
|
|
86
|
+
*/
|
|
87
|
+
clearScript(): void;
|
|
71
88
|
/**
|
|
72
89
|
* Set up event forwarding from a device to the manager
|
|
73
90
|
* @param device Device to forward events from
|
|
@@ -4,9 +4,11 @@ exports.DeviceManager = void 0;
|
|
|
4
4
|
/**
|
|
5
5
|
* Device Manager
|
|
6
6
|
*
|
|
7
|
-
* Central manager for all haptic devices
|
|
7
|
+
* Central manager for all haptic devices.
|
|
8
|
+
* Handles unified script loading and distribution to devices.
|
|
8
9
|
*/
|
|
9
10
|
const events_1 = require("./events");
|
|
11
|
+
const script_loader_1 = require("./script-loader");
|
|
10
12
|
/**
|
|
11
13
|
* Device Manager class
|
|
12
14
|
* Handles registration and control of multiple haptic devices
|
|
@@ -15,22 +17,29 @@ class DeviceManager extends events_1.EventEmitter {
|
|
|
15
17
|
constructor() {
|
|
16
18
|
super(...arguments);
|
|
17
19
|
this.devices = new Map();
|
|
18
|
-
this.
|
|
20
|
+
this.currentFunscript = null;
|
|
21
|
+
this.currentScriptOptions = null;
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
24
|
* Register a device with the manager
|
|
22
25
|
* @param device Device to register
|
|
23
26
|
*/
|
|
24
27
|
registerDevice(device) {
|
|
25
|
-
|
|
28
|
+
var _a;
|
|
26
29
|
if (this.devices.has(device.id)) {
|
|
27
30
|
return;
|
|
28
31
|
}
|
|
29
32
|
this.devices.set(device.id, device);
|
|
30
|
-
// Forward events from this device
|
|
31
33
|
this.setupDeviceEventForwarding(device);
|
|
32
|
-
// Emit device added event
|
|
33
34
|
this.emit("deviceAdded", device);
|
|
35
|
+
// If we have a script loaded, prepare it on the new device
|
|
36
|
+
if (this.currentFunscript) {
|
|
37
|
+
device
|
|
38
|
+
.prepareScript(this.currentFunscript, (_a = this.currentScriptOptions) !== null && _a !== void 0 ? _a : undefined)
|
|
39
|
+
.catch((error) => {
|
|
40
|
+
console.error(`Error preparing script on newly registered device ${device.id}:`, error);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
44
|
/**
|
|
36
45
|
* Unregister a device from the manager
|
|
@@ -56,6 +65,12 @@ class DeviceManager extends events_1.EventEmitter {
|
|
|
56
65
|
getDevice(deviceId) {
|
|
57
66
|
return this.devices.get(deviceId);
|
|
58
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the currently loaded funscript
|
|
70
|
+
*/
|
|
71
|
+
getCurrentFunscript() {
|
|
72
|
+
return this.currentFunscript;
|
|
73
|
+
}
|
|
59
74
|
/**
|
|
60
75
|
* Connect to all registered devices
|
|
61
76
|
* @returns Object with success status for each device
|
|
@@ -91,36 +106,66 @@ class DeviceManager extends events_1.EventEmitter {
|
|
|
91
106
|
return results;
|
|
92
107
|
}
|
|
93
108
|
/**
|
|
94
|
-
* Load a script
|
|
95
|
-
*
|
|
109
|
+
* Load a script - fetches, parses, and prepares on all connected devices
|
|
110
|
+
*
|
|
111
|
+
* This is the main entry point for loading scripts. It:
|
|
112
|
+
* 1. Fetches and parses the script (once, centrally)
|
|
113
|
+
* 2. Applies any transformations (inversion, sorting)
|
|
114
|
+
* 3. Distributes to all connected devices
|
|
115
|
+
* 4. Returns the funscript along with per-device results
|
|
116
|
+
*
|
|
117
|
+
* @param scriptData Script data to load (URL or content)
|
|
96
118
|
* @param options Options for script loading (e.g., invertScript)
|
|
97
|
-
* @returns
|
|
119
|
+
* @returns ScriptLoadResult with funscript and per-device status
|
|
98
120
|
*/
|
|
99
|
-
async
|
|
100
|
-
|
|
101
|
-
|
|
121
|
+
async loadScript(scriptData, options) {
|
|
122
|
+
// Step 1: Fetch and parse the script centrally
|
|
123
|
+
const loadResult = await (0, script_loader_1.loadScript)(scriptData, options);
|
|
124
|
+
if (!loadResult.success || !loadResult.funscript) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
funscript: null,
|
|
128
|
+
error: loadResult.error,
|
|
129
|
+
devices: {},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Store the loaded script
|
|
133
|
+
this.currentFunscript = loadResult.funscript;
|
|
134
|
+
this.currentScriptOptions = options !== null && options !== void 0 ? options : null;
|
|
135
|
+
// Step 2: Prepare on all connected devices
|
|
136
|
+
const deviceResults = {};
|
|
102
137
|
for (const [id, device] of this.devices.entries()) {
|
|
138
|
+
// Only prepare on connected devices (or buttplug which manages its own connection)
|
|
103
139
|
if (device.isConnected || device.id === "buttplug") {
|
|
104
140
|
try {
|
|
105
|
-
|
|
141
|
+
const result = await device.prepareScript(loadResult.funscript, options);
|
|
142
|
+
deviceResults[id] = result;
|
|
106
143
|
}
|
|
107
144
|
catch (error) {
|
|
108
|
-
console.error(`Error
|
|
109
|
-
|
|
145
|
+
console.error(`Error preparing script on device ${id}:`, error);
|
|
146
|
+
deviceResults[id] = {
|
|
147
|
+
success: false,
|
|
148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
149
|
+
};
|
|
110
150
|
}
|
|
111
151
|
}
|
|
112
152
|
else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (const [id, result] of Object.entries(results)) {
|
|
118
|
-
if (result.scriptContent) {
|
|
119
|
-
transformedResults["script"] = result.scriptContent;
|
|
153
|
+
deviceResults[id] = {
|
|
154
|
+
success: false,
|
|
155
|
+
error: "Device not connected",
|
|
156
|
+
};
|
|
120
157
|
}
|
|
121
|
-
transformedResults[id] = result.success;
|
|
122
158
|
}
|
|
123
|
-
|
|
159
|
+
// Emit event
|
|
160
|
+
this.emit("scriptLoaded", {
|
|
161
|
+
funscript: loadResult.funscript,
|
|
162
|
+
devices: deviceResults,
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
funscript: loadResult.funscript,
|
|
167
|
+
devices: deviceResults,
|
|
168
|
+
};
|
|
124
169
|
}
|
|
125
170
|
/**
|
|
126
171
|
* Start playback on all connected devices
|
|
@@ -193,12 +238,18 @@ class DeviceManager extends events_1.EventEmitter {
|
|
|
193
238
|
}
|
|
194
239
|
return results;
|
|
195
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Clear the currently loaded script
|
|
243
|
+
*/
|
|
244
|
+
clearScript() {
|
|
245
|
+
this.currentFunscript = null;
|
|
246
|
+
this.currentScriptOptions = null;
|
|
247
|
+
}
|
|
196
248
|
/**
|
|
197
249
|
* Set up event forwarding from a device to the manager
|
|
198
250
|
* @param device Device to forward events from
|
|
199
251
|
*/
|
|
200
252
|
setupDeviceEventForwarding(device) {
|
|
201
|
-
// Forward common events
|
|
202
253
|
const eventsToForward = [
|
|
203
254
|
"error",
|
|
204
255
|
"connected",
|
|
@@ -211,7 +262,6 @@ class DeviceManager extends events_1.EventEmitter {
|
|
|
211
262
|
for (const eventName of eventsToForward) {
|
|
212
263
|
device.on(eventName, (data) => {
|
|
213
264
|
this.emit(`device:${device.id}:${eventName}`, data);
|
|
214
|
-
// Also emit a general event for any device
|
|
215
265
|
this.emit(`device:${eventName}`, { deviceId: device.id, data });
|
|
216
266
|
});
|
|
217
267
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
/**
|
|
18
|
+
* Core exports
|
|
19
|
+
*/
|
|
20
|
+
__exportStar(require("./device-interface"), exports);
|
|
21
|
+
__exportStar(require("./device-manager"), exports);
|
|
22
|
+
__exportStar(require("./events"), exports);
|
|
23
|
+
__exportStar(require("./script-loader"), exports);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Loader
|
|
3
|
+
*
|
|
4
|
+
* Centralized script fetching and parsing.
|
|
5
|
+
* Handles fetching from URLs, parsing CSV/JSON, and applying transformations.
|
|
6
|
+
*/
|
|
7
|
+
import { Funscript, ScriptData, ScriptOptions } from "./device-interface";
|
|
8
|
+
/**
|
|
9
|
+
* Parse CSV content to Funscript format
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseCSVToFunscript(csvText: string): Funscript;
|
|
12
|
+
/**
|
|
13
|
+
* Apply inversion to funscript actions
|
|
14
|
+
*/
|
|
15
|
+
export declare function invertFunscript(funscript: Funscript): Funscript;
|
|
16
|
+
/**
|
|
17
|
+
* Validate funscript structure
|
|
18
|
+
*/
|
|
19
|
+
export declare function isValidFunscript(content: unknown): content is Funscript;
|
|
20
|
+
/**
|
|
21
|
+
* Result of loading a script
|
|
22
|
+
*/
|
|
23
|
+
export interface LoadScriptResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
funscript: Funscript | null;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load and parse a script from ScriptData
|
|
30
|
+
*
|
|
31
|
+
* @param scriptData - The script data (URL or content)
|
|
32
|
+
* @param options - Script options (e.g., inversion)
|
|
33
|
+
* @returns Parsed funscript or error
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadScript(scriptData: ScriptData, options?: ScriptOptions): Promise<LoadScriptResult>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Script Loader
|
|
4
|
+
*
|
|
5
|
+
* Centralized script fetching and parsing.
|
|
6
|
+
* Handles fetching from URLs, parsing CSV/JSON, and applying transformations.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.parseCSVToFunscript = parseCSVToFunscript;
|
|
10
|
+
exports.invertFunscript = invertFunscript;
|
|
11
|
+
exports.isValidFunscript = isValidFunscript;
|
|
12
|
+
exports.loadScript = loadScript;
|
|
13
|
+
/**
|
|
14
|
+
* Parse CSV content to Funscript format
|
|
15
|
+
*/
|
|
16
|
+
function parseCSVToFunscript(csvText) {
|
|
17
|
+
const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
18
|
+
const actions = [];
|
|
19
|
+
// Check if there's a header line (contains non-numeric characters in first column)
|
|
20
|
+
let startIndex = 0;
|
|
21
|
+
if (lines.length > 0 && isNaN(parseFloat(lines[0].split(",")[0]))) {
|
|
22
|
+
startIndex = 1;
|
|
23
|
+
}
|
|
24
|
+
// Parse each line
|
|
25
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
26
|
+
const line = lines[i].trim();
|
|
27
|
+
if (!line)
|
|
28
|
+
continue;
|
|
29
|
+
const columns = line.split(",");
|
|
30
|
+
if (columns.length >= 2) {
|
|
31
|
+
const at = parseFloat(columns[0].trim());
|
|
32
|
+
const pos = parseFloat(columns[1].trim());
|
|
33
|
+
if (!isNaN(at) && !isNaN(pos)) {
|
|
34
|
+
actions.push({
|
|
35
|
+
at: Math.round(at),
|
|
36
|
+
pos: Math.min(100, Math.max(0, Math.round(pos))),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
actions,
|
|
43
|
+
metadata: { convertedFrom: "csv" },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Apply inversion to funscript actions
|
|
48
|
+
*/
|
|
49
|
+
function invertFunscript(funscript) {
|
|
50
|
+
return {
|
|
51
|
+
...funscript,
|
|
52
|
+
actions: funscript.actions.map((action) => ({
|
|
53
|
+
...action,
|
|
54
|
+
pos: 100 - action.pos,
|
|
55
|
+
})),
|
|
56
|
+
inverted: !funscript.inverted,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate funscript structure
|
|
61
|
+
*/
|
|
62
|
+
function isValidFunscript(content) {
|
|
63
|
+
if (!content || typeof content !== "object") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const obj = content;
|
|
67
|
+
if (!Array.isArray(obj.actions)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// Check that actions have the required properties
|
|
71
|
+
return obj.actions.every((action) => action &&
|
|
72
|
+
typeof action === "object" &&
|
|
73
|
+
typeof action.at === "number" &&
|
|
74
|
+
typeof action.pos === "number");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Load and parse a script from ScriptData
|
|
78
|
+
*
|
|
79
|
+
* @param scriptData - The script data (URL or content)
|
|
80
|
+
* @param options - Script options (e.g., inversion)
|
|
81
|
+
* @returns Parsed funscript or error
|
|
82
|
+
*/
|
|
83
|
+
async function loadScript(scriptData, options) {
|
|
84
|
+
try {
|
|
85
|
+
let funscript;
|
|
86
|
+
if (scriptData.content) {
|
|
87
|
+
// Content already provided
|
|
88
|
+
if (!isValidFunscript(scriptData.content)) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
funscript: null,
|
|
92
|
+
error: "Invalid funscript format: content is not a valid funscript",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
funscript = scriptData.content;
|
|
96
|
+
}
|
|
97
|
+
else if (scriptData.url) {
|
|
98
|
+
// Fetch from URL
|
|
99
|
+
const response = await fetch(scriptData.url);
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
funscript: null,
|
|
104
|
+
error: `Failed to fetch script: ${response.status} ${response.statusText}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const fileExtension = scriptData.url.toLowerCase().split(".").pop();
|
|
108
|
+
if (fileExtension === "csv") {
|
|
109
|
+
const csvText = await response.text();
|
|
110
|
+
funscript = parseCSVToFunscript(csvText);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Assume JSON/funscript
|
|
114
|
+
const text = await response.text();
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(text);
|
|
117
|
+
if (!isValidFunscript(parsed)) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
funscript: null,
|
|
121
|
+
error: "Invalid funscript format: missing or invalid actions array",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
funscript = parsed;
|
|
125
|
+
}
|
|
126
|
+
catch (_a) {
|
|
127
|
+
// Try parsing as CSV if JSON fails
|
|
128
|
+
funscript = parseCSVToFunscript(text);
|
|
129
|
+
if (funscript.actions.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
funscript: null,
|
|
133
|
+
error: "Failed to parse script: not valid JSON or CSV",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
funscript: null,
|
|
143
|
+
error: "Invalid script data: either URL or content must be provided",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Validate we have actions
|
|
147
|
+
if (!funscript.actions || funscript.actions.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
funscript: null,
|
|
151
|
+
error: "Invalid funscript: no actions found",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Apply inversion if requested
|
|
155
|
+
if (options === null || options === void 0 ? void 0 : options.invertScript) {
|
|
156
|
+
funscript = invertFunscript(funscript);
|
|
157
|
+
}
|
|
158
|
+
// Sort actions by timestamp
|
|
159
|
+
funscript.actions.sort((a, b) => a.at - b.at);
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
funscript,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
funscript: null,
|
|
169
|
+
error: `Script loading error: ${error instanceof Error ? error.message : String(error)}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|