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