main
Raw Download raw file
  1package playlist
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"strings"
  8
  9	"github.com/channelmeter/iso8601duration"
 10	"google.golang.org/api/option"
 11	"google.golang.org/api/youtube/v3"
 12)
 13
 14type Playlist struct {
 15	Id     string  `json:"id"`
 16	Title  string  `json:"title"`
 17	Tracks []Track `json:"playlist"`
 18}
 19
 20type Track struct {
 21	Filename  string   `json:"file"`
 22	TrimStart float64  `json:"trim_start,omitempty"`
 23	TrimEnd   float64  `json:"trim_end,omitempty"`
 24	Fade      float64  `json:"fade,omitempty"`    // crossfade
 25	Details   *Details `json:"details,omitempty"` // YouTube metadata
 26}
 27
 28type Details struct {
 29	Title     string `json:"title,omitempty"`
 30	Artist    string `json:"artist,omitempty"`
 31	VideoID   string `json:"video_id,omitempty"`
 32	Duration  string `json:"duration,omitempty"`
 33	Thumbnail string `json:"thumbnail,omitempty"`
 34	ChannelId string `json:"channel_id,omitempty"`
 35}
 36
 37var (
 38	ErrPlaylistNotFound = errors.New("playlist not found")
 39)
 40
 41func newTrackFromPlaylistItem(item *youtube.PlaylistItem) Track {
 42
 43	sn := item.Snippet
 44
 45	channel := sn.VideoOwnerChannelTitle
 46	if strings.HasSuffix(channel, " - Topic") {
 47		channel = strings.TrimSuffix(channel, " - Topic")
 48	}
 49
 50	// get the highset resolution returned
 51	tn := ""
 52	switch {
 53	case sn.Thumbnails.Maxres != nil:
 54		tn = sn.Thumbnails.Maxres.Url
 55	case sn.Thumbnails.High != nil:
 56		tn = sn.Thumbnails.High.Url
 57	case sn.Thumbnails.Medium != nil:
 58		tn = sn.Thumbnails.Medium.Url
 59	case sn.Thumbnails.Default != nil:
 60		tn = sn.Thumbnails.Default.Url
 61	}
 62
 63	return Track{
 64		Filename: sn.ResourceId.VideoId + ".wav",
 65		Details: &Details{
 66			Title:     sn.Title,
 67			VideoID:   sn.ResourceId.VideoId,
 68			Thumbnail: tn,
 69			Artist:    channel,
 70			ChannelId: sn.VideoOwnerChannelId,
 71		},
 72	}
 73}
 74
 75func formatDuration(raw string) string {
 76	dur, err := duration.FromString(raw)
 77	if err != nil {
 78		return "0:00"
 79	}
 80	d := dur.ToDuration()
 81	mins := int(d.Minutes())
 82	secs := int(d.Seconds()) % 60
 83	return fmt.Sprintf("%d:%02d", mins, secs)
 84}
 85
 86func LookupPlaylist(apiKey, playlistID string) (*Playlist, error) {
 87
 88	const (
 89		_batchSize = 50
 90	)
 91
 92	ctx := context.Background()
 93	service, err := youtube.NewService(ctx, option.WithAPIKey(apiKey))
 94	if err != nil {
 95		return nil, fmt.Errorf("setting up youtube api client: %w", err)
 96	}
 97
 98	playlistCall := service.Playlists.
 99		List([]string{"snippet"}).
100		Id(playlistID)
101	playlistResp, err := playlistCall.Do()
102	if err != nil {
103		err = fmt.Errorf("fetching playlist details: %w", err)
104		return nil, err
105	}
106	if len(playlistResp.Items) == 0 {
107		err = fmt.Errorf("playlist %q: %w",
108			playlistID,
109			ErrPlaylistNotFound)
110		return nil, err
111	}
112
113	pl := &Playlist{
114		Id:     playlistID,
115		Title:  playlistResp.Items[0].Snippet.Title,
116		Tracks: make([]Track, 0),
117	}
118
119	itemsCall := service.PlaylistItems.
120		List([]string{"snippet"}).
121		PlaylistId(playlistID).
122		MaxResults(_batchSize)
123
124	// map tracks by Id while keeping them in their Playlist
125	trackMap := make(map[string]*Track)
126
127	// build trackList
128	for {
129		resp, err := itemsCall.Do()
130		if err != nil {
131			err = fmt.Errorf("fetching playlist items: %w", err)
132			return nil, err
133		}
134
135		for _, item := range resp.Items {
136			track := newTrackFromPlaylistItem(item)
137			pl.Tracks = append(pl.Tracks, track)
138			trackMap[track.Details.VideoID] = &pl.Tracks[len(pl.Tracks)-1]
139		}
140
141		if resp.NextPageToken == "" {
142			break
143		}
144		itemsCall.PageToken(resp.NextPageToken)
145	}
146
147	for i := 0; i < len(pl.Tracks); i = i + _batchSize {
148
149		end := min(i+_batchSize, len(pl.Tracks))
150		idSlice := make([]string, 0, end-i)
151		for _, t := range pl.Tracks[i:end] {
152			idSlice = append(idSlice, t.Details.VideoID)
153		}
154		ids := strings.Join(idSlice, ",")
155
156		resp, err := service.Videos.
157			List([]string{"contentDetails"}).
158			Id(ids).
159			Do()
160		if err != nil {
161			err = fmt.Errorf("fetching video details: %w", err)
162			return nil, err
163		}
164
165		for _, item := range resp.Items {
166			t, trackFound := trackMap[item.Id]
167			if trackFound {
168				t.Details.Duration = formatDuration(item.ContentDetails.Duration)
169			}
170		}
171
172	}
173	return pl, nil
174}