BDSMLR - clickable links to original high-res images and display timestamps

This script modifies images to link to their original ("-og") high-res version. The link is available as soon as a border appears around an image. The color of the box indicates the range of the image height. In addition, the script unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only). It also fixes some broken images in the stream (cdno08).

Verze ze dne 22. 04. 2022. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         BDSMLR - clickable links to original high-res images and display timestamps
// @namespace    bdsmlr_linkify
// @version      3.3.6
// @license      GNU AGPLv3
// @description  This script modifies images to link to their original ("-og") high-res version. The link is available as soon as a border appears around an image. The color of the box indicates the range of the image height. In addition, the script unhides/expands all images of large multi-image posts and displays the timestamp of the post in the upper right corner (dashboard only). It also fixes some broken images in the stream (cdno08).
// @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/?latest*
// @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//*
// @run-at document-end
// ==/UserScript==

// jshint esversion:8


//console.info("START href: ", window.location.href);



//------------------------------------------------------------
// FIRST PART OF SCRIPT #2 - function that gets called by event oberver that is registered as part of 1st part #1 (see very end of this script)
//------------------------------------------------------------

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 imagesrcfixed;
  var cdnmatches;
  var cdnnumber;

  // iterate over all posts within the supplied context
  matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'postholder')]"
                           + " | " +
                           "./descendant-or-self::div[contains(@class,'post_content')]"
                           + " | " +
                           "./descendant-or-self::div[contains(@class,'commenttext')]"
                           , 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 :-(
        // This info is needed because in VERY early BDSMLR days the full resolution "-og" image sometimes only existed on the original blog hostname...
        // ...and these very old posts are still around...
        origblog = null;
			  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) {
          tmpstr = origpostlink.getAttribute("href"); //everything after and including "/post" gets truncated away later anyway
          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) {
          //second method might find the originial blog URL (xxxx.bdsmlr.com) - however, often this is just a re-poster - not the original
          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;
              }
            }
          }
        } // if none of the above worked then we're out of luck and origblog remains null
        

				// iterate over all images 
        // skip over items that already have a link to a "non-cdn" bdsmlr url or that are not bdsmlr links at all
        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" +
                                  " | " +
                                  ".//div[contains(@class,'image_container') or contains(@class,'image_content')]//div[contains(@class,'textcontent')]" + 
                                    "//img[contains(@src,'https://cdn') and contains(@src,'.bdsmlr.com')]" +
                                  " | " +
                                  "./descendant-or-self::div[contains(@class,'singlecomment') or contains(@class,'commenttext')]" + 
                                    "//img[contains(@src,'https://cdn') and contains(@src,'.bdsmlr.com') and (contains(@class,'fr-dib') or contains(@class,'fr-fic'))]"
                                  ,
                                  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"); 
              }  
            }
            // CDNO08 seem to have been broken during the botched infrastructure upgrade in early 2021 
            // (after which the ownership of bdsmlr seems to have changed - and the new team doesn't know all the history and thus issues)
            // CDNO08 now redirects to CDN12 - but the image is to be found on CDN08, instead?!
            // Also, some URLs are HTTP - which do not seem to work (error as if image does not exist) - but HTTPS does work
            // 2021-11-01: Same goes from cdno12 -> cdn12 (instead of only cdno08 -> cdn08)
            // CAREFUL: This observation is just based on a few purly-by-chance discoveries - no idea if there are other locations besides CDN012/CDN08
            imagesrcfixed = null;
            if (imagesrc.toLowerCase().startsWith("http://")) {
              imagesrcfixed = "https://" + imagesrc.substring(7);
            } else {
              imagesrcfixed = imagesrc;
            }
            if (imagesrcfixed.toLowerCase().startsWith("https://cdno08.bdsmlr.com/")) {
              imagesrcfixed = "https://cdn08.bdsmlr.com/" + imagesrcfixed.substring(26);
            }                                          
            if (imagesrcfixed.toLowerCase().startsWith("https://cdno12.bdsmlr.com/")) {
              imagesrcfixed = "https://cdn12.bdsmlr.com/" + imagesrcfixed.substring(26);
            }
            if (imagesrcfixed.toLowerCase().startsWith("https://cdno012.bdsmlr.com/")) {
              imagesrcfixed = "https://cdn012.bdsmlr.com/" + imagesrcfixed.substring(27);
            }
            if (imagesrcfixed.toLowerCase().startsWith("https://cdno06.bdsmlr.com/")) {
              imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(26);
            }
            if (imagesrcfixed.toLowerCase().startsWith("https://cdno010.bdsmlr.com/")) {
              imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(27);
            }
            if (imagesrcfixed.toLowerCase().startsWith("https://cdno05.bdsmlr.com/")) {
              imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(26);
            }
            if (imagesrcfixed !== imagesrc) {
              image.setAttribute("src", imagesrcfixed);
              imagesrc = image.src;
            }
            
            if (imageurl && imageurl.length > 5) {
              getBestImageUrlPromise(imagesrc, imageurl, origblog, image)
                .then( (result) => {
                
                	if ( (result !== null) && (result.image !== null) && (result.url !== null) ) {
                    var linkelem;
                    var divelem;
                    // Sometimes, images in comments are weirdly structured in hierarchies with paragraph elements -> it's better to create the link as direct parent of the image itself
                    // same goes for the element that the colored border box can be added to - there is no appropriate div element - so we use the image itself
                    if (  (result.image.parentNode.nodeName.toLowerCase() == "p")  ||
                         ((result.image.previousSibling !== null) && (result.image.previousSibling.nodeName.toLowerCase() == "p")) ||
                         ((result.image.nextSibling !== null) && (result.image.nextSibling.nodeName.toLowerCase() == "p")) ) {
                      linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image, result.url);  
                      divelem = result.image;
                    } else { 
                      linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image.parentNode, result.url);
                      var divmatch = result.image.ownerDocument.evaluate("./ancestor::div[(contains(@class,'hide') or contains(@class,'earlycomments')) and ancestor::div[contains(@class,'post')]]",
                                                                         linkelem, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
                      divelem = divmatch.singleNodeValue; 
                    }

                    divelem.style = "border: 5px solid grey;";
                    divelem.setAttribute("title", getSizeText(result.size)); 

                    getImageDimensionsPromise(result.url, divelem, result.size)
                      .then( (result2) => {

                        result2.element.style = "border: 5px solid " + result2.color + ";";
                        result2.element.setAttribute("title", getSizeText(result2.size) + " - " + result2.width + " x " + result2.height); 
                    });
                  }
              });
            }
          }
        }
        
        
        // 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 isOriginalImageURL(imageurl) {
  if (imageurl === null) {
    return false;
  }
  var tmpstr = imageurl.toLowerCase();
  if (tmpstr.includes("bdsmlr.com/")) {
    var pos = tmpstr.lastIndexOf(".");
    var pos2 = tmpstr.lastIndexOf("-og.");
    if (pos > 0 && (pos2+3)==pos) {
      return true;
    }
  }
  return false;
}


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.nodeName.toLowerCase() == "a") {
    wrapElement.setAttribute("href", linkTarget);
    wrapElement.setAttribute("target", "_blank");
    return wrapElement;
  } 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);
    return newNode;
  }
}


function getSizeText(sizeInBytes) {
  if (sizeInBytes === null) {
    return "";
  }
  if (sizeInBytes >= 1048576) {
    return (sizeInBytes / 1048576).toFixed(1) + " MB";
  }
  else if (sizeInBytes >= 1024) {
    return (sizeInBytes / 1024).toFixed(0) + " KB";
  }
  else {
    return sizeInBytes.toFixed(0) + " B";
  }
}



// This ASYNC method returns a promise to retrieve the HTTP response header data for the supplied URL.
// It uses an "HTTP HEAD" request which does NOT download the response payload (to minimize network traffic)
async function checkUrlHeaderOnlyPromise(url, allowredirect) {

  return fetch(url, (allowredirect ? { redirect: 'follow', method: 'HEAD' } : { redirect: 'error', method: 'HEAD' } ) ).then(
    function(response) {
      if (response.ok) {
        var contentLength = parseInt(response.headers.get('Content-Length'), 10);
        if (isNaN(contentLength))
          contentLength = 0;
        if (response.redirected && allowredirect) {
          return { url: response.url, size: contentLength }
        } else {
          return { url: url, size: contentLength };
        }
      } else {
        return { url: url, size: -1 };
      }
    },
    function(rejectreason) {
      return { url: url, size: -1 };
    });
}





// This ASYNC method returns a promise to determine the url of the originally uploaded image (the one with a suffix of "-og" in the name)
// This requires "testing" a lot of URLs, i.e. it causes server traffic (especially if it is an image on the "old" CDN servers from BDSMLR's early times).
// To minimize network traffic, this method only requests the HTTP headers for all these URLs - the image itself nor error webpages (404) are not fully downloaded.
// If multiple "-og" variants are found, the one with the largest size (in bytes, not image dimensions!) is chosen. 
// If the og version is smaller than the non-og version it still sticks with the og, if it is larger than 5kb (5kb to exclude error pages, the non-og is typically upscaled and worse quality than the og)
// "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise "then" function of the caller
// IMPORTANT DISCOVERY: For some(!) image servers, the path part (the part after hostname) is CASE-SENSITIVE! Urgh!!
async function getBestImageUrlPromise(imageurl, linkurl, blogurl, imageelement) {
  var knownImagePromise;
  var urlsToCheckPromises = [];
  var matches;
  var imageurl_cdnnum;
  var linkurl_cdnnum;
  var imageurl_cdnnumstr;
  var linkurl_cdnnumstr;
  var imageurl_path;
  var linkurl_path;
  var blogurl_base;
  var knownImageResult;
  var bestImageUrl;
  var bestImageSize;
  var bestImageIsOG;

//console.info("checkAllUrlheaders-Enter: ", "imageurl:" + imageurl + "   linkurl:" + linkurl + "   blogurl:" + blogurl);
  
  // get CDN image server number as int and as string
  matches = imageurl.toLowerCase().match("https?:\/\/cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\/"); 
  if (matches !== null) {
    imageurl_cdnnumstr = matches[1];
    imageurl_cdnnum = parseInt(matches[1], 10);
  }  else {
    imageurl_cdnnum = NaN;
  }
  // get CDNO image server number as int and as string
  matches = linkurl.toLowerCase().match("https?:\/\/cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\/"); 
  if (matches !== null) {
    linkurl_cdnnumstr = matches[1];
    linkurl_cdnnum = parseInt(matches[1], 10);
  }  else {
    linkurl_cdnnum = NaN;
  }

  // get non-hostname part of the url, including leading "/" (NOTE: fixes buggy urls with multiple "//" after the hostname)
  matches = imageurl.match(/https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)/i);
  if (matches !== null) {
    imageurl_path = matches[1];
  } else {
    imageurl_path = null;
  }
  // get non-hostname part of the url, including leading "/" (NOTE: fixes buggy urls with multiple "//" after the hostname)
  matches = linkurl.match(/https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)/i); 
  if (matches !== null) {                                                            
    linkurl_path = matches[1];
  } else {
    linkurl_path = null;
  }
  
  // get base hostname for the originating blog
  if (blogurl !== null) {
    matches = blogurl.toLowerCase().match("https?:\/\/([^./]+\.bdsmlr\.com)"); 
    if (matches !== null) {
      blogurl_base = matches[1];
    } else {
      blogurl_base = null;
    }
  } else {
    blogurl_base = null;
  }
  
  // fetch promise for the image that is currently shown in the webpage (ALLOW redirection on this URL)
  knownImagePromise = checkUrlHeaderOnlyPromise(imageurl, true);

  // image to which the current unmodified link in the webpage is pointing to (ALLOW redirection on this URL)
  urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(linkurl, true));
  
  // Use a Set to collect all other URLs that are to be tested - this automatically eliminates dups that the URL selection logic below might create
  var uniqueset = new Set();

  // append "-og" suffix to the original image and link urls (without modifying the hostname)
  uniqueset.add(getOriginalImageURL(imageurl));
  uniqueset.add(getOriginalImageURL(linkurl));

  if (isNaN(imageurl_cdnnum) || imageurl_cdnnum <= 5) {
    //old CDN servers (cdn02 - cdn05, cdno02 - cdno05 or non-cdn location) 
    //  -> "Wild West" as to where the "-og" image variant might be "hiding"
    uniqueset.add(getOriginalImageURL("https://bdsmlr.com" + imageurl_path));
		if (blogurl_base !== null) {
      uniqueset.add(getOriginalImageURL("https://" + blogurl_base + imageurl_path));
    }  
    uniqueset.add(getOriginalImageURL("https://cdn02.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn03.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn04.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn05.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno02.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno03.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno04.bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno05.bdsmlr.com" + imageurl_path));
  } 

  if (isNaN(linkurl_cdnnum) || linkurl_cdnnum <= 5) {
    //old CDN servers (cdn02 - cdn05, cdno02 - cdno05 or non-cdn location) 
    //  -> "Wild West" as to where the "-og" image variant might be "hiding"
    uniqueset.add(getOriginalImageURL("https://bdsmlr.com" + linkurl_path));
		if (blogurl_base !== null) {
      uniqueset.add(getOriginalImageURL("https://" + blogurl_base + linkurl_path));
    }  
    uniqueset.add(getOriginalImageURL("https://cdn02.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn03.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn04.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdn05.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno02.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno03.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno04.bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno05.bdsmlr.com" + linkurl_path));
  } 
    
  if (!isNaN(imageurl_cdnnum) && imageurl_cdnnum > 5) {
    //new CDN servers (cdn06+)
    uniqueset.add(getOriginalImageURL("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
  } 
    
  if (!isNaN(linkurl_cdnnum) && linkurl_cdnnum > 5) {
    //new CDN servers (cdn06+)
    uniqueset.add(getOriginalImageURL("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
    uniqueset.add(getOriginalImageURL("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
  } 
    
  for (var str of uniqueset) {
    // some BDSMLR image servers have faulty redirections that are circular - causing huge delay and not resulting in anything useful
    // Thus, all URLs in the Set are to be tested WITHOUT allowing redirection
    urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(str, false));
  }
  
  // Get the  data for the image currently shown in the webpage as starting point
  knownImageResult = await knownImagePromise;
  bestImageUrl = knownImageResult.url;
  bestImageSize = knownImageResult.size;
  bestImageIsOG = isOriginalImageURL(bestImageUrl);

  // wait until all URLs have resolved (i.e. have their HTTP headers loaded with image or error info)
  await Promise.allSettled(urlsToCheckPromises).
    then( (results) => results.forEach((result) => {
      // If this result is a better image than currently known - replace and use this one as next best known image
      if ( (isOriginalImageURL(result.value.url) && !bestImageIsOG && (result.value.size > 5120)) ||
           ( (result.value.size > bestImageSize) && (result.value.size > 5120) && (isOriginalImageURL(result.value.url) == bestImageIsOG) ) ) {
        bestImageSize = result.value.size;
        bestImageUrl = result.value.url;
        bestImageIsOG = isOriginalImageURL(bestImageUrl);
      }  
    })); 
    
  return {url: bestImageUrl, size: bestImageSize, isOG: bestImageIsOG, image: imageelement}; 
}



// This ASYNC method gets the natural dimensions of the supplied image
// This means the image needs to be downloaded fully, unfortunately!
// Thus, a delay is to be expected, except if the image is already cached
// Depending on the image height, the method suggests a "markup color" and then discards the downloaded image again (but does not invalidate cache).
// "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise then function of the caller
async function getImageDimensionsPromise(imageurl, divelement, imagesize) {
  var image;
  var imageH;
  var imageW;
  var color;
  
  // sanity check - skip full download of image if it is larger than 20MB
  if ( (imagesize !== null) && (imagesize > 20971520) ) {
    return {url: imageurl, element: divelement, width: "unknown", height: "unknown", color: "Grey", size:imagesize}; 
  }
    
  image = new Image();
  image.src = imageurl;
  
  await image.decode().then(function() {
    imageH = image.naturalHeight;
    imageW = image.naturalWidth;
  });
  
  image.src = "data:,"; // clear the image now that we no longer need it

  if (imageH >= 2160) {
    color = "SpringGreen";
  } else if (imageH >= 1080) {
    color = "Green";
  } else if (imageH >= 810) {
    color = "YellowGreen";
  } else if (imageH >= 540) {
    color = "Yellow";
  } else if (imageH >=270 ) {
    color = "Orange";
  } else if (imageH < 270 && imageH > 0 ) {
    color = "Red";
  } else {
    color = "Grey";
  }
    
  return {url: imageurl, element: divelement, width: imageW, height: imageH, color: color, size:imagesize}; 
}







//------------------------------------------------------------
// 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

// fix buggy redirection from BDSMLR
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) );
}
// prevent running for URL to image or media
else if ( !(window.location.href.includes('bdsmlr.com/uploads/')) ) {
  
  // 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
  // "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: true };
  // 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);
  }

}