Penzu Export Capture

Export your own Penzu journal entries to a JSON file by capturing the entries loaded in the browser.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==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);
  });
})();