dc-fetch series

시리즈 게시글 목차 불러오기

当前为 2023-12-23 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         dc-fetch series
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  시리즈 게시글 목차 불러오기
// @author       You
// @match        https://gall.dcinside.com/board/view*
// @match        https://gall.dcinside.com/mgallery/board/view*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dcinside.com
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==

(async function() {
    'use strict';

    const CACHE_VALID_TIME = 3 * 3600 * 1000

    async function getCached(key)
    {
        const value = await GM.getValue(key);
        if(!value) return null;
        const parsed = JSON.parse(value);
        if(Date.now() - parsed.time >= CACHE_VALID_TIME) return null;
        return parsed.value;
    }

    async function setCached(key, value, forced = false)
    {
        const cached = getCached(key);
        if(!forced && JSON.stringify(value) === JSON.stringify(cached)) return cached;
        await GM.setValue(key, JSON.stringify({ value, time: Date.now() }));
        return value;
    }

    async function invalidateCached(key)
    {
        await GM.setValue(key, JSON.stringify({ value: null, time: -Infinity }));
    }

    function extractQueryString(href) // without ?
    {
        if(href.includes('?')) href = href.slice(href.indexOf('?') + 1)
        if(href.includes('#')) href = href.slice(0, href.indexOf('#'))
        return href;
    }

    function parseQueryString(str)
    {
        const pairs = extractQueryString(str).split('&').map(v => v.split('='))
        const map = new Map();

        for(let [key, value] of pairs){
            if(key.endsWith('[]')){
                key = key.slice(0, -2);
                value = value.split(',');
            }

            if(map.has(key)){
                const old = map.get(key)
                if(Array.isArray(old))
                    map.set(key, old.concat(value))
                else
                    map.set(key, [].concat(old, value))
            }else{
                map.set(key, value)
            }
        }

        return map;
    }

    async function fetchDom(uri)
    {
        const key = 'fetch|' + uri;
        let text = await getCached(key);
        if(text === null){
            console.log('fetch ' + uri);
            const res = await fetch(uri);
            text = await res.text();
            await setCached(key, text);
        }else{
            console.log('fetch ' + uri + ' from cache');
        }
        return new DOMParser().parseFromString(text, 'text/html');
    }

    async function search(id, keyword, is_mgallery = false)
    {
        const uri = `https://gall.dcinside.com/${is_mgallery ? 'mgallery/' : '' }board/lists`;
        const qs = `?id=${id}&s_type=search_subject_memo&s_keyword=${encodeURIComponent(keyword).replace(/%/g, '.')}`;
        const dom = await fetchDom(`${uri}${qs}`);
        console.log(dom);
        const search_list = dom.getElementById('kakao_seach_list');
        const trs = Array.from(search_list.getElementsByTagName('tr'));
        return trs.map(tr => {
            try{
                const $ = selector => tr.querySelector(selector)
                const text = element => element.innerText.trim()
                return {
                    no: +text($('.gall_num')),
                    uri: $('a').href,
                    title: text($('.gall_tit')),
                    gall_title: text($('.gall_name')),
                    date: text($('.gall_date'))
                }
            }catch(e){
                // console.error(dom, e)
                return null;
            }
        }).filter(article => article);
    }

    function parseTitle(title)
    {
        title = title.trim()
        if(title.match(/^.{0,4}\)/))
           title = title.split(')').slice(1).join(')');
        const matched = title.match(/(\d+)화/)
        if(matched !== null) {
            let comment = title.slice(matched.index + matched[0].length).trim()
            while(comment.startsWith('(') && comment.endsWith(')')){
                comment = comment.slice(1, -1).trim()
            }
            return {
                keyword: title.slice(0, matched.index).trim(),
                series_no: +matched[1].trim(),
                comment
            }
        } else {
            return {
                keyword: title,
                series_no: 1,
                comment: ''
            }
        }
    }

    function normalize(title)
    {
        return title.replace(/[[\]{}()~?!*&^%$#@+_":><';|\\ ,]/g, '')
    }

    function str_distance(a, b)
    {
        if(a === b) return 0;
        function make_pairs(str)
        {
            return Array(str.length-1).fill(null).map((_, i) => str.slice(i, i+2))
        }
        const a_pairs = make_pairs(a);
        const a_set = new Set(a_pairs);
        const b_pairs = make_pairs(b)
        const b_set = new Set(b_pairs);
        let distance = 1;

        b_pairs.forEach(pair => {
            if(!a_set.has(pair)) {
                ++distance
            }
        });

        a_pairs.forEach(pair => {
            if(!b_set.has(pair)) {
                ++distance
            }
        });

        return distance;
    }

    const query = parseQueryString(location.search)
    if(!query.has('id') || !query.has('no')) return;
    const id = query.get('id');
    const no = query.get('no');
    const title = document.getElementsByClassName('title_subject')[0].innerText;
    const {keyword, series_no, comment} = parseTitle(title);

    const search_result = await search(id, keyword, location.pathname.startsWith('/mgallery'));
    const normalized_keyword = normalize(keyword);
    const related = search_result
        .map(result => {
            return {
                ...result,
                ...parseTitle(result.title)
            }
        })
        .filter(article => {
            const article_qs = parseQueryString(article.uri)
            // if(article_qs.get('id') !== id) return false;
            return str_distance(normalize(article.keyword), normalized_keyword) <= 4
        })

    console.log('keyword', keyword);
    console.log('related', related);

    const series_article_sorted = related
        .concat({series_no, title, uri: location.href})
        .sort((a, b) => b.series_no - a.series_no);

    function is_same_article_uri(a, b) {
        if(typeof a !== 'string' || typeof b !== 'string') return false;
        const a_content_qs = parseQueryString(a);
        const b_content_qs = parseQueryString(b);
        return a_content_qs.get('id') === b_content_qs.get('id') && a_content_qs.get('no') === b_content_qs.get('no')
    }

    function series_content_assertion(dom) {
        const content = dom.getElementsByClassName('write_div')[0];
        if(content.innerHTML.length < 30) throw new Error('낚시(너무 짧음)');
        const series = dom.getElementsByClassName('dc_series')[0];
        if(!series) throw new Error('시리즈 없음');
    }

    async function getFail(uri)
    {
        const key = 'fail|' + uri;
        return await getCached(key);
    }


    async function setFail(uri, message)
    {
        const key = 'fail|' + uri;
        return await setCached(key, message);
    }

    async function getSeries(series_last_article)
    {
        const key = 'series|' + series_last_article.uri;
        const value = await getCached(key);

        if(value) return new DOMParser().parseFromString(value, 'text/html').body.children[0];

        const dom = await fetchDom(series_last_article.uri);
        series_content_assertion(dom);

        const series = dom.getElementsByClassName('dc_series')[0];
        const series_content = Array.from(series.children)

        const last_article_series_element = series_content.at(-2).cloneNode(true);
        last_article_series_element.href = series_last_article.uri;
        last_article_series_element.innerText = '· ' + series_last_article.title

        series.append(last_article_series_element)
        series.append(series_content.at(-1).cloneNode(true));

        await setCached(key, series.outerHTML)

        return series;
    }

    async function invalidate(uri)
    {
        await invalidateCached('fail|' + uri);
        await invalidateCached('series|' + uri);
    }

    // series_article_sorted.forEach(article => invalidate(article.uri));

    for(const series_last_article of series_article_sorted) {
        try{
            const lastFail = await getFail(series_last_article.uri);
            if(lastFail) throw { message: lastFail };

            const series = await getSeries(series_last_article);
            const series_content = Array.from(series.children)

            const content_self = series_content.filter(content => {
                if(typeof content.href !== 'string') return false;
                const content_qs = parseQueryString(content.href);
                return is_same_article_uri(content.href, location.href);
            });

            /*
            if(!is_same_article_uri(series_last_article.uri, location.href) && content_self.length === 0)
                throw new Error('자기 자신이 없음');
            */
            content_self.forEach(content => {
                content.style.fontWeight = 'bold';
            });

            const local_series = document.getElementsByClassName('dc_series')[0];
            const content = document.getElementsByClassName('write_div')[0];
            if(!local_series){
                content.prepend(series);
            }else{
                local_series.style.display = 'none';
                local_series.parentNode.insertBefore(series, local_series);
            }
            content.append(series.cloneNode(true))
            console.log('성공: ', series_last_article.uri);
            break;
        }catch(e){
            console.log('실패: ', series_last_article.uri, e);
            await setFail(series_last_article.uri, e.message);
        }
    }
})();