Greasy Fork 还支持 简体中文。

dc-fetch series

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
        }
    }
})();