Raw Source
brucebentley / Eza's Image Glutton

// ==UserScript==
// @name           Eza's Image Glutton
// @version        1.42.7
// @description    Redirects to high-resolution images on gallery sites, skipping past descriptions & comments.
// @author         Ezalias (https://greasyfork.org/en/users/4876-ezalias)
// @copyright      2021, Bruce Bentley (https://github.com/brucebentley)
// @license        MIT; https://opensource.org/licenses/MIT
// @homepage       https://greasyfork.org/en/users/4876-ezalias
// @namespace      https://github.com/brucebentley
// @match          *://*.deviantart.com/*/art/*
// @match          *://*.deviantart.com/art/*
// @match          *://*.media.tumblr.com/*/s*
// @match          *://*.tumblr.com/*
// @match          *://*.tumblr.com/image/*
// @match          *://gfycat.com/*
// @match          *://*imgur.com/*
// @match          *://i.imgur.com/*.gifv
// @match          *://luscious.net/c/*
// @match          *://luscious.net/pictures/c/*
// @match          *://pbs.twimg.com/media/*
// @match          *://r34hub.com/en/show/*
// @match          *://www.gifdeliverynetwork.com/*
// @match          *://www.redgifs.com/watch/*
// @exclude        *://pbs.twimg.com/media/*=orig*
// @exclude        *://www.deviantart.com/users/outgoing?*
// @exclude        *?comment*
// @exclude        *&pid=*
// @exclude        *#c*
// @exclude        *#comment*
// @exclude        *#dnr
// @run-at         document-idle
// @noframes
// @require        https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js?updated=20180103
// @downloadURL    https://raw.githubusercontent.com/brucebentley/userscripts/main/src/media/images/ezas-image-glutton.user.js
// @updateURL      https://raw.githubusercontent.com/brucebentley/userscripts/main/src/media/images/ezas-image-glutton.user.js
// @grant          none
// ==/UserScript==

// Any single-image submission will redirect to the full-size image. On multi-image submissions, every page except the first will redirect to its full-size image.
// If you go 'back' to the normal gallery page (to favorite the image, read its description, leave a comment, etc.) then this script will not send you forward again.
// https://greasyfork.org/scripts/4713-eza-s-image-glutton
// https://sleazyfork.org/scripts/4713-eza-s-image-glutton

// Commentary on the UserScript block:
//   @exclude #dnr                                        - This string is appened to a URL when redirecting, to prevent back-trapping.
//   @exclude http://www.deviantart.com/users/outgoing?*  - Archaic misfire prevention; should probably be changed to automatically redirect through.
//   @exclude ?comment etc.                               - Site-specific cases where redirecting to the image is not desirable. E.g. when linked to individuals comments / comment pages.

// global variables, for simplicity
let image_url = ''; // location of the full-size image to redirect to
let wait_for_dnr = false; // some site URLs use '#' liberally, so if this const isn't empty, only '#dnr' will stop a redirect
let simple_redirect = false; // some domains are kicking back my JS redirect (for native referral), so do naive location=url instead
let page_failed = false; // If the page 503s or otherwise forces us to reload, wait a moment, then reload.
let interval_handle; // In case we need to set an interval, this is the global handle to kill it. Because a simple 'die' or 'clearInterval( this )' would be too much to ask.
let assume_extension = true; // Most sites should obviously point to an image, so if there's no file extension, guess '.jpg'. This breaks DeviantArt.

// detect site, extract image URL, then decide whether or not to redirect
switch (
  document.domain.replace('www.', '') // Remove 'www' to avoid cases where both example.com and www.example.com are supported.
) {
// ------ Simple extract_image_url_after / get_link_with_text / querySelector sites
case 'gfycat.com':
  extract_image_url_after('og:video', '//');
  break;
case 'redgifs.com':
  extract_image_url_after('og:video', '//');
  break;
case 'gifdeliverynetwork.com':
  extract_image_url_after('og:video', '//');
  break;
case 'r34hub.com':
  image_url = document.querySelector('.media-wrapper img, .media-wrapper source').src;
  break;
// ------ Slightly complicated extract_image_url_after sites
case 'luscious.net':
  image_url = document.getElementsByClassName('icon-download')[0].href;
  wait_for_dnr = true;
  break;
case 'i.imgur.com':
  image_url = document.querySelector('source').src;
  break;
// ------ Simple custom sites
case 'pbs.twimg.com':
  image_url =
    window.location.href
      .split('&')
      .filter((s) => !s.match('name='))
      .join('&') + '&name=orig';
  if (image_url.indexOf('?') < 0) {
    image_url = image_url.replace('&', '?');
  } // string.match( '?' ) reads the ? like a regex. In quotes. Ugh.
  assume_extension = false;
  break;
}
// ------ Holdovers from the previous method; domains that don't neatly conform to document.domain switch selection.
if (domain('tumblr.com')) {
  // 2020 Tumblr is some bullshit. You cannot get a bare image. It always redirects to a page. But it's no longer a page with the large-sized image, because go fuck yourself. But: the general /image page we're still somehow redirecting from does have a large image... which you cannot right-click and save. I have fucking tried. Just bullshit of the highest order, instead of using CORS to deny hotlinks and just delivering a goddamn JPG.
  extract_image_url_after('"og:image"', 'http');
  simple_redirect = true;
  // https://66.media.tumblr.com/99be3174c6066bb219cca0008f351ce5/4692a02d2fc92812-d6/s500x750/bd6f4ec02cc4a9aa036f3274b9c8af447e285a59.png
  if (document.querySelector('a')) {
    image_url = image_url.replace(/\/s\d*x\d*\//, '/s3072x3072/'); // E.g. s400x600, s640x960.
    image_url = image_url + '#dnr'; // Prevent infinite loops
    // Adding do-not-redirect sometimes breaks this, but I'd rather publish missing functionality than an infinite redirect trap.
    // I guess I should detect whether we're on an HTML page or a bare image?
  }
}
if (domain('deviantart.com')) {
  scrape_deviantart();
  wait_for_dnr = true;
}

if (page_failed) {
  image_url = '';
} // If the page refused to load properly, do not redirect. (The delayed automatic reload is now in reload_if.)

// Don't redirect if the filetype is obviously not an image. SWF, TXT, MP3, etc.
// It's tedious to detect flash, story, and music pages on every website supported, so instead let's just cancel redirection based on those file extensions.
// Added ZIP & RAR because apparently DeviantArt lets you host 3D models and stuff. Automatically downloading those is not what this script is for.
const ext = image_url.substring(image_url.lastIndexOf('.') + 1, image_url.length); // e.g. 'png'
const not_images = ['mp3', 'swf', 'txt', 'docx', 'pdf', 'doc', 'rtf', 'midi', 'mid', 'wav', 'flv', 'cab', 'zip', 'rar'];
for (const n in not_images) {
  if (ext === not_images[n]) {
    image_url = '';
  }
} // If the extension is in our blacklist, don't redirect.
// Oh right. Doesn't work on FA because FA points to the icon. Yaaayfuck.

// Redirect as a function.
// Slightly clunky way to trigger this from website-specific functions. 'Don't repeat yourself.'
// Conveniently - any delay should (should) prevent Chrome from eating #dnr pages.
// The minimum value for setTimeout is probably 4ms or 10ms, but aiming lower can't hurt.
setTimeout(redirect, 1);

// Execution continues:

function redirect() {
  // Having defined image_url based on the page's HTML or DOM, modify the current URL to prevent back-traps, then redirect to that full image.
  let do_we_redirect = true; // If we've come this far we'll probably go to an image.
  if (image_url === '') {
    do_we_redirect = false;
  } // Don't redirect to an empty string. (Emptying this string is how some functions fail safe.)
  if (!wait_for_dnr && window.location.href.match('#')) {
    do_we_redirect = false;
  } // Don't redirect if the wait_for_dnr flag is false and there's a hash. (E.g. FA comments.)
  if (window.location.href.match('#dnr')) {
    do_we_redirect = false;
  } // Don't redirect if there's a #dnr in the URL.
  if (do_we_redirect === true) {
    // So much clearer than a mess of &&s and ||s.
    // some images don't redirect properly, even if you manually 'view image' - so we append '.jpg' to URLs without extensions, forcing the browser to consider them images
    // even if this doesn't work, the new URL should just 404, which is better than the semi-modal 'octet stream' dialog seen otherwise.
    if (assume_extension) {
      if (image_url.lastIndexOf('/') > image_url.lastIndexOf('.')) {
        image_url = image_url + '.jpg';
      } // if there's not a '.' after the last '/' then slap a file extension on there
      if (image_url[image_url.length - 1] === '.') {
        image_url = image_url + 'jpg';
      } // if the URL ends with a dot, slap a file extension on there
    }

    // modify current location, so that when the user clicks 'back,' they aren't immediately sent forward again
    const modified_url = window.location.href + '#dnr'; // add do-not-redirect tag to current URL
    history.replaceState({ foo: 'bar' }, 'Do-not-redirect version', modified_url); // modify URL without redirecting. {foo:'bar'} is a meaningless but necessary state object.

    image_url = encodeURI(image_url); // Executing code with strings from the page has always been a mildly horrifying attack surface - hopefully this defangs it.
    if (simple_redirect) {
      window.location.href = image_url;
      // This has different referral properties than clicking a link or displaying an image, so some sites 403
    } else {
      location.assign("javascript:window.location.href='" + image_url + "';");
    } // Pixiv-friendly redirect to full image: maintains referral, happens within document's scope.
  }
} // End of main execution.

// ----- //         Functions for readability

function extract_image_url_after(string_before_url, url_begins_with) {
  // extract the first quote-delimited string that appears after unique first const and begins with second var
  const html_elements = document.getElementsByTagName('html'); // this avoids doing getElementsEtc every time, while accessing the whole page HTML by reference
  const string_index = html_elements[0].innerHTML.indexOf(string_before_url); // find a unique string somewhere before the image URL

  if (string_index > -1) {
    const image_index = html_elements[0].innerHTML.indexOf(url_begins_with, string_index); // find where the image URL starts after the unique string
    const delimiter_index = html_elements[0].innerHTML.indexOf('', image_index); // find first doublequote after the image URL starts
    image_url = html_elements[0].innerHTML.substring(image_index, delimiter_index); // grab the image URL up to the next doublequote
  }
  //   return image_url;      // Debug
}

function reload_if(error_string) {
  // Put reload on a delayed thread  so that errors in the get-an-image-URL part can fail safely.
  if (document.body.innerHTML.match(error_string)) {
    // Look for a string indicating the page failed to load
    page_failed = true; // Prevent spurious redirection.
    setTimeout(function () {
      location.reload();
    }, Math.floor(Math.random() * 10 + 1) * 1000);
    // 1s-10s pause. Most errors will be 503s, so let's avoid hammering the site like a naive spider. .
  }
}

function get_link_with_text(string_in_text) {
  //   image_url = Array.from( document.querySelectorAll( 'a' ) ).find( a => a.textContent.match( string_in_text ) ).href;    // Goddamn TypeErrors.
  let link = Array.from(document.querySelectorAll('a')).find((a) => a.textContent.match(string_in_text));
  if (link) {
    image_url = link.href;
  }
}

// Sensible 'are we on this site or not?!' function.
function domain(ending) {
  let want = ending.split('.').reverse(); // Reverse order, from TLD to domain to subdomain(s).
  let have = document.domain.split('.').reverse();
  for (let n = 0; n < want.length; n++) {
    if (want[n] !== have[n]) {
      return false;
    }
  } // Implicit else
  return true;
}

// ----- //         Functions for individual websites (separated for being especially long)

// Keeping old notes about Deviantart is pointless because there is nothing constant or reliable about this entire goddamn website.
function scrape_deviantart() {
  image_url = (document.querySelector('link[href*="token="]') || { href: '' }).href; // Can't wait for object?.property syntax.
  if (image_url === '') {
    image_url = Array.from(document.head.getElementsByTagName('meta')).filter((meta) => meta.content.match('fullview'))[0].content;
  }
  // You'd think 'fullview' would always be preferable, but on e.g. https://www.deviantart.com/thecynicalhound/art/Tofauti-Sawa-169-781760760 it's a JPG of a PNG submission.
  // Pretty sure the meta-fullview thing doesn't even work anymore.
}

/*
Test suite of random URLs from the relevant sites:
http://rule34.xxx/index.php?page=post&s=view&id=1399731
http://rule34.xxx/index.php?page=post&s=view&id=1415193
*/