package main import ( "context" "flag" "fmt" "hash/crc32" "io" "io/ioutil" "net/http" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" ) const ( LogLevelInfo int = 1 LogLevelVerbose int = 2 ) type config struct { youtubeDl string mkvmerge string mediainfo string overrideOutput string subsOnly bool deleteTemporaries bool loglevel int } func performDownload(ctx context.Context, cfg *config, targetUrl string) error { // if cfg.loglevel >= LogLevelInfo { fmt.Printf("Starting download for '%s'...\n", targetUrl) } // 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")) if cfg.loglevel >= LogLevelVerbose { ytdl.Stdout = os.Stdout ytdl.Stderr = os.Stderr } err = ytdl.Run() if err != nil { return err } // Determine video's total length minfo := exec.CommandContext(ctx, cfg.mediainfo, `--Inform=General;%Duration%`, filepath.Join(tmpdir, "downloaded.mkv")) if cfg.loglevel >= LogLevelVerbose { minfo.Stderr = os.Stderr } ret, err := minfo.Output() if err != nil { return err } msecsDuration, err := strconv.ParseInt(strings.TrimSpace(string(ret)), 10, 64) if err != nil { return err } if cfg.loglevel >= LogLevelVerbose { fmt.Printf("Video duration is %d ms\n", msecsDuration) } // 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.WriteSubtitle(fh, float64(msecsDuration)/1000) fh.Close() if err != nil { return err } // Mux the subtitles into the file mkvm := exec.CommandContext(ctx, cfg.mkvmerge, `--title`, ltc.Title, `-o`, filepath.Join(tmpdir, "muxed.mkv"), `--language`, `0:jpn`, `--language`, `1:jpn`, filepath.Join(tmpdir, "downloaded.mkv"), `--language`, `0:eng`, `--default-track`, `0`, filepath.Join(tmpdir, "subtitles.srt")) if cfg.loglevel >= LogLevelVerbose { 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 } var invalidChars *strings.Replacer if runtime.GOOS == "windows" { // compile-time constant comparison will be elided invalidChars = strings.NewReplacer(`"`, `_`, `*`, `_`, `<`, `_`, `>`, `_`, `?`, `_`, `\`, `_`, `|`, `_`, `/`, `_`, `:`, `_`) } else { invalidChars = strings.NewReplacer(`/`, `_`) } outputFile = fmt.Sprintf(`[Loadtup] %s [%08X].mkv`, invalidChars.Replace(ltc.Title), hw.Sum()) } err = os.Rename(filepath.Join(tmpdir, "muxed.mkv"), outputFile) if err != nil { return err } // Done if cfg.loglevel >= LogLevelInfo { fmt.Printf("Download complete for '%s'\n", outputFile) } 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 --mediainfo PATH Override path to mediainfo --output PATH Override output filename (only valid for a single URL) --delete-temporary=false Preserve temporary files --loglevel 0|1|2 Set verbosity (0=silent, 1=normal, 2=verbose) `) 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.mediainfo, "mediainfo", "mediainfo", "") flag.StringVar(&cfg.overrideOutput, "output", "", "") flag.BoolVar(&cfg.deleteTemporaries, "delete-temporary", true, "") flag.IntVar(&cfg.loglevel, "loglevel", 1, "") 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) } } }