- // ==UserScript==
- // @name Pinback Next
- // @namespace https://www.octt.eu.org/
- // @match https://*.pinterest.*/*
- // @grant none
- // @version 0.5.2
- // @author OctoSpacc
- // @license MIT
- // @description The backup and export tool Pinterest forgot, finally revived!
- // @run-at context-menu
- // @grant GM_registerMenuCommand
- // ==/UserScript==
-
- GM_registerMenuCommand('Open Export Tool', async function() {
- const version = '0.5.2';
- const homepageUrl = 'https://greasyfork.org/en/scripts/489596-pinback-next';
- var boards = {}
- , board = {}
- , pins = []
- , pin_count = 0
- , username;
-
- if (match = location.href.match(/^https:\/\/www\.pinterest\..*?\/([a-z0-9_]{1,30})/i)) {
- username = match[1];
- const resource = await getResource('Boards', { username, field_set_key: 'detailed' });
- start(resource);
- } else {
- alert('Log in and visit your profile (pinterest.com/username) or board to start');
- return false;
- }
-
- function start(json) {
- if (document.querySelector('#pboverlay')) return false;
-
- var overlay = document.createElement('div');
- overlay.id = 'pboverlay';
- overlay.innerHTML = `
- <style>
- #pboverlay { display: block; bottom: 0; left: 0; right: 0; top: 0; z-index: 9999; position: fixed; background: rgba(0, 0, 0, 0.8); color: white; text-align: center; }
- #pboverlay .close { color: white; position: absolute; top:10px; right:20px; font-size: 30px; }
- #pboverlay .standardForm { top: 50%; margin-top: -100px; position: absolute; width: 100%; max-width: none; }
- #pboverlay h1 { color: white; }
- #pboverlay .controls a { display: inline-block; }
- #pboverlay select, #pboverlay meter { width: 100%; max-width: 300px; !important }
- #pboverlay .btn, #pboverlay select { padding: 1em; background-color: lightgray; color: black; border: none; border-radius: 4px; }
- #pboverlay a { color: #eeeeee; }
- </style>
- <a href="#" class="close">×</a>
- <form class="standardForm">
- <h1>Choose a board to export</h1>
- <p class="controls">
- <select></select>
- <button class="btn">
- <span>Export 📦</span>
- </button>
- </p>
- <p><a href="${homepageUrl}">Pinback Next v${version}</a></p>
- </form>`;
-
- document.querySelector('body').appendChild(overlay);
- const select = document.querySelector('#pboverlay select');
- const option = document.createElement('option');
- option.text = '[ All public pins ]';
- option.value = 'all';
- select.add(option);
-
- Array.prototype.forEach.call(json.resource_response.data, function (board) {
- const option = document.createElement('option');
- option.text = board.name;
- option.value = board.id;
- option.selected = (location.pathname === board.url);
- select.add(option);
- });
-
- document.querySelector('#pboverlay button').onclick = async function() {
- document.querySelector('#pboverlay .controls').innerHTML = '<meter min="0" max="100"></meter>';
- displayStatus('Exporting...');
- let resource;
- var selected = select.querySelector('option:checked');
- if (selected.value == 'all') {
- displayStatus('Exporting all pins...');
- resource = await getResource('User', { username });
- } else {
- displayStatus(`Exporting ${selected.text}...`);
- resource = await getResource('Board', { board_id: selected.value });
- }
- await parseBoard(resource);
- return false;
- };
-
- document.querySelector('#pboverlay .close').onclick = function() {
- location.href = location.pathname;
- return false;
- };
- }
-
- async function parseBoard(json) {
- board = json.resource_response.data;
- const feed = await getFeed();
- await parseFeed(feed);
- }
-
- async function getFeed(bookmarks) {
- if (board.type == 'user') {
- return await getResource('UserPins', { username, page_size: 25, bookmarks });
- } else {
- return await getResource('BoardFeed', { board_id: board.id, page_size: 25, bookmarks });
- }
- }
-
- async function parseFeed(json) {
- json.resource_response.data.forEach(function(p) {
- if (p.type == 'pin') {
- if (!boards[p.board.id]) boards[p.board.id] = {
- id: p.board.id,
- name: p.board.name,
- url: `https://www.pinterest.com${p.board.url}`,
- privacy: p.board.privacy,
- pins: [],
- }
- boards[p.board.id].pins.push({
- id: p.id,
- link: p.link,
- title: p.title,
- description: p.description,
- note: p.pin_note?.text,
- url: `https://www.pinterest.com/pin/${p.id}`,
- image: p.images.orig.url,
- color: p.dominant_color,
- // longitude: (p.place && p.place.longitude),
- // latitude: (p.place && p.place.latitude),
- pinner: p.pinner.username,
- creator: p.native_creator?.username,
- privacy: p.privacy,
- date: Date.parse(p.created_at),
- });
- displayProgress(pin_count++, board.pin_count);
- }
- })
-
- const bookmarks = json.resource.options.bookmarks;
- if (bookmarks[0] === '-end-') {
- done();
- } else {
- const feed = await getFeed(bookmarks);
- await parseFeed(feed);
- }
- }
-
- async function getResource(resource, options) {
- try {
- const response = await fetch(`/resource/${resource}Resource/get/?data=${encodeURIComponent(JSON.stringify({ options: options }))}`);
- const result = await response.json();
- if (response.status >= 200 && response.status < 300) {
- return result;
- } else {
- throw result;
- }
- } catch (err) {
- alert('An error has occurred.');
- console.error(err);
- }
- }
-
- function displayStatus(text) {
- document.querySelector('#pboverlay h1').innerText = text;
- }
-
- function displayProgress(part, total) {
- displayStatus(`Exporting ${part} of ${total}...`);
- document.querySelector('#pboverlay meter').value = ((part / total) * 100);
- }
-
- function markPrivacy(p) {
- return (p != 'public') ? 1 : 0;
- }
-
- function escapeHtml(unsafe) {
- return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
- }
-
- function done() {
- displayStatus('Export complete!');
-
- let data = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
- <!-- Generated by Pinback Next v${version} (${homepageUrl}) -->
- <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
- <STYLE>
- dl dt { margin-top: 1em; margin-left: 1em; margin-right: 1em; }
- dl dd { margin-top: 0.5em; }
- dl dt > a { word-wrap: anywhere; display: inline-block; min-width: 50%; }
- details > summary > * { display: inline-block; }
- </STYLE>
- <SCRIPT> window.addEventListener('load', function(){
- for (var headerElem of document.querySelectorAll('dl dt > h3')) {
- var sectionElem = headerElem.parentElement;
- var detailsElem = document.createElement('details');
- detailsElem.open = true;
- detailsElem.innerHTML = ('<summary>' + headerElem.outerHTML + '</summary>');
- headerElem.remove();
- detailsElem.innerHTML += sectionElem.outerHTML;
- sectionElem.replaceWith(detailsElem);
- }
-
- var toggleButton = document.createElement('button');
- toggleButton.innerHTML = 'Toggle List/Grid view 🪟️';
- toggleButton.onclick = function(){
- document.querySelector('style').innerHTML += \``+`
- dl,
- dl dt,
- dl dt > a { display: inline-block; }
- dl dt > a,
- dl dt > p { width: min-content; max-width: 33vw; }
- dl dt > a > img { max-width: 33vw; max-height: 33vh; }
- dl > p,
- dl dt > p,
- dl dt > h3 { margin: 0; }
- `+`\`;
- for (var linkElem of document.querySelectorAll('dl dt > a')) {
- var descHtml = '';
- var markElem = linkElem.parentElement;
- var nextElem = markElem.nextElementSibling;
- if (nextElem?.tagName === 'DD') {
- descHtml += ('<p>' + nextElem.innerHTML + '</p>');
- var nextNextElem = nextElem.nextElementSibling;
- if (nextNextElem?.tagName === 'DD') {
- descHtml += ('<p>' + nextNextElem.innerHTML + '</p>');
- nextNextElem.remove();
- }
- nextElem.remove();
- }
- linkElem.innerHTML = ('<img loading="lazy" src="' + linkElem.getAttribute('IMAGE') + '"/>' + '<p>' + linkElem.innerHTML + '</p>');
- markElem.innerHTML += descHtml;
- }
- toggleButton.onclick = function(){ location.reload() };
- }
- document.querySelector('div').appendChild(toggleButton);
- }); </SCRIPT>
- <TITLE>Bookmarks</TITLE>
- <H1>Bookmarks</H1>
- <DIV></DIV>
- <DL><p>`;
-
- for (const id in boards) {
- const b = boards[id];
- data += `<DT><H3 GUID="${b.id}" ORIGLINK="${b.url}" CATEGORY="${b.category}" PRIVATE="${markPrivacy(b.privacy)}">${escapeHtml(b.name)}</H3>\n<DL><p>\n`;
-
- b.pins.forEach(function(p) {
- data += ` <DT><A HREF="${p.link || p.url}" GUID="${p.id}" ORIGLINK="${p.url}" IMAGE="${p.image}" COLOR="${p.color}" AUTHOR="${p.pinner}" ORIGAUTHOR="${p.creator || ''}" ADD_DATE="${p.date}" PRIVATE="${markPrivacy(p.privacy)}">${escapeHtml(p.title || p.description?.trim() || p.link || p.url)}</A>\n`;
- if (p.title && p.description?.trim())
- data += ` <DD>${escapeHtml(p.description)}\n`;
- if (p.note)
- data += ` <DD>${escapeHtml(p.note)}\n`;
- });
-
- data += '</DL><p>\n';
- }
-
- data += '</DL><p>';
-
- const filename = ((board.url || username).replace(/^\/|\/$/g, '').replace(/\//g,'-') + '.html');
- const blob = new Blob([data], { type: 'text/html' });
- const url = URL.createObjectURL(blob);
- document.querySelector('#pboverlay .controls').innerHTML = `<a class="btn" href="${url}" download="${filename}"><span>Save export file 📄</span></a>`;
-
- if (typeof(document.createElement('a').download) === 'undefined') {
- document.querySelector('#pboverlay .controls a').onclick = function() {
- alert('Use "File > Save As" in your browser to save a copy of your export.');
- }
- }
- }
- });