Recently Written · git

subplz-web

git clone https://github.com/equwal/subplz-web

Log | Files | Refs


commit 2e539051cb769e0086b04056a1a63852b5f83020
equwal <truex@equwal.com>
2026-09-21 03:06:21 -0700

Share the subtitles of a Japanese book on honjimaku

honjimaku.com is a free library of subtitles for Japanese audiobooks.
After a conversion in Japanese, the page offers to send the .srt there:
the title, the ID of the audiobook, and the visitor's honjimaku API
key, which stays in this browser. honjimaku finds the book or makes the
entry, checks the file, and says why if it refuses it. The dialog says
that the subtitles hold the words of the book.

 frontend/app.js     | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 frontend/index.html | 21 ++++++++++++++++++
 frontend/style.css  |  5 +++++
 3 files changed, 90 insertions(+)
diff --git a/frontend/app.js b/frontend/app.js
index 16adae3..973675e 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -542,8 +542,72 @@ function renderResultButtons() {
     (b) => makeVideo('mkv', b));
   button('⬇ Video for YouTube (.mp4)', 'No subtitles baked in — add the .srt in YouTube Studio', '',
     (b) => makeVideo('mp4', b));
+  // honjimaku is a library for Japanese books.
+  if (running.job.language === 'ja') {
+    button('⇪ Share on honjimaku', 'Give the subtitles to honjimaku.com, a free library for Japanese audiobooks', 'secondary',
+      () => openShare());
+  }
+}
+
+/* ---------------- share on honjimaku ---------------- */
+
+// A test can point this at a local honjimaku.
+const honjimaku = () => window.SUBREAD_HONJIMAKU || 'https://honjimaku.com';
+const shareEl = {
+  dialog: $('share-dialog'), form: $('share-form'), title: $('share-title'), bookId: $('share-book-id'),
+  key: $('share-key'), send: $('share-send'), status: $('share-status'),
+};
+
+function openShare() {
+  const r = running.job.result;
+  shareEl.title.value = shareEl.title.value || r.stem;
+  try { shareEl.key.value = localStorage.getItem('honjimaku-key') || ''; } catch { /* private window */ }
+  shareEl.status.hidden = true;
+  shareEl.dialog.showModal();
 }
 
+shareEl.form.addEventListener('submit', async (e) => {
+  e.preventDefault();
+  const r = running?.job.result;
+  if (!r) return;
+  const say = (text, link) => {
+    shareEl.status.hidden = false;
+    shareEl.status.textContent = text;
+    if (link) {
+      const a = document.createElement('a');
+      a.href = link; a.target = '_blank'; a.rel = 'noopener'; a.textContent = ' Open it on honjimaku.';
+      shareEl.status.append(a);
+    }
+  };
+  const key = shareEl.key.value.trim();
+  const call = async (path, options) => {
+    const res = await fetch(honjimaku() + path, { ...options, credentials: 'omit', headers: { Authorization: key, ...(options.headers || {}) } });
+    const body = await res.json().catch(() => ({}));
+    if (!res.ok) throw new Error(body.error || `honjimaku answered ${res.status}.`);
+    return body;
+  };
+  shareEl.send.disabled = true;
+  say('Sending…');
+  try {
+    // Finds the book if honjimaku has it, however the title is typed, or makes it.
+    const bookId = shareEl.bookId.value.trim();
+    const { entry_id: entry } = await call('/api/entries', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ name: shareEl.title.value.trim(), ...(bookId ? { book_id: bookId } : {}) }),
+    });
+    const form = new FormData();
+    form.append('file', new File([r.srt], r.srtName, { type: 'application/x-subrip' }));
+    const done = await call(`/api/entries/${entry}/upload`, { method: 'POST', body: form });
+    try { localStorage.setItem('honjimaku-key', key); } catch { /* private window */ }
+    if (done.problems?.length) say(`honjimaku did not take the file. ${done.problems.join(' ')}`);
+    else say('Shared. Thank you.', `${honjimaku()}/entry/${entry}`);
+  } catch (err) {
+    say(err.message === 'Failed to fetch' ? 'honjimaku.com could not be reached.' : err.message);
+  }
+  shareEl.send.disabled = false;
+});
+
 async function makeEpub(btn) {
   const label = btn.textContent;
   btn.disabled = true;
diff --git a/frontend/index.html b/frontend/index.html
index 13ae656..67950aa 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -197,6 +197,27 @@
   <p id="signin-note" class="muted small" hidden></p>
 </dialog>
 
+<dialog id="share-dialog" class="modal">
+  <form method="dialog"><button class="modal-x" aria-label="Close">×</button></form>
+  <h2>Share on honjimaku</h2>
+  <p class="muted"><a href="https://honjimaku.com" target="_blank" rel="noopener">honjimaku.com</a> is a free
+    library of subtitles for Japanese audiobooks. Other learners can then read along with this book
+    and do not have to make the subtitles again.</p>
+  <form id="share-form" class="share-form" autocomplete="off">
+    <label for="share-title">Title of the book, as the audiobook shop writes it</label>
+    <input id="share-title" required maxlength="200">
+    <label for="share-book-id">Audiobook ID, if there is one (Audible ASIN, audiobook.jp number)</label>
+    <input id="share-book-id" maxlength="40" pattern="[A-Za-z0-9 ._\-]*" placeholder="B0BPXSSWVF">
+    <label for="share-key">Your honjimaku API key
+      (<a href="https://honjimaku.com/account" target="_blank" rel="noopener">make an account, then "Generate API key"</a>)</label>
+    <input id="share-key" type="password" required autocomplete="off">
+    <p class="muted small">The key stays in this browser. The subtitles go from this tab to honjimaku.com and to
+      nowhere else. They hold the words of the book: share them only if you may.</p>
+    <button class="primary" id="share-send">Share the subtitles</button>
+  </form>
+  <p id="share-status" class="muted small" hidden></p>
+</dialog>
+
 <dialog id="pricing-dialog" class="modal wide">
   <form method="dialog"><button class="modal-x" aria-label="Close">×</button></form>
   <h2>Credits for our server</h2>
diff --git a/frontend/style.css b/frontend/style.css
index cae96bb..96d7202 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -303,6 +303,11 @@ button.compact { padding: 6px 12px; font-size: 13px; }
 .cloud { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 14px; }
 .cloud p { margin: 0 0 10px; }
 
+.share-form { display: grid; gap: 6px; margin: 14px 0 8px; }
+.share-form label { font-size: 13px; font-weight: 600; margin-top: 6px; }
+.share-form input { padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; font: inherit; background: transparent; color: inherit; }
+.share-form button { margin-top: 10px; }
+
 /* ---------- dialogs ---------- */
 dialog.modal {
   border: 1px solid var(--line); border-radius: var(--radius);