diff --git a/custom_javascript/open_scene_in_iina/screenshot.png b/custom_javascript/open_scene_in_iina/screenshot.png new file mode 100644 index 00000000..61429960 Binary files /dev/null and b/custom_javascript/open_scene_in_iina/screenshot.png differ diff --git a/scripts/open_scene_in_iina/README.md b/scripts/open_scene_in_iina/README.md new file mode 100644 index 00000000..cc247f7e --- /dev/null +++ b/scripts/open_scene_in_iina/README.md @@ -0,0 +1,37 @@ +# Open scene in IINA + +Adds a button to open scene in [IINA](https://iina.io/) media player + +![screenshot](screenshot.png) + +## Installation + +Copy [custom.js](https://raw.githubusercontent.com/stashapp/CommunityScripts/main/custom_javascript/open_scene_in_iina/custom.js) and paste in Settings → Interface → Custom Javascript → Edit + +File will be saved as custom.js in your stash configuration + +[https://github.com/stashapp/stash/pull/3132](https://github.com/stashapp/stash/pull/3132) + +## Settings + +| Key | Description | +| - | - | +| `urlScheme` | Protocol to open media player. Only tested with IINA on macOS | +| `replacePath` | Replace docker container path with local path. Default `["", ""]` | + +## Example + +```js +const settings = { + "urlScheme": "iina://weblink?url=file://", + "replacePath": ["/data/", "/Volumes/folder/"], +}; +``` + +## Notes + +Stop Chrome from asking permission to open IINA every time + +```bash +defaults write com.google.Chrome URLAllowlist -array-add "iina://*" +``` diff --git a/scripts/open_scene_in_iina/custom.js b/scripts/open_scene_in_iina/custom.js new file mode 100644 index 00000000..a8983d09 --- /dev/null +++ b/scripts/open_scene_in_iina/custom.js @@ -0,0 +1,133 @@ +// settings +const settings = { + urlScheme: "iina://weblink?url=file://", + replacePath: ["", ""], // example ["/data/", "/Volumes/temp/"], +}; + +// style +const style = document.createElement("style"); +style.innerHTML = ` + .button { + border-radius: 3.5px; + cursor: pointer; + padding: 2px 9px 3px 13px; + } + .button:hover { + background-color: rgba(138, 155, 168, .15); + } + .button svg { + fill: currentColor; + width: 1em; + vertical-align: middle; + } + .button span { + font-size: 15px; + font-weight: 500; + letter-spacing: 0.1em; + color: currentColor; + vertical-align: middle; + margin-left: 3px; + } +`; +document.head.appendChild(style); + +// api +const getFilePath = async (href) => { + const regex = /\/scenes\/(.*)\?/, + sceneId = regex.exec(href)[1], + graphQl = `{ findScene(id: ${sceneId}) { files { path } } }`, + response = await fetch("/graphql", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: graphQl }), + }); + return response.json(); +}; + +// promise +const waitForElm = (selector) => { + return new Promise((resolve) => { + if (document.querySelector(selector)) { + return resolve(document.querySelector(selector)); + } + const observer = new MutationObserver((mutations) => { + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)); + observer.disconnect(); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }); +}; + +// initial +waitForElm("video").then(() => { + addButton(); +}); + +// route +let previousUrl = ""; +const observer = new MutationObserver(function (mutations) { + if (window.location.href !== previousUrl) { + previousUrl = window.location.href; + waitForElm("video").then(() => { + addButton(); + }); + } +}); +const config = { subtree: true, childList: true }; +observer.observe(document, config); + +// main +const addButton = () => { + const scenes = document.querySelectorAll("div.row > div"); + for (const scene of scenes) { + if (scene.querySelector("a.button") === null) { + const scene_url = scene.querySelector("a.scene-card-link"), + popover = scene.querySelector("div.card-popovers"), + button = document.createElement("a"); + button.innerHTML = ` + + + + IINA + `; + button.classList.add("button"); + if (popover) popover.append(button); + button.onmouseover = () => { + if ([button.title.length, button.href.length].indexOf(0) > -1) { + getFilePath(scene_url.href).then((result) => { + const filePath = result.data.findScene.files[0].path.replace( + settings.replacePath[0], + "" + ); + button.title = filePath; + + // replace known characters with issues... + const uriFix = filePath + .replaceAll(" ", "%20") + .replaceAll("[", "%5B") + .replaceAll("]", "%5D") + .replaceAll("-", "%2D"); + + button.href = + settings.urlScheme + + settings.replacePath[1] + + encodeURIComponent(uriFix); + }); + } + }; + } + } +};