您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Grab the GUID of a Plex entry on demand
当前为
// ==UserScript== // @name Plex GUID Grabber // @namespace @soitora/plex-guid-grabber // @description Grab the GUID of a Plex entry on demand // @version 3.1.0 // @license MPL-2.0 // @icon https://app.plex.tv/desktop/favicon.ico // @homepageURL https://soitora.com/Plex-GUID-Grabber/ // @include *:32400/* // @include *://plex.*/* // @include https://app.plex.tv/* // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @grant GM_addStyle // @grant GM_getResourceText // @run-at document-end // ==/UserScript== GM_addStyle(`button[id$="-guid-button"] { margin-right: 4px; } button[id$="-guid-button"]:not([id="imdb-guid-button"]):hover img { filter: invert(100%) grayscale(100%) contrast(120%); } button[id="imdb-guid-button"]:hover img { filter: grayscale(100%) contrast(120%); } button[id="imdb-guid-button"] img { width: 30px !important; height: 30px !important; } `); // SweetAlert2 Toast const Toast = Swal.mixin({ toast: true, position: "bottom-right", showConfirmButton: false, timer: 5000, timerProgressBar: true, }); // Variables let buttonContainer = null; let clipboard = null; const siteConfig = { plex: { id: "plex-guid-button", name: "Plex", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/plex.webp", buttonLabel: "Copy Plex GUID", visible: ["album", "artist", "movie", "season", "episode", "show"], }, imdb: { id: "imdb-guid-button", name: "IMDb", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/imdb.webp", buttonLabel: "Open IMDB", visible: ["movie", "show"], }, tmdb: { id: "tmdb-guid-button", name: "TMDB", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/tmdb-small.webp", buttonLabel: "Open TMDB", visible: ["movie", "show"], }, tvdb: { id: "tvdb-guid-button", name: "TVDB", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/tvdb.webp", buttonLabel: "Open TVDB", visible: ["movie", "show"], }, mbid: { id: "musicbrainz-guid-button", name: "MusicBrainz", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/musicbrainz.webp", buttonLabel: "Open MusicBrainz", visible: ["album", "artist"], }, anidb: { id: "anidb-guid-button", name: "AniDB", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/anidb.webp", buttonLabel: "Open AniDB", visible: ["show", "movie"], }, youtube: { id: "youtube-guid-button", name: "YouTube", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/youtube.webp", buttonLabel: "Open YouTube", visible: ["movie", "show", "episode"], }, }; // Initialize console.log("\x1b[36mPGG", "🔍 Plex GUID Grabber"); function handleButtons(metadata, pageType, guid) { const buttonContainer = $(document).find(".PageHeaderRight-pageHeaderRight-j9Yjqh"); console.debug("\x1b[36mPGG \x1b[32mDebug", "Button container found:", buttonContainer.length > 0); // Check if container exists or button already exists if (!buttonContainer.length || $("#" + siteConfig.plex.id).length) return; const buttons = Object.keys(siteConfig).reduce((acc, site) => { acc[site] = { handler: (event) => handleButtonClick(event, site, guid[site], pageType, metadata), config: siteConfig[site], }; return acc; }, {}); Object.entries(buttons).forEach(([site, { handler, config }]) => { if (siteConfig[site].visible.includes(pageType)) { const $button = $("<button>", { id: config.id, "aria-label": config.buttonLabel, class: "_1v4h9jl0 _76v8d62 _76v8d61 _76v8d68 tvbry61 _76v8d6g _76v8d6h _1v25wbq1g _1v25wbq18", css: { marginRight: "8px", display: guid[site] ? "block" : "none", opacity: 0, transition: "opacity 0.3s ease-in-out", }, html: ` <div class="_1h4p3k00 _1v25wbq8 _1v25wbq1w _1v25wbq1g _1v25wbq1c _1v25wbq14 _1v25wbq3g _1v25wbq2g"> <img src="${config.icon}" alt="${config.buttonLabel}" title="${config.buttonLabel}" style="width: 32px; height: 32px;"> </div> `, }).on("click", (e) => handler(e)); buttonContainer.prepend($button); setTimeout(() => { $button.css("opacity", 1); }, 50); } }); } async function handleButtonClick(event, site, guid, pageType, metadata) { console.debug("\x1b[36mPGG \x1b[32mDebug", "Button clicked:", site, guid, pageType); let title = $(metadata).find("Directory, Video").first(); title = title.attr("parentTitle") || title.attr("title"); const urlMap = { imdb: `https://www.imdb.com/title/${guid}/`, tmdb: pageType === "movie" ? `https://www.themoviedb.org/movie/${guid}` : `https://www.themoviedb.org/tv/${guid}`, tvdb: pageType === "movie" ? `https://www.thetvdb.com/dereferrer/movie/${guid}` : `https://www.thetvdb.com/dereferrer/series/${guid}`, mbid: pageType === "album" ? `https://musicbrainz.org/album/${guid}` : `https://musicbrainz.org/artist/${guid}`, anidb: `https://anidb.net/anime/${guid}`, youtube: `https://www.youtube.com/watch?v=${guid}`, }; const url = urlMap[site]; if (!siteConfig[site].visible.includes(pageType)) { Toast.fire({ icon: "warning", title: `${siteConfig[site].name} links are not available for ${pageType} pages.`, }); return; } if (!guid) { Toast.fire({ icon: "warning", title: `No ${siteConfig[site].name} GUID found for this item.`, }); return; } if (site === "plex") { // Destroy existing clipboard instance if it exists if (clipboard) { clipboard.destroy(); clipboard = null; } // Create new clipboard instance clipboard = new ClipboardJS(`#${siteConfig.plex.id}`, { text: () => guid, }); clipboard.on("success", (e) => { Toast.fire({ icon: "success", title: `Copied ${siteConfig[site].name} guid to clipboard.`, html: `<span><strong>${title}</strong><br>${guid}</span>`, }); e.clearSelection(); }); clipboard.onClick({ currentTarget: $(`#${siteConfig.plex.id}`)[0], }); return; } else if (url) { const ctrlClick = event.ctrlKey || event.metaKey; const newTab = window.open(url, "_blank"); if (!ctrlClick) { newTab.focus(); } Toast.fire({ icon: "success", title: `Opened ${siteConfig[site].name} in a new tab.`, }); } } async function getGuid(metadata) { if (!metadata) return null; const $directory = $(metadata).find("Directory, Video").first(); console.debug("\x1b[36mPGG \x1b[32mDebug", "Directory/Video outerHTML:", $directory[0]?.outerHTML); console.debug("\x1b[36mPGG \x1b[32mDebug", "Directory/Video innerHTML:", $directory[0]?.innerHTML); if (!$directory.length) { console.error("\x1b[36mPGG \x1b[31mError", "Main element not found in XML"); return null; } const guid = { plex: $directory.attr("guid"), imdb: null, tmdb: null, tvdb: null, mbid: null, anidb: null, youtube: null, }; const extractGuid = (service, value) => { const normalizedService = service.toLowerCase(); if (normalizedService.startsWith("tsdb")) { guid.tmdb = value; } else if (guid.hasOwnProperty(normalizedService)) { guid[normalizedService] = value; } }; const plexGuid = guid.plex; if (plexGuid?.startsWith("com.plexapp.agents.hama://")) { const match = plexGuid.match(/com\.plexapp\.agents\.hama:\/\/(\w+)-(\d+)/); if (match) { extractGuid(match[1], match[2]); } } $directory.find("Guid").each(function () { const guidId = $(this).attr("id"); if (!guidId) return; const [service, value] = guidId.split("://"); if (service && value) { extractGuid(service, value); } }); return guid; } async function getLibraryMetadata(metadataPoster) { const img = metadataPoster.find("img").first(); if (!img?.length) return null; const imgSrc = img.attr("src"); if (!imgSrc) return null; const url = new URL(imgSrc); const serverUrl = `${url.protocol}//${url.host}`; const plexToken = url.searchParams.get("X-Plex-Token"); const urlParam = url.searchParams.get("url"); const metadataKey = urlParam?.match(/\/library\/metadata\/(\d+)/)?.[1]; if (!plexToken || !metadataKey) return null; try { const response = await fetch(`${serverUrl}/library/metadata/${metadataKey}?X-Plex-Token=${plexToken}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return new DOMParser().parseFromString(await response.text(), "text/xml"); } catch (error) { console.error("\x1b[36mPGG \x1b[31mError", "Failed to fetch metadata:", error.message); return null; } } async function observeMetadataPoster() { let isObserving = true; const observer = new MutationObserver( debounce(async () => { if (!isObserving) return; if (!window.location.href.includes("%2Flibrary%2Fmetadata%2")) { isObserving = false; console.debug("\x1b[36mPGG \x1b[32mDebug", "Not a metadata page."); return; } const $metadataPoster = $("div[data-testid='metadata-poster']"); console.debug("\x1b[36mPGG \x1b[32mDebug", "Metadata poster found:", $metadataPoster.length > 0); if (!$metadataPoster.length) return; isObserving = false; const metadata = await getLibraryMetadata($metadataPoster); console.debug("\x1b[36mPGG \x1b[32mDebug", "Metadata retrieved:", !!metadata); const pageType = $(metadata).find("Directory, Video").first().attr("type"); let title = $(metadata).find("Directory, Video").first(); title = title.attr("parentTitle") || title.attr("title"); console.log("\x1b[36mPGG", "Type:", pageType); console.log("\x1b[36mPGG", "Title:", title); if (pageType) { const guid = await getGuid(metadata); console.log("\x1b[36mPGG", "Guid:", guid); if (guid) { handleButtons(metadata, pageType, guid); } } }, 100) ); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-page-type"], }); const handleNavigation = debounce(() => { isObserving = true; console.debug("\x1b[36mPGG \x1b[32mDebug", "Navigation detected - resuming observation."); }, 100); $(window).on("hashchange popstate", handleNavigation); } function debounce(func, wait) { let timeout; return function (...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } $(document).ready(observeMetadataPoster);