main
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}