// ==UserScript==
// @name いろいろ自動読み込み [仮]
// @description レス末尾にサムネイル画像やツイート埋め込みなどを追加します。
// @version 2.1.0
// @match *://*.2chan.net/*
// @icon https://icons.duckduckgo.com/ip2/www.2chan.net.ico
// @namespace https://gf.qytechs.cn/users/809755
// @grant GM_xmlhttpRequest
// @run-at document-end
// @license MIT
// @connect item.rakuten.co.jp
// @connect booth.pm
// @connect steampowered.com
// @connect shueisha.co.jp
// @connect shonenjump.com
// @connect shonenjumpplus.com
// @connect tonarinoyj.jp
// @connect shonenmagazine.com
// @connect yanmaga.jp
// @connect comic-days.com
// @connect sunday-webry.com
// @connect urasunday.com
// @connect shogakukan.co.jp
// @connect web-ace.jp
// @connect comic-walker.com
// @connect 7irocomics.jp
// @connect 123hon.com
// @connect chijin-plus.com
// @connect comic-action.com
// @connect comic-boost.com
// @connect comic-brise.com
// @connect comic-fuz.com
// @connect comic-gardo.com
// @connect comic-medu.com
// @connect comic-meteor.jp
// @connect comic-polaris.jp
// @connect comic-ryu.jp
// @connect comic-trail.com
// @connect comic-valkyrie.com
// @connect comic-zenon.com
// @connect comicborder.com
// @connect comicbunch.com
// @connect comicbushi-web.com
// @connect comicnettai.com
// @connect comico.jp
// @connect comicride.jp
// @connect comip.jp
// @connect cycomi.com
// @connect digitalmargaret.jp
// @connect feelweb.jp
// @connect futabanet.jp
// @connect ganganonline.com
// @connect heros-web.com
// @connect ichijinsha.co.jp
// @connect inthelife.club
// @connect j-nbooks.jp
// @connect kuragebunch.com
// @connect leedcafe.com
// @connect mag-garden.co.jp
// @connect magcomi.com
// @connect manga-comisol.jp
// @connect manga-park.com
// @connect mangabox.me
// @connect mangacross.jp
// @connect mangafactory.jp
// @connect novema.jp
// @connect pixiv.net
// @connect souffle.life
// @connect sukima.me
// @connect takeshobo.co.jp
// @connect tameshiyo.me
// @connect webnewtype.com
// @connect yawaspi.com
// @connect ext.nicovideo.jp
// @connect tver.jp
// ==/UserScript==
/* jshint esversion: 8 */
(function () {
'use strict';
setTimeout(() => {
setupTwitterWidget();
setInterval(main, 2000);
}, 1000);
function main() {
const targetDomains = [
[/2chan\.net$/, futaba],
[/amazon(?:\.co)?\.jp$/, amazon],
[/(?:youtube\.com|youtu\.be)$/, youtube],
[/twitter\.com$/, twitter],
[/twimg\.com$/, twimg],
[/(?:www\.nicovideo\.jp|nico\.ms)$/, niconico],
[/www\.pixiv\.net$/, pixiv],
];
const DomainOGP = [
/****** ショップ系 ******/
'item.rakuten.co.jp',
'booth.pm',
'steampowered.com',
/****** 未分類 ******/
'tver.jp',
/****** Webマンガ? ******/
/*----- 集英社 -----*/
'shueisha.co.jp',
'shonenjump.com',
'shonenjumpplus.com',
'tonarinoyj.jp',
/*----- 講談社 -----*/
'shonenmagazine.com',
'yanmaga.jp',
'comic-days.com',
/*----- 小学館 -----*/
'sunday-webry.com',
'urasunday.com',
'shogakukan.co.jp',
/*----- KADOKAWA -----*/
'web-ace.jp',
'comic-walker.com',
/*----- その他 -----*/
'7irocomics.jp',
'123hon.com',
'chijin-plus.com',
'comic-action.com',
'comic-boost.com',
'comic-brise.com',
'comic-fuz.com',
'comic-gardo.com',
'comic-medu.com',
'comic-meteor.jp',
'comic-polaris.jp',
'comic-ryu.jp',
'comic-trail.com',
'comic-valkyrie.com',
'comic-zenon.com',
'comicborder.com',
'comicbunch.com',
'comicbushi-web.com',
'comicnettai.com',
'comico.jp',
'comicride.jp',
'comip.jp',
'cycomi.com',
'digitalmargaret.jp',
'feelweb.jp',
'futabanet.jp',
'ganganonline.com',
'heros-web.com',
'ichijinsha.co.jp',
'inthelife.club',
'j-nbooks.jp',
'kuragebunch.com',
'leedcafe.com',
'mag-garden.co.jp',
'magcomi.com',
'manga-comisol.jp',
'manga-park.com',
'mangabox.me',
'mangacross.jp',
'mangafactory.jp',
'novema.jp',
'pixiv.net',
'souffle.life',
'sukima.me',
'takeshobo.co.jp',
'tameshiyo.me',
'webnewtype.com',
'yawaspi.com',
//'comic-earthstar.jp',
//'firecross.jp',
//'ganma.jp',
//'pash-up.jp',
];
const ignoreExt = /(?:bmp|jpe?g|png|gif|web[pm]|mp[34]|pdf|exe|zip)/;
(async () => {
for (let a of document.querySelectorAll("blockquote a:not(.checked):not(.imgAdded)")) {
await (a => {
return new Promise((resolve, reject) => {
a.classList.add('checked');
for (const value of targetDomains) {
if (value[0].test(a.host)) {
return resolve(value[1](a));
}
}
if (a.parentNode.nodeName.toLowerCase() !== 'blockquote') {
// 引用されてるときはOGPの処理をスキップ
return resolve();
}
const domainChk = (DomainOGP.some(d => escapeRegExp(d).test(a.host)));
const extChk = !ignoreExt.test(a.pathname.split(/[#?]/)[0].split('/').pop().split('.').pop());
if (domainChk && extChk) {
return resolve(addOGP(a));
}
return resolve();
});
})(a);
}
})();
}
function futaba(a) {
const m = a.href.match(/dec\.2chan\.net\/up2?\/src\/(fu?\d+?)\.(.+)/i);
if (m) {
if (/bmp|jpe?g|png|gif|webp/i.test(m[2])) {
const src = `https://${a.host.replace(/\./g, '-')}.cdn.ampproject.org/ii/w128/s/${a.host}${a.pathname}`;
const a2 = createLinkThumbnails(a.href, src, '#FFE');
appendDetails(a, a2);
}
}
}
function amazon(a) {
const m = a.href.match(/amazon[^/]+jp\/.*?(?:dp|gp\/(?:product|aw\/d)|ASIN)\/(\w+)/);
if (m) {
// const src = `https://ws-fe.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=JP&ASIN=${m[1]}&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL120_`;
const href = `https://www.amazon.co.jp/dp/${m[1]}`;
a.href = a.innerText = href;
// const a2 = createLinkThumbnails(href, src, '#F90');
// appendDetails(a, a2);
}
}
function youtube(a) {
const m = a.href.match(/(?:youtu\.be\/|youtube\.com\/(?:watch.*?\Wv=|shorts\/))(.{11})/);
if (m) {
const src = `https://i.ytimg.com/vi/${m[1]}/default.jpg`;
const a2 = createLinkThumbnails(a.href, src, '#FCC');
appendDetails(a, a2);
}
}
function twitter(a) {
const m = a.href.match(/twitter\.com\/(\w+?)\/\w+?\/(\d+)/);
if (m) {
a.href = `https://twitter.com/${m[1]}/status/${m[2]}`;
const blockquote = document.createElement('blockquote');
blockquote.classList.add('twitter-tweet');
blockquote.innerHTML = `<a href="${a.href}" class='checked'></a>`;
appendDetails(a, blockquote, 'tweetArea');
twttr.widgets.load(blockquote);
}
}
function twimg(a) {
const m = a.href.match(/pbs\.twimg\.com\/media\/([\w-]+).+?(png|jpg)/);
if (m) {
const urlThumb = `https://pbs.twimg.com/media/${m[1]}.${m[2]}?name=thumb`;
const urlOrig = `https://pbs.twimg.com/media/${m[1]}.${m[2]}?name=orig`;
a.href = urlOrig;
const a2 = createLinkThumbnails(urlOrig, urlThumb, '#1DA1F2');
appendDetails(a, a2);
}
}
async function niconico(a) {
const m = a.href.match(/(?:nico\.ms|www\.nicovideo\.jp\/watch)\/(\w+)/);
if (m) {
const src = await (id => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://ext.nicovideo.jp/api/getthumbinfo/${id}`,
anonymouse: true,
onload: function (response) {
const res = response.responseText;
const reg1 = new RegExp(`<thumbnail_url>([^<]+?)<`, 'i');
const found = res.match(reg1);
if (found) {
return resolve(found[1]);
}
reject(response);
},
onerror: function (error) {
reject(error);
}
});
}))(m[1]);
const a2 = createLinkThumbnails(a.href, src);
appendDetails(a, a2);
}
}
async function pixiv(a) {
const m = a.href.match(/www\.pixiv\.net\/artworks\/(\d+)/);
if (m) {
const urlThumb = `https://embed.pixiv.net/decorate.php?illust_id=${m[1]}`;
const result = await (url => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "HEAD",
url: url,
anonymouse: true,
onload: function (response) {
if (response.status !== 404) {
return resolve(true);
}
reject(`${url} 404`);
},
onerror: function (error) {
reject(error);
}
});
}))(urlThumb);
if (result === true) {
const a2 = createLinkThumbnails(a.href, urlThumb);
appendDetails(a, a2);
}
}
}
async function addOGP(a) {
const src = await getOGP(a.href);
if (src) {
const a2 = createLinkThumbnails(a.href, src);
appendDetails(a, a2);
}
}
function getOGP(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
anonymouse: true,
onload: function (response) {
const res = response.responseText;
const regexp = [
new RegExp(`(?:og|twitter):image['"\\s]+content["'=\\s]+?([^'">]+?)["']`, 'i'),
new RegExp(`meta\\s+?content["'=\\s]+?([^'">]+?)["'\\s]+?(?:property|name)["'=\\s]+?(?:twitter|og):image`, 'i'),
new RegExp(`imageUrl['":\s]+([^"']+?)["']`, 'i'),
];
/*
const meta = (new DOMParser()).parseFromString(res, 'text/html').querySelectorAll('meta[property*="image"]');
console.table(meta);
*/
let found;
for (const reg of regexp) {
found = res.match(reg);
if (found) {
const decodedString = (new DOMParser()).parseFromString(`<!doctype html><body>${found[1].replace(/\\+u0026/g, '&')}`, 'text/html').body.textContent;
const newURL = (new URL(decodedString, response.finalUrl)).href;
return resolve(newURL);
}
}
reject(`${url} Not Found`);
},
onerror: function (error) {
reject(`${url} OGP Error`);
}
});
});
}
function appendDetails(parent, element, targetArea = 'thumbsArea') {
for (let i = 0; i < 3; i++) {
parent = parent.parentNode;
if (parent.nodeName.toLowerCase() === 'blockquote') {
let details = parent.querySelector('.previewArea');
if (!details) {
details = createDetails();
parent.appendChild(details);
}
let target = details.querySelector(`.${targetArea}`);
target.appendChild(element);
return;
}
}
}
function escapeRegExp(s) {
return new RegExp(`${s.replace(/\./g, '\.')}$`);
}
function setupTwitterWidget() {
// if ([...document.querySelectorAll('blockquote a')].some(a => /twitter\.com$/.test(a.host))) {
const script = document.createElement('script');
script.setAttribute('async', true);
script.setAttribute('charset', 'utf-8');
script.src = 'https://platform.twitter.com/widgets.js';
document.body.appendChild(script);
// }
}
function createDetails() {
const summary = document.createElement('summary');
summary.innerHTML = '<input type="button" value="開閉ボタン" onclick="(()=>{this.parentNode.click()})();">';
const div1 = document.createElement('div');
div1.classList.add('thumbsArea');
const div2 = document.createElement('div');
div2.classList.add('tweetArea');
const details = document.createElement('details');
details.classList.add('previewArea');
details.setAttribute('open', true);
details.appendChild(summary);
details.appendChild(div1);
details.appendChild(div2);
return details;
}
function createLinkThumbnails(linkUrl, imgSrc, solid = '#ccc') {
const img = document.createElement('img');
img.src = imgSrc;
img.setAttribute("loading", "lazy");
img.setAttribute('align', 'top');
img.setAttribute('style', `max-width: 160px; margin: 5px; border: 2px solid ${solid};`);
const a = document.createElement('a');
a.setAttribute('target', '_blank');
a.classList.add('checked');
a.href = linkUrl;
a.appendChild(img);
return a;
}
})();