reading_time.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  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. "errors"
  6. "fmt"
  7. "log/slog"
  8. "strconv"
  9. "github.com/PuerkitoBio/goquery"
  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. "miniflux.app/v2/internal/reader/readingtime"
  15. "miniflux.app/v2/internal/storage"
  16. )
  17. func fetchWatchTime(websiteURL, query string, isoDate bool) (int, error) {
  18. requestBuilder := fetcher.NewRequestBuilder()
  19. requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
  20. requestBuilder.WithProxyRotator(proxyrotator.ProxyRotatorInstance)
  21. responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(websiteURL))
  22. defer responseHandler.Close()
  23. if localizedError := responseHandler.LocalizedError(); localizedError != nil {
  24. slog.Warn("Unable to fetch watch time", slog.String("website_url", websiteURL), slog.Any("error", localizedError.Error()))
  25. return 0, localizedError.Error()
  26. }
  27. doc, docErr := goquery.NewDocumentFromReader(responseHandler.Body(config.Opts.HTTPClientMaxBodySize()))
  28. if docErr != nil {
  29. return 0, docErr
  30. }
  31. duration, exists := doc.FindMatcher(goquery.Single(query)).Attr("content")
  32. if !exists {
  33. return 0, errors.New("duration not found")
  34. }
  35. ret := 0
  36. if isoDate {
  37. parsedDuration, err := parseISO8601Duration(duration)
  38. if err != nil {
  39. return 0, fmt.Errorf("unable to parse iso duration %s: %v", duration, err)
  40. }
  41. ret = int(parsedDuration.Minutes())
  42. } else {
  43. parsedDuration, err := strconv.Atoi(duration)
  44. if err != nil {
  45. return 0, fmt.Errorf("unable to parse duration %s: %v", duration, err)
  46. }
  47. ret = parsedDuration / 60
  48. }
  49. return ret, nil
  50. }
  51. func updateEntryReadingTime(store *storage.Storage, feed *model.Feed, entry *model.Entry, entryIsNew bool, user *model.User) {
  52. if !user.ShowReadingTime {
  53. slog.Debug("Skip reading time estimation for this user", slog.Int64("user_id", user.ID))
  54. return
  55. }
  56. // Define watch time fetching scenarios
  57. watchTimeScenarios := [...]struct {
  58. shouldFetch func(*model.Entry) bool
  59. fetchFunc func(string) (int, error)
  60. platform string
  61. }{
  62. {shouldFetchYouTubeWatchTimeForSingleEntry, fetchYouTubeWatchTimeForSingleEntry, "YouTube"},
  63. {shouldFetchNebulaWatchTime, fetchNebulaWatchTime, "Nebula"},
  64. {shouldFetchOdyseeWatchTime, fetchOdyseeWatchTime, "Odysee"},
  65. {shouldFetchBilibiliWatchTime, fetchBilibiliWatchTime, "Bilibili"},
  66. }
  67. // Iterate through scenarios and attempt to fetch watch time
  68. for _, scenario := range watchTimeScenarios {
  69. if scenario.shouldFetch(entry) {
  70. if entryIsNew {
  71. if watchTime, err := scenario.fetchFunc(entry.URL); err != nil {
  72. slog.Warn("Unable to fetch watch time",
  73. slog.String("platform", scenario.platform),
  74. slog.Int64("user_id", user.ID),
  75. slog.Int64("entry_id", entry.ID),
  76. slog.String("entry_url", entry.URL),
  77. slog.Int64("feed_id", feed.ID),
  78. slog.String("feed_url", feed.FeedURL),
  79. slog.Any("error", err),
  80. )
  81. } else {
  82. entry.ReadingTime = watchTime
  83. }
  84. } else {
  85. entry.ReadingTime = store.GetReadTime(feed.ID, entry.Hash)
  86. }
  87. break
  88. }
  89. }
  90. // Fallback to text-based reading time estimation
  91. if entry.ReadingTime == 0 && entry.Content != "" {
  92. entry.ReadingTime = readingtime.EstimateReadingTime(entry.Content, user.DefaultReadingSpeed, user.CJKReadingSpeed)
  93. }
  94. }