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
+
+
+
+## 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);
+ });
+ }
+ };
+ }
+ }
+};