| New file |
| | |
| | | #!/usr/local/bin/python3 |
| | | import httpx |
| | | import aaxConvert |
| | | import argparse |
| | | import audible |
| | | import json |
| | | import time |
| | | from audible.aescipher import decrypt_voucher_from_licenserequest |
| | | from audible.activation_bytes import ( |
| | | extract_activation_bytes, |
| | | fetch_activation_sign_auth |
| | | ) |
| | | |
| | | _audible_auth = audible.Authenticator.from_file('./audible_auth_Jeremy') |
| | | ab = fetch_activation_sign_auth(auth=_audible_auth) |
| | | ab = extract_activation_bytes(ab) |
| | | _actbytes = ab |
| | | _bearer_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjOWZlOGUyOS0xZjM3LTQ1OWEtOWJiOS02NDg3ZjNjNDNiNGUiLCJ1c2VybmFtZSI6InJvb3QiLCJpYXQiOjE3NTAwNDA1MDl9.kDueJYRlXna-IRZu5jDjTUjwGYp0y01P9TdNw4d5PeE' # noqa E501 |
| | | _header = { |
| | | "Authorization": f"Bearer {_bearer_token}" |
| | | } |
| | | _metadata = {} |
| | | |
| | | |
| | | def get_args(): |
| | | ''' |
| | | Parses the args for runtime |
| | | |
| | | Returns: |
| | | args class type |
| | | ''' |
| | | parser = argparse.ArgumentParser(exit_on_error=False) |
| | | parser.add_argument("-a", "--asin", |
| | | help="ASIN of the book to download and convert", |
| | | default=None, required=False) |
| | | return parser.parse_args() |
| | | |
| | | |
| | | def get_library_ids() -> list: |
| | | ''' |
| | | Returns a list of library id's |
| | | |
| | | Args: |
| | | url (str): url to query |
| | | |
| | | Returns: |
| | | list: A list of libraries avaliable to query. |
| | | ''' |
| | | libs = [] |
| | | with httpx.Client(base_url="https://bookshelf.darkurthe.net/api/", |
| | | headers=_header) as c: |
| | | libraries = c.get('libraries').json() |
| | | for library in libraries['libraries']: |
| | | libs.append(library["id"]) |
| | | c.close() |
| | | return libs |
| | | |
| | | |
| | | def get_bookshelf_books(library_id, limit=0): |
| | | with httpx.Client(base_url="https://bookshelf.darkurthe.net/api/", |
| | | headers=_header) as c: |
| | | items = c.get(f'libraries/{library_id}/items?limit={limit}', |
| | | timeout=30.0).json() |
| | | return items['results'] |
| | | |
| | | |
| | | def search_local_books(a_book, l_books) -> list: |
| | | search_title = [book for book in l_books if a_book['title'] in |
| | | book['media']['metadata']['title']] |
| | | search_asin = [ |
| | | book for book in l_books if a_book['asin'] == book['media']['metadata']['asin'] |
| | | ] |
| | | if search_title or search_asin: |
| | | return False |
| | | else: |
| | | return True |
| | | |
| | | |
| | | def get_audible_books() -> list: |
| | | ''' |
| | | Downloads a list of audiobooks from audible. |
| | | |
| | | Returns: |
| | | list: A list of books and details in dictionary format |
| | | ''' |
| | | with audible.Client(auth=_audible_auth) as client: |
| | | library = client.get( |
| | | "1.0/library/", |
| | | num_results=1000, |
| | | response_groups="product_desc, product_attrs", |
| | | sort_by="-PurchaseDate" |
| | | ) |
| | | books = library['items'] |
| | | return books |
| | | |
| | | |
| | | def get_license_request(asin: str) -> str: |
| | | ''' |
| | | Gets the url for the book to download |
| | | |
| | | Args: |
| | | asin (str): The audiobook asin("Amazon Standard Identification Number") |
| | | |
| | | Returns: |
| | | dict: The dict of the license request |
| | | ''' |
| | | body = { |
| | | "supported_drm_types": [ |
| | | "Mpeg", |
| | | "Adrm" |
| | | ], |
| | | "quality": "High", |
| | | "consumption_type": "Download" |
| | | } |
| | | with audible.Client(auth=_audible_auth) as client: |
| | | content_url = client.post( |
| | | f"1.0/content/{asin}/licenserequest", |
| | | body=body |
| | | ) |
| | | return content_url |
| | | |
| | | |
| | | def get_content_url(licenserequest: dict) -> str: |
| | | ''' |
| | | Gets the url out of the license request. |
| | | |
| | | Arg: |
| | | license request (dict): |
| | | |
| | | Ret: |
| | | str: The url to download the file from |
| | | ''' |
| | | return licenserequest['content_license']['content_metadata'][ |
| | | 'content_url']['offline_url'] |
| | | |
| | | |
| | | def get_content(asin: str, content_url: str) -> None: |
| | | '''Downloads the content from audible |
| | | |
| | | Args: |
| | | asin (str): The audiobook asin("Amazon Standard Identification Number") |
| | | content_url (str): The url to download the book from |
| | | ''' |
| | | headers = {"User-Agent": "Audible/671 CFNetwork/1240.0.4 Darwin/20.6.0"} |
| | | dl = httpx.get(content_url, timeout=30, headers=headers) |
| | | with open(f"/tmp/{asin}.aax", 'wb') as f: |
| | | f.write(dl.content) |
| | | |
| | | |
| | | def download_books(asin: str) -> None: |
| | | ''' |
| | | Download the book and hand it off to the converter |
| | | |
| | | Args: |
| | | title (str): The title of the book to be downloaded |
| | | asin (str): The amazon number of the book being downloaded |
| | | ''' |
| | | lr = get_license_request(asin) |
| | | print(ab) |
| | | get_content(asin, get_content_url(lr)) |
| | | decrypted_voucher = decrypt_voucher_from_licenserequest(_audible_auth, lr) |
| | | key_iv = { |
| | | 'key': decrypted_voucher['key'], |
| | | 'iv': decrypted_voucher['iv'] |
| | | } |
| | | print(f"{asin} ready for conversion.") |
| | | aaxConvert.convert_aax(f"/tmp/{asin}.aax", _actbytes, key_iv) |
| | | |
| | | |
| | | def main(): |
| | | args = get_args() |
| | | asin_manual = args.asin |
| | | audible_books = get_audible_books() |
| | | if asin_manual is not None: |
| | | download_books(asin_manual) |
| | | else: |
| | | library_ids = get_library_ids() |
| | | for id in library_ids: |
| | | bookshelf_books = get_bookshelf_books(id) |
| | | for book in audible_books: |
| | | if book['content_type'] == 'Product': |
| | | search = search_local_books(book, bookshelf_books) |
| | | if search: |
| | | print(f"{book['title']}, {book['asin']}") |
| | | download_books(book['asin']) |
| | | time.sleep(20) |
| | | |
| | | |
| | | if __name__ == "__main__": |
| | | main() |