시리즈 게시글 목차 불러오기
// ==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_r###lt = await search(id, keyword, location.pathname.startsWith('/mgallery')); const normalized_keyword = normalize(keyword); const related = search_r###lt .map(r###lt => { return { ...r###lt, ...parseTitle(r###lt.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); } } })();