// ==UserScript== // @name X-links Extension - Mangadex // @namespace mycropen // @author mycropen // @version 1.5 // @description Linkify and format Mangadex & Dynasty-Scans links // @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", "zh": "lang_cn", "zh-hk": "lang_hk", "id": "lang_id", "th": "lang_th", }; var DataAggregator = function (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(), }; }; DataAggregator.prototype.add_author_id = function (id) { this.data.author_ids.push(id); }; DataAggregator.prototype.add_artist_id = function (id) { this.data.artist_ids.push(id); }; DataAggregator.prototype.add_data = function (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(); }; DataAggregator.prototype.validate = function () { // 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; 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) { 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, " "); this.callback(null, aggdata); 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]) } replace_icon(this.context, "mangadex", icon_name, apply_style); } }; var aggregators = {}; 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 "dynasty" or "mangadex" 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-"+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 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; } 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 = aggregators[ctx[1]]; 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; } 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); 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); 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 DataAggregator 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-"+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 DataAggregator(callback); aggregator.context = ctx[1]; 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) { // console.log(["md_create_actions", data, url_info, callback, aggregators[url_info.id]]); // [id, base_title] var title_data = [aggregators[url_info.id].data.manga.id, aggregators[url_info.id].data.final.manga]; // [[id, name], [id, name], ...] var group_data = Array(); if (aggregators[url_info.id].data.groups.length > 0) { for (let i = 0; i < aggregators[url_info.id].data.groups.length; i++) { group_data.push([aggregators[url_info.id].data.groups[i].id, aggregators[url_info.id].data.groups[i].name]); } } // [[id, name, roles], [id, name, roles], ...] var author_data = Array(); var added_author_ids = Array(); var mangadata; if (aggregators[url_info.id].data.authors.length > 0) { for (let i = 0; i < aggregators[url_info.id].data.authors.length; i++) { if (added_author_ids.indexOf(aggregators[url_info.id].data.authors[i].id) != -1) continue; let roles = Array(); if (aggregators[url_info.id].data.author_ids.indexOf(aggregators[url_info.id].data.authors[i].id) != -1) roles.push("Author"); if (aggregators[url_info.id].data.artist_ids.indexOf(aggregators[url_info.id].data.authors[i].id) != -1) roles.push("Artist"); author_data.push([aggregators[url_info.id].data.authors[i].id, aggregators[url_info.id].data.authors[i].name, roles]); added_author_ids.push(aggregators[url_info.id].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 (aggregators[url_info.id].data.manga.relationships) mangadata = aggregators[url_info.id].data.manga; else mangadata = xlinks_api.cache_get("manga_" + aggregators[url_info.id].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:", aggregators[url_info.id].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: aggregators[url_info.id].data.manga.id, context: "manga_" + aggregators[url_info.id].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 (aggregators[url_info.id].data.manga.tags) mangadata = aggregators[url_info.id].data.manga; else { let cached_mangadata = xlinks_api.cache_get("manga_" + aggregators[url_info.id].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] var urls = Array(); var base_url = "https://mangadex.org/"; var last_descriptor = ""; urls.push(["Title:", base_url + "title/" + title_data[0], title_data[1]]); for (let i = 0; i < group_data.length; i++) { let 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++) { let 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++) { let descriptor = tag_data[i][2] + ":"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + "tag/" + tag_data[i][0], tag_data[i][1]]); } callback(null, urls); }; // Dynasty-Scans var ds_get_data = function (info, callback) { var data = xlinks_api.cache_get(info.id); callback(null, data); }; var ds_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 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-"+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); 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) { // 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 = ""; 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++) { let 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++) { let 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++) { let 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++) { let descriptor = "Tag:"; if (last_descriptor == descriptor) descriptor = ""; else last_descriptor = descriptor urls.push([descriptor, base_url + data.links.tags[data.tags[i]], data.tags[i]]); } callback(null, urls); }; xlinks_api.init({ namespace: "mangadex", name: "Mangadex & Dynasty links", author: "mycropen", description: "Linkify and format Mangadex & Dynasty-Scans links", version: [1,5], registrations: 1, main: main_fn }, function (err) { if (err === null) { xlinks_api.insert_styles(".xl-site-tag-icon[data-xl-site-tag-icon=lang_jp]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAMAAACsjQ8GAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAUVBMVEX////89ffstsPjlaj77/LacYq+CTS8AC2/CjXacov78PP22+HGJUvGJkz23OLac4z99vjst8Tjlqnkl6nkmKrsuMW/CzbbdY3GJ0323ePadI1SYYEeAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAABAAAAAQAYp+hIAAAAAd0SU1FB+cBDAEkH80BkfYAAAB6SURBVCjPzZHJDoAgDEQRVBCVVXD5/w+VGIMQSrj6rvOaTlqE/kaHCcFdNe6HkQbYxOF8XujLKsD5mAejB4SBJkigH0sFpgpB0wxdCCYXbCHYXNhaK1yjpC9LIpkKO3AHvn75AR5bROM4K8+STw9/VZ4VUM4Yp9DfuAEcEgb8vRe9xgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wMS0xMlQwMTozMToyMiswMDowMJwMZyQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDEtMTBUMDM6NTA6NTkrMDA6MDAFGLMuAAAAAElFTkSuQmCC)}.xl-site-tag-icon[data-xl-site-tag-icon=lang_cn]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAMAAACsjQ8GAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAclBMVEXuHCXxQB/xRx7vKSP4oBD4pA/uHyX0cRf0cBfwOCDuIiTuICTuHSXvJCTvKyP6vQvxSh381wfvKCPvLyL6tgz//wDwMiH1fhX4mxD6uwv6wgryUB3vIyT1dxbyURz+7wPvJyP2iRP4oQ/zXRrzWRv///96F0p+AAAAAWJLR0QlwwHJDwAAAAlwSFlzAAAABAAAAAQAYp+hIAAAAAd0SU1FB+cBDAEkN/i0OQwAAABvSURBVCjPY2AgATAy4ZRiZgaRLKxsuBSwc4ApTi5uHiyyvHx8/Px8fLxApoCgEBYFwiKiQMAvBmRyiQtjtUFCVFQCr/MlpaQkISxprPIysjw8sjIglpy8GF6TFPiEufHJKyqx4dXPoKzCMAoGMwAAo/cEXRjamKYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjMtMDEtMTJUMDE6MzE6MjIrMDA6MDCcDGckAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIzLTAxLTEwVDAzOjUwOjU5KzAwOjAwBRizLgAAAABJRU5ErkJggg==)}.xl-site-tag-icon[data-xl-site-tag-icon=lang_kr]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAMAAACsjQ8GAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABoVBMVEX///+7u7vCwsLBwcG9vb3u7u4cHBySkpJ5eXl2dnYdHR3w8PBeXl5paWkwMDCHh4c3Nzf4+Pj29vaFhYU0NDRmZmaOjo6zs7MiIiJzc3NLS0tRUVF+fn56enpNTU1JSUmMjIxnZ2e2trbv7+8eHh4VFRWGhoYuLi75+fn9+PnpparYXGXPN0LPOEPYXWbqqa7++/tqamqxsbEXFxcbGxvNzc1/f3/Ozs733+HSQ07NLjrXWWL66+zY2NgYGBh8fHzPz8/09PSQkJBOTk739ffMOkdSUlKPj4/19fWkhKLrq6++vr56T3y0MUaWNVa3MUXZX2g0Rox7OGMCRp8AR6ADR55vOmjQPEcJTaNoOmzLLzvLLjtiO2+pP1kzbLMZRJQ3QIQXRJV9VYCOrdWjjaqUlJRPT099fX32+fwtZ7EyaK/69/lTU1PQ0NCAgIAxMTHh6fQya7Pm7fbX19czMzPT09MkJCSgoKD3+fyQr9Y1bbQKTqQ3b7SUstf5+/0nJyeRkZG0tLQhISGsrKxXV1d4eHhlZWX+/v5xcXG5ubm8vLzgoCRaAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAAABAAAAAQAYp+hIAAAAAd0SU1FB+cBDAElAS4VndQAAAFISURBVCjPY2CgEmBkQhdhZkHmsbKxc6DKc7JzcSNxeXj5+AUEEXwhAWERUTEEX1xCUkpaBtkAWTl5BUUlGE9ZhV1VTV2DgUFTS1tHV0/fgIFBw9DImN2EFarA1IyP39icgcHC0goMrG0YGGzthEXsHWBGODo5Ay1wcbWCAmugGbJu7h5IdrIALfC0ggMvBgZBb4yw8IFI+vr5WwVgDaxAsHxQcEhoWDhWBRGRUVbRMSEgEItVQVxIfEIiWD4kCUOSCejI5BA4SAE6Ejl2PFLT0hkYMjJh8lnZDAwcOexCMPncvHx+Y1MGhgKoisIiBoZiO+ESs1KoAm4V9rJyUFBXVFZVV9fU1gEtMDSvb2CDBTVDY1Nzi3Q6spta03JYRJUQfB7efP4SISRHlQiLtKWiJhjJdmQT2jsaVJATDEMnSgIDga4uBioBAMJERI7yM7r7AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIzLTAxLTEyVDAxOjMxOjIyKzAwOjAwnAxnJAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMy0wMS0xMFQwMzo1MDo1OSswMDowMAUYsy4AAAAASUVORK5CYII=)}.xl-site-tag-icon[data-xl-site-tag-icon=lang_hk]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAMAAACsjQ8GAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAABFFBMVEW6AAC6AQHFKSntvLzrtLS+DQ345OT+/f355+e7AgLWZmb+/Pzwycn////DIiLGKyvBGxu/ERHfiYnyzs7HMjLfiorilZXUYGC8BwfadXXxzc3VZ2f45eXln5/TXFz77+/vw8O7AwPTXl7xy8vYbm7ruLjGLCzmpqbtvr767u756urMQ0O8CAjz09P99vb34uLwx8fDICDHNDTLQEDMRETadnb12dnmoaG/ExPAFhboqan99fX02trgjIzEJibz0NDz1NTJNzfcf3/ZcXHuvr7MQUHuv7/EJSX9+Pj67e3SWFj//v6/EhLehob++/vwy8v88/Ppra3HMTHCHx/uwMDilJTWZ2f99/f12tq8BgbSWVnFKirY+jP4AAAAAWJLR0QN9rRh9QAAAAlwSFlzAAAABAAAAAQAYp+hIAAAAAd0SU1FB+cBDQA3Izr/obEAAACmSURBVCjPY2AYhIARvzQTMwteeVY2dg5OfAq4uHl4+XBJ8gsIMgqx8QqLiIqJS2BTIMnLKyUtIyvHwSbPrqCIRYESrzKviqqaugavjKaWNhYFOrp6+gaGRsYmpmbmFlgdYWllbWNrYmfvYIjTE45Ozi6uMmxuuOTdPXg1Pb14Nb19cCjw9fMPCAziFeYNxi5vEcKrF8rKEBYegTMwLSJBZFQ0w7ADACDyErqWmAEaAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIzLTAxLTEyVDAxOjMxOjIyKzAwOjAwnAxnJAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMy0wMS0xMFQwMzo1MDo1OSswMDowMAUYsy4AAAAASUVORK5CYII=)}.xl-site-tag-icon[data-xl-site-tag-icon=lang_id]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYAQMAAAChnW13AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEXnABH///9GPYQdAAAAAWJLR0QB/wIt3gAAAAlwSFlzAAAABAAAAAQAYp+hIAAAAAd0SU1FB+cBDQA3KkMjGRUAAAAQSURBVAjXY2CgAPwHAjIJAJsjL9Ejwc0pAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIzLTAxLTEyVDAxOjMxOjIyKzAwOjAwnAxnJAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMy0wMS0xMFQwMzo1MDo1OSswMDowMAUYsy4AAAAASUVORK5CYII=)}.xl-site-tag-icon[data-xl-site-tag-icon=lang_th]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAYBAMAAABpfeIHAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAFVBMVEWlGTHq2d/09fhGQ18tKkrb2+L///+mSNUGAAAAAWJLR0QGYWa4fQAAAAlwSFlzAAAABAAAAAQAYp+hIAAAAAd0SU1FB+cBDQA3LkRO3QwAAAAkSURBVBjTY2CgAhBEAwxKaIAIAWM0wOCCBugkEIoGyPELFQAAxhk0Eaz+gbcAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjMtMDEtMTJUMDE6MzE6MjIrMDA6MDCcDGckAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIzLTAxLTEwVDAzOjUwOjU5KzAwOjAwBRizLgAAAABJRU5ErkJggg==)}.xl-site-tag-icon[data-xl-site-tag-icon=site_DS]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAYBQTFRF+YqH/+XMlC4zbAMG/sSr2oN9/Hh58E1V1E1So4CX2dvn6ez3Zlqg/8uz/OrX+WVn49Xc2ur9/tvFuJag6+Tp/tO86Pf/ulZW5pWK2cPJnj1C9pyU/bql9t3Z2H13p0VIu2Zm06Sjy2lmpVZb9ePh9t3L/bSjwoeH/FtguXl82ouD/v/+qFxi5sjG3Z6YmU9W5qaZ+m1w/r+p+JKN2WVn+6mc1ztEzZaXwXFusjY9tEVL5by46mVo8f7/9LOg5Kqj6J2R7Hl327O128rP//Pa+qSYx3l049zk/cSyqmFl1ef9+r6x/K2fzDA62tPc3ef21ZeM9byq1a6w3uLt6bmo9q2cyq22yqOox46QpU5R88y+//v26omF9dbFilZz/LWo/NbO/fDs9MOw/M/B/8+3hh8l/9bC/8S9fhMZ/t/I8q+h+aCW2OX538/V/8ev8KeX1d7wsk9P/vTw8NbR0bzC9LWpvF9e+bylyZ6gznFs+YB/opfB1FxepWBvr2xu8vT5Ltt4cwAAA3xJREFUeNo0kflb2loQhseTEKgQwr6EyCprXJpQEFAUUUERceG2Ba1aldAAEo22aUnR+6/fg+39fjjPec68M9/MHDD/levtsP2528Y2l8tVt9XHNuDL5kajYf/ess+3XIXXsmf+2PMphqI/SaUSSnYR/OuxNxr3xx1/uz6/jJJlj3Pe419FNZpWfhZOCl0wm+32xuM4vNOut5Nsp+xZ8pd3ulKhp5CkIZOsAY7b79uRcN02vxPKJMsu6XO5tYS0niKSPi1YgWMMtOvdZN3WTmub6fKYSPP1GKvSJCmKXq0C9zPgF6rijtmHzBL/SYrxY7YUohWSEytRAzQw0FJVv82VVt3hAz69ilzPSGWHMw/OUIDHR7s9Ha0t2/yo0oum+dgrWrthQ7KsPtCi6KuA0+lMo6m2bitoihLt8Og1xt6GSpJEyD6FHA5xhVZoEtU6EfZBJCdni3I4KQe9vsz6GiF9VEQMmJ0sOVn5uhRVOCV6VkWdDiqZ+s/rfIRQFfIUHv3IR2o/VkocSSqTavrl3Yu3+GHhaeGJPyAe6FOox1TlISp6fXhwOvj5wGK52i5uW58jNxcLskbfQlL6SPoMmQmemuz50jGLJUaZtq0HxPLTRVLaPAWppoiG4IQTOY7r/YOuLJYSxQhfu8Svp5tFZAhCiOPEYHTYo2mSc59LFovFRAnMtw6RvFh4Dgcz4Es0E4bNwWGmVqEdA/XlStoQmFR2WTpb7JytaQYIBHLNL5mYRBDIPRi8R0SRYpgUAysL4SrPb5UgABCSCSx0Mhg47koyzICUkdpWlxerwS8QD5QIQkbhyv7cHAYO1aIgCKl8/ujINC1N4QecQ57w+hS3uz/AuvvWlE0UY5oBxnxehw9AUdNpMyEqPQd2cLgPr4sb+gZjzB8ZjUeMTm2DIFA67F0rbsesgpuuFHEOZcwbU0YjowOAQG2Y9L0EOQPmBv1+ODTSKQq3iSXo2SwIU5Oe/R/ABArlsjpeVYphBAovATamFGCAxsAc1q3sFfeyQOFRcHzUHIKgQzbQFGnHG7C7QrB0IjfKYvNZnngLOD5qcsrfArsxgnjfE6+vc6NR7lqk3ZcAgVyC2+/1B28F1uVX1B3c0fv74r4ys43DKHF415vlW63W3S1Z+v2OOLE63Hhxb6Zx2Do9v4zPtGi1rsks/u5VOWK9/PMWj5//J8AARTPMVw1SAkMAAAAASUVORK5CYII=)}.xl-site-tag-icon[data-xl-site-tag-icon=site_MD]{background-image:url(data:image/png;base64,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)}"); xlinks_api.register({ settings: { sites: [ // namespace //name, default, title, description, descriptor? ["mangadex", true, "mangadex.org", "Enable link processing for mangadex.org"], ["dynasty", true, "dynasty-scans.com", "Enable link processing for dynasty-scans.com"], // descriptor: { type: string, options: <array of [string:value, string:label, string:description, {type: ...}]> } // the {type: ...} is optional and can take the following values: "checkbox", "select", "textbox", "textarea", "button" // {type: "checkbox"} // {type: "select", options: [[value, label_text, description?], [value, label_text, description?], ...]} // {type: "textbox"} or {type: "textbox", get: (v) => {...}, set: (v) => {...}} // {type: "textarea"} // {type: "button", text: "Clear", on_change: on_cache_clear_click} // for pre-existing vars: [ "name" ] ], mangadex: [ ["show_icon", true, "Show an icon instead of an [MD] tag", ""], ["show_orig_lang", true, "Show original language", "Include the original language of a series as a tag [ja], [ko], [zh], etc."], ["use_flags", true, "Use country flags", "Show country flags instead of language tags in place of the [MD] tag or icon."], ["show_author", true, "Show author name", ""], ["show_artist", true, "Show artist name", "People that are both author and artist will only appear once."], ["show_volume", true, "Show volume number", ""], ["show_ch_title", true, "Show chapter title", ""], ["show_pages", true, "Show page count", ""], ["show_group", false, "Show group name", ""], ["custom_title", false, "Non-default series title language", "With the default title as fallback"], ["title_search_order", "en, orig-ro, orig", "Series title search order", "orig = original language; -ro = romanized; e.g. ja, ja-ro, en, zh, zh-hk, ko, id, th, es, vi, de, ru, uk, fr, fa, pt, pt-br, tr, ...", {type: "textbox"}], // ["show_ch_lang", false, "Show chapter language", "Include the language a chapter was translated into"], ["tag_filter", "", "Tag filter", "(https://mangadex.org/tag) List of English tag names separated by a comma; e.g. \"4-koma, genderswap, gore\"", {type: "textbox"} ], ["tag_filter_style", "invert", "How to modify the icon on a tag filter match", "Only works if you show an icon or flag instead of an [MD] tag", { type: "select", options: [ // [ value, label_text, description? ] ["none", "No change", ""], ["rotate180", "Rotate 180°", "transform: rotate(180deg)"], ["long_strip", "Long strip", "transform: scaleX(0.5)"], ["invert", "Invert colors", "filter: invert(1)"], ["grayscale", "Grayscale", "filter: grayscale(1)"], ["opacity50", "Opacity 50%", "filter: opacity(0.5)"], ["drop_shadow", "Drop shadow", "filter: drop-shadow(0.0rem 0.0rem 0.15rem #FF0000)"], ["sepia", "Sepia", "filter: sepia(1)"], ["blur1.5", "Blur", "filter: blur(1.5px)"], ["hue_rotate90", "Hue rotate 90°", "filter: hue-rotate(90deg)"], ["hue_rotate180", "Hue rotate 180°", "filter: hue-rotate(180deg)"], ["hue_rotate270", "Hue rotate 270°", "filter: hue-rotate(270deg)"], ["custom", "Custom CSS", "<Your CSS here!>"], ] } ], ["tag_filter_style_custom", "", "Custom CSS for matched tag filters", "If you picked \"Custom CSS\" above. I hope you know what you're doing.", {type: "textbox"} ], ], dynasty: [ ["show_icon", true, "Show an icon instead of a [DS] tag", ""], ["show_author", true, "Show author name", ""], ["show_pages", true, "Show page count", ""], ["show_group", false, "Show group name", ""], ["tag_filter", "", "Tag filter", "(https://dynasty-scans.com/tags) List of tags separated by a comma; e.g. \"long strip, het, elf\"", {type: "textbox"} ], ["tag_filter_style", "invert", "How to modify the icon on a tag filter match", "Only works if you show an icon instead of a [DS] tag", { type: "select", options: [ // [ value, label_text, description? ] ["none", "No change", ""], ["rotate180", "Rotate 180°", "transform: rotate(180deg)"], ["long_strip", "Long strip", "transform: scaleX(0.5)"], ["invert", "Invert colors", "filter: invert(1)"], ["grayscale", "Grayscale", "filter: grayscale(1)"], ["opacity50", "Opacity 50%", "filter: opacity(0.5)"], ["drop_shadow", "Drop shadow", "filter: drop-shadow(0.0rem 0.0rem 0.15rem #FF0000)"], ["sepia", "Sepia", "filter: sepia(1)"], ["blur1.5", "Blur", "filter: blur(1.5px)"], ["hue_rotate90", "Hue rotate 90°", "filter: hue-rotate(90deg)"], ["hue_rotate180", "Hue rotate 180°", "filter: hue-rotate(180deg)"], ["hue_rotate270", "Hue rotate 270°", "filter: hue-rotate(270deg)"], ["custom", "Custom CSS", "<Your CSS here!>"], ] } ], ["tag_filter_style_custom", "", "Custom CSS for matched tag filters", "If you picked \"Custom CSS\" above. I hope you know what you're doing.", {type: "textbox"} ], ] }, request_apis: [{ group: "mangadex", namespace: "mangadex", type: "generic", count: 1, concurrent: 1, delay_okay: 220, delay_error: 5000, functions: { // get_data: md_get_data, // set_data: md_set_data, setup_xhr: md_generic_api_xhr, parse_response: md_generic_parse_response }, }, { group: "dynasty", namespace: "dynasty", type: "chapter", count: 1, concurrent: 1, delay_okay: 100, delay_error: 5000, functions: { setup_xhr: ds_chapter_setup_xhr, parse_response: ds_chapter_parse_response }, }], linkifiers: [{ // mangadex.org can only be preceeded by "https://" or "www." or both or neither // the link ends either with the ID, a "/", a "#" or "/5" (for page 5) regex: /(https?:\/\/)?(www\.)?(?<=(www\.|\/\/|\s+|^))mangadex\.org\/chapter\/([a-z0-9\-]+)(\/|\/\d+|\/\d+)?#?/i, prefix_group: 1, prefix: "https://", }, { regex: /(https?:\/*)?(?:www\.)?dynasty-scans.com\/chapters\/(?:[^\s]+)?/i, prefix_group: 1, prefix: "https://", }], commands: [{ url_info: md_ch_url_get_info, to_data: md_ch_url_info_to_data, actions: md_create_actions, // details: create_details }, { url_info: ds_ch_url_get_info, to_data: ds_ch_url_info_to_data, actions: ds_create_actions, // details: create_details }] }); } }); }; main(xlinks_api); })();