// ==UserScript==
// @name SaveDoubanBroadcast
// @name:zh-cn 备份个人豆瓣广播至本地
// @namespace https://github.com/JimSunJing
// @version 0.1.4
// @description Save douban Broadcast from douban website. Target page: douban.com/mine/statuses
// @description:zh-cn 保存豆瓣广播内容到本地. 需要打开 douban.com/mine/statuses
// @author JimSunJing
// @match https://www.douban.com/people/*/statuses*
// @require https://unpkg.com/dexie/dist/dexie.js
// @license MIT
// ==/UserScript==
(() => {
'use strict';
// 获取网页中每个广播对象
let statuses;
const DB_NAME = "dbBroadast";
// inject style
const injectStyle = () => {
const style = document.createElement('style');
style.innerHTML = `
.btnS {
background-color: #edf4ed;
color: black;
padding: 3px 6px;
margin: 3px;
border-radius: 7px;
border: none;
}
.newBtnContainer {
background-color: #fff6ed;
padding: 10px;
margin: 30px 0px;
border-radius: 10px
}
.newInput {
margin: 3px 0px;
padding: 3px;
width: 70%;
}
`;
document.head.appendChild(style);
}
// create button
const createBtnS = (innerT = '') => {
let btn = document.createElement("button");
btn.classList.add("btnS");
btn.innerText = innerT;
return btn;
}
const addScriptBtn = () => {
// 在网页旁边的用户信息栏下添加按钮
const aside = document.querySelector(".aside");
// 添加一个控制新增按钮的div
const newBtnContainer = document.createElement("div");
newBtnContainer.classList.add("newBtnContainer");
aside.appendChild(newBtnContainer);
// 开始备份信息的按钮
const btnBackup = createBtnS("备份该页广播");
btnBackup.addEventListener("click", saveDexie);
newBtnContainer.appendChild(btnBackup);
newBtnContainer.appendChild(document.createElement("br"));
// 添加清空 Dexie 按钮
const btnClearDexie = createBtnS("清空数据");
btnClearDexie.addEventListener("click", clearDexie);
newBtnContainer.appendChild(btnClearDexie);
newBtnContainer.appendChild(document.createElement("br"));
// 添加导出 csv 按钮
const btnExportCSV = createBtnS("保存为csv");
btnExportCSV.addEventListener("click", exportCSV);
newBtnContainer.appendChild(btnExportCSV);
newBtnContainer.appendChild(document.createElement("br"));
// notification
const noti = document.createElement("p");
noti.id = 'scriptNoti';
newBtnContainer.appendChild(noti);
// 自动翻页保存的表格
autoForm();
// 为每个广播添加伪删除按钮
for (let i = 0; i < statuses.length; i++) {
// 创建隐藏按钮
const container = document.createElement("div");
container.style.display = 'flex';
container.style.justifyContent = 'flex-end';
const hide = createBtnS("隐藏");
container.appendChild(hide);
// 如果是转发的动态, 在按按钮的时候会跳转, 需要修改按钮位置
if (/^status-real.*/.test(statuses[i].parentElement.className) && statuses[i - 1]) {
hide.innerText = '↓隐藏↓';
statuses[i - 1].appendChild(container);
hide.addEventListener("click", () => {
container.parentElement.nextElementSibling.remove();
container.remove();
})
} else {
statuses[i].appendChild(container);
hide.addEventListener("click", () => {
container.parentElement.remove();
})
}
// console.log('appended hide:',i);
}
}
// 设置自动备份
const autoForm = () => {
// add input
let area = document.querySelector(".newBtnContainer");
if (!area) {
alert("脚本运行错误, 请刷新重试!");
return null;
}
if (document.getElementById('auto-form') !== null) {
return null;
}
let inputForm = document.createElement("form");
inputForm.id = 'auto-form';
inputForm.addEventListener("submit", e => {
e.preventDefault();
// alert(`form cont ${inputForm.elements.CONT.value}`);
localStorage.setItem("cont", Math.floor(Number(inputForm.elements.CONT.value)));
alert(`开始连续备份: ${localStorage.getItem("cont")}`);
if(location.href.indexOf("?p=") < 0) {
window.location.href = location.href + '?p=1&cont=' + localStorage.getItem("cont");
}else {
window.location.href = location.href + '&cont=' + localStorage.getItem("cont");
}
})
area.appendChild(inputForm);
let contInput = document.createElement("input");
contInput.classList.add("newInput");
contInput.setAttribute("placeholder", "输入要自动备份的页数");
contInput.setAttribute("required", '');
contInput.setAttribute("type", "number");
contInput.setAttribute("name", "CONT");
contInput.setAttribute("step", "1");
inputForm.appendChild(contInput);
let submitBtn = createBtnS("开始");
submitBtn.setAttribute("type", "submit");
inputForm.appendChild(document.createElement("br"));
inputForm.appendChild(submitBtn);
}
const updateStatuses = () => {
statuses = document.getElementsByClassName("status-item");
}
const clearStorage = (name) => {
localStorage.removeItem(name);
}
const notification = (msg) => {
document.getElementById("scriptNoti").innerText = msg;
}
// 备份该页的广播信息
const backupStatuses = () => {
updateStatuses();
// extract status info from web
const savedStatuses = [];
for (let i = 0; i < statuses.length; i++) {
if (/deleted/.test(statuses[i].classList)){
continue;
}
// extract pure text of status
const match = statuses[i].innerText;
const full = match ? match.replace('\n', '')
.match(/^.*(?=\s\d*?回应.*)|^.+/s)[0] : "";
let saying = "";
if (match.indexOf("\n\n") > -1) {
saying = match ? match.split('\n\n')[1] : "";
} else {
const t = match ? match.replace('\n', '')
.match(/^(.*\s转发:)(.*)(?=\s\d*?回应.*)|^.+/s)[2] : "";
saying = t ? t.match(/^.*(?=\n.*)/)[0] : "";
}
let uid, sid, time, link;
// status sid
sid = statuses[i].getAttribute("data-sid");
// author uid
uid = statuses[i].getAttribute("data-uid");
if (uid === null) {
uid = document.querySelector(".aside .content a").href
.match(/(?<=https:\/\/www.douban.com\/people\/).*(?=\/)/);
}
link = `https://www.douban.com/people/${uid}/status/${sid}/`;
// status create time
time = statuses[i].querySelector(".created_at").getAttribute("title");
let row = {
id: sid,
sid: sid,
saying: saying,
full: full,
uid: uid,
link: link,
created: time
}
// img links
let pics = statuses[i].querySelectorAll(".pics-wrapper img");
// console.log('pics', i, ':', pics);
if (pics && pics.length > 0) {
// console.log('status', i, 'adding pics');
for (let j = 0; j < pics.length; j++){
row[`img${j}`] = pics[j].src.replace('.webp','.jpg');
}
}
if (match) savedStatuses.push(row);
}
// save statuses in Dexie
// console.log('saved statuses', savedStatuses);
return savedStatuses;
}
// save into Dexie
const saveDexie = () => {
const db = new Dexie(DB_NAME);
db.version(1).stores({
status: `
id,sid,saying,full,uid,link,created
`
});
const statuses = backupStatuses();
db.status.bulkPut(statuses)
.then(() => {
console.log(`saved ${statuses.length} statuses.`);
db.status.toCollection().count(count => {
console.log(`Dexie current stores:`, count);
notification(`已保存 ${count} 个广播.`);
})
let nextPage = document.querySelector(".paginator span.next a").href;
const cont = localStorage.getItem("cont");
if (statuses.length > 0 && nextPage && cont !== null && Number(cont) > 0) {
nextPage = nextPage + `&cont=${Number(cont) - 1}`;
localStorage.setItem("cont", Number(cont) - 1);
window.location.href = nextPage;
}
}).catch(e => {
console.error("error:", e);
});
}
const clearDexie = () => {
Dexie.delete(DB_NAME).then(() => {
console.log("Database successfully deleted");
notification("已清空保存的广播.");
}).catch((err) => {
console.error("Could not delete database",err);
});
}
const exportCSV = () => {
const db = new Dexie(DB_NAME);
db.version(1).stores({
status: `
id,sid,saying,full,uid,link,created
`
});
let pack = db.status.orderBy('created').reverse();
pack.toArray().then(data => {
data = data.map(e => {~
delete e.id;
return e;
})
let title = ['广播内容','广播带前缀','广播id','用户id','链接','时间','图片1','图片2',
'图片3','图片4','图片5','图片6','图片7','图片8','图片9'];
let key = ['saying','full','sid','uid','link','created', 'img0', 'img1','img2',
'img3','img4','img5','img6','img7','img8'];
JSonToCSV.setDataConver({
data: data,
fileName: 'broadcastBackUp_' + new Date().toISOString(),
columns: {title, key}
})
// console.log("pack:", data);
})
}
// 导出CSV函数
// https://github.com/liqingzheng/pc/blob/master/JsonExportToCSV.js
let JSonToCSV = {
/*
* obj是一个对象,其中包含有:
* ## data 是导出的具体数据
* ## fileName 是导出时保存的文件名称 是string格式
* ## showLabel 表示是否显示表头 默认显示 是布尔格式
* ## columns 是表头对象,且title和key必须一一对应,包含有
title:[], // 表头展示的文字
key:[], // 获取数据的Key
formatter: function() // 自定义设置当前数据的 传入(key, value)
*/
setDataConver: function (obj) {
var bw = this.browser();
if (bw['ie'] < 9) return; // IE9以下的
var data = obj['data'],
ShowLabel = typeof obj['showLabel'] === 'undefined' ? true : obj['showLabel'],
fileName = (obj['fileName'] || 'UserExport') + '.csv',
columns = obj['columns'] || {
title: [],
key: [],
formatter: undefined
};
ShowLabel = typeof ShowLabel === 'undefined' ? true : ShowLabel;
var row = "",
CSV = '',
key;
// 如果要现实表头文字
if (ShowLabel) {
// 如果有传入自定义的表头文字
if (columns.title.length) {
columns.title.map(function (n) {
row += n + ',';
});
} else {
// 如果没有,就直接取数据第一条的对象的属性
for (key in data[0]) row += key + ',';
}
row = row.slice(0, -1); // 删除最后一个,号,即a,b, => a,b
CSV += row + '\r\n'; // 添加换行符号
}
// 具体的数据处理
data.map(function (n) {
row = '';
// 如果存在自定义key值
if (columns.key.length) {
columns.key.map(function (m) {
row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(m, n[m]) || n[m] || '' : n[m] || '') + '",';
});
} else {
for (key in n) {
row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(key, n[key]) || n[key] || '' : n[key] || '') + '",';
}
}
row = row.slice(0, row.length - 1); // 删除最后一个,
CSV += row + '\r\n'; // 添加换行符号
});
if (!CSV) return;
this.SaveAs(fileName, CSV);
},
SaveAs: function (fileName, csvData) {
var bw = this.browser();
if (!bw['edge'] || !bw['ie']) {
var alink = document.createElement("a");
alink.id = "linkDwnldLink";
alink.href = this.getDownloadUrl(csvData);
document.body.appendChild(alink);
var linkDom = document.getElementById('linkDwnldLink');
linkDom.setAttribute('download', fileName);
linkDom.click();
document.body.removeChild(linkDom);
} else if (bw['ie'] >= 10 || bw['edge'] == 'edge') {
var _utf = "\uFEFF";
var _csvData = new Blob([_utf + csvData], {
type: 'text/csv'
});
navigator.msSaveBlob(_csvData, fileName);
} else {
var oWin = window.top.open("about:blank", "_blank");
oWin.document.write('sep=,\r\n' + csvData);
oWin.document.close();
oWin.document.execCommand('SaveAs', true, fileName);
oWin.close();
}
},
getDownloadUrl: function (csvData) {
var _utf = "\uFEFF"; // 为了使Excel以utf-8的编码模式,同时也是解决中文乱码的问题
if (window.Blob && window.URL && window.URL.createObjectURL) {
csvData = new Blob([_utf + csvData], {
type: 'text/csv'
});
return URL.createObjectURL(csvData);
}
// return 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(csvData);
},
browser: function () {
var Sys = {};
var ua = navigator.userAgent.toLowerCase();
var s;
(s = ua.indexOf('edge') !== -1 ? Sys.edge = 'edge' : ua.match(/rv:([\d.]+)\) like gecko/)) ? Sys.ie = s[1] :
(s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
(s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
(s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
(s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
(s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
return Sys;
}
};
const init = () => {
// 自动翻页判断
if (location.href.indexOf('cont=') > -1) {
if (location.href.indexOf('cont=0') > -1){
saveDexie();
window.location.href = location.href.split("&cont=0")[0];
}
// 自动翻页保存
setTimeout(() => {
saveDexie();
}, 1500)
} else {
// 初次使用 && 自定义页面
clearStorage("cont");
injectStyle();
updateStatuses();
addScriptBtn();
}
}
setTimeout(() => {
if (document.querySelector('.pl2') !== null) {
init();
}
}, 500)
})();