diff --git a/.eslintrc.js b/.eslintrc.js index a0c9d1d..610cd90 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,7 @@ module.exports = { "require-atomic-updates": "off", "no-unused-vars": "warn", "no-useless-escape": "warn", - "no-await-in-loop": "warn", + "no-await-in-loop": "off", "dot-location": ["warn", "property"], "semi": "warn", "no-restricted-globals": ["error", "status"], @@ -23,5 +23,8 @@ module.exports = { "arrays": "always-multiline", "objects": "always-multiline", }], + "no-constant-condition": ["warn", { + "checkLoops": false, + }], } } diff --git a/src/js/UI/linkDisplay.js b/src/js/UI/linkDisplay.js index ea1403d..3598828 100644 --- a/src/js/UI/linkDisplay.js +++ b/src/js/UI/linkDisplay.js @@ -26,9 +26,15 @@ let load = exports.load = () => { loadActions(actions.available(status.serverID, status.linkType, status.automaticDone)); shared.applyColors(); - $("#KG-linkdisplay").show(); + show(true); }; +/** + * @param {Boolean} instant + */ +let show = exports.show = (instant) => + instant ? $("#KG-linkdisplay").show() : $("#KG-linkdisplay").slideDown(); + let hide = exports.hide = () => $("#KG-linkdisplay").slideUp(); @@ -36,10 +42,12 @@ function setTitle(text) { $("#KG-linkdisplay .KG-dialog-title").text(text); } +// TODO convert to html table +// TODO add metadata display /** * @param {Episode[]} episodes */ -function loadLinks(episodes) { // TODO refactor this +function loadLinks(episodes) { let html = ""; let padLength = Math.max(2, page.episodeCount().toString().length); util.for(episodes, (i, /** @type {Episode} */ obj) => { diff --git a/src/js/actions/beta.js b/src/js/actions/beta.js index aa10b2b..7c30531 100644 --- a/src/js/actions/beta.js +++ b/src/js/actions/beta.js @@ -41,8 +41,6 @@ async function tryGetQuality(episode) { let parsedQualityPrefs = preferences.general.quality_order.replace(/\s/g, "").split(","); for (let i of parsedQualityPrefs) { if (qualityStrings[i]) { - // don't want to needlessly spam the servers - // eslint-disable-next-line no-await-in-loop if (await util.ajax.head(rawLink + qualityStrings[i]).status == HttpStatusCodes.OK) { episode.processedLink = rawLink + qualityStrings[i]; return; diff --git a/src/js/config/sites/kissanime.js b/src/js/config/sites/kissanime.js index 2a388a3..e535773 100644 --- a/src/js/config/sites/kissanime.js +++ b/src/js/config/sites/kissanime.js @@ -15,6 +15,7 @@ let servers = new Dictionary([ regex: /"https:\/\/www.novelplanet.me\/v\/.*?"/, name: "Nova", linkType: LinkTypes.EMBED, + customStep: "modalBegin", }), new Server("beta2", { diff --git a/src/js/steps/captchaModal.js b/src/js/steps/captchaModal.js new file mode 100644 index 0000000..5ba2cca --- /dev/null +++ b/src/js/steps/captchaModal.js @@ -0,0 +1,86 @@ +// needed for jsdoc +/* eslint-disable no-unused-vars */ +const Episode = require("../types/Episode"); +/* eslint-enable no-unused-vars */ + +const util = require("../util"), + statusManager = require("../statusManager"), + config = require("../config"), + linkDisplay = require("../UI/linkDisplay"), + captchaModal = require("../UI/captchaModal"), + Captcha = require("../types/Captcha"); + +const status = statusManager.get(), + site = config.sites.current(); + +exports.modalBegin = async () => { + linkDisplay.show(); + linkDisplay.showSpinner(); + let progress = 0; + let func = async ( /** @type {Episode} */ episode) => { + let html = await doCaptcha(`${episode.kissLink}&s=${status.serverID}`); + getLink(html, episode); + progress++; + setStatusText(`${progress}/${promises.length}`); + }; + let promises = []; + util.for(status.episodes, (i, /** @type {Episode} */ obj) => { + promises.push(func(obj)); + }); + setStatusText(`0/${promises.length}`); + await Promise.all(promises); + status.func = "defaultFinished"; + statusManager.save(); + linkDisplay.load(); +}; + +function setStatusText(str) { + linkDisplay.setSpinnerText(str); + captchaModal.setStatusText(str); +} + +/** + * Handles the entire captcha process + * @param {String} url + * @returns {String} html of or empty string if captcha failed + */ +async function doCaptcha(url) { + while (true) { + let html = (await util.ajax.get(url)).response; + let $form = $(html).find("form#formVerify1"); + if ($form.length == 0) { + return html; // no captcha! + } + let texts = []; + $form.find("span:lt(2)").each((_i, obj) => texts.push(obj.innerText.replace(/[ \n]*(\w)/, "$1"))); + let images = []; + $form.find("img").each((i, obj) => images.push(obj.src)); + let answerCap = (await captchaModal.queue(new Captcha(texts, images))).join(",") + ","; // trailing comma because... kissanime + let response = (await util.ajax.post("/Special/AreYouHuman2", util.urlEncode({ reUrl: url, answerCap }), { "content-type": "application/x-www-form-urlencoded" })).response; + if (response.includes("AreYouHuman2")) { + continue; // captcha failed, retry + } + return response; // captcha completed + } +} + +/** + * @param {String} html + * @param {Episode} episode + */ +function getLink(html, episode) { + let link = site.servers.get(status.serverID).findLink(html); + if (link) { + episode.grabbedLink = link; + } else { + episode.error = "error: server not available or captcha"; + } +} + +/** + * @param {String} html + * @returns {Boolean} + */ +function isCaptcha(html) { + return $(html).find("form#formVerify1").length > 0; +} diff --git a/src/js/steps/index.js b/src/js/steps/index.js index 41638eb..ab30b5c 100644 --- a/src/js/steps/index.js +++ b/src/js/steps/index.js @@ -1,6 +1,7 @@ //allows multiple different approaches to collecting links -module.exports = Object.assign( +module.exports = Object.assign({}, require("./default"), - require("./turbo") + require("./turbo"), + require("./captchaModal") ); diff --git a/src/js/util/ajax.js b/src/js/util/ajax.js index d3ad91a..563623f 100644 --- a/src/js/util/ajax.js +++ b/src/js/util/ajax.js @@ -1,3 +1,8 @@ +/** + * @typedef {Object} Response + * @property {String} response + */ + /** * Makes an HTTP request * @param {String} method @@ -5,7 +10,7 @@ * @param {Object} obj * @param {any} obj.data * @param {Object} obj.headers - * @returns {Promise} Response + * @returns {Promise} Response * @private */ function request(method, url, { data, headers } = {}) { @@ -25,7 +30,7 @@ function request(method, url, { data, headers } = {}) { * Makes a HTTP GET request * @param {String} url * @param {Object} headers - * @returns {Promise} Response + * @returns {Promise} Response */ exports.get = (url, headers) => { return request("GET", url, { headers }); @@ -36,7 +41,7 @@ exports.get = (url, headers) => { * @param {String} url * @param {Object} headers * @param {Object} data - * @returns {Promise} Response + * @returns {Promise} Response */ exports.post = (url, data, headers) => { return request("POST", url, { data, headers }); @@ -46,7 +51,7 @@ exports.post = (url, data, headers) => { * Makes a HTTP HEAD request * @param {String} url * @param {Object} headers - * @returns {Promise} Response + * @returns {Promise} Response */ exports.head = (url, headers) => { return request("HEAD", url, { headers }); diff --git a/src/js/util/index.js b/src/js/util/index.js index 1a59907..58e8367 100644 --- a/src/js/util/index.js +++ b/src/js/util/index.js @@ -80,3 +80,16 @@ exports.merge = (obj1, obj2) => */ exports.last = (arr) => arr[arr.length - 1]; + +/** + * form/urlencodes the given object + * @param {Object} obj + * @returns {String} + */ +exports.urlEncode = (obj) => { + let str = ""; + for (let i in obj) { + str += `${encodeURIComponent(i)}=${encodeURIComponent(obj[i])}&`; + } + return str.slice(0, -1); // remove trailing '&' +};