sub_helper.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. package sub_helper
  2. import (
  3. "github.com/allanpk716/ChineseSubFinder/internal/common"
  4. "github.com/allanpk716/ChineseSubFinder/internal/pkg"
  5. "github.com/allanpk716/ChineseSubFinder/internal/pkg/archive_helper"
  6. "github.com/allanpk716/ChineseSubFinder/internal/pkg/decode"
  7. "github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
  8. "github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
  9. "github.com/allanpk716/ChineseSubFinder/internal/types"
  10. "github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
  11. "github.com/allanpk716/ChineseSubFinder/internal/types/supplier"
  12. "github.com/go-rod/rod/lib/utils"
  13. "io/ioutil"
  14. "os"
  15. "path"
  16. "path/filepath"
  17. "regexp"
  18. "strconv"
  19. "strings"
  20. )
  21. // OrganizeDlSubFiles 需要从汇总来是网站字幕中,解压对应的压缩包中的字幕出来
  22. func OrganizeDlSubFiles(tmpFolderName string, subInfos []supplier.SubInfo) (map[string][]string, error) {
  23. // 缓存列表,整理后的字幕列表
  24. // SxEx - []string 字幕的路径
  25. var siteSubInfoDict = make(map[string][]string)
  26. tmpFolderFullPath, err := pkg.GetTmpFolder(tmpFolderName)
  27. if err != nil {
  28. return nil, err
  29. }
  30. // 把后缀名给改好
  31. ChangeVideoExt2SubExt(subInfos)
  32. // 第三方的解压库,首先不支持 io.Reader 的操作,也就是得缓存到本地硬盘再读取解压
  33. // 且使用 walk 会无法解压 rar,得指定具体的实例,太麻烦了,直接用通用的接口得了,就是得都缓存下来再判断
  34. // 基于以上两点,写了一堆啰嗦的逻辑···
  35. for i := range subInfos {
  36. // 先存下来,保存是时候需要前缀,前缀就是从那个网站下载来的
  37. nowFileSaveFullPath := path.Join(tmpFolderFullPath, GetFrontNameAndOrgName(&subInfos[i]))
  38. err = utils.OutputFile(nowFileSaveFullPath, subInfos[i].Data)
  39. if err != nil {
  40. log_helper.GetLogger().Errorln("getFrontNameAndOrgName - OutputFile", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  41. continue
  42. }
  43. nowExt := strings.ToLower(subInfos[i].Ext)
  44. epsKey := pkg.GetEpisodeKeyName(subInfos[i].Season, subInfos[i].Episode)
  45. _, ok := siteSubInfoDict[epsKey]
  46. if ok == false {
  47. // 不存在则实例化
  48. siteSubInfoDict[epsKey] = make([]string, 0)
  49. }
  50. if nowExt != ".zip" && nowExt != ".tar" && nowExt != ".rar" && nowExt != ".7z" {
  51. // 是否是受支持的字幕类型
  52. if IsSubExtWanted(nowExt) == false {
  53. continue
  54. }
  55. // 加入缓存列表
  56. siteSubInfoDict[epsKey] = append(siteSubInfoDict[epsKey], nowFileSaveFullPath)
  57. } else {
  58. // 那么就是需要解压的文件了
  59. // 解压,给一个单独的文件夹
  60. unzipTmpFolder := path.Join(tmpFolderFullPath, subInfos[i].FromWhere)
  61. err = os.MkdirAll(unzipTmpFolder, os.ModePerm)
  62. if err != nil {
  63. return nil, err
  64. }
  65. err = archive_helper.UnArchiveFile(nowFileSaveFullPath, unzipTmpFolder)
  66. // 解压完成后,遍历受支持的字幕列表,加入缓存列表
  67. if err != nil {
  68. log_helper.GetLogger().Errorln("archiver.UnArchive", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  69. continue
  70. }
  71. // 搜索这个目录下的所有符合字幕格式的文件
  72. subFileFullPaths, err := SearchMatchedSubFile(unzipTmpFolder)
  73. if err != nil {
  74. log_helper.GetLogger().Errorln("searchMatchedSubFile", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  75. continue
  76. }
  77. // 这里需要给这些下载到的文件进行改名,加是从那个网站来的前缀,后续好查找
  78. for _, fileFullPath := range subFileFullPaths {
  79. newSubName := AddFrontName(subInfos[i], filepath.Base(fileFullPath))
  80. newSubNameFullPath := path.Join(tmpFolderFullPath, newSubName)
  81. // 改名
  82. err = os.Rename(fileFullPath, newSubNameFullPath)
  83. if err != nil {
  84. log_helper.GetLogger().Errorln("os.Rename", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  85. continue
  86. }
  87. // 加入缓存列表
  88. siteSubInfoDict[epsKey] = append(siteSubInfoDict[epsKey], newSubNameFullPath)
  89. }
  90. }
  91. }
  92. return siteSubInfoDict, nil
  93. }
  94. // ChangeVideoExt2SubExt 检测 Name,如果是视频的后缀名就改为字幕的后缀名
  95. func ChangeVideoExt2SubExt(subInfos []supplier.SubInfo) {
  96. for x, info := range subInfos {
  97. tmpSubFileName := info.Name
  98. // 如果后缀名是下载字幕目标的后缀名 或者 是压缩包格式的,则跳过
  99. if strings.Contains(tmpSubFileName, info.Ext) == true || archive_helper.IsWantedArchiveExtName(tmpSubFileName) == true {
  100. } else {
  101. subInfos[x].Name = tmpSubFileName + info.Ext
  102. }
  103. }
  104. }
  105. // SelectChineseBestBilingualSubtitle 找到合适的双语中文字幕,简体->繁体,以及 字幕类型的优先级选择
  106. func SelectChineseBestBilingualSubtitle(subs []subparser.FileInfo, subTypePriority int) *subparser.FileInfo {
  107. // 先傻一点实现优先双语的,之前的写法有 bug
  108. for _, info := range subs {
  109. // 找到了中文字幕
  110. if language.HasChineseLang(info.Lang) == true {
  111. // 字幕的优先级 0 - 原样, 1 - srt , 2 - ass/ssa
  112. if subTypePriority == 1 {
  113. // 1 - srt
  114. if strings.ToLower(info.Ext) == common.SubExtSRT {
  115. // 优先双语
  116. if language.IsBilingualSubtitle(info.Lang) == true {
  117. return &info
  118. }
  119. }
  120. } else if subTypePriority == 2 {
  121. // 2 - ass/ssa
  122. if strings.ToLower(info.Ext) == common.SubExtASS || strings.ToLower(info.Ext) == common.SubExtSSA {
  123. // 优先双语
  124. if language.IsBilingualSubtitle(info.Lang) == true {
  125. return &info
  126. }
  127. }
  128. } else {
  129. // 优先双语
  130. if language.IsBilingualSubtitle(info.Lang) == true {
  131. return &info
  132. }
  133. }
  134. }
  135. }
  136. return nil
  137. }
  138. // SelectChineseBestSubtitle 找到合适的中文字幕,简体->繁体,以及 字幕类型的优先级选择
  139. func SelectChineseBestSubtitle(subs []subparser.FileInfo, subTypePriority int) *subparser.FileInfo {
  140. // 先傻一点实现优先双语的,之前的写法有 bug
  141. for _, info := range subs {
  142. // 找到了中文字幕
  143. if language.HasChineseLang(info.Lang) == true {
  144. // 字幕的优先级 0 - 原样, 1 - srt , 2 - ass/ssa
  145. if subTypePriority == 1 {
  146. // 1 - srt
  147. if strings.ToLower(info.Ext) == common.SubExtSRT {
  148. return &info
  149. }
  150. } else if subTypePriority == 2 {
  151. // 2 - ass/ssa
  152. if strings.ToLower(info.Ext) == common.SubExtASS || strings.ToLower(info.Ext) == common.SubExtSSA {
  153. return &info
  154. }
  155. } else {
  156. return &info
  157. }
  158. }
  159. }
  160. return nil
  161. }
  162. // GetFrontNameAndOrgName 返回的名称包含,那个网站下载的,这个网站中排名第几,文件名
  163. func GetFrontNameAndOrgName(info *supplier.SubInfo) string {
  164. infoName := ""
  165. fileName, err := decode.GetVideoInfoFromFileName(info.Name)
  166. if err != nil {
  167. log_helper.GetLogger().Warnln("", err)
  168. infoName = info.Name
  169. } else {
  170. infoName = fileName.Title + "_S" + strconv.Itoa(fileName.Season) + "E" + strconv.Itoa(fileName.Episode) + filepath.Ext(info.Name)
  171. }
  172. info.Name = infoName
  173. return "[" + info.FromWhere + "]_" + strconv.FormatInt(info.TopN, 10) + "_" + infoName
  174. }
  175. // AddFrontName 添加文件的前缀
  176. func AddFrontName(info supplier.SubInfo, orgName string) string {
  177. return "[" + info.FromWhere + "]_" + strconv.FormatInt(info.TopN, 10) + "_" + orgName
  178. }
  179. // SearchMatchedSubFile 搜索符合后缀名的视频文件,排除 Sub_SxE0 这样的文件夹中的文件
  180. func SearchMatchedSubFile(dir string) ([]string, error) {
  181. // 这里有个梗,会出现 __MACOSX 这类文件夹,那么里面会有一样的文件,需要用文件大小排除一下,至少大于 1 kb 吧
  182. var fileFullPathList = make([]string, 0)
  183. pathSep := string(os.PathSeparator)
  184. files, err := ioutil.ReadDir(dir)
  185. if err != nil {
  186. return nil, err
  187. }
  188. for _, curFile := range files {
  189. fullPath := dir + pathSep + curFile.Name()
  190. if curFile.IsDir() {
  191. // 需要排除 Sub_S1E0、Sub_S2E0 这样的整季的字幕文件夹,这里仅仅是缓存,不会被加载的
  192. matched := regOneSeasonSubFolderNameMatch.FindAllStringSubmatch(curFile.Name(), -1)
  193. if len(matched) > 0 {
  194. continue
  195. }
  196. // 内层的错误就无视了
  197. oneList, _ := SearchMatchedSubFile(fullPath)
  198. if oneList != nil {
  199. fileFullPathList = append(fileFullPathList, oneList...)
  200. }
  201. } else {
  202. // 这里就是文件了
  203. if curFile.Size() < 1000 {
  204. continue
  205. }
  206. if IsSubExtWanted(filepath.Ext(curFile.Name())) == true {
  207. fileFullPathList = append(fileFullPathList, fullPath)
  208. }
  209. }
  210. }
  211. return fileFullPathList, nil
  212. }
  213. // SearchVideoMatchSubFileAndRemoveDefaultMark 找到找个视频目录下相匹配的字幕,同时去除这些字幕中 .default 的标记
  214. func SearchVideoMatchSubFileAndRemoveDefaultMark(oneVideoFullPath string) error {
  215. dir := filepath.Dir(oneVideoFullPath)
  216. fileName := filepath.Base(oneVideoFullPath)
  217. fileName = strings.ToLower(fileName)
  218. fileName = strings.ReplaceAll(fileName, filepath.Ext(fileName), "")
  219. pathSep := string(os.PathSeparator)
  220. files, err := ioutil.ReadDir(dir)
  221. if err != nil {
  222. return err
  223. }
  224. for _, curFile := range files {
  225. if curFile.IsDir() {
  226. continue
  227. } else {
  228. // 这里就是文件了
  229. if curFile.Size() < 1000 {
  230. continue
  231. }
  232. // 判断的时候用小写的,后续重命名的时候用原有的名称
  233. nowFileName := strings.ToLower(curFile.Name())
  234. // 后缀名得对
  235. if IsSubExtWanted(filepath.Ext(nowFileName)) == false {
  236. continue
  237. }
  238. // 字幕文件名应该包含 视频文件名(无后缀)
  239. if strings.Contains(nowFileName, fileName) == false {
  240. continue
  241. }
  242. // 得包含 .default. 找个关键词
  243. if strings.Contains(nowFileName, types.Emby_default+".") == false {
  244. continue
  245. }
  246. oldPath := dir + pathSep + curFile.Name()
  247. newPath := dir + pathSep + strings.ReplaceAll(curFile.Name(), types.Emby_default+".", ".")
  248. err = os.Rename(oldPath, newPath)
  249. if err != nil {
  250. return err
  251. }
  252. }
  253. }
  254. return nil
  255. }
  256. // IsOldVersionSubPrefixName 是否是老版本的字幕命名 .chs_en[shooter] ,符合也返回这个部分+字幕格式后缀名 .chs_en[shooter].ass, 修改后的名称
  257. func IsOldVersionSubPrefixName(subFileName string) (bool, string, string) {
  258. /*
  259. 传入的必须是字幕格式的文件,这个就再之前判断,不要在这里再判断
  260. 传入的文件名可能有一下几种情况
  261. 无罪之最 - S01E01 - 重建生活.chs[shooter].ass
  262. 无罪之最 - S01E03 - 初见端倪.zh.srt
  263. Loki - S01E01 - Glorious Purpose WEBDL-1080p Proper.chs_en.ass
  264. 那么就需要先剔除,字幕的格式后缀名,然后再向后取后缀名就是 .chs[shooter] or .zh
  265. 再判断即可
  266. */
  267. // 无罪之最 - S01E01 - 重建生活.chs[shooter].ass -> 无罪之最 - S01E01 - 重建生活.chs[shooter]
  268. subTypeExt := filepath.Ext(subFileName)
  269. subFileNameWithOutExt := strings.ReplaceAll(subFileName, subTypeExt, "")
  270. // .chs[shooter]
  271. nowExt := filepath.Ext(subFileNameWithOutExt)
  272. // .chs_en[shooter].ass
  273. orgMixExt := nowExt + subTypeExt
  274. orgFileNameWithOutOrgMixExt := strings.ReplaceAll(subFileName, orgMixExt, "")
  275. // 这里也有两种情况,一种是单字幕 SaveMultiSub: false
  276. // 一种的保存了多字幕 SaveMultiSub: true
  277. // 先判断 单字幕
  278. switch nowExt {
  279. case types.Emby_chs:
  280. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChs, subTypeExt, "", true)
  281. case types.Emby_cht:
  282. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangCht, subTypeExt, "", false)
  283. case types.Emby_chs_en:
  284. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChsEn, subTypeExt, "", true)
  285. case types.Emby_cht_en:
  286. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChtEn, subTypeExt, "", false)
  287. case types.Emby_chs_jp:
  288. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChsJp, subTypeExt, "", true)
  289. case types.Emby_cht_jp:
  290. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChtJp, subTypeExt, "", false)
  291. case types.Emby_chs_kr:
  292. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChsKr, subTypeExt, "", true)
  293. case types.Emby_cht_kr:
  294. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, types.MatchLangChtKr, subTypeExt, "", false)
  295. }
  296. // 再判断 多字幕情况
  297. spStrings := strings.Split(nowExt, "[")
  298. if len(spStrings) != 2 {
  299. return false, "", ""
  300. }
  301. // 分两段来判断是否符合标准
  302. // 第一段
  303. firstOk := true
  304. lang := types.MatchLangChs
  305. site := ""
  306. switch spStrings[0] {
  307. case types.Emby_chs:
  308. lang = types.MatchLangChs
  309. case types.Emby_cht:
  310. lang = types.MatchLangCht
  311. case types.Emby_chs_en:
  312. lang = types.MatchLangChsEn
  313. case types.Emby_cht_en:
  314. lang = types.MatchLangChtEn
  315. case types.Emby_chs_jp:
  316. lang = types.MatchLangChsJp
  317. case types.Emby_cht_jp:
  318. lang = types.MatchLangChtJp
  319. case types.Emby_chs_kr:
  320. lang = types.MatchLangChsKr
  321. case types.Emby_cht_kr:
  322. lang = types.MatchLangChtKr
  323. default:
  324. firstOk = false
  325. }
  326. // 第二段
  327. secondOk := true
  328. tmpSecond := strings.ReplaceAll(spStrings[1], "]", "")
  329. switch tmpSecond {
  330. case common.SubSiteZiMuKu:
  331. site = common.SubSiteZiMuKu
  332. case common.SubSiteSubHd:
  333. site = common.SubSiteSubHd
  334. case common.SubSiteShooter:
  335. site = common.SubSiteShooter
  336. case common.SubSiteXunLei:
  337. site = common.SubSiteXunLei
  338. default:
  339. secondOk = false
  340. }
  341. // 都要符合条件
  342. if firstOk == true && secondOk == true {
  343. return true, orgMixExt, makeMixSubExtString(orgFileNameWithOutOrgMixExt, lang, subTypeExt, site, false)
  344. }
  345. return false, "", ""
  346. }
  347. // GenerateMixSubName 这里会生成类似的文件名 xxxx.chinese(中英,shooter)
  348. func GenerateMixSubName(videoFileName, subExt string, subLang types.Language, extraSubPreName string) (string, string, string) {
  349. videoFileNameWithOutExt := strings.ReplaceAll(filepath.Base(videoFileName),
  350. filepath.Ext(videoFileName), "")
  351. note := ""
  352. // extraSubPreName 那个字幕网站下载的
  353. if extraSubPreName != "" {
  354. note = "," + extraSubPreName
  355. }
  356. defaultString := ".default"
  357. forcedString := ".forced"
  358. subNewName := videoFileNameWithOutExt + ".chinese" + "(" + language.Lang2ChineseString(subLang) + note + ")" + subExt
  359. subNewNameWithDefault := videoFileNameWithOutExt + ".chinese" + "(" + language.Lang2ChineseString(subLang) + note + ")" + defaultString + subExt
  360. subNewNameWithForced := videoFileNameWithOutExt + ".chinese" + "(" + language.Lang2ChineseString(subLang) + note + ")" + forcedString + subExt
  361. return subNewName, subNewNameWithDefault, subNewNameWithForced
  362. }
  363. func makeMixSubExtString(orgFileNameWithOutExt, lang string, ext, site string, beDefault bool) string {
  364. tmpDefault := ""
  365. if beDefault == true {
  366. tmpDefault = types.Emby_default
  367. }
  368. if site == "" {
  369. return orgFileNameWithOutExt + types.Emby_chinese + "(" + lang + ")" + tmpDefault + ext
  370. }
  371. return orgFileNameWithOutExt + types.Emby_chinese + "(" + lang + "," + site + ")" + tmpDefault + ext
  372. }
  373. // DeleteOneSeasonSubCacheFolder 删除一个连续剧中的所有一季字幕的缓存文件夹
  374. func DeleteOneSeasonSubCacheFolder(seriesDir string) error {
  375. files, err := ioutil.ReadDir(seriesDir)
  376. if err != nil {
  377. return err
  378. }
  379. pathSep := string(os.PathSeparator)
  380. for _, curFile := range files {
  381. if curFile.IsDir() == true {
  382. matched := regOneSeasonSubFolderNameMatch.FindAllStringSubmatch(curFile.Name(), -1)
  383. if matched == nil || len(matched) < 1 {
  384. continue
  385. }
  386. fullPath := seriesDir + pathSep + curFile.Name()
  387. err = os.RemoveAll(fullPath)
  388. if err != nil {
  389. return err
  390. }
  391. }
  392. }
  393. return nil
  394. }
  395. var (
  396. regOneSeasonSubFolderNameMatch = regexp.MustCompile(`(?m)^Sub_S\dE0`)
  397. )