kemono.party importer: Patreon
Provides a button to add a one-click importer for patreon content
This script is marked as "broken" and is likely not working
Details: Patreon changed the menu structure
This script is marked as "external"This script was commisioned by someone else, and I will not check if it works. If it breaks,
contact me Click here to install
Browse More Scripts
// ==UserScript==
// @name kemono.party importer: Patreon
// @namespace 1788de4e09f079dbe2138354150004afc50aae5c
// @version 1.6
// @description Provides a button to add a one-click importer for patreon content
// @author /u/AyrA_ch
// @match https://*.patreon.com/*
// @match https://patreon.com/*
// @run-at document-idle
// @grant GM_cookie
// @external true
// @expired true
// @broken Patreon changed the menu structure
// ==/UserScript==
//Version history
// 1.6 - Re-attach export form if it's removed by patreon scripts
// 1.5 - Fix timeout function always firing
// 1.4 - Added detection for when the cookie fnction is not working
// 1.3 - The banner at the bottom can now be closed
// 1.2 - Debug logger
// 1.1 - Added restriction on how often a user can import
// 1.0 - Initial version (basic importer)
(function () {
'use strict';
//Icons for the button
const HTML = {
//Floppy disk (importer ready)
floppy: "\uD83D\uDCBE",
//Hourglass (waiting for cookie data)
wait: "\u231B",
//Red cross (import delay has not elapsed yet)
blocked: "\u274C",
//Banner close button
close: "\u274C"
};
//Known error codes. This helps to find out where an error happens if a user reports it.
//Entries are indexed as follows:
//0: error(true) or warning(false)
//1: code
//2: message
//Warnings will not show the code and error name
const ERR = {
nobox: [true, 1, "Search form should be present on main page but is not. Please report this error"],
cookie: [true, 2, "Unable to read your cookies"],
dom: [true, 3, "Search form found but it's not in the expected location"],
delay: [false, 4, "You're trying to import too often"],
importremind: [false, 5, "You have not imported content for a long time. Click on the " + HTML.floppy + " next to the search box"],
noimport: [false, 6, "You have never imported anything. Click on the " + HTML.floppy + " next to the search box"],
cookiefunc: [true, 7, "GM_cookie is missing from your script manager. Ask the script manager developer to implement this."],
cookietimeout: [true, 8, "Timeout reading cookies. The browser is not responding to our cookie read request."],
formdeleted: [true, 9, "Patreon has deleted the entire search form. If this error persists after reloading, report this error."]
};
//Number of seconds a user has to wait between imports.
const importDelay = 43200; //12 hours
//Remind the user if he hasn't imported in a long time.
const importReminder = 86400 * 7; //7 days
//This style makes the button fit more in line with the rest of the toolbar
const btnStyle = 'width:2rem;height:2rem;background-color:transparent;border:none;';
//Patreon uses randomly generated class names, so we anchor to a known static name
//In our case, it's the search box, because its name is always "q" and is unlikely to change.
let box = document.querySelector("input[name='q']");
//Holds the banner div
let banner = null;
//Holds the session id
let sessionId = null;
//Function to be used as debug logger
const dbg = console.debug.bind(console, "IMPORTER:");
//Shows error banner
const showErr = function (name, details) {
let msg = "Unknown error";
//Build error message
if (ERR[name]) {
if (ERR[name][0]) {
msg = "#" + ERR[name][1] + " " + name.toUpperCase() + ": " + ERR[name][2];
} else {
msg = ERR[name][2];
}
}
if (details) {
msg += "; " + details;
}
if (!banner) {
//Create banner if it doesn't exists
banner = document.createElement("div");
banner.setAttribute("style", "position:fixed;bottom:0px;left:0px;right:0px;" +
"height:2rem;text-align:center;padding-top:1rem");
document.body.appendChild(banner);
}
//Nonexistent entries are considered errors too
if (!ERR[name] || ERR[name][0]) {
//Show errors in red
banner.style.backgroundColor = "#FEE";
banner.style.color = "#F00";
banner.textContent = "kemono importer is not functioning properly. " +
"If this error persists, contact support with this code: " + msg;
} else {
//Show warning as yellow
banner.style.backgroundColor = "#FFA";
banner.style.color = "#000";
banner.textContent = "Warning: " + msg;
}
//Add close button to banner
const a = document.createElement("button");
a.setAttribute("style",
"padding:0px;" +
"display:inline-block;" +
"float:right;" +
"margin-right:2em;" +
"border:none;" +
"background:transparent;");
a.textContent = HTML.close;
a.addEventListener("click", function (e) {
e.preventDefault();
banner.parentNode.removeChild(banner);
banner = null;
});
banner.appendChild(a);
dbg("Error:", name, details);
};
const lastImportDate = function () {
return +localStorage.lastImport;
}
//Checks whether the importDelay has elapsed or not
const canImport = function () {
const lastImported = lastImportDate();
//Returns true, if lastImported is NaN or if the time has elapsed
return !lastImported || Date.now() - lastImported >= importDelay * 1000;
};
//Checks whether we should remind the user to import again
const remindImport = function () {
const lastImported = lastImportDate();
if (lastImported) {
return Date.now() - lastImported >= importReminder * 1000;
}
return false;
};
//Callback for the cookie result function
const cookieResult = function (cookies, err) {
console.timeEnd("read cookie");
dbg("Got cookie response");
const submit = box.querySelector("[type='submit']");
if (!err) {
const c = sessionId = cookies[0];
if (!c) {
console.log("User not logged in. No session cookie found.");
return;
}
//Set the cookie value and enable the submit button
const cookiebox = box.querySelector("[name='session_key']");
submit.value = canImport() ? HTML.floppy : HTML.blocked;
submit.disabled = false;
cookiebox.value = c.value;
dbg("Ready to import");
} else {
//Got an error
submit.value = HTML.blocked;
showErr("cookie", err);
//Maybe add a retry?
dbg("Cookie error");
}
};
console.group("importer init");
console.time("importer init");
if (!box) {
//No search box. Maybe on some page without the form
if (location.pathname === "/home") {
dbg("Searchbox can't be found on home. Needs script rewrite");
showErr("nobox");
} else {
dbg("No search form available. Will not look for cookie");
}
return;
}
//Find containing list of search box (value of nodeName is always uppercase)
while (box && box.nodeName !== "UL") {
dbg("Trying to find parent tag. Current:", box.nodeName);
box = box.parentNode;
}
if (!box) {
dbg("DOM changed. Needs script rewrite.");
//list container of box not found
showErr("dom");
return;
}
dbg("Found form container. DOM still has the expected layout.");
dbg("Creating import form");
const list = box.appendChild(document.createElement("li"));
//Setting innerHTML is dirty but I'm not going to set up an entire form through the DOM.
//
//The reason you should not do this (apart from potential XSS if you're using user data)
//is because it kills all events on all elements that are already inside.
//It also causes a complete re-render of this page section.
//In our case, the element is empty since we just created it.
//If you really need a way around this, there is "element.insertAdjacentHTML"
//See the showErr function for how to use this.
list.innerHTML = "<form method='post' action='https://kemono.party/api/import'>" +
"<input type='hidden' name='service' value='patreon' />" +
"<input type='hidden' name='session_key' value='' />" +
"<input type='submit' value='" + HTML.wait + "' disabled title='Import all creators on kemono.party' style='" + btnStyle + "' />" +
"</form>";
//Asks user if he really wants to send over the cookie.
//Prevents accidental imports if the user just tried to reach his profile icon which is next to the import button.
//It also prevents the user from importing too often
list.querySelector("form").addEventListener("submit", function (e) {
let nextImport = null;
let lastImported = lastImportDate();
dbg("Import attempt");
//Check if this is the first import attempt
if (!lastImported) {
dbg("This is a first import");
lastImported = "never";
} else {
//Check if the delay between imports has elapsed
if (!canImport()) {
//Construct date of earliest possible next import
nextImport = new Date(+lastImported + importDelay * 1000);
dbg("User is importing too often. Next attempt is at", nextImport);
}
//Construct date of last import
lastImported = new Date(+lastImported).toLocaleString();
}
if (!canImport()) {
//User tries too often
e.preventDefault();
//Get delay in minutes
var delay = ((nextImport.getTime() - Date.now()) / 60000) | 0;
if (delay < 60) {
delay += " minutes";
} else {
//Convert to nice time string
var h = (delay / 60 | 0);
var m = delay % 60;
delay = h + " hour" + (h === 1 ? '' : 's') + ", " + m + " minute" + (m === 1 ? '' : 's');
}
//Show detailed import error
showErr("delay", "Try again at " + nextImport.toLocaleString() + " (in " + delay + ")");
return false;
}
if (!confirm("Send your session cookie to kemono.party to import all posts of all creators on patreon?\nDate of last import: " + lastImported)) {
//Import cancelled by user
dbg("User cancelled the import");
e.preventDefault();
return false;
}
//Save time of last import
localStorage.lastImport = Date.now();
dbg("Updated import timestamp to", new Date());
return true;
});
//Check if an element is part of the document
const isInDocument = function (x) {
while (x) {
if (x === document.documentElement) {
return true;
}
x = x.parentNode;
}
return false;
};
//Observe form for changes that hide the exporter
const MO = new MutationObserver(function () {
if (!isInDocument(box)) {
MO.disconnect();
showErr("formdeleted");
}
if (list.parentNode !== box) {
dbg("Had to reattach export form");
//Do not immediately add it back or we may create a feedback loop
if (!MO.timer) {
MO.timer = setTimeout(function () {
MO.timer = null;
box.appendChild(list);
}, 500);
}
}
});
MO.observe(document.body, {
subtree: true,
childList: true
});
if (remindImport()) {
dbg("user has not imported data for a long time");
showErr("importremind");
} else if (!+localStorage.lastImport) {
dbg("user has never imported data");
showErr("noimport");
}
console.timeEnd("importer init");
console.groupEnd("importer init");
//Requesting the session cookie. This is callback based
console.time("read cookie");
if (typeof(GM_cookie) === typeof(function () {})) {
GM_cookie("list", {
name: "session_id"
}, cookieResult);
setTimeout(function () {
if (sessionId === null) {
dbg("No session after 5 seconds");
showErr("cookietimeout");
}
}, 5000);
} else {
dbg("Client is probably using a different or outdated script manager");
showErr("cookiefunc");
}
})();
/*
LICENSE:
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
The full license text can be found here: http://creativecommons.org/licenses/by-nc-sa/4.0/
The link has an easy to understand version of the license and the full license text.
DISCLAIMER:
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/
This script is marked as "expired".
It's likely not going to work the way it should.
If you really need this script,
contact me.
This script is marked as "external".
This means that the script was developed for someone else.
Because I don't use the script myself I will not know when it breaks.
If it breaks,
contact me for a fix.
User Script Managers
A userscript manager is the browser extension that injects scripts into websites
to change their behavior to your liking.
Recommendation
All scripts on this site have been developed and tested with Tampermonkey on firefox.
Try other browsers and other script managers at your own risk.
No script should use firefox or chrome specific features,
which means they should also work in other modern browsers.
If you prefer, you can use greasemonkey.
Get Tampermonkey,
Get Greasemonkey (Firefox only)
Script Installation
Once you have obtained a user script manager,
clicking on the install button will pop up an installation prompt.
To allow script manager detection,
you can install this helper script.
It's not necessary but simplifies your future visits to this site.
Script Installation
We detected, that you have a script manager installed and active.
Click the "Install Script" button to obtain the script.