// ==UserScript== // @name 堆糖网下载 // @namespace https://www.saintic.com/ // @version 1.1.4 // @description 堆糖网(duitang.com)专辑图片批量下载到本地 // @author staugur // @match http*://duitang.com/album/* // @match http*://www.duitang.com/album/* // @grant GM_setClipboard // @grant GM_info // @grant GM_download // @icon https://static.saintic.com/cdn/images/favicon-64.png // @license BSD 3-Clause License // @date 2018-06-26 // @modified 2020-11-13 // @github https://github.com/staugur/grab_huaban_board/blob/master/grab_duitang_album.js // @supportURL https://blog.saintic.com/blog/256.html // @downloadURL https://update.greasyfork.icu/scripts/369842/%E5%A0%86%E7%B3%96%E7%BD%91%E4%B8%8B%E8%BD%BD.user.js // @updateURL https://update.greasyfork.icu/scripts/369842/%E5%A0%86%E7%B3%96%E7%BD%91%E4%B8%8B%E8%BD%BD.meta.js // ==/UserScript== ;(function () { 'use strict' //字符串是否包含子串 function isContains(str, substr) { //str是否包含substr return str.indexOf(substr) >= 0 } //数组是否包含某元素 function arrayContains(arr, obj) { var i = arr.length while (i--) { if (arr[i] === obj) { return true } } return false } //判断页面中id是否存在 function hasId(id) { //有此id返回true,否则返回false var element = document.getElementById(id) if (element) { return true } else { return false } } //获取url查询参数 function getUrlQuery(key, acq) { /* 获取URL中?之后的查询参数,不包含锚部分,比如url为http://passport.saintic.com/user/message/?status=1&Action=getCount 若无查询的key,则返回整个查询参数对象,即返回{status: "1", Action: "getCount"}; 若有查询的key,则返回对象值,返回值可以指定默认值acq:如key=status, 返回1;key=test返回acq */ var str = location.search var obj = {} if (str) { str = str.substring(1, str.length) // 以&分隔字符串,获得类似name=xiaoli这样的元素数组 var arr = str.split('&') //var obj = new Object(); // 将每一个数组元素以=分隔并赋给obj对象 for (var i = 0; i < arr.length; i++) { var tmp_arr = arr[i].split('=') obj[decodeURIComponent(tmp_arr[0])] = decodeURIComponent( tmp_arr[1] ) } } return key ? obj[key] || acq : obj } //计算百分比 function calculatePercentage(num, total) { //小数点后两位百分比 return Math.round((num / total) * 10000) / 100.0 + '%' } //加载css文件 function addCSS(href) { var link = document.createElement('link') link.type = 'text/css' link.rel = 'stylesheet' link.href = href document.getElementsByTagName('head')[0].appendChild(link) } //加载js文件 function addJS(src, cb) { var script = document.createElement('script') script.type = 'text/javascript' script.src = src document.getElementsByTagName('head')[0].appendChild(script) script.onload = typeof cb === 'function' ? cb : function () {} } //时间戳转化为日期格式 function formatUnixtimestamp(unixtimestamp) { var unixtimestamp = new Date(unixtimestamp * 1000) var year = 1900 + unixtimestamp.getYear() var month = '0' + (unixtimestamp.getMonth() + 1) var date = '0' + unixtimestamp.getDate() var hour = '0' + unixtimestamp.getHours() var minute = '0' + unixtimestamp.getMinutes() var second = '0' + unixtimestamp.getSeconds() return ( year + '-' + month.substring(month.length - 2, month.length) + '-' + date.substring(date.length - 2, date.length) + ' ' + hour.substring(hour.length - 2, hour.length) + ':' + minute.substring(minute.length - 2, minute.length) ) } //加星隐藏部分 function setStarHidden(str) { if (str) { return str.substr(0, 4) + ' **** ' + str.substr(-4) } } //封装localStorage class StorageMix { constructor(key) { this.key = key this.obj = window.localStorage if (!this.obj) { console.error('浏览不支持localStorage') return false } } //设置或跟新本地存储数据 set(data) { if (data) { return this.obj.setItem(this.key, JSON.stringify(data)) } } //获取本地存储数据 get() { var data = null try { data = JSON.parse(this.obj.getItem(this.key)) } catch (e) { console.error(e) } finally { return data } } clear() { //清除对象 return this.obj.removeItem(this.key) } } //显示条款 function showTerms(cb, onlyShow = false) { let s = new StorageMix('userTermsVer') if (s.get() !== 'yes') { let html = [ '
', '本使用条款及免责声明(以下简称“本声明”)适用于', '所有用户脚本(以下简称“此脚本”),', '在您阅读本声明后若不同意此声明中的任何条款,', '或对本声明存在质疑,请立刻停止使用此脚本。', '若您已经开始或正在使用此脚本,则表示您已阅读并同意本声明的所有条款。', '
', '

总则:使用过程中请遵守所在国家或地区的相关法律法规。

', '

1. 此脚本使用localStorage存储公告、阅读条款状态等,不使用cookie技术。

', '

2. 此脚本不记录除远程方式外的下载情况,第三方Tdi下载与此脚本和作者无关。

', '

3. 此脚本使用BSD 3-Clause许可证开源,请遵循许可协议条款。

', '

4. 此脚本请个人使用,勿用于商业用途!

', '

5. 用户使用此脚本导致的版权、知识产权、所在网站本身侵权,此脚本作者概不负责!

', '

6. 用户须自己承担使用此脚本访问网站的风险,', '并承担为此而造成的风险责任,与作者本人及相关服务无关!

', '

7. 本声明可随时修改条款,如有变更通过公告发布,声明立时生效。

' ].join('') layer.open({ type: 1, title: '使用条款与免责声明', closeBtn: false, area: 'auto', shade: 0.7, shadeClose: false, id: 'userTerm', //设定一个id,防止重复弹出 btn: onlyShow !== true ? ['我同意', '我不同意'] : ['关闭'], btnAlign: 'c', scrollbar: false, content: '
' + html + '
', zIndex: layer.zIndex, success: function (layero) { layer.setTop(layero) }, yes: function (index, layero) { layer.close(index) if (onlyShow !== true) { s.set('yes') typeof cb === 'function' && cb() } } }) } else { if (onlyShow !== true) { typeof cb === 'function' && cb() } } } //由于@require方式引入jquery时layer使用异常,故引用cdn中jquery v1.10.1;加载完成后引用又拍云中layer v3.1.1 addJS('https://static.saintic.com/cdn/jquery/1.10.1/jquery.min.js', function () { $.noConflict() addJS('https://static.saintic.com/cdn/layer/3.1.1/layer.js') }) //正则 var isEmail = /^[\w.\-]+@(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,3}$/i var isMobile = /^1\d{10}$/i //设置提醒弹框 function setupRemind() { var email = getReceiveBy('email') || '', mobile = getReceiveBy('mobile') || '', token = getReceiveBy('token') || '' var content_overview = [ '
', '

堆糖网下载脚本功能设置,包括提醒、公告等。


', '
提醒功能旨在提交远程下载后,查询下载进度并在下载完成发送邮箱、短信、微信等消息,以供用户下载。
', '

    邮箱:' + (email || '未设置!') + '

', '

    手机:' + (mobile || '未设置!') + '

', '

    密钥:' + (setStarHidden(token) || '未设置!') + '

', '

    微信:采用本站公众号,关注后,发送"@下载链接"即可查询状态。

', '
公告功能目前支持清理缓存公告。
', '

    点击重置状态:此操作将已读公告标记为未读,下次请求后会重新展示公告。

', '

    重新阅读公告:手动查看堆糖网公告。

', '
帮助说明与反馈。
', '

    查看FAQ:关于设置方面的问题说明,请先阅读!

', '

    提交反馈:问题反馈或功能建议。

', '
使用条款与免责声明
', '
' ].join('') var content_remind = [ '
', '
保存邮箱

', '
保存手机

', '
保存密钥

', '

微信下载进度查询:

', '', '
' ].join('') var content_weixin = [ '
', '

微信下载进度查询:

', '

  请使用微信APP扫描此二维码并关注,发送"@下载链接"即可,服务器会返回下载进度。

', '', '
' ].join('') var content_help = [ '
', '

1. 什么是密钥?
  答:密钥是在您在诏预开放平台创建的Api Token,与用户一一对应,拥有它可以访问平台公共接口、处理您账号的相关事务等,此处仅作为您使用此脚本查询远端下载记录,以便及时下载完成的压缩包,省去了复制下载链接等步骤。切记密钥不可泄露,否则可能造成账号风险!

', '

2. 怎么创建密钥?
  答:请登录开放平台:https://open.saintic.com,在控制台处可以创建密钥(您可以使用QQ/微博/码云/GitHub等快捷登录)!

', '

3. 微信怎么查询下载进度?
  答:请使用微信APP扫描此二维码并关注,发送"@下载链接"即可,服务器会返回下载状态。

', '
' ].join('') layer.tab({ area: ['550px', '470px'], tab: [ { title: '概述', content: content_overview }, { title: '设置提醒', content: content_remind } ], success: function (layero, index) { var body = layer.getChildFrame('body', index) body.context.getElementById( 'save_remind_email' ).onclick = function () { var value = body.context.getElementById('set_remind_email') .value if (value && !isEmail.test(value)) { layer.msg('请输入正确的邮箱地址') return } setupReceiveTo('email', value) body.context.getElementById('overview_email').innerHTML = value || '已清空' } body.context.getElementById( 'save_remind_mobile' ).onclick = function () { var value = body.context.getElementById('set_remind_mobile') .value if (value && !isMobile.test(value)) { layer.msg('请输入正确的手机号') return } setupReceiveTo('mobile', value) body.context.getElementById('overview_mobile').innerHTML = value || '已清空' } body.context.getElementById( 'reset_notice_status' ).onclick = function () { var storage = new StorageMix('grab_duitang_album') storage.clear() layer.msg('重置成功', { icon: 1 }) } body.context.getElementById( 'reshow_notice' ).onclick = function () { var storage = new StorageMix('grab_duitang_album') storage.clear() showNotice() } body.context.getElementById( 'save_remind_token' ).onclick = function () { var value = body.context.getElementById('set_remind_token') .value setupReceiveTo('token', value) body.context.getElementById( 'overview_token' ).innerHTML = !value ? '已清空' : setStarHidden(value) } body.context.getElementById( 'grab_setting_help' ).onclick = function () { layer.open({ type: 1, title: 'FAQ', content: content_help, closeBtn: false, shadeClose: false, shade: 0, btn: '我知道了', btnAlign: 'c', zIndex: layer.zIndex, success: function (layero) { layer.setTop(layero) }, yes: function (index, layero) { layer.close(index) } }) } body.context.getElementById( 'reshow_userterms' ).onclick = function () { let s = new StorageMix('userTermsVer') s.clear() showTerms(null, true) } } }) } /** * 设置接收信息 * @param type 参数: mobile|email|token */ function setupReceiveTo(type, value) { var es = new StorageMix('grab_duitang_album_remind_email') var ms = new StorageMix('grab_duitang_album_remind_mobile') var ts = new StorageMix('grab_duitang_album_token') if (type === 'email') { if (value) { if (!isEmail.test(value)) { layer.msg('请输入正确的邮箱地址') return } es.set(value) layer.msg('邮箱:' + value + ',设置成功!', { icon: 1 }) } else { es.clear() layer.msg('邮箱已清空!', { icon: 1 }) } } else if (type === 'mobile') { if (value) { if (!isMobile.test(value)) { layer.msg('请输入正确的手机号') return } ms.set(value) layer.msg('手机号:' + value + ',设置成功!', { icon: 1 }) } else { ms.clear() layer.msg('手机号已清空!', { icon: 1 }) } } else if (type === 'token') { if (!value) { ts.clear() layer.msg('密钥已清空!', { icon: 1 }) } else { ts.set(value) layer.msg('密钥:' + value + ',设置成功!', { icon: 1 }) } } else { layer.msg('暂不支持此方式!') return } } /** * 读取接收信息值 * @param type 参数: mobile|email|token */ function getReceiveBy(type) { var str = '', es = new StorageMix('grab_duitang_album_remind_email'), ms = new StorageMix('grab_duitang_album_remind_mobile'), ts = new StorageMix('grab_duitang_album_token') if (type === 'email') { str = es.get() } else if (type === 'mobile') { str = ms.get() } else if (type === 'token') { str = ts.get() } return str || '' } /* 下载用户专辑接口 */ //交互确定专辑下载方式 function interactiveAlbum(album_id, pins, pin_number, user_id) { var downloadMethod = 0, msg = [ '
当前专辑共' + pin_number + '张图片,抓取了' + pins.length + '张,抓取率:' + calculatePercentage(pins.length, pin_number) + '!
', '请选择以下三种下载方式:
', '1. 文本
    即所有图片地址按行显示,提供复制,粘贴至迅雷、QQ旋风等下载工具批量下载即可,推荐使用此方法。
', '2. 本地
    即所有图片直接保存到硬盘中,由于是批量下载,所以浏览器设置中请关闭"下载前询问每个文件的保存位置",并且允许浏览器下载多个文件的授权申请,以保证可以自动批量保存,否则每次保存时会弹出询问,对您造成困扰。
', '3. 远程
    即所有图片将由远端服务器下载并压缩,提供压缩文件链接,直接下载此链接解压即可。
', '

寻求帮助?请点击我!

' ].join('') layer.open({ type: 1, title: '选择专辑图片下载方式', content: msg, closeBtn: false, shadeClose: false, shade: 0, btn: ['文本', '本地', '远程'], btnAlign: 'c', zIndex: layer.zIndex, success: function (layero) { layer.setTop(layero) }, yes: function (index, layero) { //文本方式下载,比如迅雷、QQ旋风 downloadMethod = 1 layer.close(index) layer.open({ type: 1, title: '文本方式下载', content: '
请点击复制按钮,粘贴到迅雷等下载!
', closeBtn: false, shadeClose: false, shade: 0, btn: '复制', btnAlign: 'c', maxmin: true, zIndex: layer.zIndex, success: function (layero) { layer.setTop(layero) }, yes: function (index, layero) { layer.close(index) GM_setClipboard( pins .map(function (pin) { return pin.imgUrl + '\n' }) .join('') ) layer.msg('复制成功', { icon: 1 }) } }) }, btn2: function (index, layero) { //本地下载 downloadMethod = 2 layer.close(index) pins.map(function (pin) { GM_download(pin.imgUrl, pin.imgName) }) }, btn3: function (index, layero) { //远端下载 downloadMethod = 3 layer.close(index) // 提醒接收配置信息读取 var email = getUrlQuery('email', getReceiveBy('email')) var mobile = getUrlQuery('sms', getReceiveBy('mobile')) jQuery.ajax({ url: 'https://open.saintic.com/CrawlHuaban/', type: 'POST', data: { site: 2, version: GM_info.script.version, board_total: pin_number, board_id: album_id, user_id: user_id, pins: JSON.stringify(pins), email: email, sms: mobile }, beforeSend: function (request) { request.setRequestHeader( 'Authorization', 'Token ' + getReceiveBy('token') ) }, success: function (res) { if (res.success === true) { var msg = [ '
下载任务已经提交!
根据专辑图片数量,所需时间不等,请稍等数分钟后访问下载链接:
', res.downloadUrl + '
它将于', res.expireTime + '过期,那时资源会被删除,请提前下载。', res.tip + '
' ].join('') layer.open({ type: 1, title: '温馨提示', content: msg, closeBtn: false, shadeClose: false, shade: 0, area: '390px', btn: '我已知晓并复制下载链接', btnAlign: 'c', maxmin: true, zIndex: layer.zIndex, success: function (layero) { layer.setTop(layero) }, yes: function (index, layero) { layer.close(index) GM_setClipboard(res.downloadUrl) var tips = '复制成功!' if (email) { tips += ' 接收提醒邮箱:' + email } if (mobile) { tips += ' 接收提醒手机:' + mobile } layer.msg(tips, { icon: 1 }) } }) } else { layer.msg('远端服务提示: ' + res.msg, { icon: 2, time: 8000 }) } } }) } }) } //专辑解析与下载 function downloadAlbum(album_id) { if (album_id) { console.group('堆糖网下载-当前专辑:' + album_id) var limit = 100, user_id = '' //get first pin data jQuery.ajax({ url: 'https://www.duitang.com/napi/blog/list/by_album/?album_id=' + album_id + '&limit=' + limit + '&start=0&_=' + Math.round(new Date()), async: false, success: function (res) { try { //console.log(res); if (res.hasOwnProperty('data') === true) { var album_data = res.data, pin_number = album_data.total, board_pins = album_data.object_list, retry = Math.ceil(pin_number / limit) console.debug( 'Current album <' + album_id + '> album number is ' + pin_number + ', first get number is ' + board_pins.length ) if (board_pins.length < pin_number) { var next_start = album_data.next_start while (1 <= retry) { //get ajax pin data jQuery.ajax({ url: 'https://www.duitang.com/napi/blog/list/by_album/?album_id=' + album_id + '&limit=' + limit + '&start=' + next_start + '&_=' + Math.round(new Date()), async: false, success: function (res) { //console.log(res); var album_next_data = res.data board_pins = board_pins.concat( album_next_data.object_list ) console.debug( 'ajax load album with next_start ' + next_start + ', get number is ' + album_next_data.object_list .length + ', merged' ) if ( album_next_data.object_list .length === 0 ) { retry = 0 return false } next_start = album_next_data.next_start } }) retry -= 1 } } if (board_pins.length > 0) { user_id = board_pins[0].sender_id } console.log( '用户:' + user_id + '的专辑' + album_id + '共抓取' + board_pins.length + '个图片' ) var pins = board_pins.map(function (pin) { return { imgUrl: pin.photo.path, imgName: pin.id + '.' + pin.photo.path.split('.')[ pin.photo.path.split('.').length - 1 ] } }) //交互确定下载方式 interactiveAlbum( album_id, pins, pin_number, user_id ) } } catch (e) { console.error(e) } } }) console.groupEnd() } } //获取公告接口 function showNotice() { jQuery.ajax({ url: 'https://open.saintic.com/CrawlHuaban/notice?catalog=3', type: 'GET', success: function (res) { if (res.code === 0) { var notices = res.data if (notices.length > 0) { var storage = new StorageMix('grab_duitang_album') var localIds = storage.get() || [] var html = '' notices.map(function (notice) { //notice{id, ctime, content} if (!arrayContains(localIds, notice.id) === true) { localIds.push(notice.id) html += '

@' + formatUnixtimestamp(notice.ctime) + ' 【 ' + notice.content + ' 】

' } }) storage.set(localIds) if (!html) { return false } layer.open({ type: 1, title: '诏预开放平台公告', closeBtn: false, area: 'auto', shade: 0, id: 'grab_huaban_board', //设定一个id,防止重复弹出 resize: true, maxmin: true, btn: ['我知道了'], btnAlign: 'c', moveType: 1, //拖拽模式,0或者1 content: '
' + html + '
', yes: function (index, layero) { layer.close(index) } }) } } } }) } /* 主入口,分出不同模块:用户、专辑 */ var album_id = getUrlQuery('id') if (album_id != undefined && /^[0-9]*$/.test(album_id)) { //当前在专辑地址下 var board_text = '下载此专辑', setup_text = '堆糖网设置' //当前是PC版,不予支持Mobile版 var caa = document .getElementById('content') .getElementsByClassName('album-action')[0] //插入下载专辑按钮 if (isContains(caa.innerText, board_text) === false) { var tmpHtml = '' + setup_text + '' + '' + board_text + '' caa.style.width = 'auto' caa.insertAdjacentHTML('afterbegin', tmpHtml) } // 监听设置提醒按钮 document.getElementById('setupRemind').onclick = function () { setupRemind() } //监听专辑点击下载事件 document.getElementById('downloadAlbum').onclick = function () { showTerms(function () { showNotice() downloadAlbum(album_id) }) } } })()