1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-01 17:38:37 +00:00

feat: optionally fetch watch time from YouTube API instead of website

This commit is contained in:
telnet23 2024-11-19 00:57:59 +00:00 committed by Frédéric Guillot
parent b61ee15c1b
commit 7e2b50efee
5 changed files with 99 additions and 0 deletions

View file

@ -4,9 +4,11 @@
package processor
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/url"
"regexp"
"strconv"
"time"
@ -33,6 +35,14 @@ func shouldFetchYouTubeWatchTime(entry *model.Entry) bool {
}
func fetchYouTubeWatchTime(websiteURL string) (int, error) {
if config.Opts.YouTubeApiKey() == "" {
return fetchYouTubeWatchTimeFromWebsite(websiteURL)
} else {
return fetchYouTubeWatchTimeFromApi(websiteURL)
}
}
func fetchYouTubeWatchTimeFromWebsite(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
@ -63,6 +73,61 @@ func fetchYouTubeWatchTime(websiteURL string) (int, error) {
return int(dur.Minutes()), nil
}
func fetchYouTubeWatchTimeFromApi(websiteURL string) (int, error) {
requestBuilder := fetcher.NewRequestBuilder()
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
parsedWebsiteURL, err := url.Parse(websiteURL)
if err != nil {
return 0, fmt.Errorf("unable to parse URL: %v", err)
}
apiQuery := url.Values{}
apiQuery.Set("id", parsedWebsiteURL.Query().Get("v"))
apiQuery.Set("key", config.Opts.YouTubeApiKey())
apiQuery.Set("part", "contentDetails")
apiURL := url.URL{
Scheme: "https",
Host: "www.googleapis.com",
Path: "youtube/v3/videos",
RawQuery: apiQuery.Encode(),
}
responseHandler := fetcher.NewResponseHandler(requestBuilder.ExecuteRequest(apiURL.String()))
defer responseHandler.Close()
if localizedError := responseHandler.LocalizedError(); localizedError != nil {
slog.Warn("Unable to fetch contentDetails from YouTube API", slog.String("website_url", websiteURL), slog.Any("error", localizedError.Error()))
return 0, localizedError.Error()
}
var videos struct {
Items []struct {
ContentDetails struct {
Duration string `json:"duration"`
} `json:"contentDetails"`
} `json:"items"`
}
if err := json.NewDecoder(responseHandler.Body(config.Opts.HTTPClientMaxBodySize())).Decode(&videos); err != nil {
return 0, fmt.Errorf("unable to decode JSON: %v", err)
}
if n := len(videos.Items); n != 1 {
return 0, fmt.Errorf("invalid items length: %d", n)
}
durs := videos.Items[0].ContentDetails.Duration
dur, err := parseISO8601(durs)
if err != nil {
return 0, fmt.Errorf("unable to parse duration %s: %v", durs, err)
}
return int(dur.Minutes()), nil
}
func parseISO8601(from string) (time.Duration, error) {
var match []string
var d time.Duration