231 lines
5.3 KiB
Go
231 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"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.WriteSRT(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)
|
|
}
|
|
}
|
|
|
|
}
|