From 2bca657babf8bec3f9cee673c92eb21f68a68a68 Mon Sep 17 00:00:00 2001
From: Chris Pomeroy <chris.pomeroy@hotmail.com>
Date: Fri, 05 Dec 2025 04:59:59 +0000
Subject: [PATCH] Updated for all the things

---
 Dockerfile               |   24 ++++
 fix-asin.py              |   78 +++++++++++++++
 getMissingFromAudible.py |  190 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 292 insertions(+), 0 deletions(-)

diff --git a/Dockerfile b/Dockerfile
new file mode 100755
index 0000000..b1d98a4
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+FROM python:3.11-slim
+
+
+RUN export DEBIAN_FRONTEND=noninteractive \
+    && apt-get update \
+    && apt-get install -y \
+        # lsb-release \
+        ffmpeg \
+        vim-common \
+        curl \
+    && apt-get install -y libssl-dev libffi-dev \
+    && useradd -U -s /bin/bash -u 506 -m default \
+    && apt-get autoremove -y \
+    && apt-get clean -y \
+    && mkdir /audiobooks/ \
+    && chown default /audiobooks \
+    && rm -rf /var/lib/apt/lists/*
+
+COPY ./aaxConvert.py /home/default/
+COPY ./getMissingFromAudible.py /home/default/
+COPY ./audible_auth* /home/default/
+
+RUN pip install httpx audible 
+USER default
diff --git a/fix-asin.py b/fix-asin.py
new file mode 100644
index 0000000..5b4f126
--- /dev/null
+++ b/fix-asin.py
@@ -0,0 +1,78 @@
+import httpx
+import audible
+import json
+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_')
+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_local_items(limit, library_id):
+    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=20.0).json()
+    return items['results']
+
+
+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 find_asin_for_book(title, a_books):
+    search = [book for book in a_books if book['title'] in title]
+    if search:
+        return search[0]['asin']
+    else:
+        print(f"Title not found {title}")
+
+
+def update_bookshelf(book_id, book_title, book_asin):
+    print(f"Will update {book_title}/{book_id} with asin: {book_asin}")
+    data = {"metadata": {"asin": book_asin}}
+    with httpx.Client(base_url="https://bookshelf.darkurthe.net/api/",
+                      headers=_header) as c:
+        update = c.patch(f'items/{book_id}/media', json=data)
+        if update.status_code == 200:
+            print(f"Updated {book_title}")
+        else:
+            print(f"There was a problem updating {book_title}:  Status code: {update.status_code}")
+    return
+
+
+audible_books = get_audible_books()
+books = get_local_items(0, '0507d3fe-db57-4e6f-bb18-a0c09cd6f196')
+
+
+for x in books:
+    if x['media']['metadata']['asin'] is None:
+        # print({x['media']['metadata']['title']})
+        asin = find_asin_for_book(x['media']['metadata']['title'], audible_books)
+        x_title = x['media']['metadata']['title']
+        x_id = x['id']
+        if asin:
+            update_bookshelf(x_id, x_title, asin)
diff --git a/getMissingFromAudible.py b/getMissingFromAudible.py
new file mode 100755
index 0000000..a82b381
--- /dev/null
+++ b/getMissingFromAudible.py
@@ -0,0 +1,190 @@
+#!/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()

--
Gitblit v1.10.0