audible-dl
An archiving tool for Audible audiobook libraries
audible-dl/audible-dl.go
Download raw file: audible-dl.go
package main import ( "flag" "fmt" "log" "os" "path/filepath" ) // If the -l or --log flag is passed, in addition to logging to an // internal buffer, the scraper will log to the file // .audible-dl-debug.log. The scraper runs synchronously so there // should be no risk of race conditions causing the contents of this // or the account buffers to be mangled var logFile *os.File = nil // Each book is stored in one of these type Book struct { Slug string // B002VA9SWS Title string // "The Hitchhiker's Guide to the Galaxy" Series string // "The Hitchhiker's Guide to the Galaxy" Runtime string // "5 hrs and 51 minutes" Summary string // "Seconds before the Earth is demolished..." CoverURL string // "https://m.media-amazon.com/..." FileName string // "TheHitchhikersGuidetotheGalaxy" DownloadURL string // "https://cds.audible.com/..." CompanionURL string // "" Authors []string // ["Douglas Adams"] Narrators []string // ["Steven Fry"] SeriesIndex int // 1 } //////////////////////////////////////////////////////////////////////// // _ _ _ // ___ _ __ | |_ _ __ _ _ _ __ ___ (_)_ __ | |_ // / _ \ '_ \| __| '__| | | | '_ \ / _ \| | '_ \| __| // | __/ | | | |_| | | |_| | |_) | (_) | | | | | |_ // \___|_| |_|\__|_| \__, | .__/ \___/|_|_| |_|\__| // |___/|_| //////////////////////////////////////////////////////////////////////// func main() { account, harpath, aaxpath, savelog := getArgs() cfgfile, datadir, tempdir, savedir := getPaths() client := MakeClient(cfgfile, tempdir, savedir, datadir) client.Validate() if savelog { var err error logFile, err = os.OpenFile(".audible-dl-debug.log", os.O_WRONLY|os.O_CREATE, 0644) expect(err, "Failed to open log file for writing") } if harpath != "" { client.ImportCookies(account, harpath) os.Exit(0) } if aaxpath != "" { m4b := client.ConvertSingleBook(account, aaxpath) fmt.Printf("%s: made %s\n", account, filepath.Base(m4b)) os.Exit(0) } client.GetCookies() client.GetDownloaded() client.ScrapeLibrary(account) logFile.Close() } //////////////////////////////////////////////////////////////////////// // _ _ _ _ // __ _ _ ___ _(_) (_) __ _ _ __(_) ___ ___ // / _` | | | \ \/ / | | |/ _` | '__| |/ _ \/ __| // | (_| | |_| |> <| | | | (_| | | | | __/\__ \ // \__,_|\__,_/_/\_\_|_|_|\__,_|_| |_|\___||___/ //////////////////////////////////////////////////////////////////////// const helpMessage string = `Usage: audible-dl [-h] [-a ACC] [-i HAR] [-s AAX] Scrape your Audible library or convert an AAX file to m4b. See audible-dl(1) for more information. Options: -h, --help Print this message and exit. -a, --account NAME Specify an account for the operation. -i, --import HAR Import login cookies from HAR. -s, --single AAX Convert the single AAX file specified in AAX. -l, --log Log scraper info to .audible-dl-debug.log ` const debugScraperMessage string = `I encountered an error while scraping your library. There are two likely causes of this: 1. Your authentication cookies for %s have expired. You can re-import them with "audible-dl -i path/to/cookies.har -a %s", see the man page for details. 2. Audible changed the structure of their website. This is most likely the case if the file .audible-dl-debug.html contains a list of books or otherwise looks like you were signed in correctly. If re-importing your cookies doesn't help, please email a bug report to "~thalia/audible-dl@lists.sr.ht", see the man page for details. ` // Like Rust's .unwrap() method. func unwrap(err interface{}) { if err != nil { log.Fatal(err) } } // Like Rust's .expect() method. func expect(err interface{}, why string) { if err != nil { log.Print(why) log.Print(err) fmt.Fprintf(os.Stderr, helpMessage) os.Exit(1) } } //////////////////////////////////////////////////////////////////////// // _ _ // ___ _ ____ _(_)_ __ ___ _ __ _ __ ___ ___ _ __ | |_ // / _ \ '_ \ \ / / | '__/ _ \| '_ \| '_ ` _ \ / _ \ '_ \| __| // | __/ | | \ V /| | | | (_) | | | | | | | | | __/ | | | |_ // \___|_| |_|\_/ |_|_| \___/|_| |_|_| |_| |_|\___|_| |_|\__| //////////////////////////////////////////////////////////////////////// // Audible-dl needs to access several locations in the filesystem in // order to save downloaded books and discover config, auth, and cache // data. Typically, it will look for internal files in the // OS-specific cache and config directories with downloaded books // stored in a location that the user must specify in the config // file. If $AUDIBLE_DL_ROOT points to a directory, all program state // will live inside it. func getPaths() (string, string, string, string) { var cfgfile, datadir, tempdir, savedir string root := os.Getenv("AUDIBLE_DL_ROOT") if root != "" { cfgfile = root + "/.audible-dl/config.yml" tempdir = root + "/.audible-dl/temp/" datadir = root + "/.audible-dl/" savedir = root + "/" } else { conf, err := os.UserConfigDir() unwrap(err) cach, err := os.UserCacheDir() unwrap(err) cfgfile = conf + "/audible-dl/config.yml" tempdir = cach + "/audible-dl/temp/" // FIXME: Ideally these would go in XDG_DATA_HOME but // golang doesn't have an os.UserDataDir() so we're // stricking them in with the config file instead. datadir = conf + "/audible-dl/" savedir = "" // Read later from config file } return cfgfile, datadir, tempdir, savedir } // Read command-line arguments. func getArgs() (string, string, string, bool) { var a, h, s string var l bool // FIXME: prevent duplicate flags flag.StringVar(&a, "a", "", "") flag.StringVar(&h, "i", "", "") flag.StringVar(&s, "s", "", "") flag.BoolVar(&l, "l", false, "") flag.StringVar(&a, "account", "", "") flag.StringVar(&h, "import", "", "") flag.StringVar(&s, "single", "", "") flag.BoolVar(&l, "log", false, "") flag.Usage = func() { fmt.Fprintf(os.Stderr, helpMessage) } flag.Parse() return a, h, s, l }