Last active
October 6, 2016 17:29
-
-
Save koteq/0757210389c6441cb738e88d4ffe5043 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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(` d${cnt} `) | |
.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