downloader.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package main
  2. import (
  3. "github.com/allanpk716/ChineseSubFinder/common"
  4. "github.com/allanpk716/ChineseSubFinder/mark_system"
  5. "github.com/allanpk716/ChineseSubFinder/model"
  6. "github.com/allanpk716/ChineseSubFinder/series_helper"
  7. "github.com/allanpk716/ChineseSubFinder/sub_supplier"
  8. "github.com/allanpk716/ChineseSubFinder/sub_supplier/shooter"
  9. "github.com/allanpk716/ChineseSubFinder/sub_supplier/subhd"
  10. "github.com/allanpk716/ChineseSubFinder/sub_supplier/xunlei"
  11. "github.com/allanpk716/ChineseSubFinder/sub_supplier/zimuku"
  12. "github.com/go-rod/rod/lib/utils"
  13. "github.com/panjf2000/ants/v2"
  14. "github.com/sirupsen/logrus"
  15. "os"
  16. "path"
  17. "path/filepath"
  18. "strings"
  19. "sync"
  20. )
  21. type Downloader struct {
  22. reqParam common.ReqParam
  23. log *logrus.Logger
  24. topic int // 最多能够下载 Top 几的字幕,每一个网站
  25. mk *mark_system.MarkingSystem // MarkingSystem
  26. }
  27. // TODO 加入一个功能,如果这个电影,发行很久了,而且没得字幕,那么重复多天后,应该就不用再扫描它了 把字幕下载周期开放出来允许设置,现在默认是 3个月
  28. func NewDownloader(_reqParam ...common.ReqParam) *Downloader {
  29. var downloader Downloader
  30. downloader.log = model.GetLogger()
  31. downloader.topic = common.DownloadSubsPerSite
  32. if len(_reqParam) > 0 {
  33. downloader.reqParam = _reqParam[0]
  34. if downloader.reqParam.Topic > 0 && downloader.reqParam.Topic != downloader.topic {
  35. downloader.topic = downloader.reqParam.Topic
  36. }
  37. // 并发线程的范围控制
  38. if downloader.reqParam.Threads <= 0 {
  39. downloader.reqParam.Threads = 2
  40. } else if downloader.reqParam.Threads >= 10 {
  41. downloader.reqParam.Threads = 10
  42. }
  43. }
  44. var sitesSequence = make([]string, 0)
  45. // TODO 这里写固定了抉择字幕的顺序
  46. sitesSequence = append(sitesSequence, common.SubSiteZiMuKu)
  47. sitesSequence = append(sitesSequence, common.SubSiteSubHd)
  48. sitesSequence = append(sitesSequence, common.SubSiteXunLei)
  49. sitesSequence = append(sitesSequence, common.SubSiteShooter)
  50. downloader.mk = mark_system.NewMarkingSystem(sitesSequence)
  51. return &downloader
  52. }
  53. func (d Downloader) DownloadSub4Movie(dir string) error {
  54. defer func() {
  55. // 所有的电影字幕下载完成,抉择完成,需要清理缓存目录
  56. err := model.ClearRootTmpFolder()
  57. if err != nil {
  58. d.log.Error(err)
  59. }
  60. // 注意并发 pool 的释放
  61. defer ants.Release()
  62. log.Infoln("Download Movie Sub End...")
  63. }()
  64. log.Infoln("Download Movie Sub Started...")
  65. nowVideoList, err := model.SearchMatchedVideoFile(dir)
  66. if err != nil {
  67. return err
  68. }
  69. // 并发控制
  70. movieDlFunc := func(i interface{}) {
  71. inData := i.(InputData)
  72. // -----------------------------------------------------
  73. // 构建每个字幕站点下载者的实例
  74. var subSupplierHub = sub_supplier.NewSubSupplierHub(
  75. shooter.NewSupplier(d.reqParam),
  76. subhd.NewSupplier(d.reqParam),
  77. xunlei.NewSupplier(d.reqParam),
  78. zimuku.NewSupplier(d.reqParam),
  79. )
  80. // 字幕都下载缓存好了,需要抉择存哪一个,优先选择中文双语的,然后到中文
  81. organizeSubFiles, err := subSupplierHub.DownloadSub4Movie(inData.OneVideoFullPath, inData.Index)
  82. if err != nil {
  83. d.log.Errorln("subSupplierHub.DownloadSub4Movie", inData.OneVideoFullPath ,err)
  84. return
  85. }
  86. if organizeSubFiles == nil || len(organizeSubFiles) < 1 {
  87. d.log.Infoln("no sub found", filepath.Base(inData.OneVideoFullPath))
  88. return
  89. }
  90. d.oneVideoSelectBestSub(inData.OneVideoFullPath, organizeSubFiles)
  91. // -----------------------------------------------------
  92. }
  93. wg := sync.WaitGroup{}
  94. p, err := ants.NewPoolWithFunc(d.reqParam.Threads, func(inData interface{}) {
  95. movieDlFunc(inData)
  96. wg.Done()
  97. })
  98. if err != nil {
  99. return err
  100. }
  101. // 一个视频文件同时多个站点查询,阻塞完毕后,在进行下一个
  102. for i, oneVideoFullPath := range nowVideoList {
  103. wg.Add(1)
  104. err = p.Invoke(InputData{OneVideoFullPath: oneVideoFullPath, Index: i})
  105. if err != nil {
  106. d.log.Errorln("movie ants.Invoke",err)
  107. }
  108. }
  109. wg.Wait()
  110. return nil
  111. }
  112. func (d Downloader) DownloadSub4Series(dir string) error {
  113. defer func() {
  114. // 所有的连续剧字幕下载完成,抉择完成,需要清理缓存目录
  115. err := model.ClearRootTmpFolder()
  116. if err != nil {
  117. d.log.Error(err)
  118. }
  119. // 注意并发 pool 的释放
  120. defer ants.Release()
  121. log.Infoln("Download Series Sub End...")
  122. }()
  123. log.Infoln("Download Series Sub Started...")
  124. // 并发控制
  125. seriesDlFunc := func(i interface{}) {
  126. inData := i.(InputData)
  127. // 构建每个字幕站点下载者的实例
  128. var subSupplierHub *sub_supplier.SubSupplierHub
  129. subSupplierHub = sub_supplier.NewSubSupplierHub(
  130. zimuku.NewSupplier(d.reqParam),
  131. subhd.NewSupplier(d.reqParam),
  132. xunlei.NewSupplier(d.reqParam),
  133. shooter.NewSupplier(d.reqParam),
  134. )
  135. // 这里拿到了这一部连续剧的所有的剧集信息,以及所有下载到的字幕信息
  136. seriesInfo, organizeSubFiles, err := subSupplierHub.DownloadSub4Series(inData.OneVideoFullPath, inData.Index)
  137. if err != nil {
  138. d.log.Errorln("subSupplierHub.DownloadSub4Series", inData.OneVideoFullPath ,err)
  139. return
  140. }
  141. if organizeSubFiles == nil || len(organizeSubFiles) < 1 {
  142. d.log.Infoln("no sub found", filepath.Base(inData.OneVideoFullPath))
  143. return
  144. }
  145. // 只针对需要下载字幕的视频进行字幕的选择保存
  146. for epsKey, episodeInfo := range seriesInfo.NeedDlEpsKeyList {
  147. // 匹配对应的 Eps 去处理
  148. d.oneVideoSelectBestSub(episodeInfo.FileFullPath, organizeSubFiles[epsKey])
  149. }
  150. // 这里会拿到一份季度字幕的列表比如,Key 是 S1E0 S2E0 S3E0,value 是新的存储位置
  151. fullSeasonSubDict := d.saveFullSeasonSub(seriesInfo, organizeSubFiles)
  152. // TODO 季度的字幕包,应该优先于零散的字幕吧,暂定就这样了,注意是全部都替换
  153. // 需要与有下载需求的季交叉
  154. for _, episodeInfo := range seriesInfo.EpList {
  155. _, ok := seriesInfo.NeedDlSeasonDict[episodeInfo.Season]
  156. if ok == false {
  157. continue
  158. }
  159. // 匹配对应的 Eps 去处理
  160. seasonEpsKey := model.GetEpisodeKeyName(episodeInfo.Season, episodeInfo.Episode)
  161. d.oneVideoSelectBestSub(episodeInfo.FileFullPath, fullSeasonSubDict[seasonEpsKey])
  162. }
  163. }
  164. wg := sync.WaitGroup{}
  165. p, err := ants.NewPoolWithFunc(d.reqParam.Threads, func(inData interface{}) {
  166. seriesDlFunc(inData)
  167. wg.Done()
  168. })
  169. if err != nil {
  170. return err
  171. }
  172. // 遍历连续剧总目录下的第一层目录
  173. seriesDirList, err := series_helper.GetSeriesList(dir)
  174. if err != nil {
  175. return err
  176. }
  177. for i, oneSeriesPath := range seriesDirList {
  178. wg.Add(1)
  179. err = p.Invoke(InputData{OneVideoFullPath: oneSeriesPath, Index: i})
  180. if err != nil {
  181. d.log.Errorln("series ants.Invoke",err)
  182. }
  183. }
  184. wg.Wait()
  185. return nil
  186. }
  187. // oneVideoSelectBestSub 一个视频,选择最佳的一个字幕(也可以保存所有网站第一个最佳字幕)
  188. func (d Downloader) oneVideoSelectBestSub(oneVideoFullPath string, organizeSubFiles []string) {
  189. var err error
  190. // 得到目标视频文件的根目录
  191. videoRootPath := filepath.Dir(oneVideoFullPath)
  192. // -------------------------------------------------
  193. // 调试缓存,把下载好的字幕写到对应的视频目录下,方便调试
  194. if d.reqParam.DebugMode == true {
  195. err = d.copySubFile2DesFolder(videoRootPath, organizeSubFiles)
  196. if err != nil {
  197. d.log.Errorln("copySubFile2DesFolder", err)
  198. }
  199. }
  200. // -------------------------------------------------
  201. if d.reqParam.SaveMultiSub == false {
  202. // 选择最优的一个字幕
  203. var finalSubFile *common.SubParserFileInfo
  204. finalSubFile = d.mk.SelectOneSubFile(organizeSubFiles)
  205. if finalSubFile == nil {
  206. d.log.Warnln("Found", len(organizeSubFiles), " subtitles but not one fit:", oneVideoFullPath)
  207. return
  208. }
  209. // 找到了,写入文件
  210. err = d.writeSubFile2VideoPath(oneVideoFullPath, *finalSubFile, "")
  211. if err != nil {
  212. d.log.Errorln("SaveMultiSub:", d.reqParam.SaveMultiSub, "writeSubFile2VideoPath:", err)
  213. return
  214. }
  215. } else {
  216. // 每个网站 Top1 的字幕
  217. siteNames, finalSubFiles := d.mk.SelectEachSiteTop1SubFile(organizeSubFiles)
  218. if len(siteNames) < 0 {
  219. d.log.Warnln("SelectEachSiteTop1SubFile found none sub file")
  220. return
  221. }
  222. for i, file := range finalSubFiles {
  223. err = d.writeSubFile2VideoPath(oneVideoFullPath, file, siteNames[i])
  224. if err != nil {
  225. d.log.Errorln("SaveMultiSub:", d.reqParam.SaveMultiSub, "writeSubFile2VideoPath:", err)
  226. return
  227. }
  228. }
  229. }
  230. }
  231. // saveFullSeasonSub 这里就需要单独存储到连续剧每一季的文件夹的特殊文件夹中
  232. func (d Downloader) saveFullSeasonSub(seriesInfo *common.SeriesInfo, organizeSubFiles map[string][]string) map[string][]string {
  233. var fullSeasonSubDict = make(map[string][]string)
  234. for _, season := range seriesInfo.SeasonDict {
  235. seasonKey := model.GetEpisodeKeyName(season, 0)
  236. subs, ok := organizeSubFiles[seasonKey]
  237. if ok == false {
  238. continue
  239. }
  240. for _, sub := range subs {
  241. subFileName := filepath.Base(sub)
  242. newSeasonSubRootPath := path.Join(seriesInfo.DirPath, "Sub_"+seasonKey)
  243. _ = os.MkdirAll(newSeasonSubRootPath, os.ModePerm)
  244. newSubFullPath := path.Join(newSeasonSubRootPath, subFileName)
  245. _, err := model.CopyFile(newSubFullPath, sub)
  246. if err != nil {
  247. d.log.Errorln("saveFullSeasonSub", subFileName, err)
  248. continue
  249. }
  250. // 从字幕的文件名推断是 哪一季 的 那一集
  251. _, gusSeason, gusEpisode, err := model.GetSeasonAndEpisodeFromSubFileName(subFileName)
  252. if err != nil {
  253. return nil
  254. }
  255. // 把整季的字幕缓存位置也提供出去,如果之前没有下载到的,这里返回出来的可以补上
  256. seasonEpsKey := model.GetEpisodeKeyName(gusSeason, gusEpisode)
  257. _, ok := fullSeasonSubDict[seasonEpsKey]
  258. if ok == false {
  259. // 初始化
  260. fullSeasonSubDict[seasonEpsKey] = make([]string, 0)
  261. }
  262. fullSeasonSubDict[seasonEpsKey] = append(fullSeasonSubDict[seasonEpsKey], sub)
  263. }
  264. }
  265. return fullSeasonSubDict
  266. }
  267. // 在前面需要进行语言的筛选、排序,这里仅仅是存储
  268. func (d Downloader) writeSubFile2VideoPath(videoFileFullPath string, finalSubFile common.SubParserFileInfo, extraSubPreName string) error {
  269. videoRootPath := filepath.Dir(videoFileFullPath)
  270. embyLanExtName := model.Lang2EmbyName(finalSubFile.Lang)
  271. // 构建视频文件加 emby 的字幕预研要求名称
  272. videoFileNameWithOutExt := strings.ReplaceAll(filepath.Base(videoFileFullPath),
  273. filepath.Ext(videoFileFullPath), "")
  274. if extraSubPreName != "" {
  275. extraSubPreName = "[" + extraSubPreName +"]"
  276. }
  277. subNewName := videoFileNameWithOutExt + embyLanExtName + extraSubPreName + finalSubFile.Ext
  278. desSubFullPath := path.Join(videoRootPath, subNewName)
  279. // 最后写入字幕
  280. err := utils.OutputFile(desSubFullPath, finalSubFile.Data)
  281. if err != nil {
  282. return err
  283. }
  284. d.log.Infoln("OrgSubName:", finalSubFile.Name)
  285. d.log.Infoln("SubDownAt:", desSubFullPath)
  286. return nil
  287. }
  288. // copySubFile2DesFolder 拷贝字幕文件到目标文件夹
  289. func (d Downloader) copySubFile2DesFolder(desFolder string, subFiles []string) error {
  290. // 需要进行字幕文件的缓存
  291. // 把缓存的文件夹新建出来
  292. desFolderFullPath := path.Join(desFolder, common.SubTmpFolderName)
  293. err := os.MkdirAll(desFolderFullPath, os.ModePerm)
  294. if err != nil {
  295. return err
  296. }
  297. // 复制下载在 tmp 文件夹中的字幕文件到视频文件夹下面
  298. for _, subFile := range subFiles {
  299. newFn := path.Join(desFolderFullPath, filepath.Base(subFile))
  300. _, err = model.CopyFile(newFn, subFile)
  301. if err != nil {
  302. return err
  303. }
  304. }
  305. return nil
  306. }
  307. type InputData struct {
  308. OneVideoFullPath string
  309. Index int
  310. }