Export your own Penzu journal entries to a JSON file by capturing the entries loaded in the browser.
// ==UserScript==
// @name Penzu Export Capture
// @name:es Exportador JSON para Penzu
// @namespace https://greasyfork.org/users/dani71153
// @version 1.0.0
// @description Export your own Penzu journal entries to a JSON file by capturing the entries loaded in the browser.
// @description:es Exporta tus propias entradas de Penzu a un archivo JSON capturando las entradas cargadas en el navegador.
// @author dani71153
// @license MIT
// @match https://penzu.com/journals/*/*
// @run-at document-start
// @grant unsafeWindow
// @noframes
// ==/UserScript==
/*
Created by dani71153.
This script is intended to export your own Penzu journal entries.
*/
(function () {
const W = unsafeWindow;
const KEY = "penzu_export_v3";
const delayMs = 15000;
const sleep = ms => new Promise(r => setTimeout(r, ms));
const load = () => JSON.parse(localStorage.getItem(KEY) || "{}");
const save = s => localStorage.setItem(KEY, JSON.stringify(s));
const clear = () => localStorage.removeItem(KEY);
function ids() {
const p = location.pathname.split("/");
return { journalId: p[2], postId: p[3] };
}
function isEntryUrl(url) {
url = String(url);
return url.includes("/api/journals/") &&
url.includes("/entries/") &&
!url.includes("/photos");
}
function download(posts, journalId) {
const blob = new Blob([JSON.stringify(posts, null, 2)], {
type: "application/json"
});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `penzu-${journalId}-export.json`;
a.click();
}
async function handle(body) {
const state = load();
if (!state.active) return;
const entry = body?.entry;
if (!entry?.id) return;
const id = String(entry.id);
if (!state.processed.includes(id)) {
state.posts.push({
id: entry.id,
created_at: entry.created_at,
updated_at: entry.updated_at,
title: entry.title,
plaintext: entry.plaintext_body,
richtext: entry.richtext_body || null,
tags: []
});
state.processed.push(id);
console.log("Exportado:", state.posts.length, entry.title);
}
const next = (body.previous || []).find(x =>
x?.entry?.id && !state.processed.includes(String(x.entry.id))
);
if (!next) {
download(state.posts, state.journalId);
clear();
console.log("Exportación completa:", state.posts.length);
return;
}
save(state);
await sleep(delayMs);
location.href = `/journals/${state.journalId}/${next.entry.id}`;
}
const oldFetch = W.fetch;
W.fetch = async function (...args) {
const res = await oldFetch.apply(this, args);
const url = args[0]?.url || args[0];
if (isEntryUrl(url)) res.clone().json().then(handle).catch(() => {});
return res;
};
const oldOpen = W.XMLHttpRequest.prototype.open;
const oldSend = W.XMLHttpRequest.prototype.send;
W.XMLHttpRequest.prototype.open = function (method, url, ...rest) {
this._url = url;
return oldOpen.call(this, method, url, ...rest);
};
W.XMLHttpRequest.prototype.send = function (...args) {
this.addEventListener("load", function () {
if (isEntryUrl(this._url)) {
try { handle(JSON.parse(this.responseText)); } catch {}
}
});
return oldSend.apply(this, args);
};
window.addEventListener("DOMContentLoaded", () => {
const btn = document.createElement("button");
btn.textContent = "Exportar Penzu";
btn.style.cssText = "position:fixed;top:20px;right:20px;z-index:999999;padding:12px;background:#111;color:white;border:0;border-radius:8px;";
btn.onclick = () => {
const { journalId } = ids();
save({ active: true, journalId, posts: [], processed: [] });
location.reload();
};
document.body.appendChild(btn);
});
})();