Created
March 16, 2025 19:04
-
-
Save brad/07007d8fec90ba49b0af5ae4846e29f5 to your computer and use it in GitHub Desktop.
Removes the encryption from overdrive books
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To use this script:
https://github.com/brad/libbydl/archive/refs/heads/develop.zip
By default the decrypted books will be saved to this same folder. To change this, edit
the BOOKS_FOLDER variable.
"Download EPUB eBook" button for each book on your bookshelf or
"Download MP3 audiobook" for *.odm audiobooks (these buttons may be buried).
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 willpresent 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.