您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Turn a page of thumbnails into high-res images
当前为
// ==UserScript== // @name Eza's Gallery Swallower // @namespace https://inkbunny.net/ezalias // @description Turn a page of thumbnails into high-res images // @license MIT // @license Public domain / no rights reserved // @include https://www.pixiv.net/en/users/* // @include https://www.pixiv.net/bookmark_new_illust.php* // @include https://www.pixiv.net/en/tags/*/artworks* // @include https://www.pixiv.net/user/*/series/* // @include https://gelbooru.com/index.php?page* // @include https://e621.net/posts* // @include https://e926.net/posts* // @include https://*.booru.org/*s=list* // @include http://www.hentai-foundry.com/pictures/user/* // @include https://baraag.net/@* // @exclude *#dnr // @noframes // @version 2.6.14 // @grant GM_registerMenuCommand // ==/UserScript== // Create a vertical view for high-res versions of all the image links you can see. // This includes all pages from multi-image submissions, where those exist. // Individual images (or whole submissions) can be removed with one click. // Each image is also a link. // This script is no longer a mess. These notes, well, they're okay. // Oh, and the CSS is this way because I use a tallscreen monitor. Change img.short's max-height if that annoys you. // Scroll down past the wall of complaining to get to the code. // It might not even be possible to identify what error code onError caught. Jesus Christ, HTML. // Fetch still doesn't work on Pixiv. // fetch( 'https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/05/23/10/59/10/81782474_p0_square1200.jpg', { credentials: 'include' } ).then(response => response.json()).then(data => console.log(data)); - returns an empty object, but never, ever logs anything. Without credentials there's at least a CORS error. // I literally just want to see the response. It should not be possible to not get that information - 'fine' is 200, 'fuck off' is 403, and even no response is 500. // Oh for fuck's sake. I just tested it in mainstream Firefox to see if it was my old browser being shitty - FF still throws a CORS error. // It's i.pximg.net from pixiv.net. Aaaugh. // I just want to know if a fucking image file exists! This is not an attack vector - you can do <img src onerror> and find the same damn information. Domain-agnostic. // Do old-school XMLHttpRequest()s work? I only want the head. Ugh, probably still stopped by CORS. // Javascript: the language where reading a file is prohibited for safety, but executing it as a script is no-questions-asked. // ... can we just run it as a script and see if it 404s? Obviously it won't work if it exists. // At this point I just need to style=visibility:hidden when things scroll offscreen. Same size, and god willing, no VRAM use. // Maybe an interval that does this.src=this.src for all img elements - to fight when things half-load? Can't exactly press F5. // Array.from( document.getElementsByTagName( 'img' ) ).forEach( i => i.src = i.src ) - inconsistently effective. // I could add ?random-number to force a reload, but that's a complete re-download, so it's inadvisable. Can I check if it failed to-- oh right, onError. // But I need to know what the error is, since we're still removing 404'd img elements with bad file extensions. // I should maybe move or copy the thumbnails into a top bit, like Pixiv Fixiv. // Also, like Pixiv Fixiv, I should probably have the ability to repopulate existing sub-spans with images, // both for "augh all PNGs slow loading" and "ah fuck loading broke." // What I have so far could easily replace Pixiv Fixiv's current code. The spans with this.parentElement.remove() stuff, anyway; we know the image count beforehand. // For both, I'd like to stretch a thumbnail behind an image as it's loading. I'd need non-square thumbs. Or at least I'd really prefer them. // I guess style=backgroundWhatever:that.src? I keep adding and removing this -> that for some damn reason. // Change the BG color to something dark. (Ego says #324.) // Testing whether loading shenanigans work: just make images visible on mouseover. // This accidentally ignores muted images - I'm calling that a feature. // Ugoira animations get a spot, but don't load an image. Semi-feature. Deserves better handling and indication. // Clunky solution to memory problems: when an image loads, remove it, and put its URL in a list. // This script suggests a generic solution to Twitter nonsense: replace tweet divs with their media. Break classes and IDs so their infinite-scrolling JS can't remove them. No worries about their stupid random class names, since we'd just parent.parent.parent and then .replace(). // Also I could probably just .remove() the sidebar bullshit. // Relying on onError suuucks. I categorically NEED a way to distinguish 'didn't finish loading' from 'file not found.' // I can maybe get around this by assuming all the thumbnails load. They're tiny and quick. // So if a thumbnail removes itself and produces a div where one file extension is expected to load, // and that element has no children, we can infer the load failed, and start over with the three separate file extensions. // Maybe ctrl+z instead of a per-image 're-show' button? Stick per-image-group IDs on a list, first in last out, as they're removed. // Still might need per-submission do-over buttons for when loading is borked. Still want them, anyway. // Be positive: have an onLoad for each image format. Signal success, not implicit failure. // Or I guess do both: onError, check for success? // Even just onError not removing an image if the other two are already gone would safely assume it timed out while the others 404'd. // Getting decent. Clone Pixiv Fixiv scaling modes, via changing image class and body class. Maybe go for unicode symbols instead of words. // E.g. <-->, the vertical version of that, the cross version... '1:1' could just be text. Point is: not English. // I can probably also do one-page-onscreen instead of as-it-comes vertical spacing, since I have divs around each image. // And use a damn dark mode already! // Buttons for forward/back? Floating over top, maybe. scrollTo stuff. Ech, but it has to update as you manually scroll down, so prev/next are at least consistently relative. // Ideally the focus is somewhere in the middle of the screen, not like, one scanline of an image counts as being 'on' that image. // Per-image removal buttons could be larger and lower-contrast. Big easy target. Ignorable. // Spinner widget for individual images loading? I.e., keep spinning while the page is unfinished. That might be as simple as a DOM check. // body.onload happens repeatedly, yeah? // Style: thinking slow rotation, lower contrast, possibly inside the other spinner. // I probably have to set/reset body.onError every time a new big-image starts loading. // onstalled? onwaiting? onshow, for other things. Is oninvalid different from onerror? // Count beside spinners? 'X images, N loading.' // I could trivially turn this into a Pixiv Fixiv replacement - right? All I need is any image URL. No JSON shenanigans. // A stand-in for images that haven't loaded would be nice. Maybe their thumbnail, in thumbnail size. Just tell me when the top manga will affect scrolling. // Killing the submission reeeally needs to kill the underlying images. Maybe add another sub-sub-sub-span for next_image to target, and remove that. Then reloading the submission recreates that span before calling next_image on p0. // Come on, I can hack together previous / next page links. Only at the bottom. Doesn't trigger automatically. Just slap it in there. // https://www.pixiv.net/en/users/1346633/artworks?p=2 -> p=1 and p=3. Needs to handle bare /artworks to infer ?p=1. // Add buttons for scrolling to prev/next image/submission. Left-aligned, top of each image. // Long-term issue: this looks boring on a widescreen monitor. It's all tallscreen settings. Massive negative space otherwise. // Needs a big reload-all button at the top. // If I'm being clunky, I can probably grab image count from the counters in the corners. // Kinda want a "remove all first images" button. So many censored thumbnails. Japan - fuck off already. We're sorry we colonialized you. Stop ruining porn. // Ooh, Array.find(). Pass it a test function and it'll return the first element that matches. // This could trivially work on Baraag. (And other Mastodon sites.) // Oh, file-extension issues in Universal Scraper don't apply here. // e621. // Gelbooru: // https://thumbs.gelbooru.com/0e/dd/thumbnail_0edd1b5e5d7a1ba2488a135d60d70c78.jpg // https://img2.gelbooru.com//images/0e/dd/0edd1b5e5d7a1ba2488a135d60d70c78.jpg // Image Toolbox on Pixiv shows the toolbox centered... with text... at the top of the screen. What is even the fuck. // This goes beyond "CSS is bullshit." This is getting into "CSS is haunted." // Removed the style making all spans position:relative, and that fixed the position, but there's still text. Only on Pixiv's bookmarks page. Why. // Gelbooru is inconsistent about WebMs. Some resolve to images. Others are blank. Dunno which is better, but pick one. // https://gelbooru.com/index.php?page=post&s=list&tags=rosiekawaii // Gelbooru is inconsistent about images as well. https://gelbooru.com/index.php?page=post&s=view&id=5884654#dnr#&dnr // https://gelbooru.com/index.php?page=post&s=list&tags=batako1812+manyuu_chifusa // https://img3.gelbooru.com//images/a5/de/a5de992f2e7cd27a79ffdc35e9ece3e1.jpeg // Oh, right, JPEG. Duh. I even anticipated that. // Still, address videos on Gelbooru. E.g. https://gelbooru.com/index.php?page=post&s=view&id=5614716&tags=rosiekawaii#dnr#&dnr // https://img3.gelbooru.com/images/3c/23/3c2376210444f7a7da737b412c722faa.jpg // https://img3.gelbooru.com//images/3c/23/3c2376210444f7a7da737b412c722faa.webm // Totally doable. So how do I check for a file without just embedding videos? Ugh, might be beyond onload / onerror behavior. // Orrrr I could check tags. Not reliable. Could be wrong. But would allow a <video> with onerror to fall back to images. // Better idea: indicate that it's probably a video, and link to the page without #dnr&dnr. Use Eza's Image Glutton, folks. // Obviously that's the Ugoira solution on Pixiv: "don't." So it should be how I handle Ugoiras now that they're broken-ish. // Embedding videos is undesirable because it implies linking to them for download. // This script is already rude on bandwidth - videos would make it a DDOS attack. // Solution: show thumbnail, not linked. (To avoid DownThemAll grabbing the thumbnail.) // FurAffinity? // https://t.furaffinity.net/[email protected] // https://d.furaffinity.net/art/ratcha/1613453026/1613453026.ratcha_party.jpg // Nope, needs a fetch. // CSS-only? Absurd tangent, but GreasyFork now lists CSS userscripts. It's probably possible to replace thumbnails on gallery websites with RegEx'd full-size versions, and fallbacks for each file extension. And obviously I think you can insert a line-break ::after each image and make them full-size or max-width / max-height. But yeesh. // Visions of replacing filter-filter-filter chains with querySelectorAll fell through because A>B[C] always grabs B... not A. // let things = Array.from( document.querySelectorAll( 'a > img[src*="thumbs"]' ) ); // You can get all images contained by a link. You cannot get all links containing an image. // ... though you could get the images and do .parentElement a bunch. // .closest( 'a' ) seeeeems to work. I do not know if it prefers hierarchy or in which direction. // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest // Oh: it is explicitly going up the chain. Kickass, perfect. // Might use thumbnails of first image as scroll-into-view links at the top of the page. // Maybe with Xs on each to remove things from there as well? Questionable. // Probing is possible without thumbnails - just onLoad( classList add "ready" ). Same results, but slower. Meh. // Genuine bugs: // https://www.pixiv.net/user/55117629/series/87453?p=2 - gets everything but the manga. Different page format. // Ugoira submissions display a big floating X, because those belong to the image-extension-trying block, not to each self-removing image. // Ahh, it's because next_image would try _p0 and fail. This rewrite assumes the first image works. Maybe do cleanup in the interval? // Dead images and possibly thumbnails take up horizontal space and cause slight movements while loading. // Spinners don't work on Baraag. // And how could they? We never update per-element style. Even if @animation worked, it'd be stuck there. // Changes since last version: // Fixed Baraag.net previous / next controls. (PEBKAC.) // ------------------------------------ Custom replacement HTML ------------------------------------ // // Replacement page. Not used immediately; it just makes more sense up here. var html = ''; var style_rules = new Object; // CSS "selector": "style" map. Blame Baraag. // ----- // CSS // CSS rules as an associative array, so they can be applied per-element on uncooperative sites. // Image style(s): style_rules[ "img.short" ] = "max-width: 90vw; max-height: 60vh; z-index: 10; vertical-align: middle;"; // Dead spinners: style_rules[ '.spacer' ] = 'position: absolute; width: 0px; height: 0px;'; style_rules[ '.other_spacer' ] = 'position: absolute; width: 0px; height: 0px;'; /* Arbitrarily smaller. */ style_rules[ '#image_counter' ] = 'position: absolute; left: 70px; top: 5px; font-size: 33px;'; // Controls: style_rules[ '.remover' ] = 'background-color:#d7dbd8; border-radius: 50%; width: 60px; height: 60px; text-align: center; display: inline-block; border:1px solid #ab1919; cursor:pointer; line-height: 20px; color:#4d1919; font-family:Arial; font-size:17px; padding: 10px 10px; text-decoration:none;'; style_rules[ '.remover:hover' ] = 'background-color:#bd2a2a;'; style_rules[ '.floating' ] = 'z-index: 1; position: absolute; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%); width: 120px; height: 120px; font-size:45px;'; style_rules[ '.reloader' ] = 'background-color:#dbd7d8; border-radius: 50%; width: 60px; height: 60px; text-align: center; display: inline-block; border:1px solid #19ab19; cursor:pointer; line-height: 20px; color:#194d19; font-family:Arial; font-size:33px; padding: 10px 10px; text-decoration:none;'; style_rules[ '.reloader:hover' ] = 'background-color:#2abd2a;'; // Hidden thumbnails, probing to check for next image in multi-image sets: style_rules[ '.test, .thumb' ] = 'display:none'; // Push all of that into a <style> block: html += '<style> '; for( selector in style_rules ) { html += selector + ' { ' + style_rules[ selector ] + ' } \n'; } html += '</style> '; // Spinners: html += '<style> .submissions_loader { position: absolute; left: 0px; top: 0px; border: 8px solid #3498db; border-top: 8px solid #111111; border-bottom: 8px solid #111111; border-radius: 50%; width: 48px; height: 48px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>'; html += '<style> .images_loader { position: absolute; left: 8px; top: 8px; z-index: -1; border: 24px solid #db9834; border-top: 24px solid #aaaaaa; border-bottom: 24px solid #AAAAAA; border-radius: 50%; width: 0px; height: 0px; animation: images_spin 3s linear infinite; } @keyframes images_spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>'; // Either I bodged the implementation of style_rules to a <style> block, or there's some other obstacle to animations. So these are separate. // ----- // Page elements html += '<div class="submissions_loader" id="submissions_spinner"></div>' /* Spinner for submissions, 'new images being found.' */ html += '<div class="images_loader" id="images_spinner"></div>' /* Spinner for images, 'images loading in high-res.' */ html += '<span id="image_counter"></span>'; /* This detaches. */ html += '<br><br><center><span id="centered"></span></center>'; /* Where stuff goes. */ html += '<br><br><br>'; /* Spacing for prev/next links. */ html += '<center><span id="links"></span></center>' html += '<br><br><br><br><br>'; /* Runout. */ // Additional HTML may be added by per-site functions. Once injected, we use the DOM. // ------------------------------------ General setup & Per-site functions ------------------------------------ // // ----- // Global variables var trigger_size = [ 15, 70, 25, 10 ]; // Left, top, font-size, padding. Pixiv defaults. var button_delay_function = false; // Should maybe have separate boolean and function. var proper_fetch = false; // If true, we fetch pages instead of modifying thumbnail URLs. (Not yet implemented.) var top_to_bottom = false; // Chronological order is confusing to describe. var automatic_pagination = false; // Pixiv-style p0, p1, p2, etc. var gather_items; // Per-site function to scrape page contents. var items; // Scraped contents of page. var formats = [ ".png", ".jpg", ".gif", ".jpeg" ]; // File extensions. (Edit: no sense not having JPEG by default.) var page_number = 1; // Default. var next_page, previous_page; // URLs of obvious purpose. var thumb_followup = ''; // Used to be part of "items," per-item, but only Pixiv uses them, and they're standard. var image_followup = ''; // Ditto. var new_image_rate = 200; // Milliseconds between checks for "ready" submissions. Lower is faster. var force_style = false; // If sites don't allow inline CSS, apply style_rules to each element. // ----- // Helper functions // Turn window.location.search into a sensible associative array. Should be standard, guys! function parse_search( search_string ) { let strings = search_string.split( /[\?\&]/ ) .filter( v => v ) // Remove empty strings let associative = new Object; strings.forEach( v => associative[ v.split('=')[0] ] = v.split('=')[1] ); return associative; } function scrub_extensions( url, format_list ) { if( format_list == null ) { format_list = formats; } // Shut up, it's global. format_list.forEach( ext => url = url.replace( ext, '' ) ) return url; } // ----- // Per-site setup and gather functions var args = parse_search( window.location.search ); // DRY switch( document.domain.replace( 'www.', '' ) ) { case 'pixiv.net': thumb_followup = '_square1200'; formats = [ ".png", ".jpg", ".gif" ]; // I have never seen a JPEG on Pixiv. I have 100,000 _p0 JPGs, and they're all ".jpg". automatic_pagination = true; // Ever-useful test profile: https://www.pixiv.net/en/users/53625793 button_delay_function = () => document.querySelector( 'img[src*="250x"]' ) || document.querySelector( 'a *[style*="240x"]' ); // https://www.pixiv.net/bookmark_new_illust.php?p=2 if( args["p"] ) { page_number = parseInt( args["p"] ); } next_page = window.location.origin + window.location.pathname + "?p=" + (page_number + 1) previous_page = window.location.origin + window.location.pathname + "?p=" + (page_number - 1); // And now per-image removers are at the bottom again. because CSS is punishment for mankind's hubris. gather_items = function() { // Not sure the Array-map approach clarified or simplified anything, in Pixiv's case... return Array.from( document.querySelectorAll( 'a[href*="/art"] img[src*="c/2"]' ) ) .map( v => v.closest( 'a' ) ) // https://www.pixiv.net/en/users/53625793 .map( v => new Object( { page: v.href, title: v.href.split('/').pop(), // https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/06/22/05/58/05/82488314_p0_square1200.jpg // https://i.pximg.net/img-original/img/2020/06/22/05/58/05/82488314_p0.jpg thumb: scrub_extensions( v.querySelector( 'img' ).src ) .replace( '_custom1200', '' ) .replace( '_master1200', '' ) .replace( '_square1200', '' ) .replace( '_p0', '_p' ), image: scrub_extensions( v.querySelector( 'img' ).src ) .replace( '_custom1200', '' ) .replace( '_master1200', '' ) .replace( '_square1200', '' ) .replace( '_p0', '_p' ) .replace( 'c/250x250_80_a2/', '' ) .replace( 'c/240x240/', '' ) .replace( 'img-master/', 'img-original/' ) .replace( 'custom-thumb/', 'img-original/' ) // Some of these are redundant but it's not worth figuring out which. } ) ) .concat( Array.from( document.querySelectorAll( 'a *[style*="background-image"]' ) ) .map( v => v.closest( 'a' ) ) // https://www.pixiv.net/bookmark_new_illust.php?p=2 .map( v => new Object( { page: v.href, title: v.href.split('/').pop(), // url("https://i.pximg.net/c/240x240/img-master/img/2021/02/17/04/05/41/87838098_p0_master1200.jpg") // https://i.pximg.net/img-original/img/2021/02/17/04/05/41/87838098_p0.jpg thumb: scrub_extensions( v.querySelector( 'div[style]' ).style.backgroundImage ) .replace( 'url("', '' ) .replace( '")', '' ) .replace( '_custom1200', '' ) .replace( '_master1200', '' ) .replace( '_square1200', '' ) .replace( '_p0', '_p' ), image: scrub_extensions( v.querySelector( 'div[style]' ).style.backgroundImage ) .replace( 'url("', '' ) .replace( '")', '' ) .replace( '_custom1200', '' ) .replace( '_master1200', '' ) .replace( '_square1200', '' ) .replace( '_p0', '_p' ) .replace( 'c/250x250_80_a2/', '' ) .replace( 'c/240x240/', '' ) .replace( 'img-master/', 'img-original/' ) .replace( 'custom-thumb/', 'img-original/' ) } ) ) ) /* let things = Array.from( links ) // There's probably a less-verbose querySelectorAll for this. .filter( a => a.href ) .filter( a => a.href.indexOf( '/artworks/' ) > 0 ) .filter( a => a.querySelector( 'img' ) ) .filter( a => a.querySelector( 'img' ).src ) .filter( a => a.querySelector( 'img' ).src.match( 'c/2' ) ); // document.querySelectorAll( 'a[href*="/art"] img[src*="c/2"]' ) things = things.concat( Array.from( links ) // Sort and reverse: side-effectful. Concat: functional. Go fuck yourself, Javascript. .filter( a => a.innerHTML.match( 'background-image' ) ) ) // document.querySelectorAll( 'a *[style*="background-image"]' ) let items = new Array; things.forEach( (v,i,a) => { let item = new Object; item.page = v.href; item.title = v.href.split('/').pop(); // Just the number - large print. // https://i.pximg.net/c/250x250_80_a2/img-master/img/2020/06/22/05/58/05/82488314_p0_square1200.jpg // https://i.pximg.net/img-original/img/2020/06/22/05/58/05/82488314_p0.jpg if( v.querySelector( 'img' ) ) { // Would it kill you to return null for all properties of null? Does it have to be a TypeError? item.thumb = v.querySelector( 'img' ).src } else { item.thumb = v.querySelector( 'div[style]' ).style.backgroundImage .replace( 'url("', '' ) .replace( '")', '' ); } item.thumb = scrub_extensions( item.thumb ) item.thumb = item.thumb .replace( '_p0', '_p' ); // Remember to add a page number. item.thumb = item.thumb .replace( '_custom1200', '' ) .replace( '_master1200', '' ) .replace( '_square1200', '' ); item.image = item.thumb .replace( 'c/250x250_80_a2/', '' ) .replace( 'c/240x240/', '' ) .replace( 'img-master/', 'img-original/' ) .replace( 'custom-thumb/', 'img-original/' ); // And still remember to add a page number. items.push( item ); } ); return items; */ } break; case 'gelbooru.com': // https://gelbooru.com/index.php?page=post&s=list&tags=shuujin_academy_uniform+chair+1girl+pink_background trigger_size = [ 15, 35, 16, 5 ]; // Left, top, font-size, padding. // https://gelbooru.com/index.php?page=post&s=list&tags=4girls // https://gelbooru.com/index.php?page=post&s=list&tags=4girls&pid=42 // https://gelbooru.com/index.php?page=comment&s=list&pid=10 // Whoops. page_number = 0; let pages_at_once = 42; if( window.location.href.match( 'page=comment' ) ) { pages_at_once = 10; } if( args["pid"] ) { page_number = parseInt( args["pid"] ); } next_page = window.location.href + "&pid=" + (page_number + pages_at_once); // Flawed, but it works. previous_page = window.location.href + "&pid=" + (page_number - pages_at_once); gather_items = function() { return Array.from( document.querySelectorAll( 'a[href*="s=view"]' ) ) .map( v => new Object( { // https://gelbooru.com/index.php?page=post&s=view&id=4179699&tags=pink_background page: v.href, title: v.href.split( '&id=' )[1].split( '&' )[0], // https://img3.gelbooru.com/thumbnails/f5/c7/thumbnail_f5c7826072943fd72076ba9121b473f0.jpg // https://img3.gelbooru.com/images/f5/c7/f5c7826072943fd72076ba9121b473f0.jpg thumb: scrub_extensions( v.querySelector( 'img' ).src ), image: scrub_extensions( v.querySelector( 'img' ).src ) .replace( '/thumbnails', '/images' ) .replace( 'thumbnail_', '' ) } ) ) } break; // Janky e621 issues seem to be because JS isn't allowed to run on the page? Somehow? It's not ScriptBlock. // "The page’s settings blocked the loading of a resource at self." // Aaaargh there's probably some horrible version of this where I fake on_error and on_load inside this script's interval function. // Intermittent issue: just had e621's images all disappear. Second time this has happened. Not sure the first time was on this site. // Both times, the page had been sitting open (second monitor) for like a minute. Fresh reload before that. Console almost certainly open. // img elements still show valid src. // .post-preview, #image-container, #c-comments .post, .mod-queue-preview.post-preview, .post-thumbnail { visibility: hidden !important; } // ... so why did that get set? It's triggering on .post-preview. That's at the <article> level... for some reason. // This code only touches article elements to check for blacklisting, it only does that if gather_items is called. Which it wasn't. case 'e621.net': case 'e926.net': // https://e621.net/posts?tags=somik+mirror trigger_size = [ 15, 50, 16, 5 ]; // Left, top, font-size, padding. // https://e621.net/posts // https://e621.net/posts?page=2 // https://e621.net/posts?tags=asthexiancal++ // https://e621.net/posts?page=2&tags=asthexiancal++ if( args["page"] ) { page_number = parseInt( args["page"] ); } next_page = window.location.href + "&page=" + (page_number + 1); // Insufficient on https://e621.net/posts if( next_page.indexOf( '?' ) < 0 ) { next_page = next_page.replace( '&', '?&' ); } // Klunk. previous_page = window.location.href + "&page=" + (page_number - 1); gather_items = function() { return Array.from( document.querySelectorAll( 'a[href*="posts/"] img' ) ) .map( v => v.closest('a') ) .filter( v => ! v.closest('article').className.match( 'blacklisted-active' ) ) // Exclude hidden posts. .map( v => new Object( { // https://e621.net/posts/1333873?q=somik+mirror page: v.href, title: v.href.split( 'posts/' )[1].split( '?q=' )[0], // https://static1.e621.net/data/preview/37/75/3775cd8664c688f98a41780f6796ce86.jpg // https://static1.e621.net/data/37/75/3775cd8664c688f98a41780f6796ce86.png thumb: scrub_extensions( v.querySelector( 'img' ).src ), image: scrub_extensions( v.querySelector( 'img' ).src ) .replace( '/preview', '' ) } ) ) } break; case 'hentai-foundry.com': var trigger_size = [ 15, 20, 25, 10 ]; // button_delay_function might be the only way to separate gallery pages from submission pages. HF's URLs are duuumb. // http://www.hentai-foundry.com/pictures/user/AmaZima/page/9 if( window.location.href.indexOf( '/page/' ) > 0 ) { page_number = parseInt( window.location.href.split( '/' ).pop() ); previous_page = window.location.href.split( '/page' )[0] + '/page/' + (page_number - 1); } next_page = window.location.href.split( '/page' )[0] + '/page/' + (page_number + 1); // page_number defaults to 1. gather_items = function() { return Array.from( document.querySelectorAll( 'a.thumbLink' ) ) .map( v => { // http://www.hentai-foundry.com/pictures/user/AmaZima/589016/Tired-but-happy-Lottie let username = v.href.split( '/' )[5]; // AmaZima let title = v.href.split( '/' )[6]; // 589016 return new Object( { page: v.href, title: title, // url("//thumbs.hentai-foundry.com/thumb.php?pid=589016&size=350") // http://pictures.hentai-foundry.com/a/AmaZima/589016/AmaZima-589016-Tired_but_happy_Lottie.png // http://pictures.hentai-foundry.com/t/Tixnen/869342/Tixnen-869342-Vasilina.jpg thumb: scrub_extensions( v.querySelector( 'span[style]' ).style.backgroundImage ) // Why would it be easy. .replace( 'url("', '' ) .replace( '")', '' ), image: window.location.protocol + "//pictures.hentai-foundry.com/" + username.slice( 0, 1 ).toLowerCase() + '/' + username + '/' + title + '/' + username + '-' + title + '-' + v.href.split( '/' ).pop().replace( /-/g, '_' ) } ) } ) } break; // Baraag... and Mastodon in general, ideally. // Oof. Baraag has multiple submissions per post, but they're not numerically related. // JS answer: let items.image be an array. Check typeOf and either show one or all. // Better JS answer: check typeOf and show one at a time, like Pixiv. (Easier to handle individual Xs that way anyhow.) // Bonus for Baraag, we can set formats to [""] and just show the correct image. // Also maybe delay when "ready" again, so we don't instantly re-ready this one submission. Avoid linearity. // We don't need a thumbnail value. Buuut we might have one anyway, if we implement Pixiv Fixiv style shortcuts at the top. // Instead of typeOf - item.thumbs vs item.thumb? Neither is especially elegant. Global boolean? // I am hard pressed not to just lean on typeof. Why keep track? If you pass an array of URLs, you get an array of images. // Out-of-bounds signalling for page count -might- still be useful for Pixiv. That'd let us skip the thumbnail-try stuff. // Argh, retweets ("boosts?") aren't of class h-entry. They're h-cite. Can I just querySelector with a comma? Sure can. // Guess that puts a nail in whether to grab links and filter down, or one-shot with complex CSS and work up. case 'baraag.net': formats = [""] force_style = true; // querySelectorAll, element.style = be round, dammit. // https://baraag.net/@Applalt/media // https://baraag.net/@Applalt/media?max_id=105165058865018699 // Yeesh. let navs = document.querySelectorAll( 'a[class*="load-more"]' ); if( navs ) { if( navs.length == 2 ) { previous_page = navs[0].href; next_page = navs[1].href; } // Both else if( window.location.href.indexOf( '?max_id=' ) > 0 ) { previous_page = navs[0].href; } // Last page else { next_page = navs[0].href; } // First page } // There is no concept of a page number. If previous_link is undefined, it simply won't display. gather_items = function() { // Is it better to get links and filter by children, or select precisely and map to ancestors? // Certainly it's clearer to get links and filter. Dunno if performance matters. // return Array.from( document.querySelectorAll( '.h-entry div[data-props*="media"]' ) ) // .map( v => v.closest( '.h-entry' ) ) return Array.from( document.querySelectorAll( '.h-entry, .h-cite' ) ) .filter( v => v.querySelector( 'div[data-props*="media"]' ) ) // Text-only posts fuck us up. .map( v => { let item = new Object; item.page = v.querySelector( 'a[class*="time"][href]' ).href; item.title = item.page.split( '/' ).pop(); // dataset.props example: Object { height: 343, sensitive: true, autoplay: true, media: Array[3] } // .media example: Array [ Object, Object, Object ] // .media[0]: Object { id: "105651575500861937", type: "image", // url: "https://baraag.net/system/media_att…", preview_url: "https://baraag.net/system/media_att…", // remote_url: null, preview_remote_url: null, text_url: "https://baraag.net/media/C62aWhAqWt…", // meta: Object, description: null, blurhash: "UFCGJfX99@RP^%t7OoV@4ms:kpn+x[V@Rja#" } // ... but none of those URLs are the post URL, so keep original item.page code. let data = JSON.parse( v .querySelector( 'div[data-props]' ) .dataset.props ) .media; // Array of objects item.thumb = data.map( v => v.preview_url ); // Arguably should be a single image? One thumb per submission? item.image = data.map( v => v.url ); return item; } ) } break; } // Subdomains always have to cause problems. if( document.domain.split( '.' ).slice( 1, 3 ).join( '.' ) == "booru.org" ) { // *://*.booru.org/* trigger_size = [ 15, 55, 16, 5 ]; // Left, top, font-size, padding. // https://svtfoe.booru.org/index.php?page=post&s=list&tags=crack_ship&pid=20 // https://svtfoe.booru.org/index.php?page=post&s=list&tags=socks&pid=20 page_number = 0; if( args["pid"] ) { page_number = parseInt( args["pid"] ); } next_page = window.location.href + "&pid=" + (page_number + 20); // Flawed, but it works. previous_page = window.location.href + "&pid=" + (page_number - 20); gather_items = function() { return Array.from( document.querySelectorAll( 'a img[src*="thumbs"]' ) ) .map( v => v.closest( 'a' ) ) .filter( v => ! v.style.display ) // Exclude hidden posts. Should be !display=="none", but typeError says fuck you. .map( v => new Object( { // https://svtfoe.booru.org/index.php?page=post&s=view&id=29292 page: v.href, title: v.href.split( '=' ).pop(), // https://thumbs.booru.org/svtfoe/thumbnails//28/thumbnail_187209e3ca22a28fd1ce75a7fd4f54aee3cf1e62.jpg // https://img.booru.org/svtfoe//images/28/187209e3ca22a28fd1ce75a7fd4f54aee3cf1e62.jpg thumb: scrub_extensions( v.querySelector( 'img[src*="thumbs"]' ).src ), image: scrub_extensions( v.querySelector( 'img[src*="thumbs"]' ).src ) .replace( 'thumbs.', 'img.' ) .replace( '/thumbnails', '/images' ) .replace( 'thumbnail_', '' ) } ) ) } } // ----- // Controls to invoke script GM_registerMenuCommand( "Swallow entire gallery", show_images ); // GreaseMonkey dropped this feature years ago, but I am stubborn. // Put button on page, since there's no menu in "modern" Userscript plugins. // Onclick, change class to some spinner, so it reacts instantly and looks like it's loading. Really the interval is waiting a second. var trigger = document.createElement( 'button' ); trigger.innerText = "Swallow gallery"; trigger.className = "unclicked_button"; trigger.onclick = function(){ this.innerText='Swallowing...'; this.className = 'clicked_button'; } // Immediate visible change, idempotent trigger.style = "position: absolute; left: " + trigger_size[0] + "px; top: " + trigger_size[1] + "px; background-color:#dbd7d8; border-radius: 20px; text-align: center; display: inline-block; border:1px solid #19ab19; cursor:pointer; line-height: 20px; color:#194d19; font-family:Arial; font-size:" + trigger_size[2] + "px; padding: " + trigger_size[3] + "px " + trigger_size[3] + "px; text-decoration:none;" // Pixiv doesn't work properly until after thumbnails load, so don't show the button until then. if( typeof( button_delay_function ) == "function" ) { // Note: checking for presence of function. add_button = setInterval( function() { if( button_delay_function() ) { // Note: function called, checking results. clearInterval( add_button ); document.body.appendChild( trigger ); } }, 1000 ); // Passive - no interaction concerns. } else { document.body.appendChild( trigger ); // Most sites are ready-to-go. } // Injecting code into the page is nontrivial - ironically because function.toString is fragile - so just look for a change in the page. var button_check = document.getElementsByClassName( 'clicked_button' ); var fake_event = setInterval( function() { if( button_check.length > 0 ) { clearInterval( fake_event ); show_images(); } }, 400 ); // Doherty threshold for frustration is 400ms. // function fetch_pages( page_links ) { } // Still expecting to use this approach on some site, but so far, nnnope. // End of main execution. // ------------------------------------ Gallery Swallower ------------------------------------ // function show_images() { // ----- // Replace page, set up furniture // Grab links and/or thumbnails using per-site code: items = gather_items(); // Array of objects, listing page link, thumbnail, presumed fullsize image, etc. // console.log( items ); // Debug. Be honest, this is staying here. if( top_to_bottom == false ) { items.reverse(); } // Erase existing page, use ours instead // document.head.innerHTML = ''; // Looks bad, accomplishes nothing. document.body.innerHTML = html; // Navigation links, at the bottom let links_element = document.getElementById( 'links' ); // Has to go after document = html, duh. // "Previous" or "Previous - Next" or "Next" let link_html = ""; if( page_number <= 1 ) { previous_page = ''; } // No previous link on first pages. (Page 0 is valid.) if( previous_page ) { link_html += "<a href='" + encodeURI( previous_page ) + "'>Previous page</a>"; } if( previous_page && next_page ) { link_html += " - "; } if( next_page ) { link_html += "<a href='" + encodeURI( next_page ) + "'>Next page</a>"; } links_element.innerHTML = link_html; let centered = document.getElementById( 'centered' ); // ----- // Per-submission links and controls // Give each item its own set of spans, with basic onClick controls to remove images or reload a submission. for( let item_key = 0; item_key < items.length; item_key++ ) { item = items[ item_key ]; let container = document.createElement( 'span' ); container.id = item_key + 'container'; let basket = document.createElement( 'span' ); // Two hard problems. basket.id = item_key; let reloader = document.createElement( 'button' ); reloader.innerText = '⟳'; reloader.onclick = function() { let e=document.getElementById( item_key ); e.innerHTML = ""; e.dataset.page_number=0; e.className="ready"; } reloader.className = 'reloader'; let link = document.createElement( 'a' ); link.href = item.page + '#dnr#&dnr'; link.innerText = ' ' + item.title + ' '; link.setAttribute( "target", "_blank" ); link.style = 'font-size:30px'; let submission_remover = document.createElement( 'button' ); // Erase whole submission. submission_remover.innerText = '❌'; submission_remover.onclick = function(){ document.getElementById( item_key ).innerHTML = ""; } submission_remover.className = 'remover'; // Add some CSS to float left or whatever. // Maybe don't do bottom remover on single-image sites. It's clutter. // Would arguably still be useful for very tall comic strips, except we don't have image-size controls yet. let bottom_submission_remover = submission_remover.cloneNode(); // Why doesn't this include innerText? bottom_submission_remover.innerText = '❌'; // Scroll back up on removal, since a bunch of vertical content disappeared. // The presumed use of this button is when you've scrolled past a long-ass manga and gone "meh," // so you don't want to hunt for the root reload / remove buttons. That crap killed me in Tumblr Scrape. bottom_submission_remover.onclick = function() { document.getElementById( item_key ).innerHTML = ""; document.getElementById( item_key + 'container' ).scrollIntoView(); } container.appendChild( reloader ); container.appendChild( link ); container.appendChild( submission_remover ); container.appendChild( document.createElement( 'br' ) ); container.appendChild( basket ); container.appendChild( bottom_submission_remover ); container.appendChild( document.createElement( 'br' ) ); centered.appendChild( container ); if( force_style ) { // Apply CSS rules to every element individually. for( selector in style_rules ) { Array.from( document.querySelectorAll( selector ) ) .forEach( element => { // Finding .cssText was so hard, you'd think it's a secret. element.style = element.style.cssText + style_rules[ selector ]; } ) } } basket.dataset.page_number = 0; basket.classList.add( "ready" ); // Signals interval function to load an image here. } // ----- // Ongoing interaction and loading // Load more images in "ready" submissions, when available. // Sloppy speedup: "while ready_element = [0]." Service all ready spaces, every time. // Cons: might 503, might lock up. // Tried while( ready_element = ready_elements[0] ). Unscientific results: same time either way. Eh. // Better kludge: forEach, setTimeout. It'll complete the for-each, then schedule each single-service action, in order. // Cons: minor race condition where one "ready" state could get scheduled repeatedly. (If another interval occurs before that timeout.) // Cons: still might 503. var ready_elements = document.getElementsByClassName( 'ready' ); var interval_object = setInterval( function() { // Add an image to the ready basket, increment page number, conditionally ready-up for another image. // Image list: just grab list[n]. Automatic pagination: probe for matching numbered thumbnail. if( ready_element = ready_elements[0] ) { // "If ready_elements.length > 0," but with race condition paranoia. ready_element.classList.remove( 'ready' ); // We fancy. let item_id = parseInt( ready_element.id ); let manga_page = parseInt( ready_element.dataset.page_number ); // Not web-page... comic-page. ready_element.dataset.page_number = 1 + manga_page; // Prepare filename, once: let image_url = items[ item_id ].image; if( typeof( items[ item_id ].image ) == "object" ) // typeof, not typeOf? "object", not "Array"? Fuck you, Javascript. { image_url = items[ item_id ].image[ manga_page ]; } if( automatic_pagination ) // Basically just Pixiv. { image_url += manga_page; } image_url += image_followup; // Usually nothing. // Try all plausible file extensions for an inline image. let forms = new Array; for( format of formats ) { let png = document.createElement( 'img' ); let apng = document.createElement( 'a' ); apng.appendChild( png ); png.className = "short loading"; // Debug-ish. Also, doesn't work for some goddamn reason. png.src = image_url + format; // console.log( image_url ); png.onerror = function() { this.parentElement.remove(); } // Remove parent link (apng / ajpg / agif) as well. png.onload = function() { this.classList.remove( "loading" ); } apng.href = png.src; apng.setAttribute( "target", "_fuckgelbooru" ); // WHY IS THERE NO OPPOSITE TO DISPLAY:NONE?! forms.push( apng ); } let remover = document.createElement( 'button' ); // Big red X, to remove this image specifically. remover.innerText = '❌'; remover.onclick = function() { this.parentElement.remove(); } remover.className = 'remover floating'; // New span per-image, so whole-submission X prevents new images from loading: let outer_span = ready_element.querySelector( 'span' ); if( outer_span == null ) { outer_span = document.createElement( 'span' ); ready_element.appendChild( outer_span ); } let inner_span = document.createElement( 'span' ); inner_span.style = "position: relative;"; // This fixes the vertically-aligned Xs. Do NOT ask me how. outer_span.appendChild( inner_span ); for( form of forms ) { inner_span.appendChild( form ); } // Insert all image tries. inner_span.appendChild( remover ); // Also insert delete-this button... afterward? Yeah, on the right-hand side. inner_span.appendChild( document.createElement( 'br' ) ); inner_span.appendChild( document.createElement( 'br' ) ); // Thumb-try stuff: if( automatic_pagination ) { forms = new Array; for( format of formats ) { png = document.createElement( 'img' ); // Reuse png.src = items[ item_id ].thumb + (manga_page+1) + thumb_followup + format; png.className = 'test'; png.onerror = function() { this.remove(); } // Any truly hideous behavior is often attributable to the wrong number of .parentElements below: png.onload = function() { this.parentElement.parentElement.parentElement.classList.add( "ready" ); this.remove(); } forms.push( png ) } for( form of forms ) { inner_span.appendChild( form ); } } // Array-of-images stuff: if( typeof( items[ item_id ].image ) == "object" ) { // Not a global boolean, because direct testing beats keeping track. if( manga_page + 1 < items[ item_id ].image.length ) { ready_element.classList.add( "ready" ); } } // Force style, but only for children of this element. // Should maybe only be for children of inner_span or outer_span? No; also applies to Pixiv thumbnail probes. // ... though "+=" does mean 30-image submissions will have one element with style=size;size;size;size;, etc. // Nevermind, text -> CSS2Properties eats all duplicates. if( force_style ) { for( selector in style_rules ) { Array.from( outer_span.querySelectorAll( selector ) ) .forEach( element => { element.style = element.style.cssText + style_rules[ selector ]; } ) } } } }, new_image_rate ); // ----- // 🍭 Spinners 🍭 // Garish spinners that indicate "finding new images" and "files still loading." // Easy answer for responsive loading / lazy spinners: use two intervals. Duh. // I could simplify this to e.g. images_spinner.className = images_loading.length and do CSS bullshit like #id.class=spinning #id.0=not-spinning. // I don't think there's any truly automatic way to spin based on the properties of children... but I wouldn't be surprised. // Like I'm pretty sure per-element style properties can't go 'animate if( self.querySelector )'. But again: not ruling it out. // It's only desirable for forced_style sites like Baraag, since lazy spinners are better, so pfffft. var submissions_loading, submissions_spinner; // These are technically global? var images_loading, images_spinner; submissions_loading = document.getElementsByClassName( 'test' ); submissions_spinner = document.getElementById( 'submissions_spinner' ); images_loading = document.getElementsByClassName( 'loading' ); images_spinner = document.getElementById( 'images_spinner' ); image_count = document.getElementsByClassName( 'short' ); // No longer updates as images are removed. Just add another class. // Tracking "short" instead counts the test images, leading to brief overcounting. But not every site uses thumbnails. image_counter = document.getElementById( 'image_counter' ); // Two hard problems. // Flickering on/off multiple times per second: bad. // Waiting an entire second between image loads: also bad. // So... add another low-impact interval. Efficient? Not really. But eeeasy. var spinner_interval = setInterval( function() { if( submissions_loading.length == 0 ) { /* If we're done testing for new images in submissions */ submissions_spinner.className = 'spacer'; } else { /* If e.g. we reload a submission and it takes a while */ submissions_spinner.className = 'submissions_loader'; } if( images_loading.length == 0 ) { /* If we're done testing for new images in submissions */ images_spinner.className = 'other_spacer'; } else { /* If e.g. we reload a submission and it takes a while */ images_spinner.className = 'images_loader'; } image_counter.innerText = '' + image_count.length + ' images'; /* Should arguably exclude loading images. */ /* Could be made accurate by counting calls to next_image. Right? Or incrementing some var when that succeeds. */ /* Just count className="thumb". Right? Haha, I'm already double-counting because loaded thumbnails don't really go away. Yeah, count "thumb." */ }, 1000 ); }