Skip to content

Instantly share code, notes, and snippets.

@brad
Created March 16, 2025 19:04
Show Gist options
  • Save brad/07007d8fec90ba49b0af5ae4846e29f5 to your computer and use it in GitHub Desktop.
Save brad/07007d8fec90ba49b0af5ae4846e29f5 to your computer and use it in GitHub Desktop.
Removes the encryption from overdrive books
import io
import os
import sys
import threading
from kivy.app import App
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.filechooser import FileChooserListView
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.uix.modalview import ModalView
from kivy.uix.popup import Popup
from kivy.uix.spinner import Spinner
from libbydl.DeDRM.dedrm_acsm import dedrm
from libbydl.DeDRM.libadobe import KEY_FOLDER
from libbydl.libbydl import cli, provision_ade_account
BOOKS_FOLDER = './'
FILE_CHOOSER_PATH = '/sdcard/Download'
# To use this script:
# 1. Install the following apps from the Google Play Store:
# - https://play.google.com/store/apps/details?id=ru.iiec.pydroid3
# - https://play.google.com/store/apps/details?id=ru.iiec.pydroid3.quickinstallrepo
# 2. Open Pydroid 3, select Pip from the side menu, and use Quick Install to install lxml
# 3. Now select the Install tab, paste the libbydl zip file URL, and click install:
# https://github.com/brad/libbydl/archive/refs/heads/develop.zip
# 4. Paste the contents of this script into a new file in Pydroid 3 and save the file.
# By default the decrypted books will be saved to this same folder. To change this, edit
# the BOOKS_FOLDER variable.
# 5. Log in to your library on overdrive.com and download *.acsm files by tapping the
# "Download EPUB eBook" button for each book on your bookshelf or
# "Download MP3 audiobook" for *.odm audiobooks (these buttons may be buried).
# 6. Run the script in Pydroid 3. The first run will provision an ADE (Adobe Digital
# Editions) account so it will spin for a few seconds. The account information is
# stored in a `keys` directory, do not delete this directory. Eventually, it will
# present you with a file chooser. Select an *.acsm or *.odm file that you have
# downloaded, and click "Decrypt eBook" (for *.acsm) or "Unpack audiobook" (for *.odm).
# In a few seconds the book file will be saved to the directory specified in
# BOOKS_FOLDER. Note that the files downloaded from overdrive do expire so make sure
# to process it shortly after downloading.
if not hasattr(sys.stdout, 'buffer'):
class CustomStdoutBuffer(io.TextIOBase):
def __init__(self, stream):
self.stream = stream
def write(self, s):
self.stream.write(s)
def flush(self):
self.stream.flush()
sys.stdout.buffer = CustomStdoutBuffer(sys.stdout)
class ProgressSpinner(ModalView):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size_hint = (None, None)
self.size = (100, 100)
self.add_widget(Spinner(text='Loading...', values=('Loading...',)))
def show(self):
self.open()
def hide(self):
self.dismiss()
class MediaFileChooser(BoxLayout):
def __init__(self, spinner, **kwargs):
super().__init__(**kwargs)
self.spinner = spinner
self.orientation = 'vertical'
# Create a FileChooser widget with a filter for .acsm files
self.filechooser = FileChooserListView(
filters=['*.acsm', '*.odm'],
path=FILE_CHOOSER_PATH
)
self.filechooser.bind(selection=self.selected)
# Add FileChooser to layout
self.add_widget(self.filechooser)
# Label to display the selected file path
self.label = Label(text="Selected file: None")
self.add_widget(self.label)
# Button to confirm file selection
self.decrypt_button = Button(text="Decrypt eBook")
self.decrypt_button.bind(on_press=self.decrypt_ebook)
# Button to unpack audiobooks
self.unpack_button = Button(text="Unpack Audiobook")
self.unpack_button.bind(on_press=self.unpack_audiobook)
# Button to exit the app
self.exit_button = Button(text="Close App")
self.exit_button.bind(on_press=self.exit)
# Create layouts to hold all the buttons
button_layout1 = BoxLayout(orientation='vertical')
button_layout2 = BoxLayout(orientation='horizontal')
button_layout2.add_widget(self.decrypt_button)
button_layout2.add_widget(self.unpack_button)
button_layout1.add_widget(button_layout2)
button_layout1.add_widget(self.exit_button)
self.add_widget(button_layout1)
def show_spinner_popup(self):
layout = BoxLayout(orientation='vertical')
spinner = Spinner(text='Loading...')
layout.add_widget(spinner)
self.popup = Popup(title='Please wait', content=layout, size_hint=(0.5, 0.5))
self.popup.open()
def hide_spinner_popup(self):
if hasattr(self, 'popup'):
self.popup.dismiss()
def selected(self, filechooser, selection):
"""This is called when a file is selected"""
if selection:
self.label.text = f"Selected file: {selection[0]}"
else:
self.label.text = "Selected file: None"
def run_odmpy_main(self, file_path):
sys.argv = ['odmpy', 'dl', '-d', BOOKS_FOLDER, file_path]
import odmpy.__main__
odmpy.__main__.main()
self.label.text = "Audiobook unpacked successfully!"
self.hide_spinner_popup()
def unpack_audiobook(self, instance):
"""Unpack the selected audiobook"""
self.show_spinner_popup()
selected_file = self.filechooser.selection
if selected_file:
file_path = selected_file[0]
if file_path.endswith('.odm'):
self.label.text = "Unpacking audiobook..."
# Run the long-running process in a separate thread
threading.Thread(target=self.run_odmpy_main, args=(file_path,)).start()
else:
print("The selected file is not an .odm file.")
self.hide_spinner_popup()
else:
print("No file selected.")
self.hide_spinner_popup()
def run_dedrm(self, file_path):
dedrm(file_path, BOOKS_FOLDER)
self.label.text = "eBook decrypted successfully!"
self.hide_spinner_popup()
def decrypt_ebook(self, instance):
"""Decrypt the selected eBook"""
self.show_spinner_popup()
selected_file = self.filechooser.selection
if selected_file:
file_path = selected_file[0]
if file_path.endswith('.acsm'):
self.label.text = "Decrypting eBook..."
# Run the long-running process in a separate thread
threading.Thread(target=self.run_dedrm, args=(file_path,)).start()
else:
print("The selected file is not an .acsm file.")
self.hide_spinner_popup()
else:
print("No file selected.")
self.hide_spinner_popup()
def exit(self, instance):
"""Exit the app"""
App.get_running_app().stop()
class GetOverdriveMedia(App):
def build(self):
self.spinner = ProgressSpinner()
self.spinner.show()
media_file_chooser = MediaFileChooser(spinner=self.spinner)
Clock.schedule_once(lambda dt: self.spinner.hide(), 0)
return media_file_chooser
if __name__ == '__main__':
if not os.path.exists(KEY_FOLDER):
spinner = ProgressSpinner()
spinner.show()
# Provide empty token as a workaround for the normal flow that requires
# syncing to a libby account first
cli.main(
args=['--token', '""', 'provision-ade-account'],
standalone_mode=False
)
spinner.hide()
os.makedirs(BOOKS_FOLDER, exist_ok=True)
GetOverdriveMedia().run()
@brad
Copy link
Author

brad commented Mar 16, 2025

To use this script:

  1. Install the following apps from the Google Play Store:
  2. Open Pydroid 3, select Pip from the side menu, and use Quick Install to install lxml
  3. Now select the Install tab, paste the libbydl zip file URL, and click install:
    https://github.com/brad/libbydl/archive/refs/heads/develop.zip
  4. Paste the contents of this script into a new file in Pydroid 3 and save the file.
    By default the decrypted books will be saved to this same folder. To change this, edit
    the BOOKS_FOLDER variable.
  5. Log in to your library on overdrive.com and download *.acsm files by tapping the
    "Download EPUB eBook" button for each book on your bookshelf or
    "Download MP3 audiobook" for *.odm audiobooks (these buttons may be buried).
  6. Run the script in Pydroid 3. The first run will provision an ADE (Adobe Digital
    Editions) account so it will spin for a few seconds. The account information is
    stored in a keys directory, do not delete this directory. Eventually, it will
    present you with a file chooser. Select an *.acsm or *.odm file that you have
    downloaded, and click "Decrypt eBook" (for *.acsm) or "Unpack audiobook" (for *.odm).
    In a few seconds the book file will be saved to the directory specified in
    BOOKS_FOLDER. Note that the files downloaded from overdrive do expire so make sure
    to process it shortly after downloading.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment