youtube.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. // This file is subject to a 1-clause BSD license.
  2. // Its contents can be found in the enclosed LICENSE file.
  3. // Package youtube provides a minimal set of bindings for Youtube's
  4. // Data API v3.
  5. //
  6. // This package requires a valid API key to be specified. You can get
  7. // one from your Google account's developer console. See:
  8. //
  9. // https://console.developers.google.com/apis
  10. //
  11. package youtube
  12. import (
  13. "encoding/json"
  14. "errors"
  15. "fmt"
  16. "io/ioutil"
  17. "net/http"
  18. "net/url"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "unicode"
  23. )
  24. var (
  25. ErrNoSuchVideo = errors.New("no such video")
  26. ErrInvalidAPIKey = errors.New("invalid or missing API key")
  27. ErrInvalidID = errors.New("invalid or missing video ID")
  28. )
  29. // VideoInfo defines some detailed properties for a specific
  30. // youtube video.
  31. type VideoInfo struct {
  32. ID string // The video's ID.
  33. Title string // Title of the video.
  34. Duration time.Duration // Duration of the video.
  35. }
  36. // GetVideoInfo returns details about a specific video, identified by
  37. // its ID. This is part of the youtube URL. E.g.:
  38. //
  39. // URL: https://www.youtube.com/watch?v=dQw4w9WgXcQ
  40. // ID: dQw4w9WgXcQ
  41. //
  42. // This returns nil and an error if the query failed.
  43. func GetVideoInfo(apiKey, id string) (*VideoInfo, error) {
  44. const videoURL = "https://www.googleapis.com/youtube/v3/videos?id=%s&part=contentDetails,snippet&key=%s"
  45. apiKey = strings.TrimSpace(apiKey)
  46. apiKey = url.QueryEscape(apiKey)
  47. if len(apiKey) == 0 {
  48. return nil, ErrInvalidAPIKey
  49. }
  50. id = strings.TrimSpace(id)
  51. id = url.QueryEscape(id)
  52. if len(id) == 0 {
  53. return nil, ErrInvalidID
  54. }
  55. var resp videoListResponse
  56. url := fmt.Sprintf(videoURL, id, apiKey)
  57. err := fetch(url, &resp)
  58. if err != nil {
  59. return nil, err
  60. }
  61. if len(resp.Items) == 0 {
  62. return nil, ErrNoSuchVideo
  63. }
  64. item := resp.Items[0]
  65. return &VideoInfo{
  66. ID: item.ID,
  67. Title: item.Snippet.Title,
  68. Duration: parseISO8601(item.ContentDetails.Duration),
  69. }, nil
  70. }
  71. // videoListResponse defines response data for a videoList request.
  72. type videoListResponse struct {
  73. Items []struct {
  74. ID string `json:"id"`
  75. Snippet struct {
  76. Title string `json:"title"`
  77. } `json:snippet`
  78. ContentDetails struct {
  79. Duration string `json:"duration"` // ISO 8601 timestamp (e.g.: "PT4M13S")
  80. } `json:"contentDetails"`
  81. } `json:"items"`
  82. }
  83. // fetch performs an API query and unmarshals the result into the
  84. // given value. Returns an error if something went booboo.
  85. func fetch(url string, v interface{}) error {
  86. resp, err := http.Get(url)
  87. if err != nil {
  88. return err
  89. }
  90. data, err := ioutil.ReadAll(resp.Body)
  91. resp.Body.Close()
  92. if err != nil {
  93. return err
  94. }
  95. return json.Unmarshal(data, v)
  96. }
  97. // parseISO8601 parses the given ISO 8601 value into a time.Duration value.
  98. // Example value: "PT4M13S"
  99. //
  100. // ref: https://en.wikipedia.org/wiki/ISO_8601#Durations
  101. func parseISO8601(v string) time.Duration {
  102. v = strings.ToUpper(v)
  103. v = strings.TrimSpace(v)
  104. if len(v) < 3 || v[0] != 'P' {
  105. return 0
  106. }
  107. var err error
  108. var sum time.Duration
  109. var inDate bool
  110. var n int64
  111. digits := make([]rune, 0, len(v)/2)
  112. for _, r := range v {
  113. switch r {
  114. case 'P':
  115. inDate = true
  116. case 'T':
  117. inDate = false
  118. case 'Y':
  119. n, err = strconv.ParseInt(string(digits), 10, 32)
  120. sum += time.Duration(n) * time.Hour * 8760
  121. digits = digits[:0]
  122. case 'M':
  123. if inDate { // M == month
  124. n, err = strconv.ParseInt(string(digits), 10, 32)
  125. sum += time.Duration(n) * time.Hour * 730
  126. digits = digits[:0]
  127. } else { // M = minutes
  128. n, err = strconv.ParseInt(string(digits), 10, 32)
  129. sum += time.Duration(n) * time.Minute
  130. digits = digits[:0]
  131. }
  132. case 'W':
  133. n, err = strconv.ParseInt(string(digits), 10, 32)
  134. sum += time.Duration(n) * time.Hour * 168
  135. digits = digits[:0]
  136. case 'D':
  137. n, err = strconv.ParseInt(string(digits), 10, 32)
  138. sum += time.Duration(n) * time.Hour * 24
  139. digits = digits[:0]
  140. case 'H':
  141. n, err = strconv.ParseInt(string(digits), 10, 32)
  142. sum += time.Duration(n) * time.Hour
  143. digits = digits[:0]
  144. case 'S':
  145. n, err = strconv.ParseInt(string(digits), 10, 32)
  146. sum += time.Duration(n) * time.Second
  147. digits = digits[:0]
  148. default:
  149. if unicode.IsDigit(r) {
  150. digits = append(digits, r)
  151. } else {
  152. return 0
  153. }
  154. }
  155. if err != nil {
  156. return 0
  157. }
  158. }
  159. return sum
  160. }