diff --git a/plugins/discordPresence/README.md b/plugins/discordPresence/README.md deleted file mode 100644 index 687cc9d6..00000000 --- a/plugins/discordPresence/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Discord Presence - -https://discourse.stashapp.cc/t/discord-presence/1374 - -A plugin which shows the metadata of the currently playing Stash scene as your Discord presence - -## Setup -### Prerequisites to get the plugin working -- Download and run [Discord Presence Server](https://github.com/NotForMyCV/discord-presence-server/releases). You **do not** need any browser extensions. -- Ensure you have CommunityScriptsUILibrary installed in your Stash plugins, if it isn't automatically installed - -#### Why the desktop app? - -This plugin relies on a separate desktop app (Discord RPC Server) running in the background. This is required because only a local app can talk to your Discord client to set a custom presence. The ability to do so from a website/browser is whitelisted by Discord (otherwise any website you visit could change your Discord client presence). Discord RPC Server is an open source application which exposes a websocket connection, so that other browser scripts and extensions (i.e. this plugin) can send presence updates to it, which it then forwards to your Discord client. - - -## Configuration -You can customize almost any part of the activity presence with the plugin options. - -| Presence element | Plugin setting name | Default value (if empty; reverts to: ) | Configuration | -|-----------------------|-------------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Activity name | Custom Discord application ID | `1236860180407521341` (displays "Stash") | Create a new application under your [Discord developer portal](https://discord.com/developers/applications). The name of the application will be the name of the activity being shown as "Playing". Copy the `APPLICATION ID` (20 digit number) from the Developer Portal and set it in the plugin options. | -| Details (first line) | Presence details text | `{title}` | Custom text and variables | -| State (second line) | Presence state text | `from {studio_name}` | Custom text and variables | -| Show activity image | Show presence image | Off | Toggle switch | -| Custom activity image | Custom presence image key | `stashbox` | After creating a Discord app (see first config option) go to your application settings > Rich Presence > Art Assets. Upload your custom image, give it a key name, and put this in the plugin option (takes a short while for the asset to appear after uploading). | -| Activity hover text | Custom image text | Empty | Custom text and variables | -| Show URL button | Show scene URL button | Off | Toggle switch | -| Custom button text | Custom button text | `Watch` | Custom text and variables | - -## String variables -You can insert metadata from the currently playing scene into configurable elements, by enclosing variables in curly braces. -For example, if you were watching a scene called "Kittens" and wanted to display "Watching Kittens" under the presence details, you would set the config option to `Watching {title}`. -Below are a list of available variable names: -- `{id}` -- `{title}` -- `{code}` -- `{details}` -- `{director}` -- `{date}` -- `{rating100}` -- `{o_counter}` -- `{organized}` -- `{interactive}` -- `{interactive_speed}` -- `{created_at}` -- `{updated_at}` -- `{resume_time}` -- `{last_played_at}` -- `{play_duration}` -- `{play_count}` -- `{url}` -- `{studio_name}` -- `{file_duration}` -- `{performers}` diff --git a/plugins/discordPresence/discordPresence.js b/plugins/discordPresence/discordPresence.js deleted file mode 100644 index 07073448..00000000 --- a/plugins/discordPresence/discordPresence.js +++ /dev/null @@ -1,287 +0,0 @@ -(async function () { - /** - * @typedef {{ - * discordClientId?: string; - * discordDetailsText?: string; - * discordStateText?: string; - * discordShowImage?: boolean; - * discordLargeImageKey?: string; - * discordLargeImageText?: string; - * discordShowUrlButton?: boolean; - * discordUrlButtonText?: string; - * }} PluginConfig - */ - - /** - * @typedef {{ - * id, title, code, details, director, urls?: string[], date, rating100, o_counter, - * organized, interactive, interactive_speed, created_at, updated_at, resume_time, - * last_played_at, play_duration, play_count, files: {duration:number}[], - * studio?: {id, name}, performers: {name, gender}[] - * }} SceneData - */ - - /** - * @typedef {{ studio_name: string, url: string, file_duration: string, performers: string } - * & Omit - * } FlattenedSceneData - */ - - const SCENE_GQL_QUERY = ` - query FindScene($id: ID!) { - findScene(id: $id) { - ...SceneData - } - } - - fragment SceneData on Scene { - id - title - code - details - director - urls - date - rating100 - o_counter - organized - interactive - interactive_speed - created_at - updated_at - resume_time - last_played_at - play_duration - play_count - files { duration } - studio { name } - performers { name, gender } - } - `; - - const PLUGIN_ID = "discordPresence"; - - const userConfig = await csLib.getConfiguration(PLUGIN_ID, {}); - console.debug("Discord Presence Plugin: user config", userConfig); - - /** @type {Required} */ - const CONFIG = { - // DEFAULTS - discordClientId: "1236860180407521341", - discordDetailsText: "{title}", - discordStateText: "from {studio_name}", - discordShowImage: false, - discordLargeImageKey: "stashbox", - discordLargeImageText: "Stashapp", - discordShowUrlButton: false, - discordUrlButtonText: "Watch", - ...userConfig, - }; - - console.debug("Discord Presence Plugin: loaded config", CONFIG); - - function throttle(mainFunction, delay) { - let timerFlag = null; - - return (...args) => { - if (timerFlag === null) { - mainFunction(...args); - timerFlag = setTimeout(() => { - timerFlag = null; - }, delay); - } - }; - } - - const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); - const player = () => document.querySelector("#VideoJsPlayer video"); - - let WAITING_FOR_REFRESH = true; - let SCENE_ID = null; - /** @type {FlattenedSceneData?} */ let cachedSceneData; - - /** @type {WebSocket} */ let ws; - const wsAlive = () => ws && ws.readyState === 1; - - // Start ws connection to RPC server and add video listener - // Will retry on disconnection/error after 10s - async function start() { - if (ws && ws.readyState <= 1) { - return; - } - - // https://github.com/NotForMyCV/discord-presence-server/releases - ws = new WebSocket("ws://localhost:6969"); - - ws.addEventListener("open", () => { - csLib.PathElementListener("/scenes/", "video", videoListener); - }); - - window.addEventListener("beforeunload", () => { - clearDiscordActivity(); - }); - - // If failed during video playback, remove the listeners - ws.addEventListener("close", async () => { - if (player()) { - unbindVideoListener(player()); - } - - await sleep(10000); - start(); - }); - - ws.addEventListener("error", async () => { - if (player()) { - unbindVideoListener(player()); - } - - console.error( - `Discord Presence Plugin: Could not connect to Discord Rich Presence Server. - Consult the README on how to setup the Rich Presence Server: - https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence` - ); - await sleep(10000); - start(); - }); - } - - start(); - - /** @return {Promise} */ - async function getSceneData(sceneId) { - if (!sceneId) return null; - - if (Number(sceneId).toString() === Number(cachedSceneData?.id).toString()) { - return cachedSceneData; - } - - const reqData = { - variables: { id: sceneId }, - query: SCENE_GQL_QUERY, - }; - - /** @type {SceneData} */ - const sceneData = await csLib - .callGQL(reqData) - .then((data) => data.findScene); - - if (!sceneData) return null; - - const newProps = { - studio_name: sceneData.studio?.name ?? "Unknown Studio", - url: sceneData.urls?.length ? sceneData.urls[0] : "", - file_duration: sceneData.files?.length ? sceneData.files[0].duration : 0, - performers: sceneData.performers.length - ? sceneData.performers.map((performer) => performer.name).join(", ") - : "Unlisted Performer(s)", - }; - - delete sceneData.urls; - delete sceneData.studio; - delete sceneData.files; - delete sceneData.performers; - - cachedSceneData = { ...sceneData, ...newProps }; - return cachedSceneData; - } - - const clearDiscordActivity = () => { - if (!!SCENE_ID === false || !wsAlive()) { - return; - } - - SCENE_ID = null; - ws.send( - JSON.stringify({ - clientId: CONFIG.discordClientId, - clearActivity: true, - }) - ); - }; - - const setDiscordActivity = throttle(async (event) => { - if (event?.type === "timeupdate") { - if (!WAITING_FOR_REFRESH) { - return; - } - - WAITING_FOR_REFRESH = false; - setTimeout(() => (WAITING_FOR_REFRESH = true), 5000); - } - - const sceneData = await getSceneData(SCENE_ID); - if (!sceneData) return; - - const currentTime = player()?.currentTime ?? 0; - const endTimestamp = - Date.now() + (sceneData.file_duration - currentTime) * 1000; - - let body = { - details: replaceVars(CONFIG.discordDetailsText, sceneData), - state: replaceVars(CONFIG.discordStateText, sceneData), - largeImageKey: CONFIG.discordShowImage - ? CONFIG.discordLargeImageKey - : undefined, - largeImageText: replaceVars(CONFIG.discordLargeImageText, sceneData), - endTimestamp: sceneData.file_duration > 0 ? endTimestamp : undefined, - buttons: - CONFIG.discordShowUrlButton && URL.canParse(sceneData.url) - ? [ - { - label: replaceVars(CONFIG.discordUrlButtonText, sceneData), - url: sceneData.url, - }, - ] - : undefined, - instance: true, - }; - - if (!wsAlive()) { - return; - } - - ws.send( - JSON.stringify({ - clientId: CONFIG.discordClientId, - presence: body, - }) - ); - }, 1000); - - /** - * Performs string replacement on templated config vars with scene data - * @param {string} templateStr - * @param {FlattenedSceneData} sceneData - */ - function replaceVars(templateStr, sceneData) { - const pattern = /{\s*(\w+?)\s*}/g; - - const replacedStr = templateStr - .replace(pattern, (_, token) => sceneData[token] ?? "") - .trim(); - - if (replacedStr.length <= 128) { - return replacedStr; - } - - return replacedStr.substring(0, 125) + "..."; - } - - const videoListener = (video) => { - SCENE_ID = parseInt(location.pathname.split("/")[2]); - video.addEventListener("playing", setDiscordActivity); - video.addEventListener("play", setDiscordActivity); - video.addEventListener("timeupdate", setDiscordActivity); - video.addEventListener("seeked", setDiscordActivity); - video.addEventListener("ended", clearDiscordActivity); - }; - - const unbindVideoListener = (video) => { - video.removeEventListener("playing", setDiscordActivity); - video.removeEventListener("play", setDiscordActivity); - video.removeEventListener("timeupdate", setDiscordActivity); - video.removeEventListener("seeked", setDiscordActivity); - video.removeEventListener("ended", clearDiscordActivity); - }; -})(); diff --git a/plugins/discordPresence/discordPresence.yml b/plugins/discordPresence/discordPresence.yml deleted file mode 100644 index 65cad628..00000000 --- a/plugins/discordPresence/discordPresence.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Discord Presence -description: Sets currently playing scene data as your Discord status. See README for prerequisites and config options (blue hyperlink next to enable/disable button) -url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence -# requires: CommunityScriptsUILibrary -version: 1.3 -settings: - discordClientId: - displayName: Custom Discord application ID - description: Set a custom client ID - type: STRING - discordDetailsText: - displayName: Presence details text - description: Format the first line of your presence text - type: STRING - discordStateText: - displayName: Presence state text - description: Format the second line of your presence text - type: STRING - discordShowImage: - displayName: Show presence image - description: Show the large presence activity image - type: BOOLEAN - discordLargeImageKey: - displayName: Custom presence image key - description: Set a presence image key (requires custom application ID and art asset, see README) - type: STRING - discordLargeImageText: - displayName: Custom image text - description: Format the hover text for the activity image - type: STRING - discordShowUrlButton: - displayName: Show scene URL button - description: Show a presence button which links to the first scene URL - type: BOOLEAN - discordUrlButtonText: - displayName: Custom button text - description: Format the text for the presence button - type: STRING -ui: - requires: - - CommunityScriptsUILibrary - javascript: - - discordPresence.js - csp: - connect-src: - - ws://localhost:6969