package youtube import ( "errors" "fmt" "log" "net/http" "net/url" "strings" "time" iso8601duration "github.com/ChannelMeter/iso8601duration" "github.com/icedream/irc-medialink/parsers" "google.golang.org/api/googleapi/transport" "google.golang.org/api/youtube/v3" ) const ( youtubeIdType_None youtubeIdType = iota youtubeIdType_Video youtubeIdType_ChannelName youtubeIdType_ChannelId youtubeIdType_Playlist header = "\x031,0You\x030,4Tube\x03" + "99,99" + /* Fix for KiwiIRC not flushing background color on empty color tag */ "\x03" /* Fix for Mibbit interpreting 99 as green instead of transparent */ ) var ( ErrNotFound = errors.New("Not found.") ) type youtubeIdType uint8 type Parser struct { Config *Config Service *youtube.Service } func getYouTubeId(uri *url.URL, followRedirects int) (youtubeIdType, string) { u := &(*uri) u.Scheme = strings.ToLower(u.Scheme) u.Host = strings.ToLower(u.Host) // Must be an HTTP URL if u.Scheme != "http" && u.Scheme != "https" { return youtubeIdType_None, "" } // Remove www. prefix from hostname if strings.HasPrefix(u.Host, "www.") { u.Host = u.Host[4:] } switch strings.ToLower(u.Host) { case "youtu.be": // http://youtu.be/{id} if s, err := url.QueryUnescape(strings.TrimLeft(u.Path, "/")); err == nil { return youtubeIdType_Video, s } case "youtube.com": if u.Path == "/watch" { // http://youtube.com/watch?v={id} return youtubeIdType_Video, u.Query().Get("v") } else if strings.HasPrefix(u.Path, "/channel/") { // https://www.youtube.com/channel/{channelid} return youtubeIdType_ChannelId, strings.Trim(u.Path[9:], "/") } else if strings.HasPrefix(u.Path, "/c/") { // http://youtube.com/c/{channelname} return youtubeIdType_ChannelName, strings.Trim(u.Path[3:], "/") } else if strings.HasPrefix(u.Path, "/user/") { // http://youtube.com/user/{channelname} return youtubeIdType_ChannelName, strings.Trim(u.Path[6:], "/") } else if strings.HasPrefix(u.Path, "/playlist") { // https://www.youtube.com/playlist?list=PLq34c5GJGiJJlrG9-ByMbuQkTvaFtIflO return youtubeIdType_Playlist, u.Query().Get("list") } else if followRedirects > 0 && len(u.Path) > 1 && !strings.Contains(u.Path[1:], "/") { // Maybe https://youtube.com/{channelname}. // Does this actually redirect to a channel? req, err := http.NewRequest("HEAD", u.String(), nil) if err != nil { log.Printf("Failed to create HEAD request from %s: %s", u, err) return youtubeIdType_None, "" } resp, err := http.DefaultTransport.RoundTrip(req) if err != nil { log.Printf("Failed to check for channel from %s: %s", u, err) return youtubeIdType_None, "" } if resp.StatusCode >= 300 && resp.StatusCode < 400 { if lu, err := resp.Location(); err == nil && lu != nil { return getYouTubeId(lu, followRedirects-1) } } } } return youtubeIdType_None, "" } func (p *Parser) Init() error { // youtube api client := &http.Client{ Transport: &transport.APIKey{Key: p.Config.ApiKey}, } if srv, err := youtube.New(client); err != nil { return err } else { p.Service = srv } return nil } func (p *Parser) Name() string { return "YouTube" } func (p *Parser) Parse(u *url.URL, referer *url.URL) (result parsers.ParseResult) { // Parse YouTube URL idType, id := getYouTubeId(u, 2) if idType == youtubeIdType_None { result.Ignored = true return // nothing relevant found in this URL } switch idType { case youtubeIdType_Video: // Get YouTube video info list, err := p.Service.Videos.List("contentDetails,id,liveStreamingDetails,snippet,statistics").Id(id).Do() if err != nil { result.Error = err return } // Any info available? if len(list.Items) < 1 { result.UserError = ErrNotFound return } // Collect information result.Information = []map[string]interface{}{} for _, item := range list.Items { r := map[string]interface{}{ "ShortUrl": fmt.Sprintf("https://youtu.be/%v", url.QueryEscape(item.Id)), "IsUpload": true, "IsLive": item.LiveStreamingDetails != nil, } if item.Snippet != nil { r["Title"] = item.Snippet.Title r["Author"] = item.Snippet.ChannelTitle r["Description"] = item.Snippet.Description r["Category"] = item.Snippet.CategoryId r["Tags"] = item.Snippet.Tags // parse publishedAt if t, err := time.Parse(time.RFC3339, item.Snippet.PublishedAt); err == nil { r["PublishedAt"] = t } else { log.Print(err) } } if item.ContentDetails != nil { // parse duration if d, err := iso8601duration.FromString(item.ContentDetails.Duration); err == nil { r["Duration"] = d.ToDuration() } else { log.Print(err) } } if item.Statistics != nil { r["Views"] = item.Statistics.ViewCount r["Comments"] = item.Statistics.CommentCount r["Likes"] = item.Statistics.LikeCount r["Dislikes"] = item.Statistics.DislikeCount r["Favorites"] = item.Statistics.FavoriteCount } /*if item.LiveStreamingDetails != nil { r["Views"] = item.LiveStreamingDetails.ConcurrentViewers }*/ r["Header"] = header result.Information = append(result.Information, r) } case youtubeIdType_ChannelId, youtubeIdType_ChannelName: // Get YouTube channel info cl := p.Service.Channels.List("id,snippet,statistics") if idType == youtubeIdType_ChannelName { cl = cl.ForUsername(id) } else { cl = cl.Id(id) } list, err := cl.Do() if err != nil { result.Error = err return } // Any info available? if len(list.Items) < 1 { result.UserError = ErrNotFound return } // Collect information result.Information = []map[string]interface{}{} for _, item := range list.Items { r := map[string]interface{}{ "Header": header, "IsProfile": true, "Title": "Channel", "Author": item.Snippet.Title, "CountryCode": item.Snippet.Country, "Description": item.Snippet.Description, "ShortUrl": item.Snippet.CustomUrl, "Comments": item.Statistics.CommentCount, "Videos": item.Statistics.VideoCount, "Views": item.Statistics.ViewCount, } if !item.Statistics.HiddenSubscriberCount { r["Followers"] = item.Statistics.SubscriberCount } result.Information = append(result.Information, r) } case youtubeIdType_Playlist: // Get YouTube channel info list, err := p.Service.Playlists.List("id,snippet").Id(id).Do() if err != nil { result.Error = err return } // Any info available? if len(list.Items) < 1 { result.UserError = ErrNotFound return } // Collect information result.Information = []map[string]interface{}{} for _, item := range list.Items { r := map[string]interface{}{ "Header": header, "IsPlaylist": true, "Title": "Playlist: " + item.Snippet.Title, "Author": item.Snippet.ChannelTitle, "PublishedAt": item.Snippet.PublishedAt, "Description": item.Snippet.Description, } // parse publishedAt if t, err := time.Parse(time.RFC3339, item.Snippet.PublishedAt); err == nil { r["PublishedAt"] = t } else { log.Print(err) } result.Information = append(result.Information, r) } default: result.Ignored = true } return }