いろいろ自動読み込み [仮]

レス末尾にサムネイル画像やツイート埋め込みなどを追加します。

// ==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;
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址