#!/usr/bin/python
# -*- coding: utf-8 -*-
import shutil
import requests
import re
import os
import commands
import multiprocessing as mup
from threading import Thread
import time
import threading

# 创建一个线程锁
threadLock = threading.Lock()

# 下载请求头,没有问题不要修改此内容
headers = {
    'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Mobile Safari/537.36',
    'Referer': 'https://bestdori.com/',
    'Host': 'bestdori.com'
}

# 立绘所在的网页根目录,默认为日服,如果下国服可以将下面url中的jp改成cn,其他服原理相同
url = 'https://bestdori.com/assets/jp/characters/resourceset/'

# 立绘id所属的标签信息,在全局变量的url所打开的网页的源代码中搜索id为res001001的立绘目录,
# 能找到所属标签为:<span data-v-4cb50f40="" class="m-l-xs">res001001</span>
# 那么此处应该填写data-v-4cb50f40
cgIDInfo = 'data-v-4cb50f40'

# 立绘id所在网页的源代码文件(html文件,可以只有包含立绘id的部分),程序使用正则表达式获取所有立绘的id
# 如果url中指定的区服有了改变,需要根据url的网页源代码重新提取相关内容填写到该文件中
# 不要改动该值,但需要改动该值对应的文件内容
htmlMsgFileName = 'htmlMsg.txt'

# 最大同时运行的线程数量
# 建议设置为2的n次方,如2,4,8,16,32,128,256
# 适当提高该值有助于提高下载速度,但不建议设置的太大,对网站的压力也许会很大
# 看不懂本说明可以不改此值
maxThread = 128

# 全局变量,计算当前共运行了几个线程
# 用户不可手动调整该值
nowthread = 0

# 立绘保存的根目录,默认为card,表示保存在程序所在目录的card文件夹下
# 用户可以自己设定此值,但如果想自定义子目录则需要修改源代码。
# 支持相对路径和绝对路径,相对路径如:card\\test,绝对路径如:D:\\card,路径最后都不要添加\\
saveRootDir = 'card'

# 特训前的立绘的图片的通用类型名
normal = 'card_normal.png'
# 特训前的立绘(只保留人物)的图片的通用类型名
trim_normal = 'trim_normal.png'
# 特训后的立绘的图片的通用类型名
special = 'card_after_training.png'
# 特训后的立绘(只保留人物)的图片的通用类型名
trim_special = 'trim_after_training.png'

# 需要下载哪些类型的立绘,默认下载全部类型,如果未来有其他类型也可在此添加
# 如:只需要下载特训后的立绘(不包括只留人物的),可以改为“downLoadType=[special]”,
# 如:需要下载特训前的所有立绘(包括只留人物的),可以改为“downLoadType=[normal,trim_normal]”
needDownLoadTypeNameList = [normal, special]

# 全局变量,写字符串到日志文件用,初始随便赋值
logFileObj = 0
# 错误日志文件名
errorLogFileName = 'error.txt'
# 下载失败列表,用于单独下载
downFailedList = []


def downloadOnePicture(cgId, pictureTypeName):
    """
    从指定url中下载指定类型的立绘CG到本地目录,
    url由固定前缀+立绘id+立绘所属图片类型名称三个要素决定。
    :param cgId:立绘的id,如res0010001
    :param pictureTypeName:某类图片的通用名称,如特训前卡片为'card_normal.png'
    :return:void
    """
    global nowthread, logFileObj
    global saveRootDir, url
    # 拼接完整的url,拼接规则来自网页下载任意一张图片后查看下载地址得到
    fullUrl = url + cgId + '_rip/' + pictureTypeName
    try:
        print('now thread num:' + str(nowthread) + ' start get:' + fullUrl)
        # 发起get请求,获得图片
        data = requests.get(fullUrl, headers=headers)
        # 获得图片存储路径
        savePath = saveRootDir + '\\' + cgId[0:6] + '\\'
        # 图片命名规则:该图片所属的目录名(也叫立绘ID)+图片所属的类型(如特训前,特训后等)
        pictureFullName = cgId + '_' + pictureTypeName
        # print 'need save path:' + savePath + pictureFullName
        # print 'save Name:'+pictureFullName
        # 如果获取到的图片大小小于100kb,则不保存到本地,否则将内存中的图片保存为文件
        if len(data.content) > 100000:
            # 调用公共方法open完成文件写入
            # 'wb'参数:以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
            with open(savePath + pictureFullName, 'wb') as file:
                file.write(data.content)
                file.close()
            print('save end, path:' + savePath + pictureFullName)
        else:
            print('less 100kb,don\'t save:' + savePath + pictureFullName)
    except Exception as e:
        errorMsg = 'pass this pic:' + fullUrl + 'because of error:\n' + str(e)
        print(errorMsg)
        downFailedList.append(cgId)
        logFileObj.write(errorMsg)
        logFileObj.flush()
    finally:
        # 使用线程锁保证下面的代码一并运行
        threadLock.acquire()
        # 执行完成后需要减少线程计数
        nowthread -= 1
        # 打印日志时展示现在还在运行的线程数量
        print('now thread num:' + str(nowthread) + ' end get:' + fullUrl)
        # 释放线程锁
        threadLock.release()


def startDownLoadOneTypeAllPictureByMutThread(cgIdList, pictureTypeName, maxThread):
    """
    发起对某个指定类型的全部立绘图片的下载,
    采取多线程下载的方式,减少每次下载时等待服务器响应时间较长的问题。
    :param cgIdList:从网页上获取到的立绘存储目录名列表,例如res001001,res001002等
    :param pictureTypeName:立绘所属类型的通用名称,如特训后的图片都叫'card_after_training.png'
    :param maxThread:可以最多同时运行的线程数量
    :return:void
    """
    global nowthread, logFileObj
    # 循环获取全部立绘的id
    for id in cgIdList:
        # 实时统计,在启动新线程前将已用线程数+1(相当于占坑)
        nowthread += 1
        try:
            # 无限循环,检查当前正在运行的线程数量是否达到设定的上限
            # 没有达到则启动新的线程,执行一张立绘图片的下载任务,启动新线程成功后跳出无限循环,否则持续循环下去,直到能够运行新线程位为止
            while 1:
                # 线程暂停0.01秒,防止执行过快导致的各类问题如崩溃
                time.sleep(0.01)
                # 一旦发现有空余则开启下载,否则无限循环等待
                if 0 <= nowthread <= maxThread:
                    # 立刻启动新线程,调用真正的下载方法,传入图片所属id和图片类型,完整下载
                    Thread(target=downloadOnePicture, args=(id, pictureTypeName)).start()
                    # 能启动新线程则强制跳出无限循环
                    break;
                # else:
                #     if threadLock.acquire(True):  # 2、获取锁状态,一个线程有锁时,别的线程只能在外面等着
                #         print("it's have " + str(self._nowthread) + " thread in running")
                #         # 注意,上面的nowthread的输出值可能为if表达式成功时的值,这是因为线程运行的随机性,因为在判断if时的该值和在print时的该值可能因为线程运行缘故导致不同
                #         # 这里的nowthread值仅能提示在print这一瞬间有几个线程还在运行,没有结束
                #         # print需要上线程锁,否则可能会和线程函数的print产生冲突导致输出错乱()即一个print执行到一半,另一个print开始执行,结果是两个print的输出值混在一起
                #         threadLock.release()  # 3、释放锁
        except Exception as e:
            errorMsg = 'start new thread to downLoad error:\n' + str(e)
            print(errorMsg)
            logFileObj.write(errorMsg)
            logFileObj.flush()
            # 发生exception则释放已经占用的线程数量
            nowthread -= 1

    # 所有立绘都加入下载队列后,主进程在此无限循环等待,直到所有线程都完成后主进程才继续,防止出现主进程已完成但某个线程未完成的情况
    print('\nwaitting picture type:"' + pictureTypeName + '" all download jobs over......')
    while 1:
        time.sleep(0.1)
        if nowthread <= 0:
            break
    print('\npicture type:"' + pictureTypeName + '" all download jobs end')


def autoCreateDir(cgIdList):
    """
    在电脑上建立目录,用于存放立绘图片。
    目录建立方式是一个根目录,一组子目录,一个队员的所有立绘会放在同一个子目录下,35个队员会有35个子目录,
    如果两组立绘的id为res001001,res001002,那么本程序会认为他们属于同一个队员的立绘,会生成res001目录用于存放这两组立绘
    :param cgIdList:从网页上获取到的立绘存储目录名列表,例如res001001,res001002等
    :return:void
    """
    global saveRootDir, logFileObj
    # 只判断最后一级目录是否存在
    if os.path.isdir(saveRootDir):
        # 清除已经创建的目录
        print('root dir exists,start recreate')
        try:
            shutil.rmtree(path=saveRootDir)
            print('delete success.')
        except Exception as e:
            errorMsg1 = 'delete root dir "' + saveRootDir + '" error:' + str(e)
            errorMsg2 = 'please delete "' + saveRootDir + '" by yourself and restart this program'
            print(errorMsg1)
            print(errorMsg2)
            logFileObj.write(errorMsg1)
            logFileObj.write(errorMsg2)
            logFileObj.flush()
    # 重建根目录
    os.mkdir(saveRootDir)

    # 重建立绘子目录,根据35个队员的id创建
    for cgID in cgIdList:
        # “cgID[0:6]”截取前段半部分的信息作为一个目录,比如一组立绘的CG id为res001001,那么截取后得到res001,
        # 这样所有001号队员的立绘都能放在一个文件夹里,这样会生成35个子文件夹(如果之后不再增加队员数量)
        cgSaveFullPath = saveRootDir + '\\' + cgID[0:6] + '\\'
        if not os.path.isdir(cgSaveFullPath):  ##不用加引号,如果是多级目录,只判断最后一级目录是否存在
            print('dir not exists,start create:' + cgSaveFullPath)
            # 只能创建单级目录,用这个命令创建级联的会报OSError错误
            os.mkdir(cgSaveFullPath)
        pass


def getCgIdList(htmlMsgFile):
    """
    从本地文件中获取所有立绘的id,组成一个列表返回
    :param htmlMsgFile:待读取的文件
    :return:完整的立绘id列表
    """
    # 一次性读取整个文件
    html = open(htmlMsgFile).read()
    # 使用正则表达式匹配指定标签内容,提取立绘id
    # 使用浏览器访问全局变量的url,然后查看网页源代码,搜索任意一个立绘id,可得到标签模板,根据标签模板生成正则表达式
    # findall表示搜索所有匹配的内容,返回的是一个列表
    list = re.findall('<span ' + cgIDInfo + '="" class="m-l-xs">(res\d*?)</span>', html)
    print(list)
    return list


if __name__ == '__main__':
    os.system('pause')
    # 获得程序开始时的时间
    start = time.time()

    # 重建一个新文件
    logFileObj = open(errorLogFileName, 'wb')
    logFileObj.truncate()
    logFileObj.close()
    # 获得文件追加操作对象
    logFileObj = open(errorLogFileName, 'a')

    # 获得所有立绘的id
    cdIdList = getCgIdList(htmlMsgFileName)
    # 生成本地存储目录
    autoCreateDir(cdIdList)
    # cpu_Count=os.cpu_count()
    # print("本机为", cpu_Count, "核 CPU")
    # p = mup.Pool(4)  # 根据自己电脑的核数选择最多开几个进程
    # for picType in (normal,trim_normal,special,trim_special):
    #     # 多进程执行任务
    #     p.apply_async(getMutTypePicture, args=(li,picType,nowthread,maxThread))

    # 循环下载所有指定类型的图片,如:想只下载特训后的图片,则修改循环范围变量的内容
    for picTypeName in needDownLoadTypeNameList:
        startDownLoadOneTypeAllPictureByMutThread(cdIdList, picTypeName, maxThread)

    # p.close()  # 禁止再创建新的子进程进程
    # p.join()  # 等待所有子进程结束后主进程才能继续
    print('all end')
    # 获得程序完成时间
    end = time.time()
    # 显示本程序运行耗时
    print("down failed:", set(downFailedList))
    print("this running use time: %f s" % (end - start))
    print('press any key to continue...')
    logFileObj.close()
    os.system('pause')