youtube.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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. Duration time.Duration // Duration of the video.
  34. }
  35. // GetVideoInfo returns details about a specific video, identified by
  36. // its ID. This is part of the youtube URL. E.g.:
  37. //
  38. // URL: https://www.youtube.com/watch?v=dQw4w9WgXcQ
  39. // ID: dQw4w9WgXcQ
  40. //
  41. // This returns nil and an error if the query failed.
  42. func GetVideoInfo(apiKey, id string) (*VideoInfo, error) {
  43. const videoURL = "https://www.googleapis.com/youtube/v3/videos?id=%s&part=contentDetails&key=%s"
  44. apiKey = strings.TrimSpace(apiKey)
  45. apiKey = url.QueryEscape(apiKey)
  46. if len(apiKey) == 0 {
  47. return nil, ErrInvalidAPIKey
  48. }
  49. id = strings.TrimSpace(id)
  50. id = url.QueryEscape(id)
  51. if len(id) == 0 {
  52. return nil, ErrInvalidID
  53. }
  54. var resp videoListResponse
  55. url := fmt.Sprintf(videoURL, id, apiKey)
  56. err := fetch(url, &resp)
  57. if err != nil {
  58. return nil, err
  59. }
  60. if len(resp.Items) == 0 {
  61. return nil, ErrNoSuchVideo
  62. }
  63. item := resp.Items[0]
  64. return &VideoInfo{
  65. ID: item.ID,
  66. Duration: parseISO8601(item.ContentDetails.Duration),
  67. }, nil
  68. }
  69. // videoListResponse defines response data for a videoList request.
  70. type videoListResponse struct {
  71. Items []struct {
  72. ID string `json:"id"`
  73. ContentDetails struct {
  74. Duration string `json:"duration"` // ISO 8601 timestamp (e.g.: "PT4M13S")
  75. } `json:"contentDetails"`
  76. } `json:"items"`
  77. }
  78. // fetch performs an API query and unmarshals the result into the
  79. // given value. Returns an error if something went booboo.
  80. func fetch(url string, v interface{}) error {
  81. resp, err := http.Get(url)
  82. if err != nil {
  83. return err
  84. }
  85. data, err := ioutil.ReadAll(resp.Body)
  86. resp.Body.Close()
  87. if err != nil {
  88. return err
  89. }
  90. return json.Unmarshal(data, v)
  91. }
  92. // parseISO8601 parses the given ISO 8601 value into a time.Duration value.
  93. // Example value: "PT4M13S"
  94. //
  95. // ref: https://en.wikipedia.org/wiki/ISO_8601#Durations
  96. func parseISO8601(v string) time.Duration {
  97. v = strings.ToUpper(v)
  98. v = strings.TrimSpace(v)
  99. if len(v) < 3 || v[0] != 'P' {
  100. return 0
  101. }
  102. var err error
  103. var sum time.Duration
  104. var inDate bool
  105. var n int64
  106. digits := make([]rune, 0, len(v)/2)
  107. for _, r := range v {
  108. switch r {
  109. case 'P':
  110. inDate = true
  111. case 'T':
  112. inDate = false
  113. case 'Y':
  114. n, err = strconv.ParseInt(string(digits), 10, 32)
  115. sum += time.Duration(n) * time.Hour * 8760
  116. digits = digits[:0]
  117. case 'M':
  118. if inDate { // M == month
  119. n, err = strconv.ParseInt(string(digits), 10, 32)
  120. sum += time.Duration(n) * time.Hour * 730
  121. digits = digits[:0]
  122. } else { // M = minutes
  123. n, err = strconv.ParseInt(string(digits), 10, 32)
  124. sum += time.Duration(n) * time.Minute
  125. digits = digits[:0]
  126. }
  127. case 'W':
  128. n, err = strconv.ParseInt(string(digits), 10, 32)
  129. sum += time.Duration(n) * time.Hour * 168
  130. digits = digits[:0]
  131. case 'D':
  132. n, err = strconv.ParseInt(string(digits), 10, 32)
  133. sum += time.Duration(n) * time.Hour * 24
  134. digits = digits[:0]
  135. case 'H':
  136. n, err = strconv.ParseInt(string(digits), 10, 32)
  137. sum += time.Duration(n) * time.Hour
  138. digits = digits[:0]
  139. case 'S':
  140. n, err = strconv.ParseInt(string(digits), 10, 32)
  141. sum += time.Duration(n) * time.Second
  142. digits = digits[:0]
  143. default:
  144. if unicode.IsDigit(r) {
  145. digits = append(digits, r)
  146. } else {
  147. return 0
  148. }
  149. }
  150. if err != nil {
  151. return 0
  152. }
  153. }
  154. return sum
  155. }