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