🏠 返回首頁 

Greasy Fork is available in English.

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