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}