diff --git a/.drone.yml b/.drone.yml index 263a8b9..464cdf1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,4 +9,6 @@ steps: - go test -v environment: IMGUR_CLIENT_ID: - from_secret: imgur_client_id \ No newline at end of file + from_secret: imgur_client_id + YOUTUBE_KEY: + from_secret: youtube_key \ No newline at end of file diff --git a/linkinfo.go b/linkinfo.go index d66cf76..5dca044 100644 --- a/linkinfo.go +++ b/linkinfo.go @@ -15,6 +15,7 @@ type LinkInfoApi struct { Client *http.Client Imgur *ImgurInfoApi + Youtube *YoutubeInfoApi linkHandlers []*LinkHandler } @@ -38,6 +39,8 @@ func New(opts ...Option) *LinkInfoApi { switch opt.(type) { case *ImgurOptions: api.Imgur = &ImgurInfoApi{api, opt.(*ImgurOptions)} + case *YoutubeOptions: + api.Youtube = &YoutubeInfoApi{api, opt.(*YoutubeOptions)} } } @@ -56,9 +59,11 @@ func (i *LinkInfoApi) registerDefaultHandlers() { }) } - i.linkHandlers = []*LinkHandler{ - {Hosts: youtubeHosts, Handler: i.YoutubeLinkHandler}, - {Hosts: twitterHosts, Handler: i.TwitterLinkHandler}, + if i.Youtube != nil { + i.linkHandlers = append(i.linkHandlers, &LinkHandler{ + Hosts: youtubeHosts, + Handler: i.Youtube.Handler, + }) } } diff --git a/youtube.go b/youtube.go index bf16a3d..130daa3 100644 --- a/youtube.go +++ b/youtube.go @@ -1,9 +1,147 @@ package linkinfo -var ( - youtubeHosts = []string{"youtube.com", "youtu.be"} +import ( + "encoding/json" + "errors" + "fmt" + "math" + "net/http" + "net/url" + "regexp" + "strings" + "time" ) -func (i *LinkInfoApi) YoutubeLinkHandler(link string) (*LinkInfo, error) { - return nil, nil +const youtubeApiUrl = "https://www.googleapis.com/youtube/v3/videos" + +var ( + youtubeHosts = []string{"youtube.com", "youtu.be"} + durationRegexp = regexp.MustCompile("^00:") +) + +type YoutubeOptions struct { + Option + + Key string +} + +type YoutubeInfoApi struct { + api *LinkInfoApi + opts *YoutubeOptions +} + +type youtubeResponse struct { + Error *youtubeError `json:"error"` + Items []*youtubeItem `json:"items"` +} + +type youtubeError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type youtubeItem struct { + Snippet *youtubeSnippet `json:"snippet"` + ContentDetails *youtubeContentDetails `json:"contentDetails"` +} + +type youtubeSnippet struct { + Title string `json:"title"` + Description string `json:"description"` +} + +type youtubeContentDetails struct { + Duration string `json:"duration"` +} + +func (i *YoutubeInfoApi) Handler(link string) (*LinkInfo, error) { + u, err := url.Parse(link) + + if err != nil { + return nil, err + } + + var id string + + query := u.Query() + + if query != nil && query.Get("v") != "" { + id = query.Get("v") + } else if u.Host == "youtu.be" { + id = u.Path[1:] + } + + if id == "" { + return i.api.DefaultLinkHandler(link) + } + + params := &url.Values{} + + params.Set("id", id) + params.Set("part", "id, snippet, contentDetails") + params.Set("key", i.opts.Key) + + req, err := http.NewRequest("GET", youtubeApiUrl+"?"+params.Encode(), nil) + + if err != nil { + return nil, err + } + + res, err := i.api.Client.Do(req) + + if err != nil { + return nil, err + } + + var response youtubeResponse + + if err := json.NewDecoder(res.Body).Decode(&response); err != nil { + return nil, err + } + + if response.Error != nil { + return nil, errors.New(response.Error.Message) + } + + if len(response.Items) < 1 { + return nil, errors.New("video not found") + } + + item := response.Items[0] + + duration, err := time.ParseDuration(strings.ToLower(item.ContentDetails.Duration[2:])) + + trimmedDuration := durationRegexp.ReplaceAllString(humanizeDuration(duration), "") + + ret := &LinkInfo{ + Title: item.Snippet.Title + " [" + trimmedDuration + "]", + Description: item.Snippet.Description, + Duration: item.ContentDetails.Duration, + } + + return ret, nil +} + +// https://gist.github.com/harshavardhana/327e0577c4fed9211f65#gistcomment-2366908 +func humanizeDuration(duration time.Duration) string { + hours := int64(duration.Hours()) + minutes := int64(math.Mod(duration.Minutes(), 60)) + seconds := int64(math.Mod(duration.Seconds(), 60)) + + chunks := []struct { + singularName string + amount int64 + }{ + {"hour", hours}, + {"minute", minutes}, + {"second", seconds}, + } + + parts := make([]string, 0) + + for _, chunk := range chunks { + parts = append(parts, fmt.Sprintf("%02d", chunk.amount)) + } + + return strings.Join(parts, ":") } diff --git a/youtube_test.go b/youtube_test.go new file mode 100644 index 0000000..a011148 --- /dev/null +++ b/youtube_test.go @@ -0,0 +1,22 @@ +package linkinfo + +import ( + "os" + "testing" +) + +func TestYoutubeInfoApi_Handler(t *testing.T) { + api := New(&YoutubeOptions{ + Key: os.Getenv("YOUTUBE_KEY"), + }) + + info, err := api.Youtube.Handler("https://www.youtube.com/watch?v=1r4Md5WKaqs") + + if err != nil { + t.Fatal("Error getting youtube info:", err) + } + + if info.Title != "CHVRCHES - Graves [04:46]" { + t.Fatal("Unexpected title", info.Title) + } +}