// ==UserScript==
// @name BDSMLR - clickable links to original high-res images
// @namespace bdsmlr_linkify
// @version 1.8.0
// @license GNU AGPLv3
// @description Modifies images to link to their original ("-og") version. Works for (a) the dashboard, (b) blogs displayed on right sidebar in the dashboard, (c) blog streams (xxx.bdsmlr.com) and (d) individual posts (xxx.bdsmlr.com/post/yyyyyyyyyy). It does NOT work for the archive view.
// @author marp
// @homepageURL https://greasyfork.org/en/users/204542-marp
// @include https://bdsmlr.com/
// @include https://bdsmlr.com/dashboard
// @include https://*.bdsmlr.com/
// @include https://*.bdsmlr.com/post/*
// @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//uploads/*
// @run-at document-end
// ==/UserScript==
function createImageLinks(myDoc, myContext) {
//console.info("createImageLinks: ", myContext);
if (myDoc===null) myDoc = myContext;
if (myDoc===null) return;
if (myContext===null) myContext = myDoc;
var matches;
var tmpstr;
var singlematch;
var origpostlink;
var origbloglink;
var origblog;
var imagematches;
var imageurl;
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/')]",
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 {
//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) {
origblog = origbloglink.getAttribute("href");
}
else {
// 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) {
origblog = origpostlink.getAttribute("href");
}
else 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
imagematches = 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",
el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var j=0, image, imagelink; (j<imagematches.snapshotLength); j++) {
image=imagematches.snapshotItem(j);
if (image) {
imagelink = image.parentNode;
imageurl = imagelink.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 exercution 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) {
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);
} else {
// if no original poster info - construct link to "-og" version of image on the current blog (e.g. works for non-retweets on individual blogs)
tmpstr = getOriginalImageURL(imageurl);
}
// get the link node
image.parentNode.setAttribute("href", tmpstr);
}
}
}
} 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;
}
}
// Two very different actions depending on if this is on twitter.com or twing.com
if (window.location.href.includes('bdsmlr.com/uploads/')) {
if (document.head.textContent &&
( document.head.textContent.toLowerCase().includes('404 not found') ||
document.head.textContent.toLowerCase().includes('403 forbidden') ) ) {
var tmpstr = window.location.href;
var pos = tmpstr.lastIndexOf(".");
var pos2 = tmpstr.lastIndexOf("-og.");
var pos3 = tmpstr.indexOf(".");
if (pos == pos2+3) {
// old mechanism - -og to be found only using server/blog name of the original poster
// seems this didn't work -> now try to find -og on the cdn servers
if (tmpstr.startsWith("https://bdsmlr.com")) {
window.location.assign("https://cdn03.bdsmlr" + tmpstr.substring(pos3));
}
else if (!tmpstr.startsWith("https://cdn")) {
window.location.assign("https://" + tmpstr.substring(pos3+1));
}
else if (tmpstr.startsWith("https://cdn03")) {
window.location.assign("https://cdn02" + tmpstr.substring(pos3));
}
else {
// seems there's no -og version to be found -> revert to non-"-og" version on cdn03.bdsmlr.com
window.location.assign("https://cdn03" + tmpstr.substring(pos3,pos2) + tmpstr.substring(pos));
}
}
else if (tmpstr.startsWith("https://cdn03")) {
// look for non-"og" version on cdn02
window.location.assign("https://cdn02" + tmpstr.substring(pos3));
}
}
}
// fix bug in bdsmlr redirection url
else if (window.location.href.includes('bdsmlr.com//uploads/')) {
var tmpstr = window.location.href;
var pos = tmpstr.lastIndexOf("//");
window.location.assign(tmpstr.substring(0,pos) + tmpstr.substring(pos+1));
}
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);
});
});
});
// 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);
//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
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, config);
}
}