diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6964772 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +lxml>=4.1.1 +requests>=2.18.4 diff --git a/safaribooks.py b/safaribooks.py new file mode 100644 index 0000000..11b35e2 --- /dev/null +++ b/safaribooks.py @@ -0,0 +1,831 @@ +import os +import sys +import json +import shutil +import logging +import argparse +import requests +from lxml import html +from html import escape +from random import random +from urllib.parse import urljoin, urlsplit +from multiprocessing import Process, Queue, Value + + +PATH = os.path.dirname(os.path.realpath(__file__)) +COOKIES_FILE = os.path.join(PATH, "cookies.json") + + +class Display: + BASE_FORMAT = logging.Formatter( + fmt="[%(asctime)s] %(message)s", + datefmt="%d/%b/%Y %H:%M:%S" + ) + + SH_DEFAULT = "\033[0m" + SH_YELLOW = "\033[33m" + SH_BG_RED = "\033[41m" + SH_BG_YELLOW = "\033[43m" + + def __init__(self): + self.columns, _ = shutil.get_terminal_size() + + self.logger = logging.getLogger("SafariBooks") + self.logger.setLevel(logging.INFO) + logs_handler = logging.FileHandler(filename=os.path.join(PATH, "info.log")) + logs_handler.setFormatter(self.BASE_FORMAT) + logs_handler.setLevel(logging.INFO) + self.logger.addHandler(logs_handler) + + self.logger.info("** Welcome to SafariBooks! **") + + self.book_ad_info = False + self.css_ad_info = Value("i", 0) + self.images_ad_info = Value("i", 0) + self.in_error = False + + self.state_status = Value("i", 0) + sys.excepthook = self.unhandled_exception + + def log(self, message): + self.logger.info(str(message)) + + def out(self, put): + sys.stdout.write("\r" + " " * self.columns + "\r" + put + "\n") + + def info(self, message, state=False): + self.log(message) + output = (self.SH_YELLOW + "[*]" + self.SH_DEFAULT if not state else + self.SH_BG_YELLOW + "[-]" + self.SH_DEFAULT) + " %s" % message + self.out(output) + + def error(self, error): + if not self.in_error: + self.in_error = True + + self.log(error) + output = self.SH_BG_RED + "[#]" + self.SH_DEFAULT + " %s" % error + self.out(output) + + def exit(self, error): + self.error(str(error)) + output = (self.SH_YELLOW + "[+]" + self.SH_DEFAULT + + " Please delete all the `/OEBPS/*.xhtml`" + " files and restart the program.") + self.out(output) + + output = self.SH_BG_RED + "[!]" + self.SH_DEFAULT + " Aborting..." + self.out(output) + sys.exit(128) + + def unhandled_exception(self, _, o, __): + self.exit("Unhandled Exception: %s (type: %s)" % (o, o.__class__.__name__)) + + def intro(self): + output = self.SH_YELLOW + """ + ____ ___ _ + / __/__ _/ _/__ _____(_) + _\ \/ _ `/ _/ _ `/ __/ / + /___/\_,_/_/ \_,_/_/ /_/ + / _ )___ ___ / /__ ___ + / _ / _ \/ _ \/ '_/(_-< + /____/\___/\___/_/\_\/___/ +""" + self.SH_DEFAULT + output += "\n" + "~" * (self.columns // 2) + self.out(output) + + def parse_description(self, desc): + try: + return html.fromstring(desc).text_content() + + except (html.etree.ParseError, html.etree.ParserError) as e: + self.log("Error parsing the description: %s" % e) + return "n/d" + + def book_info(self, info): + description = self.parse_description(info["description"]).replace("\n", " ") + for t in [ + ("Title", info["title"]), ("Authors", ", ".join(aut["name"] for aut in info["authors"])), + ("Identifier", info["identifier"]), ("ISBN", info["isbn"]), + ("Publishers", ", ".join(pub["name"] for pub in info["publishers"])), + ("Rights", info["rights"]), + ("Description", description[:500] + "..." if len(description) >= 500 else description), + ("URL", info["web_url"]) + ]: + self.info("{0}: {1}".format(t[0], t[1]), True) + + def state(self, origin, done): + progress = int(done * 100 / origin) + bar = int(progress * (self.columns - 11) / 100) + if self.state_status.value < progress: + self.state_status.value = progress + sys.stdout.write( + "\r " + self.SH_BG_YELLOW + "[" + ("#" * bar).ljust(self.columns - 11, "-") + "]" + + self.SH_DEFAULT + ("%4s" % progress) + "%" + ("\n" if progress == 100 else "") + ) + + def done(self, epub_file): + self.info("Done: %s\n\n" + " If you like it, please * this project on GitHub to make it known:\n" + " https://github.com/lorenzodifuccia/safaribooks\n" + " e don't forget to renew your Safari Books Online subscription:\n" + " https://www.safaribooksonline.com/signup/\n\n" % epub_file + + self.SH_BG_RED + "[!]" + self.SH_DEFAULT + " Bye!!") + + @staticmethod + def api_error(response): + message = "API: " + if "detail" in response and "Not found" in response["detail"]: + message += "book's not present in Safari Books Online.\n" \ + " The book identifier are the digits that you can find in the URL:\n" \ + " `https://www.safaribooksonline.com/library/view/book-name/XXXXXXXXXXXXX/`" + + else: + os.remove(COOKIES_FILE) + message += "Out-of-Session%s.\n" % (" (%s)" % response["detail"]) if "detail" in response else "" +\ + Display.SH_YELLOW + "[+]" + Display.SH_DEFAULT + \ + " Use the `--cred` option in order to perform the auth login to Safari Books Online." + + return message + + +class SafariBooks: + + HEADERS = { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", + "accept-encoding": "gzip, deflate, br", + "accept-language": "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7", + "cache-control": "no-cache", + "cookie": "", + "pragma": "no-cache", + "referer": "https://www.safaribooksonline.com/home/", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/62.0.3202.94 Safari/537.36" + } + + BASE_URL = "https://www.safaribooksonline.com" + LOGIN_URL = BASE_URL + "/accounts/login/" + API_TEMPLATE = BASE_URL + "/api/v1/book/{0}/" + + BASE_01_HTML = "\n" \ + "\n" \ + "\n" \ + "{0}\n" \ + "" \ + "\n" \ + "{1}\n" + + CONTAINER_XML = "" \ + "" \ + "" \ + "" \ + "" \ + "" + + # Format: ID, Title, Authors, Description, Subjects, Publisher, Rights, CoverId, MANIFEST, SPINE, CoverUrl + CONTENT_OPF = "\n" \ + "\n" \ + "\n"\ + "{1}\n" \ + "{2}\n" \ + "{3}\n" \ + "{4}" \ + "{5}\n" \ + "{6}\n" \ + "en-US\n" \ + "{0}\n" \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + "{8}\n" \ + "\n" \ + "\n{9}\n" \ + "\n" \ + "" + + # Format: ID, Depth, Title, Author, NAVMAP + TOC_NCX = "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "" \ + "{2}" \ + "{3}" \ + "{4}" \ + "" + + def __init__(self, args): + self.args = args + self.display = Display() + self.display.intro() + + self.cookies = {} + + if not args.cred: + if not os.path.isfile(COOKIES_FILE): + self.display.exit("Login: unable to find cookies file.\n" + " Please use the --cred option to perform the login.") + + self.cookies = json.load(open(COOKIES_FILE)) + + else: + self.display.info("Logging into Safari Books Online...", state=True) + self.do_login(*[c.replace("'", "").replace('"', "") for c in args.cred]) + if not args.no_cookies: + json.dump(self.cookies, open(COOKIES_FILE, "w")) + + self.book_id = args.bookid + self.api_url = self.API_TEMPLATE.format(self.book_id) + self.book_info = self.get_book_info() + self.display.book_info(self.book_info) + self.book_chapters = self.get_book_chapters() + self.display.info("Found %s chapters!" % len(self.book_chapters)) + self.chapters_queue = self.book_chapters[:] + + self.book_title = self.book_info["title"] + self.base_url = self.book_info["web_url"] + + self.BOOK_PATH = os.path.join(PATH, self.book_title) + self.create_dirs() + self.display.info("Output directory:\n %s" % self.BOOK_PATH) + + self.chapter_title = "" + self.filename = "" + self.css = [] + self.images = [] + + self.display.info("Downloading book contents...", state=True) + self.BASE_HTML = self.BASE_01_HTML + (self.KINDLE_HTML if not args.no_kindle else "") + self.BASE_02_HTML + self.get() + + self.css_path = "" + self.images_path = "" + self.cover = "" + + self.display.info("Downloading book CSSs...", state=True) + self.collect_css() + self.display.info("Downloading book images...", state=True) + self.collect_images() + + self.display.info("Creating EPUB file...", state=True) + self.create_epub() + + if not args.no_cookies: + json.dump(self.cookies, open(COOKIES_FILE, "w")) + + self.display.done(self.book_title + ".epub") + + if not self.display.in_error and not args.log: + os.remove(os.path.join(PATH, "info.log")) + + sys.exit(0) + + def return_cookies(self): + return " ".join(["{0}={1};".format(k, v) for k, v in self.cookies.items()]) + + def return_headers(self, url): + if "safaribooksonline" in urlsplit(url).netloc: + self.HEADERS["cookie"] = self.return_cookies() + + else: + self.HEADERS["cookie"] = "" + + return self.HEADERS + + def update_cookies(self, jar): + for cookie in jar: + self.cookies.update({ + cookie.name: cookie.value + }) + + def requests_provider(self, url, post=False, data=None, update_cookies=True, **kwargs): + try: + response = getattr(requests, "post" if post else "get")( + url, + headers=self.return_headers(url), + data=data, + **kwargs + ) + + except (requests.ConnectionError, requests.ConnectTimeout, requests.RequestException) as request_exception: + self.display.error(str(request_exception)) + return 0 + + if update_cookies: + self.update_cookies(response.cookies) + + return response + + def do_login(self, email, password): + response = self.requests_provider(self.BASE_URL) + if response == 0: + self.display.exit("Login: unable to reach Safari Books Online. Try again...") + + csrf = [] + try: + csrf = html.fromstring(response.text).xpath("//input[@name='csrfmiddlewaretoken'][@value]") + + except (html.etree.ParseError, html.etree.ParserError) as parsing_error: + self.display.error(parsing_error) + self.display.exit( + "Login: error trying to parse the home of Safari Books Online." + ) + + if not len(csrf): + self.display.exit("Login: no CSRF Token found in the page." + " Unable to continue the login." + " Try again...") + + csrf = csrf[0].attrib["value"] + response = self.requests_provider( + self.LOGIN_URL, + post=True, + data=( + ("csrfmiddlewaretoken", ""), ("csrfmiddlewaretoken", csrf), + ("email", email), ("password1", password), + ("is_login_form", "true"), ("leaveblank", ""), + ("dontchange", "http://") + ), + allow_redirects=False + ) + + if response == 0: + self.display.exit("Login: unable to perform auth to Safari Books Online.\n Try again...") + + if response.status_code != 302: + try: + error_page = html.fromstring(response.text) + errors_message = error_page.xpath("//ul[@class='errorlist']//li/text()") + recaptcha = error_page.xpath("//div[@class='g-recaptcha']") + messages = ([" `%s`" % error for error in errors_message + if "password" in error or "email" in error] if len(errors_message) else []) +\ + ([" `ReCaptcha required (wait or do logout from the website).`"] if len(recaptcha) else[]) + self.display.exit("Login: unable to perform auth login to Safari Books Online.\n" + + self.display.SH_YELLOW + "[*]" + self.display.SH_DEFAULT + " Details:\n" + "%s" % "\n".join(messages if len(messages) else [" Unexpected error!"])) + except (html.etree.ParseError, html.etree.ParserError) as parsing_error: + self.display.error(parsing_error) + self.display.exit( + "Login: your login went wrong and it encountered in an error" + " trying to parse the login details of Safari Books Online. Try again..." + ) + + def get_book_info(self): + response = self.requests_provider(self.api_url) + if response == 0: + self.display.exit("API: unable to retrieve book info.") + + response = response.json() + if not isinstance(response, dict) or len(response.keys()) == 1: + self.display.exit(self.display.api_error(response)) + + if "last_chapter_read" in response: + del response["last_chapter_read"] + + return response + + def get_book_chapters(self, page=0): + response = self.requests_provider(urljoin(self.api_url, "chapter/" + ("" if not page else "?page=%s" % page))) + if response == 0: + self.display.exit("API: unable to retrieve book chapters.") + + response = response.json() + + if not isinstance(response, dict) or len(response.keys()) == 1: + self.display.exit(self.display.api_error(response)) + + if "results" not in response or not len(response["results"]): + self.display.exit("API: unable to retrieve book chapters.") + + result = [] + result.extend([c for c in response["results"] if "cover." in c["filename"]]) + for c in result: + del response["results"][response["results"].index(c)] + + result += response["results"] + return result + (self.get_book_chapters(page + 1) if response["next"] else []) + + def get_html(self, url): + response = self.requests_provider(url) + if response == 0: + self.display.exit( + "Crawler: error trying to retrieve this page: %s (%s)\n From: %s" % + (self.filename, self.chapter_title, url) + ) + + root = None + try: + root = html.fromstring(response.text, base_url=self.BASE_URL) + + except (html.etree.ParseError, html.etree.ParserError) as parsing_error: + self.display.error(parsing_error) + self.display.exit( + "Crawler: error trying to parse this page: %s (%s)\n From: %s" % + (self.filename, self.chapter_title, url) + ) + + return root + + def link_replace(self, link): + if link[0] == "/" and ("cover" in link or "images" in link or "graphics" in link + or link[-3:] in ["jpg", "jpeg", "png"]): + self.images.append(link) + self.display.log("Crawler: found a new image at %s" % link) + image = link.split("/")[-1] + return "Images/" + image + + elif link[0] not in ["/", "h"]: + return link.replace(".html", ".xhtml") + + return link + + def parse_html(self, root): + if random() > 0.5: + if len(root.xpath("//div[@class='controls']/a/text()")): + self.display.exit(self.display.api_error(" ")) + + book_content = root.xpath("//div[@id='sbo-rt-content']") + if not len(book_content): + self.display.exit( + "Parser: book content's corrupted or not present: %s (%s)" % + (self.filename, self.chapter_title) + ) + + page_css = "" + stylesheet_links = root.xpath("//link[@rel='stylesheet']") + if len(stylesheet_links): + stylesheet_count = 0 + for s in stylesheet_links: + css_url = urljoin("https:", s.attrib["href"]) if s.attrib["href"][:2] == "//" \ + else urljoin(self.base_url, s.attrib["href"]) + + if css_url not in self.css: + self.css.append(css_url) + self.display.log("Crawler: found a new CSS at %s" % css_url) + + stylesheet_count += 1 + page_css += "2}.css\" " \ + "rel=\"stylesheet\" type=\"text/css\" />\n".format(stylesheet_count) + + stylesheets = root.xpath("//style") + if len(stylesheets): + for css in stylesheets: + if "data-template" in css.attrib and len(css.attrib["data-template"]): + css.text = css.attrib["data-template"] + del css.attrib["data-template"] + + try: + page_css += html.tostring(css, method="xml", encoding='unicode') + "\n" + + except (html.etree.ParseError, html.etree.ParserError) as parsing_error: + self.display.error(parsing_error) + self.display.exit( + "Parser: error trying to parse one CSS found in this page: %s (%s)" % + (self.filename, self.chapter_title) + ) + + book_content = book_content[0] + book_content.rewrite_links(self.link_replace) + + xhtml = None + try: + xhtml = html.tostring(book_content, method="xml", encoding='unicode') + + except (html.etree.ParseError, html.etree.ParserError) as parsing_error: + self.display.error(parsing_error) + self.display.exit( + "Parser: error trying to parse HTML of this page: %s (%s)" % + (self.filename, self.chapter_title) + ) + + return page_css, xhtml + + def create_dirs(self): + if os.path.isdir(self.BOOK_PATH): + self.display.log("Book directory already exists: %s" % self.book_title) + + else: + os.makedirs(self.BOOK_PATH) + + oebps = os.path.join(self.BOOK_PATH, "OEBPS") + if not os.path.isdir(oebps): + self.display.book_ad_info = True + os.makedirs(oebps) + + def save_page_html(self, contents): + self.filename = self.filename.replace(".html", ".xhtml") + open(os.path.join(self.BOOK_PATH, "OEBPS", self.filename), "w")\ + .write(self.BASE_HTML.format(contents[0], contents[1])) + self.display.log("Created: %s" % self.filename) + + def get(self): + if not len(self.chapters_queue): + return + + next_chapter = self.chapters_queue.pop(0) + self.chapter_title = next_chapter["title"] + self.filename = next_chapter["filename"] + + if os.path.isfile(os.path.join(self.BOOK_PATH, "OEBPS", self.filename.replace(".html", ".xhtml"))): + if not self.display.book_ad_info and \ + next_chapter not in self.book_chapters[:self.book_chapters.index(next_chapter)]: + self.display.info("File `%s` already exists.\n" + " If you want to download again all the book%s,\n" + " please delete the `/OEBPS/*.xhtml` files and restart the program." % + (self.filename.replace(".html", ".xhtml"), + " (especially because you selected the `--no-kindle` option)" if self.args.no_kindle + else "")) + self.display.book_ad_info = 1 + + else: + self.save_page_html(self.parse_html(self.get_html(urljoin(self.base_url, self.filename)))) + + self.display.state(len(self.book_chapters), len(self.book_chapters) - len(self.chapters_queue)) + self.get() + + def _thread_download_css(self, url, done_queue): + css_file = os.path.join(self.css_path, "Style{0:0>2}.css".format(self.css.index(url))) + if os.path.isfile(css_file): + if not self.display.css_ad_info.value and url not in self.css[:self.css.index(url)]: + self.display.info("File `%s` already exists.\n" + " If you want to download again all the CSSs,\n" + " please delete the `/OEBPS/*.xhtml` and `/OEBPS/Styles/*`" + " files and restart the program." % + css_file) + self.display.css_ad_info.value = 1 + + else: + response = self.requests_provider(url, update_cookies=False) + if response == 0: + self.display.error("Error trying to retrieve this CSS: %s\n From: %s" % (css_file, url)) + + with open(css_file, 'wb') as s: + for chunk in response.iter_content(1024): + s.write(chunk) + + done_queue.put(1) + self.display.state(len(self.css), done_queue.qsize()) + + def _thread_download_images(self, url, done_queue): + image_name = url.split("/")[-1] + image_path = os.path.join(self.images_path, image_name) + if os.path.isfile(image_path): + if not self.display.images_ad_info.value and url not in self.images[:self.images.index(url)]: + self.display.info("File `%s` already exists.\n" + " If you want to download again all the images,\n" + " please delete the `/OEBPS/*.xhtml` and `/OEBPS/Images/*`" + " files and restart the program." % + image_name) + self.display.images_ad_info.value = 1 + + else: + response = self.requests_provider(urljoin(self.BASE_URL, url), + update_cookies=False, + stream=True) + if response == 0: + self.display.error("Error trying to retrieve this image: %s\n From: %s" % (image_name, url)) + + with open(image_path, 'wb') as img: + for chunk in response.iter_content(1024): + img.write(chunk) + + done_queue.put(1) + self.display.state(len(self.images), done_queue.qsize()) + + def _start_multiprocessing(self, operation, full_queue, done_queue=None): + if not done_queue: + done_queue = Queue(0) + + if len(full_queue) > 5: + for i in range(0, len(full_queue), 5): + self._start_multiprocessing(operation, full_queue[i:i+5], done_queue) + + else: + process_queue = [Process(target=operation, args=(arg, done_queue)) for arg in full_queue] + for proc in process_queue: + proc.start() + + for proc in process_queue: + proc.join() + + def collect_css(self): + self.css_path = os.path.join(self.BOOK_PATH, "OEBPS", "Styles") + if os.path.isdir(self.css_path): + self.display.log("CSSs directory already exists: %s" % self.css_path) + + else: + os.makedirs(self.css_path) + self.display.css_ad_info.value = 1 + + self.display.state_status.value = -1 + self._start_multiprocessing(self._thread_download_css, self.css) + + def collect_images(self): + self.images_path = os.path.join(self.BOOK_PATH, "OEBPS", "Images") + if os.path.isdir(self.images_path): + self.display.log("Images directory already exists: %s" % self.images_path) + + else: + os.makedirs(self.images_path) + self.display.images_ad_info.value = 1 + + if self.display.book_ad_info == 1: + self.display.info("Some of the book contents were already downloaded.\n" + " If you want to be sure that all the images will be downloaded,\n" + " please delete the `/OEBPS/*.xhtml` files and restart the program.") + + self.display.state_status.value = -1 + self._start_multiprocessing(self._thread_download_images, self.images) + + def create_content_opf(self): + self.cover = self.images[0] if len(self.images) else "" + self.css = next(os.walk(self.css_path))[2] + self.images = next(os.walk(self.images_path))[2] + + manifest = [] + spine = [] + for c in self.book_chapters: + c["filename"] = c["filename"].replace(".html", ".xhtml") + item_id = escape("".join(c["filename"].split(".")[:-1])) + manifest.append("".format( + item_id, c["filename"] + )) + spine.append("".format(item_id)) + + alt_cover_id = False + for i in self.images: + dot_split = i.split(".") + head = "img_" + escape("".join(dot_split[:-1])) + extension = dot_split[-1] + manifest.append("".format( + head, i, "jpeg" if "jp" in extension else extension + )) + + if not alt_cover_id: + alt_cover_id = head + + for i in range(1, len(self.css) + 1): + manifest.append("2}\" href=\"Styles/Style{0:0>2}.css\" " + "media-type=\"text/css\" />".format(i)) + + authors = "\n".join("{0}".format( + escape(aut["name"]) + ) for aut in self.book_info["authors"]) + + subjects = "\n".join("{0}".format(escape(sub["name"])) + for sub in self.book_info["subjects"]) + + return self.CONTENT_OPF.format( + (self.book_info["isbn"] if len(self.book_info["isbn"]) else self.book_id), + escape(self.book_title), + authors, + escape(self.book_info["description"]), + subjects, + ", ".join(escape(pub["name"]) for pub in self.book_info["publishers"]), + escape(self.book_info["rights"]), + self.cover if self.cover else alt_cover_id, + "\n".join(manifest), + "\n".join(spine), + self.book_chapters[0]["filename"].replace(".html", ".xhtml") + ) + + @staticmethod + def parse_toc(l, c=0, mx=0): + r = "" + for cc in l: + c += 1 + if int(cc["depth"]) > mx: + mx = int(cc["depth"]) + + r += "" \ + "{2}" \ + "".format( + cc["fragment"] if len(cc["fragment"]) else cc["id"], c, + escape(cc["label"]), cc["href"].replace(".html", ".xhtml") + ) + + if cc["children"]: + sr, c, mx = SafariBooks.parse_toc(cc["children"], c, mx) + r += sr + + r += "\n" + + return r, c, mx + + def create_toc(self): + response = self.requests_provider(urljoin(self.api_url, "toc/")) + if response == 0: + self.display.exit("API: unable to retrieve book chapters. " + "Don't delete any files, just run again this program" + " in order to complete the `.epub` creation!") + + response = response.json() + + if not isinstance(response, list) and len(response.keys()) == 1: + self.display.exit( + self.display.api_error(response) + + " Don't delete any files, just run again this program" + " in order to complete the `.epub` creation!" + ) + + navmap, _, max_depth = self.parse_toc(response) + return self.TOC_NCX.format( + (self.book_info["isbn"] if len(self.book_info["isbn"]) else self.book_id), + max_depth, + self.book_title, + ", ".join(aut["name"] for aut in self.book_info["authors"]), + navmap + ) + + def create_epub(self): + open(os.path.join(self.BOOK_PATH, "mimetype"), "w").write("application/epub+zip") + meta_info = os.path.join(self.BOOK_PATH, "META-INF") + if os.path.isdir(meta_info): + self.display.log("META-INF directory already exists: %s" % meta_info) + + else: + os.makedirs(meta_info) + + open(os.path.join(meta_info, "container.xml"), "w").write(self.CONTAINER_XML) + open(os.path.join(self.BOOK_PATH, "OEBPS", "content.opf"), "w").write(self.create_content_opf()) + open(os.path.join(self.BOOK_PATH, "OEBPS", "toc.ncx"), "w").write(self.create_toc()) + + zip_file = os.path.join(self.BOOK_PATH, self.book_title) + if os.path.isfile(zip_file + ".epub"): + os.remove(zip_file + ".epub") + if os.path.isfile(zip_file + ".zip"): + os.remove(zip_file + ".zip") + shutil.make_archive(zip_file, 'zip', self.BOOK_PATH) + os.rename(zip_file + ".zip", zip_file + ".epub") + + +# MAIN +arguments = argparse.ArgumentParser(prog="safaribooks.py", + description="Download and generate an EPUB of your favorite books" + " from Safari Books Online.", + add_help=False, + allow_abbrev=False) + +arguments.add_argument( + "--cred", metavar="", default=False, + help="Credentials used to perform the auth login on Safari Books Online." + " Es. ` --cred \"account_mail@mail.com:password01\" `." +) +arguments.add_argument( + "--no-cookies", dest="no_cookies", action='store_true', + help="Prevent your session data to be saved into `cookies.json` file." +) +arguments.add_argument( + "--no-kindle", dest="no_kindle", action='store_true', + help="Remove some CSS rules that block overflow on `table` and `pre` elements." + " Use this option if you're not going to export the EPUB to E-Readers like Amazon Kindle." +) +arguments.add_argument( + "--preserve-log", dest="log", action='store_true', help="Leave the `info.log` file even if there isn't any error." +) +arguments.add_argument("--help", action="help", default=argparse.SUPPRESS, help='Show this help message.') + +arguments.add_argument( + "bookid", metavar='', + help="Book digits ID that you want to download. You can find it in the URL (X-es):" + " `https://www.safaribooksonline.com/library/view/book-name/XXXXXXXXXXXXX/`" +) + +args_parsed = arguments.parse_args() + +if args_parsed.cred: + cred = args_parsed.cred.split(":") + if len(cred) != 2 or "@" not in cred[0]: + arguments.error("invalid credential: %s" % args_parsed.cred) + + args_parsed.cred = cred + +else: + if args_parsed.no_cookies: + arguments.error("invalid option: `--no-cookies` is valid only if you use the `--cred` option") + +if not args_parsed.bookid.isdigit(): + arguments.error("invalid book id: %s" % args_parsed.bookid) + +SafariBooks(args_parsed)