Skip to content

Instantly share code, notes, and snippets.

@koteq
Last active October 6, 2016 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save koteq/0757210389c6441cb738e88d4ffe5043 to your computer and use it in GitHub Desktop.
Save koteq/0757210389c6441cb738e88d4ffe5043 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @version 1.0
// @name Pixiv Top
// @description Load 7 pages and order it by bookmarks count
// @match *://www.pixiv.net/search.php*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.15.0/lodash.min.js
// ==/UserScript==
/* jshint esnext: true */
(function(_, $, moment, document) {
'use strict';
const GLOBAL_PAGE_LOAD_LIMIT = 50;
/**
* While loop for Promises.
* See http://stackoverflow.com/a/17238793/6050634
*
* @param {function()} condition - is a function that returns a boolean.
* @param {function()} action - is a function that returns a promise.
* @return {Promise} for the completion of the loop.
*/
function promiseWhile(condition, action) {
return new Promise((resolve, reject) => {
function loop() {
if (!condition()) {
return resolve();
}
action().then(loop).catch(reject);
}
setTimeout(loop, 0);
});
}
class ImageItem {
constructor(node) {
this.node = node;
this.$node = $(node);
}
/**
* @return float - score based on formula used on Hacker News.
*/
getScore() {
const gravity = 1.8;
const points = this.getBookmarksCount();
const ageInHours = moment().diff(this.getCreationMoment(), 'hours');
return (points - 1) / Math.pow((ageInHours + 2), gravity);
}
getBookmarksCount() {
return parseInt(this.$node.find('.bookmark-count:first').text()) || 0;
}
getCreationMoment() {
const dateStr = this.$node.find('._thumbnail:first').attr('src').match(/\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2}/);
if (dateStr !== null) {
return moment(`${dateStr} +0900`, 'YYYY/MM/DD/HH/mm Z'); // +0900 is the Japan Standard Time offset
}
else {
// It's much safer for our logic to return valid moment even if no date could really be parsed.
return moment();
}
}
}
class ImageCrawler {
constructor() {
this.currentPage = 0;
this.pageUrlTpl = $('ul.page-list:first a:first').attr('href').replace(/\bp=\d+/, 'p=%page%');
}
/**
* @return {Promise<ImageItem[]>}
*/
getNextPageImages() {
this.currentPage += 1;
return this._getPageContent(this.currentPage)
.then(page => _(this._getImageNodes(page))
.map(imageNode => new ImageItem(imageNode))
.value());
}
/**
* @return {Promise<string|Document>}
*/
_getPageContent(pageNo) {
return new Promise((resolve, reject) => {
if (pageNo === 1) {
return resolve(document);
}
else {
$.ajax({
url: this._getPageUrl(pageNo),
success: page => { resolve(page); },
error: (xhr, status, throwable) => { reject(status); },
});
}
});
}
_getPageUrl(pageNum) {
return this.pageUrlTpl.replace(/%page%/, pageNum);
}
_getImageNodes(page) {
return Array.from($(page).find('ul._image-items:first > li.image-item'));
}
}
class MainController {
constructor({daysToLoad}) {
this.daysToLoad = daysToLoad;
this.crawler = new ImageCrawler();
this.$container = $('ul._image-items:first');
this.$nav =
$('<div>').css({
'position': 'fixed',
'top': '250px',
'left': '100px',
}).appendTo(document.body);
}
run() {
this.$container.css({'-webkit-filter': 'contrast(0)'});
this._loadImages(this.daysToLoad)
.then(images =>
_(images).groupBy(image => moment().diff(image.getCreationMoment(), 'days'))
.forEach((dayImages, createdDaysAgo) => {
this._addHeaderAndNavLink(createdDaysAgo);
// Sort and display images.
_(dayImages)
.sortBy(image => image.getScore())
.reverse()
.forEach(image => image.$node.detach().appendTo(this.$container));
}))
.then(() => this.$container.css({'-webkit-filter': 'none'}));
}
_addHeaderAndNavLink(daysAgo) {
const anchor_id = '_pixiv_top_anchor_' + daysAgo;
const daysAgoMoment = moment().subtract(daysAgo, 'days');
const daysAgoStr = daysAgoMoment.calendar(null, {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
lastDay: '[Yesterday]',
nextWeek: () => '[' + daysAgoMoment.fromNow() + ']',
lastWeek: () => '[' + daysAgoMoment.fromNow() + ']',
sameElse: () => '[' + daysAgoMoment.fromNow() + ']',
});
$('<li>')
.text(daysAgoStr)
.css({
'border-bottom': '1px solid black',
'font': '18px/2 sans-serif',
})
.attr('id', anchor_id)
.appendTo(this.$container);
// Populate nav with fully loaded days only
if (daysAgo < this.daysToLoad) {
$('<a>')
.text(daysAgoStr)
.attr('href', '#' + anchor_id)
.appendTo(this.$nav);
$('<br>').appendTo(this.$nav);
}
}
/**
* Crawls images page by page until needed days loaded.
*
* @return {Promise<ImageItem[]>}
*/
_loadImages(daysToLoad) {
let images = [];
return promiseWhile(() => {
// Condition
let daysCondition = true;
if (images.length) {
const lastImage = images[images.length - 1];
const createdDaysAgo = moment().diff(lastImage.getCreationMoment(), 'days');
daysCondition = createdDaysAgo < daysToLoad;
console.log(`Loading images. Page ${this.crawler.currentPage}. Last image created ${createdDaysAgo} days ago.`);
}
return daysCondition && this.crawler.currentPage < GLOBAL_PAGE_LOAD_LIMIT;
}, () => {
// Action
return this.crawler.getNextPageImages()
.then(pageImages => images = images.concat(pageImages))
.catch(reason => console.error(`Images loading failed on page ${this.crawler.currentPage} due to ${reason}`));
}).then(() => console.log(`Done loading images. Crawled ${this.crawler.currentPage} pages. ${images.length} images found.`))
.then(() => images);
}
}
[7, 5, 3, 2, 1].forEach(cnt => {
$('<button>')
.html(`&nbsp;d${cnt}&nbsp;`)
.insertAfter('span.next:first')
.click(() => {
(new MainController({daysToLoad: cnt})).run();
});
});
})(_.noConflict(), jQuery.noConflict(true), moment, document);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment