// ==UserScript== // @name X-links Extension - Mangadex // @namespace mycropen // @author mycropen // @version 1.6.4 // @description Linkify and format chapter links for Mangadex, Dynasty-Scans, comick.io and bato.to // @include http://boards.4chan.org/* // @include https://boards.4chan.org/* // @include http://boards.4channel.org/* // @include https://boards.4channel.org/* // @include http://8ch.net/* // @include https://8ch.net/* // @include https://archived.moe/* // @include https://boards.fireden.net/* // @include http://desuarchive.org/* // @include https://desuarchive.org/* // @include http://fgts.jp/* // @include https://fgts.jp/* // @include http://boards.38chan.net/* // @include http://forums.e-hentai.org/* // @include https://forums.e-hentai.org/* // @include https://meguca.org/* // @homepage https://dnsev-h.github.io/x-links/ // @supportURL https://github.com/mycropen/x-links/issues // @updateURL https://raw.githubusercontent.com/mycropen/x-links/stable/builds/x-links-ext-mangadex.meta.js // @downloadURL https://raw.githubusercontent.com/mycropen/x-links/stable/builds/x-links-ext-mangadex.user.js // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA4klEQVR4Ae2ZoQ7CMBRF+VIMBjGDwSAwmImZGcQUYoYPq32fAPK8LCSleZCmzb3JcUtzD+ndBDslHuVVQr0zJdCAQHoaQEggTQYj9C8ggRVCAqPBDfoUkMBq8HAs4J8vLZ2uEH/VSqC6QEZmMbg7ZgiWzu2wJQEJZGRmgwn+cNf9jxXcRn0BCZA/33VKb848OfbQioAEikqni+MMpRugdGADFQQkEL7rlN7c3QG+2EZgrPUEJPD7V+RgcHQcoGAXDQlIoLx0/kxKhwbahoAEPn5ZYwKU7ldAAvqLSQLNRlEU5Q1O5fOjZV4u4AAAAABJRU5ErkJggg== // @icon64 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAOVBMVEUBAAAAAADmce/ml+/mje/mku/mhO/mY+/mbO/mdu/me+/mie/mXu/mm+/mpe/mf+/mqu/maO/moe9+hYmYAAAAAXRSTlMAQObYZgAAAJRJREFUeF7t1zkOAzEMBEFRe9+2//9YtzOCIOR8oEoX7GCgZEtXigWtb8qBF36ywIgD8gHcyAIHZqgHbnxwwRCPH1igEvCRCwMmZMd+cKVAjEwY0RpvgDkKAe/feANmVJxQC8TjHRssqDBHI5CPt6FihR8zjicQaD6eFW8sMEcxEI99fEG2vFrgwY4scEI/0P8X0HVf06IrwbJZHiwAAAAASUVORK5CYII= // @grant none // @run-at document-start // ==/UserScript== (function () { "use strict"; /*#{begin_debug:timing=true}#*/ var xlinks_api = (function () { "use strict"; // Private var ready = (function () { var callbacks = [], check_interval = null, check_interval_time = 250; var callback_check = function () { if ( (document.readyState === "interactive" || document.readyState === "complete") && callbacks !== null ) { var cbs = callbacks, cb_count = cbs.length, i; callbacks = null; for (i = 0; i < cb_count; ++i) { cbs[i].call(null); } window.removeEventListener("load", callback_check, false); window.removeEventListener("DOMContentLoaded", callback_check, false); document.removeEventListener("readystatechange", callback_check, false); if (check_interval !== null) { clearInterval(check_interval); check_interval = null; } return true; } return false; }; window.addEventListener("load", callback_check, false); window.addEventListener("DOMContentLoaded", callback_check, false); document.addEventListener("readystatechange", callback_check, false); return function (cb) { if (callbacks === null) { cb.call(null); } else { callbacks.push(cb); if (check_interval === null && callback_check() !== true) { check_interval = setInterval(callback_check, check_interval_time); } } }; })(); var ttl_1_hour = 60 * 60 * 1000; var ttl_1_day = 24 * ttl_1_hour; var ttl_1_year = 365 * ttl_1_day; var cache_prefix = ""; var cache_storage = window.localStorage; var cache_set = function (key, data, ttl) { cache_storage.setItem(cache_prefix + "ext-" + key, JSON.stringify({ expires: Date.now() + ttl, data: data })); }; var cache_get = function (key) { var json = parse_json(cache_storage.getItem(cache_prefix + "ext-" + key), null); if ( json !== null && typeof(json) === "object" && Date.now() < json.expires && typeof(json.data) === "object" ) { return json.data; } cache_storage.removeItem(key); return null; }; var random_string_alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var random_string = function (count) { var alpha_len = random_string_alphabet.length, s = "", i; for (i = 0; i < count; ++i) { s += random_string_alphabet[Math.floor(Math.random() * alpha_len)]; } return s; }; var is_object = function (obj) { return (obj !== null && typeof(obj) === "object"); }; var get_regex_flags = function (regex) { var s = ""; if (regex.global) s += "g"; if (regex.ignoreCase) s += "i"; if (regex.multiline) s += "m"; return s; }; var create_temp_storage = function () { var data = {}; var fn = { length: 0, key: function (index) { return Object.keys(data)[index]; }, getItem: function (key) { if (Object.prototype.hasOwnProperty.call(data, key)) { return data[key]; } return null; }, setItem: function (key, value) { if (!Object.prototype.hasOwnProperty.call(data, key)) { ++fn.length; } data[key] = value; }, removeItem: function (key) { if (Object.prototype.hasOwnProperty.call(data, key)) { delete data[key]; --fn.length; } }, clear: function () { data = {}; fn.length = 0; } }; return fn; }; var set_shared_node = function (node) { var par = document.querySelector(".xl-extension-sharing-elements"), id = random_string(32); if (par === null) { par = document.createElement("div"); par.className = "xl-extension-sharing-elements"; par.style.setProperty("display", "none", "important"); document.body.appendChild(par); } try { node.setAttribute("data-xl-sharing-id", id); par.appendChild(node); } catch (e) { return null; } return id; }; var settings_descriptor_info_normalize = function (input) { var info = {}, opt, label, desc, a, i, ii, v; if (typeof(input.type) === "string") { info.type = input.type; } if (Array.isArray((a = input.options))) { info.options = []; for (i = 0, ii = a.length; i < ii; ++i) { v = a[i]; if ( Array.isArray(v) && v.length >= 2 && typeof((label = v[1])) === "string" ) { opt = [ v[0], v[1] ]; if (typeof((desc = v[2])) === "string") { opt.push(desc); } info.options.push(opt); } } } return info; }; var config = {}; var CommunicationChannel = function (name, key, is_extension, channel, callback) { var self = this; this.port = null; this.port_other = null; this.post = null; this.on_message = null; this.origin = null; this.name_key = null; this.is_extension = is_extension; this.name = name; this.key = key; this.callback = callback; if (channel === null) { this.name_key = name; if (key !== null) { this.name_key += "_"; this.name_key += key; } this.origin = window.location.protocol + "//" + window.location.host; this.post = this.post_window; this.on_message = function (event) { self.on_window_message(event); }; window.addEventListener("message", this.on_message, false); } else { this.port = channel.port1; this.port_other = channel.port2; this.post = this.post_channel; this.on_message = function (event) { self.on_port_message(event); }; this.port.addEventListener("message", this.on_message, false); this.port.start(); } }; CommunicationChannel.prototype.post_window = function (message, transfer) { var msg = { ext: this.is_extension, key: this.name_key, data: message }; try { window.postMessage(msg, this.origin, transfer); } catch (e) { // Tampermonkey bug try { unsafeWindow.postMessage(msg, this.origin, transfer); } catch (e2) {} } }; CommunicationChannel.prototype.post_channel = function (message, transfer) { this.port.postMessage(message, transfer); }; CommunicationChannel.prototype.post_null = function () { }; CommunicationChannel.prototype.on_window_message = function (event) { var data = event.data; if ( event.origin === this.origin && is_object(data) && data.ext === (!this.is_extension) && // jshint ignore:line data.key === this.name_key && is_object((data = data.data)) ) { this.callback(event, data, this); } }; CommunicationChannel.prototype.on_port_message = function (event) { var data = event.data; if (is_object(data)) { this.callback(event, data, this); } }; CommunicationChannel.prototype.close = function () { if (this.on_message !== null) { if (this.port === null) { window.removeEventListener("message", this.on_message, false); } else { this.port.removeEventListener("message", this.on_message, false); this.port.close(); this.port = null; } this.on_message = null; this.post = this.post_null; } }; var api = null; var API = function () { this.event = null; this.reply_callbacks = {}; this.init_state = 0; this.handlers = API.handlers_init; this.functions = {}; this.url_info_functions = {}; this.url_info_to_data_functions = {}; this.details_functions = {}; this.actions_functions = {}; var self = this; this.channel = new CommunicationChannel( "xlinks_broadcast", null, true, null, function (event, data, channel) { self.on_message(event, data, channel, {}); } ); }; API.prototype.on_message = function (event, data, channel, handlers) { var action = data.xlinks_action, action_is_null = (action === null), action_data, reply, fn, err; if ( (action_is_null || typeof(action) === "string") && is_object((action_data = data.data)) ) { reply = data.reply; if (typeof(reply) === "string") { if (Object.prototype.hasOwnProperty.call(this.reply_callbacks, reply)) { fn = this.reply_callbacks[reply]; delete this.reply_callbacks[reply]; this.event = event; fn.call(this, null, action_data); this.event = null; } else { err = "Cannot reply to extension"; } } else if (action_is_null) { err = "Missing extension action"; } else if (Object.prototype.hasOwnProperty.call(handlers, action)) { handlers[action].call(this, action_data, channel, data.id); } else { err = "Invalid extension call"; } if (err !== undefined && typeof((reply = data.id)) === "string") { this.send( channel, null, reply, { err: "Invalid extension call" } ); } } }; API.prototype.send = function (channel, action, reply_to, data, timeout_delay, on_reply) { var self = this, id = null, timeout = null, cb, i; if (on_reply !== undefined) { for (i = 0; i < 10; ++i) { id = random_string(32); if (!Object.prototype.hasOwnProperty.call(this.reply_callbacks)) break; } cb = function () { if (timeout !== null) { clearTimeout(timeout); timeout = null; } on_reply.apply(this, arguments); }; this.reply_callbacks[id] = cb; cb = null; if (timeout_delay >= 0) { timeout = setTimeout(function () { timeout = null; delete self.reply_callbacks[id]; on_reply.call(self, "Response timeout"); }, timeout_delay); } } channel.post({ xlinks_action: action, data: data, id: id, reply: reply_to }); }; API.prototype.reply_error = function (channel, reply_to, err) { channel.post({ xlinks_action: null, data: { err: err }, id: null, reply: reply_to }); }; API.prototype.post_message = function (msg) { try { window.postMessage(msg, this.origin); } catch (e) { // Tampermonkey bug try { unsafeWindow.postMessage(msg, this.origin); } catch (e2) { console.log("window.postMessage failed! Your userscript manager may need to be updated!"); console.log("window.postMessage exception:", e, e2); } } }; API.prototype.init = function (info, callback) { if (this.init_state !== 0) { if (typeof(callback) === "function") callback.call(null, this.init_state === 1 ? "Init active" : "Already started"); return; } this.init_state = 1; var self = this, de = document.documentElement, count = info.registrations, namespace = info.namespace || "", send_info = { namespace: namespace }, a, v, i; if (typeof((v = info.name)) === "string") send_info.name = v; if (typeof((v = info.author)) === "string") send_info.author = v; if (typeof((v = info.description)) === "string") send_info.description = v; if (Array.isArray((v = info.version))) { for (i = 0; i < v.length; ++i) { if (typeof(v[i]) !== "number") break; } if (i === v.length) send_info.version = v.slice(0); } if (typeof(count) !== "number" || count < 0) { count = 1; } send_info.registrations = count; send_info.main = (typeof(info.main) === "function") ? info.main.toString() : null; if (de) { a = de.getAttribute("data-xlinks-extensions-waiting"); a = (a ? (parseInt(a, 10) || 0) : 0) + count; de.setAttribute("data-xlinks-extensions-waiting", a); de = null; } ready(function () { self.send( self.channel, "init", null, send_info, 10000, function (err, data) { err = self.on_init(err, data, namespace); if (err === "Internal") { self.channel.close(); this.init_state = 3; } if (typeof(callback) === "function") callback.call(null, err); } ); }); }; API.prototype.on_init = function (err, data, namespace) { var self = this, api_key, ch, v; if (err === null) { if (!is_object(data)) { err = "Could not generate extension key"; } else if (typeof((err = data.err)) !== "string") { if (typeof((api_key = data.key)) !== "string") { err = "Could not generate extension key"; } else { // Valid err = null; if (typeof((v = data.cache_prefix)) === "string") { cache_prefix = v; } if (typeof((v = data.cache_mode)) === "string") { if (v === "session") { cache_storage = window.sessionStorage; } else if (v === "none") { cache_storage = create_temp_storage(); } } // New channel ch = (this.event.ports && this.event.ports.length === 1) ? { port1: this.event.ports[0], port2: null } : null; this.channel.close(); this.channel = new CommunicationChannel( namespace, api_key, true, ch, function (event, data, channel) { self.on_message(event, data, channel, API.handlers); } ); } } } this.init_state = (err === null) ? 2 : 0; return err; }; API.prototype.register = function (data, callback) { if (this.init_state !== 2) { if (typeof(callback) === "function") callback.call(null, "API not init'd", 0); return; } // Data var send_data = { settings: {}, request_apis: [], linkifiers: [], commands: [], create_url: null }; var request_apis_response = [], command_fns = [], array, entry, fn_map, a_data, a, i, ii, k, o, v; // Settings o = data.settings; if (is_object(o)) { for (k in o) { a = o[k]; if (Array.isArray(a)) { send_data.settings[k] = a_data = []; for (i = 0, ii = a.length; i < ii; ++i) { v = a[i]; if (Array.isArray(v) && typeof(v[0]) === "string") { entry = [ v[0] ]; if (v.length > 1) { entry.push( (v[1] === undefined ? null : v[1]), "" + (v[2] || ""), "" + (v[3] || "") ); if (v.length > 4 && is_object(v[4])) { entry.push(settings_descriptor_info_normalize(v[4])); } } a_data.push(entry); } } } } } // Request APIs array = data.request_apis; if (Array.isArray(array)) { for (i = 0, ii = array.length; i < ii; ++i) { a = array[i]; fn_map = {}; a_data = { group: "other", namespace: "other", type: "other", count: 1, concurrent: 1, delays: { okay: 200, error: 5000 }, functions: [] }; if (typeof((v = a.group)) === "string") a_data.group = v; if (typeof((v = a.namespace)) === "string") a_data.namespace = v; if (typeof((v = a.type)) === "string") a_data.type = v; if (typeof((v = a.count)) === "number") a_data.count = Math.max(1, v); if (typeof((v = a.concurrent)) === "number") a_data.concurrent = Math.max(1, v); if (typeof((v = a.delay_okay)) === "number") a_data.delay_okay = Math.max(0, v); if (typeof((v = a.delay_error)) === "number") a_data.delay_error = Math.max(0, v); if (is_object((o = a.functions))) { for (k in o) { v = o[k]; if (typeof(v) === "function") { a_data.functions.push(k); fn_map[k] = v; } } } request_apis_response.push({ functions: fn_map }); send_data.request_apis.push(a_data); } } // Linkifiers array = data.linkifiers; if (Array.isArray(array)) { for (i = 0, ii = array.length; i < ii; ++i) { a = array[i]; a_data = { regex: null, prefix_group: 0, prefix: "" }; v = a.regex; if (typeof(v) === "string") { a_data.regex = [ v ]; } else if (v instanceof RegExp) { a_data.regex = [ v.source, get_regex_flags(v) ]; } else if (Array.isArray(v)) { if (typeof(v[0]) === "string") { if (typeof(v[1]) === "string") { a_data.regex = [ v[0], v[1] ]; } else { a_data.regex = [ v[0] ]; } } } if (typeof((v = a.prefix_group)) === "number") a_data.prefix_group = v; if (typeof((v = a.prefix)) === "string") a_data.prefix = v; send_data.linkifiers.push(a_data); } } // URL info functions array = data.commands; if (Array.isArray(array)) { for (i = 0, ii = array.length; i < ii; ++i) { a = array[i]; a_data = { url_info: false, to_data: false, actions: false, details: false }; o = { url_info: null, to_data: null, actions: null, details: null }; if (typeof((v = a.url_info)) === "function") { a_data.url_info = true; o.url_info = v; } if (typeof((v = a.to_data)) === "function") { a_data.to_data = true; o.to_data = v; } if (typeof((v = a.actions)) === "function") { a_data.actions = true; o.actions = v; } if (typeof((v = a.details)) === "function") { a_data.details = true; o.details = v; } command_fns.push(o); send_data.commands.push(a_data); } } // URL create functions o = data.create_url; if (is_object(o)) { send_data.create_url = o; } // Send this.send( this.channel, "register", null, send_data, 10000, function (err, data) { var o; if (err !== null || (err = data.err) !== null) { if (typeof(callback) === "function") callback.call(null, err, 0); } else if (!is_object((o = data.response))) { if (typeof(callback) === "function") callback.call(null, "Invalid extension response", 0); } else { var okay = this.register_complete(o, request_apis_response, command_fns, send_data.settings); if (typeof(callback) === "function") callback.call(null, null, okay); } } ); }; API.prototype.register_complete = function (data, request_apis, command_fns, settings) { var reg_count = 0, setting_ns, errors, name, fn, e, o, i, ii, k, v; // Request APIs errors = []; i = 0; if (Array.isArray((o = data.request_apis))) { for (ii = o.length; i < ii; ++i) { e = o[i]; if (i >= request_apis.length) { errors.push("Invalid"); } else if (typeof(e) === "string") { errors.push(e); } else if (!is_object(e)) { errors.push("Invalid"); } else { ++reg_count; for (k in e) { if (Object.prototype.hasOwnProperty.call(e, k) && Object.prototype.hasOwnProperty.call(request_apis[i].functions, k)) { fn = request_apis[i].functions[k]; this.functions[e[k]] = fn; } } } } } for (ii = request_apis.length; i < ii; ++i) { errors.push("Invalid"); } // URL infos errors = []; i = 0; if (Array.isArray((o = data.commands))) { for (ii = o.length; i < ii; ++i) { e = o[i]; if (i >= command_fns.length) { errors.push("Invalid"); } else if (typeof(e) === "string") { errors.push(e); } else if (!is_object(e) || typeof((k = e.id)) !== "string") { errors.push("Invalid"); } else { ++reg_count; this.url_info_functions[k] = command_fns[i].url_info; this.url_info_to_data_functions[k] = command_fns[i].to_data; if (command_fns[i].actions !== null) this.actions_functions[k] = command_fns[i].actions; if (command_fns[i].details !== null) this.details_functions[k] = command_fns[i].details; } } } for (ii = command_fns.length; i < ii; ++i) { errors.push("Invalid"); } // Settings for (k in settings) { setting_ns = settings[k]; for (i = 0, ii = setting_ns.length; i < ii; ++i) { name = setting_ns[i][0]; if ( !is_object(data.settings) || !is_object((o = data.settings[k])) || (v = o[name]) === undefined ) { v = (setting_ns[i].length > 1) ? setting_ns[i][1] : false; } o = config[k]; if (o === undefined) config[k] = o = {}; o[name] = v; } } return reg_count; }; API.handlers_init = {}; API.handlers = { request_end: function (data) { var id = data.id; if (typeof(id) === "string") { // Remove request delete requests_active[id]; } }, api_function: function (data, channel, reply) { var self = this, req = null, state, id, args, fn, ret; if ( typeof((id = data.id)) !== "string" || !Array.isArray((args = data.args)) ) { // Error this.reply_error(channel, reply, "Invalid extension data"); return; } // Exists if (!Array.prototype.hasOwnProperty.call(this.functions, id)) { // Error this.reply_error(channel, reply, "Invalid extension function"); return; } fn = this.functions[id]; // State if (is_object((state = data.state))) { id = state.id; req = requests_active[id]; if (req === undefined) { requests_active[id] = req = new Request(); } load_request_state(req, state); } // Callback args = Array.prototype.slice.call(args); args.push(function () { // Err var i = 0, ii = arguments.length, arguments_copy = new Array(ii); for (; i < ii; ++i) arguments_copy[i] = arguments[i]; self.send( channel, null, reply, { err: null, args: arguments_copy } ); }); // Call ret = fn.apply(req, args); }, url_info: function (data, channel, reply) { var self = this, id, url, fn; if ( typeof((id = data.id)) !== "string" || typeof((url = data.url)) !== "string" ) { // Error this.reply_error(channel, reply, "Invalid extension data"); return; } // Exists if (!Array.prototype.hasOwnProperty.call(this.url_info_functions, id)) { // Error this.reply_error(channel, reply, "Invalid extension function"); return; } fn = this.url_info_functions[id]; // Call fn(url, function (err, data) { self.send( channel, null, reply, { err: err, data: data } ); }); }, url_info_to_data: function (data, channel, reply) { var self = this, id, url_info; if ( typeof((id = data.id)) !== "string" || !is_object((url_info = data.url)) ) { // Error this.reply_error(channel, reply, "Invalid extension data"); return; } // Exists if (!Array.prototype.hasOwnProperty.call(this.url_info_to_data_functions, id)) { // Error this.reply_error(channel, reply, "Invalid extension function"); return; } // Call this.url_info_to_data_functions[id](url_info, function (err, data) { self.send( channel, null, reply, { err: err, data: data } ); }); }, create_actions: function (data, channel, reply) { var self = this, id, fn_data, fn_info; if ( typeof((id = data.id)) !== "string" || !is_object((fn_data = data.data)) || !is_object((fn_info = data.info)) ) { // Error this.reply_error(channel, reply, "Invalid extension data"); return; } // Exists if (!Array.prototype.hasOwnProperty.call(this.actions_functions, id)) { // Error this.reply_error(channel, reply, "Invalid extension function"); return; } // Call this.actions_functions[id](fn_data, fn_info, function (err, data) { self.send( channel, null, reply, { err: err, data: data } ); }); }, create_details: function (data, channel, reply) { var self = this, id, fn_data, fn_info; if ( typeof((id = data.id)) !== "string" || !is_object((fn_data = data.data)) || !is_object((fn_info = data.info)) ) { // Error this.reply_error(channel, reply, "Invalid extension data"); return; } // Exists if (!Array.prototype.hasOwnProperty.call(this.details_functions, id)) { // Error this.reply_error(channel, reply, "Invalid extension function"); return; } // Call this.details_functions[id](fn_data, fn_info, function (err, data) { self.send( channel, null, reply, { err: err, data: set_shared_node(data) } ); }); }, }; var RequestErrorMode = { None: 0, NoCache: 1, Save: 2 }; var ImageFlags = { None: 0x0, NoLeech: 0x1 }; var requests_active = {}; var Request = function () { }; var load_request_state = function (request, state) { for (var k in state) { request[k] = state[k]; } }; // Public var init = function (info, callback) { if (api === null) api = new API(); api.init(info, callback); }; var register = function (data, callback) { if (api === null) { callback.call(null, "API not init'd", 0); return; } api.register(data, callback); }; var request = function (namespace, type, unique_id, info, callback) { if (api === null || api.init_state !== 2) { callback.call(null, "API not init'd", null); return; } api.send( api.channel, "request", null, { namespace: namespace, type: type, id: unique_id, info: info }, -1, function (err, data) { if (err !== null || (err = data.err) !== null) { data = null; } else if ((data = data.data) === null) { err = "Invalid extension data"; } callback.call(null, err, data); } ); }; var insert_styles = function (styles) { var head = document.head, n; if (head) { n = document.createElement("style"); n.textContent = styles; head.appendChild(n); } }; var parse_json = function (text, def) { try { return JSON.parse(text); } catch (e) {} return def; }; var parse_html = function (text, def) { try { return new DOMParser().parseFromString(text, "text/html"); } catch (e) {} return def; }; var parse_xml = function (text, def) { try { return new DOMParser().parseFromString(text, "text/xml"); } catch (e) {} return def; }; var get_domain = function (url) { var m = /^(?:[\w\-]+):\/*((?:[\w\-]+\.)*)([\w\-]+\.[\w\-]+)/i.exec(url); return (m === null) ? [ "", "" ] : [ m[1].toLowerCase(), m[2].toLowerCase() ]; }; var get_image = function (url, flags, callback) { if (api === null || api.init_state !== 2) { callback.call(null, "API not init'd", null); return; } // Send api.send( api.channel, "get_image", null, { url: url, flags: flags }, 10000, function (err, data) { if (err !== null) { data = null; } else if (!is_object(data)) { err = "Invalid data"; } else if (typeof((err = data.err)) !== "string" && typeof((data = data.url)) !== "string") { data = null; err = "Invalid data"; } callback.call(null, err, data); } ); }; // Exports return { RequestErrorMode: RequestErrorMode, ImageFlags: ImageFlags, init: init, config: config, register: register, request: request, get_image: get_image, insert_styles: insert_styles, parse_json: parse_json, parse_html: parse_html, parse_xml: parse_xml, get_domain: get_domain, random_string: random_string, is_object: is_object, ttl_1_hour: ttl_1_hour, ttl_1_day: ttl_1_day, ttl_1_year: ttl_1_year, cache_set: cache_set, cache_get: cache_get }; })(); var main = function main_fn(xlinks_api) { var $$ = function (selector, root) { return (root || document).querySelectorAll(selector); }; var $ = (function () { var d = document; var Module = function (selector, root) { return (root || d).querySelector(selector); }; Module.add = function (parent, child) { return parent.appendChild(child); }; Module.tnode = function (text) { return d.createTextNode(text); }; Module.node = function (tag, class_name, text) { var elem = d.createElement(tag); elem.className = class_name; if (text !== undefined) { elem.textContent = text; } return elem; }; Module.node_ns = function (namespace, tag, class_name) { var elem = d.createElementNS(namespace, tag); elem.setAttribute("class", class_name); return elem; }; Module.node_simple = function (tag) { return d.createElement(tag); }; return Module; })(); // utility var interpolate = function (str, params) { // evaluate template strings const names = Object.keys(params); const vals = Object.values(params); return new Function(...names, `return \`${str}\`;`)(...vals); } var lang_to_flag = { "ja": "lang_jp", "jp": "lang_jp", "ko": "lang_kr", "cn": "lang_cn", "zh": "lang_cn", "zh-hk": "lang_hk", "id": "lang_id", "th": "lang_th", }; var site_short = { "mangadex": "md", "dynasty": "ds", "bato": "bt", "comick": "ck", } var replace_icon = function (ch_id, site, icon_name, apply_style) { // url_info.icon was set to a placeholder if use_flags is set // this replaces it with the real icon (e.g. a flag) after the manga data was acquired // also applies a style to the icon if apply_style is true // site must be either "mangadex", "dynasty" or "bato" because it's directly used as the attribute name of xlinks_api.config var style_str = ""; if (apply_style) { switch (xlinks_api.config[site].tag_filter_style) { case "none": style_str = ""; break; case "rotate180": style_str = "transform: rotate(180deg)"; break; case "long_strip": style_str = "transform: scaleX(0.5)"; break; case "invert": style_str = "filter: invert(1)"; break; case "grayscale": style_str = "filter: grayscale(1)"; break; case "opacity50": style_str = "filter: opacity(0.5)"; break; case "drop_shadow": style_str = "filter: drop-shadow(0.0rem 0.0rem 0.15rem #FF0000)"; break; case "sepia": style_str = "filter: sepia(1)"; break; case "blur1.5": style_str = "filter: blur(1.5px)"; break; case "hue_rotate90": style_str = "filter: hue-rotate(90deg)"; break; case "hue_rotate180": style_str = "filter: hue-rotate(180deg)"; break; case "hue_rotate270": style_str = "filter: hue-rotate(270deg)"; break; case "custom": style_str = xlinks_api.config[site].tag_filter_style_custom.trim(); break; } } // console.log([ch_id, apply_style, xlinks_api.config[site].tag_filter_style, style_str]); nodes = $$("span.xl-site-tag-icon[data-xl-site-tag-icon=replaceme-"+CSS.escape(site_short[site])+"-"+CSS.escape(ch_id)+"]"); for (let i = 0; i < nodes.length; i++) { nodes[i].setAttribute("data-xl-site-tag-icon", icon_name); let node_style = nodes[i].getAttribute("style"); if (node_style === undefined || node_style === null) nodes[i].setAttribute("style", style_str) else nodes[i].setAttribute("style", [node_style.trim(";"), style_str].join(";")) } } // functions that interact with the API // Mangadex class MangadexDataAggregator { constructor(final_callback) { this.callback = final_callback; this.context = null; this.group_num = 0; this.author_num = 1; this.artist_num = 1; this.data = { groups: Array(), authors: Array(), author_ids: Array(), artist_ids: Array(), }; } add_author_id(id) { this.data.author_ids.push(id); } add_artist_id(id) { this.data.artist_ids.push(id); } add_data(category, data) { if (category == "group") this.data.groups.push(data); else if (category == "author") this.data.authors.push(data); else this.data[category] = data; this.validate(); } validate() { // call this.callback if this.data has all necessary categories if (this.data.chapter == undefined) return; if (this.data.manga == undefined) return; if (xlinks_api.config.mangadex.show_group && this.data.groups.length < this.group_num) return; if (xlinks_api.config.mangadex.show_author) { let has_authors = 0; for (let i = this.data.authors.length - 1; i >= 0; i--) { if (this.data.author_ids.indexOf(this.data.authors[i].id) != -1) has_authors++; } if (has_authors < this.author_num) return; } if (xlinks_api.config.mangadex.show_artist) { let has_artists = 0; for (let i = this.data.authors.length - 1; i >= 0; i--) { if (this.data.artist_ids.indexOf(this.data.authors[i].id) != -1) has_artists++; } if (has_artists < this.artist_num) return; } // console.log(["valid data", this.context, this.group_num, this.author_num, this.artist_num, this.data]); // make a usable object out of the categories var aggdata = {}; var template = ""; var data = this.data; data.final = { manga_lang: "", author: "", manga: null, volume: "", chapter: "", title: "", pages: "", group: "", }; // aggdata.title // "${manga_lang} ${author} ${manga} ${volume} ${chapter} ${title} ${pages} ${group}" if (xlinks_api.config.mangadex.show_orig_lang && !xlinks_api.config.mangadex.use_flags && data.manga.originalLanguage) template += "${manga_lang} "; if ((xlinks_api.config.mangadex.show_author || xlinks_api.config.mangadex.show_artist) && data.authors.length > 0 && this.author_num + this.artist_num > 0) template += "${author} "; if (data.manga.title || data.manga.altTitles) template += "${manga} "; if (xlinks_api.config.mangadex.show_volume && data.chapter.volume) template += "${volume} "; if (data.chapter.chapter) template += "${chapter} "; if (xlinks_api.config.mangadex.show_ch_title && data.chapter.title) template += "${title} "; if (xlinks_api.config.mangadex.show_pages && data.chapter.pages) template += "${pages} "; if (xlinks_api.config.mangadex.show_group && data.groups.length > 0) template += "${group}"; if (data.manga.originalLanguage) data.final.manga_lang = "["+data.manga.originalLanguage+"]"; if (data.authors.length > 0) { var author_names = Array(); for (let i = 0; i < data.authors.length; i++) { if (xlinks_api.config.mangadex.show_author && data.author_ids.indexOf(data.authors[i].id) != -1 && author_names.indexOf(data.authors[i].name) == -1) { author_names.push(data.authors[i].name); } if (xlinks_api.config.mangadex.show_artist && data.artist_ids.indexOf(data.authors[i].id) != -1 && author_names.indexOf(data.authors[i].name) == -1) { author_names.push(data.authors[i].name); } } data.final.author = "[" + author_names.join(", ") + "]"; } if (xlinks_api.config.mangadex.custom_title) { var title_order = xlinks_api.config.mangadex.title_search_order.replace(/\s+/g, "").split(","); var title_found = false; for (let i = 0; i < title_order.length; i++) { if (title_found) break; let lcode = title_order[i]; lcode = lcode.replace(/orig/i, data.manga.originalLanguage); if (data.manga.title[lcode] !== undefined) { data.final.manga = data.manga.title[lcode]; break; } else { // altTitles is an array of objects with each single entries for (var j = 0; j < data.manga.altTitles.length; j++) { if (data.manga.altTitles[j][lcode] !== undefined) { data.final.manga = data.manga.altTitles[j][lcode]; title_found = true; break; } } } } } if (!data.final.manga) { // take the default title let keys = Object.keys(data.manga.title); if (keys.length) data.final.manga = data.manga.title[keys[0]]; } if (data.chapter.volume) data.final.volume = "vol. " + data.chapter.volume; if (data.chapter.chapter) data.final.chapter = "ch. " + data.chapter.chapter; if (data.chapter.title) data.final.title = '- "' + data.chapter.title + '"'; if (data.chapter.pages) data.final.pages = "(" + data.chapter.pages + "p)"; if (xlinks_api.config.mangadex.show_group && data.groups.length > 0) { let combined_group_name = ""; for (let i = 0; i < data.groups.length; i++) { combined_group_name += data.groups[i].name + ', ' } combined_group_name = combined_group_name.replace(/, $/, ""); data.final.group = "[" + combined_group_name + "]"; if (combined_group_name == "null") console.log(["null group name:", data.groups]); } aggdata.title = interpolate(template, data.final); aggdata.title = aggdata.title.replace(/^\s+/, ""); aggdata.title = aggdata.title.replace(/\s$/, ""); aggdata.title = aggdata.title.replace(/\s+/g, " "); if (xlinks_api.config.mangadex.show_icon) { // modify the [MD] tag into an icon or flag // apply a style if the tag filter is tripped let icon_name = "site_MD"; let apply_style = false; if (xlinks_api.config.mangadex.show_orig_lang && xlinks_api.config.mangadex.use_flags && lang_to_flag[data.manga.originalLanguage] !== undefined) icon_name = lang_to_flag[data.manga.originalLanguage] // search for tags matching the tag filter if (xlinks_api.config.mangadex.tag_filter.trim() !== "") { // "A a, bB, C c c" -> ["a a", "bb", "c c c"] let tag_array = xlinks_api.config.mangadex.tag_filter.trim().replace(/,\s+/g, ",").toLowerCase().split(","); for (let i = 0; i < data.manga.tags.length; i++) { if (data.manga.tags[i].attributes.name.en !== undefined) { if (tag_array.indexOf(data.manga.tags[i].attributes.name.en.toLowerCase()) >= 0) { apply_style = true; break; } } } // console.log([aggdata.title, data.manga.tags, tag_array, apply_style]) } this.tag_filter_tripped = apply_style; replace_icon(this.context, "mangadex", icon_name, apply_style); } this.callback(null, aggdata); } } var md_aggregators = {}; var md_get_data = function (info, callback) { var data = xlinks_api.cache_get(info.id); callback(null, data); }; var md_set_data = function (data, info, callback) { var lifetime = 7 * xlinks_api.ttl_1_day; if (info.lifetime !== undefined) lifetime = info.lifetime; xlinks_api.cache_set(info.id, data, lifetime); callback(null); }; var md_generic_api_xhr = function (callback) { var info = this.infos[0]; // context should be "{type}_{id}" var ctx = null; if (info.context !== undefined) ctx = info.context; let req_url = "https://api.mangadex.org/" + info.type + "/" + info.id; if (info.includes !== undefined && info.includes.length > 0) { req_url += "?"; for (let i = 0; i < info.includes.length; i++) { req_url += "includes[]=" + info.includes[i] + "&"; } req_url = req_url.replace(/&$/, ""); } // console.log(["MD API call", info.type, info.id, info.includes, req_url]); callback(null, { method: "GET", url: req_url, headers: {"accept": "application/json"}, context: ctx, }); }; var md_generic_parse_response = function (xhr, callback) { // split xhr.context by '_' and forward to the actual parse_response function if (xhr.context) { var ctx = xhr.context.split("_"); if (ctx[0] == "chapter") md_chapter_parse_response(xhr, callback); else if (ctx[0] == "group") md_group_parse_response(xhr, callback); else if (ctx[0] == "manga") md_manga_parse_response(xhr, callback); else if (ctx[0] == "author") md_author_parse_response(xhr, callback); else callback("Invalid response context: " + xhr.context); } else callback("Invalid response: No context."); }; var md_chapter_setup_xhr = function (callback) { var info = this.infos[0]; var ctx = null; if (info.context !== undefined) ctx = info.context; callback(null, { method: "GET", url: "https://api.mangadex.org/chapter/" + info.id, headers: {"accept": "application/json"}, context: ctx, }); }; var md_chapter_parse_response = function (xhr, callback) { if (xhr.status !== 200) { callback("Invalid response."); return; } var jsdata = xlinks_api.parse_json(xhr.responseText, null); if (jsdata == null || jsdata.data == undefined || jsdata.data.attributes == undefined) { callback("Cannot parse response."); return; } var data = { id: jsdata.data.id || null, chapter: jsdata.data.attributes.chapter || null, volume: jsdata.data.attributes.volume || null, publishAt: jsdata.data.attributes.publishAt || null, pages: jsdata.data.attributes.pages || null, title: jsdata.data.attributes.title || null, }; if (jsdata.data.relationships !== undefined) data.relationships = jsdata.data.relationships; // console.log(["parse_ch_response", jsdata.data, data]); callback(null, [data]); }; var md_manga_setup_xhr = function (callback) { var info = this.infos[0]; var ctx = null; if (info.context !== undefined) ctx = info.context; callback(null, { method: "GET", url: "https://api.mangadex.org/manga/" + info.id, headers: {"accept": "application/json"}, context: ctx, }); }; var md_manga_parse_response = function (xhr, callback) { if (xhr.status !== 200) { callback("Invalid response."); return; } var ctx; if (xhr.context !== undefined) { ctx = xhr.context.split("_"); } else { callback("Invalid response context"); return; } var aggregator = md_aggregators[ctx[1]]; var jsdata = xlinks_api.parse_json(xhr.responseText, null); if (jsdata == null || jsdata.data == undefined || jsdata.data.attributes == undefined) { callback("Cannot parse response."); return; } var data = { id: jsdata.data.id || null, title: jsdata.data.attributes.title || null, altTitles: jsdata.data.attributes.altTitles || null, descrition: jsdata.data.attributes.descrition || null, originalLanguage: jsdata.data.attributes.originalLanguage || null, state: jsdata.data.attributes.state || null, status: jsdata.data.attributes.status || null, tags: jsdata.data.attributes.tags || null, relationships: jsdata.data.relationships || null, }; // if (xlinks_api.config.mangadex.show_author || xlinks_api.config.mangadex.show_artist) md_get_other_manga_data(ctx[1], data, aggregator); xlinks_api.cache_set("manga_" + jsdata.data.id, data, 7 * xlinks_api.ttl_1_day); aggregator.add_data("manga", data); callback(null, [data]); }; var md_get_other_manga_data = function (context, data, aggregator) { var has_authors = 0; var author_num = 0; var has_artists = 0; var artist_num = 0; for (let i = 0; i < data.relationships.length; i++) { if (data.relationships[i].type == "author") author_num++; if (data.relationships[i].type == "artist") artist_num++; } aggregator.author_num = author_num; aggregator.artist_num = artist_num; var requested_authors = Array(); var added_authors = Array(); for (let i = 0; i < data.relationships.length; i++) { switch (data.relationships[i].type) { case "author": { has_authors++; if (data.relationships[i].attributes !== undefined) { let authordata = { id: data.relationships[i].id || null, name: data.relationships[i].attributes.name || null, }; xlinks_api.cache_set("author_" + authordata.id, authordata, 7 * xlinks_api.ttl_1_day); aggregator.add_author_id(authordata.id); if (added_authors.indexOf(authordata.id) != -1) break; aggregator.add_data("author", authordata); } else { let author_url_info = { id: data.relationships[i].id, context: "author_" + context, type: "author", }; aggregator.add_author_id(author_url_info.id); if (added_authors.indexOf(author_url_info.id) != -1) break; let cached_authordata = xlinks_api.cache_get("author_" + author_url_info.id); if (cached_authordata !== null) { added_authors.push(cached_authordata.id); aggregator.add_data("author", cached_authordata); } else if (requested_authors.indexOf(author_url_info.id) == -1) { xlinks_api.request("mangadex", "generic", author_url_info.id, author_url_info, (err, data) => {}); requested_authors.push(author_url_info.id); } } break; } case "artist": { has_artists++; if (data.relationships[i].attributes !== undefined) { let authordata = { id: data.relationships[i].id || null, name: data.relationships[i].attributes.name || null, }; xlinks_api.cache_set("author_" + authordata.id, authordata, 7 * xlinks_api.ttl_1_day); aggregator.add_artist_id(authordata.id); if (added_authors.indexOf(authordata.id) != -1) break; aggregator.add_data("author", authordata); } else { let author_url_info = { id: data.relationships[i].id, context: "author_" + context, type: "author", }; aggregator.add_artist_id(author_url_info.id); if (added_authors.indexOf(author_url_info.id) != -1) break; let cached_authordata = xlinks_api.cache_get("author_" + author_url_info.id); if (cached_authordata !== null) { added_authors.push(cached_authordata.id); aggregator.add_data("author", cached_authordata); } else if (requested_authors.indexOf(author_url_info.id) == -1) { xlinks_api.request("mangadex", "generic", author_url_info.id, author_url_info, (err, data) => {}); requested_authors.push(author_url_info.id); } } break; } } } if (xlinks_api.config.mangadex.show_author && has_authors < author_num) { // let authordata = {id: "fake_author", name: ""}; // aggregator.add_author_id("fake_author"); // aggregator.add_data("author", authordata); aggregator.author_num = has_authors; aggregator.validate(); } if (xlinks_api.config.mangadex.show_artist && has_artists == artist_num) { // let artistdata = {id: "fake_artist", name: ""}; // aggregator.add_artist_id("fake_artist"); // aggregator.add_data("artist", artistdata); aggregator.artist_num = has_artists; aggregator.validate(); } }; var md_group_setup_xhr = function (callback) { var info = this.infos[0]; var ctx = null; if (info.context !== undefined) ctx = info.context; callback(null, { method: "GET", url: "https://api.mangadex.org/group/" + info.id, headers: {"accept": "application/json"}, context: ctx, }); }; var md_group_parse_response = function (xhr, callback) { if (xhr.status !== 200) { callback("Invalid response."); return; } var ctx; if (xhr.context !== undefined) { ctx = xhr.context.split("_"); } else { callback("Invalid response context"); return; } var jsdata = xlinks_api.parse_json(xhr.responseText, null); if (jsdata == null || jsdata.data == undefined || jsdata.data.attributes == undefined) { callback("Cannot parse response."); return; } var data = { id: jsdata.data.id || null, name: jsdata.data.attributes.name || null, }; xlinks_api.cache_set("group_" + jsdata.data.id, data, 7 * xlinks_api.ttl_1_day); md_aggregators[ctx[1]].add_data("group", data); callback(null, [data]); }; var md_author_parse_response = function (xhr, callback) { if (xhr.status !== 200) { callback("Invalid response."); return; } var ctx; if (xhr.context !== undefined) { ctx = xhr.context.split("_"); } else { callback("Invalid response context"); return; } jsdata = xlinks_api.parse_json(xhr.responseText, null); if (jsdata == null || jsdata.data == undefined || jsdata.data.attributes == undefined) { callback("Cannot parse response."); return; } var data = { id: jsdata.data.id || null, name: jsdata.data.attributes.name || null, }; xlinks_api.cache_set("author_" + jsdata.data.id, data, 7 * xlinks_api.ttl_1_day); md_aggregators[ctx[1]].add_data("author", data); callback(null, [data]); }; var md_ch_url_get_info = function (url, callback) { var m = /(https?:\/*)?(?:www\.)?mangadex\.org\/chapter\/([a-z0-9\-]+)/i.exec(url), url_info; if (m !== null && m[2] !== undefined) { url_info = { id: m[2], site: "mangadex", type: "chapter", tag: "MD", context: "chapter_" + m[2], includes: ["scanlation_group"], }; // If we don't need the author or artist, we only need to make one API call // This will populate the chapter's relationship attributes with *some* manga information // if (!xlinks_api.config.mangadex.show_author && !xlinks_api.config.mangadex.show_artist) // url_info.includes.push("manga"); // hack a way to use the site icon as a language flag // The MangadexDataAggregator will decide if it's an icon or flag and how to style it if (xlinks_api.config.mangadex.show_icon) url_info.icon = "replaceme-"+site_short[url_info.site]+"-"+url_info.id; callback(null, url_info); } else { callback(null, null); } }; var md_ch_url_info_to_data = function (url_info, callback) { // make chapter api call with its id as context // the parse_response functions will then add their data to the aggregator var ctx = url_info.context.split("_"); var aggregator = new MangadexDataAggregator(callback); aggregator.context = ctx[1]; md_aggregators[ctx[1]] = aggregator; var chdata = xlinks_api.cache_get(url_info.id, null); if (chdata !== null) { aggregator.add_data("chapter", chdata); md_get_other_ch_data(url_info, chdata, aggregator); } else { xlinks_api.request("mangadex", "generic", url_info.id, url_info, (err, data) => { if (err == null) { md_set_data(data, url_info, (err) => {}); aggregator.add_data("chapter", data); md_get_other_ch_data(url_info, data, aggregator); } }); } }; var md_get_other_ch_data = function (url_info, data, aggregator) { var has_manga = false; var has_groups = 0; var group_num = 0; if (data.relationships !== undefined) { for (let i = 0; i < data.relationships.length; i++) { if (data.relationships[i].type == "scanlation_group") group_num++; } if (xlinks_api.config.mangadex.show_group) aggregator.group_num = group_num; for (let i = 0; i < data.relationships.length; i++) { switch (data.relationships[i].type) { case "scanlation_group": { has_groups++; if (data.relationships[i].attributes !== undefined) { let groupdata = { id: data.relationships[i].id || null, name: data.relationships[i].attributes.name || null, }; xlinks_api.cache_set("group_" + groupdata.id, groupdata, 7 * xlinks_api.ttl_1_day); aggregator.add_data("group", groupdata); } else { let group_url_info = { id: data.relationships[i].id, context: "group_" + data.id, type: "group", }; let cached_groupdata = xlinks_api.cache_get("group_" + group_url_info.id); if (cached_groupdata !== null) aggregator.add_data("group", cached_groupdata); else xlinks_api.request("mangadex", "generic", group_url_info.id, group_url_info, (err, data) => {}); } break; } case "manga": { has_manga = true; // (v1.4) new strategy: always make a manga API call to so we have the author/artist info ready for the actions pane let manga_url_info = { id: data.relationships[i].id, context: "manga_" + data.id, type: "manga", includes: ["author", "artist"], }; let cached_mangadata = xlinks_api.cache_get("manga_" + manga_url_info.id); if (cached_mangadata !== null && cached_mangadata.relationships) { md_get_other_manga_data(data.id, cached_mangadata, aggregator); aggregator.add_data("manga", cached_mangadata); } else xlinks_api.request("mangadex", "generic", manga_url_info.id, manga_url_info, (err, data) => {}); break; } } } } if (!has_manga) { mangadata = { title: null, altTitles: null, descrition: null, originalLanguage: null, state: null, status: null, tags: null, }; aggregator.add_data("manga", mangadata); } if (xlinks_api.config.mangadex.show_group && has_groups < group_num) { aggregator.group_num = has_groups; aggregator.validate(); } }; var md_create_actions = function (data, url_info, callback, retry = false) { let aggregator = md_aggregators[url_info.id]; // console.log(["md_create_actions", data, url_info, aggregator]); const tag_marker_Y = " [X]"; const tag_marker_N = ""; // [id, base_title] var title_data = [aggregator.data.manga.id, aggregator.data.final.manga]; // [[id, name], [id, name], ...] var group_data = Array(); if (aggregator.data.groups.length > 0) { for (let i = 0; i < aggregator.data.groups.length; i++) { group_data.push([aggregator.data.groups[i].id, aggregator.data.groups[i].name]); } } // [[id, name, roles], [id, name, roles], ...] var author_data = Array(); var added_author_ids = Array(); var mangadata; if (aggregator.data.authors.length > 0) { for (let i = 0; i < aggregator.data.authors.length; i++) { if (added_author_ids.indexOf(aggregator.data.authors[i].id) != -1) continue; let roles = Array(); if (aggregator.data.author_ids.indexOf(aggregator.data.authors[i].id) != -1) roles.push("Author"); if (aggregator.data.artist_ids.indexOf(aggregator.data.authors[i].id) != -1) roles.push("Artist"); author_data.push([aggregator.data.authors[i].id, aggregator.data.authors[i].name, roles]); added_author_ids.push(aggregator.data.authors[i].id); } } else { // get data from aggregator's manga data or cached manga data var author_ids = Array(); var artist_ids = Array(); var author_list = Array(); if (aggregator.data.manga.relationships) mangadata = aggregator.data.manga; else mangadata = xlinks_api.cache_get("manga_" + aggregator.data.manga.id); if (mangadata !== null && mangadata.relationships) { for (let i = 0; i < mangadata.relationships.length; i++) { if (mangadata.relationships[i].type != "author" && mangadata.relationships[i].type != "artist") continue; author_list.push(mangadata.relationships[i]) if (mangadata.relationships[i].type == "author") author_ids.push(mangadata.relationships[i].id) if (mangadata.relationships[i].type == "artist") artist_ids.push(mangadata.relationships[i].id) } for (let i = 0; i < author_list.length; i++) { if (added_author_ids.indexOf(author_list[i].id) != -1) continue; let roles = Array(); if (author_ids.indexOf(author_list[i].id) != -1) roles.push("Author"); if (artist_ids.indexOf(author_list[i].id) != -1) roles.push("Artist"); author_data.push([author_list[i].id, author_list[i].attributes.name, roles]); added_author_ids.push(author_list[i].id); } } else { // console.log(["No cached manga data with relationships:", aggregator.data.manga.id, mangadata]); } } if (author_data.length == 0 && !retry) { // console.log("No authors found. Initiating new manga data request."); let manga_url_info = { id: aggregator.data.manga.id, context: "manga_" + aggregator.data.chapter.id, type: "manga", includes: ["author", "artist"], }; // Not aborting here and returning incomplete details would lock the details pane until the page is reloaded. // Exit this function and let the request callback try again. xlinks_api.request("mangadex", "generic", manga_url_info.id, manga_url_info, (err, mdata) => { if (err == null) { md_create_actions(data, url_info, callback, true); } }); return; } // [[id, name, group], [id, name, group], ...] var tag_data = Array(); var tag_groups = {}; // console.log([]); if (aggregator.data.manga.tags) mangadata = aggregator.data.manga; else { let cached_mangadata = xlinks_api.cache_get("manga_" + aggregator.data.manga.id); if (cached_mangadata.tags) mangadata = cached_mangadata; else mangadata = {tags: []}; } for (let i = 0; i < mangadata.tags.length; i++) { let tag_id = mangadata.tags[i].id; let tag_group = mangadata.tags[i].attributes.group; tag_group = tag_group.charAt(0).toUpperCase() + tag_group.substr(1); let tag_name = ""; if (mangadata.tags[i].attributes.name.en !== undefined) tag_name = mangadata.tags[i].attributes.name.en; else { let keys = Object.keys(mangadata.tags[i].attributes.name); tag_name = mangadata.tags[i].attributes.name[keys[0]]; } if (tag_groups[tag_group] == undefined) tag_groups[tag_group] = Array(); tag_groups[tag_group].push([tag_id, tag_name, tag_group]); } let tag_group_keys = Object.keys(tag_groups); for (let i = 0; i < tag_group_keys.length; i++) { tag_data = tag_data.concat(tag_groups[tag_group_keys[i]]); } // array of [descriptor, url, link_text] let urls = []; let base_url = "https://mangadex.org/"; let last_descriptor = ""; let descriptor = ""; let tag_marker = ""; let tag_array = xlinks_api.config.mangadex.tag_filter.trim().replace(/,\s+/g, ",").toLowerCase().split(","); urls.push(["Title:", base_url + "title/" + title_data[0], title_data[1]]); for (let i = 0; i < group_data.length; i++) { descriptor = "Group:"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + "group/" + group_data[i][0], group_data[i][1]]); } for (let i = 0; i < author_data.length; i++) { descriptor = author_data[i][2].join(", ") + ":"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + "author/" + author_data[i][0], author_data[i][1]]); } for (let i = 0; i < tag_data.length; i++) { descriptor = tag_data[i][2] + ":"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor tag_marker = (aggregator.tag_filter_tripped && (tag_array.indexOf(tag_data[i][1].toLowerCase()) >= 0)) ? tag_marker_Y : tag_marker_N; urls.push([descriptor+tag_marker, base_url + "tag/" + tag_data[i][0], tag_data[i][1]]); } callback(null, urls); }; // Dynasty-Scans var ds_get_data = function (info) { var data = xlinks_api.cache_get("ds_" + info.id); return data; }; var ds_set_data = function (data, info, err_callback) { var lifetime = 7 * xlinks_api.ttl_1_day; if (info.lifetime !== undefined) lifetime = info.lifetime; xlinks_api.cache_set("ds_" + info.id, data, lifetime); if (err_callback !== null) err_callback(null); }; var ds_chapter_setup_xhr = function (callback) { var info = this.infos[0]; callback(null, { method: "GET", url: "https://dynasty-scans.com/chapters/" + info.id, }); }; var ds_chapter_parse_response = function (xhr, callback) { if (xhr.status !== 200) { callback("Invalid response."); return; } var html = xlinks_api.parse_html(xhr.responseText, null), info = this.infos[0], data, nn, n2; if (html === null) { callback("Invalid response"); return; } data = { base_title: null, title: null, authors: [], groups: [], volumes: [], pages: null, releasedAt: null, tags: [], links: { // each entry: [link, label] // or {name: link} base_title: [], authors: {}, groups: {}, volumes: {}, tags: {}, }, }; // title if ((nn = $("#chapter-title > b", html)) !== null) { data.base_title = nn.textContent.trim(); if ((n2 = $("a", nn)) !== null) data.links.base_title = [n2.getAttribute("href"), n2.innerText]; } else { callback(null, [{error: "Invalid chapter"}]); return; } // authors if ((nn = $$("#chapter-title > a", html)).length > 0) { for (let i = 0; i < nn.length; i++) { data.authors.push(nn[i].textContent.trim()); data.links.authors[nn[i].textContent.trim()] = nn[i].getAttribute("href"); } } // pages data.pages = $$("a.page", html).length; // volumes if ((nn = $$("span.volumes > a", html)).length > 0) { for (let i = 0; i < nn.length; i++) { data.volumes.push(nn[i].textContent.trim()); data.links.volumes[nn[i].textContent.trim()] = nn[i].getAttribute("href"); } } // groups if ((nn = $$("span.scanlators > a", html)).length > 0) { for (let i = 0; i < nn.length; i++) { if (nn[i].textContent.trim().length > 0) { data.groups.push(nn[i].textContent.trim()); data.links.groups[nn[i].textContent.trim()] = nn[i].getAttribute("href"); } else if ((n2 = $("img", nn[i])) !== null) { // check the a > img.alt attribute, e.g. for /u/ scanlations data.groups.push(n2.getAttribute("alt")); data.links.groups[n2.getAttribute("alt")] = nn[i].getAttribute("href"); } } } // releasedAt if ((nn = $("span.released", html)) !== null) { data.releasedAt = nn.textContent.trim(); data.releasedAt = data.releasedAt.replace(/\s+/g, " "); } // tags if ((nn = $$("span.tags > a.label", html)).length > 0) { for (let i = 0; i < nn.length; i++) { data.tags.push(nn[i].textContent.trim()); data.links.tags[nn[i].textContent.trim()] = nn[i].getAttribute("href"); } } ds_set_data(data, info, (err) => { if (err !== null) console.log("Error caching Dynasty data."); }) data.title = ds_make_title(data, info); callback(null, [data]); }; var ds_ch_url_get_info = function (url, callback) { var m = /(https?:\/*)?(?:www\.)?dynasty-scans\.com\/chapters\/([^#]+)/i.exec(url), url_info; if (m !== null && m[2] !== undefined) { url_info = { id: m[2], site: "dynasty", type: "chapter", tag: "DS", }; if (xlinks_api.config.dynasty.show_icon) { if (xlinks_api.config.dynasty.tag_filter.trim() !== "") url_info.icon = "replaceme-"+site_short[url_info.site]+"-"+url_info.id; else url_info.icon = "site_DS"; } callback(null, url_info); } else { callback(null, null); } }; var ds_ch_url_info_to_data = function (url_info, callback) { // var dsdata = xlinks_api.cache_get(url_info.id, null); var dsdata = ds_get_data(url_info); if (dsdata !== null) { dsdata.title = ds_make_title(dsdata, url_info); callback(null, dsdata); } else xlinks_api.request("dynasty", "chapter", url_info.id, url_info, callback); }; var ds_make_title = function (data, url_info) { var title = data.base_title; if (xlinks_api.config.dynasty.show_author && data.authors.length > 0) title = "[" + data.authors.join(", ") + "] " + title; if (xlinks_api.config.dynasty.show_pages) title += " (" + String(data.pages) + "p)"; if (xlinks_api.config.dynasty.show_group && data.groups.length > 0) { title += " [" + data.groups.join(', ') + "]"; } title = title.replace(/\s+/g, " "); // modify the icon if there's a tag filter defined if (xlinks_api.config.dynasty.tag_filter.trim() !== "") { // "A a, bB, C c c" -> ["a a", "bb", "c c c"] var tag_array = xlinks_api.config.dynasty.tag_filter.trim().replace(/,\s+/g, ",").toLowerCase().split(","); var filter_match = false; for (let i = 0; i < data.tags.length; i++) { if (tag_array.indexOf(data.tags[i].toLowerCase()) >= 0) { filter_match = true; break; } } // console.log([title, data.tags, tag_array, filter_match]) replace_icon(url_info.id, "dynasty", "site_DS", filter_match); } return title; }; var ds_create_actions = function (data, info, callback, retry = false) { const tag_marker_Y = " [X]"; const tag_marker_N = ""; // console.log(["ds_create_actions", data, info, callback]); if (data.links == undefined) { if (retry) { callback(null, [["Error:", null, "No details available. Try clearing the cache in the X-Links settings."], ["Hint:", null, "The [X-links] button at the top/bottom of the page or in the drop-down of the 4chan X header."]]); return; } // No link data. Must be from an earlier version that didn't collect it. // Make a new request and let its callback try again. xlinks_api.request("dynasty", "chapter", info.id, info, (err, dsdata) => { if (err == null) { ds_create_actions(dsdata, info, callback, true); } }); return; } // array of [descriptor, url, link_text] var urls = Array(); var base_url = "https://dynasty-scans.com"; var last_descriptor = ""; let descriptor = ""; let tag_array = xlinks_api.config.dynasty.tag_filter.trim().replace(/,\s+/g, ",").toLowerCase().split(","); let tag_marker = ""; if (data.releasedAt) urls.push(["Released:", null, data.releasedAt]); if (data.links.base_title.length > 0) urls.push(["Title:", base_url + data.links.base_title[0], data.links.base_title[1]]); for (i=0; i < data.authors.length; i++) { descriptor = "Author:"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + data.links.authors[data.authors[i]], data.authors[i]]); } for (i=0; i < data.groups.length; i++) { descriptor = "Group:"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + data.links.groups[data.groups[i]], data.groups[i]]); } for (i=0; i < data.volumes.length; i++) { descriptor = "Volume:"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + data.links.volumes[data.volumes[i]], data.volumes[i]]); } for (i=0; i < data.tags.length; i++) { descriptor = "Tag:"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor tag_marker = (tag_array.indexOf(data.tags[i].toLowerCase()) >= 0) ? tag_marker_Y : tag_marker_N; urls.push([descriptor+tag_marker, base_url + data.links.tags[data.tags[i]], data.tags[i]]); } callback(null, urls); }; // bato.to class BatoDataAggregator { constructor(final_callback) { this.callback = final_callback; this.context = null; this.tag_filter_tripped = false; this.domain = "bato.to"; this.data = { series: { title: "", language: "", url: "", genres: [], // [[author, url], [author, url], ...] authors: Array(), }, chapter: { series_id: "", num: "", // e.g. "Volume 5 Chapter 21.5" title: "", // e.g. "Extras: Volume 4 & Twitter Part 4" // pages: 0, } }; } add_data(category, data) { this.data[category] = data; this.validate(); } validate() { if (this.data.series.title == "") return; if (this.data.chapter.num == "") return; // make a usable object out of the categories var aggdata = { language: "", author: "", series: "", chapter_num: "", chapter_title: "", // pages: "", title: "", }; var template = ""; // "${language} ${author} ${series} ${chapter_num} ${chapter_title} ${pages} ${group}" if (xlinks_api.config.bato.show_orig_lang && !xlinks_api.config.bato.use_flags && this.data.series.language) template += "${language} "; if (xlinks_api.config.bato.show_author && this.data.series.authors.length > 0) template += "${author} "; if (this.data.series.title) template += "${series} "; if (this.data.chapter.num) template += "${chapter_num} "; if (xlinks_api.config.bato.show_ch_title && this.data.chapter.title) template += "${chapter_title} "; // if (xlinks_api.config.bato.show_pages && this.data.chapter.pages) // template += "${pages} "; if (this.data.series.authors.length > 0) { let author_names = Array(); for (let i = 0; i < this.data.series.authors.length; i++) { author_names.push(this.data.series.authors[i][0]); } aggdata.author = "[" + author_names.join(", ") + "]"; } if (this.data.series.title) aggdata.series = this.data.series.title; if (this.data.chapter.num) aggdata.chapter_num = this.data.chapter.num; if (this.data.chapter.title) aggdata.chapter_title = '- "' + this.data.chapter.title + '"'; if (this.data.series.language) aggdata.language = '[' + this.data.series.language + ']'; if (this.data.chapter.pages) aggdata.pages = "(" + this.data.chapter.pages + "p)"; aggdata.title = interpolate(template, aggdata); aggdata.title = aggdata.title.replace(/^\s+/, ""); aggdata.title = aggdata.title.replace(/\s$/, ""); aggdata.title = aggdata.title.replace(/\s+/g, " "); if (xlinks_api.config.bato.show_icon) { // modify the [MD] tag into an icon or flag // apply a style if the tag filter is tripped let icon_name = "site_BT"; let apply_style = false; if (xlinks_api.config.bato.show_orig_lang && xlinks_api.config.bato.use_flags && lang_to_flag[this.data.series.language] !== undefined) icon_name = lang_to_flag[this.data.series.language] // search for tags matching the tag filter if (xlinks_api.config.bato.tag_filter.trim() !== "") { // "A a, bB, C c c" -> ["a a", "bb", "c c c"] let tag_array = xlinks_api.config.bato.tag_filter.trim().replace(/,\s+/g, ",").toLowerCase().split(","); for (let i = 0; i < this.data.series.genres.length; i++) { if (tag_array.indexOf(this.data.series.genres[i].toLowerCase()) >= 0) { apply_style = true; break; } } // console.log([aggdata.title, data.manga.tags, tag_array, apply_style]) } this.tag_filter_tripped = apply_style; replace_icon(this.context, "bato", icon_name, apply_style); } this.callback(null, aggdata); } } var bt_aggregators = {}; var bt_v3_lang_short = { "Japanese": "jp", "Korean": "kr", "Chinese": "zh", "Chinese (繁)": "zh", "Chinese (粵)": "zh", "Indonesian": "id", "Thai": "th", }; const bt_mirrors = [ "xbato\\.com", "xbato\\.net", "xbato\\.org", "zbato\\.com", "zbato\\.net", "zbato\\.org", "readtoto\\.com", "readtoto\\.net", "readtoto\\.org", "batocomic\\.com", "batocomic\\.net", "batocomic\\.org", "batotoo\\.com", "batotwo\\.com", "battwo\\.com", "comiko\\.net", "comiko\\.org", "mangatoto\\.com", "mangatoto\\.net", "mangatoto\\.org", "dto\\.to", "fto\\.to", "jto\\.to", "hto\\.to", "mto\\.to", "wto\\.to", "bato\\.to", ]; const bt_mirrors_re = bt_mirrors.join("|"); var bt_get_data = function (key) { var data = xlinks_api.cache_get("bt_" + key); return data; }; var bt_set_data = function (key, data, err_callback) { var lifetime = 7 * xlinks_api.ttl_1_day; xlinks_api.cache_set("bt_" + key, data, lifetime); if (err_callback !== null) err_callback(null); }; var bt_chapter_setup_xhr = function (callback) { var info = this.infos[0]; // context should be "chapter_{id}" var ctx = null; if (info.context !== undefined) ctx = info.context; if (info.bt_version == 2) { callback(null, { method: "GET", url: "https://"+info.domain+"/chapter/"+info.id+"/1", context: [2, "chapter", ctx], }); } else if (info.bt_version == 3) { callback(null, { method: "GET", url: "https://"+info.domain+"/title/"+info.series_id+"/"+info.id+"?load=0", context: [3, "chapter", ctx], }); } else { console.error(['bt_chapter_setup_xhr: Unsupported bt_version', info]); } }; var bt_series_setup_xhr = function (callback) { var info = this.infos[0]; // context should be "chapter_{id}" var ctx = null; if (info.context !== undefined) ctx = info.context; callback(null, { method: "GET", url: "https://"+info.domain+"/title/"+info.series_id, context: [3, "series", ctx], }); }; var bt_generic_parse_response = function (xhr, callback) { var bt_version = xhr.context[0]; var resp_type = xhr.context[1]; if (resp_type == "chapter") { if (bt_version == 2) bt_chapter_v2_parse_response(xhr, callback); else if (bt_version == 3) bt_chapter_v3_parse_response(xhr, callback); else console.error(['bt_generic_parse_response: Unsupported chapter bt_version', xhr]); } else if (resp_type = "series") { if (bt_version == 3) bt_series_v3_parse_response(xhr, callback); else console.error(['bt_generic_parse_response: Unsupported series bt_version', xhr]); } else { console.error(['bt_generic_parse_response: Unsupported response type', xhr]); } } var bt_chapter_v2_parse_response = function (xhr, callback) { var ctx = xhr.context[2]; var chapter_id = ctx.split('_')[1]; var html = xlinks_api.parse_html(xhr.responseText, null); var ch_data = { series_id: "", num: "", title: "", // pages: 0, }; // ch_data.pages = $$('img.page-img', html).length; // if (ch_data.pages == 0) ch_data.pages = $$('select optgroup[label="Page"] option', html).length; var series_a = $('h3.nav-title a', html); if (series_a !== null) { // "https://bato.to/series/137465" or just "/series/137465" let series_a_split = series_a.href.split('/'); ch_data.series_id = series_a_split[series_a_split.length - 1]; // make the series "API" request url_info = { bt_version: 3, id: chapter_id, series_id: ch_data.series_id, site: "bato", type: "chapter", tag: "BT", context: ctx, } xlinks_api.request("bato", "series", url_info.series_id, url_info, (err, data) => { if (err !== null) return; bt_set_data("series_"+url_info.series_id, data, (err) => {}); let aggregator = bt_aggregators[url_info.id]; aggregator.add_data("series", data); } ); } else console.error("bt_chapter_v2_parse_response: not found: 'h3.nav-title a'"); // get chapter title from the dropdown var ch_option = $('select optgroup option[value="'+chapter_id+'"]', html); if (ch_option !== null) { // ch_option.innerText examples: // "Volume 1 Chapter 1\n : You’re a Bad Girl, Huh?\n " // "Volume 5 Chapter 21.5\n : Extras: Volume 4 & Twitter Part 4\n " // "Chapter 1\n " // they always end on an extra \n and a bunch of spaces let title_split = ch_option.innerText.split('\n'); if (title_split.length > 1) ch_data.num = title_split[0].trim(); if (title_split.length > 2) ch_data.title = title_split[1].trim(); if (ch_data.title.startsWith(': ')) ch_data.title = ch_data.title.substring(2); } else console.error("bt_chapter_v2_parse_response: not found: 'select optgroup option[value=\""+chapter_id+"\"]'"); callback(null, [ch_data]); }; var bt_chapter_v3_parse_response = function (xhr, callback) { var ctx = xhr.context[2]; var chapter_id = ctx.split('_')[1]; var html = xlinks_api.parse_html(xhr.responseText, null); // console.log(["bt_chapter_v3_parse_response", chapter_id, html]); var ch_data = { series_id: "", num: "", title: "", // pages: 0, }; // ch_data.pages = $$('div[name="image-item"]', html).length; // console.log(["bt_chapter_v3_parse_response", "pages", $$('div[name="image-item"]', html)]); var series_a = $('h3 a[href*="/title/"]', html); if (series_a !== null) { var title_m = /title\/(\d+)/.exec(series_a.href); if (title_m) ch_data.series_id = title_m[1]; } var ch_option = $('select optgroup[label="Chapters"] option[key="'+chapter_id+'"]', html); // console.log(["bt_chapter_v3_parse_response", ch_option]); if (ch_option !== null) { // ch_option.innerText examples: // "Volume 5 Chapter 21.5 : Extras: Volume 4 & Twitter Part 4" // "Volume 1 Chapter 1 : You’re a Bad Girl, Huh?" // "Chapter 1" let title_split = ch_option.innerText.split(' : '); if (title_split.length > 0) ch_data.num = title_split[0].trim(); if (title_split.length > 1) ch_data.title = title_split[1].trim(); } else console.error("bt_chapter_v3_parse_response: not found: 'select optgroup[label=\"Chapters\"] option[key=\""+chapter_id+"\"]'"); callback(null, [ch_data]); }; var bt_series_v3_parse_response = function (xhr, callback) { var ctx = xhr.context[2]; var chapter_id = ctx.split('_')[1]; // console.log(["bt_series_v3_parse_response", xhr]); var html = xlinks_api.parse_html(xhr.responseText, null); var series_data = { title: "", language: "", url: "", genres: [], authors: [], }; var series_a = $('h3 a[href*="/title/"]', html); if (series_a !== null) { series_data.title = series_a.innerText; // series_a.href would get expanded using the domain the script is running on and not bato.to series_data.url = series_a.getAttribute("href"); } var language_spans = $$('div.whitespace-nowrap span', html); // console.log(["bt_series_v3_parse_response", "language_spans:", language_spans]); if (language_spans.length > 0) { // [🇬🇧, English, Tr From, 🇨🇳, Chinese] let orig_lang = language_spans[language_spans.length - 1].innerText; if (bt_v3_lang_short[orig_lang] !== undefined) series_data.language = bt_v3_lang_short[orig_lang]; } else console.error("bt_series_v3_parse_response: not found: 'div.whitespace-nowrap span'"); //