-
Notifications
You must be signed in to change notification settings - Fork 3
/
main.go
125 lines (106 loc) · 3.58 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
package main
import (
"context"
"fmt"
"github.com/alfg/mp4"
"io"
"log"
"math/rand"
"net/url"
"strconv"
"strings"
)
type Args struct {
VideoURL *url.URL `arg:"positional,required" help:"url of the video to download audio from. Must be a netflix url. e.g. https://www.netflix.com/watch/12345678?trackId=12345678"`
DownloadDir string `arg:"positional" default:"." help:"directory where to download the audio files. Defaults to current working directory."`
ChromeURL *url.URL `arg:"-c, --chrome-url" default:"http://127.0.0.1:9222" help:"url of the chrome debugger."`
}
var Version string
func (Args) Version() string {
return Version
}
func main() {
args := &Args{}
mustParse(args)
// Connect to Chrome debugger, retry until success
ctx := context.Background()
chromeURL := args.ChromeURL.String()
chrome := tryConnectToChromeUntilSuccess(ctx, chromeURL)
log.Printf("Ꙫ Sucessfully connected to Chrome at %s", chromeURL)
// Create task queue for downloading
q := NewDownloadQueue()
defer q.Release()
// Listen for download status updates
q.OnStatusReceived(func(status DownloadStatus) {
task := status.Task()
switch s := status.(type) {
case Queuing:
break
case Begin:
log.Printf("▼ [%s] Downloading %s to %s", s.TaskId(), task.VideoUrl, task.ToPath)
case Finished:
log.Printf("✓ [%s] Finished %s ⟾ %s (got %d bytes in %f)", s.TaskId(), task.VideoUrl, task.ToPath, s.BytesReceived(), s.Duration().Seconds())
}
})
nflx := NewNFLX(chrome)
// Navigate to the initial url
err := nflx.NavigateTo(ctx, args.VideoURL.String())
if err != nil {
log.Fatal(err)
}
// Listen for received media urls and queue them for download. Also listen for navigated events to update the current url
var browserURL = args.VideoURL.String()
for events := range nflx.Listen(ctx) {
switch events.evType {
case MediaUrlReceivedEvent:
err := q.QueueDownload(DownloadTask{
SrcURL: toDownloadableURL(string(events.payload)),
ToPath: toDownloadPath(browserURL, args.DownloadDir),
VideoUrl: browserURL,
})
if err != nil {
log.Println(err)
}
case NavigatedEvent:
log.Printf("ᐅ Navigate to %s \n", events.payload)
browserURL = string(events.payload)
}
}
}
// probeFileFormat reads the first 30k bytes of the file and checks if it is an audio file.
func probeFileFormat(body io.Reader, header []byte) (isAudio bool, err error) {
_, err = body.Read(header)
if err != nil {
return false, fmt.Errorf("error reading header: %w", err)
}
rd, err := mp4.OpenFromBytes(header)
if err != nil {
return false, err
}
return rd.Ftyp.MajorBrand == "mp42", nil
}
// toDownloadableURL removes the path from the payload to make the resource downloadable. In our case the path
// always contains a download-range in bytes which we can discard. See isMediaURL.
func toDownloadableURL(audioURL string) string {
// We need to remove the path from the audio url to get a downloadable url
u, err := url.Parse(audioURL)
if err != nil {
log.Fatal(err)
}
u.Path = ""
return u.String()
}
// toDownloadPath returns the path where to download the audio file. The path is composed of the video id, the track
// id and a random number.
func toDownloadPath(videoURL string, downloadDir string) string {
u, err := url.Parse(videoURL)
if err != nil {
log.Fatal(err)
}
if strings.HasPrefix(u.Path, "/watch") && u.Query().Has("trackId") {
videoId := strings.TrimLeft(u.Path, "/watch/")
trackId := u.Query().Get("trackId")
return downloadDir + "/" + videoId + "-" + trackId + "-" + strconv.Itoa(rand.Int())
}
return downloadDir + "/" + "DL-" + strconv.Itoa(rand.Int())
}