Parent directory

get_durations.go

3715 bytes
  1package youtube
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"fmt"
  7	"io"
  8	"math"
  9	"net/http"
 10	"net/url"
 11	"strings"
 12	"time"
 13
 14	"github.com/TheEdgeOfRage/ytrssil-api/models"
 15)
 16
 17type APIVideoListResponse struct {
 18	Items []APIVideo `json:"items"`
 19}
 20
 21type APIVideo struct {
 22	ID                   string                   `json:"id"`
 23	Snippet              APISnippet               `json:"snippet"`
 24	ContentDetails       APIContentDetails        `json:"contentDetails"`
 25	Player               APIPlayer                `json:"player"`
 26	LiveStreamingDetails *APILiveStreamingDetails `json:"liveStreamingDetails"`
 27}
 28
 29type APISnippet struct {
 30	PublishedAt          string `json:"publishedAt"`
 31	ChannelID            string `json:"channelId"`
 32	Title                string `json:"title"`
 33	ChannelName          string `json:"channelTitle"`
 34	LiveBroadcastContent string `json:"liveBroadcastContent"`
 35}
 36
 37type APILiveStreamingDetails struct {
 38	ActualEndTime string `json:"actualEndTime"`
 39}
 40
 41type APIPlayer struct {
 42	EmbedHTML string `json:"embedHtml"`
 43}
 44
 45type APIContentDetails struct {
 46	Duration   string `json:"duration"`
 47	Definition string `json:"definition"`
 48}
 49
 50func parseISO8601Duration(s string) (time.Duration, error) {
 51	if s == "P0D" {
 52		return 0, nil
 53	}
 54
 55	var total time.Duration
 56	s = strings.ToLower(strings.TrimPrefix(s, "P"))
 57
 58	if i := strings.Index(s, "d"); i != -1 {
 59		days, err := time.ParseDuration(s[:i] + "h")
 60		if err != nil {
 61			return 0, fmt.Errorf("failed to parse days: %w", err)
 62		}
 63		total += days * 24
 64		s = s[i+1:]
 65	}
 66
 67	s = strings.TrimPrefix(s, "t")
 68	if s == "" {
 69		return total, nil
 70	}
 71
 72	d, err := time.ParseDuration(s)
 73	if err != nil {
 74		return 0, err
 75	}
 76
 77	return total + d, nil
 78}
 79
 80func (c *youTubeClient) GetVideoDurations(ctx context.Context, videos map[string]*models.Video) error {
 81	if len(videos) == 0 {
 82		return nil
 83	}
 84
 85	ids := strings.Builder{}
 86	for id := range videos {
 87		ids.WriteString(id)
 88		ids.WriteString(",")
 89	}
 90	videoIDs := ids.String()
 91	query := url.Values{}
 92	query.Add("id", videoIDs)
 93	query.Add("part", "contentDetails,liveStreamingDetails")
 94	query.Add("key", c.apiKey)
 95
 96	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "", nil)
 97	if err != nil {
 98		return fmt.Errorf("failed to set up request: %w", err)
 99	}
100	req.URL = &url.URL{
101		Scheme:   "https",
102		Host:     "www.googleapis.com",
103		Path:     "/youtube/v3/videos",
104		RawQuery: query.Encode(),
105	}
106
107	c.log.Info("Making request to YouTube API for video durations", "videoIDs", videoIDs)
108	resp, err := http.DefaultClient.Do(req)
109	if err != nil {
110		return fmt.Errorf("failed to fetch video details: %w", err)
111	}
112	defer resp.Body.Close()
113
114	if resp.StatusCode != http.StatusOK {
115		body, err := io.ReadAll(resp.Body)
116		var bodyStr string
117		if err != nil {
118			c.log.Error("failed to decode error body", "error", err)
119			bodyStr = "failed to decode body"
120		} else {
121			bodyStr = string(body)
122		}
123		return fmt.Errorf("got non-200 status from YouTube API [%d]: %v", resp.StatusCode, bodyStr)
124	}
125
126	decoder := json.NewDecoder(resp.Body)
127	var respData APIVideoListResponse
128	err = decoder.Decode(&respData)
129	if err != nil {
130		return fmt.Errorf("failed to decode video details: %w", err)
131	}
132
133	for _, v := range respData.Items {
134		duration, err := parseISO8601Duration(v.ContentDetails.Duration)
135		if err != nil {
136			c.log.Error(
137				"Failed to parse video duration",
138				"error", err,
139				"durationString", v.ContentDetails.Duration,
140				"videoID", v.ID,
141			)
142			return fmt.Errorf("failed to parse video duration [%v]: %w", v.ContentDetails.Duration, err)
143		}
144		videos[v.ID].DurationSeconds = int(math.Round(duration.Seconds()))
145		videos[v.ID].IsLive = v.LiveStreamingDetails != nil && v.LiveStreamingDetails.ActualEndTime == ""
146	}
147
148	return nil
149}