Topic: Userscripts (Index to gallery and post tweaks)

Posted under e621 Tools and Applications

I like to browse one-handed.

Tested on latest Firefox with Greasemonkey.

Index Gallery:

  • Next page: [Space] at bottom
  • Previous page: [Shift-Space] at top
  • Convert to gallery: [Ctrl-Space] or [View all] button
  • (Oldest first: [Ctrl-shift-space])
  • Next in gallery: [Space]
  • Previous in gallery: [Shift-space]
  • Play gif or webm: [W]
  • Seek: [A]/[D]
  • Volume: [Shift-W]/[Shift-S]

Post Tweaks:

  • Play webm: [W] (Also focuses image)
  • Seek: [A]/[D]
  • Volume: [Shift-W]/[Shift-S]

I can make some improvements if there's interest, but I think I saw dedicated extensions for this sort of thing. Figured I'd post what I have anyway.

Index Gallery

Reload gallery page to get out of it, it just replaces the current document
Lazily loads items in sequence, adjust timerInterval if your internet is awesome.
Does not handle flash submissions
Firefox doesn't like pages of videos (Video thumbnails, please)
No blacklist support (I don't know where it is in the document scope)
Rudimentary include/exclude filter commented out/disabled inside of extractItem around line 130
Unviewed/Unloaded shown in tab title

// ==UserScript==
// @name        E621_IndexGallery
// @namespace   eig
// @include     https://e621.net/post/index/*
// @version     1
// @grant       none
// ==/UserScript==

let timerInterval = 1000

let baseTitle = document.title

let mainLoop = null
let paused = false

let loading = 0

let onItemLoad = () => {
	loading--
}

let onItemError = () => {
	loading--
}

let imgOnLoad = function() {
	if (this.width + this.height == 0) {
		this.onerror()
	}
	else if (this.complete) {
		// console.log('Active:', loading, 'Finished:', this.src)
		onItemLoad()
	}
}

let imgOnLoadComplete = function() {
	if (this.width + this.height == 0) {
		this.onerror()
	}
	else {
		// console.log('Active:', loading, 'Finished:', this.src)
		onItemLoad()
	}
}

let extensions = [
	'jpg',
	'png',
	'jpeg',
	'gif'
]

let clickGif = function() {
	if (this.hasClassName('gif-loader')) {
		this.playing = true
		this.src = this.item.base+'.gif'
		this.removeClassName('gif-loader')
	}
	// else {
	// 	this.playing = false
	// 	this.src = this.item.thumb
	// 	this.addClassName('gif-loader')
	// }
}

let imgOnError = function() {
	this._ext++
	let nextExt = extensions[this._ext]
	if (nextExt) {
		if (nextExt == 'gif') {
			this.src = this.item.thumb
			this.addClassName('gif-loader')
			this.addEventListener('click', clickGif)
			// console.log('Loading gif placeholder: '+this.item.base)
		}
		else {
			this.src = this._src+'.'+nextExt
			// console.log('Falling back to: '+this.src)
		}
	}
	else {
		console.log('Failed to load: '+this.item.page)
		onItemError()
	}
}

let urls = []

/*
We have to load each video page to discover the url for the video itself, because there's no direct connection to the video
*/
let videoPageLoad = function() {
	if (this.status != 200) {
		console.log(this.statusText)
		// TODO: Error
		return
	}
	else {
		this.item.src = this.responseXML.querySelector('#webm-container > source').src
		this.item.node.src = this.item.src
	}
	// console.dir(this.responseXML)
	// console.log('Video loaded : '+this.responseURL+', '+this.item.src)
	// console.log('videoPageLoad', this)
}

let videoPageError = function() {
	// console.log('videoPageError', this)
	onItemError()
	loading--
}

let videoOnCanPlay = function() {
	// console.log('videoOnCanPlay', this)
	onItemLoad()
	loading--
}

let videoOnError = function() {
	// console.log('videoOnError', this)
	onItemLoad()
	loading--
}

let re_src = /(.+)preview\/(.+)\.jpg$/
let extractItem = e => {
	if (e.src.includes('download-preview')) {
		return null
	}
	// if (!(e.alt.includes('female') || e.alt.includes('intersex')) || e.alt.includes('my_little_pony') || e.alt.includes('sonic_(series)')) {
	// 	return null
	// }
	let item = {
		page: e.parentElement.href,
		thumb: e.src
	}
	if (e.src.includes('webm-preview')) {
		item.video = true
	}
	else if (e.src.includes('preview/')) {
		let match = re_src.exec(e.src)
		if (match) {
			item.base = match[1]+match[2]
		}
	}
	return item
}

let extractPreviews = doc => {
	return Array.from(doc.querySelectorAll('img.preview'), extractItem)
		.filter(x => x)
}

let widgets = {}
let timeoutID
let currentImg

let unviewed = -1

let mainItem = (item) => {
	// Wrapper
	let div = document.createElement('div')

	// Video
	if (item.video) {
		// console.log('Adding video: '+item.page)
		let video = document.createElement('video')
		item.node = video
		video.loop = true
		video.autoplay = false
		video.muted = true
		video.controls = true
		video.preload = 'metadata'
		video.type = 'video/webm'
		video.addEventListener('canplay', videoOnCanPlay)
		video.addEventListener('error', videoOnError)
		div.appendChild(video)
		// Weight videos heigher
		loading++

		let xhr = new XMLHttpRequest()
		xhr.item = item
		xhr.addEventListener('load', videoPageLoad)
		xhr.addEventListener('error', videoPageError)
		xhr.open('GET', item.page)
		xhr.responseType = 'document';
		xhr.send()
	}
	// Image
	else {
		// console.log('Adding image: '+item.base)
		let img = document.createElement('img')
		img.onerror = imgOnError
		img.onload = imgOnLoad
		img.onloadcomplete = imgOnLoadComplete
		img.item = item
		img.src = item.base+'.jpg'
		img._src = item.base
		img._ext = 0
		div.appendChild(img)
	}

	// Link
	let a = document.createElement('a')
	a.href = item.page
	a.innerHTML = item.page
	div.appendChild(a)

	widgets.output.appendChild(div)

	loading++

	if (!currentImg) {
		currentImg = div
		currentImg.scrollIntoView({behavior: 'smooth'})
	}
	else {
		div.viewed = false
		unviewed++
	}
}

let updateUI = () => {
	if (urls.length === 0) {
		document.title = baseTitle
		widgets.status.innerHTML = 'Done'
	}
	else {
		if (unviewed < 10) {
			widgets.status.innerHTML = urls.length+' Remaining'
		}
		else {
			widgets.status.innerHTML = urls.length+' (Waiting)'
		}
		document.title = `(${unviewed}/${urls.length}) ${baseTitle}`
	}
}

mainLoop = function() {
	if (urls.length > 0 && !paused && loading < 2) {
		if (unviewed < 5) {
			mainItem(urls.shift())
		}
	}
	updateUI()
}

let onPauseClick = function() {
	paused = !paused
	if (paused) {
		widgets.pause.innerHTML = 'Resume'
	}
	else {
		widgets.pause.innerHTML = 'Pause'
	}
}

let onDownloadClick = function(alt) {
	// Begin
	urls = extractPreviews(document)
	if (alt) {
		urls.reverse()
	}

	// $(window).off()
	document.querySelector('body').innerHTML =
`<style id='__style'>
	body {
		text-align: center;
	}
	#__status {
		font-size: 1.4em;
		margin: 10px;
	}
	#__control {
		position: fixed;
		background: grey;
		padding-right: 5px;
		border-radius: 0px 0px 10px 0px;
		z-index: 100;
	}
	#__control > * {
		display: inline-block
	}
	#__output img, #__output video {
		display: block;
		position: relative;
		height: calc(100vh - 25px);
		width: 98vw;
		object-fit: contain;
	}
	img.gif-loader::after {
		position: absolute;
		top: 50%;
		left: 50%;
		content: 'Click to play GIF';
	}
	#__output a {
		margin-bottom: 5vh;
	}
	body * {
		margin-left: auto;
		margin-right: auto;
	}
</style>
<div id='__container'>
	<div id='__control'>
		<button id='__pause'>Pause</button>
		<div id='__status'></div>
	</div>
	<div id='__output'><div></div></div>
</div>`

	widgets.control = document.getElementById('__control')
	widgets.status = document.getElementById('__status')
	widgets.pause = document.getElementById('__pause')
	widgets.output = document.getElementById('__output')

	widgets.pause.addEventListener('click', onPauseClick)

	let videoFunction = (func) => {
		return (...args) => {
			if (currentImg && currentImg.firstChild && currentImg.firstChild.nodeName == 'VIDEO') {
				func(currentImg.firstChild, ...args)
			}
		}
	}

	// Video helpers
	let seek = videoFunction((video, mod) => {
		video.currentTime = Math.min(Math.max(video.currentTime + mod, 0), video.duration)
	})
	let attenuate = videoFunction((video, mod) => {
		video.volume = Math.min(Math.max(video.volume + mod, 0), 1)
		if (video.volume > 0 && video.muted) {
			video.muted = false
		}
	})

	let playOrPause = () => {
		if (!currentImg) {
			return null
		}
		let node = currentImg.firstChild
		if (node.nodeName == 'VIDEO') {
			if (!node.playing) {
				node.play()
				node.playing = true
			}
			else {
				node.pause()
				node.playing = false
			}
		}
		else {
			node.click()
		}
	}
	document.onkeydown = function(event) {
		if (event.key == ' ') {
			if (currentImg) {
				// Pause focus
				if (currentImg.firstChild && currentImg.firstChild.playing) {
					playOrPause()
					currentImg.playOnFocus = true
				}
				// Get next focus
				let sibling = event.shiftKey ? currentImg.previousSibling : currentImg.nextSibling
				if (sibling) {
					sibling.scrollIntoView()
					currentImg = sibling
					// Resume previously paused focus
					if (currentImg.playOnFocus == true) {
						playOrPause()
						playOnFocus = false
					}
					// Set as viewed for buffered loading
					if (!sibling.viewed) {
						sibling.viewed = true
						unviewed--
					}
				}
			}
			event.preventDefault()
		}

		// Volume up
		else if (event.key == 'W') {
			attenuate(0.2)
		}

		// Volume down
		else if (event.key == 'S') {
			attenuate(-0.2)
		}

		// Play or pause
		else if (event.key == 'w') {
			playOrPause()
		}

		// Seek forward
		else if (event.key == 'd') {
			seek(5)
		}

		// Seek backward
		else if (event.key == 'a') {
			seek(-5)
		}
	}
	setInterval(mainLoop, timerInterval)
}

let divHeader = document.querySelector('.sidebar')
widgets.download = document.createElement('div')
widgets.download.innerHTML = '<button>Load all</button>'
widgets.download.lastChild.addEventListener('click', onDownloadClick, false)
divHeader.parentNode.insertBefore(widgets.download, divHeader.nextSibling)

document.onkeydown = function(event) {
	if (event.key == ' ' && event.ctrlKey) {
		onDownloadClick(event.altKey)
	}
	else if (event.key == ' ') {
		let link = null
		// Previous page at top
		if (event.shiftKey && window.scrollY == 0) {
			link = document.querySelector('a.prev_page')
		}
		// Next page at bottom
		else if (!event.shiftKey && (window.innerHeight + window.scrollY) >= document.body.offsetHeight-100) {
			link = document.querySelector('a.next_page')
		}
		if (link) {
			link.click()
		}
	}
}
PostTweaks

Also works on r34

// ==UserScript==
// @name           e6+ tweaks
// @namespace      e6pt
// @include		   https://e621.net/post*
// @include		   https://rule34.xxx/index.php?page=post*
// @grant       none
// ==/UserScript==

let img = document.getElementById('image')
let webm = document.getElementById('webm-container') // e621
	|| document.getElementById('gelcomVideoPlayer') // rule34
let e = webm || img

// Video helpers
let seek = (video, mod) => {
	video.currentTime = Math.min(Math.max(video.currentTime + mod, 0), video.duration)
}
let attenuate = (video, mod) => {
	video.volume = Math.min(Math.max(video.volume + mod, 0), 1)
}

// Pause/play/seek keyboard controls
let playing = false
let onKeyDown = event => {
	if (event.key == 'w') {
		e.scrollIntoView()
	}
	if (webm) {
		webm = document.getElementById('webm-container') // e621
			|| document.getElementById('gelcomVideoPlayer') // rule34 reloads theirs?

		// Volume up
		if (event.key == 'W') {
			if (webm.muted) {
				webm.muted = false
				webm.volume = 0
			}
			attenuate(webm, 0.2)
		}

		// Volume down
		else if (event.key == 'S') {
			attenuate(webm, -0.2)
		}

		else if (event.key == 'w') {
			if (event.shiftKey) {
			}
			// Play or pause
			else {
				if (!playing) {
					webm.play()
					playing = true
				}
				else {
					webm.pause()
					playing = false
				}
			}
		}

		// Seek forward
		else if (event.key == 'd') {
			seek(webm, 5)
		}

		// Seek backwards
		else if (event.key == 'a') {
			seek(webm, -5)
		}

		// Mute toggle
		else if (event.key == 'e') {
			webm.muted = !webm.muted
		}
	}
}
document.onkeydown = onKeyDown

// Focus content
if (e) {
	e.scrollIntoView()
	setTimeout(() => {
		e.scrollIntoView()
		document.onkeydown = onKeyDown
	}, 3000)
}

// Mute video
if (webm) {
	webm.muted = true
}
  • 1