Greasy Fork is available in English.

豆瓣读书+电影+音乐+游戏+舞台剧导出工具

将读过/看过/听过/玩过的读书/电影/音乐/游戏/舞台剧条目分别导出为 csv 文件

  1. // ==UserScript==
  2. // @name 豆瓣读书+电影+音乐+游戏+舞台剧导出工具
  3. // @namespace https://www.douban.com/people/MoNoMilky/
  4. // @version 0.4.4
  5. // @description 将读过/看过/听过/玩过的读书/电影/音乐/游戏/舞台剧条目分别导出为 csv 文件
  6. // @author Bambooom
  7. // @match https://book.douban.com/people/*/collect*
  8. // @match https://book.douban.com/people/*/wish*
  9. // @match https://movie.douban.com/people/*/collect*
  10. // @match https://movie.douban.com/people/*/wish*
  11. // @match https://music.douban.com/people/*/collect*
  12. // @match https://music.douban.com/people/*/wish*
  13. // @match https://www.douban.com/location/people/*/drama/collect*
  14. // @match https://www.douban.com/location/people/*/drama/wish*
  15. // @match https://www.douban.com/people/*
  16. // @require https://unpkg.com/dexie@latest/dist/dexie.js
  17. // @require https://cdn.bootcdn.net/ajax/libs/dexie/3.2.4/dexie.js
  18. // @grant none
  19. // @original-script https://openuserjs.org/scripts/KiseXu/%E8%B1%86%E7%93%A3%E7%94%B5%E5%BD%B1%E5%AF%BC%E5%87%BA%E5%B7%A5%E5%85%B7
  20. // @license MIT
  21. // ==/UserScript==
  22.  
  23.  
  24. (function () {
  25. 'use strict';
  26. var MOVIE = 'movie', BOOK = 'book', MUSIC = 'music', GAME = 'game', DRAMA = 'drama', people;
  27. /* global $, Dexie */
  28.  
  29. function getExportLink(type, people, isWish = false) { // type=book/movie/music, isWish=想读/想看/想听
  30. return `https://${type}.douban.com/people/${people}/${isWish ? 'wish' : 'collect'}?start=0&sort=time&rating=all&filter=all&mode=list&export=1`;
  31. }
  32.  
  33. function getGameExportLink(people, isWish = false) { // type=game
  34. return `https://www.douban.com/people/${people}/games?action=${isWish ? 'wish' : 'collect'}&start=0&export=1`;
  35. }
  36.  
  37. function getDramaExportLink(people, isWish = false) { // type=game
  38. return `https://www.douban.com/location/people/${people}/drama/${isWish ? 'wish' : 'collect'}?start=0&sort=time&mode=grid&rating=all&export=1`;
  39. }
  40.  
  41. if (location.href.indexOf('//www.douban.com/people/') > -1) {
  42. // 加入导出按钮
  43. let match = location.href.match(/www\.douban\.com\/people\/([^/]+)\//);
  44. people = match ? match[1] : null;
  45. $('#book h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getExportLink(BOOK, people) + '">导出读过的书</a>');
  46. $('#book h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getExportLink(BOOK, people, true) + '">导出想读</a>');
  47. $('#movie h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getExportLink(MOVIE, people) + '">导出看过的片</a>');
  48. $('#movie h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getExportLink(MOVIE, people, true) + '">导出想看</a>');
  49. $('#music h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getExportLink(MUSIC, people) + '">导出听过的碟</a>');
  50. $('#music h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getExportLink(MUSIC, people, true) + '">导出想听</a>');
  51. $('#game h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getGameExportLink(people) + '">导出玩过的游戏</a>');
  52. $('#game h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getGameExportLink(people, true) + '">导出想玩</a>');
  53. $('#drama h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getDramaExportLink(people) + '">导出看过的舞台剧</a>');
  54. $('#drama h2 .pl a:last').after('&nbsp;·&nbsp;<a href="' + getDramaExportLink(people, true) + '">导出想看</a>');
  55. }
  56.  
  57. if (location.href.indexOf('//www.douban.com/location/people/') > -1) { // for drama link
  58. let match = location.href.match(/www\.douban\.com\/location\/people\/([^/]+)\//);
  59. people = match ? match[1] : null;
  60. }
  61.  
  62. if (location.href.indexOf('//book.douban.com/') > -1 && location.href.indexOf('export=1') > -1) {
  63. init(BOOK, location.href.indexOf('wish') > -1);
  64. }
  65.  
  66. if (location.href.indexOf('//movie.douban.com/') > -1 && location.href.indexOf('export=1') > -1) {
  67. init(MOVIE, location.href.indexOf('wish') > -1);
  68. }
  69.  
  70. if (location.href.indexOf('//music.douban.com/') > -1 && location.href.indexOf('export=1') > -1) {
  71. init(MUSIC, location.href.indexOf('wish') > -1);
  72. }
  73.  
  74. if (people && location.href.indexOf('//www.douban.com/people/' + people + '/games') > -1 && location.href.indexOf('export=1') > -1) {
  75. init(GAME, location.href.indexOf('wish') > -1);
  76. }
  77.  
  78. if (people && location.href.indexOf('//www.douban.com/location/people/' + people + '/drama') > -1 && location.href.indexOf('export=1') > -1) {
  79. init(DRAMA, location.href.indexOf('wish') > -1);
  80. }
  81.  
  82. function escapeQuote(str) {
  83. return str.replaceAll('"', '""'); // " need to be replaced with two quotes to escape inside csv quoted string
  84. }
  85.  
  86. // 获取当前页数据
  87. function getCurPageItems(type, isWish = false) {
  88. var items = [];
  89.  
  90. var elems = $('li.item');
  91.  
  92. if (type === GAME) {
  93. elems = $('.game-list .common-item');
  94. } else if (type === DRAMA) {
  95. elems = $('.grid-view .item');
  96. }
  97.  
  98. elems.each(function (index) {
  99. var item = {
  100. title: escapeQuote($(this).find('.title a').text().trim()),
  101. link: $(this).find('.title a').attr('href').trim(),
  102. };
  103. if (!isWish) {
  104. item['rating_date'] = $(this).find('.date').text().trim().replaceAll('-', '/'); // 2020-07-17 => 2020/07/17
  105. if (type === GAME) {
  106. let rating = $(this).find('.rating-info .rating-star').attr('class');
  107. rating = rating
  108. ? (rating.slice(19, 20) === 'N' ? '' : Number(rating.slice(19, 20)))
  109. : '';
  110. item.rating = rating;
  111.  
  112. } else if (type === DRAMA) {
  113. let rating = $(this).find('.date')[0].previousElementSibling;
  114. if (rating) {
  115. rating = $(rating).attr('class').slice(6, 7);
  116. }
  117. item.rating = rating ? Number(rating) : '';
  118.  
  119. } else {
  120. item.rating = ($(this).find('.date span').attr('class')) ? $(this).find('.date span').attr('class').slice(6, 7) : '';
  121. }
  122. }
  123.  
  124. var co = $(this).find('.comment');
  125. if (co.length) {
  126. co = co[0];
  127. if (type === MOVIE) {
  128. // 电影条目在 collect 页面显示了有用数,eg “(1有用)”,所以需要单独提取 childNode 进行 trim,否则结果的 csv 里包含大量多余空格空行,很容易处理错误
  129. item.comment = co.firstChild.textContent.trim() + (co.firstElementChild ? co.firstElementChild.textContent.trim() : '');
  130. } else { // 图书及音乐条目没有显示 有用数
  131. item.comment = co.textContent.trim();
  132. }
  133. item.comment = escapeQuote(item.comment);
  134. } else if (type === GAME) {
  135. co = $(this).find('.user-operation');
  136. if (co.length) {
  137. co = co[0];
  138. item.comment = co.previousElementSibling.textContent.trim();
  139. item.comment = escapeQuote(item.comment);
  140. }
  141. } else if (type === DRAMA) {
  142. co = $(this).find('.opt-ln');
  143. if (co.length) {
  144. co = co[0].previousElementSibling;
  145. if ($(co).find('.date').length === 0) {
  146. item.comment = co.textContent.trim();
  147. }
  148. }
  149. }
  150.  
  151. if (type === GAME) {
  152. let extra = $(this).find('.desc')[0].firstChild.textContent.trim();
  153. item.release_date = extra.split(' / ').slice(-1)[0];
  154. items[index] = item;
  155. return; // for type=game, here is over
  156. }
  157.  
  158. if (type === DRAMA) {
  159. let extra = $(this).find('.intro')[0].textContent.trim();
  160. item.mixed_info = extra;
  161. items[index] = item;
  162. return; // for type=drama, here is over
  163. }
  164.  
  165. var intro = $(this).find('.intro').text().split(' / ');
  166. if (intro.length) {
  167. if (type === MOVIE) {
  168. intro = intro[0];
  169. var res = intro.match(/^(\d{4}-\d{2}-\d{2})\((.*)\)$/);
  170. if (res) {
  171. item.release_date = res[1].replaceAll('-', '/');
  172. item.country = res[2];
  173. }
  174. } else {
  175. // 不一定有准确日期,可能是 2009-5 这样的, 也可能就只有年份 2000
  176. var dateReg = /\d{4}(?:-\d{1,2})?(?:-\d{1,2})?/;
  177. if (!dateReg.test(intro[0])) { // intro 首项非日期,则一般为作者或音乐家
  178. if (type === BOOK) {
  179. item.author = escapeQuote(intro[0]);
  180. } else if (type === MUSIC) {
  181. item.musician = escapeQuote(intro[0]);
  182. }
  183. }
  184. var d = intro.filter(function(txt) {return dateReg.test(txt);});
  185. if (d.length) {
  186. item.release_date = d[0].replaceAll('-', '/');
  187. }
  188. }
  189. }
  190.  
  191. items[index] = item;
  192. });
  193.  
  194. return items;
  195. }
  196.  
  197. function init(type, isWish = false) {
  198. const db = new Dexie('db_export'); // init indexedDB
  199. const ver = isWish ? 2 : 1;
  200. if (type === MOVIE) {
  201. db.version(ver).stores({
  202. items: isWish
  203. ? '++id, title, release_date, country, link'
  204. : '++id, title, rating, rating_date, comment, release_date, country, link',
  205. });
  206. } else if (type === BOOK) {
  207. db.version(ver).stores({
  208. items: isWish
  209. ? '++id, title, release_date, author, link'
  210. : '++id, title, rating, rating_date, comment, release_date, author, link',
  211. });
  212. } else if (type === MUSIC) {
  213. db.version(ver).stores({
  214. items: isWish
  215. ? '++id, title, release_date, musician, link'
  216. : '++id, title, rating, rating_date, comment, release_date, musician, link',
  217. });
  218. } else if (type === GAME) {
  219. db.version(ver).stores({
  220. items: isWish
  221. ? '++id, title, release_date, link'
  222. : '++id, title, rating, rating_date, comment, release_date, link',
  223. });
  224. } else if (type === DRAMA) {
  225. db.version(ver).stores({
  226. items: isWish
  227. ? '++id, title, mixed_info, link'
  228. : '++id, title, rating, rating_date, comment, mixed_info, link',
  229. });
  230. }
  231.  
  232. const items = getCurPageItems(type, isWish);
  233. db.items.bulkAdd(items).then(function() {
  234. console.log('添加成功+', items.length);
  235.  
  236. let nextPageLink = $('.paginator span.next a').attr('href');
  237. if (nextPageLink) {
  238. nextPageLink = nextPageLink + '&export=1';
  239. window.location.href = nextPageLink;
  240. } else {
  241. exportAll(type, isWish);
  242. }
  243. }).catch(function (error) {
  244. console.error("Ooops: " + error);
  245. });
  246. }
  247.  
  248. function exportAll(type, isWish = false) {
  249. const db = new Dexie('db_export');
  250. const ver = isWish ? 2 : 1;
  251. if (type === MOVIE) {
  252. db.version(ver).stores({
  253. items: isWish
  254. ? '++id, title, release_date, country, link'
  255. : '++id, title, rating, rating_date, comment, release_date, country, link',
  256. });
  257. } else if (type === BOOK) {
  258. db.version(ver).stores({
  259. items: isWish
  260. ? '++id, title, release_date, author, link'
  261. : '++id, title, rating, rating_date, comment, release_date, author, link',
  262. });
  263. } else if (type === MUSIC) {
  264. db.version(ver).stores({
  265. items: isWish
  266. ? '++id, title, release_date, musician, link'
  267. : '++id, title, rating, rating_date, comment, release_date, musician, link',
  268. });
  269. } else if (type === GAME) {
  270. db.version(ver).stores({
  271. items: isWish
  272. ? '++id, title, release_date, link'
  273. : '++id, title, rating, rating_date, comment, release_date, link',
  274. });
  275. } else if (type === DRAMA) {
  276. db.version(ver).stores({
  277. items: isWish
  278. ? '++id, title, mixed_info, link'
  279. : '++id, title, rating, rating_date, comment, mixed_info, link',
  280. });
  281. }
  282.  
  283. let results = isWish ? db.items : db.items.orderBy('rating_date').reverse();
  284. results.toArray().then(function (all) {
  285. all = all.map(function(item) {
  286. delete item.id;
  287. return item;
  288. });
  289.  
  290. let title = isWish ? ['标题'] : ['个人评分', '打分日期', '我的短评'];
  291. let key = isWish ? ['title', 'release_date'] : ['title', 'rating', 'rating_date', 'comment', 'release_date'];
  292. if (type === MOVIE) {
  293. title.unshift('电影/电视剧/番组');
  294. title = title.concat(['上映日期', '制片国家', '条目链接']);
  295. key = key.concat(['country', 'link']);
  296. } else if (type === BOOK) {
  297. title.unshift('书名');
  298. title = title.concat(['出版日期', '作者', '条目链接']);
  299. key = key.concat(['author', 'link']);
  300. } else if (type === MUSIC) {
  301. title.unshift('单曲/专辑');
  302. title = title.concat(['发行日期', '音乐家', '条目链接']);
  303. key = key.concat(['musician', 'link']);
  304. } else if (type === GAME) {
  305. title.unshift('游戏名称');
  306. title = title.concat(['发行日期', '条目链接']);
  307. key.push('link');
  308. } else if (type === DRAMA) {
  309. title.unshift('舞台剧名称');
  310. title = title.concat(['混合信息', '条目链接']);
  311. key.pop();
  312. key = key.concat(['mixed_info', 'link']);
  313. }
  314.  
  315. JSonToCSV.setDataConver({
  316. data: all,
  317. fileName: 'db-' + type + '-' + (isWish ? 'wishlist-' : '') + new Date().toISOString().split('T')[0].replaceAll('-', ''),
  318. columns: {title, key},
  319. });
  320. db.delete();
  321. });
  322. }
  323.  
  324. // 导出CSV函数
  325. // https://github.com/liqingzheng/pc/blob/master/JsonExportToCSV.js
  326. var JSonToCSV = {
  327. /*
  328. * obj是一个对象,其中包含有:
  329. * ## data 是导出的具体数据
  330. * ## fileName 是导出时保存的文件名称 是string格式
  331. * ## showLabel 表示是否显示表头 默认显示 是布尔格式
  332. * ## columns 是表头对象,且title和key必须一一对应,包含有
  333. title:[], // 表头展示的文字
  334. key:[], // 获取数据的Key
  335. formatter: function() // 自定义设置当前数据的 传入(key, value)
  336. */
  337. setDataConver: function (obj) {
  338. var bw = this.browser();
  339. if (bw['ie'] < 9) return; // IE9以下的
  340. var data = obj['data'],
  341. ShowLabel = typeof obj['showLabel'] === 'undefined' ? true : obj['showLabel'],
  342. fileName = (obj['fileName'] || 'UserExport') + '.csv',
  343. columns = obj['columns'] || {
  344. title: [],
  345. key: [],
  346. formatter: undefined
  347. };
  348. ShowLabel = typeof ShowLabel === 'undefined' ? true : ShowLabel;
  349. var row = "",
  350. CSV = '',
  351. key;
  352. // 如果要现实表头文字
  353. if (ShowLabel) {
  354. // 如果有传入自定义的表头文字
  355. if (columns.title.length) {
  356. columns.title.map(function (n) {
  357. row += n + ',';
  358. });
  359. } else {
  360. // 如果没有,就直接取数据第一条的对象的属性
  361. for (key in data[0]) row += key + ',';
  362. }
  363. row = row.slice(0, -1); // 删除最后一个,号,即a,b, => a,b
  364. CSV += row + '\r\n'; // 添加换行符号
  365. }
  366. // 具体的数据处理
  367. data.map(function (n) {
  368. row = '';
  369. // 如果存在自定义key值
  370. if (columns.key.length) {
  371. columns.key.map(function (m) {
  372. row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(m, n[m]) || n[m] || '' : n[m] || '') + '",';
  373. });
  374. } else {
  375. for (key in n) {
  376. row += '"' + (typeof columns.formatter === 'function' ? columns.formatter(key, n[key]) || n[key] || '' : n[key] || '') + '",';
  377. }
  378. }
  379. row = row.slice(0, row.length - 1); // 删除最后一个,
  380. CSV += row + '\r\n'; // 添加换行符号
  381. });
  382. if (!CSV) return;
  383. this.SaveAs(fileName, CSV);
  384. },
  385. SaveAs: function (fileName, csvData) {
  386. var bw = this.browser();
  387. if (!bw['edge'] || !bw['ie']) {
  388. var alink = document.createElement("a");
  389. alink.id = "linkDwnldLink";
  390. alink.href = this.getDownloadUrl(csvData);
  391. document.body.appendChild(alink);
  392. var linkDom = document.getElementById('linkDwnldLink');
  393. linkDom.setAttribute('download', fileName);
  394. linkDom.click();
  395. document.body.removeChild(linkDom);
  396. } else if (bw['ie'] >= 10 || bw['edge'] == 'edge') {
  397. var _utf = "\uFEFF";
  398. var _csvData = new Blob([_utf + csvData], {
  399. type: 'text/csv'
  400. });
  401. navigator.msSaveBlob(_csvData, fileName);
  402. } else {
  403. var oWin = window.top.open("about:blank", "_blank");
  404. oWin.document.write('sep=,\r\n' + csvData);
  405. oWin.document.close();
  406. oWin.document.execCommand('SaveAs', true, fileName);
  407. oWin.close();
  408. }
  409. },
  410. getDownloadUrl: function (csvData) {
  411. var _utf = "\uFEFF"; // 为了使Excel以utf-8的编码模式,同时也是解决中文乱码的问题
  412. if (window.Blob && window.URL && window.URL.createObjectURL) {
  413. csvData = new Blob([_utf + csvData], {
  414. type: 'text/csv'
  415. });
  416. return URL.createObjectURL(csvData);
  417. }
  418. // return 'data:attachment/csv;charset=utf-8,' + _utf + encodeURIComponent(csvData);
  419. },
  420. browser: function () {
  421. var Sys = {};
  422. var ua = navigator.userAgent.toLowerCase();
  423. var s;
  424. (s = ua.indexOf('edge') !== -1 ? Sys.edge = 'edge' : ua.match(/rv:([\d.]+)\) like gecko/)) ? Sys.ie = s[1]:
  425. (s = ua.match(/msie ([\d.]+)/)) ? Sys.ie = s[1] :
  426. (s = ua.match(/firefox\/([\d.]+)/)) ? Sys.firefox = s[1] :
  427. (s = ua.match(/chrome\/([\d.]+)/)) ? Sys.chrome = s[1] :
  428. (s = ua.match(/opera.([\d.]+)/)) ? Sys.opera = s[1] :
  429. (s = ua.match(/version\/([\d.]+).*safari/)) ? Sys.safari = s[1] : 0;
  430. return Sys;
  431. }
  432. };
  433.  
  434. })();