· 6 years ago · Sep 21, 2019, 02:10 AM
1// ==UserScript==
2// @name mitgal-helper_2
3// @version 1.2.1-beta
4// @include https://gall.dcinside.com/mgallery/board/lists/?id=candleman*
5// @include https://gall.dcinside.com/mgallery/board/view/?id=candleman*
6// @include https://gall.dcinside.com/mgallery/board/lists?id=candleman*
7// @include https://gall.dcinside.com/mgallery/board/view?id=candleman*
8// @include https://gall.dcinside.com/mgallery/board/lists?id=momoirocode*
9// @include https://gall.dcinside.com/mgallery/board/lists/?id=momoirocode*
10// @include https://gall.dcinside.com/mgallery/board/view/?id=momoirocode*
11// @include https://gall.dcinside.com/mgallery/board/view?id=momoirocode*
12// @author ggee
13// @grant GM.xmlHttpRequest
14// ==/UserScript==
15
16;(() => {
17
18 const DL_BASE_URL = 'https://www.dlsite.com/maniax/work/=/product_id'
19 const HVDB_BASE_URL = 'https://hvdb.me/Dashboard/Add/?id='
20 const RJ_REGEXP = /(rj)?\d{6}/gi
21
22 const style = document.createElement('style')
23 style.appendChild(document.createTextNode(`
24.RJ-modal { display: none; align-items: center; justify-content: center; background: rgba(0,0,0,0.5); position: fixed; width: 100%; height: 100%; top: 0; right: 0; bottom: 0; left: 0; z-index: 2147483647 }
25.RJ-modal-content { display: flex; flex-direction: column; width: 320px; border-radius: 10px; background: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.8); padding-top: 10px; padding-bottom: 10px; text-align: center }
26.RJ-modal-title { width: 320px; padding-top: 0.3em; padding-bottom: 0.3em }
27.RJ-modal-meta { text-align: left; padding: 10px 20px; line-height: 1.5 }
28.RJ-modal-meta a { border-bottom: 1px dotted }
29.RJ-modal-meta .main_genre > a + a { margin-left: 10px }
30.RJ-modal-meta > dt { float: left; clear: left; text-align: right; min-width: 50px; font-weight: bold }
31.RJ-modal-meta > dd { padding-left: 60px }
32.RJ-modal-img { width: 100% }
33.RJ-modal-button { box-sizing: border-box; width: 100%; display: block; padding-top: 0.6em; padding-bottom: 0.6em; font-size: 1.8em; font-weight: bold }
34.RJ-modal-button:hover, .RJ-modal-button:focus { background-color: #eee }
35.RJ-rel { border: 1px dashed #999; border-radius: 5px; padding: 2px; font-weight: bold; }
36.RJ-link { background: #999; color: #fff; border-radius: 5px; line-height: 1; display: inline-block; width: 20px; height: 13px; text-align: center; font-weight: bold; cursor: pointer }
37.RJ-link:first-child { margin-left: 2px }
38.RJ-link:hover { background: blue }
39`))
40 document.head.appendChild(style)
41
42 function request(options = {}) {
43 return new Promise((resolve, reject) => {
44 GM.xmlHttpRequest(Object.assign({
45 method: 'GET',
46 onload: function (resp) {
47 if (resp.status === 200) { resolve(resp.response) }
48 else { reject(resp) }
49 },
50 onerror: function (resp) {
51 reject(resp)
52 },
53 }, options))
54 })
55 }
56
57 function fetchAPI(rj) {
58 const url = `https://www.dlsite.com/maniax/product/info/ajax?product_id=${rj}&cdn_cache_min=1`
59 return request({ url, responseType: 'json' })
60 }
61
62 function makeModal() {
63 const container = document.createElement('div')
64 container.className = 'RJ-modal'
65 container.addEventListener('click', function(event) { if (event.target === this) container.style.display = 'none' })
66 document.body.appendChild(container)
67 return container
68 }
69
70 function makeRate(rate) {
71 const half = rate % 1 ? '◐' : ''
72 const star = Array(Math.floor(rate)).fill('●').join('')
73 const empty = Array(Math.floor(5 - rate)).fill('○').join('')
74 return [star, half, empty].join('')
75 }
76
77 const modalContainer = makeModal()
78
79 async function showModal(rj) {
80 modalContainer.innerHTML = ''
81
82 const content = document.createElement('figure')
83 content.className = 'RJ-modal-content'
84
85 const title = document.createElement('h1')
86 title.textContent = rj
87 title.className = 'RJ-modal-title'
88 content.appendChild(title)
89
90 const imgA = document.createElement('a')
91 imgA.href = getThumbSrc(rj)
92 imgA.target = '_blank'
93
94 const img = document.createElement('img')
95 img.src = imgA.href
96 img.className = 'RJ-modal-img'
97 imgA.appendChild(img)
98 content.appendChild(imgA)
99
100 const a = document.createElement('a')
101 a.className = 'RJ-modal-button'
102 a.href = `${DL_BASE_URL}/${rj}.html`
103 a.target = '_blank'
104 a.textContent = 'DLsite'
105
106 img.addEventListener('error', (event) => {
107 // 이미지 로딩에 실패했을 경우 예고작으로 간주함
108 if (event.target.classList.contains('RJ-announce')) { return }
109 event.target.classList.add('RJ-announce')
110 replaceAnnounceUrl({ a, img })
111 })
112
113 img.addEventListener('load', (event) => {
114 addMetadata({ rj, a, img })
115 })
116
117 const a2 = a.cloneNode(true)
118 a2.href = `${HVDB_BASE_URL}${rj}`
119 a2.textContent = 'HVDB'
120
121 content.appendChild(a)
122 content.appendChild(a2)
123
124 modalContainer.appendChild(content)
125 modalContainer.style.display = 'flex'
126 }
127
128 let tryCount = 0
129
130 async function addMetadata({ rj, a, img }) {
131 const numFormat = new Intl.NumberFormat()
132 const toBlank = aHtml => aHtml.replace(/a href/g, 'a target="_blank" href')
133 const insertAfter = (rf, node) => rf.parentNode.insertBefore(node, rf.nextSibling)
134
135 const metaList = document.createElement('dl')
136 metaList.className = 'RJ-modal-meta'
137
138 const meta = {}
139 const DICT = {
140 'PRICE': '가격',
141 'RATE': '평점',
142 'DL': '다운수',
143 'WISH': '찜수',
144 '年齢指定': '수위',
145 'ジャンル': '태그',
146 }
147
148 try {
149 const body = await request({ url: a.href })
150 const frag = document.createElement('div')
151 frag.innerHTML = body
152
153 const table = frag.querySelector('#work_outline')
154 const rows = Array.from(table.querySelectorAll('tr'))
155 const pHtml = rows.forEach(row => {
156 const [th, td] = Array.from(row.children)
157 switch (th.textContent) {
158 case '販売日':
159 case '予告開始日':
160 case '声優':
161 case '年齢指定':
162 case 'ジャンル':
163 meta[th.textContent] = toBlank(td.innerHTML)
164 break
165 default:
166 break
167 }
168 })
169
170 } catch (e) {
171 console.error(e)
172 if (e.status === 404) {
173 if (tryCount++ < 1) {
174 replaceAnnounceUrl({ a })
175 addMetadata({ rj, a, img })
176 }
177 return
178 }
179 }
180
181 try {
182 const api = await fetchAPI(rj)
183 const data = api[rj] || {}
184 const { dl_count, wishlist_count, rate_average, rate_count, price_str } = data
185 meta.RATE = `${makeRate(rate_average)} (${rate_count})`
186 meta.DL = numFormat.format(dl_count)
187 meta.WISH = numFormat.format(wishlist_count)
188 meta.PRICE = `${price_str}円`
189 } catch (e) { console.error(e) }
190
191 Object.entries(DICT).forEach(([ key, value ]) => {
192 if (!meta[key]) { return }
193 const dt = document.createElement('dt')
194 const dd = document.createElement('dd')
195 dt.textContent = value
196 dd.innerHTML = meta[key]
197 metaList.appendChild(dt)
198 metaList.appendChild(dd)
199 // dt가 목차, dd가 내용물
200 // 원하는 dict이랑 meta만 없애버리면
201
202 })
203
204 insertAfter(a, metaList)
205 // img 뒤에 추가
206 }
207
208 function replaceAnnounceUrl({ a, img }) {
209 if (img) {
210 const toSrcReplaced = img.src.replace('/work/', '/ana/').replace('_img_', '_ana_img_')
211 if (img.src !== toSrcReplaced) { img.src = toSrcReplaced }
212 }
213 if (a) {
214 const toHrefReplaced = a.href.replace('/work/', '/announce/')
215 if (a.href !== toHrefReplaced) { a.href = toHrefReplaced }
216 }
217 }
218
219 function makeModalButton(rj) {
220 const a = document.createElement('span')
221 a.setAttribute('data-rj', rj)
222
223 rj = rj.length === 6 ? `RJ${rj}` : rj.toUpperCase()
224 a.textContent = '?'
225 a.className = 'RJ-link'
226 a.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); showModal(rj) })
227 return a
228 }
229
230 function getThumbSrc(rj) {
231 rj = rj.replace(/rj/gi, '')
232 let bucket = Math.ceil(Number(rj) / 1000) * 1000
233 bucket = ('0' + String(bucket)).slice(-6)
234 return `https://img.dlsite.jp/modpub/images2/work/doujin/RJ${bucket}/RJ${rj}_img_main.jpg`
235 }
236
237
238 function filterText(text = '') {
239 let matches
240 let i = 0
241 let rjs = []
242
243 while ((matches = RJ_REGEXP.exec(text)) !== null) {
244 let rj = matches[0]
245 let startIndex = matches.index
246 let lastIndex = RJ_REGEXP.lastIndex
247
248 if (text[startIndex - 1] && /[\w/]/.test(text[startIndex - 1])) {
249 // 앞글자가 붙어있음: pass
250 continue
251 }
252
253 if (text[lastIndex] && /[\w/]/.test(text[lastIndex])) {
254 // 뒷글자가 붙어있음: pass
255 continue
256 }
257
258 rjs.push(rj)
259 // text replace
260 text = `${text.slice(0, matches.index)}__{${i++}}__${text.slice(lastIndex)}`
261 }
262
263 for (let j = 0, len = rjs.length; j < len; j++) {
264 text = text.replace(
265 new RegExp('__\\{' + j + '\\}__', 'g'),
266 `<em class="RJ-rel" data-rj="${rjs[j]}">${rjs[j]}</em>`
267 )
268 }
269
270 return text
271 }
272
273 //
274 function addLink(node) {
275 if (!node) return
276 let matches = node.textContent.match(RJ_REGEXP)
277 if (!matches) return
278 if (node.classList.contains('RJ')) return
279 node.classList.add('RJ')
280
281 matches = [...new Set(matches)]
282
283 node.innerHTML = filterText(node.innerHTML)
284
285 matches.forEach(match => {
286 const mb = makeModalButton(match)
287 const rj = mb.getAttribute('data-rj')
288 const target = node.querySelector(`[data-rj="${rj}"]`)
289 if (!target) return
290 target.appendChild(mb)
291 //버튼을 뒤에 추가
292 })
293 }
294
295 const titles = Array.from(document.querySelectorAll('.gall_tit > a')).filter(a => !a.classList.contains('reply_numbox'))
296 const body = document.querySelector('.writing_view_box')
297 const titleSubject = document.querySelector('.title_subject')
298 const comments = Array.from(document.querySelectorAll('.usertxt'))
299
300 titles.map(addLink)
301 addLink(titleSubject)
302 addLink(body)
303 comments.map(addLink)
304 // 타이틀 댓글 제목 등에 링크 추가해주는 부분, 즉 갤용
305
306})();