bilibili.go 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package processor // import "miniflux.app/v2/internal/reader/processor"
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "log/slog"
  8. "regexp"
  9. "strings"
  10. "miniflux.app/v2/internal/config"
  11. "miniflux.app/v2/internal/model"
  12. "miniflux.app/v2/internal/proxyrotator"
  13. "miniflux.app/v2/internal/reader/fetcher"
  14. )
  15. var (
  16. bilibiliVideoIdRegex = regexp.MustCompile(`/video/(?:av(\d+)|BV([a-zA-Z0-9]+))`)
  17. )
  18. func shouldFetchBilibiliWatchTime(entry *model.Entry) bool {
  19. if !config.Opts.FetchBilibiliWatchTime() {
  20. return false
  21. }
  22. return strings.Contains(entry.URL, "bilibili.com/video/")
  23. }
  24. func extractBilibiliVideoID(websiteURL string) (string, string, error) {
  25. matches := bilibiliVideoIdRegex.FindStringSubmatch(websiteURL)
  26. if matches == nil {
  27. return "", "", fmt.Errorf("no video ID found in URL: %s", websiteURL)
  28. }
  29. if matches[1] != "" {
  30. return "aid", matches[1], nil
  31. }
  32. if matches[2] != "" {
  33. return "bvid", matches[2], nil
  34. }
  35. return "", "", fmt.Errorf("unexpected regex match result for URL: %s", websiteURL)
  36. }
  37. func fetchBilibiliWatchTime(websiteURL string) (int, error) {
  38. requestBuilder := fetcher.NewRequestBuilder()
  39. requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
  40. requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
  41. idType, videoID, extractErr := extractBilibiliVideoID(websiteURL)
  42. if extractErr != nil {
  43. return 0, extractErr
  44. }
  45. bilibiliApiURL := "https://api.bilibili.com/x/web-interface/view?" + idType + "=" + videoID
  46. responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(bilibiliApiURL))
  47. defer responseHandler.Close()
  48. if localizedError := responseHandler.LocalizedError(); localizedError != nil {
  49. slog.Warn("Unable to fetch Bilibili API",
  50. slog.String("website_url", websiteURL),
  51. slog.String("api_url", bilibiliApiURL),
  52. slog.Any("error", localizedError.Error()))
  53. return 0, localizedError.Error()
  54. }
  55. var result map[string]any
  56. doc := json.NewDecoder(responseHandler.Body(config.Opts.HTTPClientMaxBodySize()))
  57. if docErr := doc.Decode(&result); docErr != nil {
  58. return 0, fmt.Errorf("failed to decode API response: %v", docErr)
  59. }
  60. if code, ok := result["code"].(float64); !ok || code != 0 {
  61. return 0, fmt.Errorf("API returned error code: %v", result["code"])
  62. }
  63. data, ok := result["data"].(map[string]any)
  64. if !ok {
  65. return 0, fmt.Errorf("data field not found or not an object")
  66. }
  67. duration, ok := data["duration"].(float64)
  68. if !ok {
  69. return 0, fmt.Errorf("duration not found or not a number")
  70. }
  71. intDuration := int(duration)
  72. durationMin := intDuration / 60
  73. if intDuration%60 != 0 {
  74. durationMin++
  75. }
  76. return durationMin, nil
  77. }