// ==UserScript==
// @name 聚合网页(美女图聚合展示演化而来)by SeLang
// @namespace http://cmsv1.findmd5.com/
// @version 0.09
// @description 目标是聚合网页,省去翻页烦恼。有需要聚合的网址请反馈。 QQ群号:455809302,点击链接加入群【油猴脚本私人定制】:https://jq.qq.com/?_wv=1027&k=45p9bea
// @author selang
// @include /https?\:\/\/*/
// @require https://cdn.staticfile.org/jquery/1.12.4/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/ipfs/0.50.2/index.min.js
// @connect *
// @grant GM_download
// @grant GM_openInTab
// @grant GM_getTab
// @grant GM_getTabs
// @grant GM_saveTab
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// ==/UserScript==
(async function () {
if (window.top === window.self) {
const NODE = await Ipfs.create(({repo: 'ipfs-monkey-aggregation'}));
const RULE_CID_KEY = '聚合网页RULE_CID_KEY';
const RULE_OFFICIAL_CID_KEY = '聚合网页OFFICIAL_RULE_CID_KEY';
const AGGREGATION_TEMPLATE_HTML_KEY = "聚合网页OFFICIAL_AGGREGATION_TEMPLATE_HTML_KEY";
const AGGREGATION_TEMPLATE_JS_KEY = "聚合网页OFFICIAL_AGGREGATION_TEMPLATE_JS_KEY";
const AGGREGATION_TEMPLATE_CSS_KEY = "聚合网页OFFICIAL_AGGREGATION_TEMPLATE_CSS_KEY";
const ENV_DEV_STATUS = 'DEV';
const ENV_PRODUCT_STATUS = 'PRODUCT';
const ENV_STATUS = ENV_PRODUCT_STATUS;
//日志
function log() {
if (true) {
console.log.apply(this, arguments);
}
};
function err() {
if (true) {
console.error.apply(this, arguments);
}
}
function priorityLog() {
console.log.apply(this, arguments);
}
function getRuleCids() {
let officialCIDValue = GM_getValue(RULE_OFFICIAL_CID_KEY);
let value = GM_getValue(RULE_CID_KEY);
if (value) {
let parse = JSON.parse(value);
if (officialCIDValue) {
return [officialCIDValue, ...parse];
} else {
return parse;
}
}
if (officialCIDValue) {
return [officialCIDValue];
} else {
return [];
}
}
function setRuleCids(key, value) {
return GM_setValue(key, value);
}
function putCache(key, value) {
return GM_setValue(key, value);
}
function getCache(key) {
return GM_getValue(key);
}
const AsyncFunction = Object.getPrototypeOf(async function () {
}).constructor;
Array.prototype.distinct = function () {
let arr = this;
let result = [];
let obj = {};
for (let i of arr) {
if (!obj[i]) {
result.push(i);
obj[i] = 1;
}
}
return result;
}
const Alpha_Script = {
sleep: function (time = 100) {
return new Promise(resolve => {
setTimeout(function () {
resolve();
}, time);
})
},
obtainHtmlAsync: function (options) {
options = options || {};
if (!options.url) {
throw new Error("参数不合法");
}
return new Promise(resolve => {
options.headers = {
...Alpha_Script.parseHeaders("Accept:image/webp,image/*,*/*;q=0.8\n" +
"Accept-Encoding:gzip, deflate, sdch\n" +
"Accept-Language:zh-CN,zh;q=0.8\n" +
"Referer:" + window.location.href + "\n" +
`User-Agent:${window.navigator.userAgent}`
), ...options.headers
};
options.method = options.method || 'GET';
let responseType = options.responseType;
switch (responseType) {
case "blob":
options.onload = options.onload || function (response) {
if (response && typeof response.status != "undefined") {
let status = response.status;
if (status >= 200 && status < 300) {
resolve({status, response});
} else if (status == 304) {
resolve({status, response});
}
}
}
break;
default:
options.onload = options.onload || function (response) {
if (response && typeof response.status != "undefined") {
let status = response.status;
if (status >= 200 && status < 300) {
let html = response.responseText;
resolve({status, response, html});
} else if (status == 304) {
resolve({status, response});
}
}
}
}
GM_xmlhttpRequest(options);
});
},
asyncPool: function (poolLimit, array, iteratorFn) {
let i = 0;
const ret = [];
const executing = [];
const enqueue = function () {
//Boundary processing, array is an empty array
if (i === array.length) {
return Promise.resolve();
}
//Initialize a promise every enqueue
const item = array[i];
const p = Promise.resolve(i).then((i) => iteratorFn(item, i, array));
i++;
//Put into promises array
ret.push(p);
//After the promise is executed, remove it from the executing array
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
//Insert the executing number to indicate the executing promise
executing.push(e);
//Using promise.rece, whenever the number of promises in the executing array is less than poollimit, the new promise is instantiated and executed
let r = Promise.resolve();
if (executing.length >= poolLimit) {
r = Promise.race(executing);
}
//Recursion until array is traversed
return r.then(() => enqueue());
};
return enqueue().then(() => Promise.allSettled(ret));
},
obtainHtml: function (options) {
options = options || {};
if (!options.url || !options.method) {
throw new Error("参数不合法");
}
GM_xmlhttpRequest(options);
},
parseHeaders: function (headStr) {
let o = {};
let myregexp = /^([^:]+):(.*)$/img;
let match = /^([^:]+):(.*)$/img.exec(headStr);
while (match != null) {
o[match[1].trim()] = match[2].trim();
match = myregexp.exec(headStr);
}
return o;
},
//获取参数
getParam: function (dest, name) {
let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
let r = dest.match(reg);
if (r != null) return decodeURI(r[2]);
return null;
},
isArray: function (value) {
return Object.prototype.toString.apply(value) === '[object Array]';
}
};
/**
* 无变化则取缓存,有变化则缓存
* @param cacheKey
* @param options 数据结构里至少要有{url:'xx'}
* @returns {Promise<*>}
*/
async function obtainDataByUrl(cacheKey, options) {
let ret = '';
let cache = getCache(cacheKey);
if (typeof cache != "undefined") {
let {etag, html} = JSON.parse(cache);
ret = html;
if (typeof etag != 'undefined') {
let {status, html, response} = await Alpha_Script.obtainHtmlAsync({
...options, headers: {
'If-None-Match': etag
}
});
if (status != 304) {
let etag = response.headers['etag'];
putCache(cacheKey, JSON.stringify({
etag, html
}))
ret = html;
}
}
} else {
let {status, html, response} = await Alpha_Script.obtainHtmlAsync(options);
let responseHeaders = Alpha_Script.parseHeaders(response.responseHeaders);
let {etag} = responseHeaders;
putCache(cacheKey, JSON.stringify({
etag, html
}))
ret = html;
}
return ret;
};
(function () {
'use strict';
priorityLog('欢迎进群:455809302交流。一起玩。');
priorityLog('一起玩不论是不是技术人员都欢迎。只要有创意也欢迎加入。点击链接加入群【油猴脚本私人级别定制】:https://jq.qq.com/?_wv=1027&k=460soLy。');
function validateUrl(url) {
let validate = true;
let lowerCaseUrl = url.toLowerCase();
if (url.startsWith('#')) {
validate = false;
} else if (lowerCaseUrl.startsWith('//')) {
url = `${window.location.protocol}${url}`;
} else if (lowerCaseUrl.startsWith("/")) {
url = `${window.location.protocol}//${window.location.hostname}${url}`;
} else if (lowerCaseUrl.startsWith('https://') || lowerCaseUrl.startsWith('http://')) {
} else {
let prefixRegex = /(.*?\/)[^\/]*$/i;
let __matched = prefixRegex.exec(window.location.href);
if (__matched != null) {
url = `${__matched[1]}${url}`;
} else {
url = `${window.location.protocol}//${window.location.hostname}/${url}`;
}
}
return {validate, url};
}
async function parseNextPages(nextSelector = 'a:contains("下一页")') {
let nextPages = [];
let existNextPage = false;
nextPages.push({
url: window.location.href,
html: $('html').prop("outerHTML")
});
let nextEs = $(nextSelector);
log('解析下一页开始...');
while (nextEs.length > 0) {
existNextPage = true;
let nextPageUrl = nextEs.attr('href');
let validateUrlResult = validateUrl(nextPageUrl);
if (!validateUrlResult.validate) {
break;
}
nextPageUrl = validateUrlResult.url;
log(nextPageUrl);
let {html} = await Alpha_Script.obtainHtmlAsync({url: nextPageUrl});
if (typeof html != "undefined") {
// console.log('nextPage Html==>', html);
let nextPageItem = {
url: nextPageUrl,
html
};
let duplicateNextPage = nextPages.filter(item => item.url == nextPageItem.url);
if (duplicateNextPage.length > 0) {
break
} else {
nextPages.push(nextPageItem);
let {$doc} = txt2Document(html);
nextEs = $doc.find(nextSelector);
}
} else {
console.log('nextPage 未获取到内容。nextPageUrl==>', nextPageUrl);
break;
}
// await Alpha_Script.sleep(1000);
}
log('解析下一页结束...');
return {existNextPage, nextPages};
}
/**
* 插件图片聚合例子
* @param parseNextPages 解析下一页的公用方法
* @param $ jquery
* @param log 日志输出
* @param Alpha_Script 公用对象
* @returns {Promise<*[]>} 图片合集
*/
async function example(parseNextPages, $, log, Alpha_Script) {
//下一页css选择器
let nextPageSelector = 'a:contains("下一页")';
//要聚合的图片css选择器
let imgSelector = 'img';
let {existNextPage, nextPages} = await parseNextPages(nextPageSelector);
if (existNextPage) {
nextPages.map(nextPage => {
let images = Array.from($($(nextPage.html)).find(imgSelector)).map(e => {
let src = e.src;
let dataOriginal = $(e).attr('data-original');
if (dataOriginal) {
src = dataOriginal;
}
return src;
});
nextPage.imgs = images;
});
let imgs = nextPages.flatMap(page => page.imgs);
return imgs;
} else {
return [];
}
}
/**
* 校验cid是否符合rule
* @param cid
* @returns {Promise<{validate: boolean}|{date: *, parseRule: *, url: *, validate: boolean, desc: *}>}
*/
async function parseRuleFromIPFS(cid) {
const {date, desc, parseRule, url, pre, excludeWebsites} = (await NODE.dag.get(cid)).value;
if (parseRule && url && desc && date) {
return {validate: true, date, desc, parseRule, url, pre, excludeWebsites};
} else {
return {validate: false};
}
}
/**
* 获取cid下所有的规则
* @param cid
* @returns {Promise<[]>}
*/
async function obtainRulesFromIPFS(cid) {
let rules = [];
let cids = [];
while (true) {
try {
let rule = await parseRuleFromIPFS(cid);
if (rule.validate) {
rules.push(rule);
if (rule.pre) {
cid = rule.pre;
if (cids.includes(cid)) {
err('闭环了');
break;
} else {
cids.push(cid);
}
} else {
break;
}
} else {
break;
}
} catch (e) {
err(e);
break;
}
}
return rules;
}
async function pluginExample() {
//这里是一个插件的例子
let cid = await NODE.dag.put(
{
url: '通用',
desc: '通用聚合',
parseRule: `return await (${example.toString()})(parseNextPages,$,log,Alpha_Script)`,
excludeWebsites: ['https://www.google.com.hk/search', 'https://greasyfork.org/', 'https://xxxxx需要排除的网站'],
date: '2020年9月30日'
}
);
cid = await NODE.dag.put(
{
url: '这里可以写书写规则的网址',
desc: '第二个通用聚合',
parseRule: `return await (${example.toString()})(parseNextPages,$,log,Alpha_Script)`,
excludeWebsites: ['https://www.google.com.hk/search', 'https://greasyfork.org/', 'https://xxxxx需要排除的网站'],
date: '2020年9月30日',
pre: cid.toString()
}
);
let cidStr = cid.toLocaleString();
// cidStr = cid.toLocaleString();
console.log('你的插件分享的地址为:', cidStr);
return cidStr;
}
let isPageResLoadComplete = false;
async function waitPageResLoadComplete() {
for (; ;) {
if (isPageResLoadComplete) {
log('加载成功');
return;
} else {
await Alpha_Script.sleep(500);
}
}
}
//热键
function aggregationDisplaySwitchHotkeys() {
$(document).keydown(function (e) {
if (e.ctrlKey && e.shiftKey) {
if (e.which == 76) {//L
log("触发快捷键");
aggregationDisplaySwitch();
}
}
});
}
aggregationDisplaySwitchHotkeys();
function aggregationDisplaySwitch() {
let aggregationIFRAMEDisplayCss = $('#aggregationIFRAME').css('display');
let aggregationIFRAMEDisplayCssCache = $('#aggregationIFRAME').attr('display-css-cache');
if (aggregationIFRAMEDisplayCss != 'none') {
$('#aggregationIFRAME').css('display', 'none');
$('body').children().each(function (index, element) {
let displayCss = $(element).attr('display-css-cache');
if (element.id != 'aggregationIFRAME') {
$(element).css('display', displayCss);
}
});
} else {
$('#aggregationIFRAME').css('display', aggregationIFRAMEDisplayCssCache);
$('body').children().each(function (index, element) {
if (element.id != 'aggregationIFRAME') {
$(element).css('display', 'none');
}
});
}
}
window.addEventListener("message", async (event) => {
let data = event.data;
if (data.from == 'page') {
if (data.method == 'obtainHtmlAsync') {
let message = {result: '', from: 'monkey'};
try {
let {html} = await Alpha_Script.obtainHtmlAsync({url: data.url});
message.result = html;
} catch (e) {
message.error = e;
} finally {
event.ports[0].postMessage(message);
}
} else if (data.method == 'pageResLoadComplete') {
isPageResLoadComplete = true;
let message = {result: '', from: 'monkey'};
try {
let html = event.data;
log('html', html);
message.result = 'page收到::' + html;
} catch (e) {
message.error = e;
} finally {
event.ports[0].postMessage(message);
}
} else if (data.method == '聚合切换') {
aggregationDisplaySwitch();
let message = {result: '', from: 'monkey'};
try {
let html = event.data;
log('html', html);
message.result = 'page收到::' + html;
} catch (e) {
message.error = e;
} finally {
event.ports[0].postMessage(message);
}
}
}
}, false);
function postMsg(msg) {
return new Promise((res, rej) => {
const {port1, port2} = new MessageChannel();
port1.onmessage = ({data}) => {
if (data.from == 'page') {
port1.close();
if (data.error) {
rej(data);
} else {
res(data);
}
}
};
msg.from = 'monkey';
window.top.document.getElementById("aggregationIFRAME").contentWindow.postMessage(msg, window.location.origin, [port2]);
});
}
function MIMEObjectURL(txt, contentType = "text/html; charset=UTF-8") {
let txtBlob = new Blob([txt], {type: contentType});
let txtBlobUrl = URL.createObjectURL(txtBlob);
return txtBlobUrl;
}
function txt2Document(txt) {
let parser = new DOMParser();
let doc = parser.parseFromString(txt, "text/html");
let $doc = $(doc);
return {doc, $doc}
}
(async () => {
// let cidStr = await pluginExample();
await pluginExample();
// 你自己写的或者他人分享的cidPath;
let cidStr = "bafyreielhccxya5tbcqoqdfxfqcn7qukjed2jejnbeaffikxm7ssdano2i";
setRuleCids(RULE_OFFICIAL_CID_KEY, cidStr);
let ruleCids = getRuleCids();
ruleCids.push(cidStr);
let rules = [];
for (const ruleCid of ruleCids) {
let rule = await obtainRulesFromIPFS(ruleCid);
rules = [...rules, ...rule];
}
log('当前规则总数:', rules.length);
for (let rule of rules) {
log('当前执行规则>> %s 编写规则参考地址:%s\r\n 规则内容:%s', rule.desc, rule.url, rule.parseRule);
let excludeWebsites = rule.excludeWebsites;
if (Alpha_Script.isArray(excludeWebsites)) {
let findAnyOnes = excludeWebsites.filter(excludeWebsite => window.location.href.startsWith(excludeWebsite));
if (findAnyOnes.length > 0) {
log('当前规则被排除');
continue;
}
}
let ruleFunc = rule.parseRule;
let imgs = await (new AsyncFunction('parseNextPages', '$', 'log', 'Alpha_Script', ruleFunc))(parseNextPages, $, log, Alpha_Script);
imgs = imgs.distinct();
if (imgs.length > 0) {
log('规则找到图片');
let tempHost;
if (ENV_STATUS == ENV_DEV_STATUS) {
tempHost = 'http://127.0.0.1:8081';
} else {
tempHost = 'https://cmsv1.findmd5.com';
}
let aggregationTemplateHtml, aggregationTemplateCss, aggregationTemplateJs;
//国内IPFS不稳定,临时处理
if (true || ENV_STATUS == ENV_DEV_STATUS) {
aggregationTemplateHtml = await obtainDataByUrl(AGGREGATION_TEMPLATE_HTML_KEY, {url: `${tempHost}/static/imageAggregation/aggregationTemplate.html`});
aggregationTemplateCss = await obtainDataByUrl(AGGREGATION_TEMPLATE_CSS_KEY, {url: `${tempHost}/static/imageAggregation/css/aggregationTemplate.css`});
aggregationTemplateJs = await obtainDataByUrl(AGGREGATION_TEMPLATE_JS_KEY, {url: `${tempHost}/static/imageAggregation/js/aggregationTemplate.js`});
} else {
let aggregationTemplateHtmlCid, aggregationTemplateJsCid, aggregationTemplateCssCid;
aggregationTemplateHtmlCid = 'bafyreicncdsi25po7rij4oh355v7w7fjfu3da4ncpkxp2gwwoxucb5ulri';
aggregationTemplateJsCid = 'bafyreiblvxkpjinurhnxgwznsgmtap37vmd7l3cvbcqf3wul4ll2bb7fmy';
aggregationTemplateCssCid = 'bafyreihws6twoxkhqoc3klnm3lucsamss3bki4akrpnwexp6u5m5thzpva';
aggregationTemplateHtml = (await NODE.dag.get(aggregationTemplateHtmlCid)).value.data;
aggregationTemplateCss = (await NODE.dag.get(aggregationTemplateCssCid)).value.data;
aggregationTemplateJs = (await NODE.dag.get(aggregationTemplateJsCid)).value.data;
}
{
let cid = await NODE.dag.put(
{
data: aggregationTemplateHtml,
desc: 'aggregationTemplate.html',
}
);
log('aggregationTemplate.html cid: ', cid.toString());
cid = await NODE.dag.put(
{
data: aggregationTemplateJs,
desc: 'aggregationTemplate.js',
}
);
log('aggregationTemplate.js cid: ', cid.toString());
cid = await NODE.dag.put(
{
data: aggregationTemplateCss,
desc: 'aggregationTemplate.css',
}
);
log('aggregationTemplate.css cid: ', cid.toString());
}
if (ENV_STATUS == ENV_DEV_STATUS) {
console.log('aggregationTemplateHtml', aggregationTemplateHtml);
console.log('aggregationTemplateCss', aggregationTemplateCss);
console.log('aggregationTemplateJs', aggregationTemplateJs);
}
let aggregationTemplateCssBlobUrl = MIMEObjectURL(aggregationTemplateCss, 'text/css; charset=utf-8');
let aggregationTemplateJsBlobUrl = MIMEObjectURL(aggregationTemplateJs, 'application/javascript; charset=utf-8');
{
let {doc, $doc} = txt2Document(aggregationTemplateHtml);
$doc.find('head').append(`<link rel="stylesheet" href="${aggregationTemplateCssBlobUrl}"/>`);
$doc.find('body').append(`<script src="${aggregationTemplateJsBlobUrl}"></script>`);
let outerHTML = doc.querySelector('html').outerHTML;
aggregationTemplateHtml = outerHTML;
}
if (ENV_STATUS == ENV_DEV_STATUS) {
console.log('final aggregationTemplateHtml', aggregationTemplateHtml);
}
let aggregationTemplateHtmlBlobUrl = MIMEObjectURL(aggregationTemplateHtml);
$('body').append(`<iframe id="aggregationIFRAME" src="${aggregationTemplateHtmlBlobUrl}" frameborder="0" scrolling="no" width="100%"></iframe>`);
await waitPageResLoadComplete();
$('body').children().each(function (index, element) {
let displayCss = $(element).css('display');
$(element).attr('display-css-cache', displayCss);
if (element.id != 'aggregationIFRAME') {
$(element).css('display', 'none');
}
});
let promiseAllResult = await Alpha_Script.asyncPool(10, imgs, async function (src, i) {
let blob = await downloadImg2Blob(src);
let url = URL.createObjectURL(blob);
// $(`#c_${i}`).append(`<img src="${src}" onload="loadHidden(this)"/>`);
return {url, blob};
});
let imgBlobSrcs = promiseAllResult.filter(p => p.status == 'fulfilled').map(p => p.value);
await postMsg({method: 'images', result: imgBlobSrcs});
log('规则执行完毕');
break;
// await Alpha_Script.sleep(5000);
}
}
log('执行完毕');
})();
async function downloadImg2Blob(imgSrc) {
let {response} = await Alpha_Script.obtainHtmlAsync({
url: imgSrc,
method: 'GET',
headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
// "Accept-Encoding": "gzip, deflate, sdch",
// "Accept-Language": "zh-CN,zh;q=0.8",
"Referer": window.location.href,
"User-Agent": window.navigator.userAgent
},
responseType: 'blob'
});
let responseHeaders = Alpha_Script.parseHeaders(response.responseHeaders);
let contentType = responseHeaders['content-type'];
if (!contentType) {
contentType = "image/png";
}
let blob = new Blob([response.response], {type: contentType});
return blob;
}
async function loadHidden(e) {
let {height, width} = await imageWidth(e);
if (!(height > 500 && width > 500)) {
e.style.display = 'none';
}
}
function getImageWidth(url) {
let img = new Image();
img.src = url;
return imageWidth(img);
}
function imageWidth(img) {
return new Promise(resolve => {
// 如果图片被缓存,则直接返回缓存数据
if (img.complete) {
resolve(img);
} else {
// 完全加载完毕的事件
img.onload = function () {
resolve(img);
}
}
});
}
GM_registerMenuCommand("规则列表", ruleListFunc, "R");
function ruleListFunc() {
let ruleCids = getRuleCids();
let delBtn = '<button type="button" class=" btn-sm btn-warning">\n' +
' 删除\n' +
' </button>';
let inject = ruleCids.map(ruleCid => `<div><span>${ruleCid}</span><span>${delBtn}</span></div>`).join("<br/>");
$('script').remove();
$('head').append('<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/4.3.1/cerulean/bootstrap.min.css"/>');
$('body').html(inject);
$('body').append('<script src="https://cdn.staticfile.org/jquery/1.12.4/jquery.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>');
}
})();
}
})
();