Pinback Next

The backup and export tool Pinterest forgot, finally revived!

目前为 2024-03-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Pinback Next
  3. // @namespace https://www.octt.eu.org/
  4. // @match https://*.pinterest.*/*
  5. // @grant none
  6. // @version 0.5.2
  7. // @author OctoSpacc
  8. // @license MIT
  9. // @description The backup and export tool Pinterest forgot, finally revived!
  10. // @run-at context-menu
  11. // @grant GM_registerMenuCommand
  12. // ==/UserScript==
  13.  
  14. GM_registerMenuCommand('Open Export Tool', async function() {
  15. const version = '0.5.2';
  16. const homepageUrl = 'https://greasyfork.org/en/scripts/489596-pinback-next';
  17. var boards = {}
  18. , board = {}
  19. , pins = []
  20. , pin_count = 0
  21. , username;
  22.  
  23. if (match = location.href.match(/^https:\/\/www\.pinterest\..*?\/([a-z0-9_]{1,30})/i)) {
  24. username = match[1];
  25. const resource = await getResource('Boards', { username, field_set_key: 'detailed' });
  26. start(resource);
  27. } else {
  28. alert('Log in and visit your profile (pinterest.com/username) or board to start');
  29. return false;
  30. }
  31.  
  32. function start(json) {
  33. if (document.querySelector('#pboverlay')) return false;
  34.  
  35. var overlay = document.createElement('div');
  36. overlay.id = 'pboverlay';
  37. overlay.innerHTML = `
  38. <style>
  39. #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; }
  40. #pboverlay .close { color: white; position: absolute; top:10px; right:20px; font-size: 30px; }
  41. #pboverlay .standardForm { top: 50%; margin-top: -100px; position: absolute; width: 100%; max-width: none; }
  42. #pboverlay h1 { color: white; }
  43. #pboverlay .controls a { display: inline-block; }
  44. #pboverlay select, #pboverlay meter { width: 100%; max-width: 300px; !important }
  45. #pboverlay .btn, #pboverlay select { padding: 1em; background-color: lightgray; color: black; border: none; border-radius: 4px; }
  46. #pboverlay a { color: #eeeeee; }
  47. </style>
  48. <a href="#" class="close">&times;</a>
  49. <form class="standardForm">
  50. <h1>Choose a board to export</h1>
  51. <p class="controls">
  52. <select></select>
  53. <button class="btn">
  54. <span>Export 📦</span>
  55. </button>
  56. </p>
  57. <p><a href="${homepageUrl}">Pinback Next v${version}</a></p>
  58. </form>`;
  59.  
  60. document.querySelector('body').appendChild(overlay);
  61. const select = document.querySelector('#pboverlay select');
  62. const option = document.createElement('option');
  63. option.text = '[ All public pins ]';
  64. option.value = 'all';
  65. select.add(option);
  66.  
  67. Array.prototype.forEach.call(json.resource_response.data, function (board) {
  68. const option = document.createElement('option');
  69. option.text = board.name;
  70. option.value = board.id;
  71. option.selected = (location.pathname === board.url);
  72. select.add(option);
  73. });
  74.  
  75. document.querySelector('#pboverlay button').onclick = async function() {
  76. document.querySelector('#pboverlay .controls').innerHTML = '<meter min="0" max="100"></meter>';
  77. displayStatus('Exporting...');
  78. let resource;
  79. var selected = select.querySelector('option:checked');
  80. if (selected.value == 'all') {
  81. displayStatus('Exporting all pins...');
  82. resource = await getResource('User', { username });
  83. } else {
  84. displayStatus(`Exporting ${selected.text}...`);
  85. resource = await getResource('Board', { board_id: selected.value });
  86. }
  87. await parseBoard(resource);
  88. return false;
  89. };
  90.  
  91. document.querySelector('#pboverlay .close').onclick = function() {
  92. location.href = location.pathname;
  93. return false;
  94. };
  95. }
  96.  
  97. async function parseBoard(json) {
  98. board = json.resource_response.data;
  99. const feed = await getFeed();
  100. await parseFeed(feed);
  101. }
  102.  
  103. async function getFeed(bookmarks) {
  104. if (board.type == 'user') {
  105. return await getResource('UserPins', { username, page_size: 25, bookmarks });
  106. } else {
  107. return await getResource('BoardFeed', { board_id: board.id, page_size: 25, bookmarks });
  108. }
  109. }
  110.  
  111. async function parseFeed(json) {
  112. json.resource_response.data.forEach(function(p) {
  113. if (p.type == 'pin') {
  114. if (!boards[p.board.id]) boards[p.board.id] = {
  115. id: p.board.id,
  116. name: p.board.name,
  117. url: `https://www.pinterest.com${p.board.url}`,
  118. privacy: p.board.privacy,
  119. pins: [],
  120. }
  121. boards[p.board.id].pins.push({
  122. id: p.id,
  123. link: p.link,
  124. title: p.title,
  125. description: p.description,
  126. note: p.pin_note?.text,
  127. url: `https://www.pinterest.com/pin/${p.id}`,
  128. image: p.images.orig.url,
  129. color: p.dominant_color,
  130. // longitude: (p.place && p.place.longitude),
  131. // latitude: (p.place && p.place.latitude),
  132. pinner: p.pinner.username,
  133. creator: p.native_creator?.username,
  134. privacy: p.privacy,
  135. date: Date.parse(p.created_at),
  136. });
  137. displayProgress(pin_count++, board.pin_count);
  138. }
  139. })
  140.  
  141. const bookmarks = json.resource.options.bookmarks;
  142. if (bookmarks[0] === '-end-') {
  143. done();
  144. } else {
  145. const feed = await getFeed(bookmarks);
  146. await parseFeed(feed);
  147. }
  148. }
  149.  
  150. async function getResource(resource, options) {
  151. try {
  152. const response = await fetch(`/resource/${resource}Resource/get/?data=${encodeURIComponent(JSON.stringify({ options: options }))}`);
  153. const result = await response.json();
  154. if (response.status >= 200 && response.status < 300) {
  155. return result;
  156. } else {
  157. throw result;
  158. }
  159. } catch (err) {
  160. alert('An error has occurred.');
  161. console.error(err);
  162. }
  163. }
  164.  
  165. function displayStatus(text) {
  166. document.querySelector('#pboverlay h1').innerText = text;
  167. }
  168.  
  169. function displayProgress(part, total) {
  170. displayStatus(`Exporting ${part} of ${total}...`);
  171. document.querySelector('#pboverlay meter').value = ((part / total) * 100);
  172. }
  173.  
  174. function markPrivacy(p) {
  175. return (p != 'public') ? 1 : 0;
  176. }
  177.  
  178. function escapeHtml(unsafe) {
  179. return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
  180. }
  181.  
  182. function done() {
  183. displayStatus('Export complete!');
  184.  
  185. let data = `<!DOCTYPE NETSCAPE-Bookmark-file-1>
  186. <!-- Generated by Pinback Next v${version} (${homepageUrl}) -->
  187. <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
  188. <STYLE>
  189. dl dt { margin-top: 1em; margin-left: 1em; margin-right: 1em; }
  190. dl dd { margin-top: 0.5em; }
  191. dl dt > a { word-wrap: anywhere; display: inline-block; min-width: 50%; }
  192. details > summary > * { display: inline-block; }
  193. </STYLE>
  194. <SCRIPT> window.addEventListener('load', function(){
  195. for (var headerElem of document.querySelectorAll('dl dt > h3')) {
  196. var sectionElem = headerElem.parentElement;
  197. var detailsElem = document.createElement('details');
  198. detailsElem.open = true;
  199. detailsElem.innerHTML = ('<summary>' + headerElem.outerHTML + '</summary>');
  200. headerElem.remove();
  201. detailsElem.innerHTML += sectionElem.outerHTML;
  202. sectionElem.replaceWith(detailsElem);
  203. }
  204.  
  205. var toggleButton = document.createElement('button');
  206. toggleButton.innerHTML = 'Toggle List/Grid view 🪟️';
  207. toggleButton.onclick = function(){
  208. document.querySelector('style').innerHTML += \``+`
  209. dl,
  210. dl dt,
  211. dl dt > a { display: inline-block; }
  212. dl dt > a,
  213. dl dt > p { width: min-content; max-width: 33vw; }
  214. dl dt > a > img { max-width: 33vw; max-height: 33vh; }
  215. dl > p,
  216. dl dt > p,
  217. dl dt > h3 { margin: 0; }
  218. `+`\`;
  219. for (var linkElem of document.querySelectorAll('dl dt > a')) {
  220. var descHtml = '';
  221. var markElem = linkElem.parentElement;
  222. var nextElem = markElem.nextElementSibling;
  223. if (nextElem?.tagName === 'DD') {
  224. descHtml += ('<p>' + nextElem.innerHTML + '</p>');
  225. var nextNextElem = nextElem.nextElementSibling;
  226. if (nextNextElem?.tagName === 'DD') {
  227. descHtml += ('<p>' + nextNextElem.innerHTML + '</p>');
  228. nextNextElem.remove();
  229. }
  230. nextElem.remove();
  231. }
  232. linkElem.innerHTML = ('<img loading="lazy" src="' + linkElem.getAttribute('IMAGE') + '"/>' + '<p>' + linkElem.innerHTML + '</p>');
  233. markElem.innerHTML += descHtml;
  234. }
  235. toggleButton.onclick = function(){ location.reload() };
  236. }
  237. document.querySelector('div').appendChild(toggleButton);
  238. }); </SCRIPT>
  239. <TITLE>Bookmarks</TITLE>
  240. <H1>Bookmarks</H1>
  241. <DIV></DIV>
  242. <DL><p>`;
  243.  
  244. for (const id in boards) {
  245. const b = boards[id];
  246. data += `<DT><H3 GUID="${b.id}" ORIGLINK="${b.url}" CATEGORY="${b.category}" PRIVATE="${markPrivacy(b.privacy)}">${escapeHtml(b.name)}</H3>\n<DL><p>\n`;
  247.  
  248. b.pins.forEach(function(p) {
  249. 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`;
  250. if (p.title && p.description?.trim())
  251. data += ` <DD>${escapeHtml(p.description)}\n`;
  252. if (p.note)
  253. data += ` <DD>${escapeHtml(p.note)}\n`;
  254. });
  255.  
  256. data += '</DL><p>\n';
  257. }
  258.  
  259. data += '</DL><p>';
  260.  
  261. const filename = ((board.url || username).replace(/^\/|\/$/g, '').replace(/\//g,'-') + '.html');
  262. const blob = new Blob([data], { type: 'text/html' });
  263. const url = URL.createObjectURL(blob);
  264. document.querySelector('#pboverlay .controls').innerHTML = `<a class="btn" href="${url}" download="${filename}"><span>Save export file 📄</span></a>`;
  265.  
  266. if (typeof(document.createElement('a').download) === 'undefined') {
  267. document.querySelector('#pboverlay .controls a').onclick = function() {
  268. alert('Use "File > Save As" in your browser to save a copy of your export.');
  269. }
  270. }
  271. }
  272. });