// ==UserScript==
// @name daum漫画图片下载
// @namespace http://weibo.com/liangxiafengge/
// @version 0.4.2
// @description 一键下载Daum的整话漫画
// @author CW2012
// @icon http://s1.daumcdn.net/photo-section/-cartoon10/favicon/201312/favicon.ico
// @match http*://webtoon.daum.net/webtoon/view/*
// @match http*://webtoon.daum.net/webtoon/viewer/*
// @match http*://webtoon.daum.net/league/viewer/*
// @match http*://webtoon.daum.net/league/view/*
// @connect http://webtoon.daum.net/
// @connect http://t1.daumcdn.net/
// @require https://cdn.bootcdn.net/ajax/libs/jszip/3.5.0/jszip.min.js
// @grant GM_xmlhttpRequest
// @grant GM_download
// @run-at document-end
// ==/UserScript==
/****************************************************************************
超级坑爹的一点:
千万不要在上面的配置区里使用tab键,否则会出现配置失败的情况
// @grant(tab) GM_xmlhttpRequest
我就是无意中在这里点了一下tab键,它又不显示出来,导致我以为tab键被自动清除了
但是它居然还TM在!!!!!!!!!!!!!!!!!!!!!!!!!!!!
最后grant的是tab,而不是GM_xmlhttpRequest
还特么报错说“而不是GM_xmlhttpRequest is not defined”靠!
****************************************************************************/
let downloadCount = 0; // 下载一话时,这一话的图片数目
let picCount;
let txt = '';
let finished = true; // 是否完成下载作业
let zip;
let eposideName;
let comicType;
(function() {
'use strict';
let tmpStr = location.href.split('#')[0];
tmpStr = tmpStr.split('\/');
comicType = tmpStr[3];
if(tmpStr[4]=='view'){
// 漫画列表页
downloadFromEposideList();
}else if(tmpStr[4]=='viewer'){
// 漫画阅读页
downloadFromRead();
}
})();
function downloadFromEposideList(){
// 考虑到页面加载数据还需要一段时间,如果需要依附的元素还没生成,下面的添加按钮的动作将无法执行
if(document.querySelectorAll('.clear_g.list_update>li').length == 0){
// 这里上列表还没加载进来时的逻辑
// 奇怪,就算不请求任何数据,这个元素也是存在的,为什么没有触发元素改变的事件???
let eposideList = document.querySelector(comicType =='league'?'#episodeList .list_update':'.clear_g.list_update');
// 监听eposideList元素,如果数据加载进来了,它的li子元素的个数会大于0
// 监听子元素变化需要开启childList,attributes是监听本元素的变化,如class变化等
new MutationObserver(eposideListChanged).observe(eposideList, {
attributes: false,
childList: true,
subtree: false
});
return;
}
}
// 发生数据加载时,添加按钮
function eposideListChanged(){
// 等待一秒钟,让元素全部添加到网页上后,再添加我们的按钮
setTimeout(()=>{
let smallPicList= document.querySelectorAll(comicType =='league'?'#episodeList .list_update>li':'.clear_g.list_update>li');
if(smallPicList.length == 0){
return;
}
// 按钮应该出现的位置:缩略图所在的元素
smallPicList.forEach((item,index)=>{
let btnParent = item.lastElementChild;
btnParent.style.display='flex';
btnParent.style.diapley = 'flex';
btnParent.style.justifyContent = 'space-between';
// 分割线
let line = document.createElement('span');
line.className = 'ico_comm ico_bar';
btnParent.appendChild(line);
// 问题来了,收费的篇章是不能下载的,那么,不如收费篇章就不要添加下载按钮好了
if(btnParent.children.length>2){
return;
}
// 创建一个“下载”锚点(a),并美化亿下
let btn = document.createElement('a');
btn.innerText = '下载这一话';
btn.style.background ='#e83d3d'; //使用和原网页一样的配色,没有突兀感
btn.style.color = '#fff';
btn.style.padding = '3px';
btn.style.borderRadius = '3px';
btn.style.textDecoration='none';// 不显示下划线
btn.style.cursor = 'pointer'; // 鼠标悬浮时,指针变成手形,让用户知道可以点击
btn.addEventListener('click', e=>{
// 获得这一话的ID,并根据ID下载
if(!finished){
alert('上一个下载作业还未完成,请等待完成后继续');
return;
}
getEposideInfoAndDownload(item.firstElementChild.href.split('/')[5]);
});
btnParent.appendChild(btn); //添加到网页中
});
},1000);
}
// 根据这一话的ID,下载一整话
function downloadEposide(eposideId){
// 一话的链接,请求API
let tmp = (comicType == 'league')?'leaguetoon':'webtoon';
let url = `http://webtoon.daum.net/data/pc/${tmp}/viewer_images/${eposideId}`;
GM_xmlhttpRequest({
method: "GET",
url: url,
onload: function(res){
if(res.status === 200){
// 分析所得数据,并下载图片
let response = JSON.parse(res.responseText); // 这里返加的是文本,需要转换成JSON对象才能用
if(parseInt(response.result.status) == 200){
let data = response.data;
// data虽然是Array,但是不能用foreach??
picCount = data.length;
downloadCount = 0;
zip = new JSZip();
finished = false;
txt = '';
for(let i=0;i<data.length;i++){
// 下载单幅图片
let item = data[i];
if(item.mediaType == 'image') {
downloadSinglePic(item.url, item.imageOrder);
} else{
++downloadCount;
txt +=`编号${i+1},`;
// downloadSingleMp4(item.url, item.imageOrder);
}
}
}else{
console.log(`ID为${eposideId}的这一话下载失败`);
}
}else{ console.log(`ID为${eposideId}的这一话下载失败`); }
},
onerror : function(err){ console.log(`ID为${eposideId}的这一话下载失败,原因为${err}`); }
});
}
// 下载单幅图片
async function downloadSinglePic(picUrl, picNum){
GM_xmlhttpRequest({
method: "GET",
url: picUrl,
responseType:'blob',
onload: function(res){
const imageBlob = res.response;
let picName = `${picNum<10? '0':''}${picNum}.jpg`;
const imgData = new File([imageBlob], picName);
zip.file(picName, imgData, { base64: true });
zip.generateAsync({ type: 'blob' }).then(function(content) {
progress(++downloadCount);
if(downloadCount>=picCount){
if(txt!=''){
txt = txt.substr(0,txt.length-1);
zip.file('部分无法下载的文件(非图片)列表.txt', txt);
zip.generateAsync({type: 'blob'}).then(
contentWithTxt=>
downloadFileByBlob(contentWithTxt, eposideName)
);
return;
}
downloadFileByBlob(content, eposideName);
// delete(document.getElementById('progressBar'))
}
});
},
onerror: ()=>{console.error('下载图片出错')}
});
}
// 其实是错误的
function downloadSingleMp4(picUrl, picNum){
// tamperMonkey的官方文档,参数说明 https://www.tampermonkey.net/documentation.php
// 下载失败、超时的每一张图片单独提醒用户
// 下载成功不打开“另存为”窗口
GM_download({
url:picUrl,
name: `${picNum<10? '0':''}${picNum}.mp4`,
saveAs:false,
onload:()=>{
// 每下载成功一张图片减一,减到0时,表示这一话已经全部下载完成
progress(++downloadCount);
if(downloadCount>=picCount){
// 但是alert弹窗的样式非常得不银杏化,因此我们来美化一下
toast(1, '这一话的所有图片全部处理完成')
}
},
onerror: ()=>{
toast(-1, `下载第${picNum}个视频时出错,请按Ctrl+shift+I打开开发者工具自行下载`)
console.log(`下载第${picNum}幅视频时出错,它的下载链接是:\n${picUrl}`);
},
ontimeout: ()=>{
toast(-1,`下载第${picNum}幅视频时超时,请按Ctrl+shift+I打开开发者工具自行下载`)
console.log(`下载第${picNum}幅视频时超时,它的下载链接是:\n${picUrl}`);
}
});
}
// 显示自定义的弹窗
// type 消息类型:1成功,0提示,-1错误
// msg消息体
const colors = ['rgba(7 123 11 / 73%)','rgb(8 99 3 / 73%)','rgba(7 135 247 / 73%)'];
function toast(type, msg){
let toastBox = document.createElement('div');
toastBox.style.position = 'fixed';
toastBox.style.background='white';
toastBox.style.borderRadius='10px';
toastBox.style.background= colors[type+1];
toastBox.style.boxShadow='rgb(25 25 25) 1px 1px 10px 1px';
toastBox.innerText=msg;
toastBox.style.color = '#fff';
toastBox.style.bottom='12vh'; // 显示的位置不能离底部太低了,会被其他元素遮挡
toastBox.style.left='2vh';
toastBox.style.transition='1.5s';
toastBox.style.padding = '10px';
document.body.appendChild(toastBox);
// 6秒后自动隐藏
setTimeout(()=>{
toastBox.style.opacity = '0';
delete(toastBox);
}, 6000);
}
// 显示下载进度
function progress(prog){
let bar = document.getElementById('progressBar');
if(bar){
bar.innerText = `正在下载:${prog}/${picCount}`;
if(prog == -1){
bar.remove();
}
return;
}
let progressBox = document.createElement('div');
progressBox.id = 'progressBar';
progressBox.style.position = 'fixed';
progressBox.style.background='white';
progressBox.style.borderRadius='10px';
progressBox.style.background= colors[2];
progressBox.style.boxShadow='0 8px 16px 0 rgba(0,0,0,.2), 0 6px 20px 0 rgba(0,0,0,.19)';
progressBox.innerText=`正在下载:0/${picCount}`;
progressBox.style.color = '#fff';
progressBox.style.bottom='200px'; // 显示的位置不能离底部太低了,会被其他元素遮挡
progressBox.style.left='2vh';
progressBox.style.transition='1.5s';
progressBox.style.padding = '10px';
document.body.appendChild(progressBox);
}
// 在阅读页上添加下载的功能
function downloadFromRead(){
// 先添加一个悬浮按钮???
// 创建一个“下载”锚点(a),并美化一下
let btn = document.createElement('a');
btn.innerText = '下载这一话';
btn.style.position = 'fixed';
btn.style.left = '20px';
btn.style.top = '45vh';
btn.style.background ='#e83d3d'; //使用和原网页一样的配色,没有突兀感
btn.style.color = '#fff';
btn.style.padding = '13px';
btn.style.fontSize= '20px';
btn.style.boxShadow='gray 1px 1px 35px 0';
btn.style.borderRadius = '13px';
btn.style.textDecoration='none';// 不显示下划线
btn.style.cursor = 'pointer'; // 鼠标悬浮时,指针变成手形,让用户知道可以点击
btn.addEventListener('click', e=>{
// 获得这一话的ID,并根据ID下载,跟上面的逻辑是一模一样的
if(!finished){
alert('上一个下载作业还未完成,请等待完成后继续');
return;
}
let eposideId = location.href.split('/')[5];
getEposideInfoAndDownload(eposideId);
});
document.body.appendChild(btn); //添加到网页中
}
// 获得这一话的信息
function getEposideInfoAndDownload(eposideId){
picCount = '正在获取....';
progress(0);
let tmp = (comicType == 'league')?'leaguetoon':'webtoon';
// http://webtoon.daum.net/data/pc/webtoon/viewer/93187?timeStamp=1607874553918
GM_xmlhttpRequest({
method: "GET",
url: `http://webtoon.daum.net/data/pc/${tmp}/viewer/${eposideId}`,
onload: function(res){
if(res.status === 200){
let data = JSON.parse(res.responseText);
if(data.result.status == 200){
data = data.data;
let prop = (comicType == 'league')?'leaguetoon':'webtoonEpisode';
eposideName = data[prop].title;
downloadEposide(eposideId);
}else{
alert('获取信息出错')
}
}
}
});
}
// 利用blob下载文件
function downloadFileByBlob(blobContent, filename) {
const blobUrl = URL.createObjectURL(blobContent)
const eleLink = document.createElement('a')
eleLink.download = filename
eleLink.style.display = 'none'
eleLink.href = blobUrl
eleLink.click();
progress(-1);
finished = true;
}