// ==UserScript== // @name Arcalive Night Restaurant Secretary // @namespace Violentmonkey Scripts // @match https://arca.live/b/* // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @version 1.0.1.1 // @author - // @description 2021. 7. 13. 오전 1:16:21 // @noframes // @downloadURL none // ==/UserScript== // onload init let loaded = false window.addEventListener('load', e => { if (!loaded) { loaded = true registerMenuCommand(); asideMake(); //easyFroalaInsertImageTrigger() autoWriteEditor(); arcaconLinkInit(); clipboardPasteToBase64Init(); userInfoViewInit(); } }); // 배열 자르기, 페이지네이션 Array.prototype.division = function(n) { var arr = this; var len = arr.length; var cnt = Math.floor(len / n) + (Math.floor(len % n) > 0 ? 1 : 0); var tmp = []; for (var i = 0; i < cnt; i++) { tmp.push(arr.splice(0, n)); } return tmp; } // 바이트값 정리 함수 function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '-'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // 키 시뮬레이트 function simulateKey(keyCode, type, modifiers) { var evtName = (typeof(type) === "string") ? "key" + type : "keydown"; var modifier = (typeof(modifiers) === "object") ? modifier : {}; var event = document.createEvent("HTMLEvents"); event.initEvent(evtName, true, false); event.keyCode = keyCode; for (var i in modifiers) { event[i] = modifiers[i]; } document.dispatchEvent(event); } // froalaeditor 이미지 삽입 감지 (미완성) async function easyFroalaInsertImageTrigger() { if (location.href.includes("/write") || location.href.includes("/edit")) { let imageResizer = document.querySelector(".fr-image-resizer") const editorTextarea = document.querySelector("#content") let oldMatch = 0 while (true) { await sleep(100) let newMatch = editorTextarea.value.match(//g) if (newMatch && oldMatch !== newMatch.length) { oldMatch = newMatch.length await sleep(500) simulateKey(37); await sleep(10) simulateKey(37, "up"); simulateKey(40); await sleep(10) simulateKey(40, "up"); await sleep(10) simulateKey(13, "up"); } else if (newMatch === null) { oldMatch = 0 } } } } // fetch 함수 function doFetch(url, options = { method: 'GET', responseType: 'document' }, silent = false) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url: url, method: options.method, responseType: options.responseType, headers: options.headers, data: options.data, onload: result => { console.debug(result) if (result.status == 200) { resolve(result.response); } else { if (!silent) { console.log(result) alert("불러오기 에러 발생 : " + url) reject(result.status); } else { console.debug(result) reject(result.status); } } } }); }); } // 사이드바 추가 function asideMake() { const rightSidebar = document.querySelector(".right-sidebar") const sidebarItem = document.createElement('div') sidebarItem.setAttribute("class", "sidebar-item") sidebarItem.setAttribute("id", "smtranslation") const itemTitle = document.createElement('div') itemTitle.setAttribute("class", "item-title") itemTitle.innerText = "번역 소식" var style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = '.sm-translate-tab { padding-bottom: .25rem;border-bottom: 0 solid #00a495;margin:8.7%;cursor:pointer;transition-duration: 0.1s }'; style.innerHTML += '.sm-translate-tab-activated { padding-bottom: .25rem;border-bottom: 2px solid #00a495;margin:8.7%;cursor:pointer;transition-duration: 0.1s }'; style.innerHTML += '.sm-translate-arrow-left { margin: auto 5px;float:right;width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-right: 7px solid black;cursor:pointer }'; style.innerHTML += '.sm-translate-arrow-right { margin: auto 5px;float:right;width: 0; height: 0; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 7px solid black;cursor:pointer }'; document.getElementsByTagName('head')[0].appendChild(style); const smallTitle = document.createElement('div') smallTitle.setAttribute("style", "margin-bottom:12px") const linkList = document.createElement('div') linkList.setAttribute("class", "link-list clearfix") const arrowDiv = document.createElement('div') arrowDiv.setAttribute("class", "sm-translation-arrows") const arrowRight = document.createElement("div") arrowRight.setAttribute("class", "sm-translate-arrow-right") const arrowLeft = document.createElement("div") arrowLeft.setAttribute("class", "sm-translate-arrow-left") arrowDiv.appendChild(arrowRight) arrowDiv.appendChild(arrowLeft) const translating = document.createElement('span') translating.setAttribute("class", "sm-translate-tab-activated") translating.innerText = "번역중" translating.onclick = (e) => { activateTab(e) arrowDiv.setAttribute("style", "display: none") translatingPageNum = 0 // 카테고리: 실황, 키워드: 번역 / 색인 getTranslatePageFetch([ `https://arca.live/b/smpeople?target=content&keyword=${srt}`, "https://arca.live/b/smpeople?category=실황&target=title&keyword=번역" ], linkList, arrowRight, arrowLeft) .then(t => { if (t) { arrowDiv.removeAttribute("style") } }) } const notranslate = document.createElement('span') notranslate.setAttribute("class", "sm-translate-tab") notranslate.innerText = "미번역" notranslate.onclick = (e) => { activateTab(e) arrowDiv.setAttribute("style", "display: none") translatingPageNum = 0 // 키워드: 번역요청 getTranslatePageFetch([ "https://arca.live/b/smpeople?target=title&keyword=번역요청" ], linkList, arrowRight, arrowLeft) .then(t => { if (t) { arrowDiv.removeAttribute("style") } }) } const dotranslate = document.createElement('span') dotranslate.setAttribute("class", "sm-translate-tab") dotranslate.innerText = "찜하기" dotranslate.onclick = (e) => { activateTab(e) arrowDiv.setAttribute("style", "display: none") translatingPageNum = 0 makeReservation(linkList, arrowRight, arrowLeft) .then(t => { arrowDiv.removeAttribute("style") }) } smallTitle.appendChild(translating) smallTitle.appendChild(notranslate) smallTitle.appendChild(dotranslate) sidebarItem.appendChild(itemTitle) sidebarItem.appendChild(smallTitle) sidebarItem.appendChild(linkList) sidebarItem.appendChild(arrowDiv) rightSidebar.prepend(sidebarItem) translating.click() } // 사이드바 내용 채우기 function getTranslatePageFetch(urlList, element, arrowRight, arrowLeft, nonPage = false) { return new Promise(async (resolve, reject) => { if (typeof urlList === "string") { urlList = [urlList] } let articleMetaArray = []; let articleArray = []; for (let url of urlList) { await doFetch(url) .then(dom => { let articleList = dom.querySelector(".article-list").querySelectorAll(".vrow:not(.notice):not(.head)") articleList.forEach(a => { let article = new Object article.href = a.href.split("?")[0] let articleSplit = a.querySelector(".title").innerText.split(/(\[?.?번역요청.?\]|\(?.?번역요청.?\))/) article.title = articleSplit[2] || articleSplit[0] article.author = a.querySelector("span.vcol.col-author > span > span:nth-child(1)").innerText let articleSpan = document.createElement('span') articleSpan.setAttribute("class", "leaf-info float-right") articleSpan.style.margin = 'auto 0 auto 10px' articleSpan.innerText = article.author let articleA = document.createElement('a') articleA.setAttribute("href", article.href) articleA.setAttribute("target", "_blank") articleA.style.padding = '.15rem 0 .15rem 0' articleA.appendChild(articleSpan) articleA.append(article.title) articleArray.push(articleA) }) }) } if (nonPage) { articleMetaArray = [articleArray] if (articleArray.length > 10) { contentStretch() } } else { articleMetaArray = articleArray.division(10) } arrowRight.onclick = (e) => { translatingPage(element, articleMetaArray, 1) } arrowLeft.onclick = (e) => { translatingPage(element, articleMetaArray, -1) } translatingPage(element, articleMetaArray, 0) if (articleMetaArray[0].length > 0) { resolve(true) } else { resolve(false) } }) } // 본문 늘리기 function contentStretch() { const contentWrapper = document.querySelector(".root-container > .content-wrapper") contentWrapper.setAttribute("style", "padding-bottom:" + contentWrapper.offsetHeight / 2 + "px") document.querySelectorAll(".ad").forEach(ad => { ad.remove() }) } // 페이지 이동 let translatingPageNum = 0 function translatingPage(element, arr, delta) { let tmpPage = translatingPageNum + delta let pageContent = arr[tmpPage] if (arr[0].length > 0 && arr[tmpPage]) { translatingPageNum = tmpPage element.innerHTML = '' pageContent.forEach(content => { element.appendChild(content) }) } } // 번역찜 function makeReservation(element, arrowRight, arrowLeft) { return new Promise((resolve, reject) => { const inputGroup = document.createElement("div") inputGroup.setAttribute("class", "input-group input-group-sm mb-3") inputGroup.setAttribute("style", "border-radius: 5px;") const inputGroupPrepend = document.createElement("div") inputGroupPrepend.setAttribute("class", "input-group-prepend") const inputGroupButton = document.createElement("div") inputGroupButton.setAttribute("class", "btn btn-sm btn-arca") inputGroupButton.setAttribute("data-toggle", "dropdown") inputGroupButton.innerText = "플랫폼" inputGroupPrepend.appendChild(inputGroupButton) const inputGroupDropdown = document.createElement("div") inputGroupDropdown.setAttribute("class", "dropdown-menu") const inputCode = document.createElement("input") inputCode.setAttribute("class", "form-control") inputCode.setAttribute("placeholder", "플랫폼을 선택하세요") inputCode.setAttribute("type", "text") const dropdowns = ["dlsite", "hitomi", "e-hentai", "getchu", "fanza"] dropdowns.forEach(str => { const inputGroupDropdownMenu = document.createElement("div") inputGroupDropdownMenu.setAttribute("class", "dropdown-item") inputGroupDropdownMenu.setAttribute("style", "cursor:pointer") inputGroupDropdownMenu.onclick = function(e) { inputGroupButton.innerText = e.target.innerText switch (e.target.innerText) { case "dlsite": inputCode.setAttribute("placeholder", "작품 코드 또는 주소") break; case "hitomi": inputCode.setAttribute("placeholder", "자료 코드 또는 주소") break; case "e-hentai": inputCode.setAttribute("placeholder", "자료 주소") break; case "getchu": case "fanza": inputCode.setAttribute("placeholder", "작품 주소") break; } } inputGroupDropdownMenu.innerText = str inputGroupDropdown.appendChild(inputGroupDropdownMenu) }) inputGroupPrepend.appendChild(inputGroupDropdown) inputGroup.appendChild(inputGroupPrepend) inputGroup.appendChild(inputCode) const checkboxDiv = document.createElement("div") const checkboxValues = [ ["역자", "식자"], ["손번역", "기계번역", "이미지번역"], ["개인", "팀", "동료 모집"] ] easyCheckbox(checkboxDiv, checkboxValues, "sm-translation-reservation-checkbox") const inputDescription = document.createElement("textarea") inputDescription.setAttribute("class", "form-control") inputDescription.setAttribute("style", "border-color: #ccc;border-radius: 5px;margin-bottom:10px;resize:none;height:10rem") inputDescription.setAttribute("placeholder", "덧붙이는 글 (선택)") const submitButton = document.createElement("div") submitButton.setAttribute("class", "btn btn-sm btn-arca float-right") submitButton.setAttribute("style", "margin:5px") submitButton.innerText = "번역찜" submitButton.onclick = function(e) { const optionList = [] let optionStr document.querySelectorAll(".sm-translation-reservation-checkbox").forEach(chkb => { if (chkb.checked) { optionList.push(chkb.value) } }) if (optionList.length > 0) { optionStr = arr2str(optionList) } else { optionStr = "선택되지 않음" } doReservation(inputGroupButton.innerText, inputCode.value, element, arrowRight, arrowLeft) .then(async data => { const dataJSON = data[1] let title if (optionStr.includes("모집")) { title = `[번역찜] [모집] ${dataJSON.title[1]}` } else { title = `[번역찜] ${dataJSON.title[1]}` } if (title.length > 50) { title = title.substring(0, 49) + "…" } dataJSON.sm_translation_option = ["번역찜 옵션", optionStr] let content await makeTable(dataJSON, true).then(table => { content = (table.outerHTML + `


${inputDescription.value}

` + indexerHTML(data[2])) }) const editorButton = document.createElement("div") editorButton.setAttribute("class", "btn btn-sm btn-arca float-right") editorButton.setAttribute("style", "margin:5px") editorButton.innerText = "에디터에서 수정" editorButton.onclick = function(e) { localStorage.setItem('sm_translation_tmp_title', title); localStorage.setItem('sm_translation_tmp_content', content.replace(/\n/g, "
")); location.href = "/b/smpeople/write/" } const directButton = document.createElement("div") directButton.setAttribute("class", "btn btn-sm btn-arca float-right") directButton.setAttribute("style", "margin:5px") directButton.innerText = "번역찜 제출" directButton.onclick = function(e) { submitReservation(title, content.replace(/\n/g, "
")) } element.appendChild(directButton) element.appendChild(editorButton) if (data[0]) { alert("번통사고 예방을 위해 목록을 확인해 주십시오") } else { submitButton.remove() } }) } element.innerHTML = '' element.appendChild(inputGroup) element.appendChild(checkboxDiv) element.appendChild(inputDescription) element.appendChild(submitButton) }) } // 체크박스 만들기 function easyCheckbox(element, valMetaList, className) { valMetaList.forEach(valList => { let checkbox let checkboxLabel let pTag = document.createElement("p") valList.forEach(val => { checkbox = document.createElement("input") checkbox.setAttribute("class", className) checkbox.setAttribute("type", "checkbox") checkbox.setAttribute("value", val) checkboxLabel = document.createElement("label") checkboxLabel.setAttribute("style", "margin:auto 5px") checkboxLabel.appendChild(checkbox) checkboxLabel.append(` ${val}`) pTag.appendChild(checkboxLabel) }) element.appendChild(pTag) }) } // 색인 const srt = "Q4VR D8W2 9H5K 0PFK 3JM7 GX0M" function indexerHTML(str) { let a = `

${str} ${srt}

` return a } // 번역찜 하기 function doReservation(platform, code, element, arrowRight, arrowLeft) { return new Promise((resolve, reject) => { let ProcessedCode let parser ProcessedCode = code.split("?")[0] switch (platform) { case "dlsite": ProcessedCode = dlsiteAuto(code) parser = dlInfo break; case "hitomi": ProcessedCode = hitomiAuto(code) parser = hitomiInfo break; case "e-hentai": ProcessedCode = ehAuto(code) parser = ehInfo break; case "getchu": ProcessedCode = getchuAuto(code) parser = getchuInfo break; case "fanza": ProcessedCode = fanzaAuto(code) parser = fanzaInfo break; default: alert("플랫폼을 선택해 주십시오") break; } parser(ProcessedCode).then(obj => { let searchList = [ `https://arca.live/b/smpeople?target=content&keyword=${ProcessedCode} ${srt}`, `https://arca.live/b/smpeople?category=번역&target=all&keyword=${obj.title[1]}`, `https://arca.live/b/smpeople?category=실황&target=all&keyword=${obj.title[1]} 번역` ] let numCode = ProcessedCode.match(/[A-Z]{2}\d+/g) if (numCode && numCode.length === 1) { searchList.push(`https://arca.live/b/smpeople?target=all&keyword=${numCode[0].replace(/[A-Z]{2}(\d+)/g, "$1")} 번역`) } getTranslatePageFetch(searchList, element, arrowRight, arrowLeft, true) .then(t => { resolve([t, obj, ProcessedCode]) }) }) }) } // 번역찜 글 제출 function submitReservation(title, content) { const url = "https://arca.live/b/smpeople/write" getWriteToken() .then(tokenList => { fetch(url, { method: 'POST', body: new URLSearchParams({ '_csrf': tokenList[0], 'token': tokenList[1], 'contentType': 'html', 'category': '실황', 'title': String(title), 'content': String(content) }) }) .then(response => { if (response.ok) { location.href = response.url } else { console.log(response) alert("번역찜 글이 제출 과정에서 오류가 발생했습니다.") } }) }) } // 에디터 자동 내용 채우기 async function autoWriteEditor() { if (location.href.includes("/write")) { const title = localStorage.getItem('sm_translation_tmp_title'); const content = localStorage.getItem('sm_translation_tmp_content'); if (title && content) { localStorage.removeItem('sm_translation_tmp_title') localStorage.removeItem('sm_translation_tmp_content') let titleBox = document.querySelector("#inputTitle") let editorBox = document.querySelector('.write-body .fr-element') while (titleBox === null || editorBox === null) { await sleep(10) titleBox = document.querySelector("#inputTitle") editorBox = document.querySelector('.write-body .fr-element') } titleBox.value = title editorBox.innerHTML = content document.querySelector("#category-실황").click() } } } // 글쓰기 토큰 function getWriteToken() { return new Promise((resolve, reject) => { if (location.href.includes("/write") || location.href.includes("/edit")) { const csrf = document.querySelector("input[name=_csrf]") const token = document.querySelector("input[name=token]") if (csrf && token) { resolve([csrf.value, token.value]) } } else { let url = "https://arca.live/b/smpeople/write" doFetch(url) .then(html => { const csrf = html.querySelector("input[name=_csrf]") const token = html.querySelector("input[name=token]") if (csrf && token) { resolve([csrf.value, token.value]) } }) } }) } // 이미지(Blob) 업로드 함수 function UploadImageFile(Blob, pretoken = null, original = false) { return new Promise(async (resolve, reject) => { var xhr = new XMLHttpRequest(); var formData = new FormData(); if (!document.querySelector("#article_write_form > input[name=token]")) { await getWriteToken() .then(tokenList => { pretoken = tokenList[1] }) } formData.append('upload', Blob, Math.random().toString(36).substring(7) + '.jpg'); formData.append('token', pretoken || document.querySelector("#article_write_form > input[name=token]").value); formData.append('saveExif', original); formData.append('saveFilename', false); xhr.onload = function() { responseJSON = JSON.parse(xhr.responseText) if (responseJSON.uploaded === true) { resolve(responseJSON.url) } else { alert("이미지 업로드 실패 : " + xhr.responseText) console.error(xhr.responseText); } } xhr.open("POST", "https://arca.live/b/upload"); xhr.send(formData); }); } // 탭 전환 function activateTab(e) { const tab = document.querySelector(".sm-translate-tab-activated") tab.setAttribute("class", "sm-translate-tab") e.target.setAttribute("class", "sm-translate-tab-activated") } // 배열을 문자열로 정리 function arr2str(arr, opt = ", ") { let idx = arr.indexOf("") while (idx > -1) { arr.splice(idx, 1) idx = arr.indexOf("") } return String(arr).replace(/"/g, '').replace('[', "").replace(']', "").replace(/,/g, opt) } // 문자열 정리 function strTrim(str) { return str.replace(/\n/g, '').replace(/^\s+|\s+$/g, '').replace(/ +/g, " ").trim() } // JSON을 글 양식을 표로 async function makeTable(obj, getTable = false) { if (location.href.includes("/write") || location.href.includes("/edit") || getTable) { const table = document.createElement("table") table.setAttribute("style", "width:100%") const tbody = document.createElement("tbody") for (let [key, value] of Object.entries(obj)) { if (value[1]) { const tr = document.createElement("tr") const tdA = document.createElement("td") tdA.setAttribute("style", "width: 20%; text-align: center;") tdA.innerText = value[0] const tdB = document.createElement("td") tdB.setAttribute("style", "width: 80%; text-align: center;") tdB.innerHTML = value[1] tr.appendChild(tdA) tr.appendChild(tdB) tbody.appendChild(tr) } } table.appendChild(tbody) if (!getTable) { let editorBox = document.querySelector('.write-body .fr-element') while (editorBox === null) { await sleep(10) editorBox = document.querySelector('.write-body .fr-element') } editorBox.appendChild(table) } else { return table } } else { return obj } } // dlsite api사용 function dlInfo(code, getTable = false) { return new Promise((resolve, reject) => { const url = `https://www.dlsite.com/maniax/api/=/product.json?workno=${code}&locale=ko-K` doFetch(url, { method: 'GET', responseType: 'application/json' }) .then(result => { const dlJSON = JSON.parse(result)[0] if (dlJSON) { const dlProcessedJSON = Object() dlProcessedJSON.title = ["제목", dlJSON["work_name"]] doFetch(`https:${dlJSON["image_main"]["url"]}`, { method: "GET", responseType: "blob" }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { dlProcessedJSON.image = ["이미지", `이미지`] dlProcessedJSON.work_type = ["분류", `${dlJSON["work_type"]} / ${dlJSON["work_type_string"]}`] dlProcessedJSON.file_info = ["파일(용량)", `${dlJSON["file_type"]}(${formatBytes(dlJSON["contents_file_size"])})`] if (dlJSON["creaters"]) { dlProcessedJSON.creators = [] for (const [key, value] of Object.entries(dlJSON["creaters"])) { let tmpArr = [] value.forEach(name => { tmpArr.push(name.name) }) dlProcessedJSON.creators.push(key + ': ' + arr2str(tmpArr, "|")) } dlProcessedJSON.creators = ["만든이", arr2str(dlProcessedJSON.creators, " / ").replace(/\|/g, ", ")] } dlProcessedJSON.maker = ["서클", dlJSON["maker_name"]] if (dlJSON["author"]) { dlProcessedJSON.authors = [] for (const [key, value] of Object.entries(dlJSON["author"])) { dlProcessedJSON.authors.push(value.author_name) } dlProcessedJSON.authors = ["작가", arr2str(dlProcessedJSON.authors)] } resolve(dlProcessedJSON) if (dlJSON["genres"]) { dlProcessedJSON.genres = [] dlJSON["genres"].forEach(genre => { dlProcessedJSON.genres.push(genre["name"]) }) dlProcessedJSON.genres = ["태그", arr2str(dlProcessedJSON.genres)] } if (dlJSON["trials"]) { dlProcessedJSON.trial = ["체험판", `체험판 다운로드(${dlJSON["trials"][0]["file_size_unit"]})`] } dlProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(dlProcessedJSON, getTable)) }) }) } else { alert("유효하지 않은 코드입니다!") } }) }) } // dlsite 정보 채우기 function dlsiteAuto(code = null) { let result = code || window.prompt("dlsite 코드 또는 주소를 입력하세요") if (result && result.match(/[A-Z]{2}\d+/g)) { if (!code) { dlInfo(result.match(/[A-Z]{2}\d+/g)[0]) } else { return result.match(/[A-Z]{2}\d+/g)[0] } } else { alert("dlsite 코드 또는 주소를 읽을 수 없습니다!") } } // hitomi api사용 function hitomiInfo(code, getTable = false) { return new Promise(async (resolve, reject) => { const url = `https://hitomi.la/galleries/${code}.html` await doFetch(url) .then(async html => { const redirect = html.querySelector("body > a") await doFetch(redirect.href) .then(async html => { const hitomiProcessedJSON = Object() const infoBox = html.querySelector(".content") const author = html.querySelector(".gallery > h2 > ul") const authors = [] author.querySelectorAll("li").forEach(tag => { authors.push(tag.innerText) }) const imgUrl = infoBox.querySelector(".cover > a > picture > img").srcset.split(" 2x")[0] await doFetch(imgUrl, { method: 'GET', responseType: 'blob' }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { hitomiProcessedJSON.title = ["제목", infoBox.querySelector(".gallery > h1").innerText] hitomiProcessedJSON.image = ["표지", `표지`] hitomiProcessedJSON.author = ["작가", arr2str(authors)] infoBox.querySelector(".gallery-info > table > tbody").querySelectorAll("tr").forEach(tr => { let rtd1 = tr.querySelector("td:nth-child(1)") let rtd2 = tr.querySelector("td:nth-child(2)") if (rtd1 && rtd2) { let td1 = strTrim(rtd1.innerText) let td2 = strTrim(rtd2.innerText) switch (td1) { case "Group": hitomiProcessedJSON.group = ["그룹", td2] break; case "Type": hitomiProcessedJSON.type = ["분류", td2] break; case "Language": hitomiProcessedJSON.language = ["언어", td2] break; case "Series": hitomiProcessedJSON.series = ["시리즈", td2] break; case "Characters": let charList = [] tr.querySelectorAll("li").forEach(tag => { charList.push(strTrim(tag.innerText)) }) hitomiProcessedJSON.characters = ["캐릭터", arr2str(charList)] break; case "Tags": let tagList = [] tr.querySelectorAll("li").forEach(tag => { tagList.push(strTrim(tag.innerText)) }) hitomiProcessedJSON.tags = ["태그", arr2str(tagList)] break; } } }) hitomiProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(hitomiProcessedJSON, getTable)) }) }) }) }) }) } // hitomi 정보 채우기 function hitomiAuto(code = null) { let result = code || window.prompt("hitomi 코드 또는 주소를 입력하세요"); if (result && result.match(/\d+\.htm/)) { if (!code) { hitomiInfo(result.match(/\d+\.htm/)[0].replace(".htm", "")) } else { return result.match(/\d+\.htm/)[0].replace(".htm", "") } } else { if (result && result.match(/\d+/g) && result.match(/\d+/g).length === 1) { if (!code) { hitomiInfo(result.match(/\d+/g)[0]) } else { return result.match(/\d+/g)[0] } } else { alert("hitomi 코드 또는 주소를 읽을 수 없습니다!") } } } // hitomi 작품 전체 가져오기 async function hitomiGetImage(code) { function subdomain_from_url(url, base) { var retval = 'b'; if (base) { retval = base; } var number_of_frontends = 3; var b = 16; var r = /\/[0-9a-f]\/([0-9a-f]{2})\//; var m = r.exec(url); if (!m) { return 'a'; } var g = parseInt(m[1], b); if (!isNaN(g)) { var o = 0; if (g < 0x80) { o = 1; } if (g < 0x40) { o = 2; } retval = String.fromCharCode(97 + o) + retval; } return retval; } function url_from_url(url, base) { return url.replace(/\/\/..?\.hitomi\.la\//, '//' + subdomain_from_url(url, base) + '.hitomi.la/'); } function full_path_from_hash(hash) { if (hash.length < 3) { return hash; } return hash.replace(/^.*(..)(.)$/, '$2/$1/' + hash); } function url_from_hash(galleryid, image, dir, ext) { ext = ext || dir || image.name.split('.').pop(); dir = dir || 'images'; return 'https://a.hitomi.la/' + dir + '/' + full_path_from_hash(image.hash) + '.' + ext; } function url_from_url_from_hash(galleryid, image, dir, ext, base) { return url_from_url(url_from_hash(galleryid, image, dir, ext), base); } const ltnUrl = `https://ltn.hitomi.la/galleries/${code}.js` const reqReferer = `https://hitomi.la/reader/${code}.html` let editorBox editorBox = document.querySelector('.write-body .fr-element') while (editorBox === null) { await sleep(10) editorBox = document.querySelector('.write-body .fr-element') } await doFetch(ltnUrl, { method: 'GET', responseType: 'application/json' }) .then(async str => { const hitomiJSON = JSON.parse(str.replace("var galleryinfo = ", "")) let fileProgress = document.createElement("progress"); fileProgress.setAttribute("value", "0") fileProgress.setAttribute("max", "1") fileProgress.setAttribute("style", "width:100%;") fileProgress.setAttribute("id", "sm-translation-hitomiAuto-fileProgress") const row = document.querySelector("#article_write_form > div.row > div") row.appendChild(fileProgress) let index = 0 for (let dict of hitomiJSON.files) { let url = url_from_url_from_hash(code, dict) var tmpPromise = await doFetch(url, { method: "GET", responseType: 'blob', headers: { referer: reqReferer } }) .then(async res => { var tmpPromise = await UploadImageFile(res).then(url => { let imgTag = document.createElement("img") imgTag.setAttribute("src", url) editorBox.appendChild(imgTag) }) }) index += 1 fileProgress.value = index / hitomiJSON.files.length } fileProgress.remove() }) } // hitomi 전체 가져오기 function hitomiFetch() { let result = window.prompt("hitomi코드를 입력하세요"); if (result && result.match(/\d+\.htm/)) { hitomiGetImage(result.match(/\d+\.htm/)[0].replace(".htm", "")) } else { if (result && result.match(/\d+/g) && result.match(/\d+/g).length === 1) { hitomiGetImage(result.match(/\d+/g)[0]) } else { alert("hitomi 코드 또는 주소를 읽을 수 없습니다!") } } } // e-hentai 정보 가져오기 function ehInfo(url, getTable = false) { return new Promise((resolve, reject) => { const apiUrl = "https://api.e-hentai.org/api.php" const gidlist = url.match(/\d+\/\w+/)[0].split("/") doFetch(apiUrl, { method: "POST", responseType: 'json', data: JSON.stringify({ "method": "gdata", "gidlist": [gidlist], "namespace": 1 }) }) .then(json => { const ehJSON = json.gmetadata[0] const ehProcessedJSON = Object() doFetch(ehJSON.thumb.replace("_l", "_250"), { method: 'GET', responseType: 'blob' }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { ehProcessedJSON.title = ["제목", ehJSON.title] ehProcessedJSON.thumb = ["표지", `표지`] const ehTags = Object() ehTags.artist = [] ehTags.group = [] ehTags.character = [] ehTags.language = [] ehTags.male = [] ehTags.female = [] ehTags.tags = [] ehJSON.tags.forEach(tag => { let tagsl = tag.split(":") switch (tagsl[0]) { case "artist": ehTags.artist.push(tagsl[1]); break; case "group": ehTags.group.push(tagsl[1]); break; case "character": ehTags.character.push(tagsl[1]); break; case "parody": ehTags.character.push(tagsl[1]); break; case "language": ehTags.language.push(tagsl[1]); break; case "male": ehTags.male.push(tagsl[1] + " ♂"); break; case "female": ehTags.female.push(tagsl[1] + " ♀"); break; default: ehTags.tags.push(tagsl[1]); break; } }) ehProcessedJSON.author = ["작가", arr2str(ehTags.artist)] ehProcessedJSON.group = ["그룹", arr2str(ehTags.group)] ehProcessedJSON.language = ["언어", arr2str(ehTags.language)] ehProcessedJSON.character = ["캐릭터", arr2str(ehTags.character)] ehProcessedJSON.tags = ["태그", arr2str(ehTags.tags.concat(ehTags.female.concat(ehTags.male)))] ehProcessedJSON.filecount = ["분량", `${ehJSON.filecount}장(${formatBytes(ehJSON.filesize)})`] ehProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(ehProcessedJSON, getTable)) }) }) }) }) } // e-hentai 정보 채우기 function ehAuto(code = null) { let result = code || window.prompt("e-hentai 주소를 입력하세요"); if (result && result.includes("e-hentai")) { if (!code) { ehInfo(result) } else { return result } } } // getchu 파싱 function getchuInfo(url, getTable = false) { return new Promise((resolve, reject) => { doFetch(url) .then(html => { const getchuProcessedJSON = Object() const infoBox = html.querySelector("#soft_table") getchuProcessedJSON.title = ["제목", strTrim(infoBox.querySelector("#soft-title").innerText)] const infoTable = infoBox.querySelectorAll("tbody > tr:nth-child(2) > th > table > tbody > tr") doFetch(`http://www.getchu.com/brandnew${infoBox.querySelector(".highslide").href.split("brandnew")[1]}`, { method: "GET", responseType: "blob", headers: { referer: url } }) .then(async blob => { var tmpPromise = UploadImageFile(blob) .then(imgUrl => { getchuProcessedJSON.image = ["이미지", `이미지`] infoTable.forEach(tr => { let infoTd = tr.querySelectorAll("td") if (infoTd.length > 1) { let td1 = strTrim(infoTd[0].innerText) let td2 = strTrim(infoTd[1].innerText) switch (td1) { case "ブランド:": getchuProcessedJSON.brand = ["브랜드", td2]; break; case "定価:": //getchuProcessedJSON.price = ["정가", td2]; break; case "発売日:": getchuProcessedJSON.o_date = ["발매일", td2]; break; case "メディア:": getchuProcessedJSON.media = ["미디어", td2]; break; case "ジャンル:": getchuProcessedJSON.genre = ["장르", dictionary(td2)]; break; case "原画:": getchuProcessedJSON.c_artist = ["원화", td2]; break; case "シナリオ:": getchuProcessedJSON.scenario = ["시나리오", td2]; break; case "時間:": getchuProcessedJSON.playtime = ["시간", td2]; break; case "アーティスト:": getchuProcessedJSON.artist = ["아티스트", td2]; break; case "商品同梱特典:": getchuProcessedJSON.special = ["상품 동봉 특전", td2]; break; default: break; } } }) getchuProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(getchuProcessedJSON, getTable)) }) }) }) }) } // getchu 정보 채우기 function getchuAuto(code = null) { let result = code || window.prompt("getchu 주소를 입력하세요"); if (result) { if (!code) { getchuInfo(result) } else { return result } } } // fanza 파싱 function fanzaInfo(url, getTable = false) { return new Promise((resolve, reject) => { doFetch(url) .then(html => { const fanzaProcessedJSON = Object() if (url.includes("doujin")) { const infoBox = html.querySelector("div.l-areaMainColumn") doFetch(infoBox.querySelector("#fn-slides > li:nth-child(1) > a").href, { method: 'GET', responseType: 'blob' }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { fanzaProcessedJSON.title = ["제목", strTrim(infoBox.querySelector("h1.productTitle__txt").innerText)] fanzaProcessedJSON.image = ["이미지", `이미지`] infoBox.querySelectorAll(".productAttribute-listItem > span").forEach(item => { if (item.className.includes("productGenre")) { let td1 = "분류" let td2 = strTrim(item.innerText) switch (td2) { case "ボイス": fanzaProcessedJSON.type = [td1, "보이스"]; break; case "コミック": fanzaProcessedJSON.type = [td1, "만화"]; break; case "CG": fanzaProcessedJSON.type = [td1, "CG"]; break; case "ゲーム": fanzaProcessedJSON.type = [td1, "게임"]; break; case "コスプレ動画": fanzaProcessedJSON.type = [td1, "코스프레 동영상"] alert("실사 자료는 아카라이브 및 심야식당 규정에 어긋납니다.") throw new Error("규정 위반 자료") } } }) infoBox.querySelectorAll(".productInformation__item").forEach(child => { let td1 = child.querySelector(".informationList__ttl").innerText let td2 let tdd = child.querySelector(".informationList__txt") if (tdd) { td2 = tdd.innerText } switch (td1) { case "シリーズ": fanzaProcessedJSON.series = ["시리즈", td2] break; case "題材": fanzaProcessedJSON.sanction = ["제재", dictionary(td2)] break; case "音声本数": fanzaProcessedJSON.voice_count = ["음성 개수", td2] break; case "動画本数": fanzaProcessedJSON.video_count = ["영상 개수", td2] break; case "ジャンル": let tagList = [] infoBox.querySelectorAll(".genreTag__item").forEach(tag => { tagList.push(dictionary(tag.innerText)) }) fanzaProcessedJSON.tags = ["태그", arr2str(tagList)] break; case "ファイル容量": fanzaProcessedJSON.size = ["용량", td2] break; } }) let sample = infoBox.querySelectorAll("div.sampleButton__item") if (sample.length) { const sampleUrlList = [] sample.forEach(tag => { if (tag.querySelector(".sampleButton__btn") && tag.querySelector(".sampleButton__txt")) { sampleUrlList.push(`${tag.querySelector(".sampleButton__txt").innerText}`) } }) fanzaProcessedJSON.sample = ["샘플", arr2str(sampleUrlList)] } fanzaProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(fanzaProcessedJSON, getTable)) }) }) } if (url.includes("dlsoft")) { const infoBox = html.querySelector("#mu > div.page-detail") doFetch(infoBox.querySelector("div.slider-area > ul > li:nth-child(1) > img").src, { method: 'GET', responseType: 'blob' }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { fanzaProcessedJSON.title = ["제목", strTrim(infoBox.querySelector("#title").innerText)] fanzaProcessedJSON.image = ["이미지", `이미지`] if (infoBox.querySelector("tr.brand > td.content")) { fanzaProcessedJSON.brand = ["브랜드", infoBox.querySelector("tr.brand > td.content").innerText] } infoBox.querySelector("div.container02").querySelectorAll("tr").forEach(tr => { let rtd1 = tr.querySelector("td.type-left") let rtd2 = tr.querySelector("td.type-right") if (rtd1 && rtd2) { let td1 = strTrim(rtd1.innerText) let td2 = strTrim(rtd2.innerText) switch (td1) { case "シリーズ": fanzaProcessedJSON.sanction = ["시리즈", td2] break; case "ゲームジャンル": fanzaProcessedJSON.genre = ["게임 장르", dictionary(td2)] break; case "ボイス": switch (td2) { case "あり": case "有り": fanzaProcessedJSON.voice = ["음성", "있음"] break; case "なし": case "無し": fanzaProcessedJSON.voice = ["음성", "있음"] break; default: fanzaProcessedJSON.voice = ["음성", td2] break; } break; case "原画": fanzaProcessedJSON.c_artist = ["원화", td2] break; case "シナリオ": fanzaProcessedJSON.scenario = ["시나리오", td2] break; case "ジャンル": let tagList = [] tr.querySelectorAll("li").forEach(tag => { tagList.push(dictionary(tag.innerText)) }) fanzaProcessedJSON.tags = ["태그", arr2str(tagList)] break; } } }) let sampleBx = infoBox.querySelector(".bx-detail-dlsample") if (sampleBx) { let sample = sampleBx.querySelectorAll("li") const sampleUrlList = [] sample.forEach(tag => { if (tag.querySelector("a") && tag.querySelector(".download-txt")) { sampleUrlList.push(`${tag.querySelector(".download-txt").innerText}`) } }) fanzaProcessedJSON.sample = ["샘플", arr2str(sampleUrlList)] } fanzaProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(fanzaProcessedJSON, getTable)) }) }) } if (url.includes("book.")) { const infoBox = html.querySelector("div.m-boxDetailProduct") doFetch(infoBox.querySelector("span.m-boxDetailProduct__pack__item > a").href, { method: 'GET', responseType: 'blob' }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { fanzaProcessedJSON.title = ["제목", strTrim(infoBox.querySelector("#title").innerText)] fanzaProcessedJSON.image = ["이미지", `이미지`] let index = 0 for (let list of infoBox.querySelectorAll(".m-boxDetailProductInfoMainList__description__list")) { let tmpList = [] list.querySelectorAll("li").forEach(tag => { tmpList.push(strTrim(tag.innerText)) }) if (index === 0) { fanzaProcessedJSON.artist = ["작가", arr2str(tmpList)] } if (index === 1) { fanzaProcessedJSON.series = ["시리즈", arr2str(tmpList)] } index += 1 } fanzaProcessedJSON.summary = ["줄거리", infoBox.querySelector(".m-boxDetailProduct__info__story").innerText] fanzaProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(fanzaProcessedJSON, getTable)) }) }) } if (url.includes("anime")) { const infoBox = html.querySelector("div.page-detail") doFetch(infoBox.querySelector("#sample-video > a").href, { method: 'GET', responseType: 'blob' }) .then(blob => { UploadImageFile(blob) .then(imgsrc => { fanzaProcessedJSON.title = ["제목", strTrim(infoBox.querySelector("#title").innerText)] fanzaProcessedJSON.image = ["이미지", `이미지`] infoBox.querySelector("table.mg-b20").querySelectorAll("tr").forEach(tag => { let rtd1 = tag.querySelector("td:nth-child(1)") let rtd2 = tag.querySelector("td:nth-child(2)") if (rtd1 && rtd2) { let td1 = strTrim(rtd1.innerText) let td2 = strTrim(rtd2.innerText) switch (td1) { case "収録時間:": fanzaProcessedJSON.playtime = ["러닝타임", td2] break; case "シリーズ:": fanzaProcessedJSON.series = ["시리즈", td2] break; case "メーカー:": fanzaProcessedJSON.maker = ["제조사", td2] break; case "レーベル:": fanzaProcessedJSON.label = ["레이블", td2] break; case "ジャンル:": fanzaProcessedJSON.genre = ["장르", dictionary(td2)] break; default: break; } } }) const sampleVideo = infoBox.querySelector("#detail-sample-movie > div > a.d-btn") if (sampleVideo) { sampleVideo.getAttribute("onclick").match(/\/digital.*\//) fanzaProcessedJSON.sample = ["샘플", `샘플 영상 보기`] } fanzaProcessedJSON.url = ["출처", `보러가기`] resolve(makeTable(fanzaProcessedJSON, getTable)) }) }) } }) }) } // fanza 정보 채우기 function fanzaAuto(code = null) { let result = code || window.prompt("fanza 주소를 입력하세요"); if (result) { if (!code) { fanzaInfo(result) } else { return result } } } // pixiv 파싱 (미완성) function pixivInfo(url) { doFetch(url) .then(html => { console.log(html) }) } // blob을 이미지 객체로 const blobToImage = (blob) => { return new Promise(resolve => { const url = URL.createObjectURL(blob) let img = new Image() img.onload = () => { URL.revokeObjectURL(url) resolve(img) } img.src = url }) } // 미방짤 제너레이터 function deviantTaste(optList, custom) { return new Promise((resolve, reject) => { doFetch("https://p-ac.namu.la/20210402/312ddf2b6cd9d18672d8818b8545c4c6dde0e92b80da025f9b52eecae181455f.png?type=orig", { method: "GET", responseType: "blob" }) .then(blob => { blobToImage(blob) .then(image => { const canvas = document.createElement("canvas") canvas.setAttribute("id", "sm-translation-image-process-canvas") canvas.setAttribute("width", "1200") canvas.setAttribute("height", "800") const ctx = canvas.getContext("2d") ctx.drawImage(image, 0, 0) ctx.lineWidth = 5; ctx.strokeStyle = 'red'; optList.forEach(taste => { deviantTasteCanvasRect(ctx, taste) }) if (custom) { ctx.font = "28pt 나눔고딕 extraBold"; ctx.fillStyle = "white"; ctx.fillText(custom, 130, 708); ctx.lineWidth = 5; ctx.strokeStyle = 'red'; ctx.strokeRect(125, 670, 10 + ctx.measureText(custom).width, 53); ctx.lineWidth = 2.5; ctx.rotate(45 * Math.PI / 180); ctx.strokeStyle = 'white'; ctx.strokeRect(555, 412, 16.5, 16.5); } canvas.toBlob(function(blob) { UploadImageFile(blob) .then(url => { resolve(`미방짤 : ${arr2str(optList)}`) }) }, "image/png") }) }) }) } // 미방짤 모달 function deviantTasteMenu() { let smtModalStyle = document.createElement("style") smtModalStyle.append(` .smtmodal { display: none; position: fixed; z-index: 100; padding-top: 100px; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.4); } .smtmodal-content { position: relative; background-color: #fefefe; margin: auto; padding: 0; border: 1px solid #888; width: 40%; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); -webkit-animation-name: animatetop; -webkit-animation-duration: 0.4s; animation-name: animatetop; animation-duration: 0.4s } @-webkit-keyframes animatetop { from {top:-300px; opacity:0} to {top:0; opacity:1} } @keyframes animatetop { from {top:-300px; opacity:0} to {top:0; opacity:1} } .smtclose { color: white; float: right; font-size: 28px; font-weight: bold; } .smtclose:hover, .smtclose:focus { color: white; text-decoration: none; cursor: pointer; } .smtmodal-header { padding: 0 20px 45px 20px; background-color: lightgray; color: white; } .smtmodal-body { padding: 2px 30px; } .smtmodal-footer { padding: 0 0 45px 0; background-color: lightgray; color: white; } .sexy-color-fresh { background: #0fb8ad; background: -webkit-linear-gradient(135deg, #0fb8ad 0%, #1fc8db 51%, #2cb5e8 75%); background: -o-linear-gradient(135deg, #0fb8ad 0%, #1fc8db 51%, #2cb5e8 75%); background: -moz-linear-gradient(135deg, #0fb8ad 0%, #1fc8db 51%, #2cb5e8 75%); background: linear-gradientlinear-gradient(135deg, #0fb8ad 0%, #1fc8db 51%, #2cb5e8 75%); background: linear-gradient(135deg, #0fb8ad 0%, #1fc8db 51%, #2cb5e8 75%); } `) let smtModal = document.createElement("div") smtModal.setAttribute("id", "sm-taste-modal") smtModal.setAttribute("class", "smtmodal") smtModal.innerHTML = `
×

미방짤 메이커

` document.head.appendChild(smtModalStyle) document.body.appendChild(smtModal) let modal = document.getElementById("sm-taste-modal"); let span = document.getElementById("sm-taste-modal-close") let body = document.getElementById("sm-taste-modal-body"); let checkList = [ ["게이", "보추", "후타"], ["청아", "중노년여성"], ["스캇", "방뇨"], ["수간", "충간", "이종간"], ["고어", "료나", "보어(식인)"], ["보태", "임신", "출산"] ] const customTaste = document.createElement("input") customTaste.setAttribute("id", "sm-taste-modal-cutom-input") customTaste.setAttribute("class", "form-control form-control-sm float-right") const customTasteLabel = document.createElement("p") customTasteLabel.innerText = "또다른 취향 태그" const customTasteP = document.createElement("p") customTasteP.setAttribute("class", "clearfix") customTasteP.setAttribute("style", "margin-bottom:20px") customTasteP.appendChild(customTaste) const confirmBtn = document.createElement("div") confirmBtn.setAttribute("id", "sm-taste-modal-confirm-btn") confirmBtn.setAttribute("class", "btn btn-arca float-right") confirmBtn.setAttribute("style", "margin-bottom:20px") confirmBtn.innerText = "미방짤 만들기" confirmBtn.onclick = function(e) { const optionList = [] document.querySelectorAll(".sm-taste-modal-taste-checkbox").forEach(chkb => { if (chkb.checked) { optionList.push(chkb.value) } }) deviantTaste(optionList, customTaste.value) .then(img => { let editorBox = document.querySelector('.write-body .fr-element') editorBox.innerHTML = img + editorBox.innerHTML modal.style.display = "none" }) } easyCheckbox(body, checkList, "sm-taste-modal-taste-checkbox") body.appendChild(customTasteLabel) body.appendChild(customTasteP) body.appendChild(confirmBtn) span.onclick = function() { modal.style.display = "none"; } window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } } modal.style.display = "block"; } // 미방짤 표시 좌표 function deviantTasteCanvasRect(ctx, opt) { switch (opt) { case "게이": ctx.strokeRect(125, 355, 85, 50); break; case "보추": ctx.strokeRect(225, 355, 85, 50); break; case "후타": ctx.strokeRect(325, 355, 85, 50); break; case "청아": ctx.strokeRect(125, 407, 85, 50); break; case "중노년여성": ctx.strokeRect(225, 407, 332, 50); break; case "스캇": ctx.strokeRect(125, 460, 85, 50); break; case "방뇨": ctx.strokeRect(225, 460, 85, 50); break; case "수간": ctx.strokeRect(125, 513, 85, 50); break; case "충간": ctx.strokeRect(225, 513, 85, 50); break; case "이종간": ctx.strokeRect(325, 513, 120, 50); break; case "고어": ctx.strokeRect(125, 566, 85, 50); break; case "료나": ctx.strokeRect(225, 566, 85, 50); break; case "보어(식인)": ctx.strokeRect(325, 566, 183, 50); break; case "보태": ctx.strokeRect(125, 619, 85, 50); break; case "임신": ctx.strokeRect(225, 619, 85, 50); break; case "출산": ctx.strokeRect(325, 619, 85, 50); break; } } // 미방짤 init function deviantTasteMenuInit() { let modal = document.getElementById("sm-taste-modal"); if (modal === null && (location.href.includes("/write") || location.href.includes("/edit"))) { deviantTasteMenu() } else { if (modal !== null) { modal.style.display = "block" } } } // 파일 -> 어레이버퍼 function fileToArrayBuffer(myFile) { return new Promise((resolve, reject) => { var reader = new FileReader(); reader.readAsArrayBuffer(myFile); reader.onloadend = function(evt) { if (evt.target.readyState == FileReader.DONE) { var arrayBuffer = evt.target.result resolve(arrayBuffer) } reject(new Error("ArrayBuffer 변환 실패")); } }) } // 어레이버퍼 덧셈 -> Uint8Array function appendBufferToArray(buffer1, buffer2) { var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); tmp.set(new Uint8Array(buffer1), 0); tmp.set(new Uint8Array(buffer2), buffer1.byteLength); return tmp; }; // 이미지 첨부파일 제너레이터 function imagezipBind(image, file) { return new Promise((resolve, reject) => { fileToArrayBuffer(image) .then(imageBuffer => { fileToArrayBuffer(file) .then(fileBuffer => { let blob = new Blob([appendBufferToArray(imageBuffer, fileBuffer)]) UploadImageFile(blob, null, true) .then(url => { resolve(`이미지+파일`) }) }, "image/jpeg") }) }) } // 이미지 jpg 컨버터 function convert() { let c = document.createElement("canvas") let ctx = c.getContext("2d"); c.width = this.width; c.height = this.height; ctx.drawImage(this, 0, 0); c.toBlob(function(blob) { resolve(blob) }, "image/jpeg"); } // 이미지 첨부파일 모달 function imagezipMaker() { let smiModalStyle = document.createElement("style") smiModalStyle.append(` .smimodal { display: none; position: fixed; z-index: 100; padding-top: 100px; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.4); } .smimodal-content { position: relative; background-color: #fefefe; margin: auto; padding: 0; border: 1px solid #888; width: 40%; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); -webkit-animation-name: animatetop; -webkit-animation-duration: 0.4s; animation-name: animatetop; animation-duration: 0.4s } @-webkit-keyframes animatetop { from {top:-300px; opacity:0} to {top:0; opacity:1} } @keyframes animatetop { from {top:-300px; opacity:0} to {top:0; opacity:1} } .smiclose { color: white; float: right; font-size: 28px; font-weight: bold; } .smiclose:hover, .smiclose:focus { color: white; text-decoration: none; cursor: pointer; } .smimodal-header { padding: 0 20px 45px 20px; background-color: lightgray; color: white; } .smimodal-body { padding: 2px 30px; } .smimodal-footer { padding: 0 0 45px 0; background-color: lightgray; color: white; } .sexy-color-gold { background: linear-gradient(to right, gold 0%, goldenrod 100%); } `) let smiModal = document.createElement("div") smiModal.setAttribute("id", "sm-imagezip-modal") smiModal.setAttribute("class", "smimodal") smiModal.innerHTML = `
×

이미지+파일 바인더

` document.head.appendChild(smiModalStyle) document.body.appendChild(smiModal) let modal = document.getElementById("sm-imagezip-modal"); let span = document.getElementById("sm-imagezip-modal-close") let body = document.getElementById("sm-imagezip-modal-body"); const imageFileH5 = document.createElement("h5") imageFileH5.setAttribute("style", "margin-bottom:20px") imageFileH5.innerText = "이미지 선택" const imageFileInput = document.createElement("input") imageFileInput.setAttribute("id", "sm-imagezip-modal-file-input") imageFileInput.setAttribute("type", "file") imageFileInput.setAttribute("style", "margin-bottom:20px") const imageUrl = document.createElement("input") imageUrl.setAttribute("id", "sm-imagezip-modal-url-input") imageUrl.setAttribute("class", "form-control form-control-sm float-right") const imageUrlLabel = document.createElement("p") imageUrlLabel.innerText = "또는 링크로 이미지 가져오기" const imageUrlP = document.createElement("p") imageUrlP.setAttribute("class", "clearfix") imageUrlP.setAttribute("style", "margin-bottom:20px") imageUrlP.appendChild(imageUrl) const fileH5 = document.createElement("h5") fileH5.innerText = "파일 선택" fileH5.setAttribute("style", "margin-bottom:20px") const fileInput = document.createElement("input") fileInput.setAttribute("id", "sm-imagezip-modal-file-input") fileInput.setAttribute("type", "file") fileInput.setAttribute("style", "margin-bottom:20px") const confirmBtn = document.createElement("div") confirmBtn.setAttribute("id", "sm-imagezip-modal-confirm-btn") confirmBtn.setAttribute("class", "btn btn-arca float-right") confirmBtn.setAttribute("style", "margin:60px 0 20px 0") confirmBtn.innerText = "이미지+파일 합치기" confirmBtn.onclick = function(e) { new Promise((resolve, reject) => { if (imageUrl.value) { doFetch(imageUrl.value, { method: "GET", responseType: "blob" }) .then(blob => { console.log(blob) resolve(imagezipBind(blob, fileInput.files[0])) }) } else { resolve(imagezipBind(imageFileInput.files[0], fileInput.files[0])) } }) .then(img => { let editorBox = document.querySelector('.write-body .fr-element') editorBox.innerHTML = img + editorBox.innerHTML modal.style.display = "none" }) } body.appendChild(imageFileH5) body.appendChild(imageFileInput) body.appendChild(imageUrlLabel) body.appendChild(imageUrlP) body.appendChild(fileH5) body.appendChild(fileInput) body.appendChild(confirmBtn) span.onclick = function() { modal.style.display = "none"; } window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } } modal.style.display = "block"; } // 이미지 첨부파일 init function imagezipMakerInit() { let modal = document.getElementById("sm-imagezip-modal"); if (modal === null && (location.href.includes("/write") || location.href.includes("/edit"))) { imagezipMaker() } else { if (modal !== null) { modal.style.display = "block" } } } // 글 댓글 수 찾기 함수 function userInfoParse(url) { return new Promise((resolve, reject) => { doFetch(url, { method: 'GET', responseType: 'document' }, true) .then(html => { const infoBox = html.querySelector(".card-block") let filtered = Array.from(infoBox.children).filter(function(value, index, arr) { if (value.localName === "div") { return value } }) const point = filtered.findIndex(function(e) { if (e.className === "clearfix") { return true } }) const articleNum = point const replyNum = filtered.length - point - 1 resolve(String(articleNum) + " / " + String(replyNum)) }) .catch(e => { resolve("삭제됨") }) }) } // 글 댓글 수 표시 함수 function userInfoView() { const smtTooltipStyle = document.createElement("style") smtTooltipStyle.append(` .smttooltip { position: relative; display: inline-block; } .smttooltip .smttooltiptext { visibility: hidden; width: 60px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 50; bottom: 125%; left: 50%; margin-left: -30px; opacity: 0; transition: opacity 0.3s; } .smttooltip .smttooltiptext::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #555 transparent transparent transparent; } .smttooltip:hover .smttooltiptext { visibility: visible; opacity: 1; }`) document.head.appendChild(smtTooltipStyle) document.querySelectorAll("div.list-area").forEach(tag => { tag.setAttribute("style", "overflow:visible") }) document.querySelectorAll("div.info-row").forEach(tag => { tag.setAttribute("style", "overflow:visible") }) document.querySelector(".article-wrapper").querySelectorAll("span.user-info").forEach(tag => { const smtTooltip = document.createElement("span") smtTooltip.setAttribute("class", "smttooltiptext") smtTooltip.innerText = " " tag.querySelector("a").classList.add("smttooltip") tag.querySelector("a").appendChild(smtTooltip) tag.querySelector("a").onmouseover = function(e) { if (e.target.href) { userInfoParse(e.target.href) .then(str => { smtTooltip.innerText = str }) } } }) } // 글 댓글 수 표시 init function userInfoViewInit() { if (location.href.match(/\/b\/.*\/\d+/) && !location.href.includes("/edit") && window.innerWidth > 580) { userInfoView() } } // 클래스 변경 감지 function addClassNameListener(element, callback) { var lastClassName = element.className; window.setInterval(function() { var className = element.className; if (className !== lastClassName) { callback(); lastClassName = className; } }, 10); } // 아카콘 링크하기 function arcaconLink() { const arcaconSrc = new Set() let arcaconId document.querySelector(".emoticons").querySelectorAll("img").forEach(tag => { arcaconSrc.add(tag.src) if (tag.getAttribute("data-emoticonid")) { arcaconId = tag.getAttribute("data-emoticonid") } }) const editorBox = document.querySelector("#article_write_form > div.write-body > div > div.fr-wrapper > div") editorBox.querySelectorAll("img").forEach(tag => { if (arcaconSrc.has(tag.src) && arcaconId !== "0" && !tag.closest("a")) { const cloneImg = tag.cloneNode(true) const aTag = document.createElement("a") aTag.appendChild(cloneImg) aTag.href = `https://arca.live/e/${arcaconId}` tag.replaceWith(aTag) } }) } // 아카콘 링크된 주소 글에서 바꾸기 async function arcaconLinkInArticle() { await sleep(1000) document.querySelector(".article-content").childNodes.forEach(tag => { const ta = tag.querySelectorAll("a").forEach(pta => { if (pta) { const taa = pta.querySelector("a") if (taa) { taa.href = pta.href } } }) }) } // 아카콘 링크하기 init async function arcaconLinkInit() { if (location.href.includes("/write") || location.href.includes("/edit")) { let arcaconButton = document.querySelector(".btn-namlacon") while (arcaconButton === null) { await sleep(10) arcaconButton = document.querySelector(".btn-namlacon") } arcaconButton.onclick = function(e) { let arcaconPopup = document.querySelector(".write-body").querySelector(".fr-popup") addClassNameListener(arcaconPopup, arcaconLink) } } else if (location.href.match(/b\/.*?\/\d+/)) { arcaconLinkInArticle() } } // 클립보드 base64 async function clipboardPasteToBase64() { let editorBox = document.querySelector('.write-body .fr-element') while (editorBox === null) { await sleep(10) editorBox = document.querySelector('.write-body .fr-element') } editorBox.addEventListener('paste', (event) => { const expression = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; const regex = new RegExp(expression); let paste = (event.clipboardData || window.clipboardData).getData('text'); let matched = false const matchUrl = [ "mega.nz", "drive.google.com", "photos.google.com", "ecchi.iwara.tv" ] matchUrl.forEach(url => { if(paste.includes(url)) { matched = true } }) if (matched && paste.match(regex)) { paste = btoa(paste) const selection = window.getSelection(); if (!selection.rangeCount) return false; selection.deleteFromDocument(); selection.getRangeAt(0).insertNode(document.createTextNode(paste)); event.preventDefault(); } }); } // 클립보드 base64 init function clipboardPasteToBase64Init() { if (location.href.includes("/write") || location.href.includes("/edit")) { clipboardPasteToBase64() } } // 메뉴 커맨드 function registerMenuCommand() { GM_registerMenuCommand("< 미방짤 만들기 >", deviantTasteMenuInit, "T"); GM_registerMenuCommand("< 이미지+파일 합치기 >", imagezipMakerInit, "Z"); GM_registerMenuCommand("DLsite 정보 채우기", dlsiteAuto, "D"); GM_registerMenuCommand("hitomi 정보 채우기", hitomiAuto, "H"); GM_registerMenuCommand("hitomi 전체 가져오기", hitomiFetch, "I"); GM_registerMenuCommand("e-hentai 정보 채우기", ehAuto, "E"); GM_registerMenuCommand("getchu 정보 채우기", getchuAuto, "G"); GM_registerMenuCommand("fanza 정보 채우기", fanzaAuto, "F"); }; // 기다리기 함수 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 사전 from https://arca.live/b/smpeople/30169768, 일부 수정 및 추가함 function dictionary(str) { let trList = [] str.split(/[\s\t\r\n\v\f\/・]/).forEach(tok => { tok = strTrim(tok) switch (tok) { case "萌え": trList.push("모에") break; case "燃え": trList.push("열혈") break; case "感動": trList.push("감동") break; case "癒し": trList.push("힐링") break; case "鬱": trList.push("우울") break; case "淡白": case "あっさり": trList.push("담백") break; case "オールハッピー": trList.push("올 해피") break; case "着衣": trList.push("착의") break; case "チラリズム": trList.push("치라리즘") break; case "フェチ": trList.push("페티즘") break; case "女性視点": trList.push("여성시점") break; case "女主人公": trList.push("여주인공") break; case "男主人公": trList.push("남주인공") break; case "逆転無し": trList.push("역전 없음") break; case "マニアック": case "変態": trList.push("마니악/변태") break; case "おさわり": trList.push("만지기") break; case "きせかえ": trList.push("옷갈아입히기") break; case "脚": trList.push("다리") break; case "お尻": case "ヒップ": trList.push("엉덩이") break; case "おっぱい": trList.push("가슴") break; case "淫語": trList.push("음어") break; case "汁": case "液大量": trList.push("체액 대량") break; case "連続絶頂": trList.push("연속 절정") break; case "断面図": trList.push("단면도") break; case "ドット": case "ドット制作": trList.push("도트") break; case "アニメ": trList.push("애니메이션") break; case "総集編": trList.push("총집편") break; case "バイノーラル": case "ダミヘ": trList.push("바이노럴/더미헤드폰") break; case "催眠音声": trList.push("최면음성") break; case "アンソロジー": trList.push("앤솔로지") break; case "技術書": trList.push("기술서") break; case "ツクール": trList.push("알만툴") break; case "3D作品": trList.push("3D 작품") break; case "東方Project": trList.push("동방Project") break; case "ピアス": trList.push("피어스") break; case "装飾品": trList.push("장식품") break; case "首輪": trList.push("목줄") break; case "鎖": trList.push("사슬") break; case "拘束具": trList.push("구속구") break; case "ムチ": trList.push("채찍") break; case "縄": trList.push("밧줄") break; case "蝋燭": trList.push("촛농") break; case "薬物": trList.push("약물") break; case "ローション": trList.push("로션") break; case "おむつ": trList.push("기저귀") break; case "おもちゃ": trList.push("장난감") break; case "道具": trList.push("도구") break; case "異物": trList.push("이물질") break; case "メガネ": case "めがね": trList.push("안경") break; case "靴下": trList.push("양말") break; case "少女": trList.push("소녀") break; case "ロリ": trList.push("로리") break; case "ぷに": trList.push("뿌니") break; case "少年": trList.push("소년") break; case "ショタ": trList.push("쇼타") break; case "年上": trList.push("연상") break; case "妹": trList.push("여동생") break; case "母親": trList.push("모친") break; case "義妹": trList.push("의여동생") break; case "娘": trList.push("딸") break; case "義母": trList.push("의어머니") break; case "実姉": trList.push("친누나/친언니") break; case "義姉": trList.push("의누나/의언니") break; case "おやじ": trList.push("아버지") break; case "熟女": trList.push("숙녀") break; case "人妻": trList.push("유부녀") break; case "お姉さん": trList.push("누나/언니") break; case "未亡人": trList.push("미망인") break; case "既婚者": trList.push("기혼자") break; case "幼なじみ": trList.push("소꿉친구") break; case "双子": trList.push("쌍둥이") break; case "姉妹": trList.push("자매") break; case "保健医": trList.push("보건의") break; case "女医": trList.push("여의사") break; case "女教師": trList.push("여교사") break; case "教師": trList.push("교사") break; case "学生": trList.push("학생") break; case "同級生": trList.push("동급생") break; case "同僚": trList.push("동료") break; case "先輩": trList.push("선배") break; case "後輩": trList.push("후배") break; case "お嬢様": trList.push("아가씨") break; case "ギャル": trList.push("갸루") break; case "ビッチ": trList.push("빗치") break; case "天然": trList.push("천연") break; case "主従": trList.push("주종") break; case "主婦": trList.push("주부") break; case "女王様": trList.push("여왕님") break; case "お姫様": trList.push("공주님") break; case "エルフ": trList.push("엘프") break; case "妖精": trList.push("요정") break; case "天使": trList.push("천사") break; case "悪魔": trList.push("악마") break; case "変身ヒロイン": trList.push("변신 히로인") break; case "魔法少女": trList.push("마법 소녀") break; case "魔法使い": trList.push("마법사") break; case "魔女": trList.push("마녀") break; case "男の娘": trList.push("오토코노코") break; case "妖怪": trList.push("요괴") break; case "擬人化": trList.push("의인화") break; case "ヤンデレ": trList.push("얀데레") break; case "人外娘": trList.push("인외") break; case "モンスター娘": trList.push("몬스터 소녀") break; case "ロボット": trList.push("로봇") break; case "アンドロイド": trList.push("안드로이드") break; case "芸能人": trList.push("예능인") break; case "アイドル": trList.push("아이돌") break; case "モデル": trList.push("모델") break; case "警察": trList.push("경찰") break; case "刑事": trList.push("형사") break; case "ヤクザ": case "裏社会": trList.push("야쿠자") break; case "不良": trList.push("불량") break; case "ヤンキー": trList.push("일진") break; case "レスラー": trList.push("레슬러") break; case "格闘家": trList.push("격투가") break; case "幽霊": trList.push("유령") break; case "ゾンビ": trList.push("좀비") break; case "けもの": trList.push("짐승") break; case "獣化": trList.push("동물화") break; case "外国人": trList.push("외국인") break; case "体育会系": trList.push("체육") break; case "スポーツ選手": trList.push("스포츠 선수") break; case "ニューハーフ": trList.push("뉴하프") break; case "戦士": trList.push("전사") break; case "くノ一": trList.push("쿠노이치") break; case "サキュバス": case "淫魔": trList.push("서큐버스/음마") break; case "制服": case "제복": trList.push("교복") break; case "セーラー服": trList.push("세일러복") break; case "体操着": trList.push("체조복") break; case "水着": trList.push("수영복") break; case "メイド": trList.push("메이드") break; case "看護婦": case "ナース": trList.push("간호사") break; case "巫女": trList.push("무녀") break; case "軍服": trList.push("군복") break; case "下着": trList.push("속옷") break; case "パンツ": trList.push("팬티") break; case "ゴスロリ": trList.push("고스로리") break; case "コスプレ": trList.push("코스프레") break; case "ボンデージ": case "ボンテージ": trList.push("본디지") break; case "ブルマ": trList.push("브루마") break; case "ミニスカ": trList.push("미니 스커트") break; case "着物": case "和服": trList.push("기모노") break; case "エプロン": trList.push("앞치마") break; case "ラバー": trList.push("고무재질") break; case "レオタード": trList.push("레오타드") break; case "シスター": trList.push("수녀") break; case "ウェイトレス": trList.push("웨이트리스") break; case "バニーガール": case "버니걸": trList.push("바니걸") break; case "スパッツ": trList.push("스판츠") break; case "ニーソックス": trList.push("니삭스") break; case "ストッキング": trList.push("스타킹") break; case "スクール水着": trList.push("학교 수영복") break; case "スーツ": trList.push("정장") break; case "ガーター": trList.push("가터") break; case "女装": trList.push("여장") break; case "学校": case "学園": case "学園もの": trList.push("학원") break; case "オフィス": case "職場": trList.push("오피스/직장") break; case "ラブコメ": trList.push("러브 코메디") break; case "耳かき": trList.push("귀청소") break; case "屋外": trList.push("옥외") break; case "ギャグ": trList.push("개그") break; case "ラブラブ": case "あまあま": trList.push("러브러브/달콤달콤") break; case "退廃": case "背徳": case "インモラル": trList.push("퇴폐/배덕/인모럴") break; case "憑依": trList.push("빙의") break; case "石化": trList.push("석화") break; case "コメディ": trList.push("코메디") break; case "日常": case "生活": trList.push("일상/생활") break; case "時間停止": trList.push("시간 정지") break; case "ミリタリー": trList.push("밀리터리") break; case "スポーツ": trList.push("스포츠") break; case "格闘": trList.push("격투") break; case "ほのぼの": trList.push("푸근함") break; case "同棲": trList.push("동거(애인끼리)") break; case "恋人同士": trList.push("연인끼리") break; case "初体験": case "첫체험": trList.push("첫 체험") break; case "色仕掛け": trList.push("미인계") break; case "女体化": trList.push("여체화") break; case "性転換(TS)": trList.push("성전환(TS)") break; case "浮気": trList.push("바람") break; case "売春": case "援交": trList.push("매춘/원교") break; case "風俗": case "ソープ": trList.push("풍속/소프") break; case "シリアス": trList.push("시리어스") break; case "ファンタジー": trList.push("판타지") break; case "歴史": case "時代物": trList.push("역사/시대물") break; case "ホラー": trList.push("호러") break; case "キャットファイト": trList.push("캣 파이트") break; case "サスペンス": trList.push("서스펜스") break; case "バイオレンス": trList.push("바이올런스") break; case "ノンフィクション": case "体験談": trList.push("논픽션/체험담") break; case "オカルト": trList.push("오컬트") break; case "歳の差": trList.push("나이차") break; case "魔法": trList.push("마법") break; case "同居": trList.push("동거") break; case "純愛": trList.push("순애") break; case "戦場": trList.push("전장") break; case "おもらし": trList.push("실금") break; case "ハーレム": trList.push("할렘") break; case "寝取られ": trList.push("NTR") break; case "女子校生": trList.push("여고생") break; case "百合": trList.push("백합") break; case "ミステリー": trList.push("미스터리") break; case "丸呑み": trList.push("통째 삼키기") break; case "のぞき": trList.push("엿보기") break; case "電車": trList.push("전차") break; case "寝取り": case "네토리": trList.push("NTL") break; case "おねショタ": trList.push("오네쇼타") break; case "睡眠姦": trList.push("수면간") break; case "ツンデレ": trList.push("츤데레") break; case "アヘ顔": trList.push("아헤가오") break; case "ソフトエッチ": trList.push("소프트 엣찌") break; case "手コキ": trList.push("손으로") break; case "足コキ": trList.push("발로") break; case "ぶっかけ": trList.push("붓카케") break; case "顔射": trList.push("안면 사정") break; case "中出し": trList.push("질내 사정") break; case "妊娠": case "孕ませ": trList.push("임신") break; case "パイズリ": trList.push("파이즈리") break; case "レズ": case "女同士": trList.push("레즈/여자끼리") break; case "ゲイ": case "男同士": trList.push("게이/남자끼리") break; case "母乳": trList.push("모유") break; case "搾乳": trList.push("착유") break; case "出産": trList.push("출산") break; case "産卵": trList.push("산란") break; case "陵辱": trList.push("능욕") break; case "オナニー": trList.push("자위") break; case "オナサポ": trList.push("자위 서포트") break; case "緊縛": trList.push("묶기/긴박") break; case "フェラ": case "フェラチオ": trList.push("펠라치오") break; case "痴漢": trList.push("치한") break; case "調教": trList.push("조교") break; case "淫乱": trList.push("음란") break; case "露出": trList.push("노출") break; case "言葉責め": trList.push("언어고문/음어") break; case "青姦": trList.push("야외 플레이") break; case "拘束": trList.push("구속") break; case "奴隷": trList.push("노예") break; case "浣腸": trList.push("관장") break; case "羞恥": case "恥辱": case "辱め": trList.push("수치/치욕") break; case "監禁": trList.push("감금") break; case "焦らし": trList.push("애태우기") break; case "くすぐり": trList.push("간지럼") break; case "鬼畜": trList.push("귀축") break; case "ノーマルプレイ": trList.push("노멀 플레이") break; case "放置プレイ": trList.push("방치 플레이") break; case "複数プレイ": trList.push("복수 플레이") break; case "乱交": trList.push("난교") break; case "強制": case "無理矢理": trList.push("강제") break; case "レイプ": trList.push("레이프") break; case "輪姦": trList.push("윤간") break; case "和姦": trList.push("화간") break; case "近親相姦": trList.push("근친상간") break; case "逆レイプ": trList.push("역 레이프") break; case "盗撮": trList.push("도촬") break; case "男性受け": trList.push("수비남") break; case "催眠": trList.push("최면") break; case "放尿": case "おしっこ": trList.push("방뇨/오줌") break; case "アナル": trList.push("애널") break; case "スカトロ": trList.push("스캇물") break; case "尿道": trList.push("요도") break; case "触手": trList.push("촉수") break; case "獣姦": trList.push("수간") break; case "機械姦": trList.push("기계간") break; case "下克上": trList.push("하극상") break; case "モブ姦": trList.push("몹 캐릭 간") break; case "異種姦": trList.push("이종간") break; case "悪堕ち": trList.push("타락") break; case "洗脳": trList.push("세뇌") break; case "ごっくん": case "食ザー": trList.push("정액 삼킴") break; case "口内射精": trList.push("구내 사정") break; case "イラマチオ": trList.push("이라마치오") break; case "スパンキング": trList.push("스파킹") break; case "耳舐め": trList.push("귀햝기") break; case "潮吹き": trList.push("시오후키") break; case "ささやき": trList.push("속삭임") break; case "拡張": trList.push("확장") break; case "ボクっ娘": trList.push("보쿠코") break; case "ショートカット": trList.push("짧은 머리") break; case "ロングヘア": trList.push("긴 머리") break; case "金髪": trList.push("금발") break; case "黒髪": trList.push("흑발") break; case "ポニーテール": trList.push("포니테일") break; case "ツインテール": trList.push("트윈테일") break; case "ネコミミ": trList.push("고양이귀") break; case "獣耳": trList.push("짐승귀") break; case "長身": trList.push("장신") break; case "筋肉": trList.push("근육") break; case "巨乳": case "爆乳": trList.push("거유/폭유") break; case "貧乳": case "微乳": case "つるぺた": trList.push("빈유/미유") break; case "複乳": case "怪乳": case "超乳": trList.push("복유/괴유/초유") break; case "乳首": case "乳輪": trList.push("유두/유륜") break; case "ぼて腹": case "妊婦": trList.push("볼록 배/임산부") break; case "スレンダー": trList.push("슬렌더") break; case "ツルペタ": trList.push("평평한 가슴") break; case "パイパン": trList.push("음모 없음") break; case "陰毛": trList.push("음모") break; case "腋毛": trList.push("겨드랑이 털") break; case "フタナリ": case "ふたなり": trList.push("후타나리") break; case "巨根": trList.push("거근") break; case "童貞": trList.push("동정") break; case "処女": trList.push("처녀") break; case "巨大化": trList.push("거대화") break; case "方言": trList.push("사투리") break; case "無表情": trList.push("무표정") break; case "褐色": case "日焼け": trList.push("갈색 피부") break; case "包茎": trList.push("포경") break; case "ムチムチ": trList.push("쭉쭉빵빵") break; case "太め": trList.push("통통한") break; case "デブ": trList.push("뚱뚱한") break; case "蟲姦": trList.push("충간") break; case "腹パン": trList.push("배빵") break; case "猟奇": trList.push("엽기") break; case "人体改造": trList.push("인체 개조") break; case "拷問": trList.push("고문") break; case "フィストファック": trList.push("Fist fuck") break; case "ニプルファック": trList.push("Nipple fuck") break; case "血液": case "流血": case "スプラッター": trList.push("혈액/유혈") break; case "狂気": trList.push("광기") break; case "リョナ": trList.push("료나") break; case "料理": case "グルメ": trList.push("요리/미식가") break; case "評論": trList.push("평론") break; case "シリーズもの": trList.push("시리즈물") break; case "遠距離恋愛": trList.push("원거리 연애") break; case "家族": trList.push("가족") break; case "ギャンブル": trList.push("도박") break; case "劇画": trList.push("극화") break; case "耽美": trList.push("탐미") break; case "ティーンズラブ": trList.push("어린 사랑") break; case "伝奇": trList.push("전기") break; case "ハードボイルド": trList.push("비장한") break; case "パラレル": trList.push("평행세계") break; case "パンチラ": trList.push("팬티보임") break; case "ブラチラ": trList.push("브라보임") break; case "ボーイズラブ": trList.push("BL") break; case "恋愛": trList.push("연애") break; case "委員長": trList.push("위원장") break; case "叔父": trList.push("숙부") break; case "義父": trList.push("시아버지") break; case "ガテン系": trList.push("가텐계") break; case "サラリーマン": trList.push("직장인") break; case "爺": trList.push("할아버지") break; case "実妹": trList.push("친동생") break; case "秘書": trList.push("비서") break; case "ヤリチン": case "プレイボーイ": trList.push("야리칭/플레이 보이") break; case "インテリ": trList.push("지식인") break; case "おかっぱ": trList.push("단발") break; case "タトゥー": case "刺青": trList.push("타투/문신") break; case "ハード系": trList.push("하드계") break; case "痴女": trList.push("치녀") break; case "茶髪": trList.push("갈색 머리") break; case "ドジっ娘": trList.push("도짓코/덜렁이") break; case "ぽっちゃり": trList.push("풍만") break; case "三つ編み": trList.push("땋은 머리") break; case "ミニ系": trList.push("작은체형/어린") break; case "ガードル": trList.push("거들") break; case "カチューシャ": trList.push("머리띠") break; case "しっぽ": trList.push("꼬리") break; case "スタンガン": trList.push("전기 충격") break; case "スポユニ": trList.push("운동복/스포츠 유니폼") break; case "男装": trList.push("남장") break; case "チャイナ": trList.push("차이나/중국풍") break; case "道着": trList.push("도복") break; case "ドラッグ": trList.push("마약") break; case "バイブ": trList.push("바이브/진동") break; case "白衣": trList.push("백의") break; case "半ズボン": trList.push("반바지") break; case "ブレザー": trList.push("재킷/블레이저") break; case "ふんどし": trList.push("훈도시") break; case "包帯": trList.push("붕대") break; case "注射器": trList.push("주사기") break; case "リボン": trList.push("리본") break; case "ローター": trList.push("로터") break; case "ローレグ": trList.push("로우 레그 컷/속옷") break; case "ワイシャツ": trList.push("와이셔츠") break; case "乙女受け": trList.push("처녀역할") break; case "オヤジ受け": trList.push("아버지역할") break; case "俺様攻め": trList.push("오레사마역할") break; case "クール受け": case "クール攻め": trList.push("쿨 플레이") break; case "クンニ": trList.push("쿤닐링구스/애무") break; case "健気受け": trList.push("씩씩한") break; case "誘い受け": trList.push("권유받은") break; case "強気受け": trList.push("강하게 받는") break; case "ヘタレ攻め": trList.push("엉터리공격역할") break; case "やんちゃ受け": trList.push("응석받이") break; case "アクション": trList.push("액션") break; case "アドベンチャー": trList.push("어드벤처") break; case "クイズ": trList.push("퀴즈") break; case "シミュレーション": trList.push("시뮬레이션") break; case "シューティング": trList.push("슈팅") break; case "その他ゲーム": trList.push("기타 게임") break; case "タイピング": trList.push("타이핑") break; case "テーブルゲーム": trList.push("테이블 게임") break; case "デジタルノベル": trList.push("디지털 노벨") break; case "パズル": trList.push("퍼즐") break; case "ロールプレイング": trList.push("롤 플래잉") break; case "オリジナル": trList.push("오리지널") break; case "二次創作": trList.push("2차 창작") break; case "漫画": trList.push("만화") break; case "アニメ": trList.push("애니메이션") break; case "二次創作": trList.push("2차 창작") break; case "ゲーム系": trList.push("게임 계열") break; case "パロディ": trList.push("패러디") break; case "その他": trList.push("기타") break; case "成人向け": trList.push("성인용") break; case "アクセサリー": trList.push("악세서리") break; case "イラスト": trList.push("일러스트") break; case "CG集": trList.push("CG집") break; case "男無": trList.push("남자 없음") break; case "音声付き": trList.push("음성 첨부") break; case "女主人公のみ": trList.push("여주인공만") break; case "擬人化": trList.push("의인화") break; case "逆転無し": trList.push("역전없음") break; case "作家複数": trList.push("여러작가") break; case "残虐表現": trList.push("잔학 표현") break; case "新作": trList.push("신작") break; case "準新作": trList.push("준신작") break; case "旧作": trList.push("구작") break; case "女性視点": trList.push("여성시점") break; case "シリーズもの": trList.push("시리즈물") break; case "全年齢向け": trList.push("전연령용") break; case "断面図あり": trList.push("단면도 있음") break; case "デモ": trList.push("데모") break; case "体験版あり": trList.push("체험판 있음") break; case "動画": trList.push("동영상") break; case "アニメーション": trList.push("애니메이션") break; case "ノベル": trList.push("노벨") break; case "ベスト": trList.push("베스트") break; case "総集編": trList.push("총집편") break; case "男性向け": trList.push("남성용") break; case "女性向け": trList.push("여성용") break; case "DL版独占販売": trList.push("DL독점") break; case "FANZA専売": trList.push("FANZA독점") break; case "がんばろう同人!": trList.push("힘내자 동인!") break; case "ゲームCP": trList.push("게임CP") break; default: trList.push(tok) break; } }) return arr2str(Array.from(new Set(trList))) }