#!/usr/bin/env python # -*- coding: utf-8 -*- # @Date : 2018-08-05 11:14:46 # @Author : Lewis Tian (chtian@hust.edu.cn) # @Link : https://lewistian.github.io/ # @Version : Python3.6 from ui_mwin import Ui_MWin from PyQt5 import QtCore from PyQt5.QtCore import QUrl, QThread, pyqtSignal from PyQt5.QtGui import QIcon, QPixmap, QDesktopServices, QCursor from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidgetItem, QMessageBox, QProgressBar, QMenu, QAction import re import sys import requests import time import json import os import threading from contextlib import closing base_headers = { 'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'Accept-Encoding':'gzip, deflate, br', 'Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6', 'Connection':'keep-alive', 'Host':'www.bilibili.com', 'Upgrade-Insecure-Requests':'1', 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36', } cur_threadId = 0 # used for a new QThread id try: with open('Cookie') as f: Cookie = f.read() # Cookie base_headers['Cookie'] = Cookie except: print("open Cookie error") class BilibiliKit(QMainWindow, Ui_MWin): def __init__(self, parent=None): super(BilibiliKit, self).__init__(parent) self.setupUi(self) self.connectSlots() self.ss = requests.Session() self.cover = None self.danmu = None self.title = None self.av = 0 self.pages = 1 # 视频分p数 self.page = 0 # 当前下载视频的索引号(从0开始) self.links = None self.slices = 1 # 视频每p的切片 self.slice = 0 # 当前下载切片的索引号(从0开始) self.row2qthread = {} # 行与 qid 的映射表 def connectSlots(self): """connect slots with buttons/ short cuts/signals """ # home tab self.search.clicked.connect(lambda: self.searchInfo(1)) self.search.setShortcut("CTRL+Return") self.download.clicked.connect(self.downloadVideo) self.download.setShortcut("CTRL+L") # download tab self.startAll.clicked.connect(self.startAllThreads) self.pauseAll.clicked.connect(self.pauseAllThreads) self.clearAll.clicked.connect(self.clearAllThreads) self.openVideoFolder.clicked.connect(self.openDownloadedVideoFolder) self.downloadWidget.customContextMenuRequested.connect(self.downloadWidgetContext) def searchInfo(self, page = 1): if not self.lineEdit_input.text(): return url = '' if page == 1: try: avNum = re.findall(r'(av\d+)', self.lineEdit_input.text())[0] except: return url = 'https://www.bilibili.com/video/' + avNum else: url = 'https://www.bilibili.com/video/av{}?p={}'.format(self.av, page) if not url: return print(url) r = self.ss.get(url, headers = base_headers, timeout = 3) info = None if r.status_code == 200: try: data = re.findall(r'<script>window\.__INITIAL_STATE__=(.*?);\(function', r.text)[0] # print(data) bili = json.loads(data) aid = bili['aid'] title = bili['videoData']['title'] pubtime = bili['videoData']['pubdate'] up = bili['upData']['name'] vtype = bili['videoData']['tname'] cover = bili['videoData']['pic'] desc = bili['videoData']['desc'] sex = bili['upData']['sex'] # get multi-videos info tmp = bili['videoData']['pages'] pages = len(tmp) cid = [ tmp[i]['cid'] for i in range(pages)] info = ('av'+aid,title,time.ctime(pubtime),up,vtype,cover,'https://comment.bilibili.com/{}.xml'.format(cid[0]),desc,sex) except: print("get video info failed!") return else: print('error code:', r.status_code) return # print(info) for x in range(len(info)): item = QTableWidgetItem(info[x]) self.tableWidget.setItem(x, 0, item) item = QTableWidgetItem() icon = QIcon() if info[-1] == '女': icon.addPixmap(QPixmap(":/images/b6"), QIcon.Normal, QIcon.Off) elif info[-1] == '男': icon.addPixmap(QPixmap(":/images/b7"), QIcon.Normal, QIcon.Off) else: icon.addPixmap(QPixmap(":/images/b8"), QIcon.Normal, QIcon.Off) item.setIcon(icon) self.tableWidget.setItem(3, 0, item) item = self.tableWidget.item(3, 0) item.setText(info[3]) self.title = title self.av = aid self.cover = cover self.pages = pages self.danmu = cid # https://comment.bilibili.com/43199797.xml self.page = page self.getVideoLinks(r) def getVideoLinks(self, r): # get video download links download_link = [] try: data = re.findall(r'<script>window\.__playinfo__=(.*?)</script>', r.text)[0] bili = json.loads(data) self.slices = len(bili['durl']) download_link = bili['durl'] except: print("get download links failed!") if download_link: for i in range(self.slices): download_link[i]['url'] = download_link[i]['url'].replace("http", "https") self.links = download_link def downloadVideo(self): if not self.links: return self.tabWidget.setCurrentIndex(1) if self.pages > 1: reply = QMessageBox.question(self, '哔哩哔哩工具箱 v1.1 - ©Tich', '发现有多个视频文件,是否全部下载?\n若否,则仅下载第一p视频(*゚∀゚*)', QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.multiDownload() return self.singleDownload() def singleDownload(self): print('single') row = self.downloadWidget.rowCount() self.downloadWidget.setRowCount(row+1) item = QTableWidgetItem(self.title) self.downloadWidget.setItem(row, 0, item) item = QTableWidgetItem('p{}'.format(self.page)) self.downloadWidget.setItem(row, 1, item) item = QTableWidgetItem('0/{}'.format(self.slices)) self.downloadWidget.setItem(row, 2, item) qpb = QProgressBar() qpb.setValue(0) self.downloadWidget.setCellWidget(row, 3, qpb) # print(self.links) t = Downloader(self.av, self.links) self.row2qthread[row] = t t.finish.connect(self.downloaded) t.signal.connect(self.updateItem) t.cur_slice.connect(self.updateItem) t.start() # print(int(t.currentThreadId())) def multiDownload(self): print('multi-videos') self.singleDownload() for x in range(2, self.pages+1): print(x) self.searchInfo(x) self.singleDownload() def downloaded(self, t, slices): """finish a video downloading """ print('downloaded') for k, v in self.row2qthread.items(): if v == t: if slices == -1: s = '下载出错:'+ self.downloadWidget.item(k, 0).text() item = QTableWidgetItem(s) self.downloadWidget.setItem(k, 0, item) elif slices == -2: s = '结束下载:'+ self.downloadWidget.item(k, 0).text() item = QTableWidgetItem(s) self.downloadWidget.setItem(k, 0, item) else: item = QTableWidgetItem('{0}/{0}'.format(slices)) self.downloadWidget.setItem(k, 2, item) QMessageBox.about(self, '哔哩哔哩工具箱 v1.1 - ©Tich', '{} 下载完成!'.format(self.downloadWidget.item(k, 0).text())) break def updateItem(self, t, array): """update downloadWidget cell t: qthread array: (val1, [val2], flag) val1: contains (downloaded count)/(total count)*100 / cur_slice val2: total slices op:0 => download counts :1 => video slices """ val = array[0] op = array[-1] for k, v in self.row2qthread.items(): if v == t: if op==0: self.downloadWidget.cellWidget(k, 3).setValue(val) else: item = QTableWidgetItem('{}/{}'.format(val, array[-2])) self.downloadWidget.setItem(k, 2, item) return def startAllThreads(self): """start all download thread """ if self.row2qthread: print('start all') for i in self.row2qthread.values(): i.resume() def pauseAllThreads(self): """pause all download thread """ if self.row2qthread: print('pause all') for i in self.row2qthread.values(): i.pause() def clearAllThreads(self): """stop all download thread """ if self.row2qthread: print('clear all') for k, v in self.row2qthread.items(): try: v.stop() except Exception as e: pass self.downloadWidget.clearContents() self.downloadWidget.setRowCount(0) self.row2qthread.clear() def openDownloadedVideoFolder(self): """open download video folder """ try: QDesktopServices.openUrl(QUrl.fromLocalFile(os.getcwd()+'/videos')); except: pass def downloadWidgetContext(self, point): """downloadWidget right click menu """ popMenu = QMenu() startAThread = QAction('开始', self) pauseAThread = QAction('暂停', self) clearAThread = QAction('删除', self) startAThread.triggered.connect(lambda: self.operateAThread(1, point)) pauseAThread.triggered.connect(lambda: self.operateAThread(2, point)) clearAThread.triggered.connect(lambda: self.operateAThread(3, point)) popMenu.addAction(startAThread) popMenu.addAction(pauseAThread) popMenu.addAction(clearAThread) popMenu.exec_(QCursor.pos()) def operateAThread(self, op, pos): cur_row = self.downloadWidget.indexAt(pos).row() print(cur_row) if op == 1: print('start') self.row2qthread[cur_row].resume() elif op == 2: print('pause') self.row2qthread[cur_row].pause() else: print('clear') self.row2qthread[cur_row].stop() class Downloader(QThread): """download class""" signal = pyqtSignal(QThread, list) # 下载量信号 list: value flag cur_slice = pyqtSignal(QThread, list) # 切片信号 list: cur_slice totla_slices flag finish = pyqtSignal(QThread, int) # 下载结束信号 def __init__(self, av, links): super(Downloader, self).__init__() self.av = av self.links = links self.id = 0 self.__flag = threading.Event() # pause flag self.__flag.set() # True self.__running = threading.Event() # stop flag self.__running.set() # True if not os.path.exists('videos'): os.mkdir('videos') def run(self): """download a video may contain muti-slices videos """ headers = { 'Accept':'*/*', 'Accept-Encoding':'gzip, deflate, br', 'Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6', 'Cache-Control':'no-cache', 'Connection':'keep-alive', 'Origin':'https://www.bilibili.com', 'Pragma':'no-cache', 'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36', } headers['Referer'] = 'https://www.bilibili.com/video/' + self.av # print(self.links) cur_count = 0 total_count = len(self.links) print('总切片', total_count) for link in self.links: url = link['url'] # print(url) filename = re.findall(r'/(\d+-\d+-\d+.flv)', url)[0] while self.__running.isSet(): # 如果被设置为了true就继续,false就终止了 print("下载", filename) with closing(requests.get(url, headers = headers, timeout = 3, stream=True)) as r: print(r.status_code) # print('headers:', r.headers) content_size = int(r.headers['content-length']) # 内容体总大小 # print(link['size']) # print('size:', content_size) chunk_size = int(content_size/100) # 单次请求最大值 # print(content_size, chunk_size) count = 0 try: with open('videos/'+filename, "wb") as file: for data in r.iter_content(chunk_size=chunk_size): if not self.__running.isSet(): self.finish2emit(-2) return self.__flag.wait() # 为True时立即返回, 为False时阻塞直到内部的标识位为True后返回 file.write(data) count += len(data) downloadCount = int(count/chunk_size) self.signal2emit(downloadCount) # print(count/chunk_size) except Exception as e: self.finish2emit(-1) print(e) return cur_count += 1 print("下载完成 %d/%d"%(cur_count, total_count)) print(downloadCount) if cur_count == total_count: self.finish2emit(total_count) return else: self.slice2emit(cur_count, total_count) break def pause(self): print('pause') self.__flag.clear() def resume(self): self.__flag.set() def stop(self): # self.__flag.set() self.__running.clear() self.exit() def signal2emit(self, val): self.signal.emit(self, [val, 0]) def slice2emit(self, cur_count, total_count): self.cur_slice.emit(self, [cur_count, total_count, 1]) def finish2emit(self, slices): self.finish.emit(self, slices) if __name__ == "__main__": app = QApplication(sys.argv) w = BilibiliKit() w.show() sys.exit(app.exec_())