您需要先安装一个扩展,例如 篡改猴、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.4.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/js-yaml/4.1.0/js-yaml.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_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant GM_setClipboard // @run-at document-end // ==/UserScript== GM_addStyle(`button[id$="-guid-button"], button[id$="-yaml-button"] { margin-right: 4px; } button[id$="-guid-button"]:not([id="imdb-guid-button"]):hover img, button[id$="-yaml-button"]:not([id="imdb-yaml-button"]):hover img { filter: invert(100%) grayscale(100%) contrast(120%); } button[id="imdb-guid-button"]:hover img, button[id="imdb-yaml-button"]:hover img { filter: grayscale(100%) contrast(120%); } button[id="imdb-guid-button"] img, button[id="imdb-yaml-button"] img { width: 30px !important; height: 30px !important; } .pgg-toast-container { min-width: 400px !important; max-width: 800px !important; } .pgg-toast-yaml { white-space: pre-wrap; font-family: monospace; } `); // Initialize GM values if they don't exist function initializeGMValues() { if (GM_getValue("USE_SOCIAL_BUTTONS") === undefined) { GM_setValue("USE_SOCIAL_BUTTONS", true); console.log(LOG_PREFIX, "Created USE_SOCIAL_BUTTONS storage"); } if (GM_getValue("SOCIAL_BUTTON_SEPARATION") === undefined) { GM_setValue("SOCIAL_BUTTON_SEPARATION", true); console.log(LOG_PREFIX, "Created SOCIAL_BUTTON_SEPARATION storage"); } if (GM_getValue("USE_PAS") === undefined) { GM_setValue("USE_PAS", false); console.log(LOG_PREFIX, "Created USE_PAS storage"); } if (GM_getValue("TMDB_API_READ_ACCESS_TOKEN") === undefined) { GM_setValue("TMDB_API_READ_ACCESS_TOKEN", ""); console.log(LOG_PREFIX, "Created TMDB_API_READ_ACCESS_TOKEN storage"); } if (GM_getValue("TMDB_LANGUAGE") === undefined) { GM_setValue("TMDB_LANGUAGE", "en-US"); console.log(LOG_PREFIX, "Created TMDB_LANGUAGE storage"); } if (GM_getValue("TVDB_API_KEY") === undefined) { GM_setValue("TVDB_API_KEY", ""); console.log(LOG_PREFIX, "Created TVDB_API_KEY storage"); } if (GM_getValue("TVDB_SUBSCRIBER_PIN") === undefined) { GM_setValue("TVDB_SUBSCRIBER_PIN", ""); console.log(LOG_PREFIX, "Created TVDB_SUBSCRIBER_PIN storage"); } if (GM_getValue("TVDB_LANGUAGE") === undefined) { GM_setValue("TVDB_LANGUAGE", "eng"); console.log(LOG_PREFIX, "Created TVDB_LANGUAGE storage"); } } // SweetAlert2 Toast const Toast = Swal.mixin({ toast: true, position: "bottom-right", showConfirmButton: false, timer: 5000, timerProgressBar: true, width: "auto", customClass: { container: "pgg-toast-container", }, }); // Variables let rightButtonContainer = null; // Constants const LOG_PREFIX = "\x1b[36mPGG"; const DEBUG_PREFIX = "\x1b[36mPGG \x1b[32mDebug"; const ERROR_PREFIX = "\x1b[36mPGG \x1b[31mError"; const DEBOUNCE_DELAY = 100; const BUTTON_FADE_DELAY = 50; const BUTTON_MARGIN = "8px"; // User configuration - Set these values in your userscript manager const USE_SOCIAL_BUTTONS = GM_getValue("USE_SOCIAL_BUTTONS", true); const SOCIAL_BUTTON_SEPARATION = GM_getValue("SOCIAL_BUTTON_SEPARATION", true); const USE_PAS = GM_getValue("USE_PAS", false); const TMDB_API_READ_ACCESS_TOKEN = GM_getValue("TMDB_API_READ_ACCESS_TOKEN", ""); const TMDB_LANGUAGE = GM_getValue("TMDB_LANGUAGE", "en-US"); const TVDB_API_KEY = GM_getValue("TVDB_API_KEY", ""); const TVDB_SUBSCRIBER_PIN = GM_getValue("TVDB_SUBSCRIBER_PIN", ""); const TVDB_LANGUAGE = GM_getValue("TVDB_LANGUAGE", "eng"); // Initialize console.log(LOG_PREFIX, "🔍 Plex GUID Grabber"); initializeGMValues(); 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"], isYamlButton: false, isSocialButton: false, }, imdb: { id: "imdb-social-button", name: "IMDb", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/imdb.webp", buttonLabel: "Open IMDB", visible: ["movie", "show"], isYamlButton: false, isSocialButton: true, }, tmdb: { id: "tmdb-social-button", name: "TMDB", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/tmdb-small.webp", buttonLabel: "Open TMDB", visible: ["movie", "show"], isYamlButton: false, isSocialButton: true, }, tvdb: { id: "tvdb-social-button", name: "TVDB", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/tvdb.webp", buttonLabel: "Open TVDB", visible: ["movie", "show"], isYamlButton: false, isSocialButton: true, }, mbid: { id: "musicbrainz-social-button", name: "MusicBrainz", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/musicbrainz.webp", buttonLabel: "Open MusicBrainz", visible: ["album", "artist"], isYamlButton: false, isSocialButton: true, }, anidb: { id: "anidb-social-button", name: "AniDB", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/anidb.webp", buttonLabel: "Open AniDB", visible: ["show", "movie"], isYamlButton: false, isSocialButton: true, }, youtube: { id: "youtube-social-button", name: "YouTube", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/youtube.webp", buttonLabel: "Open YouTube", visible: ["movie", "show", "episode"], isYamlButton: false, isSocialButton: true, }, tmdbYaml: { id: "tmdb-yaml-button", name: "TMDB YAML", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/tmdb-pas.webp", buttonLabel: "Copy TMDB YAML", visible: ["movie", "show"], isYamlButton: true, isSocialButton: false, }, tvdbYaml: { id: "tvdb-yaml-button", name: "TVDB YAML", icon: "https://raw.githubusercontent.com/Soitora/Plex-GUID-Grabber/main/.github/images/tvdb-pas.webp", buttonLabel: "Copy TVDB YAML", visible: ["movie", "show"], isYamlButton: true, isSocialButton: false, }, }; function handleButtons(metadata, pageType, guid) { const leftButtonContainer = $(document).find(".PageHeaderLeft-pageHeaderLeft-GB_cUK"); const rightButtonContainer = $(document).find(".PageHeaderRight-pageHeaderRight-j9Yjqh"); console.debug(DEBUG_PREFIX, "Button container found:", rightButtonContainer.length > 0); if (!rightButtonContainer.length || $("#" + siteConfig.plex.id).length) return; const $directory = $(metadata).find("Directory, Video").first(); const title = $directory.attr("parentTitle") || $directory.attr("title"); const buttons = createButtonsConfig(guid, pageType, metadata); Object.entries(buttons).forEach(([site, { handler, config }]) => { if (config.visible.includes(pageType)) { if (config.isYamlButton && !USE_PAS) return; let shouldShow = true; if (config.isYamlButton) { const apiSite = site === "tmdbYaml" ? "tmdb" : "tvdb"; shouldShow = !!guid[apiSite]; } const $button = createButtonElement(config, shouldShow, guid[site], title); if ($button) { if (site === "plex") { $button.on("click", () => handlePlexButtonClick(guid[site], config, title)); } else if (config.isYamlButton) { $button.on("click", async () => handleYamlButtonClick(metadata, site, pageType, guid, title)); } else { $button.on("click", (e) => handler(e)); } appendButtonToContainer($button, config, rightButtonContainer, leftButtonContainer); setTimeout(() => { $button.css("opacity", 1); }, BUTTON_FADE_DELAY); } } }); } function createButtonsConfig(guid, pageType, metadata) { return Object.keys(siteConfig).reduce((acc, site) => { acc[site] = { handler: (event) => handleButtonClick(event, site, guid[site], pageType, metadata), config: siteConfig[site], }; return acc; }, {}); } function createButtonElement(config, shouldShow, guid, title) { if (!USE_SOCIAL_BUTTONS && config.isSocialButton) { return null; } const buttonClasses = ["_1v4h9jl0", "_76v8d62", "_76v8d61", "_76v8d68", "tvbry61", "_76v8d6g", "_76v8d6h", "_1v25wbq1g", "_1v25wbq18"].join(" "); const imageContainerClasses = ["_1h4p3k00", "_1v25wbq8", "_1v25wbq1w", "_1v25wbq1g", "_1v25wbq1c", "_1v25wbq14", "_1v25wbq3g", "_1v25wbq2g"].join(" "); return $("<button>", { id: config.id, "aria-label": config.buttonLabel, class: buttonClasses, css: { marginRight: BUTTON_MARGIN, display: (config.isYamlButton ? shouldShow : guid) ? "block" : "none", opacity: 0, transition: "opacity 0.3s ease-in-out", }, html: ` <div class="${imageContainerClasses}"> <img src="${config.icon}" alt="${config.buttonLabel}" title="${config.buttonLabel}" style="width: 32px; height: 32px;"> </div> `, }); } function handlePlexButtonClick(guid, config, title) { console.log(LOG_PREFIX, "GUID Output:", guid); try { GM_setClipboard(guid); Toast.fire({ icon: "success", title: `Copied ${config.name} guid to clipboard.`, html: `<span><strong>${title}</strong><br>${guid}</span>`, }); } catch (error) { console.error(ERROR_PREFIX, "Failed to copy guid:", error); } } async function handleYamlButtonClick(metadata, site, pageType, guid, title) { try { const yamlOutput = await generateYamlOutput(metadata, site, pageType, guid); console.log(LOG_PREFIX, "YAML Output:\n", yamlOutput); if (yamlOutput) { GM_setClipboard(yamlOutput); Toast.fire({ icon: "success", title: `Copied YAML output to clipboard.`, html: `<span><strong>${title}</strong><br><span class="pgg-toast-yaml">${yamlOutput.replace(/\n/g, "<br>")}</span></span>`, }); } } catch (error) { console.error(ERROR_PREFIX, "Failed to generate YAML:", error); Toast.fire({ icon: "error", title: "Failed to generate YAML", html: error.message, }); } } function appendButtonToContainer($button, config, rightButtonContainer, leftButtonContainer) { if (config.isYamlButton || config.id === siteConfig.plex.id) { rightButtonContainer.prepend($button); } else { if (SOCIAL_BUTTON_SEPARATION) { leftButtonContainer.append($button); } else { rightButtonContainer.prepend($button); } } } function checkApiKeys(site) { if (site === "tmdb" && !TMDB_API_READ_ACCESS_TOKEN) { Toast.fire({ icon: "error", title: "TMDB Read Access Token Missing", html: "Please set your TMDB Read Access Token in the userscript settings", }); return false; } if (site === "tvdb" && !TVDB_API_KEY) { Toast.fire({ icon: "error", title: "TVDB API Key Missing", html: "Please set your TVDB API key in the userscript settings", }); return false; } return true; } async function handleButtonClick(event, site, guid, pageType, metadata) { console.debug(DEBUG_PREFIX, "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 (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(DEBUG_PREFIX, "Directory/Video outerHTML:", $directory[0]?.outerHTML); console.debug(DEBUG_PREFIX, "Directory/Video innerHTML:", $directory[0]?.innerHTML); if (!$directory.length) { console.error(ERROR_PREFIX, "Main element not found in XML"); return null; } const guid = initializeGuid($directory); if (guid.plex?.startsWith("com.plexapp.agents.hama://")) { extractHamaGuid(guid, guid.plex); } $directory.find("Guid").each(function () { const guidId = $(this).attr("id"); if (guidId) { const [service, value] = guidId.split("://"); if (service && value) { extractGuid(guid, service, value); } } }); return guid; } function initializeGuid($directory) { return { plex: $directory.attr("guid"), imdb: null, tmdb: null, tvdb: null, mbid: null, anidb: null, youtube: null, }; } function extractHamaGuid(guid, plexGuid) { const match = plexGuid.match(/com\.plexapp\.agents\.hama:\/\/(\w+)-(\d+)/); if (match) { extractGuid(guid, match[1], match[2]); } } function extractGuid(guid, service, value) { const normalizedService = service.toLowerCase(); if (normalizedService.startsWith("tsdb")) { guid.tmdb = value; } else if (guid.hasOwnProperty(normalizedService)) { guid[normalizedService] = value; } } async function getLibraryMetadata(metadataPoster) { const img = metadataPoster.find("img").first(); if (!img?.length) { console.debug(DEBUG_PREFIX, "No image found in metadata poster"); return null; } const imgSrc = img.attr("src"); if (!imgSrc) { console.debug(DEBUG_PREFIX, "No src attribute found in image"); 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) { console.debug(DEBUG_PREFIX, "Missing plexToken or metadataKey", { plexToken: !!plexToken, metadataKey: !!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(ERROR_PREFIX, "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(DEBUG_PREFIX, "Not a metadata page."); return; } const $metadataPoster = $("div[data-testid='metadata-poster']"); console.debug(DEBUG_PREFIX, "Metadata poster found:", $metadataPoster.length > 0); if (!$metadataPoster.length) return; isObserving = false; const metadata = await getLibraryMetadata($metadataPoster); console.debug(DEBUG_PREFIX, "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(LOG_PREFIX, "Type:", pageType); console.log(LOG_PREFIX, "Title:", title); if (pageType) { const guid = await getGuid(metadata); console.log(LOG_PREFIX, "Guid:", guid); if (guid) { handleButtons(metadata, pageType, guid); } } }, DEBOUNCE_DELAY) ); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-page-type"], }); const handleNavigation = debounce(() => { isObserving = true; console.debug(DEBUG_PREFIX, "Navigation detected - resuming observation."); }, DEBOUNCE_DELAY); $(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); }; } async function getTVDBToken() { const LOGIN_URL = "https://api4.thetvdb.com/v4/login"; const API_KEY = TVDB_API_KEY; const PIN = TVDB_SUBSCRIBER_PIN; try { const response = await fetch(LOGIN_URL, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ apikey: API_KEY, pin: PIN }), }); //console.log(DEBUG_PREFIX, "TVDB Token Response:", response); if (!response.ok) { throw new Error(`Login failed: ${response.status} ${response.statusText}`); } const data = await response.json(); //console.log(DEBUG_PREFIX, "TVDB Token Data:", data.data); return data.data.token; } catch (error) { console.error(DEBUG_PREFIX, "Authentication error:", error); return null; } } async function fetchApiData(url, headers = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { ...headers, }, onload: function (response) { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); resolve(data); } catch (error) { console.error(ERROR_PREFIX, "Failed to parse JSON response:", error); Toast.fire({ icon: "error", title: "API Error", html: "Failed to parse JSON response", }); reject(new Error("Failed to parse JSON response")); } } else { console.error(ERROR_PREFIX, `API error: ${response.status} - ${response.responseText}`); Toast.fire({ icon: "error", title: "API Error", html: `Status: ${response.status} - ${response.responseText}`, }); reject(new Error(`API error: ${response.status} - ${response.responseText}`)); } }, onerror: function (error) { console.error(ERROR_PREFIX, "Network error:", error); Toast.fire({ icon: "error", title: "Network Error", html: error.message, }); reject(new Error(`Network error: ${error}`)); }, }); }); } async function generateYamlOutput(metadata, site, pageType, guid) { const apiSite = site === "tmdbYaml" ? "tmdb" : "tvdb"; if (!checkApiKeys(apiSite)) return ""; const mediaType = pageType === "movie" ? "movie" : "tv"; const $directory = $(metadata).find("Directory, Video").first(); const plex_guid = $directory.attr("guid"); try { const { title, numberOfSeasons } = await fetchTitleAndSeasons(apiSite, mediaType, guid); return constructYamlOutput(title, plex_guid, numberOfSeasons, guid, mediaType); } catch (error) { console.error(ERROR_PREFIX, "Error generating YAML output:", error); return ""; } } async function fetchTitleAndSeasons(apiSite, mediaType, guid) { if (apiSite === "tmdb") { return fetchTmdbData(mediaType, guid[apiSite]); } else if (apiSite === "tvdb") { return fetchTvdbData(mediaType, guid[apiSite]); } } async function fetchTmdbData(mediaType, tmdbId) { const url = mediaType === "tv" ? `https://api.themoviedb.org/3/tv/${tmdbId}?language=${TMDB_LANGUAGE}` : `https://api.themoviedb.org/3/movie/${tmdbId}?language=${TMDB_LANGUAGE}`; const data = await fetchApiData(url, { Accept: "application/json", Authorization: `Bearer ${TMDB_API_READ_ACCESS_TOKEN}`, }); const title = mediaType === "tv" ? data.name : data.title; const numberOfSeasons = mediaType === "tv" ? data.number_of_seasons || 1 : 1; return { title, numberOfSeasons }; } async function fetchTvdbData(mediaType, tvdbId) { const tvdbBearerToken = await getTVDBToken(); if (!tvdbBearerToken) { console.error(ERROR_PREFIX, "Failed to retrieve TVDB token."); return { title: "", numberOfSeasons: 1 }; } const url = mediaType === "tv" ? `https://api4.thetvdb.com/v4/series/${tvdbId}/translations/${TVDB_LANGUAGE}` : `https://api4.thetvdb.com/v4/movies/${tvdbId}/translations/${TVDB_LANGUAGE}`; const data = await fetchApiData(url, { Accept: "application/json", Authorization: `Bearer ${tvdbBearerToken}`, }); const title = data.data.name; const numberOfSeasons = mediaType === "tv" ? await fetchTvdbSeasons(tvdbId, tvdbBearerToken) : 1; return { title, numberOfSeasons }; } async function fetchTvdbSeasons(tvdbId, tvdbBearerToken) { const episodesData = await fetchApiData(`https://api4.thetvdb.com/v4/series/${tvdbId}/episodes/default/${TVDB_LANGUAGE}`, { Accept: "application/json", Authorization: `Bearer ${tvdbBearerToken}`, }); const seriesSeasons = new Set(); episodesData.data.episodes.forEach((episode) => { if (episode.seasonNumber !== 0) { seriesSeasons.add(episode.seasonNumber); } }); return seriesSeasons.size || 1; } function constructYamlOutput(title, plex_guid, numberOfSeasons, guid, mediaType) { const data = [ { title: title, guid: plex_guid, seasons: Array.from({ length: numberOfSeasons }, (_, i) => ({ season: i + 1, "anilist-id": 0, })), }, ]; let yamlOutput = jsyaml.dump(data, { quotingType: `"`, forceQuotes: { title: true }, indent: 2, }); yamlOutput = yamlOutput.replace(/^(\s*guid: )"([^"]+)"$/gm, "$1$2").trim(); const url_IMDB = guid.imdb ? `\n # imdb: https://www.imdb.com/title/${guid.imdb}/` : ""; const url_TMDB = guid.tmdb ? `\n # tmdb: https://www.themoviedb.org/${mediaType}/${guid.tmdb}` : ""; const url_TVDB = guid.tvdb ? `\n # tvdb: https://www.thetvdb.com/dereferrer/${mediaType === "tv" ? "series" : "movie"}/${guid.tvdb}` : ""; const guidRegex = /^(\s*guid:.*)$/m; return yamlOutput .replace(guidRegex, `$1${url_IMDB}${url_TMDB}${url_TVDB}`) .replace(/^/gm, " ") .replace(/^\s\s$/gm, "\n"); } $(document).ready(observeMetadataPoster);