dc-fetch series

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

  1. // ==UserScript==
  2. // @name dc-fetch series
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description 시리즈 게시글 목차 불러오기
  6. // @author You
  7. // @match https://gall.dcinside.com/board/view*
  8. // @match https://gall.dcinside.com/mgallery/board/view*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=dcinside.com
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (async function() {
  16. 'use strict';
  17.  
  18. const CACHE_VALID_TIME = 3 * 3600 * 1000
  19.  
  20. async function getCached(key)
  21. {
  22. const value = await GM.getValue(key);
  23. if(!value) return null;
  24. const parsed = JSON.parse(value);
  25. if(Date.now() - parsed.time >= CACHE_VALID_TIME) return null;
  26. return parsed.value;
  27. }
  28.  
  29. async function setCached(key, value, forced = false)
  30. {
  31. const cached = getCached(key);
  32. if(!forced && JSON.stringify(value) === JSON.stringify(cached)) return cached;
  33. await GM.setValue(key, JSON.stringify({ value, time: Date.now() }));
  34. return value;
  35. }
  36.  
  37. async function invalidateCached(key)
  38. {
  39. await GM.setValue(key, JSON.stringify({ value: null, time: -Infinity }));
  40. }
  41.  
  42. function extractQueryString(href) // without ?
  43. {
  44. if(href.includes('?')) href = href.slice(href.indexOf('?') + 1)
  45. if(href.includes('#')) href = href.slice(0, href.indexOf('#'))
  46. return href;
  47. }
  48.  
  49. function parseQueryString(str)
  50. {
  51. const pairs = extractQueryString(str).split('&').map(v => v.split('='))
  52. const map = new Map();
  53.  
  54. for(let [key, value] of pairs){
  55. if(key.endsWith('[]')){
  56. key = key.slice(0, -2);
  57. value = value.split(',');
  58. }
  59.  
  60. if(map.has(key)){
  61. const old = map.get(key)
  62. if(Array.isArray(old))
  63. map.set(key, old.concat(value))
  64. else
  65. map.set(key, [].concat(old, value))
  66. }else{
  67. map.set(key, value)
  68. }
  69. }
  70.  
  71. return map;
  72. }
  73.  
  74. async function fetchDom(uri)
  75. {
  76. const key = 'fetch|' + uri;
  77. let text = await getCached(key);
  78. if(text === null){
  79. console.log('fetch ' + uri);
  80. const res = await fetch(uri);
  81. text = await res.text();
  82. await setCached(key, text);
  83. }else{
  84. console.log('fetch ' + uri + ' from cache');
  85. }
  86. return new DOMParser().parseFromString(text, 'text/html');
  87. }
  88.  
  89. async function search(id, keyword, is_mgallery = false)
  90. {
  91. const uri = `https://gall.dcinside.com/${is_mgallery ? 'mgallery/' : '' }board/lists`;
  92. const qs = `?id=${id}&s_type=search_subject_memo&s_keyword=${encodeURIComponent(keyword).replace(/%/g, '.')}`;
  93. const dom = await fetchDom(`${uri}${qs}`);
  94. console.log(dom);
  95. const search_list = dom.getElementById('kakao_seach_list');
  96. const trs = Array.from(search_list.getElementsByTagName('tr'));
  97. return trs.map(tr => {
  98. try{
  99. const $ = selector => tr.querySelector(selector)
  100. const text = element => element.innerText.trim()
  101. return {
  102. no: +text($('.gall_num')),
  103. uri: $('a').href,
  104. title: text($('.gall_tit')),
  105. gall_title: text($('.gall_name')),
  106. date: text($('.gall_date'))
  107. }
  108. }catch(e){
  109. // console.error(dom, e)
  110. return null;
  111. }
  112. }).filter(article => article);
  113. }
  114.  
  115. function parseTitle(title)
  116. {
  117. title = title.trim()
  118. if(title.match(/^.{0,4}\)/))
  119. title = title.split(')').slice(1).join(')');
  120. const matched = title.match(/(\d+)화/)
  121. if(matched !== null) {
  122. let comment = title.slice(matched.index + matched[0].length).trim()
  123. while(comment.startsWith('(') && comment.endsWith(')')){
  124. comment = comment.slice(1, -1).trim()
  125. }
  126. return {
  127. keyword: title.slice(0, matched.index).trim(),
  128. series_no: +matched[1].trim(),
  129. comment
  130. }
  131. } else {
  132. return {
  133. keyword: title,
  134. series_no: 1,
  135. comment: ''
  136. }
  137. }
  138. }
  139.  
  140. function normalize(title)
  141. {
  142. return title.replace(/[[\]{}()~?!*&^%$#@+_":><';|\\ ,]/g, '')
  143. }
  144.  
  145. function str_distance(a, b)
  146. {
  147. if(a === b) return 0;
  148. function make_pairs(str)
  149. {
  150. return Array(str.length-1).fill(null).map((_, i) => str.slice(i, i+2))
  151. }
  152. const a_pairs = make_pairs(a);
  153. const a_set = new Set(a_pairs);
  154. const b_pairs = make_pairs(b)
  155. const b_set = new Set(b_pairs);
  156. let distance = 1;
  157.  
  158. b_pairs.forEach(pair => {
  159. if(!a_set.has(pair)) {
  160. ++distance
  161. }
  162. });
  163.  
  164. a_pairs.forEach(pair => {
  165. if(!b_set.has(pair)) {
  166. ++distance
  167. }
  168. });
  169.  
  170. return distance;
  171. }
  172.  
  173. const query = parseQueryString(location.search)
  174. if(!query.has('id') || !query.has('no')) return;
  175. const id = query.get('id');
  176. const no = query.get('no');
  177. const title = document.getElementsByClassName('title_subject')[0].innerText;
  178. const {keyword, series_no, comment} = parseTitle(title);
  179.  
  180. const search_result = await search(id, keyword, location.pathname.startsWith('/mgallery'));
  181. const normalized_keyword = normalize(keyword);
  182. const related = search_result
  183. .map(result => {
  184. return {
  185. ...result,
  186. ...parseTitle(result.title)
  187. }
  188. })
  189. .filter(article => {
  190. const article_qs = parseQueryString(article.uri)
  191. // if(article_qs.get('id') !== id) return false;
  192. return str_distance(normalize(article.keyword), normalized_keyword) <= 4
  193. })
  194.  
  195. console.log('keyword', keyword);
  196. console.log('related', related);
  197.  
  198. const series_article_sorted = related
  199. .concat({series_no, title, uri: location.href})
  200. .sort((a, b) => b.series_no - a.series_no);
  201.  
  202. function is_same_article_uri(a, b) {
  203. if(typeof a !== 'string' || typeof b !== 'string') return false;
  204. const a_content_qs = parseQueryString(a);
  205. const b_content_qs = parseQueryString(b);
  206. return a_content_qs.get('id') === b_content_qs.get('id') && a_content_qs.get('no') === b_content_qs.get('no')
  207. }
  208.  
  209. function series_content_assertion(dom) {
  210. const content = dom.getElementsByClassName('write_div')[0];
  211. if(content.innerHTML.length < 30) throw new Error('낚시(너무 짧음)');
  212. const series = dom.getElementsByClassName('dc_series')[0];
  213. if(!series) throw new Error('시리즈 없음');
  214. }
  215.  
  216. async function getFail(uri)
  217. {
  218. const key = 'fail|' + uri;
  219. return await getCached(key);
  220. }
  221.  
  222.  
  223. async function setFail(uri, message)
  224. {
  225. const key = 'fail|' + uri;
  226. return await setCached(key, message);
  227. }
  228.  
  229. async function getSeries(series_last_article)
  230. {
  231. const key = 'series|' + series_last_article.uri;
  232. const value = await getCached(key);
  233.  
  234. if(value) return new DOMParser().parseFromString(value, 'text/html').body.children[0];
  235.  
  236. const dom = await fetchDom(series_last_article.uri);
  237. series_content_assertion(dom);
  238.  
  239. const series = dom.getElementsByClassName('dc_series')[0];
  240. const series_content = Array.from(series.children)
  241.  
  242. const last_article_series_element = series_content.at(-2).cloneNode(true);
  243. last_article_series_element.href = series_last_article.uri;
  244. last_article_series_element.innerText = '· ' + series_last_article.title
  245.  
  246. series.append(last_article_series_element)
  247. series.append(series_content.at(-1).cloneNode(true));
  248.  
  249. await setCached(key, series.outerHTML)
  250.  
  251. return series;
  252. }
  253.  
  254. async function invalidate(uri)
  255. {
  256. await invalidateCached('fail|' + uri);
  257. await invalidateCached('series|' + uri);
  258. }
  259.  
  260. // series_article_sorted.forEach(article => invalidate(article.uri));
  261.  
  262. for(const series_last_article of series_article_sorted) {
  263. try{
  264. const lastFail = await getFail(series_last_article.uri);
  265. if(lastFail) throw { message: lastFail };
  266.  
  267. const series = await getSeries(series_last_article);
  268. const series_content = Array.from(series.children)
  269.  
  270. const content_self = series_content.filter(content => {
  271. if(typeof content.href !== 'string') return false;
  272. const content_qs = parseQueryString(content.href);
  273. return is_same_article_uri(content.href, location.href);
  274. });
  275.  
  276. /*
  277. if(!is_same_article_uri(series_last_article.uri, location.href) && content_self.length === 0)
  278. throw new Error('자기 자신이 없음');
  279. */
  280. content_self.forEach(content => {
  281. content.style.fontWeight = 'bold';
  282. });
  283.  
  284. const local_series = document.getElementsByClassName('dc_series')[0];
  285. const content = document.getElementsByClassName('write_div')[0];
  286. if(!local_series){
  287. content.prepend(series);
  288. }else{
  289. local_series.style.display = 'none';
  290. local_series.parentNode.insertBefore(series, local_series);
  291. }
  292. content.append(series.cloneNode(true))
  293. console.log('성공: ', series_last_article.uri);
  294. break;
  295. }catch(e){
  296. console.log('실패: ', series_last_article.uri, e);
  297. await setFail(series_last_article.uri, e.message);
  298. }
  299. }
  300. })();

QingJ © 2025

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