package main import ( "context" "flag" "fmt" "hash/crc32" "io" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" ) type config struct { youtubeDl string mkvmerge string overrideOutput string deleteTemporaries bool } func performDownload(ctx context.Context, cfg *config, targetUrl string) error { // var content []byte var err error if targetUrl == "-" { // Read HTML page from stdin content, err = ioutil.ReadAll(os.Stdin) if err != nil { return err } } else { // Download HTML page from URL resp, err := http.Get(targetUrl) if err != nil { return err } content, err = ioutil.ReadAll(resp.Body) if err != nil { return err } _ = resp.Body.Close() // swallow error } ltc, err := NewLoadTupContent(content) if err != nil { return err } err = ltc.Validate() if err != nil { return err } // Create temporary directory tmpdir, err := ioutil.TempDir("", "loadtup-dl-") if err != nil { return err } if cfg.deleteTemporaries { defer os.RemoveAll(tmpdir) } // Download the video ytdl := exec.CommandContext(ctx, cfg.youtubeDl, `-f`, `bestvideo+bestaudio`, "https://youtu.be/"+ltc.VideoID, `--merge-output-format`, `mkv`, "-o", filepath.Join(tmpdir, "downloaded")) ytdl.Stdout = os.Stdout ytdl.Stderr = os.Stderr err = ytdl.Run() if err != nil { return err } // Determine video's total length // Create the subtitle file (clamped to total length) fh, err := os.OpenFile(filepath.Join(tmpdir, "subtitles.srt"), os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return err } err = ltc.WriteSRT(fh) fh.Close() if err != nil { return err } // Mux the subtitles into the file mkvm := exec.CommandContext(ctx, cfg.mkvmerge, `-o`, filepath.Join(tmpdir, "muxed.mkv"), filepath.Join(tmpdir, "downloaded.mkv"), filepath.Join(tmpdir, "subtitles.srt")) mkvm.Stdout = os.Stdout mkvm.Stderr = os.Stderr err = mkvm.Run() if err != nil { return err } // Determine final filename outputFile := cfg.overrideOutput if outputFile == "" { // Generate the CRC32 and put it into the filename hw := NewCRCwriter(crc32.IEEE, ioutil.Discard) fhm, err := os.OpenFile(filepath.Join(tmpdir, "muxed.mkv"), os.O_RDONLY, 0400) if err != nil { return err } _, err = io.Copy(hw, fhm) fhm.Close() if err != nil { return err } outputFile = fmt.Sprintf(`[Loadtup] %s [%08X].mkv`, ltc.Title, hw.Sum()) } err = os.Rename(filepath.Join(tmpdir, "muxed.mkv"), outputFile) if err != nil { return err } // Done return nil } func usage() { fmt.Fprintln(os.Stderr, `Usage: loadtup-dl [options] [--] URL|- [URL...] Supported URLs take the form 'https://loadtup.com/abcdefghijk'. Use a hyphen to read equivalent loadtup.com HTML content from stdin. Options: --youtube-dl PATH Override path to youtube-dl --mkvmerge PATH Override path to mkvmerge --output PATH Override output filename (only valid for a single URL) --delete-temporary=false Preserve temporary files `) os.Exit(1) } func main() { ctx := context.Background() cfg := config{} flag.StringVar(&cfg.youtubeDl, "youtube-dl", "youtube-dl", "") flag.StringVar(&cfg.mkvmerge, "mkvmerge", "mkvmerge", "") flag.StringVar(&cfg.overrideOutput, "output", "", "") flag.BoolVar(&cfg.deleteTemporaries, "delete-temporary", true, "") flag.Usage = usage flag.Parse() if len(flag.Args()) == 0 { usage() // n.b. calls os.Exit(1) } if len(flag.Args()) > 1 && cfg.overrideOutput != "" { fmt.Fprintln(os.Stderr, "Can't use --output when supplying multiple URLs") os.Exit(1) } for _, targetUrl := range flag.Args() { err := performDownload(ctx, &cfg, targetUrl) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } } }