// ==UserScript==
// @name KissGrabber
// @namespace thorou
// @version 2.1.0
// @description extracts embed links from kiss sites
// @author Thorou
// @license GPLv3 - http://www.gnu.org/licenses/gpl-3.0.txt
// @copyright 2019 Leon Timm
// @homepageURL https://github.com/thorio/KGrabber/
// @match https://kissanime.ru/*
// @match https://kimcartoon.to/*
// @match https://kissasian.sh/*
// @run-at document-end
// @noframes
// @grant GM_xmlhttpRequest
// @connect rapidvideo.com
// @downloadURL none
// ==/UserScript==
unsafeWindow.KG = {};
KG.knownServers = {
"rapidvideo": {
regex: '"https://www.rapidvideo.com/e/.*?"',
name: "RapidVideo (no captcha)",
linkType: "embed",
},
"nova": {
regex: '"https://www.novelplanet.me/v/.*?"',
name: "Nova Server",
linkType: "embed",
},
"beta2": {
regex: '"https://lh3.googleusercontent.com/.*?"',
name: "Beta2 Server",
linkType: "direct",
},
"p2p": {
regex: '"https://p2p2.replay.watch/public/dist/index.html\\\\?id=.*?"',
name: "P2P Server",
linkType: "embed",
},
"openload": {
regex: '"https://openload.co/embed/.*?"',
name: "Openload",
linkType: "embed",
},
"mp4upload": {
regex: '"https://www.mp4upload.com/embed-.*?"',
name: "Mp4Upload",
linkType: "embed",
},
"streamango": {
regex: '"https://streamango.com/embed/.*?"',
name: "Streamango",
linkType: "embed",
},
"beta": {
regex: '"https://lh3.googleusercontent.com/.*?"',
name: "Beta Server",
linkType: "direct",
},
}
KG.serverOverrides = {
"kissanime.ru": {},
"kimcartoon.to": {
"rapidvideo": null,
"p2p": null,
"beta2": null,
"nova": null,
"mp4upload": null,
"rapid": {
regex: '"https://www.rapidvideo.com/e/.*?"',
name: "RapidVideo",
linkType: "embed",
},
"fs": {
regex: '"https://video.xx.fbcdn.net/v/.*?"',
name: "FS (fbcdn.net)",
linkType: "direct",
},
"gp": {
regex: '"https://lh3.googleusercontent.com/.*?"',
name: "GP (googleusercontent.com)",
linkType: "direct",
},
"fe": {
regex: '"https://www.luxubu.review/v/.*?"',
name: "FE (luxubu.review)",
linkType: "embed",
},
},
"kissasian.sh": {
"rapidvideo": null,
"p2p": null,
"beta2": null,
"nova": null,
"mp4upload": null,
"streamango": null,
"beta": null, //should work, but script can't load data because of https/http session storage separation
"rapid": {
regex: '"https://www.rapidvideo.com/e/.*?"',
name: "RapidVideo",
linkType: "embed",
},
"fe": {
regex: '"https://www.gaobook.review/v/.*?"',
name: "FE (gaobook.review)",
linkType: "embed",
},
"mp": {
regex: '"https://www.mp4upload.com/embed-.*?"',
name: "MP (mp4upload.com)",
linkType: "embed",
},
},
}
KG.supportedSites = {
"kissanime.ru": {
contentPath: "/Anime/*",
noCaptchaServer: "rapidvideo",
buttonColor: "#548602",
buttonTextColor: "#fff",
},
"kimcartoon.to": {
contentPath: "/Cartoon/*",
noCaptchaServer: "rapid",
buttonColor: "#ecc835",
buttonTextColor: "#000",
optsPosition: 1,
fixes: ["kimcartoon.to_UIFix"],
},
"kissasian.sh": {
contentPath: "/Drama/*",
noCaptchaServer: "rapid",
buttonColor: "#F5B54B",
buttonTextColor: "#000",
},
}
KG.preferences = {
quality: "1080, 720, 480, 360",
}
//entry function
KG.siteLoad = () => {
if (!KG.supportedSites[location.hostname]) {
console.warn("KG: site not supported");
return;
}
KG.applyServerOverrides();
if (KG.if(location.pathname, KG.supportedSites[location.hostname].contentPath) && $(".bigBarContainer .bigChar").length != 0) {
KG.injectWidgets();
}
if (KG.loadStatus()) {
KG.steps[KG.status.func]();
}
}
//saves data to session storage
KG.saveStatus = () => {
sessionStorage["KG-status"] = JSON.stringify(KG.status);
}
//attempts to load data from session storage
KG.loadStatus = () => {
if (!sessionStorage["KG-status"]) {
return false;
}
try {
KG.status = JSON.parse(sessionStorage["KG-status"]);
} catch (e) {
console.error("KG: unable to parse JSON");
return false;
}
return true;
}
//clears data from session storage
KG.clearStatus = () => {
sessionStorage.clear("KG-data");
}
//patches the knownServers object based on the current url
KG.applyServerOverrides = () => {
var over = KG.serverOverrides[location.hostname]
for (var i in over) {
if (KG.knownServers[i]) {
if (over[i] === null) { //server should be removed
delete KG.knownServers[i];
} else { //server should be patched
console.err("KG: patching server entries not implemented");
}
} else { //server should be added
KG.knownServers[i] = over[i];
}
}
}
//injects element into page
KG.injectWidgets = () => {
var site = KG.supportedSites[location.hostname];
var epCount = $(".listing a").length;
//css
$(document.head).append(``);
//box on the right
$(`#rightside .clear2:eq(${site.optsPosition || 0})`).after(optsHTML);
$("#KG-input-to").val(epCount)
.attr("max", epCount);
$("#KG-input-from").attr("max", epCount);
for (var i in KG.knownServers) {
$(``);
for (var i in KG.exporters) {
var $exporter = $(``).appendTo("#KG-input-export");
if ((KG.exporters[i].requireSamePage && !onSamePage) ||
(KG.exporters[i].requireDirectLinks && KG.status.linkType != "direct")
) {
$exporter.attr("disabled", true);
}
}
//actions
$("#KG-action-container .KG-button").remove();
for (var i in KG.actions) {
if (
(!KG.actions[i].requireLinkType || KG.status.linkType == KG.actions[i].requireLinkType) &&
KG.actions[i].servers.includes(KG.status.server)
) {
$("#KG-action-container")
.append(``);
}
}
$("#KG-linkdisplay").show();
}
KG.showSpinner = () => {
$("#KG-linkdisplay-text").html(`
Loading...
`);
}
KG.exportData = (exporter) => {
$("#KG-input-export").val("");
var text = KG.exporters[exporter].export(KG.status);
$("#KG-linkdisplay-export-text").text(text);
$("#KG-input-export-download").attr({
href: `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`,
download: `${KG.status.title}.${KG.exporters[exporter].extension}`,
})
$("#KG-linkdisplay-export").show();
}
//hides the linkdisplay
KG.closeLinkdisplay = () => {
$("#KG-linkdisplay").hide();
KG.clearStatus();
}
//saves a new preferred server
KG.updatePreferredServer = () => {
localStorage["KG-preferredServer"] = $("#KG-input-server").val();
}
//loads preferred server
KG.loadPreferredServer = () => {
$("#KG-input-server").val(localStorage["KG-preferredServer"]);
}
//applies regex to html page to find a link
KG.findLink = (regexString) => {
var re = new RegExp(regexString);
var result = document.body.innerHTML.match(re);
if (result && result.length > 0) {
return result[0].split('"')[1];
}
return "";
}
//wildcard-enabled string comparison
KG.if = (str, rule) => {
return new RegExp("^" + rule.split("*").join(".*") + "$").test(str);
}
//iterates over an array with supplied function
//either (array, min, max, func)
//or (array, func)
KG.for = (array, min, max, func) => {
if (typeof min == "function") {
func = min;
max = array.length - 1;
}
min = Math.max(0, min) || 0;
max = Math.min(array.length - 1, max);
for (var i = min; i <= max; i++) {
func(i, array[i]);
}
}
KG.get = (url) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: (o) => {
resolve(o.response);
},
onerror: () => {
reject();
}
});
});
}
//allows multiple different approaches to collecting links, if sites differ greatly
KG.steps = {};
//default
KG.steps.defaultBegin = () => {
KG.status.func = "defaultGetLink";
KG.saveStatus();
location.href = KG.status.episodes[KG.status.current].kissLink + `&s=${KG.status.server}`;
}
KG.steps.defaultGetLink = () => {
if (!KG.if(location.pathname, KG.supportedSites[location.hostname].contentPath)) { //captcha
return;
}
link = KG.findLink(KG.knownServers[KG.status.server].regex);
KG.status.episodes[KG.status.current].grabLink = link || "error (selected server may not be available)";
KG.status.current++;
if (KG.status.current >= KG.status.episodes.length) {
KG.status.func = "defaultFinished";
location.href = KG.status.url;
} else {
location.href = KG.status.episodes[KG.status.current].kissLink + `&s=${KG.status.server}`;
}
KG.saveStatus();
}
KG.steps.defaultFinished = () => {
KG.displayLinks();
}
//allows for multiple ways to export collected data
KG.exporters = {};
KG.exporters.list = {
name: "list",
extension: "txt",
requireSamePage: false,
requireDirectLinks: false,
export: (data) => {
var str = "";
for (var i in data.episodes) {
str += data.episodes[i].grabLink + "\n";
}
return str;
}
}
KG.exporters.m3u = {
name: "m3u8 playlist",
extension: "m3u8",
requireSamePage: true,
requireDirectLinks: true,
export: (data) => {
var listing = $(".listing a").get().reverse();
var str = "#EXTM3U\n";
KG.for(data.episodes, (i, obj) => {
str += `#EXTINF:0,${listing[obj.num-1].innerText}\n${obj.grabLink}\n`;
});
return str;
}
}
KG.exporters.json = {
name: "json",
extension: "json",
requireSamePage: true,
requireDirectLinks: false,
export: (data) => {
var listing = $(".listing a").get().reverse();
var json = {
title: data.title,
server: data.server,
linkType: data.linkType,
episodes: []
};
for (var i in data.episodes) {
json.episodes.push({
number: data.episodes[i].num,
name: listing[data.episodes[i].num - 1].innerText,
link: data.episodes[i].grabLink
});
}
return JSON.stringify(json);
},
}
KG.exporters.html = {
name: "html list",
extension: "html",
requireSamePage: true,
requireDirectLinks: true,
export: (data) => {
var listing = $(".listing a").get().reverse();
var str = "\n \n";
KG.for(data.episodes, (i, obj) => {
str += ` ${listing[obj.num-1].innerText} \n`;
});
str += " \n\n";
return str;
}
}
KG.exporters.csv = {
name: "csv",
extension: "csv",
requireSamePage: true,
requireDirectLinks: false,
export: (data) => {
var listing = $(".listing a").get().reverse();
var str = "episode, name, url\n";
for (var i in data.episodes) {
str += `${data.episodes[i].num}, ${listing[data.episodes[i].num-1].innerText}, ${data.episodes[i].grabLink}\n`;
}
return str;
}
}
KG.exporters.aria2c = {
name: "aria2c file",
extension: "txt",
requireSamePage: false,
requireDirectLinks: true,
export: (data) => {
var padLength = Math.max(2, data.episodes[data.episodes.length - 1].num.toString().length);
var str = "";
KG.for(data.episodes, (i, obj) => {
str += `${obj.grabLink}\n -o E${obj.num.toString().padStart(padLength, "0")}.mp4\n`;
});
return str;
}
}
//further options after grabbing, such as converting embed to direct links
KG.actions = {
"rapidvideo_getDirect": {
name: "get direct links",
requireLinkType: "embed",
servers: ["rapidvideo", "rapid"],
execute: async (data) => {
KG.showSpinner();
var promises = [];
for (var i in data.episodes) {
promises.push(KG["rapidvideo_getDirect"](data.episodes[i]));
}
await Promise.all(promises);
data.linkType = "direct";
KG.saveStatus();
KG.displayLinks();
},
},
}
//additional function to reduce clutter
//asynchronously gets the direct link
KG["rapidvideo_getDirect"] = async (ep) => {
$html = $(await KG.get(ep.grabLink));
$sources = $html.find("source");
if ($sources.length == 0) {
ep.grabLink = "error: no sources found";
return;
}
var sources = {};
KG.for($sources, (i, obj) => {
sources[obj.dataset.res] = obj.src;
});
parsedQualityPrefs = KG.preferences.quality.replace(/\ /g, "").split(",");
for (var i of parsedQualityPrefs) {
if (sources[i]) {
ep.grabLink = sources[i];
return;
}
}
ep.grabLink = "error: preferred qualities not found";
}
//if something doesn't look right on a specific site, a fix can be written here
KG.fixes = {}
KG.fixes["kimcartoon.to_UIFix"] = () => {
//linkdisplay
var $ld = $("#KG-linkdisplay");
$ld.find(".barTitle").removeClass("barTitle")
.css({
"height": "20px",
"padding": "5px",
});
$("#KG-linkdisplay-title").css({
"font-size": "20px",
"color": $("a.bigChar").css("color"),
})
$ld.find(".arrow-general").remove();
//opts
var $opts = $("#KG-opts-widget");
var title = $opts.find(".barTitle").text();
$opts.find(".barTitle").remove();
$opts.find(".arrow-general").remove();
$opts.before(`
${title}
`)
}
//HTML and CSS pasted here because Tampermonkey apparently doesn't allow resources to be updated
//if you have a solution for including extra files that are updated when the script is reinstalled please let me know: thorio.git@gmail.com
//the grabber widget injected into the page
var optsHTML = `
KissGrabber
from
to
`;
//initially hidden HTML that is revealed and filled in by the grabber script
var linkListHTML = `