main.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import base64
  2. import datetime
  3. import os
  4. import shutil
  5. import threading
  6. from PyQt5 import QtCore
  7. from PyQt5.QtCore import QBasicTimer
  8. from PyQt5.QtWidgets import *
  9. from PyQt5.QtGui import *
  10. import FileOperator
  11. import TestFileSystem
  12. from MainWindow import Ui_MainWindow
  13. from icon import img
  14. import TitleSpider
  15. global canRun
  16. def GetSeries(dataList):
  17. return int(dataList.split('_')[1])
  18. class MainApp(QMainWindow, Ui_MainWindow):
  19. def __init__(self):
  20. global canRun
  21. QMainWindow.__init__(self)
  22. if not self.SetUI():
  23. canRun = False
  24. return
  25. else:
  26. canRun = True
  27. self.setupUi(self)
  28. self.SetBaseInfo()
  29. self.InitMenuBar()
  30. self.HandleButtons()
  31. self.setFixedSize(self.width(), self.height())
  32. self.SetLogText()
  33. self.SetProgressBar()
  34. self.progressBar.setValue(0)
  35. self.InitCheckBox()
  36. self.path = os.getcwd()
  37. self.saveName = "set.ini"
  38. self.joinedPath = os.path.join(self.path, self.saveName) # 配置文件的位置
  39. self.isTextFileExists = False
  40. self.isTextFirstColumnHaveContent = False
  41. self.isTextSecondColumnHaveContent = False
  42. self.Log(
  43. "欢迎使用本工具,本工具发布于B站@落点dev。\n温馨提示:\n使用程序处理5GB以上的文件夹,出现未响应是正常现象。响应时间和硬盘读写速度挂钩,请耐心等待程序响应即可。\n\n期间你可以打开输出目录以观察处理进度,程序出现其它问题可以b站评论区评论或私信,如需快速回复可联系qq:3152319989")
  44. self.InitOutPutPath() # 发布绿色版时注释
  45. def SetProgressBar(self):
  46. self.progressBar.setRange(0, 1000) # 1000的原因是防止进度条出现小于0的数
  47. self.progressBar.setValue(0)
  48. def InitOutPutPath(self):
  49. self.isTextFileExists = FileOperator.SaveForOutput(self.path, self.saveName)
  50. if self.isTextFileExists is False: # 如果没有文件,在已经创建文件的前提下,等待用户输入手动填入OutputText. 相关事件在button事件实现
  51. # line = FileOperator.ReadForOutput() #
  52. pass
  53. else: # 存在文件 则读取文件,并自动赋给文本框
  54. self.lines = FileOperator.ReadForOutput(self.joinedPath)
  55. if len(self.lines) >= 2:
  56. if self.lines[0] != '':
  57. if os.path.isdir(self.lines[0].strip()):
  58. self.downloadDirEdit.setText(self.lines[0].strip())
  59. self.isTextFirstColumnHaveContent = True
  60. if self.lines[1] != '':
  61. if os.path.isdir(self.lines[1].strip()):
  62. self.outputDirEdit.setText(self.lines[1].strip())
  63. self.isTextSecondColumnHaveContent = True
  64. else:
  65. pass
  66. # self.Log("检测到配置文件set.ini 为空")
  67. # QMessageBox.critical(self, "错误", "配置文件set.ini !")
  68. def InitCheckBox(self):
  69. # self.txtFileCheckBox.setChecked(True) # 默认保存Txt
  70. # self.deleteFileCheckBox.setChecked(False) # 默认保留源目录
  71. # self.copyToOutput.setChecked(True) # 默认使用复制的方式
  72. self.txtFileCheckBox.setChecked(False) # 默认不保存Txt
  73. self.deleteFileCheckBox.setChecked(True) # 默认不保留源目录
  74. self.moveToOutput.setChecked(True) # 默认不使用复制的方式
  75. self.localMode.setChecked(True)
  76. def MutiThreadCopy(self, mp4List, outputPath, videoCount):
  77. t = threading.Thread(target=self.CopyFile, args=(mp4List, outputPath, videoCount))
  78. t.start()
  79. t.join()
  80. def MutiThreadMove(self, mp4List, outputPath, videoCount):
  81. t = threading.Thread(target=self.MoveFile, args=(mp4List, outputPath, videoCount))
  82. t.start()
  83. t.join()
  84. def CheckIsChecked(self): # 按下按钮的事件里调用,检查checkbox状态,并提示。最后给对应的bool变量赋值
  85. self.isSaveTxt = self.txtFileCheckBox.isChecked()
  86. self.isDeleteDir = self.deleteFileCheckBox.isChecked()
  87. if self.copyToOutput.isChecked() or self.moveToOutput.isChecked():
  88. pass
  89. else:
  90. self.Log("错误:请至少勾选一种输出方式!")
  91. return False
  92. # QMessageBox.critical(self, "错误", "请至少勾选一种输出方式!")
  93. if self.localMode.isChecked() or self.spiderMode.isChecked():
  94. pass
  95. else:
  96. self.Log("错误:请至少勾选一种处理模式(本地模式 或 爬虫模式)!")
  97. return False
  98. # QMessageBox.critical(self, "错误", "请至少勾选一种处理模式(本地模式 或 爬虫模式)!")
  99. if self.copyToOutput.isChecked():
  100. self.isCopyOutput = True
  101. else:
  102. self.isCopyOutput = False
  103. if self.moveToOutput.isChecked():
  104. self.isMoveOutput = True
  105. else:
  106. self.isMoveOutput = False
  107. if self.localMode.isChecked():
  108. self.isLocalMode = True
  109. else:
  110. self.isLocalMode = False
  111. if self.spiderMode.isChecked():
  112. self.Log("警告:" + "爬虫模式依赖网络,代理模式下程序不会正常运行")
  113. # QMessageBox.warning(self, "警告", "爬虫模式依赖网络,关闭本窗口前请确保代理服务是关闭状态")
  114. self.isSpiderMode = True
  115. else:
  116. self.isSpiderMode = False
  117. return True
  118. def SetLogText(self):
  119. self.activityLogEdit.setReadOnly(True)
  120. def Log(self, msg):
  121. self.statusbar.showMessage(msg)
  122. self.activityLogEdit.appendPlainText('[{0}]'.format(str(datetime.datetime.now())[0:19]))
  123. self.activityLogEdit.appendPlainText(msg)
  124. self.activityLogEdit.appendPlainText('')
  125. self.activityLogEdit.moveCursor(QTextCursor.End)
  126. def LogOnBar(self, msg):
  127. self.statusbar.showMessage(msg)
  128. def HandleButtons(self):
  129. self.downloadDirButton.clicked.connect(self.OpenDownloadDir)
  130. self.outputDirButton.clicked.connect(self.OpenOutputDir)
  131. self.renameButton.clicked.connect(self.MutiThreadRenameFile) # 多线程会导致 newMessageBox 的提示无法正常弹出
  132. # self.renameButton.clicked.connect(self.RenameFile)
  133. self.copyToOutput.clicked.connect(self.DisableMove)
  134. self.moveToOutput.clicked.connect(self.DisableCopy)
  135. self.localMode.clicked.connect(self.DisableSpiderMode)
  136. self.spiderMode.clicked.connect(self.DisableLocalMode)
  137. def MutiThreadRenameFile(self):
  138. t = threading.Thread(target=self.RenameFile)
  139. t.start()
  140. t.join()
  141. # 处理checkbox冲突
  142. def DisableCopy(self):
  143. if self.copyToOutput.isChecked():
  144. self.copyToOutput.setChecked(False)
  145. def DisableMove(self):
  146. if self.moveToOutput.isChecked():
  147. self.moveToOutput.setChecked(False)
  148. def DisableSpiderMode(self):
  149. if self.spiderMode.isChecked():
  150. self.spiderMode.setChecked(False)
  151. self.renameButton.setText("一键解密+整理+重命名")
  152. def DisableLocalMode(self):
  153. if self.localMode.isChecked():
  154. self.localMode.setChecked(False)
  155. self.renameButton.setText("一键解密+爬取+整理+重命名")
  156. def InitMenuBar(self):
  157. # 添加menu“帮助”的事件
  158. aboutAction = QAction('&关于', self)
  159. # aboutAction.setStatusTip('关于')
  160. aboutAction.triggered.connect(self.ShowAboutDialog)
  161. # 已有菜单栏,此处只需要添加菜单
  162. mainPageMenu = self.menubar.addMenu('&主页')
  163. helpMenu = self.menubar.addMenu('&帮助')
  164. # 菜单绑定之前添加的事件
  165. helpMenu.addAction(aboutAction)
  166. # 设置UI
  167. def SetUI(self):
  168. tmp = None
  169. try:
  170. tmp = open('tmp.png', "wb+")
  171. tmp.write(base64.b64decode(img))
  172. except Exception as e:
  173. # self.Log("错误:"+"您当前安装在了C盘,软件权限不足:\n解决方案一:请关闭本程序并每次使用管理员权限运行本软件 \n解决方案二:卸载本软件再重新安装至其它非系统盘 比如:D盘、E盘!")
  174. QMessageBox.critical(self, "错误",
  175. "您当前安装在了C盘,软件权限不足:\n解决方案一:请关闭本程序并每次使用管理员权限运行本软件 \n解决方案二:卸载本软件再重新安装至其它非系统盘 比如:D盘、E盘!")
  176. return False
  177. finally:
  178. if tmp:
  179. tmp.close()
  180. icon = QIcon('tmp.png')
  181. os.remove("tmp.png")
  182. self.setWindowIcon(icon)
  183. return True
  184. def ShowAboutDialog(self):
  185. about_text = "<p>描述:这是一款致力于解决BiliBili UWP版下载后的视频加密、命名信息丢失和存放位置不合理等痛点的软件</p><p>版本:4.5</p><p>@Author:LZY</p><p>@github:love" \
  186. "-in-cpp</p> "
  187. QMessageBox.about(self, '说明', about_text)
  188. def OpenDownloadDir(self):
  189. if self.isTextFirstColumnHaveContent is False:
  190. dName = QFileDialog.getExistingDirectory(self, '选择下载文件夹', '/')
  191. self.downloadDirEdit.setText(dName)
  192. else:
  193. dName = QFileDialog.getExistingDirectory(self, '选择下载文件夹', self.lines[0].strip())
  194. self.downloadDirEdit.setText(dName)
  195. def OpenOutputDir(self):
  196. if self.isTextSecondColumnHaveContent is False:
  197. dName = QFileDialog.getExistingDirectory(self, '选择输出文件夹', '/')
  198. self.outputDirEdit.setText(dName)
  199. else:
  200. dName = QFileDialog.getExistingDirectory(self, '选择输出文件夹', self.lines[1].strip())
  201. self.outputDirEdit.setText(dName)
  202. def SetBaseInfo(self):
  203. self.setWindowTitle('BiliBili UWP版视频解密整理工具')
  204. self.downloadDirEdit.setToolTip(r"例如:E:\BiliDownload\44938322")
  205. self.downloadDirEdit.setPlaceholderText("路径请具体到单个数字名称的文件夹,暂不支持文件夹的批量处理")
  206. self.outputDirEdit.setPlaceholderText("您希望处理后的文件被保存到的地方")
  207. # def FindFiles(self,downloadPath):
  208. def RenameFile(self):
  209. if self.CheckIsChecked() is False:
  210. return
  211. self.progressBar.setValue(0)
  212. # 进入目录查找dvi文件
  213. downloadPath = self.downloadDirEdit.toPlainText()
  214. outputPath = self.outputDirEdit.toPlainText()
  215. if os.path.isdir(downloadPath) is False or os.path.isdir(self.downloadDirEdit.toPlainText().strip()) is False:
  216. self.Log('UWP下载目录的路径存在非法输入!')
  217. else:
  218. self.Log("进入目录:{0}".format(downloadPath))
  219. dviInfoList = FileOperator.GetDviInfo(downloadPath) # 获取dvi文件信息
  220. if dviInfoList[0] is False:
  221. self.Log(
  222. '没有找到.dvi文件!请检查下载目录后重试!请确保使用的是「bilibili uwp客户端」下载的视频而非「桌面客户端」或其它客户端,请谨慎甄别,本工具不能处理其他客户端下载的视频;如果确认使用的是uwp客户端下载的视频,请仔细查看视频 1分20秒 「选择下载目录」的片段')
  223. else:
  224. # 在outputDir下新建名为dvi[3]文件夹
  225. try:
  226. outputPath = FileOperator.MakeDir(outputPath, dviInfoList[3])
  227. except Exception as e:
  228. self.Log("错误" + "已经存在同名文件夹! Error:" + str(e))
  229. # QMessageBox.critical(self, "错误", "已经存在同名文件夹! Error:" + str(e))
  230. return
  231. if self.isSpiderMode:
  232. self.Log("开始爬取BV:{0}, 标题:{1} 的所有视频标题,请稍后...".format(dviInfoList[1], dviInfoList[3]))
  233. self.progressBar.setValue(10)
  234. try:
  235. TitleSpider.GetTxt(dviInfoList[1], outputPath)
  236. except Exception as e:
  237. self.Log("错误" + "请检查网络后重试 Error:" + str(e))
  238. self.progressBar.setValue(0)
  239. # QMessageBox.critical(self, "错误", "请检查网络后重试 Error:" + str(e))
  240. return
  241. # 调用爬虫产生.txt
  242. global fileName
  243. fileName = TitleSpider.fileName
  244. self.LogOnBar('已成功爬取文件: {0} ! 注:只显示部分文件名'.format(fileName[0:35]))
  245. self.Log('已成功爬取文件: {0} !'.format(fileName))
  246. self.progressBar.setValue(20)
  247. elif self.isLocalMode:
  248. self.Log("开始遍历获取BV:{0}, 标题:{1} 的所有视频标题,请稍后...".format(dviInfoList[1], dviInfoList[3]))
  249. localVideoTitleList = FileOperator.GetLocalVideoTitle(downloadPath, dviInfoList[2])
  250. fileName = FileOperator.GetTxt(localVideoTitleList, dviInfoList[3], outputPath)
  251. self.progressBar.setValue(10)
  252. self.Log('已成功获取文件: {0} !'.format(fileName))
  253. self.progressBar.setValue(20)
  254. else:
  255. self.Log("impossible")
  256. # 找到所有downloadPath的.mp4文件
  257. mp4List = FileOperator.FindSpecialMp4Files(downloadPath, dviInfoList[2])[0] # mp4真正在的地方
  258. # Log
  259. mp4nameList = FileOperator.FindSpecialMp4Files(downloadPath, dviInfoList[2])[1]
  260. try:
  261. mp4nameList.sort(key=GetSeries)
  262. except Exception as e:
  263. self.Log("错误" + "存在干扰文件!排序错误,请联系作者!" + str(e))
  264. self.progressBar.setValue(0)
  265. # QMessageBox.critical(self, "错误", "存在干扰文件!排序错误,请联系作者!" + str(e))
  266. return
  267. s = "查询到以下mp4文件:\n"
  268. videoCount = 0
  269. for item in mp4nameList:
  270. s += (item + '\n')
  271. videoCount += 1
  272. self.Log(s)
  273. self.progressBar.setValue(30)
  274. if os.path.isdir(outputPath) is False or os.path.isdir(
  275. self.outputDirEdit.toPlainText().strip()) is False:
  276. self.Log('输出目录的路径存在非法输入!')
  277. self.progressBar.setValue(0)
  278. else:
  279. # 记忆输出目录
  280. FileOperator.WriteForOutput(self.joinedPath, os.path.dirname(downloadPath),
  281. self.outputDirEdit.toPlainText()) # 发布绿色版时注释
  282. # 解密
  283. self.Log("开始解密...")
  284. FileOperator.DecryptMp4(downloadPath, dviInfoList[2])
  285. self.progressBar.setValue(40)
  286. self.Log("解密完毕!")
  287. self.progressBar.setValue(50)
  288. # 复制 todo
  289. # self.SetProgressBar(self.progressBar.value(), self.progressBar.value() + videoCount + 2) # 适配进度条
  290. self.CopyOrMove(self.isCopyOutput, mp4List, outputPath, videoCount)
  291. # 重命名
  292. self.Log("开始重命名...")
  293. self.progressBar.setValue(998)
  294. FileOperator.DoRename(outputPath, fileName, dviInfoList[2], self.isLocalMode)
  295. self.Log("重命名完毕!")
  296. self.progressBar.setValue(1000)
  297. # 进度条100%
  298. # self.progressBar.setValue(100)
  299. # 是否保存.txt文件
  300. if self.isSaveTxt is True:
  301. pass
  302. else:
  303. self.Log("正在删除程序运行过程中产生的.txt文件")
  304. FileOperator.DeleteTxt(outputPath, fileName)
  305. self.Log("删除.txt文件成功!")
  306. # 是否删除源文件夹
  307. if self.isDeleteDir is True:
  308. self.Log("正在删除源文件夹")
  309. FileOperator.DeleteDir(downloadPath)
  310. self.Log("删除源文件夹成功!")
  311. else:
  312. pass
  313. # 重命名输出文件夹 搁置
  314. def CopyFile(self, srcFileList, dstFolder, videoCount):
  315. for file in srcFileList:
  316. shutil.copy(file, dstFolder)
  317. self.progressBar.setValue(self.progressBar.value() + int(950/videoCount))
  318. def MoveFile(self, srcFileList, dstFolder, videoCount):
  319. for file in srcFileList:
  320. shutil.move(file, dstFolder)
  321. self.progressBar.setValue(self.progressBar.value() + int(950/videoCount))
  322. # 输出方式:复制或移动
  323. def CopyOrMove(self, isCopyTo, mp4List, outputPath, videoCount):
  324. if isCopyTo is True:
  325. self.Log("进入目录:{0}".format(outputPath))
  326. self.Log("正在复制... 这可能需要一段时间...")
  327. self.MutiThreadCopy(mp4List, outputPath, videoCount) # 多线程复制
  328. # self.CopyFile(mp4List, outputPath, videoCount);
  329. self.Log("复制完毕!")
  330. else:
  331. self.Log("进入目录:{0}".format(outputPath))
  332. self.Log("正在移动... 这可能需要一段时间...")
  333. self.MutiThreadMove(mp4List, outputPath, videoCount) # 多线程移动
  334. # self.MoveFile(mp4List, outputPath, videoCount)
  335. self.Log("移动完毕!")
  336. def DSpiderMode(self):
  337. pass
  338. def DoLocalMode(self):
  339. pass
  340. if __name__ == '__main__':
  341. QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
  342. app = QApplication([])
  343. window = MainApp()
  344. if canRun:
  345. window.show()
  346. app.exec_()