您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This script modifies images to link to their original ("-og") high-res version (READ WORKAROUND NOTE!). In addition, it unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only).
当前为
// ==UserScript== // @name BDSMLR - clickable links to original high-res images and display timestamps // @namespace bdsmlr_linkify // @version 2.7.0 // @license GNU AGPLv3 // @description This script modifies images to link to their original ("-og") high-res version (READ WORKAROUND NOTE!). In addition, it unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only). // @author marp // @homepageURL https://gf.zukizuki.org/en/users/204542-marp // @include https://bdsmlr.com/ // @include https://bdsmlr.com/?group* // @include https://bdsmlr.com/dashboard // @include https://*.bdsmlr.com/ // @include https://*.bdsmlr.com/post/* // @include https://bdsmlr.com/search/* // @include https://*.bdsmlr.com/search/* // @include https://bdsmlr.com/blog/* // @include https://bdsmlr.com/originalblogposts/* // @include https://bdsmlr.com/likes // @include https://bdsmlr.com/uploads/photos/* // @include https://bdsmlr.com/uploads/pictures/* // @include https://*.bdsmlr.com/uploads/photos/* // @include https://*.bdsmlr.com/uploads/pictures/* // @include https://bdsmlr.com//* // @run-at document-end // ==/UserScript== //console.info("START href: ", window.location.href); //------------------------------------------------------------ // FIRST PART OF SCRIPT #2 - function that gets called by event oberver registers as part of 1st part #1 (see below) //------------------------------------------------------------ function createImageLinks(myDoc, myContext) { //console.info("createImageLinks: ", myContext); if (myDoc===null) myDoc = myContext; if (myDoc===null) return; if (myContext===null) myContext = myDoc; var tmpstr; var singlematch; var origpostlink; var origbloglink; var origblog; var matches, matches2; var imageurl; var imagesrc; var cdnmatches; var cdnnumber; matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'postholder')] | ./descendant-or-self::div[contains(@class,'post_content')]", myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i<matches.snapshotLength); i++) { el = matches.snapshotItem(i); if (el) { try { // try to find info about original poster (if this is a reblog) as well as the link to the individual (potentially reblogged) post // both info only seem to be present on dashboard and on rightside overlay blogs - but not always on individual blogs (xxx.bdsmlr.com) or on individual blog post URLs :-( singlematch = myDoc.evaluate(".//div[contains(@class,'originalposter')]/a[contains(@href,'.bdsmlr.com/post/')]" + " | " + ".//div[contains(@class,'original')]/a[contains(@href,'.bdsmlr.com')]", el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com/post/yyyyyyyy if (origpostlink) { origblog = origpostlink.getAttribute("href"); //everything after and including "/post" gets truncated away later anyway } else { origblog = null; } if (origblog === null) { //second method might find the originial blog URL (xxxx.bdsmlr.com) singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]//i[contains(@class,'retweet') or contains(@class,'rbthis')]" + "/following-sibling::a[contains(@class,'adata') or contains(@class,'ndata')]", el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); origbloglink = singlematch.singleNodeValue; // xxxx.bdsmlr.com if (origbloglink) { tmpstr = origbloglink.getAttribute("href"); if ( tmpstr && (tmpstr.length > 10) && !(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...") origblog = tmpstr; } } if (origblog === null) { // if neither of the two above find anything then this is likely NOT a reblogged post but the original post -> get the orginial blog post URL singlematch = myDoc.evaluate(".//a[(contains(@class,'adata') or contains(@class,'ndata')) and contains(@href,'.bdsmlr.com/post/')]", el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com if (origpostlink) { tmpstr = origpostlink.getAttribute("href"); if ( tmpstr && (tmpstr.length > 10) && !(tmpstr.includes("//.bdsmlr.com") ) ) { // some urls are invalid and need to be ignored ("https://.bdsmlr.com/...") origblog = tmpstr; } } if (origblog === null) { if ( !window.location.href.startsWith("https://bdsmlr.com") ) { // if no link to neither original blog nor original blog post was found then we assume that this is the original blog post or blog itself (this is a rather shaky assumtion - fingers crossed...) origblog = window.location.href; } else { // however - if the current url is the dashboard then we're out of luck origblog = null; } } } } // iterate over all links to images (i.e. does NOT (yet) create links to images where none exist in the first place) // skip over items that already have a link to a "non-cdn" bdsmlr url matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "//a[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') and contains(@href,'.bdsmlr.com'))]/img" + " | " + ".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "//div[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') and contains(@href,'.bdsmlr.com'))]/img", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var j=0, image, imageparent; (j<matches2.snapshotLength); j++) { image=matches2.snapshotItem(j); if (image) { imagesrc = image.src; imageparent = image.parentNode; imageurl = imageparent.getAttribute("href"); if (imageurl === null || imageurl.length < 5) { imageurl = image.getAttribute("src"); // No idea why this is needed... DevTools inspector always shows a valid image src attribute... but at script execution time... apparently not... seems to be some bdsmlr JavaScript post-processing... if (imageurl === null || imageurl.length < 5) { imageurl = image.getAttribute("data-echo"); } } if (imageurl && imageurl.length > 5) { // get url to "-og" image on save host - if the url already conatrins "-og" it will simply return the passed in argument (tmpstr == imageurl) tmpstr = getOriginalImageURL(imageurl); // analyze to which cdn server the url points cdnmatches = imageurl.toLowerCase().match("https:\/\/cdn([0-9]+)\.bdsmlr\.com\/"); // check for old image server (old = cdn06, new = cdno06) if (cdnmatches !== null) { cdnnumber = parseInt(cdnmatches[1], 10); } else { cdnnumber = NaN; } // ONLY IF there is no -og url already OR if the cdn server is cdn 01, cdn 02 or cdn 03 -> apply this script's algorithm to locate -og image // -> i.e do not touch already existing -og urls to cdn04 or beyond servers (cdn05, etc.) // => NOTE: script algorithm will still trigger if the image is not found (see the script logic that applies to "*bdsmlr.com/uploads/*") further below if ( !isNaN(cdnnumber) && //special treatment only for some of the old image servers bit not new ones ((imageurl.length != tmpstr.length) || // will be unequal if there was no -og url to begin with (but instead created by getOriginalImageURL) cdnnumber <= 3) ) { // if we have the url of the original blog then we use a different mechanism to reconstruct the orig image URL // otherwise we stay with tmpstr as is if (origblog && origblog.length > 5) { // if we have info about original poster -> construct link to "-og" version of image on orig posters blog (e.g. https://<origposter>.bdsmlr.com/<....>/imagename-og.jpg) tmpstr = getOriginalPosterImageURL(imageurl, origblog); tmpstr = getOriginalImageURL(tmpstr); } // tmpstr = tmpstr + "invalidurl"; //create an invalid url which will ensure to trigger the second part of this script (see below - 404 error for "...bdsmlr.com/uploads/..." urls) } // hopefully temporary workaround to avoid bdsmlr circular redirections - such cirular redirs cause GreaseMonkey NOT to trigger at all! :-( // there seem to be no redirs in place on "https://bdsmlr.com/uploads/..." while there seem to be several circular ones on various cdn (old) and cdno (new) servers // BIG ISSUE that forces this workaround: GreaseMonkey does NOT seem to trigger on (circular) redirection errors :-( if (tmpstr.toLowerCase().startsWith("https://cdn") && tmpstr.toLowerCase().indexOf("bdsmlr-com")<=0 ) { tmpstr = "https://" + tmpstr.substring(tmpstr.toLowerCase().indexOf(".bdsmlr.com") + 1); } // insert or get existing link node parent and set the link target // -> pass on context information to second part of this script (below) insertOrChangeLinkElement(myDoc, imageparent, tmpstr + "?cdnnumber=" + cdnnumber + "&initialurl=" + encodeURIComponent(imageurl) + "&initialsrc=" + encodeURIComponent(imagesrc)); } } } // multi-image posts - unhide all images (instead of having to manually click on "show x more images" matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "/div[contains(@style,'display:none')]", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var j=0, node; (j<matches2.snapshotLength); j++) { node=matches2.snapshotItem(j); if (node) { node.style.display = "initial"; } } // multi-image posts - hide the "show x more images" element matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + "/following-sibling::div[contains(@class,'viewAll')]", el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var j=0, node; (j<matches2.snapshotLength); j++) { node=matches2.snapshotItem(j); if (node) { node.style.display = "none"; } } } catch (e) { console.warn("error: ", e); } } } } // try to find the timestamp info and display in upper right corner function displayTimestamps(myDoc, myContext) { //console.info("displayTimestamps: ", myContext); if (myDoc===null) myDoc = myContext; if (myDoc===null) return; if (myContext===null) myContext = myDoc; var matches; var tmpstr; var singlematch; var postinfo; var timestamp; var newnode; matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'feed') and @title]", myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i<matches.snapshotLength); i++) { el = matches.snapshotItem(i); if (el) { try { timestamp = el.getAttribute("title"); if (timestamp && timestamp.length>5 && timestamp.length<70) { singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]", el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); postinfo = singlematch.singleNodeValue; if (postinfo) { newnode = myDoc.createElement("div"); newnode.setAttribute("style", "float:right; margin-right: 10px; padding-right: 10px;"); newnode.innerHTML = timestamp; postinfo.appendChild(newnode); } } } catch (e) { console.warn("error: ", e); } } } } function getOriginalPosterImageURL(imageurl, originalposter) { if (originalposter === null) { return imageurl; } var pos = imageurl.toLowerCase().indexOf(".bdsmlr.com"); var pos2 = originalposter.toLowerCase().indexOf(".bdsmlr.com"); if (pos > 0 && pos2 > 0) { return originalposter.substring(0, pos2) + imageurl.substring(pos); } else { return imageurl; } } function getOriginalImageURL(imageurl) { if (imageurl === null) { return imageurl; } var pos = imageurl.lastIndexOf("."); var pos2 = imageurl.lastIndexOf("-og."); if (pos > 0 && (pos2+3)!=pos) { return imageurl.substring(0, pos) + "-og" + imageurl.substring(pos); } else { return imageurl; } } function insertOrChangeLinkElement(myDoc, wrapElement, linkTarget) { if (wrapElement.tagName.toLowerCase() == "a") { wrapElement.setAttribute("href", linkTarget); wrapElement.setAttribute("target", "_blank"); } else { var parentnode = wrapElement.parentNode; var newnode = myDoc.createElement("a"); newnode.setAttribute("href", linkTarget); newnode.setAttribute("target", "_blank"); parentnode.replaceChild(newnode, wrapElement); newnode.appendChild(wrapElement); } } //------------------------------------------------------------ // SECOND PART OF SCRIPT //------------------------------------------------------------ // this is a function used asychroniously via Promise objects // a "parent" Promise is passed in and this function "chains" another "child" Promise (the fetch object) to it // the (asynchronuous!) return value is NULL if the Url is not valid (404, etc) or the fetched URL if the request is sucesfull (OK 200) function checkUrl(checkUrlPromise, baseimageurl, hostprefix, allowredirect) { var imageurl; //console.info("checkUrl-Enter: ", "baseimageurl:" + baseimageurl + " hostprefix:" + hostprefix + " allowredir:" + allowredirect); if (baseimageurl.startsWith("https://")) { baseimageurl = baseimageurl.substring(8); } if (hostprefix !== null && hostprefix.length > 0) { imageurl = "https://" + hostprefix + "." + baseimageurl; } else { imageurl = "https://" + baseimageurl; } //return the newly "chained" Promise that now has a fetch promise as "child" return checkUrlPromise.then( function(promiseresult) { // if the prior/parent promise resolves into a valid Url - return that Url and skip the child/follow-up fetch if ( (promiseresult !==null) && (promiseresult.length > 10) && (promiseresult.indexOf("default_image")<=0) ) { return promiseresult; } else { //console.info("checkUrl-A: ", imageurl + " allowredir:" + allowredirect); // the prior Promise did NOT resolve into a valid URL -> try to fetch this new URL return fetch(imageurl, (allowredirect ? { redirect: 'follow' } : { redirect: 'error' } ) ).then( function(response) { if (response.ok) { if (response.redirected && allowredirect) { return response.url; } else { return imageurl; } } else { return null; } }, function(rejectreason) { return null; }); } }, function(rejectreason) { return null; } ); } // Two very different actions depending on if this is for the URL of an image or for a bdsmlr page (page = dashboard, blog stream or blog post) // -> If this if statement evaluates to true it is in the context of an image -> execute SECOND part of script -> algorithm to try to find the "-og" version of the image if ( window.location.href.includes('bdsmlr.com/uploads/') ) { // the "og search algorithm" only triggers if the current url is invalid - otherwise do nothing if ( document.head.textContent !== null && ( document.head.textContent.toLowerCase().includes('404 not found') || document.head.textContent.toLowerCase().includes('403 forbidden') || document.head.textContent.toLowerCase().includes('problem loading page') || document.body.textContent.toLowerCase().includes('page isn’t redirecting properly') || document.body.textContent.toLowerCase().includes('could not be found') ) ) { var tmpstr = window.location.href; // if exists, strip the suffix that was used to trigger this part of the script if (tmpstr.lastIndexOf('invalidurl') > 0) { tmpstr = tmpstr.substring(0, tmpstr.lastIndexOf("invalidurl")); } var pos = tmpstr.lastIndexOf("."); var pos2 = tmpstr.lastIndexOf("-og."); var pos3 = tmpstr.indexOf("."); var baseimageurl = null; var blogprefix = null; var initialUrl = null; var initialUrlIsOldServer = false; var initialSrc = null; var initialSrcIsOldServer = false; var cdnnumber = -1; var checkUrlPromise = null; // retrieve "context" parameters that might have been passed in for the above part of the script if (window.location.search !== null) { var urlParams = new URLSearchParams(window.location.search); initialUrl = urlParams.get('initialurl'); if (initialUrl !== null) { initialUrl = decodeURIComponent(initialUrl); initialUrlIsOldServer = initialUrl.indexOf("//cdno") <= 0; } initialSrc = urlParams.get('initialsrc'); if (initialSrc !== null) { initialSrc = decodeURIComponent(initialSrc); initialSrcIsOldServer = initialSrc.indexOf("//cdno") <= 0; } cdnnumber = parseInt(urlParams.get('cdnnumber')); } // check on what kind of url this script was triggered if ( !(tmpstr.startsWith("https://bdsmlr.com")) && !(tmpstr.startsWith("https://cdn")) && (tmpstr.startsWith("https://")) ) { //this is a url to a specific blog (xxxx.bdsmlr.com/uploads/...) baseimageurl = tmpstr.substring(pos3+1); blogprefix = tmpstr.substring(8, pos3); } else if (tmpstr.startsWith("https://bdsmlr.com")) { baseimageurl = tmpstr.substring(8); } else { // this should be a url to some kind of (old) cdn or (new) cdno server (cdnxx.bdsmlr.com/uploads/...) baseimageurl = tmpstr.substring(pos3+1); } // starting Promise which resolves to invalid URL (=null) // this is the start of the to follow "daisy-chaining" of several fetch requests to test several potential URLs checkUrlPromise = Promise.resolve(null); if ((pos == pos2+3) && initialUrlIsOldServer ) { // if old image server -> check if this is an -og url if ( blogprefix !== null && blogprefix.length > 0 ) { //this is a url to a specific blog (xxxx.bdsmlr.com/uploads/...) checkUrlPromise = checkUrl(checkUrlPromise, baseimageurl, blogprefix, false); } checkUrlPromise = checkUrl(checkUrlPromise, baseimageurl, null, false); // "bdsmlr.com/uploads/..." // try old cdn servers (cdn02, cdn03 and cdn04) and depending on cdnnumber context param -> also try newer cdn servers (cdn05 and beyond) var cdn = 4; if ( !isNaN(cdnnumber) && cdnnumber > 4 ) { cdn = cdnnumber; } while (cdn >= 2) { checkUrlPromise = checkUrl(checkUrlPromise, baseimageurl, "cdn" + cdn.toString().padStart(2, '0'), false); // "cdnXX.bdsmlr.com/uploads/..." cdn--; } } // no valid -og URL -> try the -og version of the initial URL that bdsmlr provided originally if ( (initialUrl !== null) && (initialUrl.length > 10) ) { checkUrlPromise = checkUrl(checkUrlPromise, getOriginalImageURL(initialUrl), null, initialUrlIsOldServer); // initialUrlIsOldServer == true -> allow bdsmlr to redirect for original URL } // still no valid URL -> try the -og version of the URL of the image shown in the original stream or blog post if ( (initialSrc !== null) && (initialSrc.length > 10) && (initialSrc !== initialUrl) ) { checkUrlPromise = checkUrl(checkUrlPromise, getOriginalImageURL(initialSrc), null, initialSrcIsOldServer); // initialSrcIsOldServer == true -> allow bdsmlr to redirect for original image source link } // no valid -og URL -> try the initial URL (without adding "-og") that bdsmlr provided originally if ( (initialUrl !== null) && (initialUrl.length > 10) ) { checkUrlPromise = checkUrl(checkUrlPromise, initialUrl, null, true); // true -> allow bdsmlr to redirect for original URL } // still no valid URL -> try the URL of the image shown in the original stream or blog post (without adding "-og") if ( (initialSrc !== null) && (initialSrc.length > 10) && (initialSrc !== initialUrl) ) { checkUrlPromise = checkUrl(checkUrlPromise, initialSrc, null, true); // true -> allow bdsmlr to redirect for original image source link } checkUrlPromise.then( function(result) { if ( (result !== null) && (result.length > 10) ) { window.location.assign(result); } } ); }; } // fix buggy redirection from BDSMLR else if ( window.location.href.includes('bdsmlr.com//') ) { var tmpstr = window.location.href; var pos = tmpstr.indexOf('bdsmlr.com//'); // remove the double // window.location.assign( tmpstr.substring(0, pos+11) + tmpstr.substring(pos+12) ); } //------------------------------------------------------------ // FIRST PART OF SCRIPT #1 - initial statement and registration of event observer //------------------------------------------------------------ // script runs NOT in the context of an image - i.e. dashboard, blog stream, individual post // -> register event observer for future to-be-loaded posts (endless scrolling) and execute first part of script (createImageLinks & displayTimestamps) on already loaded posts else { // create an observer instance and iterate through each individual new node var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(addedNode) { createImageLinks(mutation.target.ownerDocument, addedNode); displayTimestamps(mutation.target.ownerDocument, addedNode); }); }); }); // configuration of the observer // NOTE: subtree is false as the wanted nodes are direct children of <div class="newsfeed"> -> notable performance improvement // "theme1" is the class used by the feed root node for individual user's blog (xxxx.bdsmlr.com) -> seems unstable/temporary name -> might be changed by bdsmlr var config = { attributes: false, childList: true, characterData: false, subtree: false }; // pass in the target node (<div> element contains all stream posts), as well as the observer options var postsmatch = document.evaluate(".//div[contains(@class,'newsfeed')] | .//div[contains(@class,'theme1')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var postsnode = postsmatch.singleNodeValue; //process already loaded nodes (the initial posts before scrolling down for the first time) createImageLinks(document, postsnode); displayTimestamps(document, postsnode); //start the observer for new nodes observer.observe(postsnode, config); // also observe the right sidebar blog stream on the dashboard // pass in the target node, as well as the observer options (subtree has to be true here - target nodes are further down the hierarchy) var config2 = { attributes: false, childList: true, characterData: false, subtree: true }; var sidepostsmatch = document.evaluate(".//div[@id='rightposts']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var sidepostsnode = sidepostsmatch.singleNodeValue; // sidebar does only exist on dashboard if (sidepostsnode) { //start the observer for overlays observer.observe(sidepostsnode, config2); } }