/** * ntfy exporter * * This saves the ntfy state in your current browser session to a JSON file that * can be imported by another browser session or (only partly tested) the Android app. * * Save the following one-liner to your bookmarks to run it on the fly: javascript:(async()=>{async function dumpStore(dbName,storeName){const db=await new Promise((resolve,reject)=>{const r=indexedDB.open(dbName);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});const tx=db.transaction(storeName,"readonly");const store=tx.objectStore(storeName);const all=await new Promise((resolve,reject)=>{const r=store.getAll();r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});db.close();return all;}const [subscriptions,users,notifications,settings]=await Promise.all([dumpStore("ntfy","subscriptions"),dumpStore("ntfy","users"),dumpStore("ntfy","notifications"),dumpStore("ntfy","prefs")]);const data={subscriptions,users,notifications,settings};const json=JSON.stringify(data,null,2);const blob=new Blob([json],{type:"application/json"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download=`ntfy-backup-${location.host}-${new Date().toISOString().replace(/[:.]/g,"-")}.json`;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);})().catch(e=>alert(e&&e.message?e.message:String(e))); * The full implementation follows */ async function dumpStore(dbName, storeName) { const db = await new Promise((resolve, reject) => { const r = indexedDB.open(dbName); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); }); const tx = db.transaction(storeName, "readonly"); const store = tx.objectStore(storeName); const all = await new Promise((resolve, reject) => { const r = store.getAll(); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); }); db.close(); return all; } Promise.all( [ dumpStore("ntfy", "subscriptions"), dumpStore("ntfy", "users"), dumpStore("ntfy", "notifications"), dumpStore("ntfy", "prefs"), ] ).then(([subscriptions, users, notifications, settings]) => { const data = { subscriptions, users, notifications, settings, }; const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `ntfy-backup-${location.host}-${new Date() .toISOString() .replace(/[:.]/g, "-")}.json`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }); /** * ntfy importer * * A backup file generated through the script above can be restored into * another browser session. Create a bookmark with the following JS: javascript:(async()=>{const DB_NAME="ntfy";const openDb=()=>new Promise((res,rej)=>{const r=indexedDB.open(DB_NAME);r.onsuccess=()=>res(r.result);r.onerror=()=>rej(r.error);r.onblocked=()=>rej(new Error("IndexedDB open blocked (close other tabs using this origin)."));});const pickFile=()=>new Promise((res,rej)=>{const i=document.createElement("input");i.type="file";i.accept="application/json";i.onchange=()=>{const f=i.files&&i.files[0];f?res(f):rej(new Error("No file selected"));};i.click();});const readJson=f=>new Promise((res,rej)=>{const fr=new FileReader();fr.onload=()=>{try{res(JSON.parse(fr.result));}catch(e){rej(e);}};fr.onerror=()=>rej(fr.error);fr.readAsText(f);});const clearAndImport=(db,storeName,rows)=>new Promise((res,rej)=>{const tx=db.transaction(storeName,"readwrite");const store=tx.objectStore(storeName);const c=store.clear();c.onerror=()=>rej(c.error);c.onsuccess=()=>{for(const row of (rows||[])) store.put(row);};tx.oncomplete=()=>res();tx.onerror=()=>rej(tx.error);tx.onabort=()=>rej(tx.error||new Error("Transaction aborted"));});try{const file=await pickFile();const data=await readJson(file);const db=await openDb();const stores=Array.from(db.objectStoreNames);const mapping=[["subscriptions",data.subscriptions],["users",data.users],["notifications",data.notifications],["prefs",data.settings]];for(const [storeName,rows] of mapping){if(!stores.includes(storeName)){console.warn("Skipping missing store:",storeName);continue;}await clearAndImport(db,storeName,rows);console.log(`Imported ${(rows||[]).length} into ${storeName}`);}db.close();location.reload();}catch(e){console.error(e);alert(e&&e.message?e.message:String(e));}})(); * Upon click it will open a file dialog - prompt a previously created backup. * The full implementation follows. */ // Run on the ntfy Web UI origin (same host) in the NEW browser/session. // 1) paste this into DevTools Console // 2) pick the JSON export file when prompted (async () => { const DB_NAME = "ntfy"; function openDb() { return new Promise((resolve, reject) => { const r = indexedDB.open(DB_NAME); r.onsuccess = () => resolve(r.result); r.onerror = () => reject(r.error); r.onblocked = () => reject(new Error("IndexedDB open blocked (close other tabs using this origin).")); // No onupgradeneeded: we assume the DB already exists with the right schema. }); } function pickFile() { return new Promise((resolve, reject) => { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.onchange = () => { const f = input.files && input.files[0]; if (!f) reject(new Error("No file selected")); else resolve(f); }; input.click(); }); } function readJson(file) { return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => { try { resolve(JSON.parse(fr.result)); } catch (e) { reject(e); } }; fr.onerror = () => reject(fr.error); fr.readAsText(file); }); } async function clearAndImportStore(db, storeName, rows) { return new Promise((resolve, reject) => { const tx = db.transaction(storeName, "readwrite"); const store = tx.objectStore(storeName); // Clear then put all rows back. const clearReq = store.clear(); clearReq.onerror = () => reject(clearReq.error); clearReq.onsuccess = () => { // Use put() so existing keys are overwritten if any remain. for (const row of (rows || [])) store.put(row); }; tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); tx.onabort = () => reject(tx.error || new Error("Transaction aborted")); }); } const file = await pickFile(); const data = await readJson(file); const db = await openDb(); const stores = Array.from(db.objectStoreNames); console.log("Found stores:", stores); // The export you created uses these top-level keys: const mapping = [ ["subscriptions", data.subscriptions], ["users", data.users], ["notifications", data.notifications], ["prefs", data.settings], // export called it "settings", store is "prefs" ]; for (const [storeName, rows] of mapping) { if (!stores.includes(storeName)) { console.warn(`Skipping missing store: ${storeName}`); continue; } await clearAndImportStore(db, storeName, rows); console.log(`Imported ${rows ? rows.length : 0} records into ${storeName}`); } db.close(); // Optional: reload so UI picks up changes immediately. location.reload(); })().catch(e => { console.error(e); alert(e && e.message ? e.message : String(e)); });