diff --git a/.cache_ggshield b/.cache_ggshield index 3dda68e..d67972f 100644 --- a/.cache_ggshield +++ b/.cache_ggshield @@ -1 +1 @@ -{"last_found_secrets": [{"match": "a5cd2a1a54ccc453d07fae38adafb7b0791b4824afad85d962266e48a54be75a", "name": "Base64 Basic Authentication - commit://staged/services/tracker-helper/test/app.test.ts"}]} \ No newline at end of file +{"last_found_secrets": [{"name": "Generic High Entropy Secret - commit://staged/services/our/src/client/vod.js", "match": "3baceea0f1ce69c8007c0619c3e518cfd24135212b012f3b4d99e82fb1cced6f"}, {"name": "Generic High Entropy Secret - commit://staged/services/our/src/client/vod.js", "match": "4d24f894db33032a2666656e35e34fe3281490468e350e0be5aa8b0faabef682"}]} \ No newline at end of file diff --git a/playbooks/do-nothing/publish.abs b/playbooks/do-nothing/publish.abs index a9ab77f..06c28a8 100644 --- a/playbooks/do-nothing/publish.abs +++ b/playbooks/do-nothing/publish.abs @@ -20,8 +20,7 @@ echo(" * Remote pin the IPFS CID") echo(" [Press Enter When Complete...]") _ = stdin() -echo(" * Create magnet link. ex: `mktorrent --web-seed https://futureporn-b2.b-cdn.net/projektmelody-fansly-2025-08-27.mp4 ~/Documents/voddo/projektmelody-fansly-2025-08-27.mp4`") -echo(" * Create magnet link. ex: `torf ~/Downloads/projektmelody-chaturbate-2025-08-27.mp4 --notorrent --notracker`") +echo(" * Create magnet link. ex: `imdl torrent create --link ~/Documents/voddo/projektmelody-fansly-2025-08-27.mp4`") echo(" [Press Enter When Complete...]") _ = stdin() diff --git a/services/our/Dockerfile b/services/our/Dockerfile index 2660303..4b97aa9 100644 --- a/services/our/Dockerfile +++ b/services/our/Dockerfile @@ -39,6 +39,9 @@ ENV LD_LIBRARY_PATH="/app/whisper.cpp/build/src:/app/whisper.cpp/build/ggml/src: # Install b2-cli RUN wget https://github.com/Backblaze/B2_Command_Line_Tool/releases/download/v4.4.1/b2-linux -O /usr/local/bin/b2 && chmod +x /usr/local/bin/b2 +# Install imdl +RUN wget https://github.com/casey/intermodal/releases/download/v0.1.14/imdl-v0.1.14-x86_64-unknown-linux-musl.tar.gz -O /tmp/imdl.tar.gz && tar xvzf /tmp/imdl.tar.gz && cp /tmp/imdl /usr/local/bin/imdl + # Copy and install dependencies COPY package.json package-lock.json ./ RUN npm install --ignore-scripts=false --foreground-scripts --verbose diff --git a/services/our/package-lock.json b/services/our/package-lock.json index d5e013c..490b102 100644 --- a/services/our/package-lock.json +++ b/services/our/package-lock.json @@ -70,6 +70,7 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/js": "^9.31.0", + "buttplug": "^3.2.2", "chokidar": "^4.0.3", "esbuild": "^0.25.9", "esbuild-copy-static-files": "^0.1.0", @@ -5527,6 +5528,19 @@ "esbuild": ">=0.18" } }, + "node_modules/buttplug": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/buttplug/-/buttplug-3.2.2.tgz", + "integrity": "sha512-TGkQzG6dxEjuFX29eRoWkh82vsQhGQ+E98tZtN8fWn1NOG7v/9H0FFkNXrpmeRt9FFS0GdHTvubfZ8dcIPGSAA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "class-transformer": "^0.5.1", + "eventemitter3": "^5.0.1", + "reflect-metadata": "^0.2.1", + "ws": "^8.16.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -5956,6 +5970,13 @@ "node": ">=18" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", @@ -7057,6 +7078,13 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -10269,6 +10297,13 @@ "node": ">= 12.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/require-addon": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz", @@ -12258,6 +12293,28 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/services/our/package.json b/services/our/package.json index 9dcf82c..bfd0f3f 100644 --- a/services/our/package.json +++ b/services/our/package.json @@ -26,6 +26,7 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/js": "^9.31.0", + "buttplug": "^3.2.2", "chokidar": "^4.0.3", "esbuild": "^0.25.9", "esbuild-copy-static-files": "^0.1.0", @@ -110,4 +111,4 @@ "prisma": { "seed": "tsx prisma/seed.ts" } -} \ No newline at end of file +} diff --git a/services/our/src/assets/js/buttplug/index.js b/services/our/src/assets/js/buttplug/index.js deleted file mode 100644 index 5af3f49..0000000 --- a/services/our/src/assets/js/buttplug/index.js +++ /dev/null @@ -1,269 +0,0 @@ - -// {{!-- -// Script 1: Load Buttplug.js from Skypack CDN and expose it to window.buttplug -// --}} -// - - - - - - -{{!-- -{{!-- -Script 3: Main ButtplugPlugin class — handles connection, syncing, and device control ---}} - - ---}} \ No newline at end of file diff --git a/services/our/src/client/downloader/download.js b/services/our/src/client/downloader/download.js new file mode 100644 index 0000000..914f58a --- /dev/null +++ b/services/our/src/client/downloader/download.js @@ -0,0 +1,31 @@ +/** + * Downloads a file from a given CDN URL and triggers a browser download. + * + * @async + * @function download + * @param {string} cdnUrl - The URL of the file to download. + * @param {string} fileName - The name to give the downloaded file (including extension). + * @returns {Promise} A promise that resolves once the download has been triggered. + * + * @example + * // Download an image and save as picture.jpg + * await download('https://example.com/image.jpg', 'picture.jpg'); + */ +export async function download(cdnUrl, fileName) { + console.log(`downloading cdnUrl=${cdnUrl} fileName=${fileName}`) + try { + const response = await fetch(cdnUrl); + if (!response.ok) throw new Error('Network response was not ok'); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('There was a problem with the fetch operation:', error); + } +} diff --git a/services/our/src/client/videojs-buttplug/ConcreteButton.js b/services/our/src/client/videojs-buttplug/ConcreteButton.js deleted file mode 100644 index 8510d0e..0000000 --- a/services/our/src/client/videojs-buttplug/ConcreteButton.js +++ /dev/null @@ -1,86 +0,0 @@ -import videojs from 'video.js' - -const MenuButton = videojs.getComponent('MenuButton'); -const Menu = videojs.getComponent('Menu'); -const Component = videojs.getComponent('Component'); -const Dom = videojs.dom; - -/** - * Convert string to title case. - * - * @param {string} string - the string to convert - * @return {string} the returned titlecase string - */ -function toTitleCase(string) { - if (typeof string !== 'string') { - return string; - } - - return string.charAt(0).toUpperCase() + string.slice(1); -} - -/** - * Extend vjs button class for funscripts button. - */ -export default class ConcreteButton extends MenuButton { - - /** - * Button constructor. - * - * @param {Player} player - videojs player instance - */ - constructor(player) { - super(player, { - title: player.localize('Funscript'), - name: 'ScriptButton' - }); - - } - - /** - * Creates button items. - * - * @return {Array} - Button items - */ - createItems() { - return []; - } - - /** - * Create the menu and add all items to it. - * - * @return {Menu} - * The constructed menu - */ - createMenu() { - const menu = new Menu(this.player_, { menuButton: this }); - - this.hideThreshold_ = 0; - - // Add a title list item to the top - if (this.options_.title) { - const titleEl = Dom.createEl('li', { - className: 'vjs-menu-title', - innerHTML: toTitleCase(this.options_.title), - tabIndex: -1 - }); - const titleComponent = new Component(this.player_, { el: titleEl }); - - this.hideThreshold_ += 1; - - menu.addItem(titleComponent); - } - - this.items = this.createItems(); - - if (this.items) { - // Add menu items to the menu - for (let i = 0; i < this.items.length; i++) { - menu.addItem(this.items[i]); - } - } - - return menu; - - } -} \ No newline at end of file diff --git a/services/our/src/client/videojs-buttplug/ConcreteMenuItem.js b/services/our/src/client/videojs-buttplug/ConcreteMenuItem.js deleted file mode 100644 index 043fa54..0000000 --- a/services/our/src/client/videojs-buttplug/ConcreteMenuItem.js +++ /dev/null @@ -1,44 +0,0 @@ -import videojs from 'video.js' -// Concrete classes -const VideoJsMenuItemClass = videojs.getComponent('MenuItem'); - -/** - * Extend vjs menu item class. - */ -export default class ConcreteMenuItem extends VideoJsMenuItemClass { - - /** - * Menu item constructor. - * - * @param {Player} player - vjs player - * @param {Object} item - Item object - * @param {ConcreteButton} scriptButton - The containing button. - * @param {FunscriptSelector} plugin - This plugin instance. - */ - constructor(player, item, scriptButton, plugin) { - super(player, { - label: item.label, - selectable: true, - selected: item.selected || false - }); - this.item = item; - this.scriptButton = scriptButton; - this.plugin = plugin; - } - - /** - * Click event for menu item. - */ - handleClick() { - - // Reset other menu items selected status. - for (let i = 0; i < this.scriptButton.items.length; ++i) { - this.scriptButton.items[i].selected(false); - } - - // Set this menu item to selected, and set quality. - this.plugin.setFunscript(this.item.value); - this.selected(true); - - } -} \ No newline at end of file diff --git a/services/our/src/client/videojs-buttplug/README.md b/services/our/src/client/videojs-buttplug/README.md deleted file mode 100644 index 6c4bdde..0000000 --- a/services/our/src/client/videojs-buttplug/README.md +++ /dev/null @@ -1 +0,0 @@ -inspired by https://github.com/chrisboustead/videojs-hls-quality-selector/tree/main diff --git a/services/our/src/client/videojs-buttplug/plugin.js b/services/our/src/client/videojs-buttplug/plugin.js deleted file mode 100644 index 64b7980..0000000 --- a/services/our/src/client/videojs-buttplug/plugin.js +++ /dev/null @@ -1,133 +0,0 @@ -import videojs from 'video.js' -import ConcreteButton from './ConcreteButton.js'; -import ConcreteMenuItem from './ConcreteMenuItem.js'; -import './plugin.css' - -const VERSION = "0.0.1" -const Plugin = videojs.getPlugin('plugin'); -const defaults = {}; - -/** - * Funscript Selector Plugin - */ -class FunscriptSelector extends Plugin { - - constructor(player, options) { - super(player); - - this.options = videojs.obj.merge(defaults, options); - - this.player.ready(() => { - this.player.addClass('vjs-funscript-selector'); - this.createFunscriptButton(); - }); - - // Player event hooks - // this.on(player, 'timeupdate', this.handleTimeUpdate); - this.on(player, 'pause', this.handlePause); - this.on(player, 'playing', this.handlePlay); - this.on(player, 'seeking', this.handleSeek); - this.on(player, 'ended', this.handleEnded); - - - - } - - handlePause = () => { - this.devicePaused = true; - // this.sendToDevice(null); // @todo - }; - - handlePlay = () => { - this.devicePaused = false; - }; - - handleEnded = () => { - this.currentActionIndex = 0; - this.lastPositionSent = null; - this.devicePaused = true; - // this.sendToDevice(null); // @todo - }; - - handleSeek = () => { - if (!this.activeFunscript) return; - const time = this.player.currentTime() * 1000; - this.currentActionIndex = 0; - for (let i = 0; i < this.activeFunscript.length; i++) { - if (this.activeFunscript[i].at > time) break; - this.currentActionIndex = i; - } - this.lastPositionSent = null; - }; - - /** - * Create the funscript menu button - */ - createFunscriptButton() { - const player = this.player; - - this._funscriptButton = new ConcreteButton(player); - - const placementIndex = player.controlBar.children().length - 2; - const buttonInstance = player.controlBar.addChild( - this._funscriptButton, - { componentClass: 'funscriptSelector' }, - this.options.placementIndex || placementIndex - ); - - buttonInstance.addClass('vjs-funscript-selector'); - this._funscriptButton.createItems = () => this.createFunscriptItems(); - this._funscriptButton.update(); - } - - /** - * Create funscript menu items - */ - createFunscriptItems() { - const player = this.player; - - const funscripts = [ - { - label: 'Vibrate', - value: player.el().querySelector('#funscript-vibrate')?.dataset.url, - }, - { - label: 'Thrust', - value: player.el().querySelector('#funscript-thrust')?.dataset.url, - } - ]; - - return funscripts.map(item => this.getFunscriptMenuItem(item)); - } - - /** - * Return a ConcreteMenuItem for the funscript - */ - getFunscriptMenuItem(item) { - return new ConcreteMenuItem(this.player, item, this._funscriptButton, this); - } - - /** - * Called by ConcreteMenuItem when clicked - */ - setFunscript(url) { - this._currentFunscript = url; - - // Trigger a custom event so your Alpine.js or other logic can pick it up - this.player.trigger('funscriptSelected', { url }); - } - - getCurrentFunscript() { - return this._currentFunscript; - } -} - -// Include the version number. -FunscriptSelector.VERSION = VERSION; - - -videojs.registerPlugin('funscripts', FunscriptSelector); - -export default FunscriptSelector; - - diff --git a/services/our/src/client/videojs-funscripts/Funscripts.js b/services/our/src/client/videojs-funscripts/Funscripts.js new file mode 100644 index 0000000..22a7dac --- /dev/null +++ b/services/our/src/client/videojs-funscripts/Funscripts.js @@ -0,0 +1,353 @@ +import videojs from 'video.js'; +import { ButtplugClient, ButtplugBrowserWebsocketClientConnector } from 'buttplug'; +import './plugin.css'; + +const MenuButton = videojs.getComponent('MenuButton'); +const VideoJsMenuItem = videojs.getComponent('MenuItem'); + +/* ------------------------- Menu Components ------------------------- */ + +class ActuatorMenuButton extends MenuButton { + constructor(player, { actuator, funscripts = [], onAssign } = {}) { + super(player, { + title: actuator?.name ?? 'Actuator', + name: 'ActuatorMenuButton', + actuator, + funscripts, + onAssign, + }); + } + + createItems() { + const { actuator, funscripts, onAssign } = this.options_ || {}; + if (!actuator || !funscripts?.length) return []; + + return funscripts.map(funscript => + new FunscriptMenuItem(this.player_, { + actuator, + funscript, + onAssign, + }) + ); + } +} + +class FunscriptMenuItem extends VideoJsMenuItem { + constructor(player, { actuator, funscript, onAssign } = {}) { + super(player, { label: funscript.name, selectable: true }); + + this.actuator = actuator; + this.funscript = funscript; + this.onAssign = onAssign; + + // Pre-select if this actuator already has this funscript assigned + this.selected(this.actuator.assignedFunscript?.url === this.funscript.url); + + this.on('click', () => { + if (!this.actuator || !this.onAssign) return; + + // Assign this funscript + this.actuator.assignedFunscript = this.funscript; + + // Update UI: only this item is selected + this.parentComponent_.children() + .filter(c => c instanceof FunscriptMenuItem) + .forEach(c => c.selected(c === this)); + + // Call external callback + this.onAssign?.(this, this.actuator, this.funscript); + }); + } +} + +/* ------------------------- Helper ------------------------- */ +function pickInitialFunscript(actuatorType, availableFunscripts) { + actuatorType = actuatorType.toLowerCase(); + + if (actuatorType.includes('vibrator')) { + return availableFunscripts.find(f => f.name.includes('vibrate')) ?? availableFunscripts[0]; + } + + if (actuatorType.includes('thrust')) { + return availableFunscripts.find(f => f.name.includes('thrust')) ?? availableFunscripts[0]; + } + + return availableFunscripts[0]; +} + +/* ------------------------- Main Plugin ------------------------- */ + +export default class FunscriptPlayer { + constructor(player, { debug = false, funscripts, ...options } = {}) { + this.player = player; + this.debug = debug; + this.funscriptCache = new Map(); // key: url, value: parsed funscript + // Normalize funscripts: ensure they’re always { name, url } + this.funscripts = (funscripts || []).map(f => { + if (typeof f === 'string') { + // Extract filename from URL + const url = f; + const name = url.split('/').pop().split('?')[0]; // remove query params + return { name, url }; + } + // Already in { name, url } format + return f; + }); + this.options = options; + + this.menuButton = null; + this.devices = []; // track connected devices + + // refresh menu when devices change + player.on('toyConnected', () => this.refreshMenu()); + player.on('toyDisconnected', () => this.refreshMenu()); + player.on('pause', () => this.stopAllDevices()); + } + + async init() { + const connector = new ButtplugBrowserWebsocketClientConnector("ws://localhost:12345"); + this.client = new ButtplugClient(this.options?.buttplugClientName || "Funscripts VideoJS Client"); + + this.client.addListener("deviceadded", device => { + this.addDevice(device); + this.debug && console.log("Device added:", device.name, device); + this.player.trigger('toyConnected'); + }); + + this.client.addListener("deviceremoved", device => { + this.removeDevice(device); + this.debug && console.log("Device removed:", device.name); + this.player.trigger('toyDisconnected'); + }); + + try { + await this.client.connect(connector); + await this.client.startScanning(); + this.initVideoListeners(); + + this.debug && console.log("[buttplug] Connected and scanning"); + } catch (err) { + console.error("[buttplug] Connection error:", err); + } + } + + async loadFunscript(funscript) { + try { + const response = await fetch(funscript.url); + if (!response.ok) throw new Error(`Failed to load ${funscript.name}`); + return await response.json(); + } catch (err) { + console.error("[funscripts] Error loading funscript:", funscript.name, err); + return null; + } + } + + getFunscriptValueAtTime(funscriptData, timeMs) { + if (!funscriptData?.actions?.length) return null; + + this.debug && console.log(`getFunscriptValueAtTime timeMs=${timeMs}`) + + // Find the latest action at or before current time + let lastAction = null; + for (const action of funscriptData.actions) { + if (action.at <= timeMs) { + lastAction = action; + } else { + break; + } + } + return lastAction ? lastAction.pos / 100 : null; // normalize 0–1 + } + + + initVideoListeners() { + + this.player.on('pause', () => { + this.debug && console.log('Video paused. Stopping devices...'); + this.stopAllDevices(); + }); + + + this.player.on('timeupdate', () => { + + if (this.player.paused() || this.player.ended()) return; // exit early if not playing. necessary for seeking while paused + + const currentTime = this.player.currentTime() * 1000; // ms + // this.debug && console.log(`timeupdate currentTime=${currentTime}`) + + this.devices.forEach(device => { + device.actuators.forEach(actuator => { + const fun = actuator.assignedFunscript; + if (!fun) return; + // this.debug && console.log(`name=${fun.name} url=${fun.url}`) + + const funscriptData = this.funscriptCache.get(fun.url); + // console.log(funscriptData) + if (!funscriptData) return; + + const position = this.getFunscriptValueAtTime(funscriptData, currentTime); + // this.debug && console.log(`position=${position}`) + + if (position !== null) { + this.debug && console.log(`actuator.name=${actuator.name}, position=${position}`) + this.sendToDevice(device, actuator, position); + } + }); + }); + }); + } + + async stopAllDevices() { + if (!this.client || !this.client.devices) return; + + for (const [deviceIndex, device] of this.client.devices.entries()) { + const scalarCmds = device.messageAttributes?.ScalarCmd || []; + for (const cmd of scalarCmds) { + try { + await this.sendToDevice(device, { + index: cmd.Index, + name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}` + }, 0); + } catch (err) { + console.warn(`[buttplug] Failed to stop ${device.name}`, err); + } + } + } + } + + + + async sendToDevice(device, actuator, position) { + try { + // console.log() + // await this.client.devices[device.index].scalar(actuator.index, position); + this.client.devices[device.index].vibrate(position) + if (this.debug) { + console.log(`[buttplug] Sent to ${actuator.name}:`, position); + } + } catch (err) { + console.error("[buttplug] Error sending command:", err); + } + } + + + /* ------------------------- Device Tracking ------------------------- */ + + addDevice(device) { + const scalarCmds = device._deviceInfo?.DeviceMessages?.ScalarCmd || []; + + const actuators = scalarCmds.map((cmd, i) => { + const assignedFunscript = pickInitialFunscript(cmd.ActuatorType, this.funscripts); + + // kick off preload asynchronously, but don’t block UI + if (assignedFunscript && !this.funscriptCache.has(assignedFunscript.url)) { + this.loadFunscript(assignedFunscript).then(data => { + if (data) this.funscriptCache.set(assignedFunscript.url, data); + this.debug && console.log("Preloaded funscript:", assignedFunscript.name); + }).catch(err => { + console.error("Error preloading funscript:", assignedFunscript.name, err); + }); + } + + return { + name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`, + type: cmd.ActuatorType, + index: cmd.Index, + assignedFunscript, + }; + }); + + this.devices.push({ + name: device._deviceInfo.DeviceName, + index: this.devices.length, + actuators, + }); + + this.debug && console.log("Processed device:", device._deviceInfo.DeviceName, "actuators:", actuators); + } + + + removeDevice(device) { + this.devices = this.devices.filter(d => d.name !== device.name); + } + + + + /* ------------------------- Menu Management ------------------------- */ + + createMenuButtons() { + // Remove existing menu buttons + if (this.menuButtons?.length) { + this.menuButtons.forEach(btn => this.player.controlBar.removeChild(btn)); + } + this.menuButtons = []; + + const actuators = this.devices.flatMap(device => + device.actuators.map(actuator => ({ + ...actuator, + deviceName: device.name, + deviceIndex: device.index, + })) + ); + + actuators.forEach(actuator => { + const button = new ActuatorMenuButton(this.player, { + actuator, + funscripts: this.funscripts, + onAssign: async (item, act, funscript) => { + act.assignedFunscript = funscript; + this.debug && console.log(`assignedFunscript = ${funscript.name}`); + + // Load and cache funscript + if (!this.funscriptCache.has(funscript.url)) { + const data = await this.loadFunscript(funscript); + console.log('load and cache funscript') // this is never being called by default. + if (data) this.funscriptCache.set(funscript.url, data); + } + + // Update UI + const menuItems = item.parentComponent_.children() + .filter(c => c instanceof FunscriptMenuItem); + + menuItems.forEach(i => { + i.selected(i.funscript.url === funscript.url); + }); + } + }); + + const placementIndex = Math.max( + 0, + this.options?.placementIndex ?? (this.player.controlBar.children().length - 2) + ); + this.player.controlBar.addChild(button, { componentClass: 'funscriptSelector' }, placementIndex); + + setTimeout(() => { + button.addClass('vjs-funscript-selector'); + button.removeClass('vjs-hidden'); + }, 0); + + this.menuButtons.push(button); + }); + + if (this.debug) { + const actuatorNames = this.menuButtons.map(b => b.options_.title); + console.log("Menu buttons created for actuators:", actuatorNames); + } + } + + refreshMenu() { + this.createMenuButtons(); + } +} + +/* ------------------------- Register Plugin ------------------------- */ + +videojs.registerPlugin("funscriptPlayer", async function (options) { + try { + const plugin = new FunscriptPlayer(this, options); + await plugin.init(); + return plugin; + } catch (err) { + console.error("Funscripts plugin failed to initialize:", err); + } +}); diff --git a/services/our/src/client/videojs-funscripts/README.md b/services/our/src/client/videojs-funscripts/README.md new file mode 100644 index 0000000..ebe3e1a --- /dev/null +++ b/services/our/src/client/videojs-funscripts/README.md @@ -0,0 +1 @@ +menu selector code copied from https://github.com/chrisboustead/videojs-hls-quality-selector/tree/main diff --git a/services/our/src/client/videojs-funscripts/deviceMap.js b/services/our/src/client/videojs-funscripts/deviceMap.js new file mode 100644 index 0000000..462e242 --- /dev/null +++ b/services/our/src/client/videojs-funscripts/deviceMap.js @@ -0,0 +1,61 @@ +// deviceMap.js +export default class DeviceMap { + constructor() { + // Map of deviceIndex => { name, funscript, index } + this.devices = new Map(); + } + + // addDevice(device) { + // if (this.devices.has(device.index)) return; + + // this.devices.set(device.index, { + // name: device.name, + // funscript: null, + // }); + + // console.log(`[DeviceMap] Added device "${device.name}", index=${device.index}`); + // } + + addDevice(device) { + const scalarCmds = device._deviceInfo?.DeviceMessages?.ScalarCmd || []; + + const actuators = scalarCmds.map((cmd, i) => { + return { + name: `${device.name} - ${cmd.ActuatorType} #${cmd.Index + 1}`, + type: cmd.ActuatorType, + index: cmd.Index, + reportedValue: cmd.Default ?? 0, // example, could use other reported info + assignedFunscript: null, // initially empty + }; + }); + + this.devices.push({ + name: device._deviceInfo.DeviceName, + index: this.devices.length, + actuators + }); + } + + + removeDevice(device) { + this.devices.delete(device.index); + console.log(`[DeviceMap] Removed device "${device.name}"`); + } + + getDevices() { + // Return array for easy menu mapping + return Array.from(this.devices.entries()).map(([index, { name, funscript }]) => ({ + index, + name, + funscript, + })); + } + + assignFunscript(deviceIndex, funscript) { + const d = this.devices.get(deviceIndex); + if (!d) return; + + d.funscript = funscript; + console.log(`[DeviceMap] Assigned funscript to "${d.name}"`); + } +} diff --git a/services/our/src/client/videojs-funscripts/devices.js b/services/our/src/client/videojs-funscripts/devices.js new file mode 100644 index 0000000..ec80b17 --- /dev/null +++ b/services/our/src/client/videojs-funscripts/devices.js @@ -0,0 +1,35 @@ + + + +export function mapDeviceToFunscriptFeatures(device) { + var mappings = []; + + if (!device || typeof device !== "object") return mappings; + + var attrs = device.messageAttributes || {}; + + // Helper to safely iterate + function forEachFeature(arr, actuatorType, funscriptType) { + if (!arr) return; + try { + // coerce to array in case it's undefined or not an array + var features = Array.isArray(arr) ? arr : []; + features.forEach(function (f, i) { + mappings.push({ + actuatorIndex: typeof f.Index === "number" ? f.Index : i, + actuatorType: actuatorType, + funscriptType: funscriptType, + stepCount: typeof f.StepCount === "number" ? f.StepCount : 100 + }); + }); + } catch (err) { + console.warn("[buttplug] Failed to process features for", actuatorType, err); + } + } + + forEachFeature(attrs.VibrateCmd, "vibrate", "Vibrate"); + forEachFeature(attrs.LinearCmd, "linear", "Oscillate"); + forEachFeature(attrs.RotateCmd, "rotate", "Rotate"); + + return mappings; +} diff --git a/services/our/src/client/videojs-funscripts/dom.js b/services/our/src/client/videojs-funscripts/dom.js new file mode 100644 index 0000000..fc8118c --- /dev/null +++ b/services/our/src/client/videojs-funscripts/dom.js @@ -0,0 +1,11 @@ +export function collectFunscripts() { + return Array.from(document.querySelectorAll('.funscript')) + .map(div => { + const url = div.getAttribute('data-url'); + if (!url) return null; + + const name = url.split('/').pop().split('?')[0]; // filename without query + return { name, url }; + }) + .filter(Boolean); // remove nulls +} \ No newline at end of file diff --git a/services/our/src/client/videojs-buttplug/plugin.css b/services/our/src/client/videojs-funscripts/plugin.css similarity index 89% rename from services/our/src/client/videojs-buttplug/plugin.css rename to services/our/src/client/videojs-funscripts/plugin.css index 7e7fca9..f68b65e 100644 --- a/services/our/src/client/videojs-buttplug/plugin.css +++ b/services/our/src/client/videojs-funscripts/plugin.css @@ -5,6 +5,7 @@ align-items: center; justify-content: center; padding: 0; + color: white; } .vjs-menu-button.vjs-funscript-selector button::before { @@ -12,7 +13,5 @@ display: block; width: 1.5em; height: 1.5em; - background: no-repeat center/contain url("data:image/svg+xml;utf8,"); - color: cyan; - /* SVG fill via currentColor */ + background: no-repeat center/contain url("data:image/svg+xml;utf8,"); } \ No newline at end of file diff --git a/services/our/src/client/vod.js b/services/our/src/client/vod.js index 88e1f9c..e531880 100644 --- a/services/our/src/client/vod.js +++ b/services/our/src/client/vod.js @@ -1,19 +1,33 @@ import videojs from 'video.js' import 'video.js/dist/video-js.min.css' - -// import 'videojs-contrib-quality-levels' +import { download } from './downloader/download.js' +import { collectFunscripts } from './videojs-funscripts/dom.js' import 'videojs-hls-quality-selector' -import './videojs-buttplug/plugin.js' +import './videojs-funscripts/Funscripts.js' + +window.download = download window.HELP_IMPROVE_VIDEOJS = false; // disable videojs tracking -var player = videojs('#player'); + + +const player = videojs('player'); player.ready(() => { - player.funscripts(); - // player.qualityLevels(); + + + // set up plugins + const funscripts = collectFunscripts() + const funscriptsOptions = { + buttplugClientName: "future.porn", + debug: false, + funscripts, + } + + player.funscriptPlayer(funscriptsOptions); + player.hlsQualitySelector({ displayCurrentQuality: true, diff --git a/services/our/src/tasks/scheduleVodProcessing.ts b/services/our/src/tasks/scheduleVodProcessing.ts index 48effec..edf19c2 100644 --- a/services/our/src/tasks/scheduleVodProcessing.ts +++ b/services/our/src/tasks/scheduleVodProcessing.ts @@ -40,16 +40,16 @@ const scheduleVodProcessing: Task = async (payload: unknown, helpers) => { const jobs: Promise[] = []; jobs.push(helpers.addJob("copyV1S3ToV2", { vodId })); - // if (!vod.sourceVideo) jobs.push(helpers.addJob("getSourceVideo", { vodId })); - // if (!vod.sourceVideoDuration) jobs.push(helpers.addJob("getSourceVideoMetadata", { vodId })) - // if (!vod.sha256sum) jobs.push(helpers.addJob("generateVideoChecksum", { vodId })); - // if (!vod.thumbnail) jobs.push(helpers.addJob("createVideoThumbnail", { vodId })); - // if (!vod.hlsPlaylist) jobs.push(helpers.addJob("createHlsPlaylist", { vodId })); - // if (!vod.cidv1) jobs.push(helpers.addJob("createIpfsCid", { vodId })); + if (!vod.sourceVideo) jobs.push(helpers.addJob("getSourceVideo", { vodId })); + if (!vod.sourceVideoDuration) jobs.push(helpers.addJob("getSourceVideoMetadata", { vodId })) + if (!vod.sha256sum) jobs.push(helpers.addJob("generateVideoChecksum", { vodId })); + if (!vod.thumbnail) jobs.push(helpers.addJob("createVideoThumbnail", { vodId })); + if (!vod.hlsPlaylist) jobs.push(helpers.addJob("createHlsPlaylist", { vodId })); + if (!vod.cidv1) jobs.push(helpers.addJob("createIpfsCid", { vodId })); if (!vod.funscriptVibrate || !vod.funscriptThrust) jobs.push(helpers.addJob("createFunscript", { vodId })); - // if (!vod.asrVttKey) jobs.push(helpers.addJob("createTranscription", { vodId })); - // if (!vod.slvttVTTKey) jobs.push(helpers.addJob("createStoryboard", { vodId })); - // if (!vod.magnetLink) jobs.push(helpers.addJob("createTorrent", { vodId })); + if (!vod.asrVttKey) jobs.push(helpers.addJob("createTranscription", { vodId })); + if (!vod.slvttVTTKey) jobs.push(helpers.addJob("createStoryboard", { vodId })); + if (!vod.magnetLink) jobs.push(helpers.addJob("createTorrent", { vodId })); const changes = jobs.length; if (changes > 0) { diff --git a/services/our/src/views/vod.hbs b/services/our/src/views/vod.hbs index ea9dae1..45e251b 100644 --- a/services/our/src/views/vod.hbs +++ b/services/our/src/views/vod.hbs @@ -47,7 +47,10 @@ data-setup='{}' data-playlist="{{signedHlsUrl vod.hlsPlaylist}}"> {{#if (hasRole "supporterTier1" user)}} - {{/if}} + + + + {{/if}} To view this video please enable JavaScript, and consider upgrading to a web browser that @@ -372,27 +375,6 @@ - async function download(cdnUrl, fileName) { - - console.log(`downloading cdnUrl=${cdnUrl} fileName=${fileName}`) - try { - const response = await fetch(cdnUrl); - if (!response.ok) throw new Error('Network response was not ok'); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - } catch (error) { - console.error('There was a problem with the fetch operation:', error); - } - } - - @@ -457,7 +439,6 @@ Script 4: Initialize the buttplug plugin and pass in the funscript URL from the --}} -
To view this video please enable JavaScript, and consider upgrading to a web browser that @@ -372,27 +375,6 @@ - async function download(cdnUrl, fileName) { - - console.log(`downloading cdnUrl=${cdnUrl} fileName=${fileName}`) - try { - const response = await fetch(cdnUrl); - if (!response.ok) throw new Error('Network response was not ok'); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.style.display = 'none'; - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - } catch (error) { - console.error('There was a problem with the fetch operation:', error); - } - } - - @@ -457,7 +439,6 @@ Script 4: Initialize the buttplug plugin and pass in the funscript URL from the --}} -