1
0

subhd.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. package subhd
  2. import (
  3. "bytes"
  4. "fmt"
  5. "github.com/PuerkitoBio/goquery"
  6. "github.com/Tnze/go.num/v2/zh"
  7. "github.com/allanpk716/ChineseSubFinder/internal/common"
  8. "github.com/allanpk716/ChineseSubFinder/internal/pkg/decode"
  9. "github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
  10. "github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
  11. "github.com/allanpk716/ChineseSubFinder/internal/pkg/notify_center"
  12. "github.com/allanpk716/ChineseSubFinder/internal/pkg/rod_helper"
  13. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
  14. "github.com/allanpk716/ChineseSubFinder/internal/types"
  15. "github.com/allanpk716/ChineseSubFinder/internal/types/language"
  16. "github.com/allanpk716/ChineseSubFinder/internal/types/series"
  17. "github.com/allanpk716/ChineseSubFinder/internal/types/supplier"
  18. "github.com/go-rod/rod"
  19. "github.com/go-rod/rod/lib/launcher"
  20. "github.com/nfnt/resize"
  21. "github.com/sirupsen/logrus"
  22. "image/jpeg"
  23. "math"
  24. "net/url"
  25. "os"
  26. "path/filepath"
  27. "regexp"
  28. "strings"
  29. "time"
  30. )
  31. type Supplier struct {
  32. reqParam types.ReqParam
  33. log *logrus.Logger
  34. topic int
  35. rodLauncher *launcher.Launcher
  36. tt time.Duration
  37. debugMode bool
  38. }
  39. func NewSupplier(_reqParam ...types.ReqParam) *Supplier {
  40. sup := Supplier{}
  41. sup.log = log_helper.GetLogger()
  42. sup.topic = common.DownloadSubsPerSite
  43. if len(_reqParam) > 0 {
  44. sup.reqParam = _reqParam[0]
  45. if sup.reqParam.Topic > 0 && sup.reqParam.Topic != sup.topic {
  46. sup.topic = sup.reqParam.Topic
  47. }
  48. }
  49. // 默认超时是 2 * 60s,如果是调试模式则是 5 min
  50. sup.tt = common.HTMLTimeOut
  51. sup.debugMode = sup.reqParam.DebugMode
  52. if sup.reqParam.DebugMode == true {
  53. sup.tt = common.OneVideoProcessTimeOut
  54. }
  55. return &sup
  56. }
  57. func (s Supplier) GetSupplierName() string {
  58. return common.SubSiteSubHd
  59. }
  60. func (s Supplier) GetReqParam() types.ReqParam {
  61. return s.reqParam
  62. }
  63. func (s Supplier) GetSubListFromFile4Movie(filePath string) ([]supplier.SubInfo, error) {
  64. return s.getSubListFromFile4Movie(filePath)
  65. }
  66. func (s Supplier) GetSubListFromFile4Series(seriesInfo *series.SeriesInfo) ([]supplier.SubInfo, error) {
  67. var browser *rod.Browser
  68. // TODO 是用本地的 Browser 还是远程的,推荐是远程的
  69. browser, err := rod_helper.NewBrowser(s.reqParam.HttpProxy, true)
  70. if err != nil {
  71. return nil, err
  72. }
  73. defer func() {
  74. _ = browser.Close()
  75. }()
  76. var subInfos = make([]supplier.SubInfo, 0)
  77. var subList = make([]HdListItem, 0)
  78. for value := range seriesInfo.NeedDlSeasonDict {
  79. // 第一级界面,找到影片的详情界面
  80. keyword := seriesInfo.Name + " 第" + zh.Uint64(value).String() + "季"
  81. detailPageUrl, err := s.step0(browser, keyword)
  82. if err != nil {
  83. s.log.Errorln("subhd step0", keyword)
  84. return nil, err
  85. }
  86. if detailPageUrl == "" {
  87. // 如果只是搜索不到,则继续换关键词
  88. s.log.Warning("subhd first search keyword", keyword, "not found")
  89. keyword = seriesInfo.Name
  90. s.log.Warning("subhd Retry", keyword)
  91. detailPageUrl, err = s.step0(browser, keyword)
  92. if err != nil {
  93. s.log.Errorln("subhd step0", keyword)
  94. return nil, err
  95. }
  96. }
  97. if detailPageUrl == "" {
  98. s.log.Warning("subhd search keyword", keyword, "not found")
  99. continue
  100. }
  101. // 列举字幕
  102. oneSubList, err := s.step1(browser, detailPageUrl, false)
  103. if err != nil {
  104. s.log.Errorln("subhd step1", keyword)
  105. return nil, err
  106. }
  107. subList = append(subList, oneSubList...)
  108. }
  109. // 与剧集需要下载的集 List 进行比较,找到需要下载的列表
  110. // 找到那些 Eps 需要下载字幕的
  111. subInfoNeedDownload := s.whichEpisodeNeedDownloadSub(seriesInfo, subList)
  112. // 下载字幕
  113. for i, item := range subInfoNeedDownload {
  114. bok, hdContent, err := s.step2Ex(browser, item.Url)
  115. if err != nil {
  116. s.log.Errorln("subhd step2Ex", err)
  117. continue
  118. }
  119. if bok == false {
  120. s.log.Errorln("subhd step2Ex return false")
  121. continue
  122. }
  123. oneSubInfo := supplier.NewSubInfo(s.GetSupplierName(), int64(i), hdContent.Filename, language.ChineseSimple, my_util.AddBaseUrl(common.SubSubHDRootUrl, item.Url), 0,
  124. 0, hdContent.Ext, hdContent.Data)
  125. oneSubInfo.Season = item.Season
  126. oneSubInfo.Episode = item.Episode
  127. subInfos = append(subInfos, *oneSubInfo)
  128. }
  129. return subInfos, nil
  130. }
  131. func (s Supplier) GetSubListFromFile4Anime(seriesInfo *series.SeriesInfo) ([]supplier.SubInfo, error) {
  132. panic("not implemented")
  133. }
  134. func (s Supplier) getSubListFromFile4Movie(filePath string) ([]supplier.SubInfo, error) {
  135. /*
  136. 虽然是传入视频文件路径,但是其实需要读取对应的视频文件目录下的
  137. movie.xml 以及 *.nfo,找到 IMDB id
  138. 优先通过 IMDB id 去查找字幕
  139. 如果找不到,再靠文件名提取影片名称去查找
  140. */
  141. // 得到这个视频文件名中的信息
  142. info, _, err := decode.GetVideoInfoFromFileFullPath(filePath)
  143. if err != nil {
  144. return nil, err
  145. }
  146. // 找到这个视频文件,尝试得到 IMDB ID
  147. // 目前测试来看,加入 年 这个关键词去搜索,对 2020 年后的影片有利,因为网站有统一的详细页面了,而之前的,没有,会影响识别
  148. // 所以,year >= 2020 年,则可以多加一个关键词(年)去搜索影片
  149. imdbInfo, err := decode.GetImdbInfo4Movie(filePath)
  150. if err != nil {
  151. // 允许的错误,跳过,继续进行文件名的搜索
  152. s.log.Errorln("model.GetImdbInfo", err)
  153. }
  154. var subInfoList []supplier.SubInfo
  155. if imdbInfo.ImdbId != "" {
  156. // 先用 imdb id 找
  157. subInfoList, err = s.getSubListFromKeyword4Movie(imdbInfo.ImdbId)
  158. if err != nil {
  159. // 允许的错误,跳过,继续进行文件名的搜索
  160. s.log.Errorln(s.GetSupplierName(), "keyword:", imdbInfo.ImdbId)
  161. s.log.Errorln("getSubListFromKeyword4Movie", "IMDBID can not found sub", filePath, err)
  162. }
  163. // 如果有就优先返回
  164. if len(subInfoList) > 0 {
  165. return subInfoList, nil
  166. }
  167. }
  168. // 如果没有,那么就用文件名查找
  169. searchKeyword := my_util.VideoNameSearchKeywordMaker(info.Title, imdbInfo.Year)
  170. subInfoList, err = s.getSubListFromKeyword4Movie(searchKeyword)
  171. if err != nil {
  172. s.log.Errorln(s.GetSupplierName(), "keyword:", searchKeyword)
  173. return nil, err
  174. }
  175. return subInfoList, nil
  176. }
  177. func (s Supplier) getSubListFromKeyword4Movie(keyword string) ([]supplier.SubInfo, error) {
  178. var browser *rod.Browser
  179. // TODO 是用本地的 Browser 还是远程的,推荐是远程的
  180. browser, err := rod_helper.NewBrowser(s.reqParam.HttpProxy, true)
  181. if err != nil {
  182. return nil, err
  183. }
  184. defer func() {
  185. _ = browser.Close()
  186. }()
  187. var subInfos []supplier.SubInfo
  188. detailPageUrl, err := s.step0(browser, keyword)
  189. if err != nil {
  190. return nil, err
  191. }
  192. // 没有搜索到字幕
  193. if detailPageUrl == "" {
  194. return nil, nil
  195. }
  196. subList, err := s.step1(browser, detailPageUrl, true)
  197. if err != nil {
  198. return nil, err
  199. }
  200. for i, item := range subList {
  201. bok, hdContent, err := s.step2Ex(browser, item.Url)
  202. time.Sleep(time.Second)
  203. if err != nil {
  204. s.log.Errorln("subhd step2Ex", err)
  205. continue
  206. }
  207. if bok == false {
  208. s.log.Errorln("subhd step2Ex return false")
  209. continue
  210. }
  211. subInfos = append(subInfos, *supplier.NewSubInfo(s.GetSupplierName(), int64(i), hdContent.Filename, language.ChineseSimple, my_util.AddBaseUrl(common.SubSubHDRootUrl, item.Url), 0, 0, hdContent.Ext, hdContent.Data))
  212. }
  213. return subInfos, nil
  214. }
  215. func (s Supplier) whichEpisodeNeedDownloadSub(seriesInfo *series.SeriesInfo, allSubList []HdListItem) []HdListItem {
  216. // 字幕很多,考虑效率,需要做成字典
  217. // key SxEx - SubInfos
  218. var allSubDict = make(map[string][]HdListItem)
  219. // 全季的字幕列表
  220. var oneSeasonSubDict = make(map[string][]HdListItem)
  221. for _, subInfo := range allSubList {
  222. _, season, episode, err := decode.GetSeasonAndEpisodeFromSubFileName(subInfo.Title)
  223. if err != nil {
  224. s.log.Errorln("whichEpisodeNeedDownloadSub.GetVideoInfoFromFileFullPath", subInfo.Title, err)
  225. continue
  226. }
  227. subInfo.Season = season
  228. subInfo.Episode = episode
  229. epsKey := my_util.GetEpisodeKeyName(season, episode)
  230. _, ok := allSubDict[epsKey]
  231. if ok == false {
  232. // 初始化
  233. allSubDict[epsKey] = make([]HdListItem, 0)
  234. if season != 0 && episode == 0 {
  235. oneSeasonSubDict[epsKey] = make([]HdListItem, 0)
  236. }
  237. }
  238. // 添加
  239. allSubDict[epsKey] = append(allSubDict[epsKey], subInfo)
  240. if season != 0 && episode == 0 {
  241. oneSeasonSubDict[epsKey] = append(oneSeasonSubDict[epsKey], subInfo)
  242. }
  243. }
  244. // 本地的视频列表,找到没有字幕的
  245. // 需要进行下载字幕的列表
  246. var subInfoNeedDownload = make([]HdListItem, 0)
  247. // 有那些 Eps 需要下载的,按 SxEx 反回 epsKey
  248. for epsKey, epsInfo := range seriesInfo.NeedDlEpsKeyList {
  249. // 从一堆字幕里面找合适的
  250. value, ok := allSubDict[epsKey]
  251. // 是否有
  252. if ok == true && len(value) > 0 {
  253. value[0].Season = epsInfo.Season
  254. value[0].Episode = epsInfo.Episode
  255. subInfoNeedDownload = append(subInfoNeedDownload, value[0])
  256. } else {
  257. s.log.Infoln("SubHD Not Find Sub can be download", epsInfo.Title, epsInfo.Season, epsInfo.Episode)
  258. }
  259. }
  260. // 全季的字幕列表,也拼进去,后面进行下载
  261. for _, infos := range oneSeasonSubDict {
  262. subInfoNeedDownload = append(subInfoNeedDownload, infos[0])
  263. }
  264. // 返回前,需要把每一个 Eps 的 Season Episode 信息填充到每个 SubInfo 中
  265. return subInfoNeedDownload
  266. }
  267. // step0 找到这个影片的详情列表
  268. func (s Supplier) step0(browser *rod.Browser, keyword string) (string, error) {
  269. var err error
  270. defer func() {
  271. if err != nil {
  272. notify_center.Notify.Add("subhd_step0", err.Error())
  273. }
  274. }()
  275. result, page, err := s.httpGetFromBrowser(browser, fmt.Sprintf(common.SubSubHDSearchUrl, url.QueryEscape(keyword)))
  276. if err != nil {
  277. return "", err
  278. }
  279. defer func() {
  280. _ = page.Close()
  281. }()
  282. // 是否有查找到的结果,至少要有结果。根据这里这样下面才能判断是分析失效了,还是就是没有结果而已
  283. re := regexp.MustCompile(`共\s*(\d+)\s*条`)
  284. matched := re.FindAllStringSubmatch(result, -1)
  285. if matched == nil || len(matched) < 1 {
  286. return "", common.SubHDStep0SubCountElementNotFound
  287. }
  288. subCount, err := decode.GetNumber2int(matched[0][0])
  289. if err != nil {
  290. return "", err
  291. }
  292. // 如果所搜没有找到字幕,就要返回
  293. if subCount < 1 {
  294. return "", nil
  295. }
  296. // 这里是确认能继续分析的详细连接
  297. doc, err := goquery.NewDocumentFromReader(strings.NewReader(result))
  298. if err != nil {
  299. return "", err
  300. }
  301. imgSelection := doc.Find("img.rounded-start")
  302. _, ok := imgSelection.Attr("src")
  303. if ok == true {
  304. if len(imgSelection.Nodes) < 1 {
  305. return "", common.SubHDStep0ImgParentLessThan1
  306. }
  307. step1Url := ""
  308. if imgSelection.Nodes[0].Parent.Data == "a" {
  309. // 第一个父级是不是超链接
  310. for _, attribute := range imgSelection.Nodes[0].Parent.Attr {
  311. if attribute.Key == "href" {
  312. step1Url = attribute.Val
  313. break
  314. }
  315. }
  316. } else if imgSelection.Nodes[0].Parent.Parent.Data == "a" {
  317. // 第二个父级是不是超链接
  318. for _, attribute := range imgSelection.Nodes[0].Parent.Parent.Attr {
  319. if attribute.Key == "href" {
  320. step1Url = attribute.Val
  321. break
  322. }
  323. }
  324. }
  325. if step1Url == "" {
  326. return "", common.SubHDStep0HrefIsNull
  327. }
  328. return step1Url, nil
  329. } else {
  330. return "", common.SubHDStep0HrefIsNull
  331. }
  332. }
  333. // step1 获取影片的详情字幕列表
  334. func (s Supplier) step1(browser *rod.Browser, detailPageUrl string, isMovieOrSeries bool) ([]HdListItem, error) {
  335. var err error
  336. defer func() {
  337. if err != nil {
  338. notify_center.Notify.Add("subhd_step1", err.Error())
  339. }
  340. }()
  341. detailPageUrl = my_util.AddBaseUrl(common.SubSubHDRootUrl, detailPageUrl)
  342. result, page, err := s.httpGetFromBrowser(browser, detailPageUrl)
  343. if err != nil {
  344. return nil, err
  345. }
  346. defer func() {
  347. _ = page.Close()
  348. }()
  349. doc, err := goquery.NewDocumentFromReader(strings.NewReader(result))
  350. if err != nil {
  351. return nil, err
  352. }
  353. var lists []HdListItem
  354. const subTableKeyword = ".pt-2"
  355. const oneSubTrTitleKeyword = "a.link-dark"
  356. const oneSubTrDownloadCountKeyword = "div.px-3"
  357. const oneSubLangAndTypeKeyword = ".text-secondary"
  358. doc.Find(subTableKeyword).EachWithBreak(func(i int, tr *goquery.Selection) bool {
  359. if tr.Find(oneSubTrTitleKeyword).Size() == 0 {
  360. return true
  361. }
  362. // 文件的下载页面,还需要分析
  363. downUrl, exists := tr.Find(oneSubTrTitleKeyword).Eq(0).Attr("href")
  364. if !exists {
  365. return true
  366. }
  367. // 文件名
  368. title := strings.TrimSpace(tr.Find(oneSubTrTitleKeyword).Text())
  369. // 字幕类型
  370. insideSubType := tr.Find(oneSubLangAndTypeKeyword).Text()
  371. if sub_parser_hub.IsSubTypeWanted(insideSubType) == false {
  372. return true
  373. }
  374. // 下载的次数
  375. downCount, err := decode.GetNumber2int(tr.Find(oneSubTrDownloadCountKeyword).Eq(1).Text())
  376. if err != nil {
  377. return true
  378. }
  379. listItem := HdListItem{}
  380. listItem.Url = downUrl
  381. listItem.BaseUrl = common.SubSubHDRootUrl
  382. listItem.Title = title
  383. listItem.DownCount = downCount
  384. // 电影,就需要第一个
  385. // 连续剧,需要多个
  386. if isMovieOrSeries == true {
  387. if len(lists) >= s.topic {
  388. return false
  389. }
  390. }
  391. lists = append(lists, listItem)
  392. return true
  393. })
  394. return lists, nil
  395. }
  396. // step2Ex 下载字幕 过防水墙
  397. func (s Supplier) step2Ex(browser *rod.Browser, subDownloadPageUrl string) (bool, *HdContent, error) {
  398. var err error
  399. defer func() {
  400. if err != nil {
  401. notify_center.Notify.Add("subhd_step2Ex", err.Error())
  402. }
  403. }()
  404. subDownloadPageUrl = my_util.AddBaseUrl(common.SubSubHDRootUrl, subDownloadPageUrl)
  405. _, page, err := s.httpGetFromBrowser(browser, subDownloadPageUrl)
  406. if err != nil {
  407. return false, nil, err
  408. }
  409. defer func() {
  410. _ = page.Close()
  411. }()
  412. // 需要先判断是否先要输入验证码,然后才到下载界面
  413. // 下载字幕
  414. bok, content, err := s.downloadSubFile(browser, page)
  415. if err != nil {
  416. return false, nil, err
  417. }
  418. if bok == false {
  419. return false, nil, nil
  420. }
  421. return true, content, nil
  422. }
  423. func (s Supplier) downloadSubFile(browser *rod.Browser, page *rod.Page) (bool, *HdContent, error) {
  424. var err error
  425. var doc *goquery.Document
  426. downloadSuccess := false
  427. fileName := ""
  428. fileByte := []byte{0}
  429. err = rod.Try(func() {
  430. tmpDir := filepath.Join(os.TempDir(), "rod", "downloads")
  431. wait := browser.WaitDownload(tmpDir)
  432. getDownloadFile := func() ([]byte, string, error) {
  433. info := wait()
  434. downloadPath := filepath.Join(tmpDir, info.GUID)
  435. defer func() { _ = os.Remove(downloadPath) }()
  436. b, err := os.ReadFile(downloadPath)
  437. if err != nil {
  438. return nil, "", err
  439. }
  440. return b, info.SuggestedFilename, nil
  441. }
  442. // 初始化页面用于查询元素
  443. pString := page.MustHTML()
  444. doc, err = goquery.NewDocumentFromReader(strings.NewReader(pString))
  445. if err != nil {
  446. return
  447. }
  448. // 移除广告
  449. page.MustEval(`testgssdqw = function () { if (document.getElementById("tbkp")) {document.getElementById("tbkp").remove()}; }`)
  450. page.MustEval(`testgssdqw()`)
  451. // 点击“验证获取下载地址”
  452. clickCodeBtn := doc.Find(btnClickCodeBtn)
  453. if len(clickCodeBtn.Nodes) < 1 {
  454. return
  455. }
  456. element := page.MustElement(btnClickCodeBtn)
  457. BtnCodeText := element.MustText()
  458. if strings.Contains(BtnCodeText, "验证") == true {
  459. // 那么需要填写验证码
  460. element.MustClick()
  461. time.Sleep(time.Second * 2)
  462. // 填写“验证码”
  463. page.MustEval(`$("#gzhcode").attr("value","` + common.SubhdCode + `");`)
  464. // 是否有“完成验证”按钮
  465. downBtn := doc.Find(btnCommitCode)
  466. if len(downBtn.Nodes) < 1 {
  467. return
  468. }
  469. element = page.MustElement(btnCommitCode)
  470. benCommit := element.MustText()
  471. if strings.Contains(benCommit, "验证") == false {
  472. log_helper.GetLogger().Errorln("btn not found 完整验证")
  473. return
  474. }
  475. element.MustClick()
  476. time.Sleep(time.Second * 2)
  477. // 点击下载按钮
  478. page.MustElement(btnClickCodeBtn).MustClick()
  479. } else if strings.Contains(BtnCodeText, "下载") == true {
  480. // 直接可以下载
  481. element.MustClick()
  482. time.Sleep(time.Second * 2)
  483. } else {
  484. log_helper.GetLogger().Errorln("btn not found 下载验证 or 下载")
  485. return
  486. }
  487. // 更新 page 的实例对应的 doc Content
  488. pString = page.MustHTML()
  489. doc, err = goquery.NewDocumentFromReader(strings.NewReader(pString))
  490. if err != nil {
  491. return
  492. }
  493. // 是否有腾讯的防水墙
  494. hasWaterWall := false
  495. waterWall := doc.Find(TCode)
  496. if len(waterWall.Nodes) >= 1 {
  497. hasWaterWall = true
  498. }
  499. log_helper.GetLogger().Debugln("Need pass WaterWall", hasWaterWall)
  500. // 过墙
  501. if hasWaterWall == true {
  502. s.passWaterWall(page)
  503. }
  504. time.Sleep(time.Second * 2)
  505. fileByte, fileName, err = getDownloadFile()
  506. if err != nil {
  507. return
  508. }
  509. downloadSuccess = true
  510. })
  511. if err != nil {
  512. return false, nil, err
  513. }
  514. var hdContent HdContent
  515. hdContent.Filename = fileName
  516. hdContent.Ext = filepath.Ext(fileName)
  517. hdContent.Data = fileByte
  518. if downloadSuccess == false {
  519. return false, &hdContent, common.SubHDStep2ExCannotFindDownloadBtn
  520. }
  521. return downloadSuccess, &hdContent, nil
  522. }
  523. func (s Supplier) passWaterWall(page *rod.Page) {
  524. //等待驗證碼窗體載入
  525. page.MustElement("#tcaptcha_iframe").MustWaitLoad()
  526. //進入到iframe
  527. iframe := page.MustElement("#tcaptcha_iframe").MustFrame()
  528. //等待拖動條加載, 延遲500秒檢測變化, 以確認加載完畢
  529. iframe.MustElement("#tcaptcha_drag_button").MustWaitStable()
  530. //等待缺口圖像載入
  531. slideBgEl := iframe.MustElement("#slideBg").MustWaitLoad()
  532. slideBgEl = slideBgEl.MustWaitStable()
  533. //取得帶缺口圖像
  534. shadowbg := slideBgEl.MustResource()
  535. // 取得原始圖像
  536. src := slideBgEl.MustProperty("src")
  537. fullbg, _, err := my_util.DownFile(strings.Replace(src.String(), "img_index=1", "img_index=0", 1))
  538. if err != nil {
  539. panic(err)
  540. }
  541. //取得img展示的真實尺寸
  542. shape, err := slideBgEl.Shape()
  543. if err != nil {
  544. panic(err)
  545. }
  546. bgbox := shape.Box()
  547. height, width := uint(math.Round(bgbox.Height)), uint(math.Round(bgbox.Width))
  548. //裁剪圖像
  549. shadowbgImg, _ := jpeg.Decode(bytes.NewReader(shadowbg))
  550. shadowbgImg = resize.Resize(width, height, shadowbgImg, resize.Lanczos3)
  551. fullbgImg, _ := jpeg.Decode(bytes.NewReader(fullbg))
  552. fullbgImg = resize.Resize(width, height, fullbgImg, resize.Lanczos3)
  553. //啓始left,排除干擾部份,所以右移10個像素
  554. left := fullbgImg.Bounds().Min.X + 10
  555. //啓始top, 排除干擾部份, 所以下移10個像素
  556. top := fullbgImg.Bounds().Min.Y + 10
  557. //最大left, 排除干擾部份, 所以左移10個像素
  558. maxleft := fullbgImg.Bounds().Max.X - 10
  559. //最大top, 排除干擾部份, 所以上移10個像素
  560. maxtop := fullbgImg.Bounds().Max.Y - 10
  561. //rgb比较阈值, 超出此阈值及代表找到缺口位置
  562. threshold := 20
  563. //缺口偏移, 拖動按鈕初始會偏移27.5
  564. distance := -27.5
  565. //取絕對值方法
  566. abs := func(n int) int {
  567. if n < 0 {
  568. return -n
  569. }
  570. return n
  571. }
  572. search:
  573. for i := left; i <= maxleft; i++ {
  574. for j := top; j <= maxtop; j++ {
  575. colorAR, colorAG, colorAB, _ := fullbgImg.At(i, j).RGBA()
  576. colorBR, colorBG, colorBB, _ := shadowbgImg.At(i, j).RGBA()
  577. colorAR, colorAG, colorAB = colorAR>>8, colorAG>>8, colorAB>>8
  578. colorBR, colorBG, colorBB = colorBR>>8, colorBG>>8, colorBB>>8
  579. if abs(int(colorAR)-int(colorBR)) > threshold ||
  580. abs(int(colorAG)-int(colorBG)) > threshold ||
  581. abs(int(colorAB)-int(colorBB)) > threshold {
  582. distance += float64(i)
  583. s.log.Debugln("對比完畢, 偏移量:", distance)
  584. break search
  585. }
  586. }
  587. }
  588. //獲取拖動按鈕形狀
  589. dragBtnBox := iframe.MustElement("#tcaptcha_drag_thumb").MustShape().Box()
  590. //启用滑鼠功能
  591. mouse := page.Mouse
  592. //模擬滑鼠移動至拖動按鈕處, 右移3的原因: 拖動按鈕比滑塊圖大3個像素
  593. mouse.MustMove(dragBtnBox.X+3, dragBtnBox.Y+(dragBtnBox.Height/2))
  594. //按下滑鼠左鍵
  595. mouse.MustDown("left")
  596. //開始拖動
  597. err = mouse.Move(dragBtnBox.X+distance, dragBtnBox.Y+(dragBtnBox.Height/2), 20)
  598. if err != nil {
  599. s.log.Errorln("mouse.Move", err)
  600. }
  601. //鬆開滑鼠左鍵, 拖动完毕
  602. mouse.MustUp("left")
  603. if s.reqParam.DebugMode == true {
  604. //截圖保存
  605. nowProcessRoot, err := my_util.GetRootDebugFolder()
  606. if err == nil {
  607. page.MustScreenshot(filepath.Join(nowProcessRoot, "result.png"))
  608. } else {
  609. s.log.Errorln("model.GetDebugFolder", err)
  610. }
  611. }
  612. }
  613. func (s Supplier) httpGetFromBrowser(browser *rod.Browser, inputUrl string) (string, *rod.Page, error) {
  614. page, err := rod_helper.NewPageNavigate(browser, inputUrl, s.tt, 5)
  615. if err != nil {
  616. return "", nil, err
  617. }
  618. pageString, err := page.HTML()
  619. if err != nil {
  620. return "", nil, err
  621. }
  622. // 每次搜索间隔在 30-40s
  623. if s.debugMode == true {
  624. time.Sleep(my_util.RandomSecondDuration(5, 10))
  625. } else {
  626. time.Sleep(my_util.RandomSecondDuration(30, 40))
  627. }
  628. return pageString, page, nil
  629. }
  630. type HdListItem struct {
  631. Url string `json:"url"`
  632. BaseUrl string `json:"baseUrl"`
  633. Title string `json:"title"`
  634. Ext string `json:"ext"`
  635. AuthorInfo string `json:"authorInfo"`
  636. Lang string `json:"lang"`
  637. Rate string `json:"rate"`
  638. DownCount int `json:"downCount"`
  639. Season int // 第几季,默认-1
  640. Episode int // 第几集,默认-1
  641. }
  642. type HdContent struct {
  643. Filename string `json:"filename"`
  644. Ext string `json:"ext"`
  645. Data []byte `json:"data"`
  646. }
  647. const TCode = "#TencentCaptcha"
  648. const btnClickCodeBtn = "button.btn-danger"
  649. const btnCommitCode = "button.btn-primary"