daum漫画图片下载

一键下载Daum的整话漫画

// ==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;
}