您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Backup douban items (currently book items only), download as a JSON file.
// ==UserScript== // @name Backup Douban Items // @namespace ylf8fzzd // @version 0.1.0 // @description Backup douban items (currently book items only), download as a JSON file. // @author ylf8fzzd // @match https://book.douban.com/people/*/do // @match https://book.douban.com/people/*/wish // @match https://book.douban.com/people/*/collect // @match https://book.douban.com/people/*/all // @grant none // ==/UserScript== (() => { 'use strict'; class BackupWrapper { doubanItems; pageSum = 0; itemSum = 0; finished = false; stopped = false; wrapperLocationSelector; wrapperId = 'db-backup-wrapper'; optionClass = 'backup-option'; inputId = 'backup-pages'; errorClass = 'backup-error'; buttonName = 'backup-button'; buttonStartClass = 'backup-start'; buttonStopClass = 'backup-stop'; buttonResumeClass = 'backup-resume'; progressBarClass = 'backup-progress'; downloadLinkClass = 'backup-download'; defaultPageNumber = 0; constructor(){ this.doubanItems = new DoubanBookItems(); this.wrapperLocationSelector = '#db-usr-profile'; this.initWrapper(); } initWrapper() { // create wrapper let wrapper = this.wrapper; if (wrapper) { wrapper.remove(); } wrapper = document.createElement('div'); wrapper.id = this.wrapperId; document.querySelector(this.wrapperLocationSelector).after(wrapper); this.initStyle(); this.initOptions(); this.initErrorMsg(); this.initProgressBar(); this.initDownloadLink(); } get wrapper() { return document.querySelector(`#${this.wrapperId}`); } initStyle() { const style = document.createElement('style'); style.innerHTML = ` #db-backup-wrapper { font-size: 1.3em; margin-bottom: 26px; } #db-backup-wrapper input { font-size: 1em; } #db-backup-wrapper label { padding: 0 10px 0 0; } #db-backup-wrapper .backup-error { color: red; } .backup-progress .page-num, .backup-progress .item-num{ font-size: 1.5em; } `; this.wrapper.append(style); } initOptions() { const options = document.createElement('div'); options.className = this.optionClass; // label const label = document.createElement('label'); label.htmlFor = this.inputId; label.title = '1: backup _only current page.\n' + '2: backup current and next 1 page and so _on.\n' + '0: backup current and all remaining pages.'; label.innerText = 'How many pages to backup?'; // input const input = document.createElement('input'); input.id = this.inputId; input.name = this.inputId; input.type = 'number'; input.min = 0; input.size = 8; input.value = this.defaultPageNumber; input.required = true; // button const button = document.createElement('button'); button.name = this.buttonName; button.className = this.buttonStartClass; button.title = 'Click to start preparing backup data.'; button.innerText = 'Start'; button.addEventListener('click', this.buttonEventHandler); options.append(label); options.append(input); options.append(button); this.wrapper.append(options); } get option() { return this.wrapper.querySelector(`.${this.optionClass}`); } get optionPageNum() { let pageNum = this.option.querySelector(`#${this.inputId}`).value; return parseInt(pageNum); // NaN when pageNum is not a number. } get optionButton() { return this.option.querySelector(`[name=${this.buttonName}]`); } // when Start button clicked. _onStart() { this.doubanItems.clearItems(); this.pageSum = 0; this.itemSum = 0; this.optionButton.innerText = 'Stop'; this.optionButton.className = this.buttonStopClass; console.log('Start.'); } // when Stop button clicked. _onStop() { this.stopped = true; this.optionButton.innerText = 'Resume'; this.optionButton.className = this.buttonResumeClass; console.log('Stop begin.'); } // when loading pages stopped _onStopEnd() { this.updateDownloadLink(); console.log('Stop End.'); } // when Resume button clicked. _onResume() { this.stopped = false; this.optionButton.innerText = 'Stop'; this.optionButton.className = this.buttonStopClass; console.log('Resume.'); } _onPageLoaded() { this.updateProgressBar(); console.log(`${this.pageSum} pages, ${this.itemSum} items loaded.`); } _onError(error) { this.updateErrorMsg(error.message); this._onStop(); this._onStopEnd(); console.log(error); } _onFinised(){ this.optionButton.className = this.buttonStartClass; this.optionButton.innerText = 'Start'; this.finished = true; this.stopped = false; this.updateProgressBar(); this.updateDownloadLink(); // return to first status. this.finished = false; console.log('Finished.'); } buttonEventHandler = async (event) => { this.clearErrorMsg(); const button = event.target; // check page number option. const pagesToLoad = this.optionPageNum; if (isNaN(pagesToLoad)) { const error = TypeError('Invalid page number.'); this._onError(error); return Promise.resolve(error); } if (button.className === this.buttonStartClass) { this._onStart(); } else if (button.className === this.buttonResumeClass) { this._onResume(); } else { this._onStop(); return Promise.resolve('Stop begin.'); } if (this.finished) { return Promise.resolve('Already finished.'); } while (true) { // This should be checked before checking stopped or not, // in case all pages have been loaded between start and end // timing of stop. if (pagesToLoad !== 0 && this.pageSum >= pagesToLoad) { this._onFinised(); return Promise.resolve('Done.'); } // if stopped by user. if (this.stopped) { this._onStopEnd(); return Promise.resolve('Stopped.'); } let itemNum; try { itemNum = await this.doubanItems.loadPage(this.pageSum + 1); } catch (error) { this._onError(error); return Promise.resolve(error); } if (itemNum === 0) { this._onFinised(); return Promise.resolve('Done.') } this.pageSum += 1; this.itemSum += itemNum; this._onPageLoaded(); } } initErrorMsg() { const error = document.createElement('div'); error.className = this.errorClass; this.wrapper.append(error); this.clearErrorMsg(); } get errorMsg() { return this.wrapper.querySelector(`.${this.errorClass}`); } updateErrorMsg(msg) { const error = this.errorMsg; error.style = 'display: block;'; error.innerText = msg; } clearErrorMsg() { const error = this.errorMsg; error.style = 'display: none;'; error.innerText = ''; } initProgressBar() { // init progress bar const progressBar = document.createElement('div'); progressBar.className = this.progressBarClass; progressBar.innerHTML = ` <span class="page-num">0</span> pages, <span class="item-num">0</span> items loaded. <span class="status">Loading more pages.</span> `; progressBar.style = 'display: none;'; this.wrapper.append(progressBar); } get progressBar() { return this.wrapper.querySelector(`.${this.progressBarClass}`); } // update progress bar with specified page number. // if finished, update the whole message. updateProgressBar(){ const progressBar = this.progressBar; progressBar.style = 'dispaly: block;'; progressBar.querySelector('.page-num').innerText = this.pageSum; progressBar.querySelector('.item-num').innerText = this.itemSum; if (this.stopped) { progressBar.querySelector('.status').innerText = 'Stopped.'; } else if (!this.finished) { progressBar.querySelector('.status').innerText = 'Loading more pages...'; } else { progressBar.querySelector('.status').innerText = 'Finished.'; } } initDownloadLink() { // init download link const a = document.createElement('a'); a.href = '#'; a.className = this.downloadLinkClass; a.textContent = 'Download Backup'; a.style = 'display: none;'; this.wrapper.append(a); } get downloadLink(){ return this.wrapper.querySelector(`.${this.downloadLinkClass}`); } updateDownloadLink(){ const output = this.doubanItems.getItemsData(); const contentType = 'application/json'; const filename = 'backup.json'; const blob = new Blob([output], {type: contentType}); const url = URL.createObjectURL(blob); const a = this.downloadLink; a.style = 'display: inline;'; a.href = url; a.setAttribute('download', filename); console.log('Download link updated.'); } } class DoubanItems { allItemsSelector; itemCountPerPage; getItemData; items = []; getItemsData() { const data = []; const getItemData = this.getItemData; for (const item of this.items) { data.push(getItemData(item)); } return JSON.stringify(data, null, 2); } clearItems() { this.items = []; } // return value: promise that resovle to item number of fetched page. async loadPage(page) { const urlSearchParams = new URLSearchParams(window.location.search); let currentStart = urlSearchParams.get('start'); if (!currentStart) { currentStart = 0; } else { currentStart = parseInt(currentStart); } const newStart = currentStart + this.itemCountPerPage * (page - 1); const baseUrl = window.location.origin + window.location.pathname + '?'; let url = baseUrl + 'start=' + newStart; let response; try{ response = await fetch(url); } catch (error){ console.log(`Failed to fetch ${url}. ${error}`); return Promise.reject(error); } if (response.status !== 200){ return Promise.reject(Error('Response status is not 200.')); console.log(`Failed to fetch ${url}. Status code: ${response.status}.`); } console.log(`${url} fetched.`); const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const items = doc.querySelectorAll(this.allItemsSelector); this.items.push(...items); return Promise.resolve(items.length); } } class DoubanBookItems extends DoubanItems { allItemsSelector = '.interest-list .subject-item'; itemCountPerPage = 15; getItemData = (item) => { const titleNode = item.querySelector('h2 a') let title = titleNode ? titleNode.innerText.trim(): ''; // remove redundant newline and spaces. title = title.replace(/[\n\r]/g, '').replace(/ +/, ' '); const pubNode = item.querySelector('.pub'); const pub = pubNode ? pubNode.innerText.trim() : ''; const linkNode = item.querySelector('h2 a') const link = linkNode ? linkNode.getAttribute('href') : ''; // <span class="rating4-t"></span> const ratingNode = item.querySelector('[class^=rating]'); const rating = ratingNode ? ratingNode.getAttribute('class'). replace('rating', '').replace('-t', ''): ''; // <span class="date">2019-02-24 读过</span> const dateAndStatusNode = item.querySelector('.date'); let date; let status; if (dateAndStatusNode) { const dateAndStatus = dateAndStatusNode.innerText.replace(/[\n\r]/g, ''); date = dateAndStatus.split(/ +/)[0]; status = dateAndStatus.split(/ +/)[1]; } else { // 未知图书: book that have been deleted by website date = ''; status = ''; } const commentNode = item.querySelector('.comment'); const comment = commentNode ? commentNode.innerText.trim() : ""; // <span class="tags">标签: XX ZZ</span> const tagsNode = item.querySelector('.tags'); const tags = tagsNode ? tagsNode.innerText.replace(/[\n\r]/g, ''). replace('标签: ','').split(/ +/) : []; return { title, pub, link, rating, date, status, tags, comment } } } new BackupWrapper(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址