/
main.go
221 lines (192 loc) · 5.66 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
package main
import (
"net/http"
"fmt"
"sync"
"flag"
"os"
"syscall"
"log"
"path/filepath"
"time"
"strings"
"os/exec"
"io/ioutil"
"encoding/json"
"github.com/levigross/grequests"
id3 "github.com/mikkyang/id3-go"
transport "google.golang.org/api/googleapi/transport"
youtube "google.golang.org/api/youtube/v3"
)
var _ = filepath.Join
var _ = grequests.Get
var _ = fmt.Println
var _ = time.Sleep
var _ = strings.Replace
const (
//playlist = "PL1531805E486A97FF" // the REAL Italo
playlist = "PLQh1lAYHwN7h0GJydjLRPrqghcg-_t6x_" // actually short Italo
//playlist = "RDmbJ0aXxpTfM" // nightcore, not Italo
maxListItems = 50
youtubeDl = "youtube-dl"
ffmpeg = "ffmpeg"
)
var (
// API key for YouTube calls
googleAPIKey string
// Command line argv
Dirname string
Artist string
Album string
)
func init() {
// Parse args
flag.StringVar(&Dirname, "directory", "outfiles", "Output directory for downloaded files")
flag.StringVar(&Artist, "artist", "", "Artist name if songs should be tagged by artist")
flag.StringVar(&Album, "album", "", "Album name if songs should be tagged by album")
flag.Parse()
// Check for command-line dependencies
for _,dependency := range []string{youtubeDl, ffmpeg} {
if _, err := exec.LookPath(dependency); err != nil {
log.Fatalf("Must have %s in your PATH", dependency)
}
}
// Make output directory
if err := os.Mkdir(Dirname, 0777); err != nil && !os.IsExist(err) {
log.Fatal("Could not make directory", Dirname, "for output files")
}
// @hack - Increase file descriptor limt
const PLAYLIST_SIZE_LIMIT, SUBPROCS_PER_VIDEO = 200, 2
// max size of YouTubePlaylist, subprocesses needed to download and convert video
fdLimit := new(syscall.Rlimit)
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, fdLimit)
fdLimit.Cur += PLAYLIST_SIZE_LIMIT * SUBPROCS_PER_VIDEO
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, fdLimit)
if err != nil {
log.Fatal(err.Error())
}
// Unpack configuration
type config struct {
GoogleAPIKey string `json:"google_api_key"`
}
configJson, err := ioutil.ReadFile("config.json")
if err != nil {
// config.json missing
log.Fatal("Must have config.json file to run")
}
conf := new(config)
err = json.Unmarshal(configJson, conf)
if err != nil {
// unmarshal error
log.Fatal("config.json is formatted incorrectly")
}
if googleAPIKey = conf.GoogleAPIKey; googleAPIKey == "" {
// google_api_key missing from config
log.Fatal("Google API Key is missing from config.json")
}
}
func main() {
// Start up the YouTube service
service, err := youtube.New(&http.Client{
Transport: &transport.APIKey{Key: googleAPIKey},
})
if err != nil {
log.Fatal(err.Error())
}
playlistItems := make([]*OrderedPlaylistItem, 0)
sieve := make(chan *youtube.PlaylistItem)
// fetch the video ids
go playlistItemsSieve(service, playlist, sieve)
// dispatch the downloads
var counter int = 1
for video := range sieve {
orderedVideo := OrderedPlaylistItem{video, counter, 1}
playlistItems = append(playlistItems, &orderedVideo)
counter++
}
wg := new(sync.WaitGroup)
for _, video := range playlistItems {
wg.Add(1)
go func(v *OrderedPlaylistItem) {
var e error = v.Download()
if shouldConvert() && e == nil {
e = v.ConvertToMp3(Artist, Album)
os.Remove(v.M4aFname())
}
if e != nil {
fmt.Println(e.Error())
}
wg.Done()
}(video)
}
wg.Wait()
}
func (video *OrderedPlaylistItem) M4aFname() string {
return filepath.Join(Dirname, fmt.Sprintf("%d - %s.m4a", video.PositionInPlaylist, video.Snippet.Title))
}
func (video *OrderedPlaylistItem) Mp3Fname() string {
return strings.TrimSuffix(video.M4aFname(), "m4a") + "mp3"
}
func (video *OrderedPlaylistItem) ConvertToMp3(artist, album string) error {
if _, err := os.Stat(video.M4aFname()); os.IsNotExist(err) {
return err
}
cmd := exec.Command(ffmpeg, "-i", video.M4aFname(), "-acodec", "libmp3lame", "-ab", "256k", video.Mp3Fname())
output, err := cmd.Output()
mp3File, err := id3.Open(video.Mp3Fname())
defer mp3File.Close()
if err == nil {
mp3File.SetArtist(artist)
mp3File.SetAlbum(album)
} else {
fmt.Println(output)
}
return err
}
func (video *OrderedPlaylistItem) Download() error {
if video.RetriesLeft < 1 {
// look for the recursive base case, exit if max retries exceeded
return fmt.Errorf("Exceeded maximum retries for video %s", video.ContentDetails.VideoId)
}
cmd := exec.Command(youtubeDl, "-o", video.M4aFname(), "https://youtube.com/watch?v="+video.ContentDetails.VideoId, "-f", "141/140")
output, err := cmd.Output()
fmt.Println(string(output))
if err == nil {
return nil
} else {
video.RetriesLeft -= 1
video.Download()
return err
}
}
func playlistItemsSieve(service *youtube.Service, playlistId string, output chan *youtube.PlaylistItem) {
var nextPageToken string
for {
req := service.PlaylistItems.List("snippet,contentDetails").PlaylistId(playlistId).MaxResults(maxListItems)
if nextPageToken != "" {
// we are paginating
req = req.PageToken(nextPageToken)
}
playlist, err := req.Do()
if err != nil {
panic(err)
}
for _, video := range playlist.Items {
output <- video
}
nextPageToken = playlist.NextPageToken
if nextPageToken == "" {
break
}
}
close(output)
}
// tells if this program should convert to .mp3
func shouldConvert() bool {
return Artist != "" || Album != ""
}
type OrderedPlaylistItem struct {
*youtube.PlaylistItem
PositionInPlaylist int
RetriesLeft int
}